@band-ai/sdk 0.1.6 → 0.2.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.
Files changed (85) hide show
  1. package/README.md +372 -0
  2. package/dist/{ClaudeSDKAdapter-CRq4R-WB.d.cts → ClaudeSDKAdapter-BY4zEbCl.d.cts} +14 -10
  3. package/dist/{ClaudeSDKAdapter-BHOkjN3X.d.ts → ClaudeSDKAdapter-Cna5P1dR.d.ts} +14 -10
  4. package/dist/{acp-server-B1Da7L6u.d.ts → acp-server-CiUqN3G3.d.ts} +2 -2
  5. package/dist/{acp-server-BdRz9_UN.d.cts → acp-server-Dlj7D563.d.cts} +2 -2
  6. package/dist/adapters.cjs +734 -279
  7. package/dist/adapters.d.cts +21 -19
  8. package/dist/adapters.d.ts +21 -19
  9. package/dist/adapters.js +65 -44
  10. package/dist/backends-D-dBdDm-.d.ts +27 -0
  11. package/dist/backends-ZLxmQrTZ.d.cts +27 -0
  12. package/dist/{chunk-MSL6CWHY.js → chunk-4IAXZBXX.js} +77 -37
  13. package/dist/{chunk-SG4WLD2H.js → chunk-EAI2M3WM.js} +38 -94
  14. package/dist/{chunk-YR3BOQNW.js → chunk-JY75VSQK.js} +7 -7
  15. package/dist/{chunk-INA6YZTZ.js → chunk-LBWKISBS.js} +51 -31
  16. package/dist/{chunk-I5EZG52P.js → chunk-N2QTUMGO.js} +532 -327
  17. package/dist/{chunk-RHVH2GZR.js → chunk-NMZLX7TP.js} +110 -56
  18. package/dist/{chunk-EDPKUUJL.js → chunk-NT7TBFX2.js} +9 -1
  19. package/dist/{chunk-GVEQ3RAH.js → chunk-RV76SKFT.js} +169 -54
  20. package/dist/{chunk-W5NDZSYE.js → chunk-TLBCKDQO.js} +23 -23
  21. package/dist/chunk-US226AS4.js +130 -0
  22. package/dist/{chunk-IZPV6QS6.js → chunk-WL2HNJZ6.js} +38 -11
  23. package/dist/chunk-WLCM2OXG.js +181 -0
  24. package/dist/config.cjs +40 -13
  25. package/dist/config.d.cts +7 -1
  26. package/dist/config.d.ts +7 -1
  27. package/dist/config.js +2 -2
  28. package/dist/converters.cjs +491 -35
  29. package/dist/converters.d.cts +5 -5
  30. package/dist/converters.d.ts +5 -5
  31. package/dist/converters.js +4 -3
  32. package/dist/core.cjs +53 -10
  33. package/dist/core.d.cts +10 -9
  34. package/dist/core.d.ts +10 -9
  35. package/dist/core.js +8 -4
  36. package/dist/disconnectReason-IuADl6TH.d.cts +34 -0
  37. package/dist/disconnectReason-IuADl6TH.d.ts +34 -0
  38. package/dist/{dtos-DUpbIu8k.d.ts → dtos-B8KU_q4d.d.cts} +26 -6
  39. package/dist/{dtos-DUpbIu8k.d.cts → dtos-B8KU_q4d.d.ts} +26 -6
  40. package/dist/index.cjs +1510 -735
  41. package/dist/index.d.cts +18 -16
  42. package/dist/index.d.ts +18 -16
  43. package/dist/index.js +21 -13
  44. package/dist/linear.cjs +1044 -112
  45. package/dist/linear.d.cts +146 -16
  46. package/dist/linear.d.ts +146 -16
  47. package/dist/linear.js +1031 -110
  48. package/dist/mcp-claude.cjs +338 -66
  49. package/dist/mcp-claude.d.cts +3 -3
  50. package/dist/mcp-claude.d.ts +3 -3
  51. package/dist/mcp-claude.js +215 -16
  52. package/dist/mcp.cjs +414 -105
  53. package/dist/mcp.d.cts +29 -16
  54. package/dist/mcp.d.ts +29 -16
  55. package/dist/mcp.js +14 -14
  56. package/dist/{opencode-BBcDchcN.d.cts → opencode-2YdscIc1.d.cts} +5 -5
  57. package/dist/{opencode-DthQQUsZ.d.ts → opencode-Bld62x5t.d.ts} +5 -5
  58. package/dist/{pagination-CcDkVFxK.d.ts → pagination-BgEx-HeL.d.ts} +2 -2
  59. package/dist/{pagination-BPiys10t.d.cts → pagination-DqdkQArT.d.cts} +2 -2
  60. package/dist/{protocols-DrhzOH6K.d.ts → protocols-Bwwcql0h.d.ts} +1 -1
  61. package/dist/{protocols-CInhtFpA.d.cts → protocols-Bz1rxcUg.d.cts} +1 -1
  62. package/dist/rest.cjs +41 -97
  63. package/dist/rest.d.cts +7 -7
  64. package/dist/rest.d.ts +7 -7
  65. package/dist/rest.js +2 -2
  66. package/dist/runtime.cjs +998 -620
  67. package/dist/runtime.d.cts +22 -29
  68. package/dist/runtime.d.ts +22 -29
  69. package/dist/runtime.js +19 -67
  70. package/dist/{schemas-BpEBkq5V.d.cts → schemas-BotL3ZLH.d.cts} +51 -56
  71. package/dist/{schemas-vos1AaBc.d.ts → schemas-DBsC4J1g.d.ts} +51 -56
  72. package/dist/{sdk-CwTAxhwI.d.cts → sdk-BTciSLRQ.d.cts} +35 -5
  73. package/dist/{sdk-CcnQ5OUC.d.ts → sdk-qKJtMma_.d.ts} +35 -5
  74. package/dist/{simpleAdapter-9OE_p6QW.d.ts → simpleAdapter--wznuoOw.d.ts} +1 -1
  75. package/dist/{simpleAdapter-DqhvjsGr.d.cts → simpleAdapter-BpT4XbZC.d.cts} +1 -1
  76. package/dist/testing.d.cts +3 -3
  77. package/dist/testing.d.ts +3 -3
  78. package/dist/{types-7jVzOeOP.d.ts → types-BTN_ZETM.d.ts} +2 -2
  79. package/dist/{types-CxII3XkA.d.ts → types-BgYFd_Yw.d.cts} +5 -5
  80. package/dist/{types-CSHFipIc.d.cts → types-BivGO7I9.d.ts} +5 -5
  81. package/dist/{types-CovcHpYf.d.cts → types-D958TC71.d.cts} +2 -2
  82. package/package.json +25 -23
  83. package/dist/backends-CzDbiQar.d.ts +0 -20
  84. package/dist/backends-SVMT-7l4.d.cts +0 -20
  85. package/dist/chunk-SFVKOEQH.js +0 -103
package/dist/linear.cjs CHANGED
@@ -31,6 +31,12 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
31
31
  var linear_exports = {};
32
32
  __export(linear_exports, {
33
33
  DEFAULT_STATUS_MAPPING: () => DEFAULT_STATUS_MAPPING,
34
+ ELICITATION_BODY_MAX_LENGTH: () => ELICITATION_BODY_MAX_LENGTH,
35
+ PROVIDER_MAX_LENGTH: () => PROVIDER_MAX_LENGTH,
36
+ SELECT_OPTION_MAX_LENGTH: () => SELECT_OPTION_MAX_LENGTH,
37
+ STALE_SESSION_CHECK_INTERVAL_MS: () => STALE_SESSION_CHECK_INTERVAL_MS,
38
+ STALE_SESSION_THRESHOLD_MS: () => STALE_SESSION_THRESHOLD_MS,
39
+ StaleSessionGuard: () => StaleSessionGuard,
34
40
  buildLinearAuthorizationHeader: () => buildLinearAuthorizationHeader,
35
41
  completeLinearSession: () => completeLinearSession,
36
42
  createInProcessLinearBridgeDispatcher: () => createInProcessLinearBridgeDispatcher,
@@ -42,12 +48,17 @@ __export(linear_exports, {
42
48
  createSqliteSessionRoomStore: () => createSqliteSessionRoomStore,
43
49
  dedupeHandles: () => dedupeHandles,
44
50
  handleAgentSessionEvent: () => handleAgentSessionEvent,
51
+ handleAppUserNotification: () => handleAppUserNotification,
45
52
  isLinearApiKey: () => isLinearApiKey,
53
+ isSessionStale: () => isSessionStale,
46
54
  postAction: () => postAction,
55
+ postAuthElicitation: () => postAuthElicitation,
47
56
  postElicitation: () => postElicitation,
48
57
  postError: () => postError,
49
58
  postResponse: () => postResponse,
59
+ postSelectElicitation: () => postSelectElicitation,
50
60
  postThought: () => postThought,
61
+ sendRecoveryActivityIfStale: () => sendRecoveryActivityIfStale,
51
62
  stripHandlePrefix: () => stripHandlePrefix,
52
63
  updatePlan: () => updatePlan
53
64
  });
@@ -57,13 +68,13 @@ module.exports = __toCommonJS(linear_exports);
57
68
  var import_sdk2 = require("@linear/sdk");
58
69
 
59
70
  // src/core/errors.ts
60
- var ThenvoiSdkError = class extends Error {
71
+ var BandSdkError = class extends Error {
61
72
  constructor(message, cause) {
62
73
  super(message, cause !== void 0 ? { cause } : void 0);
63
- this.name = "ThenvoiSdkError";
74
+ this.name = "BandSdkError";
64
75
  }
65
76
  };
66
- var UnsupportedFeatureError = class extends ThenvoiSdkError {
77
+ var UnsupportedFeatureError = class extends BandSdkError {
67
78
  constructor(message) {
68
79
  super(message);
69
80
  this.name = "UnsupportedFeatureError";
@@ -99,20 +110,24 @@ function dedupeHandles(handles) {
99
110
 
100
111
  // src/integrations/linear/activities.ts
101
112
  var import_sdk = require("@linear/sdk");
102
- async function postActivity(client, sessionId, content) {
113
+ var ELICITATION_BODY_MAX_LENGTH = 1e4;
114
+ var SELECT_OPTION_MAX_LENGTH = 200;
115
+ var PROVIDER_MAX_LENGTH = 100;
116
+ async function postActivity(client, sessionId, content, options) {
103
117
  await client.createAgentActivity({
104
118
  agentSessionId: sessionId,
105
- content
119
+ content,
120
+ ...options?.ephemeral ? { ephemeral: true } : {}
106
121
  });
107
122
  }
108
- async function postBodyActivity(client, sessionId, type, body) {
109
- await postActivity(client, sessionId, { type, body });
123
+ async function postBodyActivity(client, sessionId, type, body, options) {
124
+ await postActivity(client, sessionId, { type, body }, options);
110
125
  }
111
- async function postThought(client, sessionId, body) {
112
- await postBodyActivity(client, sessionId, import_sdk.LinearDocument.AgentActivityType.Thought, body);
126
+ async function postThought(client, sessionId, body, options) {
127
+ await postBodyActivity(client, sessionId, import_sdk.LinearDocument.AgentActivityType.Thought, body, options);
113
128
  }
114
- async function postError(client, sessionId, body) {
115
- await postBodyActivity(client, sessionId, import_sdk.LinearDocument.AgentActivityType.Error, body);
129
+ async function postError(client, sessionId, body, options) {
130
+ await postBodyActivity(client, sessionId, import_sdk.LinearDocument.AgentActivityType.Error, body, options);
116
131
  }
117
132
  async function postResponse(client, sessionId, body) {
118
133
  await postBodyActivity(client, sessionId, import_sdk.LinearDocument.AgentActivityType.Response, body);
@@ -120,14 +135,53 @@ async function postResponse(client, sessionId, body) {
120
135
  async function postElicitation(client, sessionId, body) {
121
136
  await postBodyActivity(client, sessionId, import_sdk.LinearDocument.AgentActivityType.Elicitation, body);
122
137
  }
123
- async function postAction(client, sessionId, body) {
138
+ async function postSelectElicitation(client, sessionId, body, options) {
139
+ await postActivity(client, sessionId, {
140
+ type: import_sdk.LinearDocument.AgentActivityType.Elicitation,
141
+ body,
142
+ signal: import_sdk.LinearDocument.AgentActivitySignal.Select,
143
+ signalMetadata: { options }
144
+ });
145
+ }
146
+ async function postAuthElicitation(client, sessionId, body, url, provider) {
147
+ await postActivity(client, sessionId, {
148
+ type: import_sdk.LinearDocument.AgentActivityType.Elicitation,
149
+ body,
150
+ signal: import_sdk.LinearDocument.AgentActivitySignal.Auth,
151
+ signalMetadata: {
152
+ url,
153
+ ...provider ? { provider } : {}
154
+ }
155
+ });
156
+ }
157
+ async function postAction(client, sessionId, body, options) {
124
158
  await postActivity(client, sessionId, {
125
159
  type: import_sdk.LinearDocument.AgentActivityType.Action,
126
160
  action: body,
127
161
  parameter: ""
128
- });
162
+ }, options);
129
163
  }
164
+ var PLAN_STATUS_MAP = {
165
+ pending: "pending",
166
+ in_progress: "inProgress",
167
+ completed: "completed",
168
+ failed: "canceled"
169
+ };
130
170
  async function updatePlan(client, sessionId, steps) {
171
+ if (typeof client.updateAgentSession === "function") {
172
+ const plan = {
173
+ steps: steps.map((step) => ({
174
+ content: step.title,
175
+ status: PLAN_STATUS_MAP[step.status]
176
+ }))
177
+ };
178
+ try {
179
+ await client.updateAgentSession(sessionId, { plan });
180
+ return;
181
+ } catch (err) {
182
+ console.warn("updateAgentSession failed, falling back to legacy plan", err);
183
+ }
184
+ }
131
185
  const planSummary = steps.map((step) => {
132
186
  const icon = step.status === "completed" ? "\u2705" : step.status === "in_progress" ? "\u23F3" : step.status === "failed" ? "\u274C" : "\u2B1C";
133
187
  return `${icon} ${step.title}`;
@@ -139,6 +193,141 @@ ${planSummary}`
139
193
  });
140
194
  }
141
195
 
196
+ // src/integrations/linear/types.ts
197
+ var STALE_SESSION_CHECK_INTERVAL_MS = 20 * 6e4;
198
+ var STALE_SESSION_THRESHOLD_MS = 25 * 6e4;
199
+
200
+ // src/integrations/linear/stale-session-guard.ts
201
+ var DEFAULT_KEEPALIVE_MESSAGE = "Still working \u2014 waiting for specialist output in the collaboration room.";
202
+ var StaleSessionGuard = class {
203
+ store;
204
+ linearClient;
205
+ logger;
206
+ checkIntervalMs;
207
+ staleThresholdMs;
208
+ keepAliveMessage;
209
+ timer = null;
210
+ constructor(options) {
211
+ this.store = options.store;
212
+ this.linearClient = options.linearClient;
213
+ this.logger = options.logger ?? new NoopLogger();
214
+ this.checkIntervalMs = options.checkIntervalMs ?? STALE_SESSION_CHECK_INTERVAL_MS;
215
+ this.staleThresholdMs = options.staleThresholdMs ?? STALE_SESSION_THRESHOLD_MS;
216
+ this.keepAliveMessage = options.keepAliveMessage ?? DEFAULT_KEEPALIVE_MESSAGE;
217
+ }
218
+ /** Start the periodic keepalive check. Safe to call multiple times. */
219
+ start() {
220
+ if (this.timer) {
221
+ return;
222
+ }
223
+ this.logger.info("stale_session_guard.started", {
224
+ checkIntervalMs: this.checkIntervalMs,
225
+ staleThresholdMs: this.staleThresholdMs
226
+ });
227
+ this.timer = setInterval(() => {
228
+ void this.tick();
229
+ }, this.checkIntervalMs);
230
+ if (typeof this.timer === "object" && "unref" in this.timer) {
231
+ this.timer.unref();
232
+ }
233
+ }
234
+ /** Stop the periodic keepalive check. */
235
+ stop() {
236
+ if (!this.timer) {
237
+ return;
238
+ }
239
+ clearInterval(this.timer);
240
+ this.timer = null;
241
+ this.logger.info("stale_session_guard.stopped", {});
242
+ }
243
+ /** Run a single keepalive check. Exposed for testing and manual invocation. */
244
+ async tick() {
245
+ if (!this.store.listActiveSessions) {
246
+ this.logger.warn("stale_session_guard.store_missing_listActiveSessions", {});
247
+ return 0;
248
+ }
249
+ let sessions;
250
+ try {
251
+ sessions = await this.store.listActiveSessions();
252
+ } catch (error) {
253
+ this.logger.error("stale_session_guard.list_sessions_failed", {
254
+ error: error instanceof Error ? error.message : String(error)
255
+ });
256
+ return 0;
257
+ }
258
+ const now = Date.now();
259
+ let kept = 0;
260
+ for (const session of sessions) {
261
+ if (isSessionStale(session, now, this.staleThresholdMs)) {
262
+ try {
263
+ await postThought(this.linearClient, session.linearSessionId, this.keepAliveMessage);
264
+ const activityTimestamp = (/* @__PURE__ */ new Date()).toISOString();
265
+ await this.store.upsert({
266
+ ...session,
267
+ lastLinearActivityAt: activityTimestamp,
268
+ updatedAt: activityTimestamp
269
+ });
270
+ kept += 1;
271
+ this.logger.info("stale_session_guard.keepalive_sent", {
272
+ sessionId: session.linearSessionId,
273
+ lastLinearActivityAt: session.lastLinearActivityAt
274
+ });
275
+ } catch (error) {
276
+ this.logger.warn("stale_session_guard.keepalive_failed", {
277
+ sessionId: session.linearSessionId,
278
+ error: error instanceof Error ? error.message : String(error)
279
+ });
280
+ }
281
+ }
282
+ }
283
+ if (kept > 0) {
284
+ this.logger.info("stale_session_guard.tick_complete", {
285
+ totalSessions: sessions.length,
286
+ keepalivesSent: kept
287
+ });
288
+ }
289
+ return kept;
290
+ }
291
+ };
292
+ function isSessionStale(session, nowMs, thresholdMs) {
293
+ const referenceIso = session.lastLinearActivityAt ?? session.updatedAt;
294
+ const referenceMs = new Date(referenceIso).getTime();
295
+ if (Number.isNaN(referenceMs)) {
296
+ return true;
297
+ }
298
+ return nowMs - referenceMs >= thresholdMs;
299
+ }
300
+ async function sendRecoveryActivityIfStale(input) {
301
+ const thresholdMs = input.staleThresholdMs ?? STALE_SESSION_THRESHOLD_MS;
302
+ if (!isSessionStale(input.session, Date.now(), thresholdMs)) {
303
+ return false;
304
+ }
305
+ try {
306
+ await postThought(
307
+ input.linearClient,
308
+ input.session.linearSessionId,
309
+ "Resuming session \u2014 reconnecting after extended specialist work."
310
+ );
311
+ const now = (/* @__PURE__ */ new Date()).toISOString();
312
+ await input.store.upsert({
313
+ ...input.session,
314
+ lastLinearActivityAt: now,
315
+ updatedAt: now
316
+ });
317
+ input.logger.info("stale_session_guard.recovery_activity_sent", {
318
+ sessionId: input.session.linearSessionId,
319
+ lastLinearActivityAt: input.session.lastLinearActivityAt
320
+ });
321
+ return true;
322
+ } catch (error) {
323
+ input.logger.warn("stale_session_guard.recovery_activity_failed", {
324
+ sessionId: input.session.linearSessionId,
325
+ error: error instanceof Error ? error.message : String(error)
326
+ });
327
+ return false;
328
+ }
329
+ }
330
+
142
331
  // src/integrations/linear/bridge/message.ts
143
332
  function buildBridgeMessage(input) {
144
333
  const header = input.action === "created" ? "[Linear]: Agent session created." : "[Linear]: Agent session updated.";
@@ -158,6 +347,8 @@ function buildBridgeMessage(input) {
158
347
  const issueStateTypeLine = input.issueStateType ? `issue_state_type: ${input.issueStateType}` : "issue_state_type: none";
159
348
  const issueAssigneeLine = input.issueAssigneeName ? `issue_assignee: ${input.issueAssigneeName}` : "issue_assignee: none";
160
349
  const issueAssigneeIdLine = input.issueAssigneeId ? `issue_assignee_id: ${input.issueAssigneeId}` : "issue_assignee_id: none";
350
+ const issueDelegateLine = input.issueDelegateName ? `issue_delegate: ${input.issueDelegateName}` : "issue_delegate: none";
351
+ const issueDelegateIdLine = input.issueDelegateId ? `issue_delegate_id: ${input.issueDelegateId}` : "issue_delegate_id: none";
161
352
  const sessionStatusLine = input.sessionStatus ? `session_status: ${input.sessionStatus}` : "session_status: none";
162
353
  const sessionTypeLine = input.sessionType ? `session_type: ${input.sessionType}` : "session_type: none";
163
354
  const sessionCreatedLine = input.sessionCreatedAt ? `session_created_at: ${input.sessionCreatedAt}` : "session_created_at: none";
@@ -193,6 +384,8 @@ Linear session context:
193
384
  - ${issueStateTypeLine}
194
385
  - ${issueAssigneeLine}
195
386
  - ${issueAssigneeIdLine}
387
+ - ${issueDelegateLine}
388
+ - ${issueDelegateIdLine}
196
389
  - inferred_session_intent: ${input.sessionIntent}
197
390
  - writeback_mode: ${input.writebackMode}
198
391
  - ${appUserLine}
@@ -208,7 +401,7 @@ Bridge responsibilities:
208
401
  - keep Linear updated with meaningful milestones
209
402
  - use linear_post_thought and linear_post_action for meaningful progress updates
210
403
  - call linear_post_response when the work is actually finished
211
- - when you delegate inside Thenvoi, include the relevant ticket context in the room message so the specialist can act without hidden state
404
+ - when you delegate inside Band, include the relevant ticket context in the room message so the specialist can act without hidden state
212
405
 
213
406
  Suggested peers available in the registry right now. They are not in the room yet:
214
407
  ${suggestedPeersLine}
@@ -278,6 +471,11 @@ function extractIssueAssigneeField(issue, field) {
278
471
  const value = assignee?.[field];
279
472
  return typeof value === "string" ? value : null;
280
473
  }
474
+ function extractIssueDelegateField(issue, field) {
475
+ const delegate = extractNestedRecord(issue, "delegate");
476
+ const value = delegate?.[field];
477
+ return typeof value === "string" ? value : null;
478
+ }
281
479
  function firstNonEmpty(...values) {
282
480
  for (const value of values) {
283
481
  if (!value) {
@@ -300,11 +498,13 @@ function extractRecord(value) {
300
498
  }
301
499
 
302
500
  // src/integrations/linear/bridge/handler.ts
501
+ var DEFAULT_APP_BASE_URL = "https://app.band.ai";
303
502
  var SUPPORTED_ACTIONS = /* @__PURE__ */ new Set(["created", "updated", "canceled", "prompted"]);
304
503
  var MAX_PEER_LOOKUP_PAGES = 25;
305
504
  var PEER_PAGE_SIZE = 100;
306
505
  var RECOVERED_ROOM_EVENT_RETRY_LIMIT = 2;
307
506
  var RECOVERED_ROOM_EVENT_RETRY_BASE_DELAY_MS = 1e3;
507
+ var AUTO_START_ELIGIBLE_TYPES = /* @__PURE__ */ new Set(["backlog", "unstarted", "triage"]);
308
508
  function createLinearBridgeRuntime() {
309
509
  return {
310
510
  roomResolutionLocks: /* @__PURE__ */ new Map(),
@@ -369,9 +569,19 @@ async function handleAgentSessionEvent(input, options) {
369
569
  });
370
570
  return;
371
571
  }
572
+ if (action === "updated" && existingBySession) {
573
+ await sendRecoveryActivityIfStale({
574
+ session: existingBySession,
575
+ linearClient: input.deps.linearClient,
576
+ store: input.deps.store,
577
+ logger
578
+ });
579
+ }
580
+ let lastLinearActivityAt = null;
372
581
  if (action === "created" && !options?.skipAcknowledgment) {
373
582
  try {
374
583
  await postThought(input.deps.linearClient, sessionId, "Received session. Setting up workspace...");
584
+ lastLinearActivityAt = (/* @__PURE__ */ new Date()).toISOString();
375
585
  } catch (ackError) {
376
586
  logger.warn("linear_thenvoi_bridge.acknowledgment_failed", {
377
587
  sessionId,
@@ -379,17 +589,59 @@ async function handleAgentSessionEvent(input, options) {
379
589
  });
380
590
  }
381
591
  }
592
+ let delegatePromise;
593
+ if (action === "created" && issueId) {
594
+ const appUserId = input.payload.appUserId;
595
+ if (appUserId) {
596
+ delegatePromise = trySetAgentAsDelegate({
597
+ linearClient: input.deps.linearClient,
598
+ issueId,
599
+ appUserId,
600
+ logger
601
+ }).catch((delegateError) => {
602
+ logger.warn("linear_thenvoi_bridge.auto_delegate_failed", {
603
+ sessionId,
604
+ issueId,
605
+ appUserId,
606
+ error: delegateError instanceof Error ? delegateError.message : String(delegateError)
607
+ });
608
+ return { set: false, delegateName: null };
609
+ });
610
+ }
611
+ }
612
+ let autoStartPromise;
613
+ if (action === "created" && issueId) {
614
+ const originalStateType = extractIssueStateField(input.payload.agentSession.issue, "type");
615
+ const teamId = extractIssueTeamId(input.payload.agentSession.issue);
616
+ if (teamId && originalStateType && AUTO_START_ELIGIBLE_TYPES.has(originalStateType)) {
617
+ autoStartPromise = tryMoveIssueToStarted({
618
+ linearClient: input.deps.linearClient,
619
+ issueId,
620
+ teamId,
621
+ logger
622
+ }).catch((autoStartError) => {
623
+ logger.warn("linear_thenvoi_bridge.auto_start_failed", {
624
+ sessionId,
625
+ issueId,
626
+ teamId,
627
+ error: autoStartError instanceof Error ? autoStartError.message : String(autoStartError)
628
+ });
629
+ return { moved: false, stateId: null, stateName: null };
630
+ });
631
+ }
632
+ }
382
633
  let roomRecord = null;
634
+ let externalUrlPromise;
383
635
  try {
384
636
  const hostAgentHandle = await resolveHostAgentHandle({
385
- thenvoiRest: input.deps.thenvoiRest,
637
+ bandRest: input.deps.bandRest,
386
638
  configuredHostHandle: config.hostAgentHandle,
387
639
  logger,
388
640
  runtime
389
641
  });
390
642
  const existingByIssue = config.roomStrategy === "issue" && issueId ? await input.deps.store.getByIssueId(issueId) : null;
391
643
  roomRecord = await resolveRoomRecord({
392
- thenvoiRest: input.deps.thenvoiRest,
644
+ bandRest: input.deps.bandRest,
393
645
  store: input.deps.store,
394
646
  roomStrategy: config.roomStrategy,
395
647
  sessionId,
@@ -397,6 +649,22 @@ async function handleAgentSessionEvent(input, options) {
397
649
  logger,
398
650
  runtime
399
651
  });
652
+ if (action === "created") {
653
+ const roomId = roomRecord.bandRoomId;
654
+ externalUrlPromise = trySetSessionExternalUrl({
655
+ linearClient: input.deps.linearClient,
656
+ sessionId,
657
+ roomId,
658
+ appBaseUrl: config.bandAppBaseUrl,
659
+ logger
660
+ }).catch((urlError) => {
661
+ logger.warn("linear_thenvoi_bridge.set_external_url_failed", {
662
+ sessionId,
663
+ roomId,
664
+ error: urlError instanceof Error ? urlError.message : String(urlError)
665
+ });
666
+ });
667
+ }
400
668
  const sessionIntent = detectSessionIntent({
401
669
  issueStateType: extractIssueStateField(input.payload.agentSession.issue, "type"),
402
670
  promptContext: input.payload.promptContext,
@@ -405,8 +673,8 @@ async function handleAgentSessionEvent(input, options) {
405
673
  issueDescription: input.payload.agentSession.issue?.description
406
674
  });
407
675
  const suggestedPeerHandles = await selectRelevantPeerHandles({
408
- thenvoiRest: input.deps.thenvoiRest,
409
- roomId: roomRecord.thenvoiRoomId,
676
+ bandRest: input.deps.bandRest,
677
+ roomId: roomRecord.bandRoomId,
410
678
  intent: sessionIntent,
411
679
  hostAgentHandle,
412
680
  planningAgentHandles: input.config.planningAgentHandles,
@@ -414,11 +682,27 @@ async function handleAgentSessionEvent(input, options) {
414
682
  logger
415
683
  });
416
684
  await ensureRoomParticipants({
417
- thenvoiRest: input.deps.thenvoiRest,
418
- roomId: roomRecord.thenvoiRoomId,
685
+ bandRest: input.deps.bandRest,
686
+ roomId: roomRecord.bandRoomId,
419
687
  handles: [hostAgentHandle],
420
688
  logger
421
689
  });
690
+ const delegateResult = await delegatePromise;
691
+ let issueDelegateId = extractIssueDelegateField(input.payload.agentSession.issue, "id");
692
+ let issueDelegateName = extractIssueDelegateField(input.payload.agentSession.issue, "displayName") ?? extractIssueDelegateField(input.payload.agentSession.issue, "name");
693
+ if (delegateResult?.set && input.payload.appUserId) {
694
+ issueDelegateId = input.payload.appUserId;
695
+ issueDelegateName = delegateResult.delegateName ?? input.payload.appUserId;
696
+ }
697
+ const autoStartResult = await autoStartPromise;
698
+ let issueStateId = extractIssueStateField(input.payload.agentSession.issue, "id");
699
+ let issueStateName = extractIssueStateField(input.payload.agentSession.issue, "name");
700
+ let resolvedStateType = extractIssueStateField(input.payload.agentSession.issue, "type");
701
+ if (autoStartResult?.moved) {
702
+ if (autoStartResult.stateId) issueStateId = autoStartResult.stateId;
703
+ if (autoStartResult.stateName) issueStateName = autoStartResult.stateName;
704
+ resolvedStateType = "started";
705
+ }
422
706
  const message = buildBridgeMessage({
423
707
  sessionId,
424
708
  issueId,
@@ -436,11 +720,13 @@ async function handleAgentSessionEvent(input, options) {
436
720
  issueTeamKey: extractIssueTeamKey(input.payload.agentSession.issue),
437
721
  issueTeamName: extractIssueTeamName(input.payload.agentSession.issue),
438
722
  issueTeamId: extractIssueTeamId(input.payload.agentSession.issue),
439
- issueStateId: extractIssueStateField(input.payload.agentSession.issue, "id"),
440
- issueStateName: extractIssueStateField(input.payload.agentSession.issue, "name"),
441
- issueStateType: extractIssueStateField(input.payload.agentSession.issue, "type"),
723
+ issueStateId,
724
+ issueStateName,
725
+ issueStateType: resolvedStateType,
442
726
  issueAssigneeId: extractIssueAssigneeField(input.payload.agentSession.issue, "id"),
443
727
  issueAssigneeName: extractIssueAssigneeField(input.payload.agentSession.issue, "displayName") ?? extractIssueAssigneeField(input.payload.agentSession.issue, "name"),
728
+ issueDelegateId,
729
+ issueDelegateName,
444
730
  commentBody: input.payload.agentSession.comment?.body,
445
731
  commentId: input.payload.agentSession.comment?.id,
446
732
  sessionIntent,
@@ -458,17 +744,17 @@ async function handleAgentSessionEvent(input, options) {
458
744
  linear_issue_id: issueId,
459
745
  linear_prompt_context: input.payload.promptContext ?? null,
460
746
  linear_writeback_mode: config.writebackMode,
461
- linear_bridge: "thenvoi",
747
+ linear_bridge: "band",
462
748
  linear_host_handle: hostAgentHandle
463
749
  };
464
750
  const shouldResetRoomSession = Boolean(
465
- existingByIssue && existingByIssue.linearSessionId !== sessionId && existingByIssue.thenvoiRoomId === roomRecord.thenvoiRoomId && (existingByIssue.status === "completed" || existingByIssue.status === "errored")
751
+ existingByIssue && existingByIssue.linearSessionId !== sessionId && existingByIssue.bandRoomId === roomRecord.bandRoomId && (existingByIssue.status === "completed" || existingByIssue.status === "errored")
466
752
  );
467
- const authenticatedHostHandle = runtime.authenticatedHostHandleCache.get(input.deps.thenvoiRest) ?? null;
753
+ const authenticatedHostHandle = runtime.authenticatedHostHandleCache.get(input.deps.bandRest) ?? null;
468
754
  const canBootstrapDirectly = options?.skipRoomWrite === true || authenticatedHostHandle === null || authenticatedHostHandle === hostAgentHandle;
469
755
  if (!options?.skipRoomWrite || !canBootstrapDirectly) {
470
756
  roomRecord = await forwardBridgeMessage({
471
- thenvoiRest: input.deps.thenvoiRest,
757
+ bandRest: input.deps.bandRest,
472
758
  store: input.deps.store,
473
759
  logger,
474
760
  roomRecord,
@@ -486,7 +772,7 @@ async function handleAgentSessionEvent(input, options) {
486
772
  await enqueueBootstrapRequest(input.deps.store, {
487
773
  eventKey,
488
774
  linearSessionId: sessionId,
489
- thenvoiRoomId: roomRecord.thenvoiRoomId,
775
+ bandRoomId: roomRecord.bandRoomId,
490
776
  expectedContent: message,
491
777
  messageType: "task",
492
778
  senderId: linearActor.id,
@@ -497,19 +783,25 @@ async function handleAgentSessionEvent(input, options) {
497
783
  } : messageMetadata
498
784
  });
499
785
  }
786
+ const now = (/* @__PURE__ */ new Date()).toISOString();
500
787
  await saveSessionRecord(input.deps.store, {
501
788
  ...roomRecord,
502
789
  status: "active",
503
790
  lastEventKey: eventKey,
504
- updatedAt: (/* @__PURE__ */ new Date()).toISOString()
791
+ lastLinearActivityAt: lastLinearActivityAt ?? roomRecord.lastLinearActivityAt ?? now,
792
+ updatedAt: now
505
793
  });
794
+ await externalUrlPromise;
506
795
  logger.info("linear_thenvoi_bridge.message_forwarded", {
507
796
  sessionId,
508
797
  issueId,
509
- roomId: roomRecord.thenvoiRoomId,
798
+ roomId: roomRecord.bandRoomId,
510
799
  action
511
800
  });
512
801
  } catch (error) {
802
+ await externalUrlPromise;
803
+ await delegatePromise;
804
+ await autoStartPromise;
513
805
  try {
514
806
  await postError(
515
807
  input.deps.linearClient,
@@ -555,18 +847,39 @@ async function completeLinearSession(input) {
555
847
  if (!existing) {
556
848
  return;
557
849
  }
850
+ const now = (/* @__PURE__ */ new Date()).toISOString();
558
851
  await saveSessionRecord(input.store, {
559
852
  ...existing,
560
853
  status: "completed",
561
854
  lastEventKey: input.lastEventKey ?? existing.lastEventKey ?? null,
562
- updatedAt: (/* @__PURE__ */ new Date()).toISOString()
855
+ lastLinearActivityAt: now,
856
+ updatedAt: now
857
+ });
858
+ }
859
+ async function trySetSessionExternalUrl(input) {
860
+ if (typeof input.linearClient.agentSessionUpdateExternalUrl !== "function") {
861
+ input.logger.info("linear_thenvoi_bridge.set_external_url_skipped_no_api", {
862
+ sessionId: input.sessionId
863
+ });
864
+ return;
865
+ }
866
+ const base = input.appBaseUrl.replace(/\/+$/, "");
867
+ const roomUrl = `${base}/rooms/${input.roomId}`;
868
+ await input.linearClient.agentSessionUpdateExternalUrl(input.sessionId, {
869
+ externalUrls: [{ label: "View in Band", url: roomUrl }]
870
+ });
871
+ input.logger.info("linear_thenvoi_bridge.external_url_set", {
872
+ sessionId: input.sessionId,
873
+ roomId: input.roomId,
874
+ url: roomUrl
563
875
  });
564
876
  }
565
877
  function normalizeConfig(config) {
566
878
  return {
567
879
  roomStrategy: config.roomStrategy ?? "issue",
568
880
  writebackMode: config.writebackMode ?? "final_only",
569
- hostAgentHandle: normalizeOptionalHandle(config.hostAgentHandle)
881
+ hostAgentHandle: normalizeOptionalHandle(config.hostAgentHandle),
882
+ bandAppBaseUrl: config.bandAppBaseUrl ?? DEFAULT_APP_BASE_URL
570
883
  };
571
884
  }
572
885
  function normalizeOptionalHandle(handle) {
@@ -576,25 +889,82 @@ function normalizeOptionalHandle(handle) {
576
889
  const normalized = stripHandlePrefix(handle);
577
890
  return normalized.length > 0 ? normalized : null;
578
891
  }
892
+ async function trySetAgentAsDelegate(input) {
893
+ const issue = await input.linearClient.issue(input.issueId);
894
+ const existingDelegateId = issue.delegateId;
895
+ if (existingDelegateId) {
896
+ input.logger.info("linear_thenvoi_bridge.delegate_already_set", {
897
+ issueId: input.issueId,
898
+ existingDelegateId
899
+ });
900
+ return { set: false, delegateName: null };
901
+ }
902
+ await input.linearClient.updateIssue(input.issueId, {
903
+ delegateId: input.appUserId
904
+ });
905
+ let delegateName = null;
906
+ try {
907
+ const updated = await input.linearClient.issue(input.issueId);
908
+ const delegate = await updated.delegate;
909
+ delegateName = delegate?.displayName ?? delegate?.name ?? null;
910
+ } catch {
911
+ }
912
+ input.logger.info("linear_thenvoi_bridge.delegate_set", {
913
+ issueId: input.issueId,
914
+ delegateId: input.appUserId
915
+ });
916
+ return { set: true, delegateName };
917
+ }
918
+ async function tryMoveIssueToStarted(input) {
919
+ const response = await input.linearClient.workflowStates({
920
+ filter: {
921
+ team: { id: { eq: input.teamId } },
922
+ type: { eq: "started" }
923
+ }
924
+ });
925
+ const nodes = Array.isArray(response.nodes) ? response.nodes : [];
926
+ const startedStates = [...nodes].sort((a, b) => a.position - b.position);
927
+ const targetState = startedStates[0];
928
+ if (!targetState?.id) {
929
+ input.logger.info("linear_thenvoi_bridge.auto_start_no_started_state", {
930
+ issueId: input.issueId,
931
+ teamId: input.teamId
932
+ });
933
+ return { moved: false, stateId: null, stateName: null };
934
+ }
935
+ await input.linearClient.updateIssue(input.issueId, {
936
+ stateId: targetState.id
937
+ });
938
+ input.logger.info("linear_thenvoi_bridge.auto_start_moved", {
939
+ issueId: input.issueId,
940
+ stateId: targetState.id,
941
+ stateName: targetState.name ?? null
942
+ });
943
+ return {
944
+ moved: true,
945
+ stateId: targetState.id,
946
+ stateName: targetState.name ?? null
947
+ };
948
+ }
579
949
  async function resolveHostAgentHandle(input) {
580
950
  if (input.configuredHostHandle) {
581
- input.runtime.resolvedHostHandleCache.set(input.thenvoiRest, input.configuredHostHandle);
951
+ input.runtime.resolvedHostHandleCache.set(input.bandRest, input.configuredHostHandle);
582
952
  return input.configuredHostHandle;
583
953
  }
584
- const cachedAuthenticated = input.runtime.authenticatedHostHandleCache.get(input.thenvoiRest);
954
+ const cachedAuthenticated = input.runtime.authenticatedHostHandleCache.get(input.bandRest);
585
955
  if (cachedAuthenticated) {
586
- input.runtime.resolvedHostHandleCache.set(input.thenvoiRest, cachedAuthenticated);
956
+ input.runtime.resolvedHostHandleCache.set(input.bandRest, cachedAuthenticated);
587
957
  return cachedAuthenticated;
588
958
  }
589
- const cached = input.runtime.resolvedHostHandleCache.get(input.thenvoiRest);
959
+ const cached = input.runtime.resolvedHostHandleCache.get(input.bandRest);
590
960
  if (cached) {
591
961
  return cached;
592
962
  }
593
- const identity = await input.thenvoiRest.getAgentMe();
963
+ const identity = await input.bandRest.getAgentMe();
594
964
  const authenticatedAgentHandle = normalizeOptionalHandle(identity.handle);
595
965
  if (authenticatedAgentHandle) {
596
- input.runtime.authenticatedHostHandleCache.set(input.thenvoiRest, authenticatedAgentHandle);
597
- input.runtime.resolvedHostHandleCache.set(input.thenvoiRest, authenticatedAgentHandle);
966
+ input.runtime.authenticatedHostHandleCache.set(input.bandRest, authenticatedAgentHandle);
967
+ input.runtime.resolvedHostHandleCache.set(input.bandRest, authenticatedAgentHandle);
598
968
  return authenticatedAgentHandle;
599
969
  }
600
970
  throw new Error(
@@ -628,14 +998,14 @@ async function handleCanceledAction(input) {
628
998
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
629
999
  });
630
1000
  if (!input.skipRoomWrite) {
631
- await input.deps.thenvoiRest.createChatEvent(existing.thenvoiRoomId, {
1001
+ await input.deps.bandRest.createChatEvent(existing.bandRoomId, {
632
1002
  content: "[Linear]: Agent session canceled. Stop in-room execution and await new instructions.",
633
1003
  messageType: "task",
634
1004
  metadata: {
635
1005
  linear_event_action: "canceled",
636
1006
  linear_session_id: input.sessionId,
637
1007
  linear_issue_id: input.issueId,
638
- linear_bridge: "thenvoi"
1008
+ linear_bridge: "band"
639
1009
  }
640
1010
  });
641
1011
  }
@@ -661,15 +1031,21 @@ async function handlePromptedAction(input) {
661
1031
  });
662
1032
  return;
663
1033
  }
1034
+ await sendRecoveryActivityIfStale({
1035
+ session: existing,
1036
+ linearClient: input.deps.linearClient,
1037
+ store: input.deps.store,
1038
+ logger: input.logger
1039
+ });
664
1040
  const message = `[Linear User Response]: ${userResponse}`;
665
1041
  const metadata = {
666
1042
  linear_event_action: "prompted",
667
1043
  linear_session_id: input.sessionId,
668
- linear_bridge: "thenvoi"
1044
+ linear_bridge: "band"
669
1045
  };
670
- const canBootstrapDirectly = input.config.hostAgentHandle !== null ? true : Boolean(normalizeOptionalHandle((await input.deps.thenvoiRest.getAgentMe()).handle));
1046
+ const canBootstrapDirectly = input.config.hostAgentHandle !== null ? true : Boolean(normalizeOptionalHandle((await input.deps.bandRest.getAgentMe()).handle));
671
1047
  if (!input.skipRoomWrite || !canBootstrapDirectly) {
672
- await input.deps.thenvoiRest.createChatEvent(existing.thenvoiRoomId, {
1048
+ await input.deps.bandRest.createChatEvent(existing.bandRoomId, {
673
1049
  content: message,
674
1050
  messageType: "text",
675
1051
  metadata
@@ -680,7 +1056,7 @@ async function handlePromptedAction(input) {
680
1056
  await enqueueBootstrapRequest(input.deps.store, {
681
1057
  eventKey: input.eventKey,
682
1058
  linearSessionId: input.sessionId,
683
- thenvoiRoomId: existing.thenvoiRoomId,
1059
+ bandRoomId: existing.bandRoomId,
684
1060
  expectedContent: message,
685
1061
  messageType: "text",
686
1062
  senderId: linearActor.id,
@@ -696,7 +1072,7 @@ async function handlePromptedAction(input) {
696
1072
  });
697
1073
  input.logger.info("linear_thenvoi_bridge.prompted_forwarded", {
698
1074
  sessionId: input.sessionId,
699
- roomId: existing.thenvoiRoomId
1075
+ roomId: existing.bandRoomId
700
1076
  });
701
1077
  }
702
1078
  async function enqueueBootstrapRequest(store, request) {
@@ -738,7 +1114,7 @@ async function resolveRoomRecord(input) {
738
1114
  }
739
1115
  async function forwardBridgeMessage(input) {
740
1116
  try {
741
- await input.thenvoiRest.createChatEvent(input.roomRecord.thenvoiRoomId, {
1117
+ await input.bandRest.createChatEvent(input.roomRecord.bandRoomId, {
742
1118
  content: input.message,
743
1119
  messageType: input.messageType,
744
1120
  metadata: input.metadata
@@ -751,26 +1127,26 @@ async function forwardBridgeMessage(input) {
751
1127
  input.logger.warn("linear_thenvoi_bridge.room_forward_recovering_with_fresh_room", {
752
1128
  sessionId: input.sessionId,
753
1129
  issueId: input.issueId,
754
- previousRoomId: input.roomRecord.thenvoiRoomId,
1130
+ previousRoomId: input.roomRecord.bandRoomId,
755
1131
  error: error instanceof Error ? error.message : String(error)
756
1132
  });
757
1133
  const recoveredRoom = await createFreshRoomRecord({
758
- thenvoiRest: input.thenvoiRest,
1134
+ bandRest: input.bandRest,
759
1135
  store: input.store,
760
1136
  sessionId: input.sessionId,
761
1137
  issueId: input.issueId,
762
1138
  logger: input.logger
763
1139
  });
764
1140
  await ensureRoomParticipants({
765
- thenvoiRest: input.thenvoiRest,
766
- roomId: recoveredRoom.thenvoiRoomId,
1141
+ bandRest: input.bandRest,
1142
+ roomId: recoveredRoom.bandRoomId,
767
1143
  handles: [input.hostAgentHandle],
768
1144
  logger: input.logger
769
1145
  });
770
1146
  let attempt = 0;
771
1147
  while (true) {
772
1148
  try {
773
- await input.thenvoiRest.createChatEvent(recoveredRoom.thenvoiRoomId, {
1149
+ await input.bandRest.createChatEvent(recoveredRoom.bandRoomId, {
774
1150
  content: input.message,
775
1151
  messageType: input.messageType,
776
1152
  metadata: input.metadata
@@ -786,7 +1162,7 @@ async function forwardBridgeMessage(input) {
786
1162
  input.logger.warn("linear_thenvoi_bridge.room_recreated_retrying", {
787
1163
  sessionId: input.sessionId,
788
1164
  issueId: input.issueId,
789
- roomId: recoveredRoom.thenvoiRoomId,
1165
+ roomId: recoveredRoom.bandRoomId,
790
1166
  attempt,
791
1167
  delayMs,
792
1168
  error: recoveredError instanceof Error ? recoveredError.message : String(recoveredError)
@@ -816,11 +1192,11 @@ function isRetryableRateLimitError(error) {
816
1192
  }
817
1193
  async function createFreshRoomRecord(input) {
818
1194
  const now = (/* @__PURE__ */ new Date()).toISOString();
819
- const created = await input.thenvoiRest.createChat();
1195
+ const created = await input.bandRest.createChat();
820
1196
  const record = {
821
1197
  linearSessionId: input.sessionId,
822
1198
  linearIssueId: input.issueId,
823
- thenvoiRoomId: created.id,
1199
+ bandRoomId: created.id,
824
1200
  status: "active",
825
1201
  lastEventKey: null,
826
1202
  createdAt: now,
@@ -846,7 +1222,7 @@ async function resolveRoomRecordImpl(input) {
846
1222
  const linkedRecord = {
847
1223
  linearSessionId: input.sessionId,
848
1224
  linearIssueId: input.issueId,
849
- thenvoiRoomId: existingByIssue.thenvoiRoomId,
1225
+ bandRoomId: existingByIssue.bandRoomId,
850
1226
  status: "active",
851
1227
  lastEventKey: existingBySession?.lastEventKey ?? null,
852
1228
  createdAt: existingBySession?.createdAt ?? now2,
@@ -857,11 +1233,11 @@ async function resolveRoomRecordImpl(input) {
857
1233
  }
858
1234
  }
859
1235
  const now = (/* @__PURE__ */ new Date()).toISOString();
860
- const created = await input.thenvoiRest.createChat();
1236
+ const created = await input.bandRest.createChat();
861
1237
  const createdRecord = {
862
1238
  linearSessionId: input.sessionId,
863
1239
  linearIssueId: input.issueId,
864
- thenvoiRoomId: created.id,
1240
+ bandRoomId: created.id,
865
1241
  status: "active",
866
1242
  lastEventKey: null,
867
1243
  createdAt: now,
@@ -882,7 +1258,7 @@ async function ensureRoomParticipants(input) {
882
1258
  }
883
1259
  let existingParticipants;
884
1260
  try {
885
- existingParticipants = await input.thenvoiRest.listChatParticipants(input.roomId);
1261
+ existingParticipants = await input.bandRest.listChatParticipants(input.roomId);
886
1262
  } catch (error) {
887
1263
  if (isRetryableRateLimitError(error)) {
888
1264
  input.logger.warn("linear_thenvoi_bridge.participant_list_rate_limited", {
@@ -902,7 +1278,7 @@ async function ensureRoomParticipants(input) {
902
1278
  }
903
1279
  let peersByHandle;
904
1280
  try {
905
- peersByHandle = await lookupPeerIdsByHandle(input.thenvoiRest, input.roomId, missingHandles);
1281
+ peersByHandle = await lookupPeerIdsByHandle(input.bandRest, input.roomId, missingHandles);
906
1282
  } catch (error) {
907
1283
  if (error instanceof UnsupportedFeatureError) {
908
1284
  input.logger.warn("linear_thenvoi_bridge.peer_lookup_unavailable", {
@@ -924,7 +1300,7 @@ async function ensureRoomParticipants(input) {
924
1300
  continue;
925
1301
  }
926
1302
  try {
927
- await input.thenvoiRest.addChatParticipant(
1303
+ await input.bandRest.addChatParticipant(
928
1304
  input.roomId,
929
1305
  {
930
1306
  participantId,
@@ -943,14 +1319,14 @@ async function ensureRoomParticipants(input) {
943
1319
  }
944
1320
  }
945
1321
  }
946
- async function lookupPeerIdsByHandle(thenvoiRest, roomId, handles) {
947
- if (!thenvoiRest.listPeers) {
1322
+ async function lookupPeerIdsByHandle(bandRest, roomId, handles) {
1323
+ if (!bandRest.listPeers) {
948
1324
  throw new UnsupportedFeatureError("Peer listing is not available in current REST adapter");
949
1325
  }
950
1326
  const targets = new Set(handles);
951
1327
  const found = /* @__PURE__ */ new Map();
952
1328
  for (let page = 1; page <= MAX_PEER_LOOKUP_PAGES && found.size < targets.size; page += 1) {
953
- const response = await thenvoiRest.listPeers({
1329
+ const response = await bandRest.listPeers({
954
1330
  page,
955
1331
  pageSize: PEER_PAGE_SIZE,
956
1332
  notInChat: roomId
@@ -991,12 +1367,12 @@ async function selectRelevantPeerHandles(input) {
991
1367
  });
992
1368
  return configuredHandles;
993
1369
  }
994
- if (!input.thenvoiRest.listPeers) {
1370
+ if (!input.bandRest.listPeers) {
995
1371
  return [];
996
1372
  }
997
1373
  let peers;
998
1374
  try {
999
- const response = await input.thenvoiRest.listPeers({
1375
+ const response = await input.bandRest.listPeers({
1000
1376
  page: 1,
1001
1377
  pageSize: PEER_PAGE_SIZE,
1002
1378
  notInChat: input.roomId
@@ -1127,6 +1503,7 @@ var SqliteSessionRoomStore = class {
1127
1503
  thenvoi_room_id,
1128
1504
  status,
1129
1505
  last_event_key,
1506
+ last_linear_activity_at,
1130
1507
  created_at,
1131
1508
  updated_at
1132
1509
  FROM linear_thenvoi_session_rooms
@@ -1147,6 +1524,7 @@ var SqliteSessionRoomStore = class {
1147
1524
  thenvoi_room_id,
1148
1525
  status,
1149
1526
  last_event_key,
1527
+ last_linear_activity_at,
1150
1528
  created_at,
1151
1529
  updated_at
1152
1530
  FROM linear_thenvoi_session_rooms
@@ -1168,23 +1546,26 @@ var SqliteSessionRoomStore = class {
1168
1546
  thenvoi_room_id,
1169
1547
  status,
1170
1548
  last_event_key,
1549
+ last_linear_activity_at,
1171
1550
  created_at,
1172
1551
  updated_at
1173
- ) VALUES (?, ?, ?, ?, ?, ?, ?)
1552
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
1174
1553
  ON CONFLICT(linear_session_id)
1175
1554
  DO UPDATE SET
1176
1555
  linear_issue_id = excluded.linear_issue_id,
1177
1556
  thenvoi_room_id = excluded.thenvoi_room_id,
1178
1557
  status = excluded.status,
1179
1558
  last_event_key = excluded.last_event_key,
1559
+ last_linear_activity_at = excluded.last_linear_activity_at,
1180
1560
  updated_at = excluded.updated_at
1181
1561
  `
1182
1562
  ).run(
1183
1563
  record.linearSessionId,
1184
1564
  record.linearIssueId,
1185
- record.thenvoiRoomId,
1565
+ record.bandRoomId,
1186
1566
  record.status,
1187
1567
  record.lastEventKey ?? null,
1568
+ record.lastLinearActivityAt ?? null,
1188
1569
  record.createdAt,
1189
1570
  record.updatedAt
1190
1571
  );
@@ -1228,7 +1609,7 @@ var SqliteSessionRoomStore = class {
1228
1609
  ).run(
1229
1610
  request.eventKey,
1230
1611
  request.linearSessionId,
1231
- request.thenvoiRoomId,
1612
+ request.bandRoomId,
1232
1613
  request.expectedContent,
1233
1614
  request.messageType,
1234
1615
  request.metadata ? JSON.stringify(request.metadata) : null,
@@ -1260,7 +1641,7 @@ var SqliteSessionRoomStore = class {
1260
1641
  return rows.map((row) => ({
1261
1642
  eventKey: row.event_key,
1262
1643
  linearSessionId: row.linear_session_id,
1263
- thenvoiRoomId: row.thenvoi_room_id,
1644
+ bandRoomId: row.thenvoi_room_id,
1264
1645
  expectedContent: row.expected_content,
1265
1646
  messageType: row.message_type,
1266
1647
  metadata: this.parseMetadata(row.metadata_json),
@@ -1278,6 +1659,29 @@ var SqliteSessionRoomStore = class {
1278
1659
  `
1279
1660
  ).run((/* @__PURE__ */ new Date()).toISOString(), eventKey);
1280
1661
  }
1662
+ async listActiveSessions() {
1663
+ const db = await this.getDb();
1664
+ const rawRows = db.prepare(
1665
+ `
1666
+ SELECT
1667
+ linear_session_id,
1668
+ linear_issue_id,
1669
+ thenvoi_room_id,
1670
+ status,
1671
+ last_event_key,
1672
+ last_linear_activity_at,
1673
+ created_at,
1674
+ updated_at
1675
+ FROM linear_thenvoi_session_rooms
1676
+ WHERE status IN ('active', 'waiting')
1677
+ ORDER BY updated_at DESC
1678
+ `
1679
+ ).all();
1680
+ if (!Array.isArray(rawRows)) {
1681
+ return [];
1682
+ }
1683
+ return rawRows.map((raw) => parseSessionRoomRow(raw)).filter((row) => row !== null).map((row) => this.toRecord(row));
1684
+ }
1281
1685
  async close() {
1282
1686
  if (!this.dbPromise) {
1283
1687
  return;
@@ -1359,15 +1763,26 @@ var SqliteSessionRoomStore = class {
1359
1763
  throw error;
1360
1764
  }
1361
1765
  }
1766
+ try {
1767
+ db.exec(`
1768
+ ALTER TABLE linear_thenvoi_session_rooms
1769
+ ADD COLUMN last_linear_activity_at TEXT
1770
+ `);
1771
+ } catch (error) {
1772
+ if (!isDuplicateColumnError(error)) {
1773
+ throw error;
1774
+ }
1775
+ }
1362
1776
  return db;
1363
1777
  }
1364
1778
  toRecord(row) {
1365
1779
  return {
1366
1780
  linearSessionId: row.linear_session_id,
1367
1781
  linearIssueId: row.linear_issue_id,
1368
- thenvoiRoomId: row.thenvoi_room_id,
1782
+ bandRoomId: row.thenvoi_room_id,
1369
1783
  status: row.status,
1370
1784
  lastEventKey: row.last_event_key,
1785
+ lastLinearActivityAt: row.last_linear_activity_at,
1371
1786
  createdAt: row.created_at,
1372
1787
  updatedAt: row.updated_at
1373
1788
  };
@@ -1420,20 +1835,22 @@ function parseSessionRoomRow(value) {
1420
1835
  }
1421
1836
  const linearSessionId = asString(row.linear_session_id);
1422
1837
  const linearIssueId = asNullableString(row.linear_issue_id);
1423
- const thenvoiRoomId = asString(row.thenvoi_room_id);
1838
+ const bandRoomId = asString(row.thenvoi_room_id);
1424
1839
  const status = asString(row.status);
1425
1840
  const lastEventKey = asNullableString(row.last_event_key);
1841
+ const lastLinearActivityAt = asNullableString(row.last_linear_activity_at);
1426
1842
  const createdAt = asString(row.created_at);
1427
1843
  const updatedAt = asString(row.updated_at);
1428
- if (!linearSessionId || linearIssueId === void 0 || !thenvoiRoomId || !status || !isSessionStatus(status) || lastEventKey === void 0 || !createdAt || !updatedAt) {
1844
+ if (!linearSessionId || linearIssueId === void 0 || !bandRoomId || !status || !isSessionStatus(status) || lastEventKey === void 0 || lastLinearActivityAt === void 0 || !createdAt || !updatedAt) {
1429
1845
  return null;
1430
1846
  }
1431
1847
  return {
1432
1848
  linear_session_id: linearSessionId,
1433
1849
  linear_issue_id: linearIssueId,
1434
- thenvoi_room_id: thenvoiRoomId,
1850
+ thenvoi_room_id: bandRoomId,
1435
1851
  status,
1436
1852
  last_event_key: lastEventKey,
1853
+ last_linear_activity_at: lastLinearActivityAt,
1437
1854
  created_at: createdAt,
1438
1855
  updated_at: updatedAt
1439
1856
  };
@@ -1451,20 +1868,20 @@ function parseBootstrapRequestRow(value) {
1451
1868
  }
1452
1869
  const eventKey = asString(row.event_key);
1453
1870
  const linearSessionId = asString(row.linear_session_id);
1454
- const thenvoiRoomId = asString(row.thenvoi_room_id);
1871
+ const bandRoomId = asString(row.thenvoi_room_id);
1455
1872
  const expectedContent = asString(row.expected_content);
1456
1873
  const messageType = asString(row.message_type);
1457
1874
  const metadataJson = asNullableString(row.metadata_json);
1458
1875
  const createdAt = asString(row.created_at);
1459
1876
  const expiresAt = asString(row.expires_at);
1460
1877
  const processedAt = asNullableString(row.processed_at);
1461
- if (!eventKey || !linearSessionId || !thenvoiRoomId || !expectedContent || !messageType || metadataJson === void 0 || !createdAt || !expiresAt || processedAt === void 0) {
1878
+ if (!eventKey || !linearSessionId || !bandRoomId || !expectedContent || !messageType || metadataJson === void 0 || !createdAt || !expiresAt || processedAt === void 0) {
1462
1879
  return null;
1463
1880
  }
1464
1881
  return {
1465
1882
  event_key: eventKey,
1466
1883
  linear_session_id: linearSessionId,
1467
- thenvoi_room_id: thenvoiRoomId,
1884
+ thenvoi_room_id: bandRoomId,
1468
1885
  expected_content: expectedContent,
1469
1886
  message_type: messageType,
1470
1887
  metadata_json: metadataJson,
@@ -1479,8 +1896,18 @@ function createSqliteSessionRoomStore(dbPath) {
1479
1896
 
1480
1897
  // src/integrations/linear/tools.ts
1481
1898
  var import_zod = require("zod");
1899
+ var LOCALHOST_HOSTNAMES = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]"]);
1900
+ function isAllowedAuthUrl(u) {
1901
+ try {
1902
+ const parsed = new URL(u);
1903
+ if (parsed.protocol === "https:") return true;
1904
+ return LOCALHOST_HOSTNAMES.has(parsed.hostname);
1905
+ } catch {
1906
+ return false;
1907
+ }
1908
+ }
1482
1909
  function createLinearTools(options) {
1483
- const { client, store, enableElicitation = true } = options;
1910
+ const { client, store, enableElicitation = true, logger } = options;
1484
1911
  const sessionBodySchema = import_zod.z.object({
1485
1912
  session_id: import_zod.z.string().describe("The Linear agent session ID"),
1486
1913
  body: import_zod.z.string().describe("The message body in Markdown format")
@@ -1492,52 +1919,121 @@ function createLinearTools(options) {
1492
1919
  const requiredIssueIdSchema = issueIdInputSchema;
1493
1920
  const optionalIssueIdSchema = issueIdInputSchema;
1494
1921
  const tools = [];
1495
- const addSessionBodyTool = (name, description, handler) => {
1922
+ const ephemeralSessionBodySchema = sessionBodySchema.extend({
1923
+ ephemeral: import_zod.z.boolean().optional().describe(
1924
+ 'If true, this activity is displayed temporarily and replaced when the next activity arrives. Use for transient status indicators like "Thinking...", "Searching...", or "Waiting for response...".'
1925
+ )
1926
+ });
1927
+ const addSessionBodyTool = (name, description, activityFn, options2) => {
1928
+ const supportsEphemeral = options2?.supportsEphemeral ?? false;
1496
1929
  tools.push({
1497
1930
  name,
1498
- description,
1499
- schema: sessionBodySchema,
1500
- handler
1931
+ description: supportsEphemeral ? description + " Set ephemeral: true for transient status updates that should disappear when the next activity arrives." : description,
1932
+ schema: supportsEphemeral ? ephemeralSessionBodySchema : sessionBodySchema,
1933
+ handler: async (args) => {
1934
+ await activityFn(
1935
+ client,
1936
+ args.session_id,
1937
+ args.body,
1938
+ supportsEphemeral && args.ephemeral === true ? { ephemeral: true } : void 0
1939
+ );
1940
+ return { ok: true };
1941
+ }
1501
1942
  });
1502
1943
  };
1503
1944
  addSessionBodyTool(
1504
1945
  "linear_post_thought",
1505
1946
  "Post a thought to the Linear agent session, visible to the user as internal reasoning.",
1506
- async (args) => {
1507
- await postThought(client, args.session_id, args.body);
1508
- return { ok: true };
1509
- }
1947
+ postThought,
1948
+ { supportsEphemeral: true }
1510
1949
  );
1511
1950
  addSessionBodyTool(
1512
1951
  "linear_post_action",
1513
1952
  "Post an action to the Linear agent session, showing the user what step is being taken.",
1514
- async (args) => {
1515
- await postAction(client, args.session_id, args.body);
1516
- return { ok: true };
1517
- }
1953
+ postAction,
1954
+ { supportsEphemeral: true }
1518
1955
  );
1519
1956
  addSessionBodyTool(
1520
1957
  "linear_post_error",
1521
1958
  "Post an error to the Linear agent session to notify the user of a failure.",
1522
- async (args) => {
1523
- await postError(client, args.session_id, args.body);
1524
- return { ok: true };
1525
- }
1959
+ postError
1526
1960
  );
1527
1961
  if (enableElicitation) {
1528
- addSessionBodyTool(
1529
- "linear_ask_user",
1530
- "Ask the Linear user a question via an elicitation activity.",
1531
- async (args) => {
1532
- await postElicitation(client, args.session_id, args.body);
1962
+ tools.push({
1963
+ name: "linear_ask_user",
1964
+ description: "Ask the Linear user a question. When options are provided, Linear renders them as a clickable picker (select signal); otherwise the user sees a free-text prompt.",
1965
+ schema: import_zod.z.object({
1966
+ session_id: import_zod.z.string().describe("The Linear agent session ID"),
1967
+ body: import_zod.z.string().max(ELICITATION_BODY_MAX_LENGTH).describe("The question to ask, in Markdown format"),
1968
+ options: import_zod.z.array(import_zod.z.object({
1969
+ label: import_zod.z.string().max(SELECT_OPTION_MAX_LENGTH).describe("Display text for the option"),
1970
+ value: import_zod.z.string().max(SELECT_OPTION_MAX_LENGTH).describe("Value returned when the option is selected")
1971
+ })).min(2).max(20).optional().describe("Clickable options for a select picker (2\u201320 items). Omit for free-text input.")
1972
+ }),
1973
+ handler: async (args) => {
1974
+ const sessionId = args.session_id;
1975
+ const body = args.body;
1976
+ const options2 = args.options;
1977
+ if (options2) {
1978
+ await postSelectElicitation(client, sessionId, body, options2);
1979
+ } else {
1980
+ await postElicitation(client, sessionId, body);
1981
+ }
1533
1982
  return { ok: true };
1534
1983
  }
1535
- );
1984
+ });
1985
+ tools.push({
1986
+ name: "linear_select",
1987
+ description: "Present the Linear user with a set of clickable options via a select elicitation. Use this instead of linear_ask_user when the user should pick from a known list of choices.",
1988
+ schema: import_zod.z.object({
1989
+ session_id: import_zod.z.string().describe("The Linear agent session ID"),
1990
+ body: import_zod.z.string().describe("The question or prompt shown above the options (Markdown)"),
1991
+ options: import_zod.z.array(
1992
+ import_zod.z.object({
1993
+ label: import_zod.z.string().describe("Display label for this option"),
1994
+ value: import_zod.z.string().describe("Value returned when this option is selected")
1995
+ })
1996
+ ).min(1).max(25).describe("The selectable options")
1997
+ }),
1998
+ handler: async (args) => {
1999
+ await postSelectElicitation(
2000
+ client,
2001
+ args.session_id,
2002
+ args.body,
2003
+ args.options
2004
+ );
2005
+ return { ok: true };
2006
+ }
2007
+ });
2008
+ tools.push({
2009
+ name: "linear_request_auth",
2010
+ description: "Ask the Linear user to link an external account by presenting an authentication button. The user sees a 'Link account' UI that opens the provided URL.",
2011
+ schema: import_zod.z.object({
2012
+ session_id: import_zod.z.string().describe("The Linear agent session ID"),
2013
+ body: import_zod.z.string().max(ELICITATION_BODY_MAX_LENGTH).describe("Explanation of why authentication is needed, in Markdown format"),
2014
+ url: import_zod.z.string().url().refine(
2015
+ isAllowedAuthUrl,
2016
+ { message: "URL must use https (http allowed only for localhost)" }
2017
+ ).describe("The authentication URL to open when the user clicks the link button"),
2018
+ provider: import_zod.z.string().min(1).max(PROVIDER_MAX_LENGTH).optional().describe("Name of the external service (e.g. 'GitHub', 'Slack')")
2019
+ }),
2020
+ handler: async (args) => {
2021
+ await postAuthElicitation(
2022
+ client,
2023
+ args.session_id,
2024
+ args.body,
2025
+ args.url,
2026
+ args.provider
2027
+ );
2028
+ return { ok: true };
2029
+ }
2030
+ });
1536
2031
  }
1537
- addSessionBodyTool(
1538
- "linear_post_response",
1539
- "Post the final response to the Linear agent session and mark the session completed when a store is available.",
1540
- async (args) => {
2032
+ tools.push({
2033
+ name: "linear_post_response",
2034
+ description: "Post the final response to the Linear agent session and mark the session completed when a store is available.",
2035
+ schema: sessionBodySchema,
2036
+ handler: async (args) => {
1541
2037
  await completeLinearSession({
1542
2038
  linearClient: client,
1543
2039
  agentSessionId: args.session_id,
@@ -1546,11 +2042,11 @@ function createLinearTools(options) {
1546
2042
  });
1547
2043
  return { ok: true };
1548
2044
  }
1549
- );
2045
+ });
1550
2046
  tools.push(
1551
2047
  {
1552
2048
  name: "linear_update_plan",
1553
- description: "Update the plan for the Linear agent session, showing progress on each step.",
2049
+ description: "Update the structured plan for the Linear agent session. Renders as a native checklist in the Linear Agent Session UI with live status indicators.",
1554
2050
  schema: import_zod.z.object({
1555
2051
  session_id: import_zod.z.string().describe("The Linear agent session ID"),
1556
2052
  steps: import_zod.z.array(import_zod.z.object({
@@ -1575,6 +2071,35 @@ function createLinearTools(options) {
1575
2071
  optionalIssueIdSchema,
1576
2072
  issueCommentLimitSchema
1577
2073
  });
2074
+ if (typeof client.issueRepositorySuggestions === "function") {
2075
+ const suggestRepositories = client.issueRepositorySuggestions.bind(client);
2076
+ tools.push({
2077
+ name: "linear_suggest_repositories",
2078
+ description: "Ask Linear to rank candidate repositories by relevance for the current issue. Returns ranked suggestions with confidence scores. Use this before asking the user which repository to work in \u2014 if a suggestion has high confidence, auto-select it; otherwise present the top options via the select elicitation signal.",
2079
+ schema: requiredIssueIdSchema.extend({
2080
+ session_id: import_zod.z.string().describe("The Linear agent session ID"),
2081
+ repositories: import_zod.z.array(
2082
+ import_zod.z.object({
2083
+ hostname: import_zod.z.string().describe("Hostname of the Git service (e.g. 'github.com')"),
2084
+ repositoryFullName: import_zod.z.string().describe("Full name in owner/name format (e.g. 'acme/backend')")
2085
+ })
2086
+ ).min(1).max(50).describe("Candidate repositories the agent has access to")
2087
+ }),
2088
+ handler: async (args) => {
2089
+ const issueId = resolveIssueId("linear_suggest_repositories", args);
2090
+ const sessionId = args.session_id;
2091
+ const candidates = args.repositories;
2092
+ const response = await suggestRepositories(
2093
+ candidates,
2094
+ issueId,
2095
+ { agentSessionId: sessionId }
2096
+ );
2097
+ const suggestions = extractRepositorySuggestions(response);
2098
+ return { suggestions };
2099
+ }
2100
+ });
2101
+ }
2102
+ addSessionCreationTools({ tools, client, store, logger });
1578
2103
  return tools;
1579
2104
  }
1580
2105
  function addIssueTools(input) {
@@ -1628,6 +2153,7 @@ function addIssueTools(input) {
1628
2153
  priority: import_zod.z.number().int().min(0).max(4).optional().describe("Priority 0-4"),
1629
2154
  state_id: import_zod.z.string().optional().describe("Workflow state ID"),
1630
2155
  assignee_id: import_zod.z.string().nullable().optional().describe("Assignee user ID, or null to unassign"),
2156
+ delegate_id: import_zod.z.string().nullable().optional().describe("Agent user ID to delegate the issue to, or null to clear delegate"),
1631
2157
  estimate: import_zod.z.number().int().optional().describe("Estimate points"),
1632
2158
  due_date: import_zod.z.string().optional().describe("Due date ISO string")
1633
2159
  }),
@@ -1636,7 +2162,7 @@ function addIssueTools(input) {
1636
2162
  throw new Error("linear_update_issue is unavailable: Linear client does not support updateIssue().");
1637
2163
  }
1638
2164
  const issueId = resolveIssueId("linear_update_issue", args);
1639
- const hasUpdates = args.title !== void 0 || args.description !== void 0 || args.priority !== void 0 || args.state_id !== void 0 || args.assignee_id !== void 0 || args.estimate !== void 0 || args.due_date !== void 0;
2165
+ const hasUpdates = args.title !== void 0 || args.description !== void 0 || args.priority !== void 0 || args.state_id !== void 0 || args.assignee_id !== void 0 || args.delegate_id !== void 0 || args.estimate !== void 0 || args.due_date !== void 0;
1640
2166
  if (!hasUpdates) {
1641
2167
  throw new Error("linear_update_issue requires at least one update field.");
1642
2168
  }
@@ -1648,6 +2174,7 @@ function addIssueTools(input) {
1648
2174
  ...args.priority !== void 0 ? { priority: args.priority } : {},
1649
2175
  ...args.state_id !== void 0 ? { stateId: args.state_id } : {},
1650
2176
  ...args.assignee_id !== void 0 ? { assigneeId: args.assignee_id } : {},
2177
+ ...args.delegate_id !== void 0 ? { delegateId: args.delegate_id } : {},
1651
2178
  ...args.estimate !== void 0 ? { estimate: args.estimate } : {},
1652
2179
  ...args.due_date !== void 0 ? { dueDate: args.due_date } : {}
1653
2180
  }
@@ -1678,7 +2205,7 @@ function addIssueTools(input) {
1678
2205
  function assertUuid(toolName, value) {
1679
2206
  const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
1680
2207
  if (!uuidPattern.test(value)) {
1681
- throw new Error(`${toolName} requires the exact Linear issue UUID from the session context. Received "${value}".`);
2208
+ throw new Error(`${toolName} requires the exact Linear UUID from the session context (not the human-readable identifier). Received "${value}".`);
1682
2209
  }
1683
2210
  }
1684
2211
  function resolveIssueId(toolName, args) {
@@ -1721,6 +2248,11 @@ async function readIssue(client, issueId) {
1721
2248
  id: issue.assignee.id ?? null,
1722
2249
  name: issue.assignee.displayName ?? issue.assignee.name ?? null
1723
2250
  } : null,
2251
+ delegate: issue.delegate ? {
2252
+ id: issue.delegate.id ?? null,
2253
+ name: issue.delegate.displayName ?? issue.delegate.name ?? null
2254
+ } : null,
2255
+ delegate_id: issue.delegateId ?? null,
1724
2256
  team: issue.team ? {
1725
2257
  id: issue.team.id ?? null,
1726
2258
  key: issue.team.key ?? null,
@@ -1776,6 +2308,296 @@ async function readWorkflowStates(client, teamId) {
1776
2308
  })).sort((left, right) => (left.position ?? Number.MAX_SAFE_INTEGER) - (right.position ?? Number.MAX_SAFE_INTEGER));
1777
2309
  return { team_id: teamId, states };
1778
2310
  }
2311
+ function addSessionCreationTools(input) {
2312
+ const { tools, client, store, logger } = input;
2313
+ const sessionCreationBaseSchema = import_zod.z.object({
2314
+ external_link: import_zod.z.string().url().optional().describe("Optional URL of an external page associated with this session"),
2315
+ room_id: import_zod.z.string().min(1).optional().describe("The Band room ID to persist the session-room mapping. Pass this when creating a session from within a Band conversation.")
2316
+ });
2317
+ const sessionCreationSchema = sessionCreationBaseSchema.extend({
2318
+ issue_id: import_zod.z.string().uuid().describe("The Linear issue ID (UUID)")
2319
+ });
2320
+ async function persistSessionRoom(sessionId, issueId, roomId) {
2321
+ if (!store || typeof roomId !== "string") return null;
2322
+ try {
2323
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2324
+ await store.upsert({
2325
+ linearSessionId: sessionId,
2326
+ linearIssueId: issueId,
2327
+ bandRoomId: roomId,
2328
+ status: "active",
2329
+ createdAt: now,
2330
+ updatedAt: now
2331
+ });
2332
+ return null;
2333
+ } catch (err) {
2334
+ logger?.warn("linear_tools.session_room_persist_failed", {
2335
+ sessionId,
2336
+ issueId,
2337
+ roomId,
2338
+ error: err instanceof Error ? err.message : String(err)
2339
+ });
2340
+ return "session-room mapping not persisted";
2341
+ }
2342
+ }
2343
+ const createOnIssue = client.agentSessionCreateOnIssue?.bind(client);
2344
+ if (typeof createOnIssue === "function") {
2345
+ tools.push({
2346
+ name: "linear_create_session_on_issue",
2347
+ description: "Create a new Linear agent session on an existing issue. Use this when the conversation produces work that should be tracked against a known Linear issue and no session exists yet.",
2348
+ schema: sessionCreationSchema,
2349
+ handler: async (args) => {
2350
+ const issueId = args.issue_id;
2351
+ const result = await createOnIssue({
2352
+ issueId,
2353
+ ...typeof args.external_link === "string" ? { externalLink: args.external_link } : {}
2354
+ });
2355
+ const session = extractCreatedSession(result, logger);
2356
+ const warning = await persistSessionRoom(session.id, session.issueId, args.room_id);
2357
+ return { ok: true, session, ...warning ? { warning } : {} };
2358
+ }
2359
+ });
2360
+ }
2361
+ const createOnComment = client.agentSessionCreateOnComment?.bind(client);
2362
+ if (typeof createOnComment === "function") {
2363
+ tools.push({
2364
+ name: "linear_create_session_on_comment",
2365
+ description: "Create a new Linear agent session on a specific comment thread. Use this to attach agent work to an existing discussion on a Linear issue.",
2366
+ schema: sessionCreationBaseSchema.extend({
2367
+ comment_id: import_zod.z.string().uuid().describe("The Linear comment ID (UUID)")
2368
+ }),
2369
+ handler: async (args) => {
2370
+ const commentId = args.comment_id;
2371
+ const result = await createOnComment({
2372
+ commentId,
2373
+ ...typeof args.external_link === "string" ? { externalLink: args.external_link } : {}
2374
+ });
2375
+ const session = extractCreatedSession(result, logger);
2376
+ const warning = await persistSessionRoom(session.id, session.issueId, args.room_id);
2377
+ return { ok: true, session, ...warning ? { warning } : {} };
2378
+ }
2379
+ });
2380
+ }
2381
+ const createIssueFn = client.createIssue?.bind(client);
2382
+ if (typeof createIssueFn === "function") {
2383
+ tools.push({
2384
+ name: "linear_create_issue",
2385
+ description: "Create a new Linear issue from scratch. Use this when the Band conversation produces work that should be tracked as a new Linear issue. After creating the issue, call linear_create_session_on_issue to attach an agent session. Never create issues without explicit human intent or clear delegation.",
2386
+ schema: import_zod.z.object({
2387
+ team_id: import_zod.z.string().uuid().describe("The Linear team ID to create the issue in"),
2388
+ title: import_zod.z.string().min(1).describe("Issue title"),
2389
+ description: import_zod.z.string().optional().describe("Issue description in Markdown"),
2390
+ priority: import_zod.z.number().int().min(0).max(4).optional().describe("Priority 0-4 (0=none, 1=urgent, 2=high, 3=normal, 4=low)"),
2391
+ assignee_id: import_zod.z.string().uuid().optional().describe("Assignee user ID (UUID)"),
2392
+ state_id: import_zod.z.string().uuid().optional().describe("Workflow state ID (UUID)"),
2393
+ label_ids: import_zod.z.array(import_zod.z.string().uuid()).optional().describe("Label IDs to attach (UUIDs)")
2394
+ }),
2395
+ handler: async (args) => {
2396
+ const result = await createIssueFn({
2397
+ teamId: args.team_id,
2398
+ title: args.title,
2399
+ ...typeof args.description === "string" ? { description: args.description } : {},
2400
+ ...typeof args.priority === "number" ? { priority: args.priority } : {},
2401
+ ...typeof args.assignee_id === "string" ? { assigneeId: args.assignee_id } : {},
2402
+ ...typeof args.state_id === "string" ? { stateId: args.state_id } : {},
2403
+ ...Array.isArray(args.label_ids) ? { labelIds: args.label_ids } : {}
2404
+ });
2405
+ const issue = extractCreatedIssue(result, logger);
2406
+ return { ok: true, issue };
2407
+ }
2408
+ });
2409
+ }
2410
+ }
2411
+ function extractCreatedSession(result, logger) {
2412
+ const payload = typeof result === "object" && result !== null ? result : {};
2413
+ const session = typeof payload.agentSession === "object" && payload.agentSession !== null ? payload.agentSession : payload;
2414
+ const id = typeof session.id === "string" ? session.id : null;
2415
+ if (!id) {
2416
+ logger?.error("linear_tools.orphan_session_created", {
2417
+ raw: safeStringify(result)
2418
+ });
2419
+ throw new Error("Linear API returned a session without an ID.");
2420
+ }
2421
+ return {
2422
+ id,
2423
+ issueId: typeof session.issueId === "string" ? session.issueId : null,
2424
+ status: typeof session.status === "string" ? session.status : null
2425
+ };
2426
+ }
2427
+ function extractCreatedIssue(result, logger) {
2428
+ const payload = typeof result === "object" && result !== null ? result : {};
2429
+ const issue = typeof payload.issue === "object" && payload.issue !== null ? payload.issue : payload;
2430
+ const id = typeof issue.id === "string" ? issue.id : null;
2431
+ if (!id) {
2432
+ logger?.error("linear_tools.orphan_issue_created", {
2433
+ raw: safeStringify(result)
2434
+ });
2435
+ throw new Error("Linear API returned an issue without an ID.");
2436
+ }
2437
+ return {
2438
+ id,
2439
+ identifier: typeof issue.identifier === "string" ? issue.identifier : null,
2440
+ url: typeof issue.url === "string" ? issue.url : null,
2441
+ title: typeof issue.title === "string" ? issue.title : null
2442
+ };
2443
+ }
2444
+ function safeStringify(value) {
2445
+ try {
2446
+ return JSON.stringify(value);
2447
+ } catch {
2448
+ return String(value);
2449
+ }
2450
+ }
2451
+ function extractRepositorySuggestions(response) {
2452
+ if (response == null || typeof response !== "object") {
2453
+ return [];
2454
+ }
2455
+ const payload = response;
2456
+ if (!Array.isArray(payload.suggestions)) {
2457
+ return [];
2458
+ }
2459
+ return payload.suggestions.filter(
2460
+ (entry) => entry != null && typeof entry === "object" && typeof entry.repositoryFullName === "string"
2461
+ ).map((entry) => ({
2462
+ repositoryFullName: entry.repositoryFullName,
2463
+ hostname: typeof entry.hostname === "string" ? entry.hostname : null,
2464
+ confidence: typeof entry.confidence === "number" ? entry.confidence : 0
2465
+ })).sort((a, b) => b.confidence - a.confidence);
2466
+ }
2467
+
2468
+ // src/integrations/linear/notification.ts
2469
+ var MAX_COMMENT_LENGTH = 4e3;
2470
+ var MAX_ACTOR_NAME_LENGTH = 100;
2471
+ function sanitizeInlineText(text) {
2472
+ return text.replace(/[\x00-\x1f\x7f]/g, " ").trim();
2473
+ }
2474
+ function sanitizeBodyText(text) {
2475
+ return text.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, "").trim();
2476
+ }
2477
+ async function handleAppUserNotification(input) {
2478
+ const { payload, deps, logger, appUserId } = input;
2479
+ const notification = payload.notification;
2480
+ const typename = notification.__typename;
2481
+ switch (typename) {
2482
+ case "IssueUnassignedFromYouNotificationWebhookPayload":
2483
+ await handleIssueUnassigned({
2484
+ notification,
2485
+ deps,
2486
+ logger
2487
+ });
2488
+ return;
2489
+ case "IssueNewCommentNotificationWebhookPayload":
2490
+ await handleIssueNewComment({
2491
+ notification,
2492
+ deps,
2493
+ logger,
2494
+ appUserId
2495
+ });
2496
+ return;
2497
+ case "IssueCommentReactionNotificationWebhookPayload":
2498
+ case "IssueEmojiReactionNotificationWebhookPayload":
2499
+ logReaction({
2500
+ notification,
2501
+ logger
2502
+ });
2503
+ return;
2504
+ default:
2505
+ logger.info("linear_thenvoi_bridge.notification_unhandled", {
2506
+ notificationType: typename
2507
+ });
2508
+ }
2509
+ }
2510
+ async function handleIssueUnassigned(input) {
2511
+ const { notification, deps, logger } = input;
2512
+ const existing = await deps.store.getByIssueId(notification.issueId);
2513
+ if (!existing || existing.status !== "active" && existing.status !== "waiting") {
2514
+ logger.info("linear_thenvoi_bridge.notification_unassigned_no_session", {
2515
+ issueId: notification.issueId,
2516
+ reason: existing ? `session_status_${existing.status}` : "no_active_session"
2517
+ });
2518
+ return;
2519
+ }
2520
+ await deps.bandRest.createChatEvent(existing.bandRoomId, {
2521
+ content: "[Linear]: Issue unassigned from agent. Disengage from active work and await reassignment.",
2522
+ messageType: "task",
2523
+ metadata: {
2524
+ linear_notification_type: "issueUnassignedFromYou",
2525
+ linear_issue_id: notification.issueId,
2526
+ linear_actor_id: notification.actorId ?? null,
2527
+ linear_bridge: "band"
2528
+ }
2529
+ });
2530
+ await deps.store.markCanceled(existing.linearSessionId);
2531
+ logger.info("linear_thenvoi_bridge.notification_unassigned_handled", {
2532
+ issueId: notification.issueId,
2533
+ sessionId: existing.linearSessionId,
2534
+ roomId: existing.bandRoomId
2535
+ });
2536
+ }
2537
+ async function handleIssueNewComment(input) {
2538
+ const { notification, deps, logger, appUserId } = input;
2539
+ if (appUserId && notification.actorId === appUserId) {
2540
+ logger.info("linear_thenvoi_bridge.notification_comment_self_skipped", {
2541
+ issueId: notification.issueId,
2542
+ commentId: notification.commentId,
2543
+ actorId: notification.actorId
2544
+ });
2545
+ return;
2546
+ }
2547
+ const existing = await deps.store.getByIssueId(notification.issueId);
2548
+ if (!existing || existing.status !== "active" && existing.status !== "waiting") {
2549
+ logger.info("linear_thenvoi_bridge.notification_comment_skipped", {
2550
+ issueId: notification.issueId,
2551
+ reason: existing ? `session_status_${existing.status}` : "no_active_session"
2552
+ });
2553
+ return;
2554
+ }
2555
+ let actorName = sanitizeInlineText(notification.actor?.name ?? "");
2556
+ if (actorName.length > MAX_ACTOR_NAME_LENGTH) {
2557
+ actorName = actorName.slice(0, MAX_ACTOR_NAME_LENGTH) + "\u2026";
2558
+ }
2559
+ if (actorName.length === 0) {
2560
+ actorName = "Unknown user";
2561
+ }
2562
+ let commentBody = sanitizeBodyText(notification.comment.body ?? "");
2563
+ if (commentBody.length > MAX_COMMENT_LENGTH) {
2564
+ commentBody = commentBody.slice(0, MAX_COMMENT_LENGTH) + "\u2026";
2565
+ }
2566
+ if (!notification.actor?.name) {
2567
+ logger.info("linear_thenvoi_bridge.notification_comment_actor_fallback", {
2568
+ issueId: notification.issueId,
2569
+ commentId: notification.commentId,
2570
+ actorId: notification.actorId ?? null
2571
+ });
2572
+ }
2573
+ await deps.bandRest.createChatEvent(existing.bandRoomId, {
2574
+ content: `[Linear Comment from ${actorName}]: ${commentBody}`,
2575
+ messageType: "text",
2576
+ metadata: {
2577
+ linear_notification_type: "issueNewComment",
2578
+ linear_issue_id: notification.issueId,
2579
+ linear_comment_id: notification.commentId,
2580
+ linear_actor_id: notification.actorId ?? null,
2581
+ linear_bridge: "band"
2582
+ }
2583
+ });
2584
+ logger.info("linear_thenvoi_bridge.notification_comment_forwarded", {
2585
+ issueId: notification.issueId,
2586
+ commentId: notification.commentId,
2587
+ sessionId: existing.linearSessionId,
2588
+ roomId: existing.bandRoomId
2589
+ });
2590
+ }
2591
+ function logReaction(input) {
2592
+ const { notification, logger } = input;
2593
+ logger.info("linear_thenvoi_bridge.notification_reaction", {
2594
+ notificationType: notification.__typename,
2595
+ issueId: notification.issueId,
2596
+ actorId: notification.actorId ?? null,
2597
+ commentId: "commentId" in notification ? notification.commentId : null,
2598
+ reactionEmoji: notification.reactionEmoji
2599
+ });
2600
+ }
1779
2601
 
1780
2602
  // src/integrations/linear/webhook.ts
1781
2603
  var import_webhooks = require("@linear/sdk/webhooks");
@@ -1911,6 +2733,7 @@ function createLinearWebhookHandler(options) {
1911
2733
  const dispatcher = options.dispatcher ?? createInlineLinearBridgeDispatcher({ logger });
1912
2734
  const webhookClient = new import_webhooks.LinearWebhookClient(options.config.linearWebhookSecret);
1913
2735
  const inFlightEventKeys = /* @__PURE__ */ new Set();
2736
+ const processedNotificationIds = /* @__PURE__ */ new Set();
1914
2737
  return async (request, response) => {
1915
2738
  if (request.method !== "POST") {
1916
2739
  logger.warn("linear_thenvoi_bridge.webhook_invalid_method", {
@@ -1932,9 +2755,9 @@ function createLinearWebhookHandler(options) {
1932
2755
  return;
1933
2756
  }
1934
2757
  const rawBody = await readRawBody(request);
1935
- let payload;
2758
+ let parsed;
1936
2759
  try {
1937
- payload = webhookClient.parseData(
2760
+ parsed = webhookClient.parseData(
1938
2761
  rawBody,
1939
2762
  signature,
1940
2763
  timestamp
@@ -1946,13 +2769,111 @@ function createLinearWebhookHandler(options) {
1946
2769
  sendText(response, 400, "Invalid webhook");
1947
2770
  return;
1948
2771
  }
1949
- if (payload.type !== "AgentSessionEvent") {
2772
+ if (parsed.type === "AppUserNotification") {
2773
+ const notificationPayload = parsed;
2774
+ if (!notificationPayload.notification) {
2775
+ logger.warn("linear_thenvoi_bridge.webhook_notification_missing_payload", {
2776
+ appUserId: notificationPayload.appUserId ?? null,
2777
+ organizationId: notificationPayload.organizationId ?? null
2778
+ });
2779
+ sendText(response, 200, "OK");
2780
+ return;
2781
+ }
2782
+ const notificationId = notificationPayload.notification?.id;
2783
+ const notificationType = notificationPayload.notification.__typename;
2784
+ const isDedupable = notificationType !== "IssueCommentReactionNotificationWebhookPayload" && notificationType !== "IssueEmojiReactionNotificationWebhookPayload";
2785
+ if (isDedupable && notificationId && processedNotificationIds.has(notificationId)) {
2786
+ logger.info("linear_thenvoi_bridge.webhook_notification_duplicate_ignored", {
2787
+ notificationId
2788
+ });
2789
+ sendText(response, 200, "OK");
2790
+ return;
2791
+ }
2792
+ logger.info("linear_thenvoi_bridge.webhook_notification_received", {
2793
+ notificationType: notificationType ?? null,
2794
+ notificationId: notificationId ?? null,
2795
+ appUserId: notificationPayload.appUserId,
2796
+ organizationId: notificationPayload.organizationId
2797
+ });
2798
+ try {
2799
+ await handleAppUserNotification({
2800
+ payload: notificationPayload,
2801
+ deps: options.deps,
2802
+ logger,
2803
+ appUserId: notificationPayload.appUserId
2804
+ });
2805
+ if (isDedupable && notificationId) {
2806
+ processedNotificationIds.add(notificationId);
2807
+ if (processedNotificationIds.size > 1e3) {
2808
+ const oldest = processedNotificationIds.values().next().value;
2809
+ if (oldest) processedNotificationIds.delete(oldest);
2810
+ }
2811
+ }
2812
+ } catch (error) {
2813
+ logger.error("linear_thenvoi_bridge.webhook_notification_failed", {
2814
+ notificationType: notificationType ?? null,
2815
+ error: serializeError(error)
2816
+ });
2817
+ }
2818
+ sendText(response, 200, "OK");
2819
+ return;
2820
+ }
2821
+ if (parsed.type === "PermissionChange") {
2822
+ const action = parsed.action;
2823
+ if (normalizeAction2(action) === "teamaccesschanged") {
2824
+ const permPayload = parsed;
2825
+ logger.info("linear_thenvoi_bridge.team_access_changed", {
2826
+ action: permPayload.action,
2827
+ addedTeamIds: permPayload.addedTeamIds,
2828
+ removedTeamIds: permPayload.removedTeamIds,
2829
+ canAccessAllPublicTeams: permPayload.canAccessAllPublicTeams,
2830
+ organizationId: permPayload.organizationId
2831
+ });
2832
+ try {
2833
+ await options.permissionCallbacks?.onTeamAccessChanged?.(permPayload);
2834
+ } catch (error) {
2835
+ logger.error("linear_thenvoi_bridge.team_access_changed_callback_failed", {
2836
+ error: serializeError(error)
2837
+ });
2838
+ }
2839
+ } else {
2840
+ logger.info("linear_thenvoi_bridge.permission_change_event_ignored", {
2841
+ action
2842
+ });
2843
+ }
2844
+ sendText(response, 200, "OK");
2845
+ return;
2846
+ }
2847
+ if (parsed.type === "OAuthApp") {
2848
+ const oauthPayload = parsed;
2849
+ if (normalizeAction2(oauthPayload.action) === "revoked") {
2850
+ logger.warn("linear_thenvoi_bridge.oauth_app_revoked", {
2851
+ oauthClientId: oauthPayload.oauthClientId,
2852
+ organizationId: oauthPayload.organizationId
2853
+ });
2854
+ try {
2855
+ await options.permissionCallbacks?.onOAuthAppRevoked?.(oauthPayload);
2856
+ } catch (error) {
2857
+ logger.error("linear_thenvoi_bridge.oauth_app_revoked_callback_failed", {
2858
+ error: serializeError(error)
2859
+ });
2860
+ }
2861
+ } else {
2862
+ logger.info("linear_thenvoi_bridge.oauth_app_event_ignored", {
2863
+ action: oauthPayload.action
2864
+ });
2865
+ }
2866
+ sendText(response, 200, "OK");
2867
+ return;
2868
+ }
2869
+ if (parsed.type !== "AgentSessionEvent") {
1950
2870
  logger.info("linear_thenvoi_bridge.webhook_ignored_event", {
1951
- type: payload.type
2871
+ type: parsed.type
1952
2872
  });
1953
2873
  sendText(response, 200, "OK");
1954
2874
  return;
1955
2875
  }
2876
+ const payload = parsed;
1956
2877
  const eventKey = getAgentSessionEventKey(payload);
1957
2878
  logger.info("linear_thenvoi_bridge.webhook_received", {
1958
2879
  sessionId: payload.agentSession.id,
@@ -2185,6 +3106,12 @@ var DEFAULT_STATUS_MAPPING = {
2185
3106
  // Annotate the CommonJS export names for ESM import in node:
2186
3107
  0 && (module.exports = {
2187
3108
  DEFAULT_STATUS_MAPPING,
3109
+ ELICITATION_BODY_MAX_LENGTH,
3110
+ PROVIDER_MAX_LENGTH,
3111
+ SELECT_OPTION_MAX_LENGTH,
3112
+ STALE_SESSION_CHECK_INTERVAL_MS,
3113
+ STALE_SESSION_THRESHOLD_MS,
3114
+ StaleSessionGuard,
2188
3115
  buildLinearAuthorizationHeader,
2189
3116
  completeLinearSession,
2190
3117
  createInProcessLinearBridgeDispatcher,
@@ -2196,12 +3123,17 @@ var DEFAULT_STATUS_MAPPING = {
2196
3123
  createSqliteSessionRoomStore,
2197
3124
  dedupeHandles,
2198
3125
  handleAgentSessionEvent,
3126
+ handleAppUserNotification,
2199
3127
  isLinearApiKey,
3128
+ isSessionStale,
2200
3129
  postAction,
3130
+ postAuthElicitation,
2201
3131
  postElicitation,
2202
3132
  postError,
2203
3133
  postResponse,
3134
+ postSelectElicitation,
2204
3135
  postThought,
3136
+ sendRecoveryActivityIfStale,
2205
3137
  stripHandlePrefix,
2206
3138
  updatePlan
2207
3139
  });