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

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,7 +1,7 @@
1
1
  import { m as __toESM } from "./esm-CYu4tXXn.mjs";
2
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
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";
4
+ import { $ as updateAdapterConfigSchema, 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 taskListQuerySchema, R as loginSchema, S as agentRuntimeConfigPayloadSchema$1, T as connectTokenExchangeSchema, U as runtimeStateMessageSchema, V as paginationQuerySchema, W as selfServiceFeishuBotSchema, X as sessionReconcileRequestSchema, Y as sessionEventSchema$1, Z as sessionStateMessageSchema, _ as addParticipantSchema, a as AGENT_SELECTOR_HEADER$1, at as updateSystemConfigSchema, b as agentBindRequestSchema, c as AGENT_TYPES, d as SYSTEM_CONFIG_DEFAULTS, et as updateAgentRuntimeConfigSchema, 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 updateOrganizationSchema, j as createOrganizationSchema, k as createChatSchema, l as AGENT_VISIBILITY, m as TASK_STATUSES, nt as updateChatSchema, o as AGENT_SOURCES, ot as updateTaskStatusSchema, p as TASK_HEALTH_SIGNALS, q as sessionCompletionMessageSchema, rt as updateMemberSchema, s as AGENT_STATUSES, st as wsAuthFrameSchema, tt as updateAgentSchema, 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-n9Y2yGTT.mjs";
5
5
  import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, watch, writeFileSync } from "node:fs";
6
6
  import { dirname, isAbsolute, join, resolve } from "node:path";
7
7
  import { ZodError, z } from "zod";
@@ -230,14 +230,16 @@ const runtimeStateSchema = z.enum([
230
230
  "blocked",
231
231
  "error"
232
232
  ]);
233
- const sessionStateSchema = z.enum([
233
+ z.enum([
234
234
  "active",
235
235
  "suspended",
236
236
  "evicted"
237
237
  ]);
238
+ /** Wire-level states a client may report. `evicted` from a stale client is rejected. */
239
+ const clientSessionStateSchema = z.enum(["active", "suspended"]);
238
240
  z.object({
239
241
  chatId: z.string().min(1),
240
- state: sessionStateSchema
242
+ state: clientSessionStateSchema
241
243
  });
242
244
  z.object({ runtimeState: runtimeStateSchema });
243
245
  z.object({
@@ -526,6 +528,7 @@ z.object({
526
528
  createdAt: z.string(),
527
529
  updatedAt: z.string()
528
530
  }).extend({ participants: z.array(chatParticipantSchema) });
531
+ z.object({ topic: z.string().trim().max(500).nullable() });
529
532
  z.object({
530
533
  agentId: z.string().min(1),
531
534
  mode: z.enum(["full", "mention_only"]).default("full")
@@ -641,7 +644,10 @@ z.object({
641
644
  displayName: z.string().min(1).max(200),
642
645
  role: memberRoleSchema.default("member")
643
646
  });
644
- z.object({ role: memberRoleSchema.optional() });
647
+ z.object({
648
+ role: memberRoleSchema.optional(),
649
+ displayName: z.string().min(1).max(200).optional()
650
+ });
645
651
  memberSchema.extend({
646
652
  username: z.string(),
647
653
  displayName: z.string(),
@@ -714,7 +720,13 @@ z.object({
714
720
  organizationId: z.string(),
715
721
  agents: z.record(z.string(), z.array(pulseBucketSchema).length(32))
716
722
  });
717
- const sessionEventKind = z.enum(["tool_call", "error"]);
723
+ const sessionEventKind = z.enum([
724
+ "tool_call",
725
+ "error",
726
+ "assistant_text",
727
+ "thinking",
728
+ "turn_end"
729
+ ]);
718
730
  const toolCallEventPayload = z.object({
719
731
  toolUseId: z.string(),
720
732
  name: z.string(),
@@ -735,20 +747,61 @@ const errorEventPayload = z.object({
735
747
  ]),
736
748
  message: z.string().max(2e3)
737
749
  });
738
- const sessionEventSchema = z.discriminatedUnion("kind", [z.object({
739
- kind: z.literal("tool_call"),
740
- payload: toolCallEventPayload
741
- }), z.object({
742
- kind: z.literal("error"),
743
- payload: errorEventPayload
744
- })]);
750
+ /**
751
+ * A text block emitted by the model within an assistant message. These are
752
+ * transient "in-progress" events used to render the assistant's reply body
753
+ * while a turn is still running. The final turn result is forwarded as a
754
+ * regular chat message (not an event); the frontend hides all assistant_text
755
+ * events for turns that have completed (i.e. once `turn_end` has been emitted).
756
+ */
757
+ const assistantTextEventPayload = z.object({ text: z.string().max(8e3) });
758
+ /**
759
+ * Marker emitted when the model produces a `thinking` content block.
760
+ * We intentionally do NOT persist the thinking content — only a presence
761
+ * signal so the UI can render a lightweight "Thinking…" status indicator.
762
+ */
763
+ const thinkingEventPayload = z.object({});
764
+ /**
765
+ * Turn boundary marker. Emitted once per completed query turn, regardless of
766
+ * success/failure, so the frontend can group events into turns and collapse
767
+ * completed turns to show only the final result message.
768
+ */
769
+ const turnEndEventPayload = z.object({ status: z.enum(["success", "error"]) });
770
+ const sessionEventSchema = z.discriminatedUnion("kind", [
771
+ z.object({
772
+ kind: z.literal("tool_call"),
773
+ payload: toolCallEventPayload
774
+ }),
775
+ z.object({
776
+ kind: z.literal("error"),
777
+ payload: errorEventPayload
778
+ }),
779
+ z.object({
780
+ kind: z.literal("assistant_text"),
781
+ payload: assistantTextEventPayload
782
+ }),
783
+ z.object({
784
+ kind: z.literal("thinking"),
785
+ payload: thinkingEventPayload
786
+ }),
787
+ z.object({
788
+ kind: z.literal("turn_end"),
789
+ payload: turnEndEventPayload
790
+ })
791
+ ]);
745
792
  z.object({
746
793
  id: z.string(),
747
794
  agentId: z.string(),
748
795
  chatId: z.string(),
749
796
  seq: z.number().int().positive(),
750
797
  kind: sessionEventKind,
751
- payload: z.union([toolCallEventPayload, errorEventPayload]),
798
+ payload: z.union([
799
+ toolCallEventPayload,
800
+ errorEventPayload,
801
+ assistantTextEventPayload,
802
+ thinkingEventPayload,
803
+ turnEndEventPayload
804
+ ]),
752
805
  createdAt: z.string()
753
806
  });
754
807
  z.object({
@@ -760,6 +813,16 @@ z.object({
760
813
  agentId: z.string(),
761
814
  chatId: z.string()
762
815
  });
816
+ z.object({
817
+ type: z.literal("session:reconcile"),
818
+ agentId: z.string().min(1),
819
+ chatIds: z.array(z.string().min(1)).max(500)
820
+ });
821
+ z.object({
822
+ type: z.literal("session:reconcile:result"),
823
+ agentId: z.string().min(1),
824
+ staleChatIds: z.array(z.string().min(1))
825
+ });
763
826
  const orgStatsSchema = z.object({
764
827
  organizationId: z.string(),
765
828
  agentCount: z.number(),
@@ -1148,6 +1211,15 @@ var ClientConnection = class extends EventEmitter {
1148
1211
  chatId
1149
1212
  }));
1150
1213
  }
1214
+ /** Ask the server which of the supplied chatIds the client should drop. */
1215
+ sendSessionReconcile(agentId, chatIds) {
1216
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;
1217
+ this.ws.send(JSON.stringify({
1218
+ type: "session:reconcile",
1219
+ agentId,
1220
+ chatIds
1221
+ }));
1222
+ }
1151
1223
  async disconnect() {
1152
1224
  this.closing = true;
1153
1225
  this.clearTimers();
@@ -1325,7 +1397,7 @@ var ClientConnection = class extends EventEmitter {
1325
1397
  }
1326
1398
  return;
1327
1399
  }
1328
- if (type === "session:suspend" || type === "session:resume" || type === "session:terminate") {
1400
+ if (type === "session:suspend" || type === "session:terminate") {
1329
1401
  const agentId = msg.agentId;
1330
1402
  const chatId = msg.chatId;
1331
1403
  if (agentId && chatId) this.emit("session:command", {
@@ -1335,6 +1407,15 @@ var ClientConnection = class extends EventEmitter {
1335
1407
  });
1336
1408
  return;
1337
1409
  }
1410
+ if (type === "session:reconcile:result") {
1411
+ const agentId = msg.agentId;
1412
+ const staleChatIds = Array.isArray(msg.staleChatIds) ? msg.staleChatIds : null;
1413
+ if (agentId && staleChatIds) this.emit("session:reconcile:result", {
1414
+ agentId,
1415
+ staleChatIds
1416
+ });
1417
+ return;
1418
+ }
1338
1419
  if (type === "new_message") {
1339
1420
  const inboxId = msg.inboxId;
1340
1421
  if (inboxId) this.emit("agent:message", inboxId, msg);
@@ -2272,6 +2353,7 @@ function cleanWorkspaces(workspaceRoot, activeChatIds, ttlMs = DEFAULT_WORKSPACE
2272
2353
  }
2273
2354
  const MAX_RETRIES = 2;
2274
2355
  const TOOL_RESULT_PREVIEW_LIMIT = 400;
2356
+ const ASSISTANT_TEXT_EVENT_LIMIT = 8e3;
2275
2357
  function extractContentBlocks(message) {
2276
2358
  if (!message || typeof message !== "object") return [];
2277
2359
  const inner = message.message;
@@ -2289,6 +2371,15 @@ function isToolResultBlock(block) {
2289
2371
  const b = block;
2290
2372
  return b.type === "tool_result" && typeof b.tool_use_id === "string";
2291
2373
  }
2374
+ function isTextBlock(block) {
2375
+ if (!block || typeof block !== "object") return false;
2376
+ const b = block;
2377
+ return b.type === "text" && typeof b.text === "string";
2378
+ }
2379
+ function isThinkingBlock(block) {
2380
+ if (!block || typeof block !== "object") return false;
2381
+ return block.type === "thinking";
2382
+ }
2292
2383
  function isResultMessage(message) {
2293
2384
  if (!message || typeof message !== "object") return false;
2294
2385
  const m = message;
@@ -2331,31 +2422,39 @@ function createToolCallProcessor(emit) {
2331
2422
  onMessage(message) {
2332
2423
  if (!message || typeof message !== "object") return;
2333
2424
  const type = message.type;
2334
- if (type === "assistant") for (const block of extractContentBlocks(message)) {
2335
- if (!isToolUseBlock(block)) continue;
2336
- pending.set(block.id, {
2337
- toolUseId: block.id,
2338
- name: block.name,
2339
- args: block.input,
2340
- startedAt: Date.now()
2425
+ if (type === "assistant") {
2426
+ for (const block of extractContentBlocks(message)) if (isToolUseBlock(block)) {
2427
+ pending.set(block.id, {
2428
+ toolUseId: block.id,
2429
+ name: block.name,
2430
+ args: block.input,
2431
+ startedAt: Date.now()
2432
+ });
2433
+ emit({
2434
+ kind: "tool_call",
2435
+ payload: {
2436
+ toolUseId: block.id,
2437
+ name: block.name,
2438
+ args: block.input,
2439
+ status: "pending"
2440
+ }
2441
+ });
2442
+ } else if (isTextBlock(block)) {
2443
+ const text = block.text.trim();
2444
+ if (text.length === 0) continue;
2445
+ emit({
2446
+ kind: "assistant_text",
2447
+ payload: { text: text.slice(0, ASSISTANT_TEXT_EVENT_LIMIT) }
2448
+ });
2449
+ } else if (isThinkingBlock(block)) emit({
2450
+ kind: "thinking",
2451
+ payload: {}
2341
2452
  });
2342
- }
2343
- else if (type === "user") {
2453
+ } else if (type === "user") {
2344
2454
  for (const block of extractContentBlocks(message)) if (isToolResultBlock(block)) pairResult(block);
2345
2455
  }
2346
2456
  },
2347
2457
  flush() {
2348
- if (pending.size === 0) return;
2349
- for (const entry of pending.values()) emit({
2350
- kind: "tool_call",
2351
- payload: {
2352
- toolUseId: entry.toolUseId,
2353
- name: entry.name,
2354
- args: entry.args,
2355
- status: "pending",
2356
- durationMs: Date.now() - entry.startedAt
2357
- }
2358
- });
2359
2458
  pending.clear();
2360
2459
  }
2361
2460
  };
@@ -2561,25 +2660,37 @@ const createClaudeCodeHandler = (config) => {
2561
2660
  retryCount = 0;
2562
2661
  if (message.result && sessionCtx.chatId) {
2563
2662
  const resultText = message.result;
2564
- sessionCtx.sdk.sendMessage(sessionCtx.chatId, {
2565
- format: "text",
2566
- content: resultText
2567
- }).then(() => {
2663
+ try {
2664
+ await sessionCtx.sdk.sendMessage(sessionCtx.chatId, {
2665
+ format: "text",
2666
+ content: resultText
2667
+ });
2568
2668
  sessionCtx.log("Result forwarded to chat");
2569
2669
  sessionCtx.reportSessionCompletion();
2570
- }).catch((err) => {
2670
+ sessionCtx.emitEvent({
2671
+ kind: "turn_end",
2672
+ payload: { status: "success" }
2673
+ });
2674
+ } catch (err) {
2571
2675
  const reason = err instanceof Error ? err.message : String(err);
2572
2676
  sessionCtx.log(`Failed to forward result: ${reason}`);
2573
- const message = `Result forward failed: ${reason}\n---\n${resultText.slice(0, 1500)}`.slice(0, 2e3);
2677
+ const forwardErrMessage = `Result forward failed: ${reason}\n---\n${resultText.slice(0, 1500)}`.slice(0, 2e3);
2574
2678
  sessionCtx.emitEvent({
2575
2679
  kind: "error",
2576
2680
  payload: {
2577
2681
  source: "runtime",
2578
- message
2682
+ message: forwardErrMessage
2579
2683
  }
2580
2684
  });
2581
- });
2582
- }
2685
+ sessionCtx.emitEvent({
2686
+ kind: "turn_end",
2687
+ payload: { status: "error" }
2688
+ });
2689
+ }
2690
+ } else sessionCtx.emitEvent({
2691
+ kind: "turn_end",
2692
+ payload: { status: "success" }
2693
+ });
2583
2694
  } else {
2584
2695
  const errors = message.errors ? message.errors.join("; ") : message.subtype;
2585
2696
  const errorLog = `Query result error: ${errors} (subtype=${message.subtype}, turns=${message.num_turns ?? "?"}, duration=${message.duration_ms ?? "?"}ms)`;
@@ -2591,6 +2702,10 @@ const createClaudeCodeHandler = (config) => {
2591
2702
  message: errors
2592
2703
  }
2593
2704
  });
2705
+ sessionCtx.emitEvent({
2706
+ kind: "turn_end",
2707
+ payload: { status: "error" }
2708
+ });
2594
2709
  }
2595
2710
  sessionCtx.setRuntimeState("idle");
2596
2711
  }
@@ -3053,39 +3168,49 @@ var SessionManager = class {
3053
3168
  const message = this.extractMessage(entry);
3054
3169
  await this.routeMessage(chatId, message, entry.id);
3055
3170
  }
3056
- /** Handle a session command from the server (suspend/resume/terminate). */
3171
+ /** Handle a server-issued session command. Terminate drops all local state without reporting back. */
3057
3172
  async handleCommand(chatId, command) {
3058
- const session = this.sessions.get(chatId);
3059
3173
  if (command === "session:suspend") {
3174
+ const session = this.sessions.get(chatId);
3060
3175
  if (session?.status === "active") {
3061
3176
  this.config.log(`Session ${chatId}: suspend command received`);
3062
3177
  this.suspendSession(session);
3063
3178
  }
3064
- } else if (command === "session:resume") {
3065
- if (session?.status === "suspended") {
3066
- this.config.log(`Session ${chatId}: resume command received`);
3067
- await this.resumeSession(session, null);
3068
- }
3069
- } else if (command === "session:terminate") {
3070
- if (session) {
3071
- this.config.log(`Session ${chatId}: terminate command received`);
3072
- if (session.status === "active") {
3073
- this._activeCount--;
3074
- await session.handler.shutdown();
3075
- }
3076
- this.addEvictedMapping(chatId, {
3077
- claudeSessionId: session.claudeSessionId,
3078
- lastActivity: session.lastActivity
3079
- });
3080
- this.sessions.delete(chatId);
3081
- this.sessionRuntimeStates.delete(chatId);
3082
- this.recomputeRuntimeState();
3083
- this.notifySessionState(chatId, "evicted");
3084
- this.persistRegistry();
3085
- this.drainPendingQueue();
3179
+ return;
3180
+ }
3181
+ if (command === "session:terminate") {
3182
+ const session = this.sessions.get(chatId);
3183
+ const hadMapping = this.evictedMappings.has(chatId);
3184
+ if (!session && !hadMapping) return;
3185
+ this.config.log(`Session ${chatId}: terminate command received`);
3186
+ if (session?.status === "active") {
3187
+ this._activeCount--;
3188
+ await session.handler.shutdown().catch(() => {});
3086
3189
  }
3190
+ this.sessions.delete(chatId);
3191
+ this.evictedMappings.delete(chatId);
3192
+ this.sessionRuntimeStates.delete(chatId);
3193
+ this.lastReportedStates.delete(chatId);
3194
+ for (let i = this.pendingQueue.length - 1; i >= 0; i--) if (this.pendingQueue[i]?.chatId === chatId) this.pendingQueue.splice(i, 1);
3195
+ this.recomputeRuntimeState();
3196
+ this.persistRegistry();
3197
+ this.drainPendingQueue();
3087
3198
  }
3088
3199
  }
3200
+ /** Chat IDs this client still holds locally (sessions + evictedMappings). */
3201
+ getHeldChatIds() {
3202
+ const ids = /* @__PURE__ */ new Set();
3203
+ for (const id of this.sessions.keys()) ids.add(id);
3204
+ for (const id of this.evictedMappings.keys()) ids.add(id);
3205
+ return [...ids];
3206
+ }
3207
+ /**
3208
+ * Apply a server-declared stale list from `session:reconcile:result` — treat
3209
+ * each chatId as if a `session:terminate` command had arrived.
3210
+ */
3211
+ applyStaleChatIds(staleChatIds) {
3212
+ for (const id of staleChatIds) this.handleCommand(id, "session:terminate");
3213
+ }
3089
3214
  /** Shut down all sessions gracefully. */
3090
3215
  async shutdown() {
3091
3216
  if (this.idleTimer) {
@@ -3286,8 +3411,6 @@ var SessionManager = class {
3286
3411
  this._activeCount--;
3287
3412
  candidate.session.handler.shutdown().catch(() => {});
3288
3413
  }
3289
- candidate.session.status = "evicted";
3290
- this.notifySessionState(candidate.key, "evicted");
3291
3414
  this.sessions.delete(candidate.key);
3292
3415
  this.sessionRuntimeStates.delete(candidate.key);
3293
3416
  this.recomputeRuntimeState();
@@ -3424,6 +3547,7 @@ var AgentSlot = class {
3424
3547
  sdk = null;
3425
3548
  agentConfigCache = null;
3426
3549
  pollingTimer = null;
3550
+ reconcileTimer = null;
3427
3551
  listeners = [];
3428
3552
  constructor(config) {
3429
3553
  this.config = config;
@@ -3459,16 +3583,26 @@ var AgentSlot = class {
3459
3583
  if (agentId === this.config.agentId) this.pullAndDispatch();
3460
3584
  };
3461
3585
  const onBound = (boundAgent) => {
3462
- if (boundAgent.agentId === this.config.agentId) this.fullStateSync();
3586
+ if (boundAgent.agentId === this.config.agentId) {
3587
+ this.fullStateSync();
3588
+ setTimeout(() => this.reconcileNow(), 5e3);
3589
+ }
3590
+ };
3591
+ const onReconcileResult = (result) => {
3592
+ if (result.agentId === this.config.agentId && this.sessionManager) this.sessionManager.applyStaleChatIds(result.staleChatIds);
3463
3593
  };
3464
3594
  this.clientConnection.on("agent:message", onMessage);
3465
3595
  this.clientConnection.on("agent:bound", onBound);
3596
+ this.clientConnection.on("session:reconcile:result", onReconcileResult);
3466
3597
  this.listeners.push({
3467
3598
  event: "agent:message",
3468
3599
  fn: onMessage
3469
3600
  }, {
3470
3601
  event: "agent:bound",
3471
3602
  fn: onBound
3603
+ }, {
3604
+ event: "session:reconcile:result",
3605
+ fn: onReconcileResult
3472
3606
  });
3473
3607
  const registryPath = join(DEFAULT_DATA_DIR, "sessions", `${this.config.name}.json`);
3474
3608
  const gitMirrorManager = createGitMirrorManager({
@@ -3511,6 +3645,7 @@ var AgentSlot = class {
3511
3645
  fn: onCommand
3512
3646
  });
3513
3647
  this.startPolling();
3648
+ this.startReconcileLoop();
3514
3649
  return agent;
3515
3650
  }
3516
3651
  async stop() {
@@ -3518,8 +3653,13 @@ var AgentSlot = class {
3518
3653
  clearInterval(this.pollingTimer);
3519
3654
  this.pollingTimer = null;
3520
3655
  }
3656
+ if (this.reconcileTimer) {
3657
+ clearInterval(this.reconcileTimer);
3658
+ this.reconcileTimer = null;
3659
+ }
3521
3660
  for (const entry of this.listeners) if (entry.event === "agent:message") this.clientConnection.off(entry.event, entry.fn);
3522
3661
  else if (entry.event === "agent:bound") this.clientConnection.off(entry.event, entry.fn);
3662
+ else if (entry.event === "session:reconcile:result") this.clientConnection.off(entry.event, entry.fn);
3523
3663
  else this.clientConnection.off(entry.event, entry.fn);
3524
3664
  this.listeners = [];
3525
3665
  await this.clientConnection.unbindAgent(this.config.agentId);
@@ -3550,6 +3690,16 @@ var AgentSlot = class {
3550
3690
  }, 5e3);
3551
3691
  this.pullAndDispatch();
3552
3692
  }
3693
+ startReconcileLoop() {
3694
+ const intervalSec = this.config.session.reconcile_interval_seconds ?? 300;
3695
+ this.reconcileTimer = setInterval(() => this.reconcileNow(), intervalSec * 1e3);
3696
+ }
3697
+ reconcileNow() {
3698
+ if (!this.sessionManager) return;
3699
+ const chatIds = this.sessionManager.getHeldChatIds();
3700
+ if (chatIds.length === 0) return;
3701
+ this.clientConnection.sendSessionReconcile(this.config.agentId, chatIds);
3702
+ }
3553
3703
  async pullAndDispatch() {
3554
3704
  if (!this.sdk || !this.sessionManager) return;
3555
3705
  try {
@@ -3578,7 +3728,8 @@ z.object({
3578
3728
  }).passthrough();
3579
3729
  const sessionConfigSchema = z.object({
3580
3730
  idle_timeout: z.number().int().positive().default(IDLE_TIMEOUT_MS / 1e3),
3581
- max_sessions: z.number().int().positive().default(50)
3731
+ max_sessions: z.number().int().positive().default(50),
3732
+ reconcile_interval_seconds: z.number().int().min(30).max(3600).default(300)
3582
3733
  }).passthrough();
3583
3734
  const agentSlotConfigSchema = z.object({
3584
3735
  agentId: z.string().min(1),
@@ -3727,7 +3878,8 @@ var ClientRuntime = class {
3727
3878
  handlerFactory,
3728
3879
  session: {
3729
3880
  idle_timeout: config.session.idle_timeout,
3730
- max_sessions: config.session.max_sessions
3881
+ max_sessions: config.session.max_sessions,
3882
+ reconcile_interval_seconds: 300
3731
3883
  },
3732
3884
  concurrency: config.concurrency,
3733
3885
  clientConnection: this.connection
@@ -4521,7 +4673,7 @@ async function onboardCreate(args) {
4521
4673
  }
4522
4674
  const runtimeAgent = args.type === "human" ? args.assistant : args.id;
4523
4675
  if (args.feishuBotAppId && args.feishuBotAppSecret) {
4524
- const { bindFeishuBot } = await import("./feishu-BOISS0DK.mjs").then((n) => n.r);
4676
+ const { bindFeishuBot } = await import("./feishu-n9Y2yGTT.mjs").then((n) => n.r);
4525
4677
  const targetAgentUuid = args.type === "human" ? assistantUuid : primary.uuid;
4526
4678
  if (!targetAgentUuid) process.stderr.write(`Warning: Cannot bind Feishu bot — no runtime agent available for "${args.id}".\n`);
4527
4679
  else {
@@ -4662,7 +4814,7 @@ function setNestedByDot(obj, dotPath, value) {
4662
4814
  if (lastKey !== void 0) current[lastKey] = value;
4663
4815
  }
4664
4816
  //#endregion
4665
- //#region ../server/dist/app-DJEePmWL.mjs
4817
+ //#region ../server/dist/app-C6y-ySN9.mjs
4666
4818
  var __defProp = Object.defineProperty;
4667
4819
  var __exportAll = (all, no_symbols) => {
4668
4820
  let target = {};
@@ -5199,6 +5351,8 @@ function parseId$1(raw) {
5199
5351
  async function adminAdapterMappingRoutes(app) {
5200
5352
  app.get("/", async (request) => {
5201
5353
  const scope = memberScope(request);
5354
+ const conditions = [eq(agents.organizationId, scope.organizationId)];
5355
+ if (scope.role !== "admin") conditions.push(eq(agents.managerId, scope.memberId));
5202
5356
  return (await app.db.select({
5203
5357
  id: adapterAgentMappings.id,
5204
5358
  platform: adapterAgentMappings.platform,
@@ -5207,7 +5361,7 @@ async function adminAdapterMappingRoutes(app) {
5207
5361
  boundVia: adapterAgentMappings.boundVia,
5208
5362
  displayName: adapterAgentMappings.displayName,
5209
5363
  createdAt: adapterAgentMappings.createdAt
5210
- }).from(adapterAgentMappings).innerJoin(agents, eq(agents.uuid, adapterAgentMappings.agentId)).where(eq(agents.organizationId, scope.organizationId)).orderBy(desc(adapterAgentMappings.createdAt))).map((r) => ({
5364
+ }).from(adapterAgentMappings).innerJoin(agents, eq(agents.uuid, adapterAgentMappings.agentId)).where(and(...conditions)).orderBy(desc(adapterAgentMappings.createdAt))).map((r) => ({
5211
5365
  id: r.id,
5212
5366
  platform: r.platform,
5213
5367
  externalUserId: r.externalUserId,
@@ -5255,11 +5409,6 @@ async function adminAdapterMappingRoutes(app) {
5255
5409
  return reply.status(204).send();
5256
5410
  });
5257
5411
  }
5258
- async function adminAdapterStatusRoutes(app) {
5259
- app.get("/", async () => {
5260
- return app.adapterManager.getBotStatuses();
5261
- });
5262
- }
5263
5412
  /** Bot credentials for external platform adapters. Credentials are encrypted at application layer (AES-256-GCM). */
5264
5413
  const adapterConfigs = pgTable("adapter_configs", {
5265
5414
  id: serial("id").primaryKey(),
@@ -5270,6 +5419,17 @@ const adapterConfigs = pgTable("adapter_configs", {
5270
5419
  createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
5271
5420
  updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
5272
5421
  }, (t) => [unique("uq_adapter_configs_agent_platform").on(t.agentId, t.platform)]);
5422
+ async function adminAdapterStatusRoutes(app) {
5423
+ app.get("/", async (request) => {
5424
+ const scope = memberScope(request);
5425
+ const conditions = [eq(agents.organizationId, scope.organizationId), ne(agents.status, "deleted")];
5426
+ if (scope.role !== "admin") conditions.push(eq(agents.managerId, scope.memberId));
5427
+ const visibleRows = await app.db.select({ id: adapterConfigs.id }).from(adapterConfigs).innerJoin(agents, eq(agents.uuid, adapterConfigs.agentId)).where(and(...conditions));
5428
+ const visibleIds = new Set(visibleRows.map((r) => r.id));
5429
+ if (visibleIds.size === 0) return [];
5430
+ return app.adapterManager.getBotStatuses().filter((s) => visibleIds.has(s.configId));
5431
+ });
5432
+ }
5273
5433
  const ALGORITHM = "aes-256-gcm";
5274
5434
  const IV_LENGTH = 12;
5275
5435
  const AUTH_TAG_LENGTH = 16;
@@ -5383,6 +5543,30 @@ function toResponse(row) {
5383
5543
  async function listAdapterConfigs(db) {
5384
5544
  return (await db.select().from(adapterConfigs).orderBy(desc(adapterConfigs.createdAt))).map(toResponse);
5385
5545
  }
5546
+ /**
5547
+ * Scoped variant used by the member-facing admin route.
5548
+ *
5549
+ * - admin: every adapter config whose agent belongs to the caller's org.
5550
+ * - non-admin: only adapter configs bound to agents the caller manages
5551
+ * (Rule: bindings follow manageability).
5552
+ *
5553
+ * Kept as a separate function so internal self-service callers
5554
+ * (`agent/feishu-bot.ts`) that only need a raw read don't accidentally pay
5555
+ * for the join.
5556
+ */
5557
+ async function listAdapterConfigsForMember(db, scope) {
5558
+ const conditions = [eq(agents.organizationId, scope.organizationId), ne(agents.status, "deleted")];
5559
+ if (scope.role !== "admin") conditions.push(eq(agents.managerId, scope.memberId));
5560
+ return (await db.select({
5561
+ id: adapterConfigs.id,
5562
+ platform: adapterConfigs.platform,
5563
+ agentId: adapterConfigs.agentId,
5564
+ credentials: adapterConfigs.credentials,
5565
+ status: adapterConfigs.status,
5566
+ createdAt: adapterConfigs.createdAt,
5567
+ updatedAt: adapterConfigs.updatedAt
5568
+ }).from(adapterConfigs).innerJoin(agents, eq(agents.uuid, adapterConfigs.agentId)).where(and(...conditions)).orderBy(desc(adapterConfigs.createdAt))).map(toResponse);
5569
+ }
5386
5570
  async function getAdapterConfig(db, id) {
5387
5571
  const [row] = await db.select().from(adapterConfigs).where(eq(adapterConfigs.id, id)).limit(1);
5388
5572
  if (!row) throw new NotFoundError(`Adapter config "${id}" not found`);
@@ -5432,8 +5616,9 @@ function parseId(raw) {
5432
5616
  return id;
5433
5617
  }
5434
5618
  async function adminAdapterRoutes(app) {
5435
- app.get("/", async () => {
5436
- return (await listAdapterConfigs(app.db)).map((c) => ({
5619
+ app.get("/", async (request) => {
5620
+ const scope = memberScope(request);
5621
+ return (await listAdapterConfigsForMember(app.db, scope)).map((c) => ({
5437
5622
  ...c,
5438
5623
  createdAt: c.createdAt.toISOString(),
5439
5624
  updatedAt: c.updatedAt.toISOString()
@@ -5695,6 +5880,47 @@ async function getAgent(db, uuid) {
5695
5880
  return agent;
5696
5881
  }
5697
5882
  /**
5883
+ * Admin-only variant: return every non-deleted agent in the org, ignoring
5884
+ * the visibility filter. Used by the `/admin` "All Agents" view so a team
5885
+ * admin can see and act on private agents owned by other members. The
5886
+ * route layer is responsible for gating this to admin callers — the
5887
+ * service does not enforce role by itself, but it does enforce org scope
5888
+ * and the not-deleted predicate.
5889
+ */
5890
+ async function listAgentsForAdmin(db, scope, limit, cursor) {
5891
+ const conditions = [eq(agents.organizationId, scope.organizationId), ne(agents.status, AGENT_STATUSES.DELETED)];
5892
+ if (cursor) conditions.push(lt(agents.createdAt, new Date(cursor)));
5893
+ const where = and(...conditions);
5894
+ const rows = await db.select({
5895
+ uuid: agents.uuid,
5896
+ name: agents.name,
5897
+ organizationId: agents.organizationId,
5898
+ type: agents.type,
5899
+ displayName: agents.displayName,
5900
+ delegateMention: agents.delegateMention,
5901
+ inboxId: agents.inboxId,
5902
+ status: agents.status,
5903
+ cloudUserId: agents.cloudUserId,
5904
+ visibility: agents.visibility,
5905
+ metadata: agents.metadata,
5906
+ managerId: agents.managerId,
5907
+ clientId: agents.clientId,
5908
+ createdAt: agents.createdAt,
5909
+ updatedAt: agents.updatedAt,
5910
+ presenceStatus: agentPresence.status,
5911
+ runtimeType: agentPresence.runtimeType,
5912
+ runtimeState: agentPresence.runtimeState,
5913
+ activeSessions: agentPresence.activeSessions
5914
+ }).from(agents).leftJoin(agentPresence, eq(agents.uuid, agentPresence.agentId)).where(where).orderBy(desc(agents.createdAt)).limit(limit + 1);
5915
+ const hasMore = rows.length > limit;
5916
+ const items = hasMore ? rows.slice(0, limit) : rows;
5917
+ const last = items[items.length - 1];
5918
+ return {
5919
+ items,
5920
+ nextCursor: hasMore && last ? last.createdAt.toISOString() : null
5921
+ };
5922
+ }
5923
+ /**
5698
5924
  * List agents visible to a specific member.
5699
5925
  * Uses agentVisibilityCondition from access-control (same rules for all roles).
5700
5926
  */
@@ -6190,17 +6416,31 @@ async function cleanupStalePresence(db, staleSeconds = 60) {
6190
6416
  `)).length;
6191
6417
  }
6192
6418
  /**
6193
- * Assert the caller's user owns this client. Throws 404 for both "not found"
6419
+ * Assert the caller can act on this client. Throws 404 for both "not found"
6194
6420
  * and "not yours" to prevent UUID enumeration across org/user boundaries.
6195
- * Used by management routes (disconnect, retire, single GET) so a cross-org
6196
- * admin cannot operate on another user's client.
6421
+ *
6422
+ * - member: owner match (`row.user_id == scope.userId`).
6423
+ * - admin: any client whose owner is a member of the admin's own org.
6424
+ *
6425
+ * Legacy unclaimed rows (`user_id IS NULL`) have no org association we can
6426
+ * verify — we explicitly refuse to grant admin access to them so a
6427
+ * cross-tenant admin can't operate on another org's orphan rows. These
6428
+ * orphans are surfaced for self-service re-registration only; the owning
6429
+ * operator must claim the row via `first-tree-hub connect` before any
6430
+ * admin action becomes available.
6197
6431
  */
6198
- async function assertClientOwner(db, clientId, userId) {
6432
+ async function assertClientOwner(db, clientId, scope) {
6199
6433
  const [row] = await db.select({
6200
6434
  id: clients.id,
6201
6435
  userId: clients.userId
6202
6436
  }).from(clients).where(eq(clients.id, clientId)).limit(1);
6203
- if (!row || row.userId !== userId) throw new NotFoundError(`Client "${clientId}" not found`);
6437
+ if (!row) throw new NotFoundError(`Client "${clientId}" not found`);
6438
+ if (row.userId === scope.userId) return;
6439
+ if (scope.role === "admin" && row.userId !== null) {
6440
+ const [sibling] = await db.select({ id: members.id }).from(members).where(and(eq(members.userId, row.userId), eq(members.organizationId, scope.organizationId))).limit(1);
6441
+ if (sibling) return;
6442
+ }
6443
+ throw new NotFoundError(`Client "${clientId}" not found`);
6204
6444
  }
6205
6445
  /**
6206
6446
  * Upsert the clients row for a given `client_id` under an authenticated user.
@@ -6280,8 +6520,32 @@ async function listActiveAgentsPinnedToClient(db, clientId) {
6280
6520
  type: agents.type
6281
6521
  }).from(agents).where(and(eq(agents.clientId, clientId), ne(agents.status, "deleted")));
6282
6522
  }
6283
- async function listClients(db, userId) {
6284
- const rows = await db.select().from(clients).where(eq(clients.userId, userId));
6523
+ /**
6524
+ * Scope-aware client listing.
6525
+ *
6526
+ * - member: only rows where `user_id = scope.userId`.
6527
+ * - admin: every claimed row whose owner is a member of the caller's
6528
+ * organization.
6529
+ *
6530
+ * Legacy unclaimed rows (`user_id IS NULL`) are intentionally hidden from
6531
+ * both roles — the `clients` table has no org column, so we cannot verify
6532
+ * which org an orphan belongs to. Exposing them to admin would leak
6533
+ * orphans across tenants. The owning operator reclaims the row via
6534
+ * `first-tree-hub connect`, after which it appears in their list.
6535
+ */
6536
+ async function listClients(db, scope) {
6537
+ const rows = scope.role === "admin" ? await db.selectDistinct({
6538
+ id: clients.id,
6539
+ userId: clients.userId,
6540
+ status: clients.status,
6541
+ sdkVersion: clients.sdkVersion,
6542
+ hostname: clients.hostname,
6543
+ os: clients.os,
6544
+ instanceId: clients.instanceId,
6545
+ connectedAt: clients.connectedAt,
6546
+ lastSeenAt: clients.lastSeenAt,
6547
+ metadata: clients.metadata
6548
+ }).from(clients).innerJoin(members, eq(members.userId, clients.userId)).where(eq(members.organizationId, scope.organizationId)) : await db.select().from(clients).where(eq(clients.userId, scope.userId));
6285
6549
  const counts = await db.select({
6286
6550
  clientId: agents.clientId,
6287
6551
  count: sql`count(*)::int`
@@ -6746,6 +7010,33 @@ async function adminAgentRoutes(app) {
6746
7010
  nextCursor: result.nextCursor
6747
7011
  };
6748
7012
  });
7013
+ /**
7014
+ * Admin-only: every agent in the caller's org, skipping the visibility
7015
+ * filter applied on the regular `/agents` list. Private agents owned by
7016
+ * other members show up here so an admin can reassign or troubleshoot.
7017
+ * Role gating is enforced here — the parent route group does NOT add
7018
+ * adminOnly because the member-facing `GET /` is shared.
7019
+ */
7020
+ app.get("/all", async (request) => {
7021
+ const scope = memberScope(request);
7022
+ if (scope.role !== "admin") throw new ForbiddenError("Admin role required");
7023
+ const query = paginationQuerySchema.parse(request.query);
7024
+ const result = await listAgentsForAdmin(app.db, scope, query.limit, query.cursor);
7025
+ return {
7026
+ items: result.items.map((a) => ({
7027
+ ...a,
7028
+ managerId: a.managerId ?? null,
7029
+ presenceStatus: a.presenceStatus ?? "offline",
7030
+ createdAt: a.createdAt.toISOString(),
7031
+ updatedAt: a.updatedAt.toISOString(),
7032
+ clientId: a.clientId ?? null,
7033
+ runtimeType: a.runtimeType ?? null,
7034
+ runtimeState: a.runtimeState ?? null,
7035
+ activeSessions: a.activeSessions ?? null
7036
+ })),
7037
+ nextCursor: result.nextCursor
7038
+ };
7039
+ });
6749
7040
  app.post("/", async (request, reply) => {
6750
7041
  const scope = memberScope(request);
6751
7042
  const body = createAgentSchema.parse(request.body);
@@ -7032,6 +7323,24 @@ async function adminChatRoutes(app) {
7032
7323
  }))
7033
7324
  };
7034
7325
  });
7326
+ /** Rename (or clear) a chat's topic. Requires participation or supervision — same gate as reading it. */
7327
+ app.patch("/:chatId", async (request) => {
7328
+ const { chatId } = request.params;
7329
+ const scope = memberScope(request);
7330
+ await assertChatAccess(app.db, scope, chatId);
7331
+ const body = updateChatSchema.parse(request.body);
7332
+ const nextTopic = body.topic && body.topic.length > 0 ? body.topic : null;
7333
+ const [updated] = await app.db.update(chats).set({
7334
+ topic: nextTopic,
7335
+ updatedAt: /* @__PURE__ */ new Date()
7336
+ }).where(eq(chats.id, chatId)).returning();
7337
+ if (!updated) throw new Error("Unexpected: chat missing after update");
7338
+ return {
7339
+ ...updated,
7340
+ createdAt: updated.createdAt.toISOString(),
7341
+ updatedAt: updated.updatedAt.toISOString()
7342
+ };
7343
+ });
7035
7344
  /** List messages in a chat with delivery status (requires participation or supervision) */
7036
7345
  app.get("/:chatId/messages", async (request) => {
7037
7346
  const { chatId } = request.params;
@@ -7151,10 +7460,18 @@ const agentChatSessions = pgTable("agent_chat_sessions", {
7151
7460
  state: text("state").notNull(),
7152
7461
  updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
7153
7462
  }, (table) => [primaryKey({ columns: [table.agentId, table.chatId] })]);
7154
- /** Upsert a session state, refresh materialized aggregates on agent_presence, and emit org-scoped NOTIFY. */
7463
+ /**
7464
+ * Upsert session state + refresh presence aggregates + NOTIFY.
7465
+ *
7466
+ * Revival defense: an admin-terminated (`evicted`) row is immutable; a client
7467
+ * report for the same chatId is silently dropped after the FOR UPDATE check.
7468
+ */
7155
7469
  async function upsertSessionState(db, agentId, chatId, state, organizationId, notifier) {
7156
7470
  const now = /* @__PURE__ */ new Date();
7471
+ let wrote = false;
7157
7472
  await db.transaction(async (tx) => {
7473
+ const [existing] = await tx.select({ state: agentChatSessions.state }).from(agentChatSessions).where(and(eq(agentChatSessions.agentId, agentId), eq(agentChatSessions.chatId, chatId))).for("update");
7474
+ if (existing?.state === "evicted") return;
7158
7475
  await tx.insert(agentChatSessions).values({
7159
7476
  agentId,
7160
7477
  chatId,
@@ -7178,8 +7495,9 @@ async function upsertSessionState(db, agentId, chatId, state, organizationId, no
7178
7495
  totalSessions,
7179
7496
  lastSeenAt: now
7180
7497
  }).where(eq(agentPresence.agentId, agentId));
7498
+ wrote = true;
7181
7499
  });
7182
- if (notifier) notifier.notifySessionStateChange(agentId, chatId, state, organizationId).catch(() => {});
7500
+ if (wrote && notifier) notifier.notifySessionStateChange(agentId, chatId, state, organizationId).catch(() => {});
7183
7501
  }
7184
7502
  async function resetActivity(db, agentId) {
7185
7503
  const now = /* @__PURE__ */ new Date();
@@ -7232,22 +7550,6 @@ async function listAgentsWithRuntime(db, scope) {
7232
7550
  type: agents.type
7233
7551
  }).from(agentPresence).innerJoin(agents, eq(agentPresence.agentId, agents.uuid)).where(and(isNotNull(agentPresence.runtimeState), agentVisibilityCondition(scope)));
7234
7552
  }
7235
- /**
7236
- * Clean up stale session rows from agent_chat_sessions.
7237
- * Removes evicted rows older than staleSeconds and suspended rows older than staleSeconds.
7238
- * Returns the number of rows deleted.
7239
- */
7240
- async function cleanupStaleSessions(db, staleSeconds = 604800) {
7241
- return (await db.execute(sql`
7242
- WITH deleted AS (
7243
- DELETE FROM agent_chat_sessions
7244
- WHERE state IN ('evicted', 'suspended')
7245
- AND updated_at < NOW() - make_interval(secs => ${staleSeconds})
7246
- RETURNING 1
7247
- )
7248
- SELECT count(*)::int AS cnt FROM deleted
7249
- `))[0]?.cnt ?? 0;
7250
- }
7251
7553
  /** Serialize a Date to ISO string, or null. */
7252
7554
  function serializeDate(d) {
7253
7555
  return d ? d.toISOString() : null;
@@ -7255,7 +7557,11 @@ function serializeDate(d) {
7255
7557
  async function adminClientRoutes(app) {
7256
7558
  app.get("/", async (request) => {
7257
7559
  const scope = memberScope(request);
7258
- return (await listClients(app.db, scope.userId)).map((c) => ({
7560
+ return (await listClients(app.db, {
7561
+ userId: scope.userId,
7562
+ organizationId: scope.organizationId,
7563
+ role: scope.role
7564
+ })).map((c) => ({
7259
7565
  id: c.id,
7260
7566
  userId: c.userId,
7261
7567
  status: c.status,
@@ -7269,7 +7575,7 @@ async function adminClientRoutes(app) {
7269
7575
  });
7270
7576
  app.get("/:clientId", async (request) => {
7271
7577
  const scope = memberScope(request);
7272
- await assertClientOwner(app.db, request.params.clientId, scope.userId);
7578
+ await assertClientOwner(app.db, request.params.clientId, scope);
7273
7579
  const client = await getClient(app.db, request.params.clientId);
7274
7580
  if (!client) throw new Error("unreachable: client missing after owner check");
7275
7581
  return {
@@ -7286,7 +7592,7 @@ async function adminClientRoutes(app) {
7286
7592
  app.post("/:clientId/disconnect", async (request) => {
7287
7593
  const scope = memberScope(request);
7288
7594
  const { clientId } = request.params;
7289
- await assertClientOwner(app.db, clientId, scope.userId);
7595
+ await assertClientOwner(app.db, clientId, scope);
7290
7596
  const agentIds = forceDisconnectClient(clientId);
7291
7597
  await disconnectClient(app.db, clientId);
7292
7598
  return {
@@ -7297,7 +7603,7 @@ async function adminClientRoutes(app) {
7297
7603
  app.delete("/:clientId", async (request, reply) => {
7298
7604
  const scope = memberScope(request);
7299
7605
  const { clientId } = request.params;
7300
- await assertClientOwner(app.db, clientId, scope.userId);
7606
+ await assertClientOwner(app.db, clientId, scope);
7301
7607
  await retireClient(app.db, clientId);
7302
7608
  forceDisconnectClient(clientId);
7303
7609
  await disconnectClient(app.db, clientId);
@@ -7577,16 +7883,26 @@ async function adminOverviewRoutes(app) {
7577
7883
  };
7578
7884
  });
7579
7885
  }
7886
+ const SUMMARY_MAX_LENGTH = 50;
7887
+ /** Extract a plain-text summary from a message's JSONB content field. */
7888
+ function extractSummary(content, maxLen = SUMMARY_MAX_LENGTH) {
7889
+ let text = "";
7890
+ if (typeof content === "object" && content !== null && "text" in content) text = String(content.text ?? "");
7891
+ else if (typeof content === "string") text = content;
7892
+ return text ? text.slice(0, maxLen) : null;
7893
+ }
7580
7894
  /** List sessions for a specific agent, with optional state filters. */
7581
7895
  async function listAgentSessions(db, agentId, filters) {
7582
7896
  const conditions = [eq(agentChatSessions.agentId, agentId)];
7583
7897
  if (filters?.state) conditions.push(eq(agentChatSessions.state, filters.state));
7898
+ else conditions.push(ne(agentChatSessions.state, "evicted"));
7584
7899
  const rows = await db.select({
7585
7900
  agentId: agentChatSessions.agentId,
7586
7901
  chatId: agentChatSessions.chatId,
7587
7902
  state: agentChatSessions.state,
7588
7903
  updatedAt: agentChatSessions.updatedAt,
7589
- chatCreatedAt: chats.createdAt
7904
+ chatCreatedAt: chats.createdAt,
7905
+ chatTopic: chats.topic
7590
7906
  }).from(agentChatSessions).innerJoin(chats, eq(agentChatSessions.chatId, chats.id)).where(and(...conditions)).orderBy(desc(agentChatSessions.updatedAt));
7591
7907
  const [presence] = await db.select({ runtimeState: agentPresence.runtimeState }).from(agentPresence).where(eq(agentPresence.agentId, agentId)).limit(1);
7592
7908
  const agentRuntimeState = presence?.runtimeState ?? null;
@@ -7597,6 +7913,15 @@ async function listAgentSessions(db, agentId, filters) {
7597
7913
  count: sql`count(*)::int`
7598
7914
  }).from(inboxEntries).where(and(eq(inboxEntries.inboxId, sql`(SELECT inbox_id FROM agents WHERE uuid = ${agentId})`), inArray(inboxEntries.chatId, chatIds))).groupBy(inboxEntries.chatId) : [];
7599
7915
  const countMap = new Map(messageCounts.map((r) => [r.chatId, r.count]));
7916
+ const firstMessages = chatIds.length > 0 ? await db.selectDistinctOn([messages.chatId], {
7917
+ chatId: messages.chatId,
7918
+ content: messages.content
7919
+ }).from(messages).where(inArray(messages.chatId, chatIds)).orderBy(messages.chatId, messages.createdAt) : [];
7920
+ const summaryMap = /* @__PURE__ */ new Map();
7921
+ for (const row of firstMessages) {
7922
+ const summary = extractSummary(row.content);
7923
+ if (summary) summaryMap.set(row.chatId, summary);
7924
+ }
7600
7925
  return rows.map((r) => ({
7601
7926
  agentId: r.agentId,
7602
7927
  chatId: r.chatId,
@@ -7604,7 +7929,9 @@ async function listAgentSessions(db, agentId, filters) {
7604
7929
  runtimeState: agentRuntimeState,
7605
7930
  startedAt: r.chatCreatedAt.toISOString(),
7606
7931
  lastActivityAt: r.updatedAt.toISOString(),
7607
- messageCount: countMap.get(r.chatId) ?? 0
7932
+ messageCount: countMap.get(r.chatId) ?? 0,
7933
+ summary: summaryMap.get(r.chatId) ?? null,
7934
+ topic: r.chatTopic ?? null
7608
7935
  }));
7609
7936
  }
7610
7937
  /** Get a single session's detail. */
@@ -7614,11 +7941,14 @@ async function getSession(db, agentId, chatId) {
7614
7941
  chatId: agentChatSessions.chatId,
7615
7942
  state: agentChatSessions.state,
7616
7943
  updatedAt: agentChatSessions.updatedAt,
7617
- chatCreatedAt: chats.createdAt
7944
+ chatCreatedAt: chats.createdAt,
7945
+ chatTopic: chats.topic
7618
7946
  }).from(agentChatSessions).innerJoin(chats, eq(agentChatSessions.chatId, chats.id)).where(and(eq(agentChatSessions.agentId, agentId), eq(agentChatSessions.chatId, chatId))).limit(1);
7619
7947
  if (!row) throw new NotFoundError(`Session (${agentId}, ${chatId}) not found`);
7620
7948
  const [presence] = await db.select({ runtimeState: agentPresence.runtimeState }).from(agentPresence).where(eq(agentPresence.agentId, agentId)).limit(1);
7621
7949
  const [countRow] = await db.select({ count: sql`count(*)::int` }).from(inboxEntries).where(and(eq(inboxEntries.inboxId, sql`(SELECT inbox_id FROM agents WHERE uuid = ${agentId})`), eq(inboxEntries.chatId, chatId)));
7950
+ const firstMsg = (await db.execute(sql`SELECT content FROM messages WHERE chat_id = ${chatId} ORDER BY created_at ASC LIMIT 1`))[0];
7951
+ const summary = firstMsg ? extractSummary(firstMsg.content) : null;
7622
7952
  return {
7623
7953
  agentId: row.agentId,
7624
7954
  chatId: row.chatId,
@@ -7626,13 +7956,16 @@ async function getSession(db, agentId, chatId) {
7626
7956
  runtimeState: presence?.runtimeState ?? null,
7627
7957
  startedAt: row.chatCreatedAt.toISOString(),
7628
7958
  lastActivityAt: row.updatedAt.toISOString(),
7629
- messageCount: countRow?.count ?? 0
7959
+ messageCount: countRow?.count ?? 0,
7960
+ summary,
7961
+ topic: row.chatTopic ?? null
7630
7962
  };
7631
7963
  }
7632
7964
  /** List all sessions across all agents, with pagination. Scoped to organization. */
7633
7965
  async function listAllSessions(db, limit, cursor, filters) {
7634
7966
  const conditions = [];
7635
7967
  if (filters?.state) conditions.push(eq(agentChatSessions.state, filters.state));
7968
+ else conditions.push(ne(agentChatSessions.state, "evicted"));
7636
7969
  if (filters?.agentId) conditions.push(eq(agentChatSessions.agentId, filters.agentId));
7637
7970
  if (filters?.organizationId) conditions.push(eq(agents.organizationId, filters.organizationId));
7638
7971
  if (cursor) conditions.push(sql`${agentChatSessions.updatedAt} < ${new Date(cursor)}`);
@@ -7661,11 +7994,54 @@ async function listAllSessions(db, limit, cursor, filters) {
7661
7994
  runtimeState: runtimeMap.get(r.agentId) ?? null,
7662
7995
  startedAt: r.chatCreatedAt.toISOString(),
7663
7996
  lastActivityAt: r.updatedAt.toISOString(),
7664
- messageCount: 0
7997
+ messageCount: 0,
7998
+ summary: null,
7999
+ topic: null
7665
8000
  })),
7666
8001
  nextCursor
7667
8002
  };
7668
8003
  }
8004
+ /** Commit `active → suspended`. No-op on suspended/evicted. Throws if row is missing. */
8005
+ async function suspendSession(db, agentId, chatId, organizationId, notifier) {
8006
+ return transitionSessionState(db, agentId, chatId, "suspended", ["active"], organizationId, notifier);
8007
+ }
8008
+ /** Commit `suspended → evicted` (terminal — listings hide it, revival defense blocks resurrection). */
8009
+ async function archiveSession(db, agentId, chatId, organizationId, notifier) {
8010
+ return transitionSessionState(db, agentId, chatId, "evicted", ["suspended"], organizationId, notifier);
8011
+ }
8012
+ async function transitionSessionState(db, agentId, chatId, target, from, organizationId, notifier) {
8013
+ const now = /* @__PURE__ */ new Date();
8014
+ let finalState = null;
8015
+ let transitioned = false;
8016
+ await db.transaction(async (tx) => {
8017
+ const [existing] = await tx.select({ state: agentChatSessions.state }).from(agentChatSessions).where(and(eq(agentChatSessions.agentId, agentId), eq(agentChatSessions.chatId, chatId))).for("update");
8018
+ if (!existing) return;
8019
+ const current = existing.state;
8020
+ finalState = current;
8021
+ if (!from.includes(current)) return;
8022
+ await tx.update(agentChatSessions).set({
8023
+ state: target,
8024
+ updatedAt: now
8025
+ }).where(and(eq(agentChatSessions.agentId, agentId), eq(agentChatSessions.chatId, chatId)));
8026
+ const [counts] = await tx.select({
8027
+ active: sql`count(*) FILTER (WHERE ${agentChatSessions.state} = 'active')::int`,
8028
+ total: sql`count(*) FILTER (WHERE ${agentChatSessions.state} != 'evicted')::int`
8029
+ }).from(agentChatSessions).where(eq(agentChatSessions.agentId, agentId));
8030
+ await tx.update(agentPresence).set({
8031
+ activeSessions: counts?.active ?? 0,
8032
+ totalSessions: counts?.total ?? 0,
8033
+ lastSeenAt: now
8034
+ }).where(eq(agentPresence.agentId, agentId));
8035
+ finalState = target;
8036
+ transitioned = true;
8037
+ });
8038
+ if (finalState === null) throw new NotFoundError(`Session (${agentId}, ${chatId}) not found`);
8039
+ if (transitioned && notifier) notifier.notifySessionStateChange(agentId, chatId, target, organizationId).catch(() => {});
8040
+ return {
8041
+ state: finalState,
8042
+ transitioned
8043
+ };
8044
+ }
7669
8045
  /**
7670
8046
  * Filter sessions to only those where the given agent is also a participant in the chat.
7671
8047
  * Used when a non-manager views sessions of an org-visible agent — they should only see
@@ -7742,13 +8118,20 @@ async function appendEvent(db, agentId, chatId, event) {
7742
8118
  throw new Error(`session_events seq contention on ${agentId}/${chatId}`);
7743
8119
  }
7744
8120
  /**
7745
- * List events for a session in `seq asc` order with cursor pagination.
7746
- * `cursor` is the last seen `seq`; pass it as-is on the next page.
8121
+ * List events for a session with cursor pagination.
8122
+ *
8123
+ * - `direction: "asc"` (default) walks oldest → newest; cursor is the last
8124
+ * seq seen on the previous page (next page starts at seq > cursor).
8125
+ * - `direction: "desc"` walks newest → oldest; cursor is the last seq seen
8126
+ * on the previous page (next page starts at seq < cursor). The chat UI
8127
+ * uses desc so its turn-grouping filter always sees the most recent
8128
+ * `turn_end` even when the chat has thousands of events.
7747
8129
  */
7748
8130
  async function listEvents(db, agentId, chatId, options) {
7749
8131
  const limit = Math.min(Math.max(options?.limit ?? DEFAULT_LIMIT, 1), MAX_LIMIT);
8132
+ const direction = options?.direction ?? "asc";
7750
8133
  const conditions = [eq(sessionEvents.agentId, agentId), eq(sessionEvents.chatId, chatId)];
7751
- if (options?.cursor !== void 0) conditions.push(gt(sessionEvents.seq, options.cursor));
8134
+ if (options?.cursor !== void 0) conditions.push(direction === "desc" ? lt(sessionEvents.seq, options.cursor) : gt(sessionEvents.seq, options.cursor));
7752
8135
  const rows = await db.select({
7753
8136
  id: sessionEvents.id,
7754
8137
  agentId: sessionEvents.agentId,
@@ -7757,7 +8140,7 @@ async function listEvents(db, agentId, chatId, options) {
7757
8140
  kind: sessionEvents.kind,
7758
8141
  payload: sessionEvents.payload,
7759
8142
  createdAt: sessionEvents.createdAt
7760
- }).from(sessionEvents).where(and(...conditions)).orderBy(asc(sessionEvents.seq)).limit(limit + 1);
8143
+ }).from(sessionEvents).where(and(...conditions)).orderBy(direction === "desc" ? desc(sessionEvents.seq) : asc(sessionEvents.seq)).limit(limit + 1);
7761
8144
  const hasMore = rows.length > limit;
7762
8145
  const items = (hasMore ? rows.slice(0, limit) : rows).map(rowToEvent);
7763
8146
  const last = items[items.length - 1];
@@ -7820,61 +8203,60 @@ async function adminSessionRoutes(app) {
7820
8203
  await assertChatAccess(app.db, scope, request.params.chatId);
7821
8204
  return getSession(app.db, request.params.agentId, request.params.chatId);
7822
8205
  });
7823
- /** GET /admin/sessions/agents/:agentId/:chatId/events — session event stream, paged by `seq` */
8206
+ /**
8207
+ * GET /admin/sessions/agents/:agentId/:chatId/events — session event stream,
8208
+ * paged by `seq`. `direction=desc` returns newest-first; the chat UI uses
8209
+ * this so its turn-grouping filter always sees the latest `turn_end`
8210
+ * regardless of total event count.
8211
+ */
7824
8212
  app.get("/agents/:agentId/:chatId/events", async (request) => {
7825
8213
  const scope = memberScope(request);
7826
8214
  await assertAgentVisible(app.db, scope, request.params.agentId);
7827
8215
  await assertChatAccess(app.db, scope, request.params.chatId);
7828
8216
  const limit = request.query.limit !== void 0 ? Number.parseInt(request.query.limit, 10) : void 0;
7829
8217
  const cursor = request.query.cursor !== void 0 ? Number.parseInt(request.query.cursor, 10) : void 0;
8218
+ const direction = request.query.direction === "desc" ? "desc" : "asc";
7830
8219
  return listEvents(app.db, request.params.agentId, request.params.chatId, {
7831
8220
  limit: Number.isFinite(limit) ? limit : void 0,
7832
- cursor: Number.isFinite(cursor) ? cursor : void 0
8221
+ cursor: Number.isFinite(cursor) ? cursor : void 0,
8222
+ direction
7833
8223
  });
7834
8224
  });
7835
- /** POST /admin/sessions/agents/:agentId/:chatId/suspend — suspend a session */
8225
+ /** POST /admin/sessions/agents/:agentId/:chatId/suspend — commit first, WS-send best-effort. */
7836
8226
  app.post("/agents/:agentId/:chatId/suspend", async (request, reply) => {
7837
8227
  const { agentId, chatId } = request.params;
7838
8228
  await assertCanManage(app.db, memberScope(request), agentId);
7839
- if (!sendToAgent$1(agentId, {
8229
+ const member = requireMember(request);
8230
+ const result = await suspendSession(app.db, agentId, chatId, member.organizationId, app.notifier);
8231
+ if (result.transitioned) sendToAgent$1(agentId, {
7840
8232
  type: "session:suspend",
7841
8233
  chatId
7842
- })) throw new ConflictError("Agent is not connected — session command requires a live connection");
7843
- return reply.status(202).send({
7844
- status: "sent",
7845
- command: "suspend",
7846
- agentId,
7847
- chatId
7848
8234
  });
7849
- });
7850
- /** POST /admin/sessions/agents/:agentId/:chatId/resume — resume a session */
7851
- app.post("/agents/:agentId/:chatId/resume", async (request, reply) => {
7852
- const { agentId, chatId } = request.params;
7853
- await assertCanManage(app.db, memberScope(request), agentId);
7854
- if (!sendToAgent$1(agentId, {
7855
- type: "session:resume",
7856
- chatId
7857
- })) throw new ConflictError("Agent is not connected — session command requires a live connection");
7858
- return reply.status(202).send({
7859
- status: "sent",
7860
- command: "resume",
8235
+ return reply.status(200).send({
7861
8236
  agentId,
7862
- chatId
8237
+ chatId,
8238
+ state: result.state,
8239
+ transitioned: result.transitioned
7863
8240
  });
7864
8241
  });
7865
- /** POST /admin/sessions/agents/:agentId/:chatId/terminate — terminate a session */
8242
+ /** POST /admin/sessions/agents/:agentId/:chatId/terminate — archive; clear events + best-effort WS. */
7866
8243
  app.post("/agents/:agentId/:chatId/terminate", async (request, reply) => {
7867
8244
  const { agentId, chatId } = request.params;
7868
8245
  await assertCanManage(app.db, memberScope(request), agentId);
7869
- if (!sendToAgent$1(agentId, {
7870
- type: "session:terminate",
7871
- chatId
7872
- })) throw new ConflictError("Agent is not connected — session command requires a live connection");
7873
- return reply.status(202).send({
7874
- status: "sent",
7875
- command: "terminate",
8246
+ const member = requireMember(request);
8247
+ const result = await archiveSession(app.db, agentId, chatId, member.organizationId, app.notifier);
8248
+ if (result.transitioned) {
8249
+ clearEvents(app.db, agentId, chatId).catch(() => {});
8250
+ sendToAgent$1(agentId, {
8251
+ type: "session:terminate",
8252
+ chatId
8253
+ });
8254
+ }
8255
+ return reply.status(200).send({
7876
8256
  agentId,
7877
- chatId
8257
+ chatId,
8258
+ state: result.state,
8259
+ transitioned: result.transitioned
7878
8260
  });
7879
8261
  });
7880
8262
  }
@@ -9323,9 +9705,47 @@ function clientWsRoutes(notifier, instanceId) {
9323
9705
  }));
9324
9706
  return;
9325
9707
  }
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);
9708
+ const payloadResult = sessionStateMessageSchema.safeParse(msg);
9709
+ if (!payloadResult.success) {
9710
+ socket.send(JSON.stringify({
9711
+ type: "error",
9712
+ message: "Unsupported session state from client; client upgrade required"
9713
+ }));
9714
+ const rawState = msg.state;
9715
+ app.log.warn({
9716
+ clientId,
9717
+ agentId,
9718
+ rawState
9719
+ }, "session:state rejected — stale client wire");
9720
+ return;
9721
+ }
9722
+ await upsertSessionState(app.db, agentId, payloadResult.data.chatId, payloadResult.data.state, session.organizationId, notifier);
9723
+ } else if (type === "session:reconcile") {
9724
+ const agentId = parsed.data.agentId;
9725
+ if (!agentId || !boundAgents.has(agentId)) {
9726
+ socket.send(JSON.stringify({
9727
+ type: "error",
9728
+ message: "Agent not bound"
9729
+ }));
9730
+ return;
9731
+ }
9732
+ const payloadResult = sessionReconcileRequestSchema.safeParse(msg);
9733
+ if (!payloadResult.success) {
9734
+ socket.send(JSON.stringify({
9735
+ type: "error",
9736
+ message: "Malformed session:reconcile frame"
9737
+ }));
9738
+ return;
9739
+ }
9740
+ const { chatIds } = payloadResult.data;
9741
+ const aliveRows = chatIds.length ? await app.db.select({ chatId: agentChatSessions.chatId }).from(agentChatSessions).where(and(eq(agentChatSessions.agentId, agentId), inArray(agentChatSessions.chatId, chatIds), ne(agentChatSessions.state, "evicted"))) : [];
9742
+ const alive = new Set(aliveRows.map((r) => r.chatId));
9743
+ const staleChatIds = chatIds.filter((id) => !alive.has(id));
9744
+ socket.send(JSON.stringify({
9745
+ type: "session:reconcile:result",
9746
+ agentId,
9747
+ staleChatIds
9748
+ }));
9329
9749
  } else if (type === "runtime:state") {
9330
9750
  const agentId = parsed.data.agentId;
9331
9751
  if (!agentId || !boundAgents.has(agentId)) {
@@ -9744,14 +10164,18 @@ async function getMember(db, id) {
9744
10164
  createdAt: row.createdAt.toISOString()
9745
10165
  };
9746
10166
  }
9747
- async function updateMember(db, id, data) {
9748
- if (!data.role) return getMember(db, id);
9749
- if (data.role === "member") {
9750
- const member = await getMember(db, id);
9751
- if (member.role === "admin") await assertNotLastAdmin(db, member.organizationId, id);
9752
- }
9753
- const [row] = await db.update(members).set({ role: data.role }).where(eq(members.id, id)).returning();
9754
- if (!row) throw new NotFoundError(`Member "${id}" not found`);
10167
+ async function updateMember(db, id, data, callerOrgId) {
10168
+ if (data.role === void 0 && data.displayName === void 0) return getMember(db, id);
10169
+ const current = await getMember(db, id);
10170
+ if (callerOrgId && current.organizationId !== callerOrgId) throw new NotFoundError(`Member "${id}" not found`);
10171
+ if (data.role === "member" && current.role === "admin") await assertNotLastAdmin(db, current.organizationId, id);
10172
+ await db.transaction(async (tx) => {
10173
+ if (data.role !== void 0 && data.role !== current.role) await tx.update(members).set({ role: data.role }).where(eq(members.id, id));
10174
+ if (data.displayName !== void 0 && data.displayName !== current.displayName) {
10175
+ await tx.update(users).set({ displayName: data.displayName }).where(eq(users.id, current.userId));
10176
+ await tx.update(agents).set({ displayName: data.displayName }).where(eq(agents.uuid, current.agentId));
10177
+ }
10178
+ });
9755
10179
  return getMember(db, id);
9756
10180
  }
9757
10181
  async function deleteMember(db, id) {
@@ -9789,7 +10213,8 @@ async function memberRoutes(app) {
9789
10213
  app.patch("/:id", async (request) => {
9790
10214
  requireAdmin(request);
9791
10215
  const body = updateMemberSchema.parse(request.body);
9792
- return updateMember(app.db, request.params.id, body);
10216
+ const m = requireMember(request);
10217
+ return updateMember(app.db, request.params.id, body, m.organizationId);
9793
10218
  });
9794
10219
  app.delete("/:id", async (request, reply) => {
9795
10220
  requireAdmin(request);
@@ -10870,7 +11295,6 @@ function createBackgroundTasks(app, instanceId, adapterManager, kaelRuntime) {
10870
11295
  let heartbeatTimer = null;
10871
11296
  let adapterOutboundTimer = null;
10872
11297
  let kaelOutboundTimer = null;
10873
- let sessionCleanupTimer = null;
10874
11298
  return {
10875
11299
  start() {
10876
11300
  inboxTimer = setInterval(async () => {
@@ -10915,14 +11339,6 @@ function createBackgroundTasks(app, instanceId, adapterManager, kaelRuntime) {
10915
11339
  log.error({ err }, "kael outbound processing failed");
10916
11340
  }
10917
11341
  }, 5e3);
10918
- sessionCleanupTimer = setInterval(async () => {
10919
- try {
10920
- const deleted = await cleanupStaleSessions(app.db);
10921
- if (deleted > 0) log.info({ count: deleted }, "cleaned up stale sessions");
10922
- } catch (err) {
10923
- log.error({ err }, "failed to clean up stale sessions");
10924
- }
10925
- }, 36e5);
10926
11342
  heartbeatInstance(app.db, instanceId).catch((err) => {
10927
11343
  log.error({ err }, "failed initial heartbeat");
10928
11344
  });
@@ -10944,10 +11360,6 @@ function createBackgroundTasks(app, instanceId, adapterManager, kaelRuntime) {
10944
11360
  clearInterval(kaelOutboundTimer);
10945
11361
  kaelOutboundTimer = null;
10946
11362
  }
10947
- if (sessionCleanupTimer) {
10948
- clearInterval(sessionCleanupTimer);
10949
- sessionCleanupTimer = null;
10950
- }
10951
11363
  }
10952
11364
  };
10953
11365
  }
@@ -11564,17 +11976,14 @@ async function buildApp(config) {
11564
11976
  }, { prefix: "/admin/overview" });
11565
11977
  await api.register(async (adminApp) => {
11566
11978
  adminApp.addHook("onRequest", memberAuth);
11567
- adminApp.addHook("onRequest", adminOnly);
11568
11979
  await adminApp.register(adminAdapterRoutes);
11569
11980
  }, { prefix: "/admin/adapters" });
11570
11981
  await api.register(async (adminApp) => {
11571
11982
  adminApp.addHook("onRequest", memberAuth);
11572
- adminApp.addHook("onRequest", adminOnly);
11573
11983
  await adminApp.register(adminAdapterMappingRoutes);
11574
11984
  }, { prefix: "/admin/adapter-mappings" });
11575
11985
  await api.register(async (adminApp) => {
11576
11986
  adminApp.addHook("onRequest", memberAuth);
11577
- adminApp.addHook("onRequest", adminOnly);
11578
11987
  await adminApp.register(adminAdapterStatusRoutes);
11579
11988
  }, { prefix: "/admin/adapters/status" });
11580
11989
  await api.register(async (memberApp) => {