dorkos 0.16.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -71060,7 +71060,7 @@ var SERVER_VERSION = resolveVersion();
71060
71060
  var IS_DEV_BUILD = checkDevBuild(SERVER_VERSION);
71061
71061
  function resolveVersion() {
71062
71062
  if (env.DORKOS_VERSION_OVERRIDE) return env.DORKOS_VERSION_OVERRIDE;
71063
- if (true) return "0.16.0";
71063
+ if (true) return "0.17.0";
71064
71064
  const pkgPath = path4.join(path4.dirname(fileURLToPath2(import.meta.url)), "../../package.json");
71065
71065
  return JSON.parse(readFileSync(pkgPath, "utf-8")).version;
71066
71066
  }
@@ -72143,7 +72143,8 @@ var ResponseContextSchema = z9.object({
72143
72143
  platform: z9.string(),
72144
72144
  maxLength: z9.number().int().optional(),
72145
72145
  supportedFormats: z9.array(z9.string()).optional(),
72146
- instructions: z9.string().optional()
72146
+ instructions: z9.string().optional(),
72147
+ formattingInstructions: z9.string().optional()
72147
72148
  }).openapi("ResponseContext");
72148
72149
  var StandardPayloadSchema = z9.object({
72149
72150
  content: z9.string(),
@@ -74473,6 +74474,20 @@ var TranscriptReader = class {
74473
74474
  const slug = this.getProjectSlug(vaultRoot2);
74474
74475
  return path14.join(os3.homedir(), ".claude", "projects", slug);
74475
74476
  }
74477
+ /**
74478
+ * Check whether a JSONL transcript file exists for the given session ID.
74479
+ * Lightweight stat-only check (no parsing). Skips boundary validation
74480
+ * since the caller is expected to have already validated.
74481
+ */
74482
+ async hasTranscript(vaultRoot2, sessionId) {
74483
+ const filePath = path14.join(this.getTranscriptsDir(vaultRoot2), `${sessionId}.jsonl`);
74484
+ try {
74485
+ await fs8.access(filePath);
74486
+ return true;
74487
+ } catch {
74488
+ return false;
74489
+ }
74490
+ }
74476
74491
  /**
74477
74492
  * List all sessions by scanning SDK JSONL transcript files.
74478
74493
  * Extracts metadata (title, timestamps, preview) from file content and stats.
@@ -75474,9 +75489,9 @@ var NodeFsHandler = class {
75474
75489
  if (this.fsw.closed) {
75475
75490
  return;
75476
75491
  }
75477
- const dirname7 = sysPath.dirname(file);
75492
+ const dirname8 = sysPath.dirname(file);
75478
75493
  const basename4 = sysPath.basename(file);
75479
- const parent = this.fsw._getWatchedDir(dirname7);
75494
+ const parent = this.fsw._getWatchedDir(dirname8);
75480
75495
  let prevStats = stats;
75481
75496
  if (parent.has(basename4))
75482
75497
  return;
@@ -75503,7 +75518,7 @@ var NodeFsHandler = class {
75503
75518
  prevStats = newStats2;
75504
75519
  }
75505
75520
  } catch (error) {
75506
- this.fsw._remove(dirname7, basename4);
75521
+ this.fsw._remove(dirname8, basename4);
75507
75522
  }
75508
75523
  } else if (parent.has(basename4)) {
75509
75524
  const at = newStats.atimeMs;
@@ -77884,6 +77899,11 @@ ${messageOpts.systemPromptAppend}` : baseAppend;
77884
77899
  };
77885
77900
  if (session.hasStarted) {
77886
77901
  sdkOptions.resume = session.sdkSessionId;
77902
+ if (session.sdkSessionId === sessionId) {
77903
+ logger.debug("[sendMessage] resuming with sdkSessionId === sessionId (expected after server restart)", {
77904
+ session: sessionId
77905
+ });
77906
+ }
77887
77907
  }
77888
77908
  const cwdSource = messageOpts?.cwd ? "opts.cwd" : opts.sessionCwd ? "session.cwd" : "default";
77889
77909
  logger.debug("[sendMessage]", {
@@ -78214,8 +78234,9 @@ var ClaudeCodeRuntime = class _ClaudeCodeRuntime {
78214
78234
  if (!session) {
78215
78235
  this.ensureSession(sessionId, {
78216
78236
  permissionMode: opts.permissionMode ?? "default",
78217
- hasStarted: true
78237
+ hasStarted: false
78218
78238
  });
78239
+ this.sessions.get(sessionId).needsTranscriptCheck = true;
78219
78240
  session = this.sessions.get(sessionId);
78220
78241
  }
78221
78242
  if (opts.permissionMode) {
@@ -78241,11 +78262,31 @@ var ClaudeCodeRuntime = class _ClaudeCodeRuntime {
78241
78262
  // ---------------------------------------------------------------------------
78242
78263
  async *sendMessage(sessionId, content3, opts) {
78243
78264
  if (!this.sessions.has(sessionId)) {
78265
+ const effectiveCwd = opts?.cwd ?? this.cwd;
78266
+ const hasTranscript = await this.transcriptReader.hasTranscript(effectiveCwd, sessionId);
78267
+ logger.debug("[sendMessage] auto-creating session", {
78268
+ session: sessionId,
78269
+ hasTranscript,
78270
+ cwd: effectiveCwd
78271
+ });
78244
78272
  this.ensureSession(sessionId, {
78245
78273
  permissionMode: opts?.permissionMode ?? "default",
78246
78274
  cwd: opts?.cwd,
78247
- hasStarted: true
78275
+ hasStarted: hasTranscript
78248
78276
  });
78277
+ } else {
78278
+ const existingSession = this.sessions.get(sessionId);
78279
+ if (existingSession.needsTranscriptCheck) {
78280
+ existingSession.needsTranscriptCheck = false;
78281
+ const effectiveCwd = opts?.cwd || existingSession.cwd || this.cwd;
78282
+ const hasTranscript = await this.transcriptReader.hasTranscript(effectiveCwd, sessionId);
78283
+ if (hasTranscript) {
78284
+ logger.debug("[sendMessage] upgrading hasStarted for existing transcript", {
78285
+ session: sessionId
78286
+ });
78287
+ existingSession.hasStarted = true;
78288
+ }
78289
+ }
78249
78290
  }
78250
78291
  const session = this.sessions.get(sessionId);
78251
78292
  yield* executeSdkQuery(sessionId, content3, session, {
@@ -84670,6 +84711,7 @@ var relayIndex = sqliteTable("relay_index", {
84670
84711
  }).notNull().default("pending"),
84671
84712
  expiresAt: text("expires_at"),
84672
84713
  // was: ttl INTEGER (Unix ms)
84714
+ sender: text("sender"),
84673
84715
  payload: text("payload"),
84674
84716
  metadata: text("metadata"),
84675
84717
  createdAt: text("created_at").notNull()
@@ -86876,7 +86918,8 @@ var SqliteIndex = class {
86876
86918
  endpointHash: message.endpointHash,
86877
86919
  status: message.status,
86878
86920
  createdAt: message.createdAt,
86879
- expiresAt: message.expiresAt
86921
+ expiresAt: message.expiresAt,
86922
+ sender: message.sender ?? null
86880
86923
  }).onConflictDoUpdate({
86881
86924
  target: relayIndex.id,
86882
86925
  set: {
@@ -86884,7 +86927,8 @@ var SqliteIndex = class {
86884
86927
  endpointHash: message.endpointHash,
86885
86928
  status: message.status,
86886
86929
  createdAt: message.createdAt,
86887
- expiresAt: message.expiresAt
86930
+ expiresAt: message.expiresAt,
86931
+ sender: message.sender ?? null
86888
86932
  }
86889
86933
  }).run();
86890
86934
  }
@@ -86931,16 +86975,15 @@ var SqliteIndex = class {
86931
86975
  return rows.map(mapRow);
86932
86976
  }
86933
86977
  /**
86934
- * Count messages sent within a time window by filtering on createdAt.
86935
- * Used by the rate limiter for sliding window log checks.
86978
+ * Count messages from a specific sender within a time window.
86979
+ * Used by the rate limiter for per-sender sliding window log checks.
86936
86980
  *
86937
- * @param sender - Unused (retained for API compatibility). Rate limiting
86938
- * is now done at the RelayCore level before indexing.
86981
+ * @param sender - The sender identity to filter by (e.g. `relay.human.slack.bot`).
86939
86982
  * @param windowStartIso - ISO 8601 timestamp marking the start of the window.
86940
- * @returns The number of messages after the window start.
86983
+ * @returns The number of messages from this sender after the window start.
86941
86984
  */
86942
- countSenderInWindow(_sender, windowStartIso) {
86943
- const rows = this.db.select({ cnt: count() }).from(relayIndex).where(sql`${relayIndex.createdAt} > ${windowStartIso}`).all();
86985
+ countSenderInWindow(sender, windowStartIso) {
86986
+ const rows = this.db.select({ cnt: count() }).from(relayIndex).where(and(sql`${relayIndex.createdAt} > ${windowStartIso}`, eq(relayIndex.sender, sender))).all();
86944
86987
  return rows[0]?.cnt ?? 0;
86945
86988
  }
86946
86989
  /**
@@ -86958,7 +87001,7 @@ var SqliteIndex = class {
86958
87001
  /**
86959
87002
  * Query messages with optional filters and cursor-based pagination.
86960
87003
  *
86961
- * Supports filtering by subject, status, sender (no-op), and endpoint hash.
87004
+ * Supports filtering by subject, status, sender, and endpoint hash.
86962
87005
  * Uses ULID cursor for pagination (messages are sorted by id DESC).
86963
87006
  *
86964
87007
  * @param filters - Optional query filters
@@ -86972,6 +87015,9 @@ var SqliteIndex = class {
86972
87015
  if (filters?.status) {
86973
87016
  conditions.push(eq(relayIndex.status, filters.status));
86974
87017
  }
87018
+ if (filters?.sender) {
87019
+ conditions.push(eq(relayIndex.sender, filters.sender));
87020
+ }
86975
87021
  if (filters?.endpointHash) {
86976
87022
  conditions.push(eq(relayIndex.endpointHash, filters.endpointHash));
86977
87023
  }
@@ -87042,7 +87088,8 @@ var SqliteIndex = class {
87042
87088
  endpointHash: hash,
87043
87089
  status: statusMap[subdir],
87044
87090
  createdAt: envelope.createdAt,
87045
- expiresAt: envelope.budget.ttl ? new Date(envelope.budget.ttl).toISOString() : null
87091
+ expiresAt: envelope.budget.ttl ? new Date(envelope.budget.ttl).toISOString() : null,
87092
+ sender: envelope.from
87046
87093
  });
87047
87094
  rebuildCount++;
87048
87095
  }
@@ -87110,7 +87157,8 @@ function mapRow(row) {
87110
87157
  endpointHash: row.endpointHash,
87111
87158
  status: row.status,
87112
87159
  createdAt: row.createdAt,
87113
- expiresAt: row.expiresAt
87160
+ expiresAt: row.expiresAt,
87161
+ sender: row.sender
87114
87162
  };
87115
87163
  }
87116
87164
  async function listMessageIds(store, hash, subdir) {
@@ -88239,6 +88287,7 @@ var RelayPublishPipeline = class {
88239
88287
  const countInWindow = this.deps.sqliteIndex.countSenderInWindow(options.from, windowStartIso);
88240
88288
  const rateLimitResult = checkRateLimit(options.from, countInWindow, this.rateLimitConfig);
88241
88289
  if (!rateLimitResult.allowed) {
88290
+ this.deps.logger?.warn?.(`publish rate-limited: sender=${options.from}, count=${rateLimitResult.currentCount}/${rateLimitResult.limit} in ${this.rateLimitConfig.windowSecs}s window, subject=${subject}`);
88242
88291
  return {
88243
88292
  messageId: "",
88244
88293
  deliveredTo: 0,
@@ -88262,6 +88311,16 @@ var RelayPublishPipeline = class {
88262
88311
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
88263
88312
  payload
88264
88313
  };
88314
+ this.deps.sqliteIndex.insertMessage({
88315
+ id: messageId,
88316
+ subject,
88317
+ endpointHash: "*",
88318
+ // placeholder — not a Maildir endpoint
88319
+ status: "delivered",
88320
+ createdAt: envelope.createdAt,
88321
+ expiresAt: envelope.budget.ttl ? new Date(envelope.budget.ttl).toISOString() : null,
88322
+ sender: options.from
88323
+ });
88265
88324
  return this.deliverAndFinalize(envelope, subject, options, messageId);
88266
88325
  }
88267
88326
  /**
@@ -88505,7 +88564,8 @@ var RelayCore = class {
88505
88564
  deliveryPipeline: this.deliveryPipeline,
88506
88565
  adapterDelivery,
88507
88566
  adapterRegistry: options?.adapterRegistry,
88508
- traceStore: options?.traceStore
88567
+ traceStore: options?.traceStore,
88568
+ logger: options?.logger
88509
88569
  }, {
88510
88570
  maxHops: options?.maxHops ?? DEFAULT_MAX_HOPS2,
88511
88571
  defaultTtlMs: options?.defaultTtlMs ?? DEFAULT_TTL_MS2,
@@ -88877,6 +88937,32 @@ var BaseRelayAdapter = class {
88877
88937
  return;
88878
88938
  this._status = { ...this._status, state: "connected" };
88879
88939
  }
88940
+ /**
88941
+ * Build a callbacks object for inbound message handling sub-modules.
88942
+ *
88943
+ * Returns bound wrappers around `trackInbound()` and `recordError()` so
88944
+ * sub-modules can update adapter status without holding a reference to the
88945
+ * adapter itself.
88946
+ */
88947
+ makeInboundCallbacks() {
88948
+ return {
88949
+ trackInbound: () => this.trackInbound(),
88950
+ recordError: (err) => this.recordError(err)
88951
+ };
88952
+ }
88953
+ /**
88954
+ * Build a callbacks object for outbound message delivery sub-modules.
88955
+ *
88956
+ * Returns bound wrappers around `trackOutbound()` and `recordError()` so
88957
+ * sub-modules can update adapter status without holding a reference to the
88958
+ * adapter itself.
88959
+ */
88960
+ makeOutboundCallbacks() {
88961
+ return {
88962
+ trackOutbound: () => this.trackOutbound(),
88963
+ recordError: (err) => this.recordError(err)
88964
+ };
88965
+ }
88880
88966
  /** Whether the adapter has been stopped or is stopping. */
88881
88967
  get isStopped() {
88882
88968
  return this._status.state === "disconnected" || this._status.state === "stopping";
@@ -89016,6 +89102,14 @@ var GROUP_SEGMENT = "group";
89016
89102
  var MAX_MESSAGE_LENGTH = 4096;
89017
89103
  var UNKNOWN_SENDER = "unknown";
89018
89104
  var MAX_CONTENT_LENGTH = 32768;
89105
+ var TELEGRAM_FORMATTING_RULES = [
89106
+ "FORMATTING RULES (you MUST follow these):",
89107
+ "- Do NOT use Markdown tables. Telegram cannot render them.",
89108
+ "- For structured data: use bullet points or bold key-value pairs.",
89109
+ "- Use **bold**, _italic_, `code`, ```code blocks```, and [links](url).",
89110
+ "- Telegram supports HTML subset: headings are not supported, use bold instead.",
89111
+ `- Keep responses concise. Messages over ${MAX_MESSAGE_LENGTH} characters are split.`
89112
+ ].join("\n");
89019
89113
  function buildSubject(chatId, isGroup) {
89020
89114
  if (isGroup) {
89021
89115
  return `${SUBJECT_PREFIX}.${GROUP_SEGMENT}.${chatId}`;
@@ -89077,7 +89171,8 @@ async function handleInboundMessage(ctx, relay, callbacks, logger3 = noopLogger)
89077
89171
  platform: "telegram",
89078
89172
  maxLength: MAX_MESSAGE_LENGTH,
89079
89173
  supportedFormats: ["text", "markdown"],
89080
- instructions: `Reply to subject ${subject} to respond to this Telegram message.`
89174
+ instructions: `Reply to subject ${subject} to respond to this Telegram message.`,
89175
+ formattingInstructions: TELEGRAM_FORMATTING_RULES
89081
89176
  },
89082
89177
  platformData: {
89083
89178
  chatId: chat.id,
@@ -89088,10 +89183,16 @@ async function handleInboundMessage(ctx, relay, callbacks, logger3 = noopLogger)
89088
89183
  }
89089
89184
  };
89090
89185
  try {
89091
- await relay.publish(subject, payload, {
89186
+ const result2 = await relay.publish(subject, payload, {
89092
89187
  from: `${SUBJECT_PREFIX}.bot`,
89093
89188
  replyTo: subject
89094
89189
  });
89190
+ if (result2.deliveredTo === 0 && result2.rejected?.length) {
89191
+ const reason = result2.rejected[0]?.reason ?? "unknown";
89192
+ callbacks.recordError(new Error(`Publish rejected: ${reason}`));
89193
+ logger3.warn(`inbound publish rejected for chat ${chat.id}: ${reason}`);
89194
+ return;
89195
+ }
89095
89196
  callbacks.trackInbound();
89096
89197
  logger3.debug(`inbound from ${senderName} in chat ${chat.id}: "${text6.slice(0, 80)}${text6.length > 80 ? "\u2026" : ""}" (${text6.length} chars) \u2192 ${subject}`);
89097
89198
  } catch (err) {
@@ -89502,9 +89603,9 @@ var VFile = class {
89502
89603
  * @returns {undefined}
89503
89604
  * Nothing.
89504
89605
  */
89505
- set dirname(dirname7) {
89606
+ set dirname(dirname8) {
89506
89607
  assertPath(this.basename, "dirname");
89507
- this.path = default2.join(dirname7 || "", this.basename);
89608
+ this.path = default2.join(dirname8 || "", this.basename);
89508
89609
  }
89509
89610
  /**
89510
89611
  * Get the extname (including dot) (example: `'.js'`).
@@ -101527,37 +101628,82 @@ function formatToolDescription(toolName, input) {
101527
101628
  }
101528
101629
  return `wants to use tool \`${toolName}\``;
101529
101630
  }
101631
+ function extractAgentIdFromEnvelope(envelope) {
101632
+ const payload = envelope.payload;
101633
+ if (payload && typeof payload === "object" && "data" in payload) {
101634
+ const data = payload.data;
101635
+ if (data && typeof data === "object" && "agentId" in data) {
101636
+ return data.agentId;
101637
+ }
101638
+ }
101639
+ return void 0;
101640
+ }
101641
+ function extractSessionIdFromEnvelope(envelope) {
101642
+ const payload = envelope.payload;
101643
+ if (payload && typeof payload === "object" && "data" in payload) {
101644
+ const data = payload.data;
101645
+ if (data && typeof data === "object" && "ccaSessionKey" in data) {
101646
+ return data.ccaSessionKey;
101647
+ }
101648
+ }
101649
+ return void 0;
101650
+ }
101651
+ function markdownToTelegramHtml(md) {
101652
+ let html2 = md;
101653
+ html2 = html2.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
101654
+ html2 = html2.replace(/```(\w*)\n([\s\S]*?)```/g, (_m, lang, code3) => {
101655
+ const cls = lang ? ` class="language-${lang}"` : "";
101656
+ return `<pre><code${cls}>${code3.trimEnd()}</code></pre>`;
101657
+ });
101658
+ html2 = html2.replace(/`([^`]+)`/g, "<code>$1</code>");
101659
+ html2 = html2.replace(/\*\*(.+?)\*\*/g, "<b>$1</b>");
101660
+ html2 = html2.replace(/\*(.+?)\*/g, "<i>$1</i>");
101661
+ html2 = html2.replace(/~~(.+?)~~/g, "<s>$1</s>");
101662
+ html2 = html2.replace(/\[(.+?)\]\((.+?)\)/g, '<a href="$2">$1</a>');
101663
+ html2 = html2.replace(/^#{1,6}\s+(.+)$/gm, "<b>$1</b>");
101664
+ return html2;
101665
+ }
101530
101666
  function formatForPlatform(content3, platform2) {
101531
101667
  switch (platform2) {
101532
101668
  case "slack":
101533
101669
  return slackifyMarkdown(content3);
101534
101670
  case "telegram":
101535
- return content3;
101671
+ return markdownToTelegramHtml(content3);
101536
101672
  case "plain":
101537
101673
  return content3.replace(/\*\*(.+?)\*\*/g, "$1").replace(/\*(.+?)\*/g, "$1").replace(/__(.+?)__/g, "$1").replace(/_(.+?)_/g, "$1").replace(/~~(.+?)~~/g, "$1").replace(/`{3}[\s\S]*?`{3}/g, (m3) => m3.replace(/`{3}\w*\n?/g, "").trim()).replace(/`(.+?)`/g, "$1").replace(/^#{1,6}\s+/gm, "").replace(/^[*-]\s+/gm, "- ").replace(/^>\s+/gm, "").replace(/\[(.+?)\]\(.+?\)/g, "$1");
101538
101674
  }
101539
101675
  }
101540
101676
 
101677
+ // ../relay/dist/adapters/telegram/stream-api.js
101678
+ async function sendMessageDraft(bot, chatId, text6) {
101679
+ await bot.api.sendMessageDraft(chatId, text6);
101680
+ }
101681
+
101541
101682
  // ../relay/dist/adapters/telegram/outbound.js
101542
101683
  var TELEGRAM_TYPING_ACTION = "typing";
101543
- var typingIntervals = /* @__PURE__ */ new Map();
101544
101684
  var TYPING_REFRESH_MS = 4e3;
101545
101685
  var DRAFT_UPDATE_INTERVAL_MS = 200;
101546
101686
  var BUFFER_TTL_MS = 5 * 60 * 1e3;
101547
- var lastDraftUpdate = /* @__PURE__ */ new Map();
101548
- var callbackIdMap = /* @__PURE__ */ new Map();
101549
101687
  var CALLBACK_ID_TTL_MS = 15 * 60 * 1e3;
101550
- var pendingApprovalTimeouts = /* @__PURE__ */ new Map();
101551
- function clearApprovalTimeout(shortKey) {
101552
- const timer = pendingApprovalTimeouts.get(shortKey);
101688
+ function createTelegramOutboundState() {
101689
+ return {
101690
+ typingIntervals: /* @__PURE__ */ new Map(),
101691
+ lastDraftUpdate: /* @__PURE__ */ new Map(),
101692
+ callbackIdMap: /* @__PURE__ */ new Map(),
101693
+ pendingApprovalTimeouts: /* @__PURE__ */ new Map()
101694
+ };
101695
+ }
101696
+ function clearApprovalTimeout(state, shortKey) {
101697
+ const timer = state.pendingApprovalTimeouts.get(shortKey);
101553
101698
  if (timer) {
101554
101699
  clearTimeout(timer);
101555
- pendingApprovalTimeouts.delete(shortKey);
101700
+ state.pendingApprovalTimeouts.delete(shortKey);
101556
101701
  }
101557
101702
  }
101558
101703
  async function sendAndTrack(bot, chatId, text6, startTime, callbacks) {
101559
101704
  try {
101560
- await bot.api.sendMessage(chatId, text6);
101705
+ const html2 = formatForPlatform(text6, "telegram");
101706
+ await bot.api.sendMessage(chatId, html2, { parse_mode: "HTML" });
101561
101707
  callbacks.trackOutbound();
101562
101708
  return { success: true, durationMs: Date.now() - startTime };
101563
101709
  } catch (err) {
@@ -101570,7 +101716,7 @@ async function sendAndTrack(bot, chatId, text6, startTime, callbacks) {
101570
101716
  }
101571
101717
  }
101572
101718
  async function deliverMessage(opts) {
101573
- const { adapterId, subject, envelope, bot, responseBuffers, callbacks, streaming, logger: logger3 = noopLogger } = opts;
101719
+ const { adapterId, subject, envelope, bot, responseBuffers, state, callbacks, streaming, logger: logger3 = noopLogger } = opts;
101574
101720
  const startTime = Date.now();
101575
101721
  if (envelope.from.startsWith(SUBJECT_PREFIX)) {
101576
101722
  logger3.debug("deliver: echo prevention \u2014 skipping self-originated message");
@@ -101595,7 +101741,7 @@ async function deliverMessage(opts) {
101595
101741
  for (const [id, buf] of responseBuffers) {
101596
101742
  if (now - buf.startedAt > BUFFER_TTL_MS) {
101597
101743
  responseBuffers.delete(id);
101598
- lastDraftUpdate.delete(id);
101744
+ state.lastDraftUpdate.delete(id);
101599
101745
  logger3.warn(`buffer: reaped stale buffer for chat ${id} (age: ${Math.round((now - buf.startedAt) / 1e3)}s)`);
101600
101746
  }
101601
101747
  }
@@ -101610,12 +101756,12 @@ async function deliverMessage(opts) {
101610
101756
  startedAt: existing?.startedAt ?? Date.now()
101611
101757
  });
101612
101758
  if (streaming && chatId > 0) {
101613
- const lastUpdate = lastDraftUpdate.get(chatId) ?? 0;
101759
+ const lastUpdate = state.lastDraftUpdate.get(chatId) ?? 0;
101614
101760
  if (Date.now() - lastUpdate >= DRAFT_UPDATE_INTERVAL_MS) {
101615
- lastDraftUpdate.set(chatId, Date.now());
101761
+ state.lastDraftUpdate.set(chatId, Date.now());
101616
101762
  logger3.debug(`stream: sendMessageDraft to chat ${chatId} (${responseBuffers.get(chatId).text.length} chars)`);
101617
101763
  try {
101618
- await bot.api.sendMessageDraft(chatId, responseBuffers.get(chatId).text);
101764
+ await sendMessageDraft(bot, chatId, responseBuffers.get(chatId).text);
101619
101765
  } catch {
101620
101766
  }
101621
101767
  }
@@ -101627,7 +101773,7 @@ async function deliverMessage(opts) {
101627
101773
  logger3.debug(`deliver: error to chat ${chatId}: "${errorMsg.slice(0, 100)}"`);
101628
101774
  const buffered = responseBuffers.get(chatId)?.text ?? "";
101629
101775
  responseBuffers.delete(chatId);
101630
- lastDraftUpdate.delete(chatId);
101776
+ state.lastDraftUpdate.delete(chatId);
101631
101777
  const text7 = buffered ? truncateText(`${buffered}
101632
101778
 
101633
101779
  [Error: ${errorMsg}]`, MAX_MESSAGE_LENGTH) : truncateText(`[Error: ${errorMsg}]`, MAX_MESSAGE_LENGTH);
@@ -101637,7 +101783,7 @@ async function deliverMessage(opts) {
101637
101783
  const buffered = responseBuffers.get(chatId);
101638
101784
  logger3.debug(`deliver: done for chat ${chatId} (buffered: ${buffered ? `${buffered.text.length} chars` : "empty"})`);
101639
101785
  responseBuffers.delete(chatId);
101640
- lastDraftUpdate.delete(chatId);
101786
+ state.lastDraftUpdate.delete(chatId);
101641
101787
  if (buffered) {
101642
101788
  return sendAndTrack(bot, chatId, truncateText(buffered.text, MAX_MESSAGE_LENGTH), startTime, callbacks);
101643
101789
  }
@@ -101650,10 +101796,10 @@ async function deliverMessage(opts) {
101650
101796
  const buffered = responseBuffers.get(chatId);
101651
101797
  if (buffered?.text) {
101652
101798
  responseBuffers.delete(chatId);
101653
- lastDraftUpdate.delete(chatId);
101799
+ state.lastDraftUpdate.delete(chatId);
101654
101800
  await sendAndTrack(bot, chatId, truncateText(buffered.text, MAX_MESSAGE_LENGTH), startTime, callbacks);
101655
101801
  }
101656
- return handleApprovalRequired(bot, chatId, data, envelope, callbacks, startTime);
101802
+ return handleApprovalRequired(bot, chatId, data, envelope, state, callbacks, startTime);
101657
101803
  }
101658
101804
  }
101659
101805
  logger3.debug(`deliver: dropping stream event '${eventType}' (whitelist)`);
@@ -101664,14 +101810,14 @@ async function deliverMessage(opts) {
101664
101810
  logger3.debug(`deliver: standard payload to chat ${chatId} (${text6.length} chars)`);
101665
101811
  return sendAndTrack(bot, chatId, text6, startTime, callbacks);
101666
101812
  }
101667
- async function handleTypingSignal(bot, subject, state) {
101813
+ async function handleTypingSignal(bot, subject, outboundState, signalState) {
101668
101814
  if (!bot)
101669
101815
  return;
101670
101816
  const chatId = extractChatId(subject);
101671
101817
  if (chatId === null)
101672
101818
  return;
101673
- if (state === "active") {
101674
- clearTypingInterval(chatId);
101819
+ if (signalState === "active") {
101820
+ clearTypingInterval(outboundState, chatId);
101675
101821
  try {
101676
101822
  await bot.api.sendChatAction(chatId, TELEGRAM_TYPING_ACTION);
101677
101823
  } catch {
@@ -101680,43 +101826,33 @@ async function handleTypingSignal(bot, subject, state) {
101680
101826
  try {
101681
101827
  await bot.api.sendChatAction(chatId, TELEGRAM_TYPING_ACTION);
101682
101828
  } catch {
101683
- clearTypingInterval(chatId);
101829
+ clearTypingInterval(outboundState, chatId);
101684
101830
  }
101685
101831
  }, TYPING_REFRESH_MS);
101686
- typingIntervals.set(chatId, intervalId);
101832
+ outboundState.typingIntervals.set(chatId, intervalId);
101687
101833
  } else {
101688
- clearTypingInterval(chatId);
101834
+ clearTypingInterval(outboundState, chatId);
101689
101835
  }
101690
101836
  }
101691
- function clearTypingInterval(chatId) {
101692
- const existing = typingIntervals.get(chatId);
101837
+ function clearTypingInterval(state, chatId) {
101838
+ const existing = state.typingIntervals.get(chatId);
101693
101839
  if (existing !== void 0) {
101694
101840
  clearInterval(existing);
101695
- typingIntervals.delete(chatId);
101841
+ state.typingIntervals.delete(chatId);
101696
101842
  }
101697
101843
  }
101698
- function clearAllTypingIntervals() {
101699
- for (const interval of typingIntervals.values())
101844
+ function clearAllTypingIntervals(state) {
101845
+ for (const interval of state.typingIntervals.values())
101700
101846
  clearInterval(interval);
101701
- typingIntervals.clear();
101702
- lastDraftUpdate.clear();
101847
+ state.typingIntervals.clear();
101848
+ state.lastDraftUpdate.clear();
101703
101849
  }
101704
- function extractAgentIdFromEnvelope(envelope) {
101705
- const payload = envelope.payload;
101706
- const data = payload?.data;
101707
- return data?.agentId ?? "unknown";
101708
- }
101709
- function extractSessionIdFromEnvelope(envelope) {
101710
- const payload = envelope.payload;
101711
- const data = payload?.data;
101712
- return data?.ccaSessionKey ?? "unknown";
101713
- }
101714
- async function handleApprovalRequired(bot, chatId, data, envelope, callbacks, startTime) {
101715
- const agentId = extractAgentIdFromEnvelope(envelope);
101716
- const sessionId = extractSessionIdFromEnvelope(envelope);
101850
+ async function handleApprovalRequired(bot, chatId, data, envelope, state, callbacks, startTime) {
101851
+ const agentId = extractAgentIdFromEnvelope(envelope) ?? "unknown";
101852
+ const sessionId = extractSessionIdFromEnvelope(envelope) ?? "unknown";
101717
101853
  const shortKey = randomBytes(6).toString("hex");
101718
- callbackIdMap.set(shortKey, { toolCallId: data.toolCallId, sessionId, agentId });
101719
- setTimeout(() => callbackIdMap.delete(shortKey), CALLBACK_ID_TTL_MS);
101854
+ state.callbackIdMap.set(shortKey, { toolCallId: data.toolCallId, sessionId, agentId });
101855
+ setTimeout(() => state.callbackIdMap.delete(shortKey), CALLBACK_ID_TTL_MS);
101720
101856
  const toolDescription = formatToolDescription(data.toolName, data.input);
101721
101857
  const inputPreview = truncateText(data.input, 400);
101722
101858
  const messageText = `*Tool Approval Required*
@@ -101739,15 +101875,15 @@ ${inputPreview}
101739
101875
  });
101740
101876
  if (data.timeoutMs && data.timeoutMs > 0) {
101741
101877
  const timer = setTimeout(async () => {
101742
- pendingApprovalTimeouts.delete(shortKey);
101743
- callbackIdMap.delete(shortKey);
101878
+ state.pendingApprovalTimeouts.delete(shortKey);
101879
+ state.callbackIdMap.delete(shortKey);
101744
101880
  try {
101745
101881
  await bot.api.editMessageText(chatId, sent.message_id, `\u23F0 *Tool Approval Timed Out*
101746
101882
  ~~\`${data.toolName}\`~~ ${toolDescription}`, { parse_mode: "Markdown" });
101747
101883
  } catch {
101748
101884
  }
101749
101885
  }, data.timeoutMs);
101750
- pendingApprovalTimeouts.set(shortKey, timer);
101886
+ state.pendingApprovalTimeouts.set(shortKey, timer);
101751
101887
  }
101752
101888
  callbacks.trackOutbound();
101753
101889
  return { success: true, durationMs: Date.now() - startTime };
@@ -101913,6 +102049,8 @@ var TelegramAdapter = class _TelegramAdapter extends BaseRelayAdapter {
101913
102049
  reconnectAttempts = 0;
101914
102050
  reconnectTimer = null;
101915
102051
  responseBuffers = /* @__PURE__ */ new Map();
102052
+ /** Instance-scoped outbound state — prevents cross-adapter leakage when multiInstance: true. */
102053
+ outboundState = createTelegramOutboundState();
101916
102054
  constructor(id, config, displayName = "Telegram") {
101917
102055
  super(id, SUBJECT_PREFIX, displayName);
101918
102056
  this.config = config;
@@ -101935,14 +102073,14 @@ var TelegramAdapter = class _TelegramAdapter extends BaseRelayAdapter {
101935
102073
  bot.on("callback_query:data", async (ctx) => {
101936
102074
  try {
101937
102075
  const data = JSON.parse(ctx.callbackQuery.data);
101938
- const entry = callbackIdMap.get(data.k);
102076
+ const entry = this.outboundState.callbackIdMap.get(data.k);
101939
102077
  if (!entry) {
101940
102078
  await ctx.answerCallbackQuery({ text: "This approval has expired." });
101941
102079
  return;
101942
102080
  }
101943
102081
  const approved = data.a === 1;
101944
- callbackIdMap.delete(data.k);
101945
- clearApprovalTimeout(data.k);
102082
+ this.outboundState.callbackIdMap.delete(data.k);
102083
+ clearApprovalTimeout(this.outboundState, data.k);
101946
102084
  const opts = { from: `telegram:${ctx.from.id}` };
101947
102085
  await relay.publish(`relay.system.approval.${entry.agentId}`, {
101948
102086
  type: "approval_response",
@@ -101968,7 +102106,7 @@ var TelegramAdapter = class _TelegramAdapter extends BaseRelayAdapter {
101968
102106
  this.bot = bot;
101969
102107
  this.signalUnsub = relay.onSignal(`${SUBJECT_PREFIX}.>`, (subject, signal) => {
101970
102108
  if (signal.type === "typing")
101971
- void handleTypingSignal(this.bot, subject, signal.state);
102109
+ void handleTypingSignal(this.bot, subject, this.outboundState, signal.state);
101972
102110
  });
101973
102111
  if (this.config.mode === "webhook") {
101974
102112
  this.webhookServer = await startWebhookMode(bot, this.id, this.config.webhookUrl, this.config.webhookPort, this.config.webhookSecret);
@@ -101986,7 +102124,11 @@ var TelegramAdapter = class _TelegramAdapter extends BaseRelayAdapter {
101986
102124
  this.signalUnsub();
101987
102125
  this.signalUnsub = null;
101988
102126
  }
101989
- clearAllTypingIntervals();
102127
+ clearAllTypingIntervals(this.outboundState);
102128
+ for (const timer of this.outboundState.pendingApprovalTimeouts.values())
102129
+ clearTimeout(timer);
102130
+ this.outboundState.pendingApprovalTimeouts.clear();
102131
+ this.outboundState.callbackIdMap.clear();
101990
102132
  if (this.bot) {
101991
102133
  if (this.config.mode === "webhook") {
101992
102134
  try {
@@ -102015,26 +102157,13 @@ var TelegramAdapter = class _TelegramAdapter extends BaseRelayAdapter {
102015
102157
  envelope,
102016
102158
  bot: this.bot,
102017
102159
  responseBuffers: this.responseBuffers,
102160
+ state: this.outboundState,
102018
102161
  callbacks: this.makeOutboundCallbacks(),
102019
102162
  streaming: this.config.streaming ?? true,
102020
102163
  logger: this.logger
102021
102164
  });
102022
102165
  }
102023
102166
  // --- Private helpers ---
102024
- /** Build callbacks for inbound message handling. */
102025
- makeInboundCallbacks() {
102026
- return {
102027
- trackInbound: () => this.trackInbound(),
102028
- recordError: (err) => this.recordError(err)
102029
- };
102030
- }
102031
- /** Build callbacks for outbound message delivery. */
102032
- makeOutboundCallbacks() {
102033
- return {
102034
- trackOutbound: () => this.trackOutbound(),
102035
- recordError: (err) => this.recordError(err)
102036
- };
102037
- }
102038
102167
  /** Start grammy bot in long-polling mode with eager token validation. */
102039
102168
  async startPollingMode(bot) {
102040
102169
  await bot.init();
@@ -102153,20 +102282,11 @@ Leave empty if no custom headers are needed.`
102153
102282
  }
102154
102283
  ]
102155
102284
  };
102156
- var WebhookAdapter = class {
102157
- id;
102158
- subjectPrefix;
102159
- displayName;
102285
+ var WebhookAdapter = class extends BaseRelayAdapter {
102160
102286
  config;
102161
- relay = null;
102162
102287
  /** Tracks nonces to prevent replay attacks. Maps `{adapterId}:{nonce}` -> expiresAt timestamp. */
102163
102288
  nonceMap = /* @__PURE__ */ new Map();
102164
102289
  nonceInterval = null;
102165
- status = {
102166
- state: "disconnected",
102167
- messageCount: { inbound: 0, outbound: 0 },
102168
- errorCount: 0
102169
- };
102170
102290
  /**
102171
102291
  * Create a new WebhookAdapter instance.
102172
102292
  *
@@ -102175,50 +102295,33 @@ var WebhookAdapter = class {
102175
102295
  * @param displayName - Human-readable name (defaults to `Webhook ({id})`)
102176
102296
  */
102177
102297
  constructor(id, config, displayName) {
102178
- this.id = id;
102298
+ super(id, config.inbound.subject, displayName ?? `Webhook (${id})`);
102179
102299
  this.config = config;
102180
- this.subjectPrefix = config.inbound.subject;
102181
- this.displayName = displayName ?? `Webhook (${id})`;
102182
102300
  }
102183
102301
  /**
102184
- * Start the adapter store the relay publisher and begin nonce pruning.
102302
+ * Connect hook — begin nonce pruning interval.
102185
102303
  *
102186
102304
  * This adapter has no external connection to establish; it relies on the
102187
102305
  * Express route calling `handleInbound()` for inbound messages. The nonce
102188
102306
  * pruning interval is started here to prevent unbounded memory growth.
102307
+ * The relay publisher reference is stored by {@link BaseRelayAdapter.start}.
102189
102308
  *
102190
- * Idempotent: safe to call multiple times.
102191
- *
102192
- * @param relay - The RelayPublisher to publish inbound messages to
102309
+ * @param _relay - The RelayPublisher (stored by base class; unused here)
102193
102310
  */
102194
- async start(relay) {
102195
- if (this.status.state === "connected")
102196
- return;
102197
- this.relay = relay;
102198
- this.status = {
102199
- ...this.status,
102200
- state: "connected",
102201
- startedAt: (/* @__PURE__ */ new Date()).toISOString()
102202
- };
102311
+ async _start(_relay) {
102203
102312
  this.nonceInterval = setInterval(() => {
102204
102313
  this.pruneExpiredNonces();
102205
102314
  }, NONCE_PRUNE_INTERVAL_MS);
102206
102315
  }
102207
102316
  /**
102208
- * Stop the adapter — clear nonce state and pruning interval.
102209
- *
102210
- * Idempotent: safe to call multiple times.
102317
+ * Disconnect hook — clear nonce state and pruning interval.
102211
102318
  */
102212
- async stop() {
102213
- if (this.status.state === "disconnected")
102214
- return;
102319
+ async _stop() {
102215
102320
  if (this.nonceInterval !== null) {
102216
102321
  clearInterval(this.nonceInterval);
102217
102322
  this.nonceInterval = null;
102218
102323
  }
102219
102324
  this.nonceMap.clear();
102220
- this.relay = null;
102221
- this.status = { ...this.status, state: "disconnected" };
102222
102325
  }
102223
102326
  /**
102224
102327
  * Handle an inbound webhook HTTP POST request.
@@ -102261,24 +102364,18 @@ var WebhookAdapter = class {
102261
102364
  metadata: { platform: "webhook", adapterId: this.id, nonce },
102262
102365
  responseContext: { platform: "webhook" }
102263
102366
  };
102264
- await this.relay.publish(this.config.inbound.subject, payload, {
102367
+ const result2 = await this.relay.publish(this.config.inbound.subject, payload, {
102265
102368
  from: `relay.webhook.${this.id}`
102266
102369
  });
102267
- this.status = {
102268
- ...this.status,
102269
- messageCount: {
102270
- ...this.status.messageCount,
102271
- inbound: this.status.messageCount.inbound + 1
102272
- }
102273
- };
102370
+ if (result2.deliveredTo === 0 && result2.rejected?.length) {
102371
+ const reason = result2.rejected[0]?.reason ?? "unknown";
102372
+ this.recordError(new Error(`Publish rejected: ${reason}`));
102373
+ return { ok: false, error: `Publish rejected: ${reason}` };
102374
+ }
102375
+ this.trackInbound();
102274
102376
  return { ok: true };
102275
102377
  } catch (err) {
102276
- this.status = {
102277
- ...this.status,
102278
- errorCount: this.status.errorCount + 1,
102279
- lastError: err instanceof Error ? err.message : String(err),
102280
- lastErrorAt: (/* @__PURE__ */ new Date()).toISOString()
102281
- };
102378
+ this.recordError(err);
102282
102379
  return { ok: false, error: "Publish failed" };
102283
102380
  }
102284
102381
  }
@@ -102313,41 +102410,17 @@ var WebhookAdapter = class {
102313
102410
  });
102314
102411
  if (!response.ok) {
102315
102412
  const error = `Outbound delivery failed: HTTP ${response.status}`;
102316
- this.status = {
102317
- ...this.status,
102318
- errorCount: this.status.errorCount + 1,
102319
- lastError: error,
102320
- lastErrorAt: (/* @__PURE__ */ new Date()).toISOString()
102321
- };
102413
+ this.recordError(error);
102322
102414
  return { success: false, error, durationMs: Date.now() - startTime };
102323
102415
  }
102324
- this.status = {
102325
- ...this.status,
102326
- messageCount: {
102327
- ...this.status.messageCount,
102328
- outbound: this.status.messageCount.outbound + 1
102329
- }
102330
- };
102416
+ this.trackOutbound();
102331
102417
  return { success: true, durationMs: Date.now() - startTime };
102332
102418
  } catch (err) {
102333
102419
  const error = err instanceof Error ? err.message : String(err);
102334
- this.status = {
102335
- ...this.status,
102336
- errorCount: this.status.errorCount + 1,
102337
- lastError: error,
102338
- lastErrorAt: (/* @__PURE__ */ new Date()).toISOString()
102339
- };
102420
+ this.recordError(err);
102340
102421
  return { success: false, error, durationMs: Date.now() - startTime };
102341
102422
  }
102342
102423
  }
102343
- /**
102344
- * Get the current adapter status.
102345
- *
102346
- * Returns a shallow copy to prevent external mutation of internal state.
102347
- */
102348
- getStatus() {
102349
- return { ...this.status };
102350
- }
102351
102424
  /** Remove expired nonces from the in-memory map. */
102352
102425
  pruneExpiredNonces() {
102353
102426
  const now = Date.now();
@@ -102391,6 +102464,15 @@ var SUBJECT_PREFIX2 = "relay.human.slack";
102391
102464
  var GROUP_SEGMENT2 = "group";
102392
102465
  var MAX_MESSAGE_LENGTH2 = 4e3;
102393
102466
  var MAX_CONTENT_LENGTH2 = 32768;
102467
+ var SLACK_FORMATTING_RULES = [
102468
+ "FORMATTING RULES (you MUST follow these):",
102469
+ "- Do NOT use Markdown tables (| col | col |). Slack cannot render them.",
102470
+ "- For structured data: use bullet points, numbered lists, or bold key-value pairs.",
102471
+ '- Example: instead of a table, write "*Name*: Alice\\n*Role*: Engineer"',
102472
+ "- Use *bold* (single asterisk), _italic_ (underscore), `code`, ```code blocks```.",
102473
+ "- Do NOT use ## headings \u2014 Slack ignores them. Use *bold text* for section titles.",
102474
+ `- Keep responses concise. Slack messages over ${MAX_MESSAGE_LENGTH2} characters are truncated.`
102475
+ ].join("\n");
102394
102476
  var SKIP_SUBTYPES = /* @__PURE__ */ new Set([
102395
102477
  "channel_join",
102396
102478
  "channel_leave",
@@ -102483,7 +102565,7 @@ function clearCaches() {
102483
102565
  userNameCache.clear();
102484
102566
  channelNameCache.clear();
102485
102567
  }
102486
- async function handleInboundMessage2(event, client, relay, botUserId, callbacks, logger3 = noopLogger) {
102568
+ async function handleInboundMessage2(event, client, relay, botUserId, callbacks, logger3 = noopLogger, typingIndicator = "none", pendingReactions) {
102487
102569
  if (event.user === botUserId) {
102488
102570
  logger3.debug(`inbound skipped: echo (own user ${botUserId})`);
102489
102571
  return;
@@ -102514,7 +102596,8 @@ async function handleInboundMessage2(event, client, relay, botUserId, callbacks,
102514
102596
  platform: "slack",
102515
102597
  maxLength: MAX_MESSAGE_LENGTH2,
102516
102598
  supportedFormats: ["text", "mrkdwn"],
102517
- instructions: `Reply to subject ${subject} to respond to this Slack message.`
102599
+ instructions: `Reply to subject ${subject} to respond to this Slack message.`,
102600
+ formattingInstructions: SLACK_FORMATTING_RULES
102518
102601
  },
102519
102602
  platformData: {
102520
102603
  channelId: event.channel,
@@ -102525,47 +102608,67 @@ async function handleInboundMessage2(event, client, relay, botUserId, callbacks,
102525
102608
  }
102526
102609
  };
102527
102610
  try {
102528
- await relay.publish(subject, payload, {
102611
+ const result2 = await relay.publish(subject, payload, {
102529
102612
  from: `${SUBJECT_PREFIX2}.bot`,
102530
102613
  replyTo: subject
102531
102614
  });
102615
+ if (result2.deliveredTo === 0 && result2.rejected?.length) {
102616
+ const reason = result2.rejected[0]?.reason ?? "unknown";
102617
+ callbacks.recordError(new Error(`Publish rejected: ${reason}`));
102618
+ logger3.warn(`inbound publish rejected for ${event.channel}: ${reason}`);
102619
+ return;
102620
+ }
102532
102621
  callbacks.trackInbound();
102533
102622
  logger3.debug(`inbound from ${senderName} in ${event.channel}: "${content3.slice(0, 80)}${content3.length > 80 ? "\u2026" : ""}" (${content3.length} chars) \u2192 ${subject}`);
102623
+ if (typingIndicator === "reaction") {
102624
+ if (pendingReactions) {
102625
+ const queue2 = pendingReactions.get(event.channel) ?? [];
102626
+ queue2.push(event.ts);
102627
+ pendingReactions.set(event.channel, queue2);
102628
+ }
102629
+ client.reactions.add({ channel: event.channel, name: "hourglass_flowing_sand", timestamp: event.ts }).then(() => {
102630
+ logger3.debug(`inbound: added typing reaction to ${event.channel}:${event.ts}`);
102631
+ }).catch((err) => {
102632
+ if (pendingReactions) {
102633
+ const queue2 = pendingReactions.get(event.channel);
102634
+ if (queue2) {
102635
+ const idx = queue2.indexOf(event.ts);
102636
+ if (idx !== -1)
102637
+ queue2.splice(idx, 1);
102638
+ if (queue2.length === 0)
102639
+ pendingReactions.delete(event.channel);
102640
+ }
102641
+ }
102642
+ logger3.warn(`inbound: failed to add typing reaction to ${event.channel}:${event.ts}: ${err instanceof Error ? err.message : String(err)}`);
102643
+ });
102644
+ }
102534
102645
  } catch (err) {
102535
102646
  callbacks.recordError(err);
102536
102647
  logger3.warn(`inbound publish failed for ${event.channel}:`, err instanceof Error ? err.message : String(err));
102537
102648
  }
102538
102649
  }
102539
102650
 
102540
- // ../relay/dist/adapters/slack/outbound.js
102651
+ // ../relay/dist/adapters/slack/stream.js
102541
102652
  import { randomUUID as randomUUID4 } from "node:crypto";
102542
- var STREAM_UPDATE_INTERVAL_MS = 1e3;
102543
- var STREAM_TTL_MS = 5 * 60 * 1e3;
102544
- var pendingApprovalTimeouts2 = /* @__PURE__ */ new Map();
102545
- function clearApprovalTimeout2(toolCallId) {
102546
- const entry = pendingApprovalTimeouts2.get(toolCallId);
102547
- if (entry) {
102548
- clearTimeout(entry.timer);
102549
- pendingApprovalTimeouts2.delete(toolCallId);
102550
- }
102653
+
102654
+ // ../relay/dist/adapters/slack/stream-api.js
102655
+ function asStreamClient(client) {
102656
+ return client;
102551
102657
  }
102552
- function streamKey(channelId, threadTs) {
102553
- return threadTs ? `${channelId}:${threadTs}` : channelId;
102658
+ async function startStream(client, channel, threadTs) {
102659
+ const result2 = await asStreamClient(client).chat.startStream({ channel, thread_ts: threadTs });
102660
+ return result2.stream_id ?? "";
102554
102661
  }
102555
- function resolveThreadTs(envelope) {
102556
- const payload = envelope.payload;
102557
- if (payload === null || typeof payload !== "object")
102558
- return void 0;
102559
- const obj = payload;
102560
- const pd = obj.platformData;
102561
- if (!pd)
102562
- return void 0;
102563
- if (typeof pd.threadTs === "string" && pd.threadTs)
102564
- return pd.threadTs;
102565
- if (typeof pd.ts === "string" && pd.ts)
102566
- return pd.ts;
102567
- return void 0;
102662
+ async function appendStream(client, streamId, text6) {
102663
+ await asStreamClient(client).chat.appendStream({ stream_id: streamId, text: text6 });
102568
102664
  }
102665
+ async function stopStream(client, streamId) {
102666
+ await asStreamClient(client).chat.stopStream({ stream_id: streamId });
102667
+ }
102668
+
102669
+ // ../relay/dist/adapters/slack/stream.js
102670
+ var STREAM_UPDATE_INTERVAL_MS = 1e3;
102671
+ var STREAM_TTL_MS = 5 * 60 * 1e3;
102569
102672
  async function wrapSlackCall(fn, callbacks, startTime, trackDelivery = false) {
102570
102673
  try {
102571
102674
  await fn();
@@ -102581,28 +102684,50 @@ async function wrapSlackCall(fn, callbacks, startTime, trackDelivery = false) {
102581
102684
  };
102582
102685
  }
102583
102686
  }
102584
- function addTypingReaction(client, channelId, threadTs, typingIndicator) {
102687
+ function buildStreamKey(channelId, streamKeyTs) {
102688
+ return streamKeyTs ? `${channelId}:${streamKeyTs}` : channelId;
102689
+ }
102690
+ function addTypingReaction(client, channelId, threadTs, typingIndicator, logger3) {
102585
102691
  if (typingIndicator !== "reaction" || !threadTs)
102586
102692
  return;
102587
- void client.reactions.add({
102588
- channel: channelId,
102589
- name: "hourglass_flowing_sand",
102590
- timestamp: threadTs
102591
- }).catch(() => {
102693
+ void client.reactions.add({ channel: channelId, name: "hourglass_flowing_sand", timestamp: threadTs }).catch((err) => {
102694
+ const msg = err instanceof Error ? err.message : String(err);
102695
+ if (!msg.includes("already_reacted")) {
102696
+ logger3?.warn(`stream: failed to add typing reaction to ${channelId}:${threadTs}: ${msg}`);
102697
+ }
102592
102698
  });
102593
102699
  }
102594
- function removeTypingReaction(client, channelId, threadTs, typingIndicator) {
102700
+ function removeTypingReaction(client, channelId, threadTs, typingIndicator, logger3) {
102595
102701
  if (typingIndicator !== "reaction" || !threadTs)
102596
102702
  return;
102597
- void client.reactions.remove({
102598
- channel: channelId,
102599
- name: "hourglass_flowing_sand",
102600
- timestamp: threadTs
102601
- }).catch(() => {
102703
+ void client.reactions.remove({ channel: channelId, name: "hourglass_flowing_sand", timestamp: threadTs }).catch((err) => {
102704
+ const msg = err instanceof Error ? err.message : String(err);
102705
+ if (!msg.includes("no_reaction")) {
102706
+ logger3?.warn(`stream: failed to remove typing reaction from ${channelId}:${threadTs}: ${msg}`);
102707
+ }
102708
+ });
102709
+ }
102710
+ function removePendingReaction(client, channelId, typingIndicator, pendingReactions, logger3) {
102711
+ if (typingIndicator !== "reaction")
102712
+ return;
102713
+ const queue2 = pendingReactions.get(channelId);
102714
+ if (!queue2 || queue2.length === 0)
102715
+ return;
102716
+ const messageTs = queue2.shift();
102717
+ if (queue2.length === 0)
102718
+ pendingReactions.delete(channelId);
102719
+ void client.reactions.remove({ channel: channelId, name: "hourglass_flowing_sand", timestamp: messageTs }).then(() => {
102720
+ logger3?.debug?.(`stream: removed pending typing reaction from ${channelId}:${messageTs}`);
102721
+ }).catch((err) => {
102722
+ const msg = err instanceof Error ? err.message : String(err);
102723
+ if (!msg.includes("no_reaction")) {
102724
+ logger3?.warn(`stream: failed to remove pending typing reaction from ${channelId}:${messageTs}: ${msg}`);
102725
+ }
102602
102726
  });
102603
102727
  }
102604
- async function handleTextDelta(channelId, textChunk, threadTs, client, streamState, callbacks, startTime, streaming, nativeStreaming, typingIndicator, streamKeyTs) {
102605
- const key = streamKey(channelId, streamKeyTs);
102728
+ async function handleTextDelta(textChunk, streaming, nativeStreaming, ctx) {
102729
+ const { channelId, threadTs, client, streamState, callbacks, startTime, typingIndicator, streamKeyTs, logger: logger3 } = ctx;
102730
+ const key = buildStreamKey(channelId, streamKeyTs);
102606
102731
  const existing = streamState.get(key);
102607
102732
  if (!streaming) {
102608
102733
  if (existing) {
@@ -102612,23 +102737,19 @@ async function handleTextDelta(channelId, textChunk, threadTs, client, streamSta
102612
102737
  channelId,
102613
102738
  threadTs: threadTs ?? "",
102614
102739
  messageTs: "",
102615
- // No message posted yet
102616
102740
  accumulatedText: textChunk,
102617
102741
  lastUpdateAt: 0,
102618
102742
  startedAt: Date.now(),
102619
102743
  streamId: randomUUID4()
102620
102744
  });
102621
- addTypingReaction(client, channelId, threadTs, typingIndicator);
102745
+ addTypingReaction(client, channelId, threadTs, typingIndicator, logger3);
102622
102746
  }
102623
102747
  return { success: true, durationMs: Date.now() - startTime };
102624
102748
  }
102625
102749
  if (existing) {
102626
102750
  existing.accumulatedText += textChunk;
102627
102751
  if (existing.nativeStreamId) {
102628
- return wrapSlackCall(() => client.chat.appendStream({
102629
- stream_id: existing.nativeStreamId,
102630
- text: formatForPlatform(textChunk, "slack")
102631
- }), callbacks, startTime);
102752
+ return wrapSlackCall(() => appendStream(client, existing.nativeStreamId, formatForPlatform(textChunk, "slack")), callbacks, startTime);
102632
102753
  }
102633
102754
  const now = Date.now();
102634
102755
  if (now - existing.lastUpdateAt < STREAM_UPDATE_INTERVAL_MS) {
@@ -102636,49 +102757,38 @@ async function handleTextDelta(channelId, textChunk, threadTs, client, streamSta
102636
102757
  }
102637
102758
  existing.lastUpdateAt = now;
102638
102759
  const formatted = formatForPlatform(existing.accumulatedText, "slack");
102639
- const streamText = formatted.replace(/\n{2,}/g, "\n");
102640
102760
  return wrapSlackCall(() => client.chat.update({
102641
102761
  channel: channelId,
102642
102762
  ts: existing.messageTs,
102643
- text: truncateText(streamText, MAX_MESSAGE_LENGTH2)
102763
+ text: truncateText(formatted.replace(/\n{2,}/g, "\n"), MAX_MESSAGE_LENGTH2)
102644
102764
  }), callbacks, startTime);
102645
102765
  }
102646
102766
  if (nativeStreaming && threadTs) {
102647
102767
  try {
102648
- const slackClient = client;
102649
- const result2 = await slackClient.chat.startStream({
102650
- channel: channelId,
102651
- thread_ts: threadTs
102652
- });
102653
- const nativeStreamId = result2.stream_id ?? "";
102768
+ const nativeStreamId = await startStream(client, channelId, threadTs);
102654
102769
  const now = Date.now();
102655
102770
  streamState.set(key, {
102656
102771
  channelId,
102657
- threadTs: threadTs ?? "",
102772
+ threadTs,
102658
102773
  messageTs: "",
102659
- // Not used in native streaming
102660
102774
  accumulatedText: textChunk,
102661
102775
  lastUpdateAt: now,
102662
102776
  startedAt: now,
102663
102777
  streamId: randomUUID4(),
102664
102778
  nativeStreamId
102665
102779
  });
102666
- await slackClient.chat.appendStream({
102667
- stream_id: nativeStreamId,
102668
- text: formatForPlatform(textChunk, "slack")
102669
- });
102670
- addTypingReaction(client, channelId, threadTs, typingIndicator);
102780
+ await appendStream(client, nativeStreamId, formatForPlatform(textChunk, "slack"));
102781
+ addTypingReaction(client, channelId, threadTs, typingIndicator, logger3);
102671
102782
  return { success: true, durationMs: Date.now() - startTime };
102672
102783
  } catch (err) {
102673
102784
  callbacks.recordError(err);
102674
102785
  }
102675
102786
  }
102676
102787
  try {
102677
- const mrkdwn = formatForPlatform(textChunk, "slack");
102678
102788
  const now = Date.now();
102679
102789
  const result2 = await client.chat.postMessage({
102680
102790
  channel: channelId,
102681
- text: truncateText(mrkdwn, MAX_MESSAGE_LENGTH2),
102791
+ text: truncateText(formatForPlatform(textChunk, "slack"), MAX_MESSAGE_LENGTH2),
102682
102792
  ...threadTs ? { thread_ts: threadTs } : {}
102683
102793
  });
102684
102794
  streamState.set(key, {
@@ -102690,26 +102800,22 @@ async function handleTextDelta(channelId, textChunk, threadTs, client, streamSta
102690
102800
  startedAt: now,
102691
102801
  streamId: randomUUID4()
102692
102802
  });
102693
- addTypingReaction(client, channelId, threadTs, typingIndicator);
102803
+ addTypingReaction(client, channelId, threadTs, typingIndicator, logger3);
102694
102804
  return { success: true, durationMs: now - startTime };
102695
102805
  } catch (err) {
102696
102806
  callbacks.recordError(err);
102697
- return {
102698
- success: false,
102699
- error: err instanceof Error ? err.message : String(err),
102700
- durationMs: Date.now() - startTime
102701
- };
102807
+ return { success: false, error: err instanceof Error ? err.message : String(err), durationMs: Date.now() - startTime };
102702
102808
  }
102703
102809
  }
102704
- async function flushStreamBuffer(channelId, threadTs, client, streamState, callbacks, streamKeyTs) {
102705
- const key = streamKey(channelId, streamKeyTs);
102810
+ async function flushStreamBuffer(ctx) {
102811
+ const { channelId, threadTs, client, streamState, callbacks, streamKeyTs } = ctx;
102812
+ const key = buildStreamKey(channelId, streamKeyTs);
102706
102813
  const existing = streamState.get(key);
102707
102814
  if (!existing || !existing.accumulatedText)
102708
102815
  return;
102709
102816
  if (existing.nativeStreamId) {
102710
- const slackClient = client;
102711
102817
  try {
102712
- await slackClient.chat.stopStream({ stream_id: existing.nativeStreamId });
102818
+ await stopStream(client, existing.nativeStreamId);
102713
102819
  } catch {
102714
102820
  }
102715
102821
  existing.nativeStreamId = void 0;
@@ -102740,19 +102846,21 @@ async function flushStreamBuffer(channelId, threadTs, client, streamState, callb
102740
102846
  callbacks.recordError(err);
102741
102847
  }
102742
102848
  }
102743
- async function handleDone(channelId, threadTs, client, streamState, callbacks, startTime, typingIndicator, streamKeyTs) {
102744
- const key = streamKey(channelId, streamKeyTs);
102849
+ async function handleDone(ctx) {
102850
+ const { channelId, threadTs, client, streamState, callbacks, startTime, typingIndicator, streamKeyTs, pendingReactions, logger: logger3 } = ctx;
102851
+ const key = buildStreamKey(channelId, streamKeyTs);
102745
102852
  const existing = streamState.get(key);
102746
102853
  streamState.delete(key);
102854
+ removePendingReaction(client, channelId, typingIndicator, pendingReactions, logger3);
102747
102855
  if (existing?.threadTs) {
102748
- removeTypingReaction(client, channelId, existing.threadTs, typingIndicator);
102856
+ removeTypingReaction(client, channelId, existing.threadTs, typingIndicator, logger3);
102749
102857
  }
102750
102858
  if (!existing) {
102859
+ logger3?.warn(`stream: done received for ${channelId} with no active stream (empty response \u2014 user may see no output)`);
102751
102860
  return { success: true, durationMs: Date.now() - startTime };
102752
102861
  }
102753
102862
  if (existing.nativeStreamId) {
102754
- const slackClient = client;
102755
- return wrapSlackCall(() => slackClient.chat.stopStream({ stream_id: existing.nativeStreamId }), callbacks, startTime, true);
102863
+ return wrapSlackCall(() => stopStream(client, existing.nativeStreamId), callbacks, startTime, true);
102756
102864
  }
102757
102865
  if (!existing.messageTs) {
102758
102866
  return wrapSlackCall(() => client.chat.postMessage({
@@ -102767,27 +102875,24 @@ async function handleDone(channelId, threadTs, client, streamState, callbacks, s
102767
102875
  text: truncateText(formatForPlatform(existing.accumulatedText, "slack"), MAX_MESSAGE_LENGTH2)
102768
102876
  }), callbacks, startTime, true);
102769
102877
  }
102770
- async function handleError(channelId, errorMsg, threadTs, client, streamState, callbacks, startTime, typingIndicator, streamKeyTs) {
102771
- const key = streamKey(channelId, streamKeyTs);
102878
+ async function handleError(errorMsg, ctx) {
102879
+ const { channelId, threadTs, client, streamState, callbacks, startTime, typingIndicator, streamKeyTs, pendingReactions, logger: logger3 } = ctx;
102880
+ const key = buildStreamKey(channelId, streamKeyTs);
102772
102881
  const existing = streamState.get(key);
102773
102882
  streamState.delete(key);
102883
+ removePendingReaction(client, channelId, typingIndicator, pendingReactions, logger3);
102774
102884
  if (existing?.threadTs) {
102775
- removeTypingReaction(client, channelId, existing.threadTs, typingIndicator);
102885
+ removeTypingReaction(client, channelId, existing.threadTs, typingIndicator, logger3);
102776
102886
  }
102777
102887
  if (existing) {
102778
102888
  if (existing.nativeStreamId) {
102779
- const slackClient = client;
102780
- const errorSuffix = formatForPlatform(`
102781
-
102782
- [Error: ${errorMsg}]`, "slack");
102783
102889
  try {
102784
- await slackClient.chat.appendStream({
102785
- stream_id: existing.nativeStreamId,
102786
- text: errorSuffix
102787
- });
102890
+ await appendStream(client, existing.nativeStreamId, formatForPlatform(`
102891
+
102892
+ [Error: ${errorMsg}]`, "slack"));
102788
102893
  } catch {
102789
102894
  }
102790
- return wrapSlackCall(() => slackClient.chat.stopStream({ stream_id: existing.nativeStreamId }), callbacks, startTime, true);
102895
+ return wrapSlackCall(() => stopStream(client, existing.nativeStreamId), callbacks, startTime, true);
102791
102896
  }
102792
102897
  const finalText = truncateText(`${formatForPlatform(existing.accumulatedText, "slack")}
102793
102898
 
@@ -102799,101 +102904,35 @@ async function handleError(channelId, errorMsg, threadTs, client, streamState, c
102799
102904
  ...threadTs ? { thread_ts: threadTs } : {}
102800
102905
  }), callbacks, startTime, true);
102801
102906
  }
102802
- return wrapSlackCall(() => client.chat.update({
102803
- channel: channelId,
102804
- ts: existing.messageTs,
102805
- text: finalText
102806
- }), callbacks, startTime, true);
102907
+ return wrapSlackCall(() => client.chat.update({ channel: channelId, ts: existing.messageTs, text: finalText }), callbacks, startTime, true);
102807
102908
  }
102808
- const text6 = truncateText(`[Error: ${errorMsg}]`, MAX_MESSAGE_LENGTH2);
102809
102909
  return wrapSlackCall(() => client.chat.postMessage({
102810
102910
  channel: channelId,
102811
- text: text6,
102911
+ text: truncateText(`[Error: ${errorMsg}]`, MAX_MESSAGE_LENGTH2),
102812
102912
  ...threadTs ? { thread_ts: threadTs } : {}
102813
102913
  }), callbacks, startTime, true);
102814
102914
  }
102815
- async function deliverMessage2(opts) {
102816
- const { adapterId, subject, envelope, client, streamState, callbacks, logger: logger3 = noopLogger } = opts;
102817
- const startTime = Date.now();
102818
- for (const [key, stream] of streamState) {
102819
- if (startTime - stream.startedAt > STREAM_TTL_MS) {
102820
- streamState.delete(key);
102821
- logger3.warn(`stream: reaped orphaned stream for ${key} (age: ${Math.round((startTime - stream.startedAt) / 1e3)}s)`);
102822
- }
102823
- }
102824
- if (envelope.from.startsWith(SUBJECT_PREFIX2)) {
102825
- logger3.debug("deliver: echo prevention \u2014 skipping self-originated message");
102826
- return { success: true, durationMs: Date.now() - startTime };
102827
- }
102828
- if (!client) {
102829
- return {
102830
- success: false,
102831
- error: `SlackAdapter(${adapterId}): not started`,
102832
- durationMs: Date.now() - startTime
102833
- };
102834
- }
102835
- const channelId = extractChannelId(subject);
102836
- if (!channelId) {
102837
- return {
102838
- success: false,
102839
- error: `SlackAdapter(${adapterId}): cannot extract channel ID from subject '${subject}'`,
102840
- durationMs: Date.now() - startTime
102841
- };
102842
- }
102843
- const threadTs = resolveThreadTs(envelope);
102844
- const payloadObj = envelope.payload && typeof envelope.payload === "object" ? envelope.payload : void 0;
102845
- const payloadCorrelationId = payloadObj?.correlationId;
102846
- const streamKeyTs = threadTs ?? payloadCorrelationId ?? envelope.from;
102847
- const eventType = detectStreamEventType(envelope.payload);
102848
- if (eventType) {
102849
- const textChunk = extractTextDelta(envelope.payload);
102850
- if (textChunk) {
102851
- logger3.debug(`deliver: text_delta to ${channelId} (${textChunk.length} chars, streaming=${opts.streaming ? opts.nativeStreaming ? "native" : "legacy" : "buffered"})`);
102852
- return handleTextDelta(channelId, textChunk, threadTs, client, streamState, callbacks, startTime, opts.streaming, opts.nativeStreaming, opts.typingIndicator, streamKeyTs);
102853
- }
102854
- const errorMsg = extractErrorMessage(envelope.payload);
102855
- if (errorMsg) {
102856
- logger3.debug(`deliver: error to ${channelId}: "${errorMsg.slice(0, 100)}"`);
102857
- return handleError(channelId, errorMsg, threadTs, client, streamState, callbacks, startTime, opts.typingIndicator, streamKeyTs);
102858
- }
102859
- if (eventType === "done") {
102860
- logger3.debug(`deliver: done for ${channelId}`);
102861
- return handleDone(channelId, threadTs, client, streamState, callbacks, startTime, opts.typingIndicator, streamKeyTs);
102862
- }
102863
- if (eventType === "approval_required") {
102864
- const approvalData = extractApprovalData(envelope.payload);
102865
- if (approvalData) {
102866
- logger3.debug(`deliver: approval_required for tool '${approvalData.toolName}' to ${channelId}`);
102867
- await flushStreamBuffer(channelId, threadTs, client, streamState, callbacks, streamKeyTs);
102868
- return handleApprovalRequired2(channelId, threadTs, approvalData, envelope, client, callbacks, startTime);
102869
- }
102870
- }
102871
- logger3.debug(`deliver: dropping stream event '${eventType}' (whitelist)`);
102872
- return { success: true, durationMs: Date.now() - startTime };
102873
- }
102874
- const content3 = extractPayloadContent(envelope.payload);
102875
- const mrkdwn = formatForPlatform(content3, "slack");
102876
- const text6 = truncateText(mrkdwn, MAX_MESSAGE_LENGTH2);
102877
- logger3.debug(`deliver: standard payload to ${channelId} (${text6.length} chars)`);
102878
- return wrapSlackCall(() => client.chat.postMessage({
102879
- channel: channelId,
102880
- text: text6,
102881
- ...threadTs ? { thread_ts: threadTs } : {}
102882
- }), callbacks, startTime, true);
102915
+
102916
+ // ../relay/dist/adapters/slack/approval.js
102917
+ function createSlackOutboundState() {
102918
+ return { pendingApprovalTimeouts: /* @__PURE__ */ new Map() };
102883
102919
  }
102884
- function extractAgentIdFromEnvelope2(envelope) {
102885
- const payload = envelope.payload;
102886
- const data = payload?.data;
102887
- return data?.agentId ?? "unknown";
102920
+ function clearAllApprovalTimeouts(state) {
102921
+ for (const entry of state.pendingApprovalTimeouts.values()) {
102922
+ clearTimeout(entry.timer);
102923
+ }
102924
+ state.pendingApprovalTimeouts.clear();
102888
102925
  }
102889
- function extractSessionIdFromEnvelope2(envelope) {
102890
- const payload = envelope.payload;
102891
- const data = payload?.data;
102892
- return data?.ccaSessionKey ?? "unknown";
102926
+ function clearApprovalTimeout2(state, toolCallId) {
102927
+ const entry = state.pendingApprovalTimeouts.get(toolCallId);
102928
+ if (entry) {
102929
+ clearTimeout(entry.timer);
102930
+ state.pendingApprovalTimeouts.delete(toolCallId);
102931
+ }
102893
102932
  }
102894
- async function handleApprovalRequired2(channelId, threadTs, data, envelope, client, callbacks, startTime) {
102895
- const agentId = extractAgentIdFromEnvelope2(envelope);
102896
- const sessionId = extractSessionIdFromEnvelope2(envelope);
102933
+ async function handleApprovalRequired2(channelId, threadTs, data, envelope, client, callbacks, startTime, state) {
102934
+ const agentId = extractAgentIdFromEnvelope(envelope) ?? "unknown";
102935
+ const sessionId = extractSessionIdFromEnvelope(envelope) ?? "unknown";
102897
102936
  const inputPreview = truncateText(data.input, 500);
102898
102937
  const toolDescription = formatToolDescription(data.toolName, data.input);
102899
102938
  const buttonValue = JSON.stringify({
@@ -102952,7 +102991,7 @@ ${inputPreview}
102952
102991
  if (result2.success && postedTs && data.timeoutMs > 0) {
102953
102992
  const msgTs = postedTs;
102954
102993
  const timer = setTimeout(async () => {
102955
- pendingApprovalTimeouts2.delete(data.toolCallId);
102994
+ state.pendingApprovalTimeouts.delete(data.toolCallId);
102956
102995
  try {
102957
102996
  await client.chat.update({
102958
102997
  channel: channelId,
@@ -102968,7 +103007,7 @@ ${inputPreview}
102968
103007
  } catch {
102969
103008
  }
102970
103009
  }, data.timeoutMs);
102971
- pendingApprovalTimeouts2.set(data.toolCallId, {
103010
+ state.pendingApprovalTimeouts.set(data.toolCallId, {
102972
103011
  timer,
102973
103012
  channelId,
102974
103013
  messageTs: msgTs,
@@ -102978,24 +103017,107 @@ ${inputPreview}
102978
103017
  return result2;
102979
103018
  }
102980
103019
 
103020
+ // ../relay/dist/adapters/slack/outbound.js
103021
+ function resolveThreadTs(envelope) {
103022
+ const payload = envelope.payload;
103023
+ if (payload === null || typeof payload !== "object")
103024
+ return void 0;
103025
+ const obj = payload;
103026
+ const pd = obj.platformData;
103027
+ if (!pd)
103028
+ return void 0;
103029
+ if (typeof pd.threadTs === "string" && pd.threadTs)
103030
+ return pd.threadTs;
103031
+ if (typeof pd.ts === "string" && pd.ts)
103032
+ return pd.ts;
103033
+ return void 0;
103034
+ }
103035
+ async function deliverMessage2(opts) {
103036
+ const { adapterId, subject, envelope, client, streamState, pendingReactions, callbacks, logger: logger3 = noopLogger } = opts;
103037
+ const startTime = Date.now();
103038
+ for (const [key, stream] of streamState) {
103039
+ if (startTime - stream.startedAt > STREAM_TTL_MS) {
103040
+ streamState.delete(key);
103041
+ logger3.warn(`stream: reaped orphaned stream for ${key} (age: ${Math.round((startTime - stream.startedAt) / 1e3)}s)`);
103042
+ }
103043
+ }
103044
+ if (envelope.from.startsWith(SUBJECT_PREFIX2)) {
103045
+ logger3.debug("deliver: echo prevention \u2014 skipping self-originated message");
103046
+ return { success: true, durationMs: Date.now() - startTime };
103047
+ }
103048
+ if (!client) {
103049
+ return { success: false, error: `SlackAdapter(${adapterId}): not started`, durationMs: Date.now() - startTime };
103050
+ }
103051
+ const channelId = extractChannelId(subject);
103052
+ if (!channelId) {
103053
+ return {
103054
+ success: false,
103055
+ error: `SlackAdapter(${adapterId}): cannot extract channel ID from subject '${subject}'`,
103056
+ durationMs: Date.now() - startTime
103057
+ };
103058
+ }
103059
+ const threadTs = resolveThreadTs(envelope);
103060
+ const payloadObj = envelope.payload && typeof envelope.payload === "object" ? envelope.payload : void 0;
103061
+ const streamKeyTs = threadTs ?? payloadObj?.correlationId ?? envelope.from;
103062
+ const ctx = {
103063
+ channelId,
103064
+ threadTs,
103065
+ client,
103066
+ streamState,
103067
+ callbacks,
103068
+ startTime,
103069
+ typingIndicator: opts.typingIndicator,
103070
+ streamKeyTs,
103071
+ pendingReactions,
103072
+ logger: logger3
103073
+ };
103074
+ const eventType = detectStreamEventType(envelope.payload);
103075
+ if (eventType) {
103076
+ const textChunk = extractTextDelta(envelope.payload);
103077
+ if (textChunk) {
103078
+ logger3.debug(`deliver: text_delta to ${channelId} (${textChunk.length} chars, streaming=${opts.streaming ? opts.nativeStreaming ? "native" : "legacy" : "buffered"})`);
103079
+ return handleTextDelta(textChunk, opts.streaming, opts.nativeStreaming, ctx);
103080
+ }
103081
+ const errorMsg = extractErrorMessage(envelope.payload);
103082
+ if (errorMsg) {
103083
+ logger3.debug(`deliver: error to ${channelId}: "${errorMsg.slice(0, 100)}"`);
103084
+ return handleError(errorMsg, ctx);
103085
+ }
103086
+ if (eventType === "done") {
103087
+ logger3.debug(`deliver: done for ${channelId}`);
103088
+ return handleDone(ctx);
103089
+ }
103090
+ if (eventType === "approval_required") {
103091
+ const approvalData = extractApprovalData(envelope.payload);
103092
+ if (approvalData) {
103093
+ logger3.debug(`deliver: approval_required for tool '${approvalData.toolName}' to ${channelId}`);
103094
+ await flushStreamBuffer(ctx);
103095
+ return handleApprovalRequired2(channelId, threadTs, approvalData, envelope, client, callbacks, startTime, opts.approvalState);
103096
+ }
103097
+ }
103098
+ logger3.debug(`deliver: dropping stream event '${eventType}' (whitelist)`);
103099
+ return { success: true, durationMs: Date.now() - startTime };
103100
+ }
103101
+ const text6 = truncateText(formatForPlatform(extractPayloadContent(envelope.payload), "slack"), MAX_MESSAGE_LENGTH2);
103102
+ logger3.debug(`deliver: standard payload to ${channelId} (${text6.length} chars)`);
103103
+ return wrapSlackCall(() => client.chat.postMessage({ channel: channelId, text: text6, ...threadTs ? { thread_ts: threadTs } : {} }), callbacks, startTime, true);
103104
+ }
103105
+
102981
103106
  // ../relay/dist/adapters/slack/slack-adapter.js
102982
103107
  var SLACK_APP_MANIFEST_YAML = `display_information:
102983
103108
  name: DorkOS Relay
102984
- settings:
102985
- socket_mode_enabled: true
102986
- event_subscriptions:
102987
- bot_events:
102988
- - message.channels
102989
- - message.groups
102990
- - message.im
102991
- - app_mention
102992
103109
  features:
103110
+ app_home:
103111
+ home_tab_enabled: false
103112
+ messages_tab_enabled: true
103113
+ messages_tab_read_only_enabled: false
102993
103114
  bot_user:
102994
103115
  display_name: DorkOS Relay
102995
103116
  always_online: false
102996
103117
  oauth_config:
102997
103118
  scopes:
102998
103119
  bot:
103120
+ - app_mentions:read
102999
103121
  - channels:history
103000
103122
  - channels:read
103001
103123
  - chat:write
@@ -103005,9 +103127,22 @@ oauth_config:
103005
103127
  - im:read
103006
103128
  - im:write
103007
103129
  - mpim:history
103008
- - app_mentions:read
103130
+ - reactions:read
103131
+ - reactions:write
103009
103132
  - users:read
103010
- - reactions:write`;
103133
+ settings:
103134
+ event_subscriptions:
103135
+ bot_events:
103136
+ - app_mention
103137
+ - message.channels
103138
+ - message.groups
103139
+ - message.im
103140
+ - message.mpim
103141
+ interactivity:
103142
+ is_enabled: true
103143
+ org_deploy_enabled: false
103144
+ socket_mode_enabled: true
103145
+ token_rotation_enabled: false`;
103011
103146
  var SLACK_CREATE_APP_URL = `https://api.slack.com/apps?new_app=1&manifest_yaml=${encodeURIComponent(SLACK_APP_MANIFEST_YAML)}`;
103012
103147
  var SLACK_MANIFEST = {
103013
103148
  type: "slack",
@@ -103026,7 +103161,7 @@ var SLACK_MANIFEST = {
103026
103161
  {
103027
103162
  stepId: "create-app",
103028
103163
  title: "Create & Configure a Slack App",
103029
- description: 'Go to api.slack.com/apps \u2192 Create New App \u2192 From Scratch.\n\n1. **Socket Mode** \u2014 Enable it (Settings \u2192 Socket Mode).\n2. **Event Subscriptions** \u2014 Turn on Enable Events, then subscribe to bot events: message.channels, message.groups, message.im, app_mention.\n3. **OAuth & Permissions** \u2014 Add bot token scopes: channels:history, channels:read, chat:write, groups:history, groups:read, im:history, im:read, im:write, mpim:history, app_mentions:read, users:read, reactions:write. Then install the app to your workspace.\n4. **App-Level Token** \u2014 In Basic Information \u2192 App-Level Tokens, generate a token with the connections:write scope.\n\n\u26A0\uFE0F Do NOT enable "Agents & AI Apps" \u2014 it adds user scopes that cause install failures on most workspaces.',
103164
+ description: 'Go to api.slack.com/apps \u2192 Create New App \u2192 From Scratch.\n\n1. **Socket Mode** \u2014 Enable it (Settings \u2192 Socket Mode).\n2. **Event Subscriptions** \u2014 Turn on Enable Events, then subscribe to bot events: app_mention, message.channels, message.groups, message.im, message.mpim.\n3. **OAuth & Permissions** \u2014 Add bot token scopes: app_mentions:read, channels:history, channels:read, chat:write, groups:history, groups:read, im:history, im:read, im:write, mpim:history, reactions:read, reactions:write, users:read. Then install the app to your workspace.\n4. **App-Level Token** \u2014 In Basic Information \u2192 App-Level Tokens, generate a token with the connections:write scope.\n\n\u26A0\uFE0F Do NOT enable "Agents & AI Apps" \u2014 it adds user scopes that cause install failures on most workspaces.',
103030
103165
  fields: ["botToken", "appToken", "signingSecret", "streaming", "nativeStreaming", "typingIndicator"]
103031
103166
  }
103032
103167
  ],
@@ -103106,10 +103241,10 @@ var SLACK_MANIFEST = {
103106
103241
  { label: "Emoji reaction", value: "reaction" }
103107
103242
  ],
103108
103243
  visibleByDefault: true,
103109
- helpMarkdown: 'When set to "Emoji reaction", adds an :hourglass_flowing_sand: reaction to your message while the agent is processing. Requires the `reactions:write` scope.'
103244
+ helpMarkdown: 'When set to "Emoji reaction", adds an :hourglass_flowing_sand: reaction to your message while the agent is processing. Requires the `reactions:write` and `reactions:read` scopes.'
103110
103245
  }
103111
103246
  ],
103112
- setupInstructions: '1. Create a Slack app at api.slack.com/apps (From Scratch, not From Manifest).\n2. Enable Socket Mode (Settings \u2192 Socket Mode).\n3. Enable Event Subscriptions and subscribe to bot events: message.channels, message.groups, message.im, app_mention.\n4. Add bot token scopes under OAuth & Permissions: channels:history, channels:read, chat:write, groups:history, groups:read, im:history, im:read, im:write, mpim:history, app_mentions:read, users:read, reactions:write.\n5. Install the app to your workspace (OAuth & Permissions \u2192 Install).\n6. Copy the Bot User OAuth Token (starts with xoxb-).\n7. Generate an App-Level Token with connections:write scope (Basic Information \u2192 App-Level Tokens).\n8. Copy the Signing Secret from Basic Information.\n\n\u26A0\uFE0F Do NOT enable "Agents & AI Apps" \u2014 it adds user-level scopes that cause invalid_scope errors on most workspace plans.'
103247
+ setupInstructions: '1. Create a Slack app at api.slack.com/apps (From Scratch, not From Manifest).\n2. Enable Socket Mode (Settings \u2192 Socket Mode).\n3. Enable Event Subscriptions and subscribe to bot events: app_mention, message.channels, message.groups, message.im, message.mpim.\n4. Add bot token scopes under OAuth & Permissions: app_mentions:read, channels:history, channels:read, chat:write, groups:history, groups:read, im:history, im:read, im:write, mpim:history, reactions:read, reactions:write, users:read.\n5. Install the app to your workspace (OAuth & Permissions \u2192 Install).\n6. Copy the Bot User OAuth Token (starts with xoxb-).\n7. Generate an App-Level Token with connections:write scope (Basic Information \u2192 App-Level Tokens).\n8. Copy the Signing Secret from Basic Information.\n\n\u26A0\uFE0F Do NOT enable "Agents & AI Apps" \u2014 it adds user-level scopes that cause invalid_scope errors on most workspace plans.'
103113
103248
  };
103114
103249
  var SlackAdapter = class extends BaseRelayAdapter {
103115
103250
  config;
@@ -103117,6 +103252,9 @@ var SlackAdapter = class extends BaseRelayAdapter {
103117
103252
  /** Bot's own user ID — cached after auth.test for echo prevention. */
103118
103253
  botUserId = "";
103119
103254
  streamState = /* @__PURE__ */ new Map();
103255
+ /** FIFO queue of message timestamps with pending hourglass reactions, keyed by channelId. */
103256
+ pendingReactions = /* @__PURE__ */ new Map();
103257
+ outboundState = createSlackOutboundState();
103120
103258
  constructor(id, config, displayName = "Slack") {
103121
103259
  super(id, SUBJECT_PREFIX2, displayName);
103122
103260
  this.config = config;
@@ -103149,10 +103287,10 @@ var SlackAdapter = class extends BaseRelayAdapter {
103149
103287
  const authResult = await app.client.auth.test();
103150
103288
  this.botUserId = authResult.user_id ?? "";
103151
103289
  app.message(async ({ event, client }) => {
103152
- await handleInboundMessage2(event, client, relay, this.botUserId, this.makeInboundCallbacks(), this.logger);
103290
+ await handleInboundMessage2(event, client, relay, this.botUserId, this.makeInboundCallbacks(), this.logger, this.config.typingIndicator ?? "none", this.pendingReactions);
103153
103291
  });
103154
103292
  app.event("app_mention", async ({ event, client }) => {
103155
- await handleInboundMessage2(event, client, relay, this.botUserId, this.makeInboundCallbacks(), this.logger);
103293
+ await handleInboundMessage2(event, client, relay, this.botUserId, this.makeInboundCallbacks(), this.logger, this.config.typingIndicator ?? "none", this.pendingReactions);
103156
103294
  });
103157
103295
  app.action("tool_approve", async ({ ack, action, body, client }) => {
103158
103296
  await ack();
@@ -103179,6 +103317,8 @@ var SlackAdapter = class extends BaseRelayAdapter {
103179
103317
  }
103180
103318
  this.botUserId = "";
103181
103319
  this.streamState.clear();
103320
+ this.pendingReactions.clear();
103321
+ clearAllApprovalTimeouts(this.outboundState);
103182
103322
  clearCaches();
103183
103323
  }
103184
103324
  /**
@@ -103197,28 +103337,16 @@ var SlackAdapter = class extends BaseRelayAdapter {
103197
103337
  envelope,
103198
103338
  client: this.app?.client ?? null,
103199
103339
  streamState: this.streamState,
103340
+ pendingReactions: this.pendingReactions,
103200
103341
  botUserId: this.botUserId,
103201
103342
  callbacks: this.makeOutboundCallbacks(),
103202
103343
  streaming: this.config.streaming ?? true,
103203
103344
  nativeStreaming: this.config.nativeStreaming ?? true,
103204
103345
  typingIndicator: this.config.typingIndicator ?? "none",
103346
+ approvalState: this.outboundState,
103205
103347
  logger: this.logger
103206
103348
  });
103207
103349
  }
103208
- /** Build callbacks for inbound message handling. */
103209
- makeInboundCallbacks() {
103210
- return {
103211
- trackInbound: () => this.trackInbound(),
103212
- recordError: (err) => this.recordError(err)
103213
- };
103214
- }
103215
- /** Build callbacks for outbound message delivery. */
103216
- makeOutboundCallbacks() {
103217
- return {
103218
- trackOutbound: () => this.trackOutbound(),
103219
- recordError: (err) => this.recordError(err)
103220
- };
103221
- }
103222
103350
  /**
103223
103351
  * Handle a tool approval or denial action from Slack interactive buttons.
103224
103352
  *
@@ -103240,7 +103368,7 @@ var SlackAdapter = class extends BaseRelayAdapter {
103240
103368
  return;
103241
103369
  }
103242
103370
  const { toolCallId, sessionId, agentId } = JSON.parse(btnAction.value);
103243
- clearApprovalTimeout2(toolCallId);
103371
+ clearApprovalTimeout2(this.outboundState, toolCallId);
103244
103372
  const opts = { from: `slack:${btnBody.user?.id ?? "unknown"}` };
103245
103373
  await relay.publish(`relay.system.approval.${agentId}`, {
103246
103374
  type: "approval_response",
@@ -103353,9 +103481,13 @@ async function handleAgentMessage(subject, envelope, context, startTime, config,
103353
103481
  if (!agentId) {
103354
103482
  return { success: false, error: `Could not extract agentId from subject: ${subject}`, durationMs: Date.now() - startTime };
103355
103483
  }
103484
+ const log = deps.logger ?? console;
103485
+ if (!deps.agentSessionStore) {
103486
+ log.warn("[CCA] agentSessionStore not provided \u2014 SDK session mapping will not persist across restarts");
103487
+ }
103356
103488
  const persistedSdkSessionId = deps.agentSessionStore?.get(agentId);
103357
103489
  const ccaSessionKey = persistedSdkSessionId ?? agentId;
103358
- const log = deps.logger ?? console;
103490
+ log.debug?.(`[CCA] session lookup: agentId=${agentId}, persistedSdkSessionId=${persistedSdkSessionId ?? "(none)"}, hasStarted=${!!persistedSdkSessionId}`);
103359
103491
  deps.traceStore.insertSpan({
103360
103492
  messageId: envelope.id,
103361
103493
  traceId: randomUUID5(),
@@ -103372,11 +103504,15 @@ async function handleAgentMessage(subject, envelope, context, startTime, config,
103372
103504
  processedAt: null,
103373
103505
  error: null
103374
103506
  });
103375
- const payloadCwd = typeof envelope.payload === "object" && envelope.payload !== null ? envelope.payload.cwd : void 0;
103507
+ const payloadObj = typeof envelope.payload === "object" && envelope.payload !== null ? envelope.payload : null;
103508
+ const bindingPerms = payloadObj?.__bindingPermissions;
103509
+ const responseContext = payloadObj?.responseContext;
103510
+ const payloadCwd = payloadObj?.cwd;
103376
103511
  const effectiveCwd = payloadCwd ?? context?.agent?.directory;
103377
- log.debug?.(`[CCA] handleAgentMessage agentId=${agentId} ccaSessionKey=${ccaSessionKey}, payloadCwd=${payloadCwd ?? "(none)"}, context.agent.directory=${context?.agent?.directory ?? "(none)"}, resolvedCwd=${effectiveCwd ?? "(deferred to session)"}`);
103512
+ const effectivePermissionMode = bindingPerms?.permissionMode ?? "default";
103513
+ log.debug?.(`[CCA] handleAgentMessage agentId=${agentId} ccaSessionKey=${ccaSessionKey}, payloadCwd=${payloadCwd ?? "(none)"}, context.agent.directory=${context?.agent?.directory ?? "(none)"}, resolvedCwd=${effectiveCwd ?? "(deferred to session)"}, permissionMode=${effectivePermissionMode}`);
103378
103514
  deps.agentManager.ensureSession(ccaSessionKey, {
103379
- permissionMode: "default",
103515
+ permissionMode: effectivePermissionMode,
103380
103516
  hasStarted: !!persistedSdkSessionId,
103381
103517
  ...effectiveCwd ? { cwd: effectiveCwd } : {}
103382
103518
  });
@@ -103384,7 +103520,6 @@ async function handleAgentMessage(subject, envelope, context, startTime, config,
103384
103520
  if (!envelope.replyTo) {
103385
103521
  log.warn(`ClaudeCodeAdapter: envelope ${envelope.id} has no replyTo \u2014 response events will not be published`);
103386
103522
  }
103387
- const payloadObj = typeof envelope.payload === "object" && envelope.payload !== null ? envelope.payload : null;
103388
103523
  if (payloadObj?.type && STREAM_EVENT_TYPES.has(payloadObj.type)) {
103389
103524
  log.debug?.(`[CCA] skipping sendMessage for StreamEvent payload type=${String(payloadObj.type)}`);
103390
103525
  deps.traceStore.updateSpan(envelope.id, { status: "processed", processedAt: Date.now() });
@@ -103392,12 +103527,15 @@ async function handleAgentMessage(subject, envelope, context, startTime, config,
103392
103527
  }
103393
103528
  const correlationId = payloadObj?.correlationId;
103394
103529
  const prompt2 = formatPromptWithContext(extractPayloadContent(envelope.payload), envelope, agentId, ccaSessionKey);
103530
+ const formatBlock = buildResponseFormatBlock(responseContext);
103395
103531
  const ttlRemaining = envelope.budget.ttl - Date.now();
103396
103532
  const controller = new AbortController();
103397
103533
  const timeout = setTimeout(() => controller.abort(), ttlRemaining > 0 ? ttlRemaining : config.defaultTimeoutMs);
103398
103534
  const isInboxReplyTo = envelope.replyTo?.startsWith("relay.inbox.");
103399
103535
  const eventStream = deps.agentManager.sendMessage(ccaSessionKey, prompt2, {
103400
- ...effectiveCwd ? { cwd: effectiveCwd } : {}
103536
+ permissionMode: effectivePermissionMode,
103537
+ ...effectiveCwd ? { cwd: effectiveCwd } : {},
103538
+ ...formatBlock ? { systemPromptAppend: formatBlock } : {}
103401
103539
  });
103402
103540
  let eventCount = 0, collectedText = "", stepCounter = 0, messageBuffer = "";
103403
103541
  let streamedDone = false, streamError;
@@ -103455,7 +103593,7 @@ async function handleAgentMessage(subject, envelope, context, startTime, config,
103455
103593
  const actualSdkId = deps.agentManager.getSdkSessionId(ccaSessionKey);
103456
103594
  if (actualSdkId && actualSdkId !== agentId) {
103457
103595
  deps.agentSessionStore.set(agentId, actualSdkId);
103458
- log.debug?.(`[CCA] persisted session mapping: ${agentId} \u2192 ${actualSdkId}`);
103596
+ log.info(`[CCA] persisted session mapping: ${agentId} \u2192 ${actualSdkId}`);
103459
103597
  } else {
103460
103598
  log.debug?.(`[CCA] no session mapping to persist: agentId=${agentId}, ccaSessionKey=${ccaSessionKey}, actualSdkId=${actualSdkId ?? "(none)"}`);
103461
103599
  }
@@ -103483,22 +103621,23 @@ function extractAgentId(subject) {
103483
103621
  return null;
103484
103622
  return segments[2] || null;
103485
103623
  }
103486
- var PLATFORM_FORMATTING = {
103487
- slack: [
103488
- "Platform: Slack (mrkdwn format)",
103489
- "Formatting rules:",
103490
- "- Use *bold* (single asterisk), _italic_, ~strikethrough~, `code`",
103491
- "- Do NOT use Markdown tables (| col | col |) \u2014 Slack cannot render them",
103492
- "- For structured data, use bullet lists with bold labels instead",
103493
- "- Keep responses under 4000 characters"
103494
- ].join("\n"),
103495
- telegram: [
103496
- "Platform: Telegram",
103497
- "Formatting rules:",
103498
- "- Use standard Markdown: **bold**, _italic_, `code`",
103499
- "- Keep responses under 4096 characters"
103500
- ].join("\n")
103501
- };
103624
+ function buildResponseFormatBlock(ctx) {
103625
+ if (!ctx?.platform)
103626
+ return "";
103627
+ const lines = [
103628
+ `Platform: ${ctx.platform}`,
103629
+ ctx.maxLength ? `Maximum response length: ${ctx.maxLength} characters` : ""
103630
+ ];
103631
+ if (ctx.formattingInstructions) {
103632
+ lines.push("", ctx.formattingInstructions);
103633
+ } else if (ctx.supportedFormats && !ctx.supportedFormats.includes("markdown")) {
103634
+ lines.push("", "FORMATTING RULES (you MUST follow these):");
103635
+ lines.push("- Avoid complex Markdown formatting (tables, headings) \u2014 use plain text with bullet points.");
103636
+ }
103637
+ return `<response_format>
103638
+ ${lines.filter(Boolean).join("\n")}
103639
+ </response_format>`;
103640
+ }
103502
103641
  function formatPromptWithContext(content3, envelope, agentId, sdkSessionId) {
103503
103642
  const lines = [
103504
103643
  `Agent-ID: ${agentId}`,
@@ -103516,12 +103655,6 @@ function formatPromptWithContext(content3, envelope, agentId, sdkSessionId) {
103516
103655
  if (envelope.replyTo) {
103517
103656
  lines.push("", `Reply to: ${envelope.replyTo}`, "If you cannot complete the task within the budget, summarize what you've done and stop.");
103518
103657
  }
103519
- const payloadObj = typeof envelope.payload === "object" && envelope.payload !== null ? envelope.payload : null;
103520
- const responseContext = payloadObj?.responseContext;
103521
- const platform2 = responseContext?.platform;
103522
- if (platform2 && PLATFORM_FORMATTING[platform2]) {
103523
- lines.push("", PLATFORM_FORMATTING[platform2]);
103524
- }
103525
103658
  return `<relay_context>
103526
103659
  ${lines.join("\n")}
103527
103660
  </relay_context>
@@ -104066,11 +104199,293 @@ import { z as z24 } from "zod";
104066
104199
  // ../../apps/server/src/services/relay/adapter-manager.ts
104067
104200
  import { readFile as readFile8 } from "node:fs/promises";
104068
104201
  import { createRequire } from "node:module";
104069
- import { dirname as dirname6, join as join12 } from "node:path";
104202
+ import { dirname as dirname7, join as join12 } from "node:path";
104203
+
104204
+ // ../../apps/server/src/services/relay/adapter-error.ts
104205
+ var AdapterError = class extends Error {
104206
+ constructor(message, code3) {
104207
+ super(message);
104208
+ this.code = code3;
104209
+ this.name = "AdapterError";
104210
+ }
104211
+ };
104212
+
104213
+ // ../../apps/server/src/services/relay/adapter-config.ts
104214
+ import { readFile as readFile4, writeFile as writeFile2, mkdir as mkdir4, rename as rename2 } from "node:fs/promises";
104215
+ import { dirname as dirname3 } from "node:path";
104216
+ var CONFIG_STABILITY_THRESHOLD_MS = 150;
104217
+ var CONFIG_POLL_INTERVAL_MS = 50;
104218
+ async function loadAdapterConfig(configPath) {
104219
+ try {
104220
+ const raw = await readFile4(configPath, "utf-8");
104221
+ const parsed = AdaptersConfigFileSchema.safeParse(JSON.parse(raw));
104222
+ if (parsed.success) {
104223
+ return parsed.data.adapters;
104224
+ } else {
104225
+ logger.warn(
104226
+ "[AdapterConfig] Malformed config, skipping invalid entries:",
104227
+ parsed.error.flatten()
104228
+ );
104229
+ return [];
104230
+ }
104231
+ } catch (err) {
104232
+ if (err.code === "ENOENT") {
104233
+ return [];
104234
+ } else {
104235
+ logger.warn("[AdapterConfig] Failed to read config:", err);
104236
+ return [];
104237
+ }
104238
+ }
104239
+ }
104240
+ async function saveAdapterConfig(configPath, configs) {
104241
+ await mkdir4(dirname3(configPath), { recursive: true });
104242
+ const tmpPath = `${configPath}.tmp`;
104243
+ await writeFile2(
104244
+ tmpPath,
104245
+ JSON.stringify({ adapters: configs }, null, 2),
104246
+ "utf-8"
104247
+ );
104248
+ await rename2(tmpPath, configPath);
104249
+ }
104250
+ async function ensureDefaultAdapterConfig(configPath) {
104251
+ try {
104252
+ await readFile4(configPath, "utf-8");
104253
+ } catch (err) {
104254
+ if (err.code === "ENOENT") {
104255
+ const defaultConfig = {
104256
+ adapters: [
104257
+ {
104258
+ id: "claude-code",
104259
+ type: "claude-code",
104260
+ builtin: true,
104261
+ enabled: true,
104262
+ config: {
104263
+ maxConcurrent: 3,
104264
+ defaultTimeoutMs: 3e5
104265
+ }
104266
+ }
104267
+ ]
104268
+ };
104269
+ try {
104270
+ await mkdir4(dirname3(configPath), { recursive: true });
104271
+ await writeFile2(
104272
+ configPath,
104273
+ JSON.stringify(defaultConfig, null, 2),
104274
+ "utf-8"
104275
+ );
104276
+ logger.info("[AdapterConfig] Generated default adapters.json with claude-code adapter");
104277
+ } catch (writeErr) {
104278
+ logger.warn("[AdapterConfig] Failed to write default config:", writeErr);
104279
+ }
104280
+ }
104281
+ }
104282
+ }
104283
+ function watchAdapterConfig(configPath, onChange) {
104284
+ const watcher = esm_default.watch(configPath, {
104285
+ persistent: true,
104286
+ ignoreInitial: true,
104287
+ awaitWriteFinish: {
104288
+ stabilityThreshold: CONFIG_STABILITY_THRESHOLD_MS,
104289
+ pollInterval: CONFIG_POLL_INTERVAL_MS
104290
+ }
104291
+ });
104292
+ watcher.on("change", onChange);
104293
+ return watcher;
104294
+ }
104295
+ function maskSensitiveFields(config, manifest) {
104296
+ if (!manifest) return config;
104297
+ const masked = structuredClone(config);
104298
+ for (const field of manifest.configFields) {
104299
+ if (field.type !== "password") continue;
104300
+ const parts = field.key.split(".");
104301
+ let current = masked;
104302
+ let found = true;
104303
+ for (let i2 = 0; i2 < parts.length - 1; i2++) {
104304
+ if (current[parts[i2]] && typeof current[parts[i2]] === "object") {
104305
+ current = current[parts[i2]];
104306
+ } else {
104307
+ found = false;
104308
+ break;
104309
+ }
104310
+ }
104311
+ const lastKey = parts.at(-1);
104312
+ if (found && lastKey in current) {
104313
+ current[lastKey] = "***";
104314
+ }
104315
+ }
104316
+ return masked;
104317
+ }
104318
+ function mergeWithPasswordPreservation(existing, incoming, manifest) {
104319
+ const result2 = { ...existing, ...incoming };
104320
+ if (!manifest) return result2;
104321
+ for (const field of manifest.configFields) {
104322
+ if (field.type !== "password") continue;
104323
+ const parts = field.key.split(".");
104324
+ const incomingValue = getNestedValue(incoming, parts);
104325
+ if (incomingValue === "" || incomingValue === "***" || incomingValue === void 0) {
104326
+ const existingValue = getNestedValue(existing, parts);
104327
+ if (existingValue !== void 0) {
104328
+ setNestedValue(result2, parts, existingValue);
104329
+ }
104330
+ }
104331
+ }
104332
+ return result2;
104333
+ }
104334
+ function getNestedValue(obj, parts) {
104335
+ let current = obj;
104336
+ for (const part of parts) {
104337
+ if (current == null || typeof current !== "object") return void 0;
104338
+ current = current[part];
104339
+ }
104340
+ return current;
104341
+ }
104342
+ function setNestedValue(obj, parts, value) {
104343
+ let current = obj;
104344
+ for (let i2 = 0; i2 < parts.length - 1; i2++) {
104345
+ if (!(parts[i2] in current) || typeof current[parts[i2]] !== "object") {
104346
+ current[parts[i2]] = {};
104347
+ }
104348
+ current = current[parts[i2]];
104349
+ }
104350
+ current[parts.at(-1)] = value;
104351
+ }
104352
+
104353
+ // ../../apps/server/src/services/relay/adapter-factory.ts
104354
+ import { dirname as dirname4 } from "node:path";
104355
+ function defaultAdapterStatus() {
104356
+ return {
104357
+ state: "disconnected",
104358
+ messageCount: { inbound: 0, outbound: 0 },
104359
+ errorCount: 0
104360
+ };
104361
+ }
104362
+ async function createAdapter(config, deps, configPath, onPluginManifest) {
104363
+ switch (config.type) {
104364
+ case "telegram": {
104365
+ const adapter = new TelegramAdapter(
104366
+ config.id,
104367
+ config.config
104368
+ );
104369
+ adapter.setLogger(createTaggedLogger(`telegram:${config.id}`));
104370
+ return adapter;
104371
+ }
104372
+ case "webhook":
104373
+ return new WebhookAdapter(
104374
+ config.id,
104375
+ config.config
104376
+ );
104377
+ case "slack": {
104378
+ const adapter = new SlackAdapter(
104379
+ config.id,
104380
+ config.config
104381
+ );
104382
+ adapter.setLogger(createTaggedLogger(`slack:${config.id}`));
104383
+ return adapter;
104384
+ }
104385
+ case "claude-code":
104386
+ return new ClaudeCodeAdapter(
104387
+ config.id,
104388
+ config.config,
104389
+ {
104390
+ agentManager: deps.agentManager,
104391
+ traceStore: deps.traceStore,
104392
+ pulseStore: deps.pulseStore,
104393
+ agentSessionStore: deps.agentSessionStore,
104394
+ logger
104395
+ }
104396
+ );
104397
+ case "plugin":
104398
+ return loadPluginAdapter(config, configPath, onPluginManifest);
104399
+ default:
104400
+ logger.warn(`[AdapterFactory] Unknown adapter type: ${config.type}`);
104401
+ return null;
104402
+ }
104403
+ }
104404
+ var CONNECTION_TEST_TIMEOUT_MS = 15e3;
104405
+ async function testAdapterConnection(adapter) {
104406
+ try {
104407
+ if (adapter.testConnection) {
104408
+ let timer;
104409
+ try {
104410
+ return await Promise.race([
104411
+ adapter.testConnection(),
104412
+ new Promise((_4, reject) => {
104413
+ timer = setTimeout(
104414
+ () => reject(new Error("Connection test timed out")),
104415
+ CONNECTION_TEST_TIMEOUT_MS
104416
+ );
104417
+ })
104418
+ ]);
104419
+ } finally {
104420
+ clearTimeout(timer);
104421
+ }
104422
+ }
104423
+ const noopRelay = {
104424
+ publish: async () => ({ messageId: "", deliveredTo: 0 }),
104425
+ onSignal: () => () => {
104426
+ },
104427
+ subscribe: () => () => {
104428
+ }
104429
+ };
104430
+ let fallbackTimer;
104431
+ try {
104432
+ await Promise.race([
104433
+ adapter.start(noopRelay),
104434
+ new Promise((_4, reject) => {
104435
+ fallbackTimer = setTimeout(
104436
+ () => reject(new Error("Connection test timed out")),
104437
+ CONNECTION_TEST_TIMEOUT_MS
104438
+ );
104439
+ })
104440
+ ]);
104441
+ } finally {
104442
+ clearTimeout(fallbackTimer);
104443
+ }
104444
+ return { ok: true };
104445
+ } catch (err) {
104446
+ const message = err instanceof Error ? err.message : String(err);
104447
+ return { ok: false, error: message };
104448
+ } finally {
104449
+ try {
104450
+ await adapter.stop();
104451
+ } catch {
104452
+ }
104453
+ }
104454
+ }
104455
+ async function loadPluginAdapter(config, configPath, onPluginManifest) {
104456
+ if (!config.plugin) {
104457
+ logger.warn(`[AdapterFactory] Plugin adapter '${config.id}' missing plugin source config`);
104458
+ return null;
104459
+ }
104460
+ const builtinMap = /* @__PURE__ */ new Map();
104461
+ const configDir = dirname4(configPath);
104462
+ const results = await loadAdapters(
104463
+ [
104464
+ {
104465
+ id: config.id,
104466
+ type: config.type,
104467
+ enabled: config.enabled,
104468
+ plugin: config.plugin,
104469
+ config: config.config
104470
+ }
104471
+ ],
104472
+ builtinMap,
104473
+ configDir
104474
+ );
104475
+ const result2 = results[0];
104476
+ if (!result2) return null;
104477
+ if (result2.manifest && onPluginManifest) {
104478
+ onPluginManifest(config.type, result2.manifest);
104479
+ }
104480
+ return result2.adapter;
104481
+ }
104482
+
104483
+ // ../../apps/server/src/services/relay/binding-subsystem.ts
104484
+ import { dirname as dirname6 } from "node:path";
104070
104485
 
104071
104486
  // ../../apps/server/src/services/relay/binding-store.ts
104072
- import { readFile as readFile4, writeFile as writeFile2, mkdir as mkdir4, rename as rename2, stat as stat4 } from "node:fs/promises";
104073
- import { dirname as dirname3, join as pathJoin } from "node:path";
104487
+ import { readFile as readFile5, writeFile as writeFile3, mkdir as mkdir5, rename as rename3, stat as stat4 } from "node:fs/promises";
104488
+ import { dirname as dirname5, join as pathJoin } from "node:path";
104074
104489
  import { randomUUID as randomUUID7 } from "node:crypto";
104075
104490
  import { z as z23 } from "zod";
104076
104491
  var BindingsFileSchema = z23.object({
@@ -104204,7 +104619,7 @@ var BindingStore = class {
104204
104619
  }
104205
104620
  async load() {
104206
104621
  try {
104207
- const raw = await readFile4(this.filePath, "utf-8");
104622
+ const raw = await readFile5(this.filePath, "utf-8");
104208
104623
  const json = JSON.parse(raw);
104209
104624
  const parsed = BindingsFileSchema.parse(json);
104210
104625
  this.bindings.clear();
@@ -104224,10 +104639,10 @@ var BindingStore = class {
104224
104639
  }
104225
104640
  async save() {
104226
104641
  const data = { bindings: this.getAll() };
104227
- await mkdir4(dirname3(this.filePath), { recursive: true });
104642
+ await mkdir5(dirname5(this.filePath), { recursive: true });
104228
104643
  const tmpPath = `${this.filePath}.tmp`;
104229
- await writeFile2(tmpPath, JSON.stringify(data, null, 2), "utf-8");
104230
- await rename2(tmpPath, this.filePath);
104644
+ await writeFile3(tmpPath, JSON.stringify(data, null, 2), "utf-8");
104645
+ await rename3(tmpPath, this.filePath);
104231
104646
  const fileStat = await stat4(this.filePath);
104232
104647
  this.lastWriteMtime = fileStat.mtimeMs;
104233
104648
  }
@@ -104257,10 +104672,12 @@ var BindingStore = class {
104257
104672
 
104258
104673
  // ../../apps/server/src/services/relay/agent-session-store.ts
104259
104674
  import { join as join11 } from "node:path";
104260
- import { readFile as readFile5, writeFile as writeFile3, mkdir as mkdir5, rename as rename3 } from "node:fs/promises";
104675
+ import { readFile as readFile6, writeFile as writeFile4, mkdir as mkdir6, rename as rename4 } from "node:fs/promises";
104261
104676
  var AgentSessionStore = class {
104262
104677
  filePath;
104263
104678
  sessions = /* @__PURE__ */ new Map();
104679
+ /** Serializes persist calls to prevent concurrent tmp+rename races. */
104680
+ writeLock = Promise.resolve();
104264
104681
  constructor(relayDir) {
104265
104682
  this.filePath = join11(relayDir, "agent-sessions.json");
104266
104683
  }
@@ -104272,7 +104689,7 @@ var AgentSessionStore = class {
104272
104689
  */
104273
104690
  async init() {
104274
104691
  try {
104275
- const raw = await readFile5(this.filePath, "utf-8");
104692
+ const raw = await readFile6(this.filePath, "utf-8");
104276
104693
  const parsed = JSON.parse(raw);
104277
104694
  this.sessions = new Map(Object.entries(parsed));
104278
104695
  logger.info(`[AgentSessionStore] Loaded ${this.sessions.size} session mapping(s)`);
@@ -104328,24 +104745,41 @@ var AgentSessionStore = class {
104328
104745
  logger.warn("[AgentSessionStore] Failed to persist after delete", { err });
104329
104746
  });
104330
104747
  }
104331
- /** Atomically write the current sessions map to disk (tmp + rename). */
104332
- async persist() {
104748
+ /**
104749
+ * Flush any pending writes to disk.
104750
+ *
104751
+ * Call during shutdown to ensure the last fire-and-forget persist
104752
+ * completes before the process exits.
104753
+ */
104754
+ async shutdown() {
104755
+ try {
104756
+ await this.writeLock;
104757
+ } catch {
104758
+ }
104759
+ }
104760
+ /** Enqueue an atomic persist, serialized to prevent concurrent tmp+rename races. */
104761
+ persist() {
104762
+ this.writeLock = this.writeLock.then(() => this.doPersist(), () => this.doPersist());
104763
+ return this.writeLock;
104764
+ }
104765
+ /** Atomic tmp+rename write. Must be serialized via writeLock. */
104766
+ async doPersist() {
104333
104767
  const dir = join11(this.filePath, "..");
104334
- await mkdir5(dir, { recursive: true });
104768
+ await mkdir6(dir, { recursive: true });
104335
104769
  const data = {};
104336
104770
  for (const [agentId, record2] of this.sessions) {
104337
104771
  data[agentId] = record2;
104338
104772
  }
104339
104773
  const json = JSON.stringify(data, null, 2);
104340
104774
  const tmp = `${this.filePath}.tmp`;
104341
- await writeFile3(tmp, json, "utf-8");
104342
- await rename3(tmp, this.filePath);
104775
+ await writeFile4(tmp, json, "utf-8");
104776
+ await rename4(tmp, this.filePath);
104343
104777
  }
104344
104778
  };
104345
104779
 
104346
104780
  // ../../apps/server/src/services/relay/binding-router.ts
104347
104781
  import { join as pathJoin2 } from "node:path";
104348
- import { readFile as readFile6, writeFile as writeFile4, mkdir as mkdir6, rename as rename4 } from "node:fs/promises";
104782
+ import { readFile as readFile7, writeFile as writeFile5, mkdir as mkdir7, rename as rename5 } from "node:fs/promises";
104349
104783
  var BindingRouter = class _BindingRouter {
104350
104784
  constructor(deps) {
104351
104785
  this.deps = deps;
@@ -104359,6 +104793,10 @@ var BindingRouter = class _BindingRouter {
104359
104793
  inFlight = /* @__PURE__ */ new Map();
104360
104794
  sessionMapPath;
104361
104795
  unsubscribe;
104796
+ /** Serializes saveSessionMap calls to prevent concurrent tmp+rename races. */
104797
+ writeLock = Promise.resolve();
104798
+ /** Guards against concurrent shutdown calls corrupting session data. */
104799
+ isShutdown = false;
104362
104800
  /** Load persisted session map, subscribe to inbound messages. */
104363
104801
  async init() {
104364
104802
  await this.loadSessionMap();
@@ -104555,7 +104993,7 @@ var BindingRouter = class _BindingRouter {
104555
104993
  }
104556
104994
  async loadSessionMap() {
104557
104995
  try {
104558
- const raw = await readFile6(this.sessionMapPath, "utf-8");
104996
+ const raw = await readFile7(this.sessionMapPath, "utf-8");
104559
104997
  const parsed = JSON.parse(raw);
104560
104998
  if (!Array.isArray(parsed)) {
104561
104999
  logger.warn("BindingRouter: sessionMap is not an array, starting fresh");
@@ -104571,19 +105009,32 @@ var BindingRouter = class _BindingRouter {
104571
105009
  );
104572
105010
  }
104573
105011
  this.sessionMap = new Map(valid);
104574
- } catch {
105012
+ } catch (err) {
105013
+ const code3 = err.code;
105014
+ if (code3 === "ENOENT") {
105015
+ logger.debug("BindingRouter: no sessions.json found, starting with empty session map");
105016
+ } else {
105017
+ logger.warn("BindingRouter: failed to load sessions.json, starting fresh", err);
105018
+ }
104575
105019
  this.sessionMap = /* @__PURE__ */ new Map();
104576
105020
  }
104577
105021
  }
104578
- async saveSessionMap() {
104579
- await mkdir6(this.deps.relayDir, { recursive: true });
105022
+ saveSessionMap() {
105023
+ this.writeLock = this.writeLock.then(() => this.doSaveSessionMap(), () => this.doSaveSessionMap());
105024
+ return this.writeLock;
105025
+ }
105026
+ /** Atomic tmp+rename write of the session map. Must be serialized via writeLock. */
105027
+ async doSaveSessionMap() {
105028
+ await mkdir7(this.deps.relayDir, { recursive: true });
104580
105029
  const data = JSON.stringify(Array.from(this.sessionMap.entries()));
104581
105030
  const tmpPath = `${this.sessionMapPath}.tmp`;
104582
- await writeFile4(tmpPath, data, "utf-8");
104583
- await rename4(tmpPath, this.sessionMapPath);
105031
+ await writeFile5(tmpPath, data, "utf-8");
105032
+ await rename5(tmpPath, this.sessionMapPath);
104584
105033
  }
104585
- /** Save session map, unsubscribe, and clear state. */
105034
+ /** Save session map, unsubscribe, and clear state. Idempotent — safe to call multiple times. */
104586
105035
  async shutdown() {
105036
+ if (this.isShutdown) return;
105037
+ this.isShutdown = true;
104587
105038
  this.unsubscribe?.();
104588
105039
  try {
104589
105040
  await this.saveSessionMap();
@@ -104594,284 +105045,82 @@ var BindingRouter = class _BindingRouter {
104594
105045
  }
104595
105046
  };
104596
105047
 
104597
- // ../../apps/server/src/services/relay/adapter-error.ts
104598
- var AdapterError = class extends Error {
104599
- constructor(message, code3) {
104600
- super(message);
104601
- this.code = code3;
104602
- this.name = "AdapterError";
104603
- }
104604
- };
104605
-
104606
- // ../../apps/server/src/services/relay/adapter-config.ts
104607
- import { readFile as readFile7, writeFile as writeFile5, mkdir as mkdir7, rename as rename5 } from "node:fs/promises";
104608
- import { dirname as dirname4 } from "node:path";
104609
- var CONFIG_STABILITY_THRESHOLD_MS = 150;
104610
- var CONFIG_POLL_INTERVAL_MS = 50;
104611
- async function loadAdapterConfig(configPath) {
104612
- try {
104613
- const raw = await readFile7(configPath, "utf-8");
104614
- const parsed = AdaptersConfigFileSchema.safeParse(JSON.parse(raw));
104615
- if (parsed.success) {
104616
- return parsed.data.adapters;
104617
- } else {
104618
- logger.warn(
104619
- "[AdapterConfig] Malformed config, skipping invalid entries:",
104620
- parsed.error.flatten()
104621
- );
104622
- return [];
104623
- }
104624
- } catch (err) {
104625
- if (err.code === "ENOENT") {
104626
- return [];
104627
- } else {
104628
- logger.warn("[AdapterConfig] Failed to read config:", err);
104629
- return [];
104630
- }
105048
+ // ../../apps/server/src/services/relay/binding-subsystem.ts
105049
+ var BindingSubsystem = class _BindingSubsystem {
105050
+ bindingStore;
105051
+ agentSessionStore;
105052
+ bindingRouter;
105053
+ isShutdown = false;
105054
+ constructor(bindingStore, agentSessionStore) {
105055
+ this.bindingStore = bindingStore;
105056
+ this.agentSessionStore = agentSessionStore;
104631
105057
  }
104632
- }
104633
- async function saveAdapterConfig(configPath, configs) {
104634
- await mkdir7(dirname4(configPath), { recursive: true });
104635
- const tmpPath = `${configPath}.tmp`;
104636
- await writeFile5(
104637
- tmpPath,
104638
- JSON.stringify({ adapters: configs }, null, 2),
104639
- "utf-8"
104640
- );
104641
- await rename5(tmpPath, configPath);
104642
- }
104643
- async function ensureDefaultAdapterConfig(configPath) {
104644
- try {
104645
- await readFile7(configPath, "utf-8");
104646
- } catch (err) {
104647
- if (err.code === "ENOENT") {
104648
- const defaultConfig = {
104649
- adapters: [
104650
- {
104651
- id: "claude-code",
104652
- type: "claude-code",
104653
- builtin: true,
104654
- enabled: true,
104655
- config: {
104656
- maxConcurrent: 3,
104657
- defaultTimeoutMs: 3e5
104658
- }
104659
- }
104660
- ]
105058
+ /**
105059
+ * Initialize the binding subsystem: BindingStore, AgentSessionStore, and BindingRouter.
105060
+ *
105061
+ * Non-fatal if initialization fails, returns undefined and logs a warning.
105062
+ * AdapterManager continues running without binding-based routing.
105063
+ *
105064
+ * @param deps - Required dependencies for subsystem initialization
105065
+ * @returns Initialized subsystem, or undefined on failure
105066
+ */
105067
+ static async init(deps) {
105068
+ const relayDir = dirname6(deps.configPath);
105069
+ try {
105070
+ const bindingStore = new BindingStore(relayDir);
105071
+ await bindingStore.init();
105072
+ logger.info("[BindingSubsystem] BindingStore initialized");
105073
+ const agentSessionStore = new AgentSessionStore(relayDir);
105074
+ await agentSessionStore.init();
105075
+ logger.info("[BindingSubsystem] AgentSessionStore initialized");
105076
+ const subsystem = new _BindingSubsystem(bindingStore, agentSessionStore);
105077
+ const agentManager = deps.agentManager;
105078
+ const sessionCreator = {
105079
+ async createSession(cwd, permissionMode) {
105080
+ const id = crypto.randomUUID();
105081
+ agentManager.ensureSession(id, { permissionMode: permissionMode ?? "acceptEdits", cwd });
105082
+ return { id };
105083
+ }
104661
105084
  };
104662
- try {
104663
- await mkdir7(dirname4(configPath), { recursive: true });
104664
- await writeFile5(
104665
- configPath,
104666
- JSON.stringify(defaultConfig, null, 2),
104667
- "utf-8"
104668
- );
104669
- logger.info("[AdapterConfig] Generated default adapters.json with claude-code adapter");
104670
- } catch (writeErr) {
104671
- logger.warn("[AdapterConfig] Failed to write default config:", writeErr);
104672
- }
104673
- }
104674
- }
104675
- }
104676
- function watchAdapterConfig(configPath, onChange) {
104677
- const watcher = esm_default.watch(configPath, {
104678
- persistent: true,
104679
- ignoreInitial: true,
104680
- awaitWriteFinish: {
104681
- stabilityThreshold: CONFIG_STABILITY_THRESHOLD_MS,
104682
- pollInterval: CONFIG_POLL_INTERVAL_MS
104683
- }
104684
- });
104685
- watcher.on("change", onChange);
104686
- return watcher;
104687
- }
104688
- function maskSensitiveFields(config, manifest) {
104689
- if (!manifest) return config;
104690
- const masked = structuredClone(config);
104691
- for (const field of manifest.configFields) {
104692
- if (field.type !== "password") continue;
104693
- const parts = field.key.split(".");
104694
- let current = masked;
104695
- let found = true;
104696
- for (let i2 = 0; i2 < parts.length - 1; i2++) {
104697
- if (current[parts[i2]] && typeof current[parts[i2]] === "object") {
104698
- current = current[parts[i2]];
104699
- } else {
104700
- found = false;
104701
- break;
104702
- }
104703
- }
104704
- const lastKey = parts.at(-1);
104705
- if (found && lastKey in current) {
104706
- current[lastKey] = "***";
104707
- }
104708
- }
104709
- return masked;
104710
- }
104711
- function mergeWithPasswordPreservation(existing, incoming, manifest) {
104712
- const result2 = { ...existing, ...incoming };
104713
- if (!manifest) return result2;
104714
- for (const field of manifest.configFields) {
104715
- if (field.type !== "password") continue;
104716
- const parts = field.key.split(".");
104717
- const incomingValue = getNestedValue(incoming, parts);
104718
- if (incomingValue === "" || incomingValue === "***" || incomingValue === void 0) {
104719
- const existingValue = getNestedValue(existing, parts);
104720
- if (existingValue !== void 0) {
104721
- setNestedValue(result2, parts, existingValue);
104722
- }
105085
+ subsystem.bindingRouter = new BindingRouter({
105086
+ bindingStore,
105087
+ relayCore: deps.relayCore,
105088
+ agentManager: sessionCreator,
105089
+ meshCore: deps.meshCore,
105090
+ relayDir,
105091
+ resolveAdapterInstanceId: deps.resolveAdapterInstanceId
105092
+ });
105093
+ await subsystem.bindingRouter.init();
105094
+ logger.info("[BindingSubsystem] BindingRouter initialized");
105095
+ return subsystem;
105096
+ } catch (err) {
105097
+ logger.warn("[BindingSubsystem] Failed to initialize binding subsystem:", err);
105098
+ return void 0;
104723
105099
  }
104724
105100
  }
104725
- return result2;
104726
- }
104727
- function getNestedValue(obj, parts) {
104728
- let current = obj;
104729
- for (const part of parts) {
104730
- if (current == null || typeof current !== "object") return void 0;
104731
- current = current[part];
105101
+ /** Get the BindingStore. */
105102
+ getBindingStore() {
105103
+ return this.bindingStore;
104732
105104
  }
104733
- return current;
104734
- }
104735
- function setNestedValue(obj, parts, value) {
104736
- let current = obj;
104737
- for (let i2 = 0; i2 < parts.length - 1; i2++) {
104738
- if (!(parts[i2] in current) || typeof current[parts[i2]] !== "object") {
104739
- current[parts[i2]] = {};
104740
- }
104741
- current = current[parts[i2]];
105105
+ /** Get the AgentSessionStore. */
105106
+ getAgentSessionStore() {
105107
+ return this.agentSessionStore;
104742
105108
  }
104743
- current[parts.at(-1)] = value;
104744
- }
104745
-
104746
- // ../../apps/server/src/services/relay/adapter-factory.ts
104747
- import { dirname as dirname5 } from "node:path";
104748
- function defaultAdapterStatus() {
104749
- return {
104750
- state: "disconnected",
104751
- messageCount: { inbound: 0, outbound: 0 },
104752
- errorCount: 0
104753
- };
104754
- }
104755
- async function createAdapter(config, deps, configPath, onPluginManifest) {
104756
- switch (config.type) {
104757
- case "telegram": {
104758
- const adapter = new TelegramAdapter(
104759
- config.id,
104760
- config.config
104761
- );
104762
- adapter.setLogger(createTaggedLogger(`telegram:${config.id}`));
104763
- return adapter;
104764
- }
104765
- case "webhook":
104766
- return new WebhookAdapter(
104767
- config.id,
104768
- config.config
104769
- );
104770
- case "slack": {
104771
- const adapter = new SlackAdapter(
104772
- config.id,
104773
- config.config
104774
- );
104775
- adapter.setLogger(createTaggedLogger(`slack:${config.id}`));
104776
- return adapter;
104777
- }
104778
- case "claude-code":
104779
- return new ClaudeCodeAdapter(
104780
- config.id,
104781
- config.config,
104782
- {
104783
- agentManager: deps.agentManager,
104784
- traceStore: deps.traceStore,
104785
- pulseStore: deps.pulseStore,
104786
- agentSessionStore: deps.agentSessionStore,
104787
- logger
104788
- }
104789
- );
104790
- case "plugin":
104791
- return loadPluginAdapter(config, configPath, onPluginManifest);
104792
- default:
104793
- logger.warn(`[AdapterFactory] Unknown adapter type: ${config.type}`);
104794
- return null;
105109
+ /** Get the BindingRouter, or undefined if initialization did not reach that step. */
105110
+ getBindingRouter() {
105111
+ return this.bindingRouter;
104795
105112
  }
104796
- }
104797
- var CONNECTION_TEST_TIMEOUT_MS = 15e3;
104798
- async function testAdapterConnection(adapter) {
104799
- try {
104800
- if (adapter.testConnection) {
104801
- let timer;
104802
- try {
104803
- return await Promise.race([
104804
- adapter.testConnection(),
104805
- new Promise((_4, reject) => {
104806
- timer = setTimeout(
104807
- () => reject(new Error("Connection test timed out")),
104808
- CONNECTION_TEST_TIMEOUT_MS
104809
- );
104810
- })
104811
- ]);
104812
- } finally {
104813
- clearTimeout(timer);
104814
- }
104815
- }
104816
- const noopRelay = {
104817
- publish: async () => ({ messageId: "", deliveredTo: 0 }),
104818
- onSignal: () => () => {
104819
- },
104820
- subscribe: () => () => {
104821
- }
104822
- };
104823
- let fallbackTimer;
104824
- try {
104825
- await Promise.race([
104826
- adapter.start(noopRelay),
104827
- new Promise((_4, reject) => {
104828
- fallbackTimer = setTimeout(
104829
- () => reject(new Error("Connection test timed out")),
104830
- CONNECTION_TEST_TIMEOUT_MS
104831
- );
104832
- })
104833
- ]);
104834
- } finally {
104835
- clearTimeout(fallbackTimer);
104836
- }
104837
- return { ok: true };
104838
- } catch (err) {
104839
- const message = err instanceof Error ? err.message : String(err);
104840
- return { ok: false, error: message };
104841
- } finally {
104842
- try {
104843
- await adapter.stop();
104844
- } catch {
105113
+ /** Shut down the BindingRouter, AgentSessionStore, and BindingStore. Idempotent. */
105114
+ async shutdown() {
105115
+ if (this.isShutdown) return;
105116
+ this.isShutdown = true;
105117
+ if (this.bindingRouter) {
105118
+ await this.bindingRouter.shutdown();
104845
105119
  }
105120
+ await this.agentSessionStore.shutdown();
105121
+ await this.bindingStore.shutdown();
104846
105122
  }
104847
- }
104848
- async function loadPluginAdapter(config, configPath, onPluginManifest) {
104849
- if (!config.plugin) {
104850
- logger.warn(`[AdapterFactory] Plugin adapter '${config.id}' missing plugin source config`);
104851
- return null;
104852
- }
104853
- const builtinMap = /* @__PURE__ */ new Map();
104854
- const configDir = dirname5(configPath);
104855
- const results = await loadAdapters(
104856
- [
104857
- {
104858
- id: config.id,
104859
- type: config.type,
104860
- enabled: config.enabled,
104861
- plugin: config.plugin,
104862
- config: config.config
104863
- }
104864
- ],
104865
- builtinMap,
104866
- configDir
104867
- );
104868
- const result2 = results[0];
104869
- if (!result2) return null;
104870
- if (result2.manifest && onPluginManifest) {
104871
- onPluginManifest(config.type, result2.manifest);
104872
- }
104873
- return result2.adapter;
104874
- }
105123
+ };
104875
105124
 
104876
105125
  // ../../apps/server/src/services/relay/adapter-manager.ts
104877
105126
  var AdapterManager = class {
@@ -104881,9 +105130,7 @@ var AdapterManager = class {
104881
105130
  configs = [];
104882
105131
  deps;
104883
105132
  manifests = /* @__PURE__ */ new Map();
104884
- bindingStore;
104885
- agentSessionStore;
104886
- bindingRouter;
105133
+ bindingSubsystem;
104887
105134
  constructor(registry2, configPath, deps) {
104888
105135
  this.registry = registry2;
104889
105136
  this.configPath = configPath;
@@ -104895,56 +105142,30 @@ var AdapterManager = class {
104895
105142
  await this.enrichManifestsWithDocs();
104896
105143
  await ensureDefaultAdapterConfig(this.configPath);
104897
105144
  this.configs = await loadAdapterConfig(this.configPath);
105145
+ await this.initBindingSubsystem();
104898
105146
  await this.startEnabledAdapters();
104899
105147
  this.configWatcher = watchAdapterConfig(this.configPath, () => {
104900
105148
  this.reload().catch((err) => {
104901
105149
  logger.warn("[AdapterManager] Hot-reload failed:", err);
104902
105150
  });
104903
105151
  });
104904
- await this.initBindingSubsystem();
104905
105152
  }
104906
- /** Initialize the binding store and router subsystem. Non-fatal on failure. */
105153
+ /** Initialize the binding subsystem. Non-fatal on failure — logs and continues. */
104907
105154
  async initBindingSubsystem() {
104908
- if (!this.deps.relayCore) {
104909
- logger.info("[AdapterManager] relayCore not provided, skipping binding subsystem");
105155
+ if (!this.deps.relayCore || !this.deps.meshCore) {
105156
+ logger.info("[AdapterManager] relayCore or meshCore not provided, skipping binding subsystem");
104910
105157
  return;
104911
105158
  }
104912
- const relayDir = dirname6(this.configPath);
104913
- try {
104914
- this.bindingStore = new BindingStore(relayDir);
104915
- await this.bindingStore.init();
104916
- logger.info("[AdapterManager] BindingStore initialized");
104917
- this.agentSessionStore = new AgentSessionStore(relayDir);
104918
- await this.agentSessionStore.init();
104919
- logger.info("[AdapterManager] AgentSessionStore initialized");
104920
- const agentManager = this.deps.agentManager;
104921
- const sessionCreator = {
104922
- async createSession(cwd, permissionMode) {
104923
- const id = crypto.randomUUID();
104924
- agentManager.ensureSession(id, { permissionMode: permissionMode ?? "acceptEdits", cwd });
104925
- return { id };
104926
- }
104927
- };
104928
- if (!this.deps.meshCore) {
104929
- logger.info("[AdapterManager] meshCore not provided, skipping binding subsystem");
104930
- return;
105159
+ this.bindingSubsystem = await BindingSubsystem.init({
105160
+ relayCore: this.deps.relayCore,
105161
+ meshCore: this.deps.meshCore,
105162
+ agentManager: this.deps.agentManager,
105163
+ configPath: this.configPath,
105164
+ resolveAdapterInstanceId: (platformType) => {
105165
+ const match = this.configs.find((c3) => c3.type === platformType && c3.enabled);
105166
+ return match?.id;
104931
105167
  }
104932
- this.bindingRouter = new BindingRouter({
104933
- bindingStore: this.bindingStore,
104934
- relayCore: this.deps.relayCore,
104935
- agentManager: sessionCreator,
104936
- meshCore: this.deps.meshCore,
104937
- relayDir,
104938
- resolveAdapterInstanceId: (platformType) => {
104939
- const match = this.configs.find((c3) => c3.type === platformType && c3.enabled);
104940
- return match?.id;
104941
- }
104942
- });
104943
- await this.bindingRouter.init();
104944
- logger.info("[AdapterManager] BindingRouter initialized");
104945
- } catch (err) {
104946
- logger.warn("[AdapterManager] Failed to initialize binding subsystem:", err);
104947
- }
105168
+ });
104948
105169
  }
104949
105170
  /** Reload config from disk and reconcile adapter state. */
104950
105171
  async reload() {
@@ -105031,15 +105252,15 @@ var AdapterManager = class {
105031
105252
  }
105032
105253
  /** Get the BindingStore, or undefined if binding subsystem was not initialized. */
105033
105254
  getBindingStore() {
105034
- return this.bindingStore;
105255
+ return this.bindingSubsystem?.getBindingStore();
105035
105256
  }
105036
105257
  /** Get the AgentSessionStore, or undefined if binding subsystem was not initialized. */
105037
105258
  getAgentSessionStore() {
105038
- return this.agentSessionStore;
105259
+ return this.bindingSubsystem?.getAgentSessionStore();
105039
105260
  }
105040
105261
  /** Get the BindingRouter, or undefined if binding subsystem was not initialized. */
105041
105262
  getBindingRouter() {
105042
- return this.bindingRouter;
105263
+ return this.bindingSubsystem?.getBindingRouter();
105043
105264
  }
105044
105265
  /** Enrich AdapterContext with Mesh agent info if meshCore is available. */
105045
105266
  buildContext(subject) {
@@ -105147,10 +105368,11 @@ var AdapterManager = class {
105147
105368
  }
105148
105369
  this.configs.splice(index2, 1);
105149
105370
  await saveAdapterConfig(this.configPath, this.configs);
105150
- if (this.bindingStore) {
105151
- const orphanBindings = this.bindingStore.getAll().filter((b3) => b3.adapterId === id);
105371
+ const bindingStore = this.bindingSubsystem?.getBindingStore();
105372
+ if (bindingStore) {
105373
+ const orphanBindings = bindingStore.getAll().filter((b3) => b3.adapterId === id);
105152
105374
  for (const binding of orphanBindings) {
105153
- await this.bindingStore.delete(binding.id);
105375
+ await bindingStore.delete(binding.id);
105154
105376
  }
105155
105377
  if (orphanBindings.length > 0) {
105156
105378
  logger.info(
@@ -105189,13 +105411,9 @@ var AdapterManager = class {
105189
105411
  }
105190
105412
  /** Stop all adapters and the config file watcher. */
105191
105413
  async shutdown() {
105192
- if (this.bindingRouter) {
105193
- await this.bindingRouter.shutdown();
105194
- this.bindingRouter = void 0;
105195
- }
105196
- if (this.bindingStore) {
105197
- await this.bindingStore.shutdown();
105198
- this.bindingStore = void 0;
105414
+ if (this.bindingSubsystem) {
105415
+ await this.bindingSubsystem.shutdown();
105416
+ this.bindingSubsystem = void 0;
105199
105417
  }
105200
105418
  if (this.configWatcher) {
105201
105419
  await this.configWatcher.close();
@@ -105229,7 +105447,7 @@ var AdapterManager = class {
105229
105447
  async buildAdapter(config) {
105230
105448
  return createAdapter(
105231
105449
  config,
105232
- { ...this.deps, agentSessionStore: this.agentSessionStore },
105450
+ { ...this.deps, agentSessionStore: this.bindingSubsystem?.getAgentSessionStore() },
105233
105451
  this.configPath,
105234
105452
  (type, manifest) => this.registerPluginManifest(type, manifest)
105235
105453
  );
@@ -105301,7 +105519,7 @@ var AdapterManager = class {
105301
105519
  resolveAdapterDocsPath(adapterType) {
105302
105520
  const require2 = createRequire(import.meta.url);
105303
105521
  const relayEntry = require2.resolve("@dorkos/relay");
105304
- const distDir = dirname6(relayEntry);
105522
+ const distDir = dirname7(relayEntry);
105305
105523
  return join12(distDir, "adapters", adapterType, "docs");
105306
105524
  }
105307
105525
  };
@@ -116188,7 +116406,7 @@ async function start() {
116188
116406
  adapterRegistry.setLogger(logger);
116189
116407
  traceStore = new TraceStore(db);
116190
116408
  logger.info("[Relay] TraceStore initialized");
116191
- relayCore = new RelayCore({ dataDir: relayDataDir, adapterRegistry, db, traceStore });
116409
+ relayCore = new RelayCore({ dataDir: relayDataDir, adapterRegistry, db, traceStore, logger });
116192
116410
  await relayCore.registerEndpoint("relay.system.console");
116193
116411
  logger.info(`[Relay] RelayCore initialized (dataDir: ${relayDataDir})`);
116194
116412
  } catch (err) {
@@ -116374,7 +116592,10 @@ async function shutdownServices() {
116374
116592
  }
116375
116593
  await tunnelManager.stop();
116376
116594
  }
116595
+ var shuttingDown = false;
116377
116596
  async function shutdown() {
116597
+ if (shuttingDown) return;
116598
+ shuttingDown = true;
116378
116599
  logger.info("Shutting down...");
116379
116600
  await shutdownServices();
116380
116601
  process.exit(0);