dorkos 0.14.0 → 0.16.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.
@@ -11744,7 +11744,7 @@ var require_multipart = __commonJS({
11744
11744
  var BUF_CRLF = Buffer.from("\r\n");
11745
11745
  var BUF_CR = Buffer.from("\r");
11746
11746
  var BUF_DASH = Buffer.from("-");
11747
- function noop() {
11747
+ function noop2() {
11748
11748
  }
11749
11749
  var MAX_HEADER_PAIRS = 2e3;
11750
11750
  var MAX_HEADER_SIZE = 16 * 1024;
@@ -12097,7 +12097,7 @@ var require_multipart = __commonJS({
12097
12097
  return;
12098
12098
  }
12099
12099
  const writecb = this._writecb;
12100
- this._writecb = noop;
12100
+ this._writecb = noop2;
12101
12101
  ssCb(false, BUF_DASH, 0, 1, false);
12102
12102
  this._writecb = writecb;
12103
12103
  } else if (matchPostBoundary === 3) {
@@ -12112,7 +12112,7 @@ var require_multipart = __commonJS({
12112
12112
  continue retrydata;
12113
12113
  } else {
12114
12114
  const writecb = this._writecb;
12115
- this._writecb = noop;
12115
+ this._writecb = noop2;
12116
12116
  ssCb(false, BUF_CR, 0, 1, false);
12117
12117
  this._writecb = writecb;
12118
12118
  }
@@ -15321,7 +15321,7 @@ var require_end_of_stream = __commonJS({
15321
15321
  callback.apply(this, args);
15322
15322
  };
15323
15323
  }
15324
- function noop() {
15324
+ function noop2() {
15325
15325
  }
15326
15326
  function isRequest(stream) {
15327
15327
  return stream.setHeader && typeof stream.abort === "function";
@@ -15329,7 +15329,7 @@ var require_end_of_stream = __commonJS({
15329
15329
  function eos(stream, opts, callback) {
15330
15330
  if (typeof opts === "function") return eos(stream, null, opts);
15331
15331
  if (!opts) opts = {};
15332
- callback = once(callback || noop);
15332
+ callback = once(callback || noop2);
15333
15333
  var readable = opts.readable || opts.readable !== false && stream.readable;
15334
15334
  var writable = opts.writable || opts.writable !== false && stream.writable;
15335
15335
  var onlegacyfinish = function onlegacyfinish2() {
@@ -16565,7 +16565,7 @@ var require_pipeline = __commonJS({
16565
16565
  var _require$codes = require_errors().codes;
16566
16566
  var ERR_MISSING_ARGS = _require$codes.ERR_MISSING_ARGS;
16567
16567
  var ERR_STREAM_DESTROYED = _require$codes.ERR_STREAM_DESTROYED;
16568
- function noop(err) {
16568
+ function noop2(err) {
16569
16569
  if (err) throw err;
16570
16570
  }
16571
16571
  function isRequest(stream) {
@@ -16603,8 +16603,8 @@ var require_pipeline = __commonJS({
16603
16603
  return from.pipe(to);
16604
16604
  }
16605
16605
  function popCallback(streams) {
16606
- if (!streams.length) return noop;
16607
- if (typeof streams[streams.length - 1] !== "function") return noop;
16606
+ if (!streams.length) return noop2;
16607
+ if (typeof streams[streams.length - 1] !== "function") return noop2;
16608
16608
  return streams.pop();
16609
16609
  }
16610
16610
  function pipeline() {
@@ -33700,7 +33700,7 @@ var require_follow_redirects = __commonJS({
33700
33700
  "ERR_STREAM_WRITE_AFTER_END",
33701
33701
  "write after end"
33702
33702
  );
33703
- var destroy = Writable.prototype.destroy || noop;
33703
+ var destroy = Writable.prototype.destroy || noop2;
33704
33704
  function RedirectableRequest(options, responseCallback) {
33705
33705
  Writable.call(this);
33706
33706
  this._sanitizeOptions(options);
@@ -34029,7 +34029,7 @@ var require_follow_redirects = __commonJS({
34029
34029
  });
34030
34030
  return exports2;
34031
34031
  }
34032
- function noop() {
34032
+ function noop2() {
34033
34033
  }
34034
34034
  function parseUrl(input) {
34035
34035
  var parsed;
@@ -34105,7 +34105,7 @@ var require_follow_redirects = __commonJS({
34105
34105
  for (var event of events) {
34106
34106
  request.removeListener(event, eventHandlers[event]);
34107
34107
  }
34108
- request.on("error", noop);
34108
+ request.on("error", noop2);
34109
34109
  request.destroy(error);
34110
34110
  }
34111
34111
  function isSubdomain(subdomain, domain2) {
@@ -34471,7 +34471,7 @@ var require_axios = __commonJS({
34471
34471
  isArray(arrayOrString) ? define(arrayOrString) : define(String(arrayOrString).split(delimiter));
34472
34472
  return obj;
34473
34473
  };
34474
- var noop = () => {
34474
+ var noop2 = () => {
34475
34475
  };
34476
34476
  var toFiniteNumber = (value, defaultValue) => {
34477
34477
  return value != null && Number.isFinite(value = +value) ? value : defaultValue;
@@ -34577,7 +34577,7 @@ var require_axios = __commonJS({
34577
34577
  freezeMethods,
34578
34578
  toObjectSet,
34579
34579
  toCamelCase: toCamelCase2,
34580
- noop,
34580
+ noop: noop2,
34581
34581
  toFiniteNumber,
34582
34582
  findKey,
34583
34583
  global: _global,
@@ -55755,7 +55755,7 @@ var require_websocket = __commonJS({
55755
55755
  var http = __require("http");
55756
55756
  var net = __require("net");
55757
55757
  var tls = __require("tls");
55758
- var { randomBytes, createHash: createHash3 } = __require("crypto");
55758
+ var { randomBytes: randomBytes2, createHash: createHash3 } = __require("crypto");
55759
55759
  var { Duplex, Readable: Readable3 } = __require("stream");
55760
55760
  var { URL: URL2 } = __require("url");
55761
55761
  var PerMessageDeflate = require_permessage_deflate();
@@ -56285,7 +56285,7 @@ var require_websocket = __commonJS({
56285
56285
  }
56286
56286
  }
56287
56287
  const defaultPort = isSecure ? 443 : 80;
56288
- const key = randomBytes(16).toString("base64");
56288
+ const key = randomBytes2(16).toString("base64");
56289
56289
  const request = isSecure ? https.request : http.request;
56290
56290
  const protocolSet = /* @__PURE__ */ new Set();
56291
56291
  let perMessageDeflate;
@@ -68835,9 +68835,11 @@ var StreamEventTypeSchema = z.enum([
68835
68835
  "tool_call_delta",
68836
68836
  "tool_call_end",
68837
68837
  "tool_result",
68838
+ "tool_progress",
68838
68839
  "approval_required",
68839
68840
  "question_prompt",
68840
68841
  "error",
68842
+ "rate_limit",
68841
68843
  "done",
68842
68844
  "session_status",
68843
68845
  "task_update",
@@ -68845,7 +68847,18 @@ var StreamEventTypeSchema = z.enum([
68845
68847
  "sync_connected",
68846
68848
  "relay_receipt",
68847
68849
  "message_delivered",
68848
- "relay_message"
68850
+ "relay_message",
68851
+ "thinking_delta",
68852
+ "subagent_started",
68853
+ "subagent_progress",
68854
+ "subagent_done",
68855
+ "system_status",
68856
+ "compact_boundary",
68857
+ "prompt_suggestion",
68858
+ "hook_started",
68859
+ "hook_progress",
68860
+ "hook_response",
68861
+ "presence_update"
68849
68862
  ]).openapi("StreamEventType");
68850
68863
  var QuestionOptionSchema = z.object({
68851
68864
  label: z.string(),
@@ -68899,6 +68912,9 @@ var CommandsQuerySchema = z.object({
68899
68912
  var TextDeltaSchema = z.object({
68900
68913
  text: z.string()
68901
68914
  }).openapi("TextDelta");
68915
+ var ThinkingDeltaSchema = z.object({
68916
+ text: z.string()
68917
+ }).openapi("ThinkingDelta");
68902
68918
  var ToolCallStatusSchema = z.enum(["pending", "running", "complete", "error"]);
68903
68919
  var ToolCallEventSchema = z.object({
68904
68920
  toolCallId: z.string(),
@@ -68907,19 +68923,30 @@ var ToolCallEventSchema = z.object({
68907
68923
  result: z.string().optional(),
68908
68924
  status: ToolCallStatusSchema
68909
68925
  }).openapi("ToolCallEvent");
68926
+ var ToolProgressEventSchema = z.object({
68927
+ toolCallId: z.string(),
68928
+ content: z.string()
68929
+ }).openapi("ToolProgressEvent");
68910
68930
  var ApprovalEventSchema = z.object({
68911
68931
  toolCallId: z.string(),
68912
68932
  toolName: z.string(),
68913
- input: z.string()
68933
+ input: z.string(),
68934
+ timeoutMs: z.number().describe("Server-side approval timeout in milliseconds")
68914
68935
  }).openapi("ApprovalEvent");
68915
68936
  var QuestionPromptEventSchema = z.object({
68916
68937
  toolCallId: z.string(),
68917
68938
  questions: z.array(QuestionItemSchema)
68918
68939
  }).openapi("QuestionPromptEvent");
68940
+ var ErrorCategorySchema = z.enum(["max_turns", "execution_error", "budget_exceeded", "output_format_error"]).openapi("ErrorCategory");
68919
68941
  var ErrorEventSchema = z.object({
68920
68942
  message: z.string(),
68921
- code: z.string().optional()
68943
+ code: z.string().optional(),
68944
+ category: ErrorCategorySchema.optional(),
68945
+ details: z.string().optional()
68922
68946
  }).openapi("ErrorEvent");
68947
+ var RateLimitEventSchema = z.object({
68948
+ retryAfter: z.number().optional()
68949
+ }).openapi("RateLimitEvent");
68923
68950
  var DoneEventSchema = z.object({
68924
68951
  sessionId: z.string()
68925
68952
  }).openapi("DoneEvent");
@@ -68928,7 +68955,8 @@ var SessionStatusEventSchema = z.object({
68928
68955
  model: z.string().optional(),
68929
68956
  costUsd: z.number().optional(),
68930
68957
  contextTokens: z.number().int().optional(),
68931
- contextMaxTokens: z.number().int().optional()
68958
+ contextMaxTokens: z.number().int().optional(),
68959
+ outputTokens: z.number().int().optional()
68932
68960
  }).openapi("SessionStatusEvent");
68933
68961
  var TaskItemSchema = z.object({
68934
68962
  id: z.string(),
@@ -68966,14 +68994,75 @@ var RelayMessageEventSchema = z.object({
68966
68994
  subject: z.string().optional(),
68967
68995
  from: z.string().optional()
68968
68996
  }).openapi("RelayMessageEvent");
68997
+ var SubagentStartedEventSchema = z.object({
68998
+ taskId: z.string(),
68999
+ subagentSessionId: z.string(),
69000
+ toolUseId: z.string().optional(),
69001
+ description: z.string()
69002
+ }).openapi("SubagentStartedEvent");
69003
+ var SubagentProgressEventSchema = z.object({
69004
+ taskId: z.string(),
69005
+ toolUses: z.number().int(),
69006
+ lastToolName: z.string().optional(),
69007
+ durationMs: z.number().int()
69008
+ }).openapi("SubagentProgressEvent");
69009
+ var SubagentDoneEventSchema = z.object({
69010
+ taskId: z.string(),
69011
+ status: z.enum(["completed", "failed", "stopped"]),
69012
+ summary: z.string().optional(),
69013
+ toolUses: z.number().int().optional(),
69014
+ durationMs: z.number().int().optional()
69015
+ }).openapi("SubagentDoneEvent");
69016
+ var SystemStatusEventSchema = z.object({
69017
+ message: z.string()
69018
+ }).openapi("SystemStatusEvent");
69019
+ var CompactBoundaryEventSchema = z.object({}).openapi("CompactBoundaryEvent");
69020
+ var PromptSuggestionEventSchema = z.object({
69021
+ suggestions: z.array(z.string())
69022
+ }).openapi("PromptSuggestionEvent");
69023
+ var HookStartedEventSchema = z.object({
69024
+ hookId: z.string(),
69025
+ hookName: z.string(),
69026
+ hookEvent: z.string(),
69027
+ toolCallId: z.string().nullable()
69028
+ }).openapi("HookStartedEvent");
69029
+ var HookProgressEventSchema = z.object({
69030
+ hookId: z.string(),
69031
+ stdout: z.string(),
69032
+ stderr: z.string()
69033
+ }).openapi("HookProgressEvent");
69034
+ var HookResponseEventSchema = z.object({
69035
+ hookId: z.string(),
69036
+ hookName: z.string(),
69037
+ exitCode: z.number().optional(),
69038
+ outcome: z.enum(["success", "error", "cancelled"]),
69039
+ stdout: z.string(),
69040
+ stderr: z.string()
69041
+ }).openapi("HookResponseEvent");
69042
+ var PresenceClientSchema = z.object({
69043
+ type: z.enum(["web", "obsidian", "mcp", "unknown"]),
69044
+ connectedAt: z.string()
69045
+ });
69046
+ var PresenceUpdateEventSchema = z.object({
69047
+ sessionId: z.string(),
69048
+ clientCount: z.number().int(),
69049
+ clients: z.array(PresenceClientSchema),
69050
+ lockInfo: z.object({
69051
+ clientId: z.string(),
69052
+ acquiredAt: z.string()
69053
+ }).nullable()
69054
+ }).openapi("PresenceUpdateEvent");
68969
69055
  var StreamEventSchema = z.object({
68970
69056
  type: StreamEventTypeSchema,
68971
69057
  data: z.union([
68972
69058
  TextDeltaSchema,
69059
+ ThinkingDeltaSchema,
68973
69060
  ToolCallEventSchema,
69061
+ ToolProgressEventSchema,
68974
69062
  ApprovalEventSchema,
68975
69063
  QuestionPromptEventSchema,
68976
69064
  ErrorEventSchema,
69065
+ RateLimitEventSchema,
68977
69066
  DoneEventSchema,
68978
69067
  SessionStatusEventSchema,
68979
69068
  TaskUpdateEventSchema,
@@ -68981,31 +69070,84 @@ var StreamEventSchema = z.object({
68981
69070
  SyncConnectedEventSchema,
68982
69071
  RelayReceiptEventSchema,
68983
69072
  MessageDeliveredEventSchema,
68984
- RelayMessageEventSchema
69073
+ RelayMessageEventSchema,
69074
+ SubagentStartedEventSchema,
69075
+ SubagentProgressEventSchema,
69076
+ SubagentDoneEventSchema,
69077
+ SystemStatusEventSchema,
69078
+ CompactBoundaryEventSchema,
69079
+ PromptSuggestionEventSchema,
69080
+ HookStartedEventSchema,
69081
+ HookProgressEventSchema,
69082
+ HookResponseEventSchema,
69083
+ PresenceUpdateEventSchema
68985
69084
  ])
68986
69085
  }).openapi("StreamEvent");
68987
69086
  var TextPartSchema = z.object({
68988
69087
  type: z.literal("text"),
68989
69088
  text: z.string()
68990
69089
  }).openapi("TextPart");
69090
+ var HookStatusSchema = z.enum(["running", "success", "error", "cancelled"]);
69091
+ var HookPartSchema = z.object({
69092
+ hookId: z.string(),
69093
+ hookName: z.string(),
69094
+ hookEvent: z.string(),
69095
+ status: HookStatusSchema,
69096
+ stdout: z.string(),
69097
+ stderr: z.string(),
69098
+ exitCode: z.number().optional()
69099
+ });
68991
69100
  var ToolCallPartSchema = z.object({
68992
69101
  type: z.literal("tool_call"),
68993
69102
  toolCallId: z.string(),
68994
69103
  toolName: z.string(),
68995
69104
  input: z.string().optional(),
68996
69105
  result: z.string().optional(),
69106
+ progressOutput: z.string().optional(),
68997
69107
  status: ToolCallStatusSchema,
68998
69108
  interactiveType: z.enum(["approval", "question"]).optional(),
68999
69109
  questions: z.array(QuestionItemSchema).optional(),
69000
- answers: z.record(z.string(), z.string()).optional()
69110
+ answers: z.record(z.string(), z.string()).optional(),
69111
+ timeoutMs: z.number().optional().describe("Approval timeout duration in milliseconds"),
69112
+ hooks: z.array(HookPartSchema).optional()
69001
69113
  }).openapi("ToolCallPart");
69002
- var MessagePartSchema = z.discriminatedUnion("type", [TextPartSchema, ToolCallPartSchema]);
69114
+ var SubagentStatusSchema = z.enum(["running", "complete", "error"]);
69115
+ var SubagentPartSchema = z.object({
69116
+ type: z.literal("subagent"),
69117
+ taskId: z.string(),
69118
+ description: z.string(),
69119
+ status: SubagentStatusSchema,
69120
+ toolUses: z.number().int().optional(),
69121
+ lastToolName: z.string().optional(),
69122
+ durationMs: z.number().int().optional(),
69123
+ summary: z.string().optional()
69124
+ }).openapi("SubagentPart");
69125
+ var ThinkingPartSchema = z.object({
69126
+ type: z.literal("thinking"),
69127
+ text: z.string(),
69128
+ isStreaming: z.boolean().optional(),
69129
+ elapsedMs: z.number().int().optional()
69130
+ }).openapi("ThinkingPart");
69131
+ var ErrorPartSchema = z.object({
69132
+ type: z.literal("error"),
69133
+ message: z.string(),
69134
+ category: ErrorCategorySchema.optional(),
69135
+ details: z.string().optional()
69136
+ }).openapi("ErrorPart");
69137
+ var MessagePartSchema = z.discriminatedUnion("type", [
69138
+ TextPartSchema,
69139
+ ToolCallPartSchema,
69140
+ SubagentPartSchema,
69141
+ ThinkingPartSchema,
69142
+ ErrorPartSchema
69143
+ ]);
69003
69144
  var MessageTypeSchema = z.enum(["command", "compaction"]).openapi("MessageType");
69004
69145
  var HistoryToolCallSchema = z.object({
69005
69146
  toolCallId: z.string(),
69006
69147
  toolName: z.string(),
69007
69148
  input: z.string().optional(),
69008
69149
  result: z.string().optional(),
69150
+ progressOutput: z.string().optional(),
69009
69151
  status: z.literal("complete"),
69010
69152
  questions: z.array(QuestionItemSchema).optional(),
69011
69153
  answers: z.record(z.string(), z.string()).optional()
@@ -70918,7 +71060,7 @@ var SERVER_VERSION = resolveVersion();
70918
71060
  var IS_DEV_BUILD = checkDevBuild(SERVER_VERSION);
70919
71061
  function resolveVersion() {
70920
71062
  if (env.DORKOS_VERSION_OVERRIDE) return env.DORKOS_VERSION_OVERRIDE;
70921
- if (true) return "0.14.0";
71063
+ if (true) return "0.16.0";
70922
71064
  const pkgPath = path4.join(path4.dirname(fileURLToPath2(import.meta.url)), "../../package.json");
70923
71065
  return JSON.parse(readFileSync(pkgPath, "utf-8")).version;
70924
71066
  }
@@ -72128,7 +72270,8 @@ var TelegramAdapterConfigSchema = z11.object({
72128
72270
  mode: z11.enum(["polling", "webhook"]).default("polling"),
72129
72271
  webhookUrl: z11.string().url().optional(),
72130
72272
  webhookPort: z11.number().int().positive().optional(),
72131
- webhookSecret: z11.string().min(1).optional()
72273
+ webhookSecret: z11.string().min(1).optional(),
72274
+ streaming: z11.boolean().default(true)
72132
72275
  }).openapi("TelegramAdapterConfig");
72133
72276
  var WebhookInboundConfigSchema = z11.object({
72134
72277
  subject: z11.string().min(1),
@@ -72149,6 +72292,7 @@ var SlackAdapterConfigSchema = z11.object({
72149
72292
  appToken: z11.string().min(1),
72150
72293
  signingSecret: z11.string().min(1),
72151
72294
  streaming: z11.boolean().default(true),
72295
+ nativeStreaming: z11.boolean().default(true),
72152
72296
  typingIndicator: z11.enum(["none", "reaction"]).default("none")
72153
72297
  }).openapi("SlackAdapterConfig");
72154
72298
  var AdapterConfigSchema = z11.object({
@@ -74206,7 +74350,9 @@ function parseTranscript(lines) {
74206
74350
  const parts = [];
74207
74351
  const toolCalls = [];
74208
74352
  for (const block of contentBlocks) {
74209
- if (block.type === "text" && block.text) {
74353
+ if (block.type === "thinking" && block.thinking) {
74354
+ parts.push({ type: "thinking", text: block.thinking, isStreaming: false });
74355
+ } else if (block.type === "text" && block.text) {
74210
74356
  const lastPart = parts[parts.length - 1];
74211
74357
  if (lastPart && lastPart.type === "text") {
74212
74358
  lastPart.text += "\n" + block.text;
@@ -76292,9 +76438,16 @@ var esm_default = { watch, FSWatcher };
76292
76438
 
76293
76439
  // ../../apps/server/src/services/runtimes/claude-code/session-broadcaster.ts
76294
76440
  import { join as join3 } from "path";
76441
+ function inferClientType(clientId) {
76442
+ if (clientId.startsWith("web-")) return "web";
76443
+ if (clientId.startsWith("obsidian-")) return "obsidian";
76444
+ if (clientId.startsWith("mcp-")) return "mcp";
76445
+ return "unknown";
76446
+ }
76295
76447
  var SessionBroadcaster = class {
76296
- constructor(transcriptReader) {
76448
+ constructor(transcriptReader, lockManager) {
76297
76449
  this.transcriptReader = transcriptReader;
76450
+ this.lockManager = lockManager;
76298
76451
  }
76299
76452
  clients = /* @__PURE__ */ new Map();
76300
76453
  callbacks = /* @__PURE__ */ new Map();
@@ -76317,18 +76470,19 @@ var SessionBroadcaster = class {
76317
76470
  /**
76318
76471
  * Register an SSE client for a session.
76319
76472
  *
76320
- * - Adds the response to the set of connected clients
76473
+ * - Adds the response to the client metadata map
76321
76474
  * - Starts a file watcher if none exists for this session
76322
76475
  * - Initializes offset to current file size (only broadcast new content)
76323
76476
  * - Sends sync_connected event to the client
76324
76477
  * - Auto-deregisters on response close
76478
+ * - Broadcasts presence update to all clients for this session
76325
76479
  *
76326
76480
  * @param sessionId - Session UUID
76327
76481
  * @param vaultRoot - Vault root path for resolving transcript directory
76328
76482
  * @param res - Express Response object configured for SSE
76329
- * @param clientId - Optional client identifier
76483
+ * @param clientId - Optional client identifier (used for type inference via prefix convention)
76330
76484
  */
76331
- registerClient(sessionId, vaultRoot2, res, _clientId) {
76485
+ registerClient(sessionId, vaultRoot2, res, clientId) {
76332
76486
  if (this.totalClientCount >= SSE.MAX_TOTAL_CLIENTS) {
76333
76487
  res.status(503).json({ error: "SSE connection limit reached", code: "SSE_LIMIT" });
76334
76488
  return;
@@ -76338,10 +76492,17 @@ var SessionBroadcaster = class {
76338
76492
  res.status(503).json({ error: "Too many connections for this session", code: "SSE_SESSION_LIMIT" });
76339
76493
  return;
76340
76494
  }
76495
+ const resolvedClientId = clientId ?? `unknown-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
76341
76496
  if (!this.clients.has(sessionId)) {
76342
- this.clients.set(sessionId, /* @__PURE__ */ new Set());
76497
+ this.clients.set(sessionId, /* @__PURE__ */ new Map());
76343
76498
  }
76344
- this.clients.get(sessionId).add(res);
76499
+ const client = {
76500
+ res,
76501
+ clientId: resolvedClientId,
76502
+ clientType: inferClientType(resolvedClientId),
76503
+ connectedAt: (/* @__PURE__ */ new Date()).toISOString()
76504
+ };
76505
+ this.clients.get(sessionId).set(resolvedClientId, client);
76345
76506
  this.totalClientCount++;
76346
76507
  if (!this.watchers.has(sessionId)) {
76347
76508
  this.startWatcher(sessionId, vaultRoot2);
@@ -76353,6 +76514,7 @@ data: ${JSON.stringify({ sessionId })}
76353
76514
  res.on("close", () => {
76354
76515
  this.deregisterClient(sessionId, res);
76355
76516
  });
76517
+ this.broadcastPresence(sessionId);
76356
76518
  }
76357
76519
  /**
76358
76520
  * Register a callback-based listener for session changes.
@@ -76396,7 +76558,8 @@ data: ${JSON.stringify({ sessionId })}
76396
76558
  /**
76397
76559
  * Deregister an SSE client from a session.
76398
76560
  *
76399
- * - Removes the response from the client set
76561
+ * - Finds and removes the client by response reference
76562
+ * - Broadcasts updated presence to remaining clients
76400
76563
  * - Stops the file watcher if no clients remain for this session
76401
76564
  * - Cleans up offsets and timers
76402
76565
  *
@@ -76404,13 +76567,22 @@ data: ${JSON.stringify({ sessionId })}
76404
76567
  * @param res - Express Response object to remove
76405
76568
  */
76406
76569
  deregisterClient(sessionId, res) {
76407
- const clientSet = this.clients.get(sessionId);
76408
- if (!clientSet) return;
76409
- if (clientSet.has(res)) {
76410
- this.totalClientCount--;
76570
+ const clientMap = this.clients.get(sessionId);
76571
+ if (!clientMap) return;
76572
+ let removed = false;
76573
+ for (const [id, client] of clientMap) {
76574
+ if (client.res === res) {
76575
+ clientMap.delete(id);
76576
+ this.totalClientCount--;
76577
+ removed = true;
76578
+ break;
76579
+ }
76580
+ }
76581
+ if (!removed) return;
76582
+ if (clientMap.size > 0) {
76583
+ this.broadcastPresence(sessionId);
76411
76584
  }
76412
- clientSet.delete(res);
76413
- if (clientSet.size === 0) {
76585
+ if (clientMap.size === 0) {
76414
76586
  this.clients.delete(sessionId);
76415
76587
  const hasCallbacks = Array.from(this.callbacks.values()).some(
76416
76588
  (entry) => entry.sessionId === sessionId
@@ -76431,6 +76603,31 @@ data: ${JSON.stringify({ sessionId })}
76431
76603
  }
76432
76604
  }
76433
76605
  }
76606
+ /**
76607
+ * Get current presence info for a session.
76608
+ *
76609
+ * @param sessionId - Session UUID
76610
+ * @returns Presence info with client count, client metadata, and lock state, or null if no clients
76611
+ */
76612
+ getPresenceInfo(sessionId) {
76613
+ const clientMap = this.clients.get(sessionId);
76614
+ if (!clientMap || clientMap.size === 0) return null;
76615
+ const clients = Array.from(clientMap.values()).map((c3) => ({
76616
+ type: c3.clientType,
76617
+ connectedAt: c3.connectedAt
76618
+ }));
76619
+ let lockInfo = null;
76620
+ if (this.lockManager) {
76621
+ const lock = this.lockManager.getLockInfo(sessionId);
76622
+ if (lock) {
76623
+ lockInfo = {
76624
+ clientId: lock.clientId,
76625
+ acquiredAt: new Date(lock.acquiredAt).toISOString()
76626
+ };
76627
+ }
76628
+ }
76629
+ return { clientCount: clientMap.size, clients, lockInfo };
76630
+ }
76434
76631
  /**
76435
76632
  * Start a chokidar file watcher for a session's JSONL transcript.
76436
76633
  *
@@ -76511,11 +76708,11 @@ data: ${JSON.stringify({ sessionId })}
76511
76708
  if (content3.length === 0) {
76512
76709
  return;
76513
76710
  }
76514
- const clientSet = this.clients.get(sessionId);
76711
+ const clientMap = this.clients.get(sessionId);
76515
76712
  const hasCallbacks = Array.from(this.callbacks.values()).some(
76516
76713
  (entry) => entry.sessionId === sessionId
76517
76714
  );
76518
- if ((!clientSet || clientSet.size === 0) && !hasCallbacks) {
76715
+ if ((!clientMap || clientMap.size === 0) && !hasCallbacks) {
76519
76716
  return;
76520
76717
  }
76521
76718
  const event = {
@@ -76526,12 +76723,12 @@ data: ${JSON.stringify({ sessionId })}
76526
76723
  data: ${JSON.stringify(event)}
76527
76724
 
76528
76725
  `;
76529
- if (clientSet) {
76530
- for (const client of Array.from(clientSet)) {
76726
+ if (clientMap) {
76727
+ for (const connectedClient of Array.from(clientMap.values())) {
76531
76728
  try {
76532
- const ok3 = client.write(eventData);
76729
+ const ok3 = connectedClient.res.write(eventData);
76533
76730
  if (!ok3) {
76534
- await new Promise((resolve4) => client.once("drain", resolve4));
76731
+ await new Promise((resolve4) => connectedClient.res.once("drain", resolve4));
76535
76732
  }
76536
76733
  } catch (err) {
76537
76734
  logger.error(
@@ -76560,6 +76757,60 @@ data: ${JSON.stringify(event)}
76560
76757
  logger.error(`[SessionBroadcaster] Failed to read offset for session ${sessionId}:`, err);
76561
76758
  }
76562
76759
  }
76760
+ /** Broadcast a presence_update SSE event to all connected clients for a session. */
76761
+ broadcastPresence(sessionId) {
76762
+ const clientMap = this.clients.get(sessionId);
76763
+ if (!clientMap || clientMap.size === 0) return;
76764
+ const clients = Array.from(clientMap.values()).map((c3) => ({
76765
+ type: c3.clientType,
76766
+ connectedAt: c3.connectedAt
76767
+ }));
76768
+ let lockInfo = null;
76769
+ if (this.lockManager) {
76770
+ const lock = this.lockManager.getLockInfo(sessionId);
76771
+ if (lock) {
76772
+ lockInfo = {
76773
+ clientId: lock.clientId,
76774
+ acquiredAt: new Date(lock.acquiredAt).toISOString()
76775
+ };
76776
+ }
76777
+ }
76778
+ const presenceData = {
76779
+ sessionId,
76780
+ clientCount: clientMap.size,
76781
+ clients,
76782
+ lockInfo
76783
+ };
76784
+ const eventData = `event: presence_update
76785
+ data: ${JSON.stringify(presenceData)}
76786
+
76787
+ `;
76788
+ for (const client of clientMap.values()) {
76789
+ try {
76790
+ client.res.write(eventData);
76791
+ } catch (err) {
76792
+ logger.error(
76793
+ `[SessionBroadcaster] Failed to write presence to client ${client.clientId}:`,
76794
+ err
76795
+ );
76796
+ }
76797
+ }
76798
+ for (const [, entry] of this.callbacks) {
76799
+ if (entry.sessionId === sessionId) {
76800
+ try {
76801
+ entry.callback({
76802
+ type: "presence_update",
76803
+ data: presenceData
76804
+ });
76805
+ } catch (err) {
76806
+ logger.error(
76807
+ `[SessionBroadcaster] Presence callback error for session ${sessionId}:`,
76808
+ err
76809
+ );
76810
+ }
76811
+ }
76812
+ }
76813
+ }
76563
76814
  /**
76564
76815
  * Shutdown the broadcaster, closing all watchers and client connections.
76565
76816
  * Should be called on server shutdown.
@@ -76574,10 +76825,10 @@ data: ${JSON.stringify(event)}
76574
76825
  });
76575
76826
  this.watchers.clear();
76576
76827
  this.callbacks.clear();
76577
- Array.from(this.clients.values()).forEach((clientSet) => {
76578
- Array.from(clientSet).forEach((client) => {
76828
+ Array.from(this.clients.values()).forEach((clientMap) => {
76829
+ Array.from(clientMap.values()).forEach((client) => {
76579
76830
  try {
76580
- client.end();
76831
+ client.res.end();
76581
76832
  } catch {
76582
76833
  }
76583
76834
  });
@@ -76708,6 +76959,9 @@ function createToolState() {
76708
76959
  let currentToolName = "";
76709
76960
  let currentToolId = "";
76710
76961
  let taskToolInput = "";
76962
+ let inThinking = false;
76963
+ let thinkingStartMs = 0;
76964
+ const toolNameById = /* @__PURE__ */ new Map();
76711
76965
  return {
76712
76966
  get inTool() {
76713
76967
  return inTool;
@@ -76721,6 +76975,19 @@ function createToolState() {
76721
76975
  get taskToolInput() {
76722
76976
  return taskToolInput;
76723
76977
  },
76978
+ get inThinking() {
76979
+ return inThinking;
76980
+ },
76981
+ set inThinking(v3) {
76982
+ inThinking = v3;
76983
+ },
76984
+ get thinkingStartMs() {
76985
+ return thinkingStartMs;
76986
+ },
76987
+ set thinkingStartMs(v3) {
76988
+ thinkingStartMs = v3;
76989
+ },
76990
+ toolNameById,
76724
76991
  appendTaskInput: (chunk) => {
76725
76992
  taskToolInput += chunk;
76726
76993
  },
@@ -76816,7 +77083,8 @@ function handleToolApproval(session, toolUseId, toolName, input) {
76816
77083
  data: {
76817
77084
  toolCallId: toolUseId,
76818
77085
  toolName,
76819
- input: JSON.stringify(input)
77086
+ input: JSON.stringify(input),
77087
+ timeoutMs: SESSIONS.INTERACTION_TIMEOUT_MS
76820
77088
  }
76821
77089
  });
76822
77090
  session.eventQueueNotify?.();
@@ -76879,6 +77147,25 @@ function buildTaskEvent(toolName, input) {
76879
77147
  }
76880
77148
 
76881
77149
  // ../../apps/server/src/services/runtimes/claude-code/sdk-event-mapper.ts
77150
+ var TOOL_CONTEXTUAL_HOOK_EVENTS = /* @__PURE__ */ new Set([
77151
+ "PreToolUse",
77152
+ "PostToolUse",
77153
+ "PostToolUseFailure"
77154
+ ]);
77155
+ function mapErrorCategory(subtype) {
77156
+ switch (subtype) {
77157
+ case "error_max_turns":
77158
+ return "max_turns";
77159
+ case "error_during_execution":
77160
+ return "execution_error";
77161
+ case "error_max_budget_usd":
77162
+ return "budget_exceeded";
77163
+ case "error_max_structured_output_retries":
77164
+ return "output_format_error";
77165
+ default:
77166
+ return "execution_error";
77167
+ }
77168
+ }
76882
77169
  async function* mapSdkMessage(message, session, sessionId, toolState) {
76883
77170
  if (message.type === "system" && "subtype" in message && message.subtype === "init") {
76884
77171
  session.sdkSessionId = message.session_id;
@@ -76892,14 +77179,147 @@ async function* mapSdkMessage(message, session, sessionId, toolState) {
76892
77179
  }
76893
77180
  return;
76894
77181
  }
77182
+ if (message.type === "system" && "subtype" in message) {
77183
+ if (message.subtype === "task_started") {
77184
+ const msg = message;
77185
+ yield {
77186
+ type: "subagent_started",
77187
+ data: {
77188
+ taskId: msg.task_id,
77189
+ subagentSessionId: message.session_id,
77190
+ toolUseId: msg.tool_use_id,
77191
+ description: msg.description
77192
+ }
77193
+ };
77194
+ return;
77195
+ }
77196
+ if (message.subtype === "task_progress") {
77197
+ const msg = message;
77198
+ const usage = msg.usage;
77199
+ yield {
77200
+ type: "subagent_progress",
77201
+ data: {
77202
+ taskId: msg.task_id,
77203
+ toolUses: usage.tool_uses,
77204
+ lastToolName: msg.last_tool_name,
77205
+ durationMs: usage.duration_ms
77206
+ }
77207
+ };
77208
+ return;
77209
+ }
77210
+ if (message.subtype === "task_notification") {
77211
+ const msg = message;
77212
+ const usage = msg.usage;
77213
+ yield {
77214
+ type: "subagent_done",
77215
+ data: {
77216
+ taskId: msg.task_id,
77217
+ status: msg.status,
77218
+ summary: msg.summary,
77219
+ toolUses: usage?.tool_uses,
77220
+ durationMs: usage?.duration_ms
77221
+ }
77222
+ };
77223
+ return;
77224
+ }
77225
+ if (message.subtype === "status") {
77226
+ const msg = message;
77227
+ const text6 = msg.body ?? msg.message ?? "";
77228
+ if (text6) {
77229
+ yield {
77230
+ type: "system_status",
77231
+ data: { message: text6 }
77232
+ };
77233
+ }
77234
+ return;
77235
+ }
77236
+ if (message.subtype === "compact_boundary") {
77237
+ yield {
77238
+ type: "compact_boundary",
77239
+ data: {}
77240
+ };
77241
+ return;
77242
+ }
77243
+ if (message.subtype === "hook_started") {
77244
+ const msg = message;
77245
+ const hookEvent = msg.hook_event;
77246
+ const isToolContextual = TOOL_CONTEXTUAL_HOOK_EVENTS.has(hookEvent);
77247
+ if (isToolContextual) {
77248
+ yield {
77249
+ type: "hook_started",
77250
+ data: {
77251
+ hookId: msg.hook_id,
77252
+ hookName: msg.hook_name,
77253
+ hookEvent,
77254
+ toolCallId: toolState.currentToolId || null
77255
+ }
77256
+ };
77257
+ } else {
77258
+ yield {
77259
+ type: "system_status",
77260
+ data: { message: `Running hook "${msg.hook_name}"...` }
77261
+ };
77262
+ }
77263
+ return;
77264
+ }
77265
+ if (message.subtype === "hook_progress") {
77266
+ const msg = message;
77267
+ const hookEvent = msg.hook_event;
77268
+ const isToolContextual = TOOL_CONTEXTUAL_HOOK_EVENTS.has(hookEvent);
77269
+ if (isToolContextual) {
77270
+ yield {
77271
+ type: "hook_progress",
77272
+ data: {
77273
+ hookId: msg.hook_id,
77274
+ stdout: msg.stdout,
77275
+ stderr: msg.stderr
77276
+ }
77277
+ };
77278
+ }
77279
+ return;
77280
+ }
77281
+ if (message.subtype === "hook_response") {
77282
+ const msg = message;
77283
+ const hookEvent = msg.hook_event;
77284
+ const isToolContextual = TOOL_CONTEXTUAL_HOOK_EVENTS.has(hookEvent);
77285
+ if (isToolContextual) {
77286
+ yield {
77287
+ type: "hook_response",
77288
+ data: {
77289
+ hookId: msg.hook_id,
77290
+ hookName: msg.hook_name,
77291
+ exitCode: msg.exit_code,
77292
+ outcome: msg.outcome,
77293
+ stdout: msg.stdout,
77294
+ stderr: msg.stderr
77295
+ }
77296
+ };
77297
+ } else if (msg.outcome === "error") {
77298
+ yield {
77299
+ type: "error",
77300
+ data: {
77301
+ message: `Hook "${msg.hook_name}" failed (${hookEvent})`,
77302
+ code: "hook_failure",
77303
+ category: "execution_error",
77304
+ details: msg.stderr || msg.stdout
77305
+ }
77306
+ };
77307
+ }
77308
+ return;
77309
+ }
77310
+ }
76895
77311
  if (message.type === "stream_event") {
76896
77312
  const event = message.event;
76897
77313
  const eventType = event.type;
76898
77314
  if (eventType === "content_block_start") {
76899
77315
  const contentBlock = event.content_block;
76900
- if (contentBlock?.type === "tool_use") {
77316
+ if (contentBlock?.type === "thinking") {
77317
+ toolState.inThinking = true;
77318
+ toolState.thinkingStartMs = Date.now();
77319
+ } else if (contentBlock?.type === "tool_use") {
76901
77320
  toolState.resetTaskInput();
76902
77321
  toolState.setToolState(true, contentBlock.name, contentBlock.id);
77322
+ toolState.toolNameById.set(contentBlock.id, contentBlock.name);
76903
77323
  yield {
76904
77324
  type: "tool_call_start",
76905
77325
  data: {
@@ -76911,7 +77331,9 @@ async function* mapSdkMessage(message, session, sessionId, toolState) {
76911
77331
  }
76912
77332
  } else if (eventType === "content_block_delta") {
76913
77333
  const delta = event.delta;
76914
- if (delta?.type === "text_delta" && !toolState.inTool) {
77334
+ if (delta?.type === "thinking_delta" && toolState.inThinking) {
77335
+ yield { type: "thinking_delta", data: { text: delta.thinking } };
77336
+ } else if (delta?.type === "text_delta" && !toolState.inTool) {
76915
77337
  yield { type: "text_delta", data: { text: delta.text } };
76916
77338
  } else if (delta?.type === "input_json_delta" && toolState.inTool) {
76917
77339
  if (TASK_TOOL_NAMES.has(toolState.currentToolName)) {
@@ -76927,8 +77349,27 @@ async function* mapSdkMessage(message, session, sessionId, toolState) {
76927
77349
  }
76928
77350
  };
76929
77351
  }
77352
+ } else if (eventType === "message_delta") {
77353
+ const delta = event.delta;
77354
+ const usage = event.usage;
77355
+ const stopReason = delta?.stop_reason;
77356
+ const outputTokens = usage?.output_tokens;
77357
+ if (outputTokens !== void 0) {
77358
+ yield {
77359
+ type: "session_status",
77360
+ data: { sessionId, outputTokens }
77361
+ };
77362
+ }
77363
+ if (stopReason === "max_tokens") {
77364
+ yield {
77365
+ type: "system_status",
77366
+ data: { message: "Response truncated \u2014 reached max output tokens." }
77367
+ };
77368
+ }
76930
77369
  } else if (eventType === "content_block_stop") {
76931
- if (toolState.inTool) {
77370
+ if (toolState.inThinking) {
77371
+ toolState.inThinking = false;
77372
+ } else if (toolState.inTool) {
76932
77373
  const wasTaskTool = TASK_TOOL_NAMES.has(toolState.currentToolName);
76933
77374
  const taskToolName = toolState.currentToolName;
76934
77375
  yield {
@@ -76962,7 +77403,7 @@ async function* mapSdkMessage(message, session, sessionId, toolState) {
76962
77403
  type: "tool_result",
76963
77404
  data: {
76964
77405
  toolCallId: toolUseId,
76965
- toolName: "",
77406
+ toolName: toolState.toolNameById.get(toolUseId) ?? "",
76966
77407
  result: summary.summary,
76967
77408
  status: "complete"
76968
77409
  }
@@ -76970,6 +77411,35 @@ async function* mapSdkMessage(message, session, sessionId, toolState) {
76970
77411
  }
76971
77412
  return;
76972
77413
  }
77414
+ if (message.type === "tool_progress") {
77415
+ const progress = message;
77416
+ yield {
77417
+ type: "tool_progress",
77418
+ data: {
77419
+ toolCallId: progress.tool_use_id,
77420
+ content: progress.content
77421
+ }
77422
+ };
77423
+ return;
77424
+ }
77425
+ if (message.type === "prompt_suggestion") {
77426
+ const suggestions = message.suggestions;
77427
+ if (Array.isArray(suggestions) && suggestions.length > 0) {
77428
+ yield {
77429
+ type: "prompt_suggestion",
77430
+ data: { suggestions }
77431
+ };
77432
+ }
77433
+ return;
77434
+ }
77435
+ if (message.type === "rate_limit_event") {
77436
+ const retryAfter = message.retry_after;
77437
+ yield {
77438
+ type: "rate_limit",
77439
+ data: { retryAfter }
77440
+ };
77441
+ return;
77442
+ }
76973
77443
  if (message.type === "result") {
76974
77444
  const result2 = message;
76975
77445
  const usage = result2.usage;
@@ -76985,11 +77455,31 @@ async function* mapSdkMessage(message, session, sessionId, toolState) {
76985
77455
  contextMaxTokens: firstModelUsage?.contextWindow
76986
77456
  }
76987
77457
  };
77458
+ const subtype = result2.subtype;
77459
+ if (subtype && subtype !== "success") {
77460
+ const errors = result2.errors;
77461
+ const category = mapErrorCategory(subtype);
77462
+ yield {
77463
+ type: "error",
77464
+ data: {
77465
+ message: errors?.[0] ?? "An unexpected error occurred.",
77466
+ code: subtype,
77467
+ category,
77468
+ details: errors?.join("\n")
77469
+ }
77470
+ };
77471
+ }
76988
77472
  yield {
76989
77473
  type: "done",
76990
77474
  data: { sessionId }
76991
77475
  };
77476
+ return;
76992
77477
  }
77478
+ logger.debug(
77479
+ "Unhandled SDK message type: %s (subtype: %s)",
77480
+ message.type,
77481
+ "subtype" in message ? message.subtype : "none"
77482
+ );
76993
77483
  }
76994
77484
 
76995
77485
  // ../../apps/server/src/services/runtimes/claude-code/context-builder.ts
@@ -77333,15 +77823,15 @@ var RESUME_FAILURE_PATTERNS = [
77333
77823
  "query closed before response",
77334
77824
  "session not found",
77335
77825
  "no such file",
77336
- "enoent",
77337
- "process exited with code"
77826
+ "enoent"
77338
77827
  ];
77828
+ var MAX_RESUME_RETRIES = 1;
77339
77829
  function isResumeFailure(err) {
77340
77830
  if (!(err instanceof Error)) return false;
77341
77831
  const msg = err.message.toLowerCase();
77342
77832
  return RESUME_FAILURE_PATTERNS.some((p2) => msg.includes(p2));
77343
77833
  }
77344
- async function* executeSdkQuery(sessionId, content3, session, opts, messageOpts) {
77834
+ async function* executeSdkQuery(sessionId, content3, session, opts, messageOpts, retryDepth = 0) {
77345
77835
  session.lastActivity = Date.now();
77346
77836
  session.eventQueue = [];
77347
77837
  const effectiveCwd = messageOpts?.cwd || opts.sessionCwd || opts.cwd;
@@ -77383,6 +77873,7 @@ ${messageOpts.systemPromptAppend}` : baseAppend;
77383
77873
  const sdkOptions = {
77384
77874
  cwd: effectiveCwd,
77385
77875
  includePartialMessages: true,
77876
+ promptSuggestions: true,
77386
77877
  settingSources: ["project", "user"],
77387
77878
  systemPrompt: {
77388
77879
  type: "preset",
@@ -77469,7 +77960,10 @@ ${messageOpts.systemPromptAppend}` : baseAppend;
77469
77960
  effectiveCwd
77470
77961
  });
77471
77962
  let emittedDone = false;
77963
+ let emittedError = false;
77472
77964
  let eventCount = 0;
77965
+ let contentEventCount = 0;
77966
+ let wasInteractive = false;
77473
77967
  const streamStart = Date.now();
77474
77968
  const toolState = createToolState();
77475
77969
  try {
@@ -77503,6 +77997,12 @@ ${messageOpts.systemPromptAppend}` : baseAppend;
77503
77997
  opts.meshCore.updateLastSeen(meshAgentId, "response_complete");
77504
77998
  }
77505
77999
  }
78000
+ if (["text_delta", "tool_call_start", "tool_result", "thinking_delta"].includes(event.type)) {
78001
+ contentEventCount++;
78002
+ }
78003
+ if (["approval_required", "question_prompt"].includes(event.type)) {
78004
+ wasInteractive = true;
78005
+ }
77506
78006
  eventCount++;
77507
78007
  yield event;
77508
78008
  }
@@ -77512,34 +78012,57 @@ ${messageOpts.systemPromptAppend}` : baseAppend;
77512
78012
  }
77513
78013
  }
77514
78014
  } catch (err) {
77515
- if (session.hasStarted && isResumeFailure(err)) {
78015
+ if (session.hasStarted && isResumeFailure(err) && retryDepth < MAX_RESUME_RETRIES) {
77516
78016
  logger.warn("[sendMessage] resume failed for stale session, retrying as new", {
77517
78017
  session: sessionId,
78018
+ retryDepth,
77518
78019
  error: err instanceof Error ? err.message : String(err)
77519
78020
  });
77520
78021
  session.hasStarted = false;
77521
- yield* executeSdkQuery(sessionId, content3, session, opts, messageOpts);
78022
+ yield* executeSdkQuery(sessionId, content3, session, opts, messageOpts, retryDepth + 1);
77522
78023
  return;
77523
78024
  }
78025
+ const errMsg = err instanceof Error ? err.message : String(err);
77524
78026
  logger.warn("[sendMessage] stream error", {
77525
78027
  session: sessionId,
77526
- error: err instanceof Error ? err.message : String(err),
78028
+ error: errMsg,
77527
78029
  durationMs: Date.now() - streamStart,
77528
- eventCount
78030
+ eventCount,
78031
+ contentEventCount,
78032
+ retryDepth
77529
78033
  });
77530
78034
  yield {
77531
78035
  type: "error",
77532
78036
  data: {
77533
- message: err instanceof Error ? err.message : "SDK error"
78037
+ message: "The agent stopped unexpectedly. The service may be temporarily overloaded \u2014 try again in a moment.",
78038
+ category: "execution_error",
78039
+ details: errMsg
77534
78040
  }
77535
78041
  };
78042
+ emittedError = true;
77536
78043
  } finally {
77537
78044
  session.activeQuery = void 0;
77538
78045
  }
78046
+ if (contentEventCount === 0 && !emittedError && !wasInteractive) {
78047
+ logger.warn("[sendMessage] stream completed with zero content events", {
78048
+ session: sessionId,
78049
+ eventCount,
78050
+ durationMs: Date.now() - streamStart
78051
+ });
78052
+ yield {
78053
+ type: "error",
78054
+ data: {
78055
+ message: "The agent did not respond. The service may be temporarily unavailable.",
78056
+ category: "execution_error"
78057
+ }
78058
+ };
78059
+ emittedError = true;
78060
+ }
77539
78061
  logger.info("[sendMessage] stream done", {
77540
78062
  session: sessionId,
77541
78063
  durationMs: Date.now() - streamStart,
77542
- eventCount
78064
+ eventCount,
78065
+ contentEventCount
77543
78066
  });
77544
78067
  if (!emittedDone) {
77545
78068
  yield {
@@ -77601,7 +78124,7 @@ var ClaudeCodeRuntime = class _ClaudeCodeRuntime {
77601
78124
  this.cwd = cwd ?? DEFAULT_CWD;
77602
78125
  this.claudeCliPath = resolveClaudeCliPath();
77603
78126
  this.transcriptReader = new TranscriptReader();
77604
- this.broadcaster = new SessionBroadcaster(this.transcriptReader);
78127
+ this.broadcaster = new SessionBroadcaster(this.transcriptReader, this.lockManager);
77605
78128
  }
77606
78129
  // ---------------------------------------------------------------------------
77607
78130
  // Capabilities
@@ -87076,7 +87599,10 @@ var SignalEmitter = class {
87076
87599
  var DEFAULT_RATE_LIMIT_CONFIG = {
87077
87600
  enabled: true,
87078
87601
  windowSecs: 60,
87079
- maxPerWindow: 100
87602
+ maxPerWindow: 100,
87603
+ perSenderOverrides: {
87604
+ "agent:": 2e3
87605
+ }
87080
87606
  };
87081
87607
  function checkRateLimit(sender, countInWindow, config = DEFAULT_RATE_LIMIT_CONFIG) {
87082
87608
  if (!config.enabled) {
@@ -87641,6 +88167,9 @@ var WatcherManager = class {
87641
88167
  };
87642
88168
 
87643
88169
  // ../relay/dist/types.js
88170
+ var noop = () => {
88171
+ };
88172
+ var noopLogger = { debug: noop, info: noop, warn: noop, error: noop };
87644
88173
  function inferEndpointType2(subject) {
87645
88174
  if (subject.startsWith("relay.inbox.dispatch."))
87646
88175
  return "dispatch";
@@ -88198,6 +88727,8 @@ var BaseRelayAdapter = class {
88198
88727
  displayName;
88199
88728
  /** Reference to the relay publisher, set on start, cleared on stop. */
88200
88729
  relay = null;
88730
+ /** Logger for debug/info/warn/error output. Silent until injected via {@link setLogger}. */
88731
+ logger = noopLogger;
88201
88732
  _status = {
88202
88733
  state: "disconnected",
88203
88734
  messageCount: { inbound: 0, outbound: 0 },
@@ -88208,6 +88739,17 @@ var BaseRelayAdapter = class {
88208
88739
  this.subjectPrefix = subjectPrefix;
88209
88740
  this.displayName = displayName;
88210
88741
  }
88742
+ /**
88743
+ * Inject a logger for structured debug output.
88744
+ *
88745
+ * Call after construction (typically from the adapter factory).
88746
+ * Until called, the adapter uses a silent no-op logger.
88747
+ *
88748
+ * @param logger - A logger compatible with consola's tagged logger
88749
+ */
88750
+ setLogger(logger3) {
88751
+ this.logger = logger3;
88752
+ }
88211
88753
  /**
88212
88754
  * Start the adapter with idempotency guard and status tracking.
88213
88755
  *
@@ -88220,6 +88762,7 @@ var BaseRelayAdapter = class {
88220
88762
  return;
88221
88763
  this._status = { ...this._status, state: "starting" };
88222
88764
  this.relay = relay;
88765
+ this.logger.info("starting");
88223
88766
  try {
88224
88767
  await this._start(relay);
88225
88768
  this._status = {
@@ -88227,6 +88770,7 @@ var BaseRelayAdapter = class {
88227
88770
  state: "connected",
88228
88771
  startedAt: (/* @__PURE__ */ new Date()).toISOString()
88229
88772
  };
88773
+ this.logger.info("connected");
88230
88774
  } catch (err) {
88231
88775
  this.recordError(err);
88232
88776
  this.relay = null;
@@ -88242,6 +88786,7 @@ var BaseRelayAdapter = class {
88242
88786
  if (this._status.state === "disconnected")
88243
88787
  return;
88244
88788
  this._status = { ...this._status, state: "stopping" };
88789
+ this.logger.info("stopping");
88245
88790
  try {
88246
88791
  await this._stop();
88247
88792
  } finally {
@@ -88251,6 +88796,7 @@ var BaseRelayAdapter = class {
88251
88796
  messageCount: this._status.messageCount,
88252
88797
  errorCount: this._status.errorCount
88253
88798
  };
88799
+ this.logger.info("stopped");
88254
88800
  }
88255
88801
  }
88256
88802
  /**
@@ -88299,6 +88845,7 @@ var BaseRelayAdapter = class {
88299
88845
  */
88300
88846
  recordError(err) {
88301
88847
  const message = err instanceof Error ? err.message : String(err);
88848
+ this.logger.warn("error:", message);
88302
88849
  this._status = {
88303
88850
  ...this._status,
88304
88851
  state: "error",
@@ -88316,6 +88863,7 @@ var BaseRelayAdapter = class {
88316
88863
  setReconnecting() {
88317
88864
  if (this._status.state !== "error")
88318
88865
  return;
88866
+ this.logger.info("reconnecting");
88319
88867
  this._status = { ...this._status, state: "reconnecting" };
88320
88868
  }
88321
88869
  /**
@@ -88501,17 +89049,23 @@ function extractChannelName(chat) {
88501
89049
  }
88502
89050
  return void 0;
88503
89051
  }
88504
- async function handleInboundMessage(ctx, relay, callbacks) {
88505
- if (!ctx.message)
89052
+ async function handleInboundMessage(ctx, relay, callbacks, logger3 = noopLogger) {
89053
+ if (!ctx.message) {
89054
+ logger3.debug("inbound skipped: no message in context");
88506
89055
  return;
89056
+ }
88507
89057
  const { chat, from, message } = ctx;
88508
- if (!chat || !message)
89058
+ if (!chat || !message) {
89059
+ logger3.debug("inbound skipped: missing chat or message");
88509
89060
  return;
89061
+ }
88510
89062
  const isGroup = isGroupChat(chat.type);
88511
89063
  const subject = buildSubject(chat.id, isGroup);
88512
89064
  const rawText = message.text ?? message.caption ?? "";
88513
- if (!rawText)
89065
+ if (!rawText) {
89066
+ logger3.debug(`inbound skipped: no text content in chat ${chat.id}`);
88514
89067
  return;
89068
+ }
88515
89069
  const text6 = rawText.slice(0, MAX_CONTENT_LENGTH);
88516
89070
  const senderName = from ? [from.first_name, from.last_name].filter(Boolean).join(" ") || from.username || UNKNOWN_SENDER : UNKNOWN_SENDER;
88517
89071
  const payload = {
@@ -88539,11 +89093,16 @@ async function handleInboundMessage(ctx, relay, callbacks) {
88539
89093
  replyTo: subject
88540
89094
  });
88541
89095
  callbacks.trackInbound();
89096
+ logger3.debug(`inbound from ${senderName} in chat ${chat.id}: "${text6.slice(0, 80)}${text6.length > 80 ? "\u2026" : ""}" (${text6.length} chars) \u2192 ${subject}`);
88542
89097
  } catch (err) {
88543
89098
  callbacks.recordError(err);
89099
+ logger3.warn(`inbound publish failed for chat ${chat.id}:`, err instanceof Error ? err.message : String(err));
88544
89100
  }
88545
89101
  }
88546
89102
 
89103
+ // ../relay/dist/adapters/telegram/outbound.js
89104
+ import { randomBytes } from "node:crypto";
89105
+
88547
89106
  // ../../node_modules/.pnpm/bail@2.0.2/node_modules/bail/index.js
88548
89107
  function bail(error) {
88549
89108
  if (error) {
@@ -100906,19 +101465,6 @@ function truncateText(text6, maxLen) {
100906
101465
  return text6;
100907
101466
  return `${text6.slice(0, maxLen - 3)}...`;
100908
101467
  }
100909
- var SILENT_EVENT_TYPES = /* @__PURE__ */ new Set([
100910
- "session_status",
100911
- "tool_call_start",
100912
- "tool_call_delta",
100913
- "tool_call_end",
100914
- "tool_result",
100915
- "approval_required",
100916
- "question_prompt",
100917
- "task_update",
100918
- "relay_receipt",
100919
- "message_delivered",
100920
- "relay_message"
100921
- ]);
100922
101468
  function detectStreamEventType(payload) {
100923
101469
  if (payload === null || typeof payload !== "object")
100924
101470
  return null;
@@ -100947,6 +101493,40 @@ function extractErrorMessage(payload) {
100947
101493
  const data = obj.data;
100948
101494
  return typeof data?.message === "string" ? data.message : null;
100949
101495
  }
101496
+ function extractApprovalData(payload) {
101497
+ if (payload === null || typeof payload !== "object")
101498
+ return null;
101499
+ const obj = payload;
101500
+ if (obj.type !== "approval_required")
101501
+ return null;
101502
+ const data = obj.data;
101503
+ if (!data?.toolCallId || !data?.toolName)
101504
+ return null;
101505
+ return {
101506
+ toolCallId: data.toolCallId,
101507
+ toolName: data.toolName,
101508
+ input: data.input ?? "",
101509
+ timeoutMs: data.timeoutMs ?? 6e5
101510
+ };
101511
+ }
101512
+ function formatToolDescription(toolName, input) {
101513
+ try {
101514
+ const parsed = JSON.parse(input);
101515
+ if (toolName === "Write" && typeof parsed.path === "string") {
101516
+ return `wants to write to \`${parsed.path}\``;
101517
+ }
101518
+ if (toolName === "Edit" && typeof parsed.file_path === "string") {
101519
+ return `wants to edit \`${parsed.file_path}\``;
101520
+ }
101521
+ if (toolName === "Bash" && typeof parsed.command === "string") {
101522
+ const cmd = parsed.command;
101523
+ const preview = cmd.length > 60 ? `${cmd.slice(0, 57)}...` : cmd;
101524
+ return `wants to run \`${preview}\``;
101525
+ }
101526
+ } catch {
101527
+ }
101528
+ return `wants to use tool \`${toolName}\``;
101529
+ }
100950
101530
  function formatForPlatform(content3, platform2) {
100951
101531
  switch (platform2) {
100952
101532
  case "slack":
@@ -100962,6 +101542,19 @@ function formatForPlatform(content3, platform2) {
100962
101542
  var TELEGRAM_TYPING_ACTION = "typing";
100963
101543
  var typingIntervals = /* @__PURE__ */ new Map();
100964
101544
  var TYPING_REFRESH_MS = 4e3;
101545
+ var DRAFT_UPDATE_INTERVAL_MS = 200;
101546
+ var BUFFER_TTL_MS = 5 * 60 * 1e3;
101547
+ var lastDraftUpdate = /* @__PURE__ */ new Map();
101548
+ var callbackIdMap = /* @__PURE__ */ new Map();
101549
+ var CALLBACK_ID_TTL_MS = 15 * 60 * 1e3;
101550
+ var pendingApprovalTimeouts = /* @__PURE__ */ new Map();
101551
+ function clearApprovalTimeout(shortKey) {
101552
+ const timer = pendingApprovalTimeouts.get(shortKey);
101553
+ if (timer) {
101554
+ clearTimeout(timer);
101555
+ pendingApprovalTimeouts.delete(shortKey);
101556
+ }
101557
+ }
100965
101558
  async function sendAndTrack(bot, chatId, text6, startTime, callbacks) {
100966
101559
  try {
100967
101560
  await bot.api.sendMessage(chatId, text6);
@@ -100977,9 +101570,10 @@ async function sendAndTrack(bot, chatId, text6, startTime, callbacks) {
100977
101570
  }
100978
101571
  }
100979
101572
  async function deliverMessage(opts) {
100980
- const { adapterId, subject, envelope, bot, responseBuffers, callbacks } = opts;
101573
+ const { adapterId, subject, envelope, bot, responseBuffers, callbacks, streaming, logger: logger3 = noopLogger } = opts;
100981
101574
  const startTime = Date.now();
100982
101575
  if (envelope.from.startsWith(SUBJECT_PREFIX)) {
101576
+ logger3.debug("deliver: echo prevention \u2014 skipping self-originated message");
100983
101577
  return { success: true, durationMs: Date.now() - startTime };
100984
101578
  }
100985
101579
  if (!bot) {
@@ -100997,18 +101591,43 @@ async function deliverMessage(opts) {
100997
101591
  durationMs: Date.now() - startTime
100998
101592
  };
100999
101593
  }
101594
+ const now = Date.now();
101595
+ for (const [id, buf] of responseBuffers) {
101596
+ if (now - buf.startedAt > BUFFER_TTL_MS) {
101597
+ responseBuffers.delete(id);
101598
+ lastDraftUpdate.delete(id);
101599
+ logger3.warn(`buffer: reaped stale buffer for chat ${id} (age: ${Math.round((now - buf.startedAt) / 1e3)}s)`);
101600
+ }
101601
+ }
101000
101602
  const eventType = detectStreamEventType(envelope.payload);
101001
101603
  if (eventType) {
101002
101604
  const textChunk = extractTextDelta(envelope.payload);
101003
101605
  if (textChunk) {
101004
- const existing = responseBuffers.get(chatId) ?? "";
101005
- responseBuffers.set(chatId, existing + textChunk);
101606
+ logger3.debug(`deliver: text_delta to chat ${chatId} (${textChunk.length} chars)`);
101607
+ const existing = responseBuffers.get(chatId);
101608
+ responseBuffers.set(chatId, {
101609
+ text: (existing?.text ?? "") + textChunk,
101610
+ startedAt: existing?.startedAt ?? Date.now()
101611
+ });
101612
+ if (streaming && chatId > 0) {
101613
+ const lastUpdate = lastDraftUpdate.get(chatId) ?? 0;
101614
+ if (Date.now() - lastUpdate >= DRAFT_UPDATE_INTERVAL_MS) {
101615
+ lastDraftUpdate.set(chatId, Date.now());
101616
+ logger3.debug(`stream: sendMessageDraft to chat ${chatId} (${responseBuffers.get(chatId).text.length} chars)`);
101617
+ try {
101618
+ await bot.api.sendMessageDraft(chatId, responseBuffers.get(chatId).text);
101619
+ } catch {
101620
+ }
101621
+ }
101622
+ }
101006
101623
  return { success: true, durationMs: Date.now() - startTime };
101007
101624
  }
101008
101625
  const errorMsg = extractErrorMessage(envelope.payload);
101009
101626
  if (errorMsg) {
101010
- const buffered = responseBuffers.get(chatId) ?? "";
101627
+ logger3.debug(`deliver: error to chat ${chatId}: "${errorMsg.slice(0, 100)}"`);
101628
+ const buffered = responseBuffers.get(chatId)?.text ?? "";
101011
101629
  responseBuffers.delete(chatId);
101630
+ lastDraftUpdate.delete(chatId);
101012
101631
  const text7 = buffered ? truncateText(`${buffered}
101013
101632
 
101014
101633
  [Error: ${errorMsg}]`, MAX_MESSAGE_LENGTH) : truncateText(`[Error: ${errorMsg}]`, MAX_MESSAGE_LENGTH);
@@ -101016,18 +101635,33 @@ async function deliverMessage(opts) {
101016
101635
  }
101017
101636
  if (eventType === "done") {
101018
101637
  const buffered = responseBuffers.get(chatId);
101638
+ logger3.debug(`deliver: done for chat ${chatId} (buffered: ${buffered ? `${buffered.text.length} chars` : "empty"})`);
101019
101639
  responseBuffers.delete(chatId);
101640
+ lastDraftUpdate.delete(chatId);
101020
101641
  if (buffered) {
101021
- return sendAndTrack(bot, chatId, truncateText(buffered, MAX_MESSAGE_LENGTH), startTime, callbacks);
101642
+ return sendAndTrack(bot, chatId, truncateText(buffered.text, MAX_MESSAGE_LENGTH), startTime, callbacks);
101022
101643
  }
101023
101644
  return { success: true, durationMs: Date.now() - startTime };
101024
101645
  }
101025
- if (SILENT_EVENT_TYPES.has(eventType)) {
101026
- return { success: true, durationMs: Date.now() - startTime };
101646
+ if (eventType === "approval_required") {
101647
+ const data = extractApprovalData(envelope.payload);
101648
+ if (data) {
101649
+ logger3.debug(`deliver: approval_required for tool '${data.toolName}' to chat ${chatId}`);
101650
+ const buffered = responseBuffers.get(chatId);
101651
+ if (buffered?.text) {
101652
+ responseBuffers.delete(chatId);
101653
+ lastDraftUpdate.delete(chatId);
101654
+ await sendAndTrack(bot, chatId, truncateText(buffered.text, MAX_MESSAGE_LENGTH), startTime, callbacks);
101655
+ }
101656
+ return handleApprovalRequired(bot, chatId, data, envelope, callbacks, startTime);
101657
+ }
101027
101658
  }
101659
+ logger3.debug(`deliver: dropping stream event '${eventType}' (whitelist)`);
101660
+ return { success: true, durationMs: Date.now() - startTime };
101028
101661
  }
101029
101662
  const content3 = extractPayloadContent(envelope.payload);
101030
101663
  const text6 = truncateText(content3, MAX_MESSAGE_LENGTH);
101664
+ logger3.debug(`deliver: standard payload to chat ${chatId} (${text6.length} chars)`);
101031
101665
  return sendAndTrack(bot, chatId, text6, startTime, callbacks);
101032
101666
  }
101033
101667
  async function handleTypingSignal(bot, subject, state) {
@@ -101065,6 +101699,66 @@ function clearAllTypingIntervals() {
101065
101699
  for (const interval of typingIntervals.values())
101066
101700
  clearInterval(interval);
101067
101701
  typingIntervals.clear();
101702
+ lastDraftUpdate.clear();
101703
+ }
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);
101717
+ const shortKey = randomBytes(6).toString("hex");
101718
+ callbackIdMap.set(shortKey, { toolCallId: data.toolCallId, sessionId, agentId });
101719
+ setTimeout(() => callbackIdMap.delete(shortKey), CALLBACK_ID_TTL_MS);
101720
+ const toolDescription = formatToolDescription(data.toolName, data.input);
101721
+ const inputPreview = truncateText(data.input, 400);
101722
+ const messageText = `*Tool Approval Required*
101723
+ \`${data.toolName}\` ${toolDescription}
101724
+
101725
+ \`\`\`
101726
+ ${inputPreview}
101727
+ \`\`\``;
101728
+ try {
101729
+ const sent = await bot.api.sendMessage(chatId, messageText, {
101730
+ parse_mode: "Markdown",
101731
+ reply_markup: {
101732
+ inline_keyboard: [
101733
+ [
101734
+ { text: "Approve", callback_data: JSON.stringify({ k: shortKey, a: 1 }) },
101735
+ { text: "Deny", callback_data: JSON.stringify({ k: shortKey, a: 0 }) }
101736
+ ]
101737
+ ]
101738
+ }
101739
+ });
101740
+ if (data.timeoutMs && data.timeoutMs > 0) {
101741
+ const timer = setTimeout(async () => {
101742
+ pendingApprovalTimeouts.delete(shortKey);
101743
+ callbackIdMap.delete(shortKey);
101744
+ try {
101745
+ await bot.api.editMessageText(chatId, sent.message_id, `\u23F0 *Tool Approval Timed Out*
101746
+ ~~\`${data.toolName}\`~~ ${toolDescription}`, { parse_mode: "Markdown" });
101747
+ } catch {
101748
+ }
101749
+ }, data.timeoutMs);
101750
+ pendingApprovalTimeouts.set(shortKey, timer);
101751
+ }
101752
+ callbacks.trackOutbound();
101753
+ return { success: true, durationMs: Date.now() - startTime };
101754
+ } catch (err) {
101755
+ callbacks.recordError(err);
101756
+ return {
101757
+ success: false,
101758
+ error: err instanceof Error ? err.message : String(err),
101759
+ durationMs: Date.now() - startTime
101760
+ };
101761
+ }
101068
101762
  }
101069
101763
 
101070
101764
  // ../relay/dist/adapters/telegram/webhook.js
@@ -101196,6 +101890,15 @@ For local development, use a tunnel service (e.g., ngrok, Cloudflare Tunnel).`
101196
101890
  placeholder: "Auto-generated if empty",
101197
101891
  description: "Secret token for validating incoming webhook requests from Telegram.",
101198
101892
  showWhen: { field: "mode", equals: "webhook" }
101893
+ },
101894
+ {
101895
+ key: "streaming",
101896
+ label: "Streaming",
101897
+ type: "boolean",
101898
+ required: false,
101899
+ description: "Stream responses in real-time using Telegram's sendMessageDraft API (DMs only). Groups always use buffer-and-flush.",
101900
+ visibleByDefault: true,
101901
+ helpMarkdown: "When enabled, recipients in DMs see text appearing in real-time (ChatGPT-style). Group chats always use buffer-and-flush regardless of this setting. Requires Telegram Bot API 9.5+."
101199
101902
  }
101200
101903
  ],
101201
101904
  setupInstructions: "Open Telegram and search for @BotFather. Send /newbot, choose a name and username. Copy the token provided."
@@ -101228,7 +101931,39 @@ var TelegramAdapter = class _TelegramAdapter extends BaseRelayAdapter {
101228
101931
  async _start(relay) {
101229
101932
  const bot = new import_grammy2.Bot(this.config.token);
101230
101933
  bot.api.config.use((0, import_auto_retry.autoRetry)());
101231
- bot.on("message", (ctx) => handleInboundMessage(ctx, relay, this.makeInboundCallbacks()));
101934
+ bot.on("message", (ctx) => handleInboundMessage(ctx, relay, this.makeInboundCallbacks(), this.logger));
101935
+ bot.on("callback_query:data", async (ctx) => {
101936
+ try {
101937
+ const data = JSON.parse(ctx.callbackQuery.data);
101938
+ const entry = callbackIdMap.get(data.k);
101939
+ if (!entry) {
101940
+ await ctx.answerCallbackQuery({ text: "This approval has expired." });
101941
+ return;
101942
+ }
101943
+ const approved = data.a === 1;
101944
+ callbackIdMap.delete(data.k);
101945
+ clearApprovalTimeout(data.k);
101946
+ const opts = { from: `telegram:${ctx.from.id}` };
101947
+ await relay.publish(`relay.system.approval.${entry.agentId}`, {
101948
+ type: "approval_response",
101949
+ toolCallId: entry.toolCallId,
101950
+ sessionId: entry.sessionId,
101951
+ approved,
101952
+ respondedBy: String(ctx.from.id),
101953
+ platform: "telegram"
101954
+ }, opts);
101955
+ const decision = approved ? "Approved" : "Denied";
101956
+ const emoji = approved ? "\u2705" : "\u274C";
101957
+ await ctx.editMessageText(`${emoji} *Tool ${decision}*`, { parse_mode: "Markdown" });
101958
+ await ctx.answerCallbackQuery({ text: `Tool ${decision}` });
101959
+ this.logger.debug?.(`[Telegram] tool ${approved ? "approved" : "denied"}: toolCallId=${entry.toolCallId}`);
101960
+ } catch (err) {
101961
+ this.logger.error("[Telegram] callback query handler error:", err);
101962
+ this.recordError(err);
101963
+ await ctx.answerCallbackQuery({ text: "Error processing approval." }).catch(() => {
101964
+ });
101965
+ }
101966
+ });
101232
101967
  bot.catch((err) => this.recordError(err));
101233
101968
  this.bot = bot;
101234
101969
  this.signalUnsub = relay.onSignal(`${SUBJECT_PREFIX}.>`, (subject, signal) => {
@@ -101280,7 +102015,9 @@ var TelegramAdapter = class _TelegramAdapter extends BaseRelayAdapter {
101280
102015
  envelope,
101281
102016
  bot: this.bot,
101282
102017
  responseBuffers: this.responseBuffers,
101283
- callbacks: this.makeOutboundCallbacks()
102018
+ callbacks: this.makeOutboundCallbacks(),
102019
+ streaming: this.config.streaming ?? true,
102020
+ logger: this.logger
101284
102021
  });
101285
102022
  }
101286
102023
  // --- Private helpers ---
@@ -101328,7 +102065,7 @@ var TelegramAdapter = class _TelegramAdapter extends BaseRelayAdapter {
101328
102065
  }
101329
102066
  const newBot = new import_grammy2.Bot(this.config.token);
101330
102067
  newBot.api.config.use((0, import_auto_retry.autoRetry)());
101331
- newBot.on("message", (ctx) => handleInboundMessage(ctx, this.relay, this.makeInboundCallbacks()));
102068
+ newBot.on("message", (ctx) => handleInboundMessage(ctx, this.relay, this.makeInboundCallbacks(), this.logger));
101332
102069
  newBot.catch((e2) => this.recordError(e2));
101333
102070
  this.bot = newBot;
101334
102071
  this.startPollingMode(newBot).catch((e2) => this.handlePollingError(e2));
@@ -101746,15 +102483,23 @@ function clearCaches() {
101746
102483
  userNameCache.clear();
101747
102484
  channelNameCache.clear();
101748
102485
  }
101749
- async function handleInboundMessage2(event, client, relay, botUserId, callbacks) {
101750
- if (event.user === botUserId)
102486
+ async function handleInboundMessage2(event, client, relay, botUserId, callbacks, logger3 = noopLogger) {
102487
+ if (event.user === botUserId) {
102488
+ logger3.debug(`inbound skipped: echo (own user ${botUserId})`);
101751
102489
  return;
101752
- if (event.bot_id)
102490
+ }
102491
+ if (event.bot_id) {
102492
+ logger3.debug(`inbound skipped: bot message (bot_id=${event.bot_id})`);
101753
102493
  return;
101754
- if (event.subtype && SKIP_SUBTYPES.has(event.subtype))
102494
+ }
102495
+ if (event.subtype && SKIP_SUBTYPES.has(event.subtype)) {
102496
+ logger3.debug(`inbound skipped: subtype '${event.subtype}'`);
101755
102497
  return;
101756
- if (!event.text)
102498
+ }
102499
+ if (!event.text) {
102500
+ logger3.debug(`inbound skipped: no text content in ${event.channel}`);
101757
102501
  return;
102502
+ }
101758
102503
  const isGroup = isGroupChannel(event.channel);
101759
102504
  const subject = buildSubject2(event.channel, isGroup);
101760
102505
  const content3 = event.text.slice(0, MAX_CONTENT_LENGTH2);
@@ -101785,8 +102530,10 @@ async function handleInboundMessage2(event, client, relay, botUserId, callbacks)
101785
102530
  replyTo: subject
101786
102531
  });
101787
102532
  callbacks.trackInbound();
102533
+ logger3.debug(`inbound from ${senderName} in ${event.channel}: "${content3.slice(0, 80)}${content3.length > 80 ? "\u2026" : ""}" (${content3.length} chars) \u2192 ${subject}`);
101788
102534
  } catch (err) {
101789
102535
  callbacks.recordError(err);
102536
+ logger3.warn(`inbound publish failed for ${event.channel}:`, err instanceof Error ? err.message : String(err));
101790
102537
  }
101791
102538
  }
101792
102539
 
@@ -101794,6 +102541,14 @@ async function handleInboundMessage2(event, client, relay, botUserId, callbacks)
101794
102541
  import { randomUUID as randomUUID4 } from "node:crypto";
101795
102542
  var STREAM_UPDATE_INTERVAL_MS = 1e3;
101796
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
+ }
102551
+ }
101797
102552
  function streamKey(channelId, threadTs) {
101798
102553
  return threadTs ? `${channelId}:${threadTs}` : channelId;
101799
102554
  }
@@ -101846,7 +102601,7 @@ function removeTypingReaction(client, channelId, threadTs, typingIndicator) {
101846
102601
  }).catch(() => {
101847
102602
  });
101848
102603
  }
101849
- async function handleTextDelta(channelId, textChunk, threadTs, client, streamState, callbacks, startTime, streaming, typingIndicator, streamKeyTs) {
102604
+ async function handleTextDelta(channelId, textChunk, threadTs, client, streamState, callbacks, startTime, streaming, nativeStreaming, typingIndicator, streamKeyTs) {
101850
102605
  const key = streamKey(channelId, streamKeyTs);
101851
102606
  const existing = streamState.get(key);
101852
102607
  if (!streaming) {
@@ -101869,6 +102624,12 @@ async function handleTextDelta(channelId, textChunk, threadTs, client, streamSta
101869
102624
  }
101870
102625
  if (existing) {
101871
102626
  existing.accumulatedText += textChunk;
102627
+ if (existing.nativeStreamId) {
102628
+ return wrapSlackCall(() => client.chat.appendStream({
102629
+ stream_id: existing.nativeStreamId,
102630
+ text: formatForPlatform(textChunk, "slack")
102631
+ }), callbacks, startTime);
102632
+ }
101872
102633
  const now = Date.now();
101873
102634
  if (now - existing.lastUpdateAt < STREAM_UPDATE_INTERVAL_MS) {
101874
102635
  return { success: true, durationMs: now - startTime };
@@ -101882,6 +102643,36 @@ async function handleTextDelta(channelId, textChunk, threadTs, client, streamSta
101882
102643
  text: truncateText(streamText, MAX_MESSAGE_LENGTH2)
101883
102644
  }), callbacks, startTime);
101884
102645
  }
102646
+ if (nativeStreaming && threadTs) {
102647
+ 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 ?? "";
102654
+ const now = Date.now();
102655
+ streamState.set(key, {
102656
+ channelId,
102657
+ threadTs: threadTs ?? "",
102658
+ messageTs: "",
102659
+ // Not used in native streaming
102660
+ accumulatedText: textChunk,
102661
+ lastUpdateAt: now,
102662
+ startedAt: now,
102663
+ streamId: randomUUID4(),
102664
+ nativeStreamId
102665
+ });
102666
+ await slackClient.chat.appendStream({
102667
+ stream_id: nativeStreamId,
102668
+ text: formatForPlatform(textChunk, "slack")
102669
+ });
102670
+ addTypingReaction(client, channelId, threadTs, typingIndicator);
102671
+ return { success: true, durationMs: Date.now() - startTime };
102672
+ } catch (err) {
102673
+ callbacks.recordError(err);
102674
+ }
102675
+ }
101885
102676
  try {
101886
102677
  const mrkdwn = formatForPlatform(textChunk, "slack");
101887
102678
  const now = Date.now();
@@ -101910,6 +102701,45 @@ async function handleTextDelta(channelId, textChunk, threadTs, client, streamSta
101910
102701
  };
101911
102702
  }
101912
102703
  }
102704
+ async function flushStreamBuffer(channelId, threadTs, client, streamState, callbacks, streamKeyTs) {
102705
+ const key = streamKey(channelId, streamKeyTs);
102706
+ const existing = streamState.get(key);
102707
+ if (!existing || !existing.accumulatedText)
102708
+ return;
102709
+ if (existing.nativeStreamId) {
102710
+ const slackClient = client;
102711
+ try {
102712
+ await slackClient.chat.stopStream({ stream_id: existing.nativeStreamId });
102713
+ } catch {
102714
+ }
102715
+ existing.nativeStreamId = void 0;
102716
+ return;
102717
+ }
102718
+ if (!existing.messageTs) {
102719
+ try {
102720
+ const result2 = await client.chat.postMessage({
102721
+ channel: channelId,
102722
+ text: truncateText(formatForPlatform(existing.accumulatedText, "slack"), MAX_MESSAGE_LENGTH2),
102723
+ ...threadTs ? { thread_ts: threadTs } : {}
102724
+ });
102725
+ existing.messageTs = result2.ts ?? "";
102726
+ existing.lastUpdateAt = Date.now();
102727
+ } catch (err) {
102728
+ callbacks.recordError(err);
102729
+ }
102730
+ return;
102731
+ }
102732
+ try {
102733
+ await client.chat.update({
102734
+ channel: channelId,
102735
+ ts: existing.messageTs,
102736
+ text: truncateText(formatForPlatform(existing.accumulatedText, "slack"), MAX_MESSAGE_LENGTH2)
102737
+ });
102738
+ existing.lastUpdateAt = Date.now();
102739
+ } catch (err) {
102740
+ callbacks.recordError(err);
102741
+ }
102742
+ }
101913
102743
  async function handleDone(channelId, threadTs, client, streamState, callbacks, startTime, typingIndicator, streamKeyTs) {
101914
102744
  const key = streamKey(channelId, streamKeyTs);
101915
102745
  const existing = streamState.get(key);
@@ -101920,6 +102750,10 @@ async function handleDone(channelId, threadTs, client, streamState, callbacks, s
101920
102750
  if (!existing) {
101921
102751
  return { success: true, durationMs: Date.now() - startTime };
101922
102752
  }
102753
+ if (existing.nativeStreamId) {
102754
+ const slackClient = client;
102755
+ return wrapSlackCall(() => slackClient.chat.stopStream({ stream_id: existing.nativeStreamId }), callbacks, startTime, true);
102756
+ }
101923
102757
  if (!existing.messageTs) {
101924
102758
  return wrapSlackCall(() => client.chat.postMessage({
101925
102759
  channel: channelId,
@@ -101941,6 +102775,20 @@ async function handleError(channelId, errorMsg, threadTs, client, streamState, c
101941
102775
  removeTypingReaction(client, channelId, existing.threadTs, typingIndicator);
101942
102776
  }
101943
102777
  if (existing) {
102778
+ if (existing.nativeStreamId) {
102779
+ const slackClient = client;
102780
+ const errorSuffix = formatForPlatform(`
102781
+
102782
+ [Error: ${errorMsg}]`, "slack");
102783
+ try {
102784
+ await slackClient.chat.appendStream({
102785
+ stream_id: existing.nativeStreamId,
102786
+ text: errorSuffix
102787
+ });
102788
+ } catch {
102789
+ }
102790
+ return wrapSlackCall(() => slackClient.chat.stopStream({ stream_id: existing.nativeStreamId }), callbacks, startTime, true);
102791
+ }
101944
102792
  const finalText = truncateText(`${formatForPlatform(existing.accumulatedText, "slack")}
101945
102793
 
101946
102794
  [Error: ${errorMsg}]`, MAX_MESSAGE_LENGTH2);
@@ -101965,14 +102813,16 @@ async function handleError(channelId, errorMsg, threadTs, client, streamState, c
101965
102813
  }), callbacks, startTime, true);
101966
102814
  }
101967
102815
  async function deliverMessage2(opts) {
101968
- const { adapterId, subject, envelope, client, streamState, callbacks } = opts;
102816
+ const { adapterId, subject, envelope, client, streamState, callbacks, logger: logger3 = noopLogger } = opts;
101969
102817
  const startTime = Date.now();
101970
102818
  for (const [key, stream] of streamState) {
101971
102819
  if (startTime - stream.startedAt > STREAM_TTL_MS) {
101972
102820
  streamState.delete(key);
102821
+ logger3.warn(`stream: reaped orphaned stream for ${key} (age: ${Math.round((startTime - stream.startedAt) / 1e3)}s)`);
101973
102822
  }
101974
102823
  }
101975
102824
  if (envelope.from.startsWith(SUBJECT_PREFIX2)) {
102825
+ logger3.debug("deliver: echo prevention \u2014 skipping self-originated message");
101976
102826
  return { success: true, durationMs: Date.now() - startTime };
101977
102827
  }
101978
102828
  if (!client) {
@@ -101998,28 +102848,135 @@ async function deliverMessage2(opts) {
101998
102848
  if (eventType) {
101999
102849
  const textChunk = extractTextDelta(envelope.payload);
102000
102850
  if (textChunk) {
102001
- return handleTextDelta(channelId, textChunk, threadTs, client, streamState, callbacks, startTime, opts.streaming, opts.typingIndicator, streamKeyTs);
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);
102002
102853
  }
102003
102854
  const errorMsg = extractErrorMessage(envelope.payload);
102004
102855
  if (errorMsg) {
102856
+ logger3.debug(`deliver: error to ${channelId}: "${errorMsg.slice(0, 100)}"`);
102005
102857
  return handleError(channelId, errorMsg, threadTs, client, streamState, callbacks, startTime, opts.typingIndicator, streamKeyTs);
102006
102858
  }
102007
102859
  if (eventType === "done") {
102860
+ logger3.debug(`deliver: done for ${channelId}`);
102008
102861
  return handleDone(channelId, threadTs, client, streamState, callbacks, startTime, opts.typingIndicator, streamKeyTs);
102009
102862
  }
102010
- if (SILENT_EVENT_TYPES.has(eventType)) {
102011
- return { success: true, durationMs: Date.now() - startTime };
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
+ }
102012
102870
  }
102871
+ logger3.debug(`deliver: dropping stream event '${eventType}' (whitelist)`);
102872
+ return { success: true, durationMs: Date.now() - startTime };
102013
102873
  }
102014
102874
  const content3 = extractPayloadContent(envelope.payload);
102015
102875
  const mrkdwn = formatForPlatform(content3, "slack");
102016
102876
  const text6 = truncateText(mrkdwn, MAX_MESSAGE_LENGTH2);
102877
+ logger3.debug(`deliver: standard payload to ${channelId} (${text6.length} chars)`);
102017
102878
  return wrapSlackCall(() => client.chat.postMessage({
102018
102879
  channel: channelId,
102019
102880
  text: text6,
102020
102881
  ...threadTs ? { thread_ts: threadTs } : {}
102021
102882
  }), callbacks, startTime, true);
102022
102883
  }
102884
+ function extractAgentIdFromEnvelope2(envelope) {
102885
+ const payload = envelope.payload;
102886
+ const data = payload?.data;
102887
+ return data?.agentId ?? "unknown";
102888
+ }
102889
+ function extractSessionIdFromEnvelope2(envelope) {
102890
+ const payload = envelope.payload;
102891
+ const data = payload?.data;
102892
+ return data?.ccaSessionKey ?? "unknown";
102893
+ }
102894
+ async function handleApprovalRequired2(channelId, threadTs, data, envelope, client, callbacks, startTime) {
102895
+ const agentId = extractAgentIdFromEnvelope2(envelope);
102896
+ const sessionId = extractSessionIdFromEnvelope2(envelope);
102897
+ const inputPreview = truncateText(data.input, 500);
102898
+ const toolDescription = formatToolDescription(data.toolName, data.input);
102899
+ const buttonValue = JSON.stringify({
102900
+ toolCallId: data.toolCallId,
102901
+ sessionId,
102902
+ agentId
102903
+ });
102904
+ let postedTs;
102905
+ const result2 = await wrapSlackCall(async () => {
102906
+ const res = await client.chat.postMessage({
102907
+ channel: channelId,
102908
+ ...threadTs ? { thread_ts: threadTs } : {},
102909
+ text: `Tool approval required: ${data.toolName} (fallback)`,
102910
+ blocks: [
102911
+ {
102912
+ type: "section",
102913
+ text: {
102914
+ type: "mrkdwn",
102915
+ text: `*Tool Approval Required*
102916
+ \`${data.toolName}\` ${toolDescription}`
102917
+ }
102918
+ },
102919
+ {
102920
+ type: "section",
102921
+ text: {
102922
+ type: "mrkdwn",
102923
+ text: `\`\`\`
102924
+ ${inputPreview}
102925
+ \`\`\``
102926
+ }
102927
+ },
102928
+ {
102929
+ type: "actions",
102930
+ block_id: "tool_approval",
102931
+ elements: [
102932
+ {
102933
+ type: "button",
102934
+ text: { type: "plain_text", text: "Approve" },
102935
+ style: "primary",
102936
+ action_id: "tool_approve",
102937
+ value: buttonValue
102938
+ },
102939
+ {
102940
+ type: "button",
102941
+ text: { type: "plain_text", text: "Deny" },
102942
+ style: "danger",
102943
+ action_id: "tool_deny",
102944
+ value: buttonValue
102945
+ }
102946
+ ]
102947
+ }
102948
+ ]
102949
+ });
102950
+ postedTs = res.ts;
102951
+ }, callbacks, startTime, true);
102952
+ if (result2.success && postedTs && data.timeoutMs > 0) {
102953
+ const msgTs = postedTs;
102954
+ const timer = setTimeout(async () => {
102955
+ pendingApprovalTimeouts2.delete(data.toolCallId);
102956
+ try {
102957
+ await client.chat.update({
102958
+ channel: channelId,
102959
+ ts: msgTs,
102960
+ text: ":hourglass: Tool approval timed out",
102961
+ blocks: [
102962
+ {
102963
+ type: "section",
102964
+ text: { type: "mrkdwn", text: ":hourglass: *Tool Approval Timed Out*\n~`" + data.toolName + "`~" }
102965
+ }
102966
+ ]
102967
+ });
102968
+ } catch {
102969
+ }
102970
+ }, data.timeoutMs);
102971
+ pendingApprovalTimeouts2.set(data.toolCallId, {
102972
+ timer,
102973
+ channelId,
102974
+ messageTs: msgTs,
102975
+ client
102976
+ });
102977
+ }
102978
+ return result2;
102979
+ }
102023
102980
 
102024
102981
  // ../relay/dist/adapters/slack/slack-adapter.js
102025
102982
  var SLACK_APP_MANIFEST_YAML = `display_information:
@@ -102070,7 +103027,7 @@ var SLACK_MANIFEST = {
102070
103027
  stepId: "create-app",
102071
103028
  title: "Create & Configure a Slack App",
102072
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.',
102073
- fields: ["botToken", "appToken", "signingSecret", "streaming", "typingIndicator"]
103030
+ fields: ["botToken", "appToken", "signingSecret", "streaming", "nativeStreaming", "typingIndicator"]
102074
103031
  }
102075
103032
  ],
102076
103033
  configFields: [
@@ -102129,6 +103086,15 @@ var SLACK_MANIFEST = {
102129
103086
  visibleByDefault: true,
102130
103087
  helpMarkdown: "When enabled, agent responses appear token-by-token in Slack via message editing. When disabled, the full response is sent as a single message after the agent finishes."
102131
103088
  },
103089
+ {
103090
+ key: "nativeStreaming",
103091
+ label: "Native Streaming",
103092
+ type: "boolean",
103093
+ required: false,
103094
+ description: "Use Slack's native streaming API (chat.startStream/appendStream/stopStream). Requires messages in threads.",
103095
+ visibleByDefault: true,
103096
+ helpMarkdown: "When enabled, uses Slack's purpose-built streaming API for smoother, flicker-free responses. When disabled, uses the legacy chat.update approach. Only applies when Stream Responses is enabled."
103097
+ },
102132
103098
  {
102133
103099
  key: "typingIndicator",
102134
103100
  label: "Typing Indicator",
@@ -102183,10 +103149,18 @@ var SlackAdapter = class extends BaseRelayAdapter {
102183
103149
  const authResult = await app.client.auth.test();
102184
103150
  this.botUserId = authResult.user_id ?? "";
102185
103151
  app.message(async ({ event, client }) => {
102186
- await handleInboundMessage2(event, client, relay, this.botUserId, this.makeInboundCallbacks());
103152
+ await handleInboundMessage2(event, client, relay, this.botUserId, this.makeInboundCallbacks(), this.logger);
102187
103153
  });
102188
103154
  app.event("app_mention", async ({ event, client }) => {
102189
- await handleInboundMessage2(event, client, relay, this.botUserId, this.makeInboundCallbacks());
103155
+ await handleInboundMessage2(event, client, relay, this.botUserId, this.makeInboundCallbacks(), this.logger);
103156
+ });
103157
+ app.action("tool_approve", async ({ ack, action, body, client }) => {
103158
+ await ack();
103159
+ await this.handleToolAction(true, action, body, client, relay);
103160
+ });
103161
+ app.action("tool_deny", async ({ ack, action, body, client }) => {
103162
+ await ack();
103163
+ await this.handleToolAction(false, action, body, client, relay);
102190
103164
  });
102191
103165
  app.error(async (error) => {
102192
103166
  this.recordError(error);
@@ -102226,7 +103200,9 @@ var SlackAdapter = class extends BaseRelayAdapter {
102226
103200
  botUserId: this.botUserId,
102227
103201
  callbacks: this.makeOutboundCallbacks(),
102228
103202
  streaming: this.config.streaming ?? true,
102229
- typingIndicator: this.config.typingIndicator ?? "none"
103203
+ nativeStreaming: this.config.nativeStreaming ?? true,
103204
+ typingIndicator: this.config.typingIndicator ?? "none",
103205
+ logger: this.logger
102230
103206
  });
102231
103207
  }
102232
103208
  /** Build callbacks for inbound message handling. */
@@ -102243,6 +103219,63 @@ var SlackAdapter = class extends BaseRelayAdapter {
102243
103219
  recordError: (err) => this.recordError(err)
102244
103220
  };
102245
103221
  }
103222
+ /**
103223
+ * Handle a tool approval or denial action from Slack interactive buttons.
103224
+ *
103225
+ * Parses the button value JSON, publishes an `approval_response` to the
103226
+ * relay bus, and updates the original Slack message to reflect the decision.
103227
+ *
103228
+ * @param approved - Whether the user clicked Approve (true) or Deny (false)
103229
+ * @param action - The Bolt action payload
103230
+ * @param body - The Bolt body payload containing message context
103231
+ * @param client - The Slack WebClient for updating messages
103232
+ * @param relay - The relay publisher for publishing approval responses
103233
+ */
103234
+ async handleToolAction(approved, action, body, client, relay) {
103235
+ try {
103236
+ const btnAction = action;
103237
+ const btnBody = body;
103238
+ if (!btnAction.value) {
103239
+ this.logger.warn("[Slack] tool action missing button value");
103240
+ return;
103241
+ }
103242
+ const { toolCallId, sessionId, agentId } = JSON.parse(btnAction.value);
103243
+ clearApprovalTimeout2(toolCallId);
103244
+ const opts = { from: `slack:${btnBody.user?.id ?? "unknown"}` };
103245
+ await relay.publish(`relay.system.approval.${agentId}`, {
103246
+ type: "approval_response",
103247
+ toolCallId,
103248
+ sessionId,
103249
+ approved,
103250
+ respondedBy: btnBody.user?.id,
103251
+ platform: "slack"
103252
+ }, opts);
103253
+ const channelId = btnBody.channel?.id;
103254
+ const messageTs = btnBody.message?.ts;
103255
+ if (channelId && messageTs) {
103256
+ const decision = approved ? "Approved" : "Denied";
103257
+ const emoji = approved ? ":white_check_mark:" : ":x:";
103258
+ await client.chat.update({
103259
+ channel: channelId,
103260
+ ts: messageTs,
103261
+ text: `${emoji} Tool ${decision} by <@${btnBody.user?.id ?? "unknown"}>`,
103262
+ blocks: [
103263
+ {
103264
+ type: "section",
103265
+ text: {
103266
+ type: "mrkdwn",
103267
+ text: `${emoji} *Tool ${decision}* by <@${btnBody.user?.id ?? "unknown"}>`
103268
+ }
103269
+ }
103270
+ ]
103271
+ });
103272
+ }
103273
+ this.logger.debug?.(`[Slack] tool ${approved ? "approved" : "denied"}: toolCallId=${toolCallId}`);
103274
+ } catch (err) {
103275
+ this.logger.error("[Slack] tool action handler error:", err);
103276
+ this.recordError(err);
103277
+ }
103278
+ }
102246
103279
  };
102247
103280
 
102248
103281
  // ../relay/dist/adapters/claude-code/agent-handler.js
@@ -102269,7 +103302,7 @@ async function publishDispatchProgress(originalEnvelope, step, step_type, text6,
102269
103302
  };
102270
103303
  await relay.publish(originalEnvelope.replyTo, { type: "progress", step, step_type, text: text6, done: false }, opts);
102271
103304
  }
102272
- async function publishResponseWithCorrelation(originalEnvelope, event, fromId, relay, log, correlationId) {
103305
+ async function publishResponseWithCorrelation(originalEnvelope, event, fromId, relay, log, correlationId, enrichment) {
102273
103306
  if (!originalEnvelope.replyTo)
102274
103307
  return;
102275
103308
  const opts = {
@@ -102278,7 +103311,20 @@ async function publishResponseWithCorrelation(originalEnvelope, event, fromId, r
102278
103311
  hopCount: originalEnvelope.budget.hopCount + 1
102279
103312
  }
102280
103313
  };
102281
- const payload = correlationId ? { ...event, correlationId } : event;
103314
+ let payload;
103315
+ if (event.type === "approval_required" && enrichment?.agentId) {
103316
+ payload = {
103317
+ ...event,
103318
+ ...correlationId ? { correlationId } : {},
103319
+ data: {
103320
+ ...event.data,
103321
+ agentId: enrichment.agentId,
103322
+ ccaSessionKey: fromId
103323
+ }
103324
+ };
103325
+ } else {
103326
+ payload = correlationId ? { ...event, correlationId } : event;
103327
+ }
102282
103328
  const result2 = await relay.publish(originalEnvelope.replyTo, payload, opts);
102283
103329
  if (result2.deliveredTo === 0 && event.type !== "done") {
102284
103330
  log.warn(`[CCA] publishResponse delivered to 0 subscribers: subject=${originalEnvelope.replyTo}, eventType=${event.type}`);
@@ -102331,7 +103377,7 @@ async function handleAgentMessage(subject, envelope, context, startTime, config,
102331
103377
  log.debug?.(`[CCA] handleAgentMessage agentId=${agentId} ccaSessionKey=${ccaSessionKey}, payloadCwd=${payloadCwd ?? "(none)"}, context.agent.directory=${context?.agent?.directory ?? "(none)"}, resolvedCwd=${effectiveCwd ?? "(deferred to session)"}`);
102332
103378
  deps.agentManager.ensureSession(ccaSessionKey, {
102333
103379
  permissionMode: "default",
102334
- hasStarted: true,
103380
+ hasStarted: !!persistedSdkSessionId,
102335
103381
  ...effectiveCwd ? { cwd: effectiveCwd } : {}
102336
103382
  });
102337
103383
  deps.traceStore.updateSpan(envelope.id, { status: "delivered", deliveredAt: Date.now() });
@@ -102380,7 +103426,7 @@ async function handleAgentMessage(subject, envelope, context, startTime, config,
102380
103426
  await publishDispatchProgress(envelope, stepCounter, "tool_result", typeof data.content === "string" ? data.content : JSON.stringify(data), ccaSessionKey, relay);
102381
103427
  }
102382
103428
  } else {
102383
- await publishResponseWithCorrelation(envelope, event, ccaSessionKey, relay, log, correlationId);
103429
+ await publishResponseWithCorrelation(envelope, event, ccaSessionKey, relay, log, correlationId, { agentId });
102384
103430
  }
102385
103431
  }
102386
103432
  }
@@ -102410,6 +103456,8 @@ async function handleAgentMessage(subject, envelope, context, startTime, config,
102410
103456
  if (actualSdkId && actualSdkId !== agentId) {
102411
103457
  deps.agentSessionStore.set(agentId, actualSdkId);
102412
103458
  log.debug?.(`[CCA] persisted session mapping: ${agentId} \u2192 ${actualSdkId}`);
103459
+ } else {
103460
+ log.debug?.(`[CCA] no session mapping to persist: agentId=${agentId}, ccaSessionKey=${ccaSessionKey}, actualSdkId=${actualSdkId ?? "(none)"}`);
102413
103461
  }
102414
103462
  }
102415
103463
  log.info(`ClaudeCodeAdapter: published ${eventCount} event(s) to ${envelope.replyTo ?? "(no replyTo)"}`);
@@ -102435,6 +103483,22 @@ function extractAgentId(subject) {
102435
103483
  return null;
102436
103484
  return segments[2] || null;
102437
103485
  }
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
+ };
102438
103502
  function formatPromptWithContext(content3, envelope, agentId, sdkSessionId) {
102439
103503
  const lines = [
102440
103504
  `Agent-ID: ${agentId}`,
@@ -102452,6 +103516,12 @@ function formatPromptWithContext(content3, envelope, agentId, sdkSessionId) {
102452
103516
  if (envelope.replyTo) {
102453
103517
  lines.push("", `Reply to: ${envelope.replyTo}`, "If you cannot complete the task within the budget, summarize what you've done and stop.");
102454
103518
  }
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
+ }
102455
103525
  return `<relay_context>
102456
103526
  ${lines.join("\n")}
102457
103527
  </relay_context>
@@ -102649,6 +103719,48 @@ var AgentQueue = class {
102649
103719
  }
102650
103720
  };
102651
103721
 
103722
+ // ../relay/dist/adapters/claude-code/approval-handler.js
103723
+ var APPROVAL_SUBJECT_PATTERN = "relay.system.approval.>";
103724
+ function parseApprovalPayload(payload) {
103725
+ if (payload === null || typeof payload !== "object")
103726
+ return null;
103727
+ const obj = payload;
103728
+ if (obj.type !== "approval_response")
103729
+ return null;
103730
+ if (typeof obj.toolCallId !== "string" || !obj.toolCallId)
103731
+ return null;
103732
+ if (typeof obj.sessionId !== "string" || !obj.sessionId)
103733
+ return null;
103734
+ if (typeof obj.approved !== "boolean")
103735
+ return null;
103736
+ return {
103737
+ type: "approval_response",
103738
+ toolCallId: obj.toolCallId,
103739
+ sessionId: obj.sessionId,
103740
+ approved: obj.approved,
103741
+ respondedBy: typeof obj.respondedBy === "string" ? obj.respondedBy : void 0,
103742
+ platform: typeof obj.platform === "string" ? obj.platform : void 0
103743
+ };
103744
+ }
103745
+ function handleApprovalResponse(envelope, agentManager, log) {
103746
+ const approval = parseApprovalPayload(envelope.payload);
103747
+ if (!approval) {
103748
+ log.warn(`[CCA] approval-handler: received malformed payload on ${envelope.subject} \u2014 expected type='approval_response' with toolCallId, sessionId, approved`);
103749
+ return;
103750
+ }
103751
+ const { toolCallId, sessionId, approved, platform: platform2 = "unknown" } = approval;
103752
+ log.debug?.(`[CCA] approval-handler: ${approved ? "approve" : "deny"} toolCallId=${toolCallId} sessionId=${sessionId} platform=${platform2}`);
103753
+ const resolved = agentManager.approveTool(sessionId, toolCallId, approved);
103754
+ if (!resolved) {
103755
+ log.warn(`[CCA] approval-handler: approveTool returned false \u2014 interaction not found (already timed out?) toolCallId=${toolCallId} sessionId=${sessionId}`);
103756
+ }
103757
+ }
103758
+ function subscribeApprovalHandler(relay, agentManager, log) {
103759
+ return relay.subscribe(APPROVAL_SUBJECT_PATTERN, (envelope) => {
103760
+ handleApprovalResponse(envelope, agentManager, log);
103761
+ });
103762
+ }
103763
+
102652
103764
  // ../relay/dist/adapters/claude-code/claude-code-adapter.js
102653
103765
  var CLAUDE_CODE_MANIFEST = {
102654
103766
  type: "claude-code",
@@ -102688,6 +103800,8 @@ var ClaudeCodeAdapter = class {
102688
103800
  relay = null;
102689
103801
  activeCount = 0;
102690
103802
  agentQueue = new AgentQueue();
103803
+ /** Unsubscribe function for the `relay.system.approval.>` subscription. */
103804
+ approvalUnsub = null;
102691
103805
  status = {
102692
103806
  state: "disconnected",
102693
103807
  messageCount: { inbound: 0, outbound: 0 },
@@ -102716,6 +103830,7 @@ var ClaudeCodeAdapter = class {
102716
103830
  */
102717
103831
  async start(relay) {
102718
103832
  this.relay = relay;
103833
+ this.approvalUnsub = subscribeApprovalHandler(relay, this.deps.agentManager, this.deps.logger ?? console);
102719
103834
  this.status = {
102720
103835
  state: "connected",
102721
103836
  messageCount: { inbound: 0, outbound: 0 },
@@ -102727,6 +103842,8 @@ var ClaudeCodeAdapter = class {
102727
103842
  * Stop the adapter — clear relay reference, drain in-flight queue entries, and mark as disconnected.
102728
103843
  */
102729
103844
  async stop() {
103845
+ this.approvalUnsub?.();
103846
+ this.approvalUnsub = null;
102730
103847
  this.relay = null;
102731
103848
  this.agentQueue.clear();
102732
103849
  this.status = { ...this.status, state: "disconnected" };
@@ -103637,21 +104754,27 @@ function defaultAdapterStatus() {
103637
104754
  }
103638
104755
  async function createAdapter(config, deps, configPath, onPluginManifest) {
103639
104756
  switch (config.type) {
103640
- case "telegram":
103641
- return new TelegramAdapter(
104757
+ case "telegram": {
104758
+ const adapter = new TelegramAdapter(
103642
104759
  config.id,
103643
104760
  config.config
103644
104761
  );
104762
+ adapter.setLogger(createTaggedLogger(`telegram:${config.id}`));
104763
+ return adapter;
104764
+ }
103645
104765
  case "webhook":
103646
104766
  return new WebhookAdapter(
103647
104767
  config.id,
103648
104768
  config.config
103649
104769
  );
103650
- case "slack":
103651
- return new SlackAdapter(
104770
+ case "slack": {
104771
+ const adapter = new SlackAdapter(
103652
104772
  config.id,
103653
104773
  config.config
103654
104774
  );
104775
+ adapter.setLogger(createTaggedLogger(`slack:${config.id}`));
104776
+ return adapter;
104777
+ }
103655
104778
  case "claude-code":
103656
104779
  return new ClaudeCodeAdapter(
103657
104780
  config.id,
@@ -103693,6 +104816,8 @@ async function testAdapterConnection(adapter) {
103693
104816
  const noopRelay = {
103694
104817
  publish: async () => ({ messageId: "", deliveredTo: 0 }),
103695
104818
  onSignal: () => () => {
104819
+ },
104820
+ subscribe: () => () => {
103696
104821
  }
103697
104822
  };
103698
104823
  let fallbackTimer;