@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.js CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  } from "./chunk-OD2G5LFQ.js";
4
4
  import {
5
5
  UnsupportedFeatureError
6
- } from "./chunk-YR3BOQNW.js";
6
+ } from "./chunk-JY75VSQK.js";
7
7
 
8
8
  // src/integrations/linear/bridge/handler.ts
9
9
  import { LinearDocument as L2 } from "@linear/sdk";
@@ -28,20 +28,24 @@ function dedupeHandles(handles) {
28
28
 
29
29
  // src/integrations/linear/activities.ts
30
30
  import { LinearDocument as L } from "@linear/sdk";
31
- async function postActivity(client, sessionId, content) {
31
+ var ELICITATION_BODY_MAX_LENGTH = 1e4;
32
+ var SELECT_OPTION_MAX_LENGTH = 200;
33
+ var PROVIDER_MAX_LENGTH = 100;
34
+ async function postActivity(client, sessionId, content, options) {
32
35
  await client.createAgentActivity({
33
36
  agentSessionId: sessionId,
34
- content
37
+ content,
38
+ ...options?.ephemeral ? { ephemeral: true } : {}
35
39
  });
36
40
  }
37
- async function postBodyActivity(client, sessionId, type, body) {
38
- await postActivity(client, sessionId, { type, body });
41
+ async function postBodyActivity(client, sessionId, type, body, options) {
42
+ await postActivity(client, sessionId, { type, body }, options);
39
43
  }
40
- async function postThought(client, sessionId, body) {
41
- await postBodyActivity(client, sessionId, L.AgentActivityType.Thought, body);
44
+ async function postThought(client, sessionId, body, options) {
45
+ await postBodyActivity(client, sessionId, L.AgentActivityType.Thought, body, options);
42
46
  }
43
- async function postError(client, sessionId, body) {
44
- await postBodyActivity(client, sessionId, L.AgentActivityType.Error, body);
47
+ async function postError(client, sessionId, body, options) {
48
+ await postBodyActivity(client, sessionId, L.AgentActivityType.Error, body, options);
45
49
  }
46
50
  async function postResponse(client, sessionId, body) {
47
51
  await postBodyActivity(client, sessionId, L.AgentActivityType.Response, body);
@@ -49,14 +53,53 @@ async function postResponse(client, sessionId, body) {
49
53
  async function postElicitation(client, sessionId, body) {
50
54
  await postBodyActivity(client, sessionId, L.AgentActivityType.Elicitation, body);
51
55
  }
52
- async function postAction(client, sessionId, body) {
56
+ async function postSelectElicitation(client, sessionId, body, options) {
57
+ await postActivity(client, sessionId, {
58
+ type: L.AgentActivityType.Elicitation,
59
+ body,
60
+ signal: L.AgentActivitySignal.Select,
61
+ signalMetadata: { options }
62
+ });
63
+ }
64
+ async function postAuthElicitation(client, sessionId, body, url, provider) {
65
+ await postActivity(client, sessionId, {
66
+ type: L.AgentActivityType.Elicitation,
67
+ body,
68
+ signal: L.AgentActivitySignal.Auth,
69
+ signalMetadata: {
70
+ url,
71
+ ...provider ? { provider } : {}
72
+ }
73
+ });
74
+ }
75
+ async function postAction(client, sessionId, body, options) {
53
76
  await postActivity(client, sessionId, {
54
77
  type: L.AgentActivityType.Action,
55
78
  action: body,
56
79
  parameter: ""
57
- });
80
+ }, options);
58
81
  }
82
+ var PLAN_STATUS_MAP = {
83
+ pending: "pending",
84
+ in_progress: "inProgress",
85
+ completed: "completed",
86
+ failed: "canceled"
87
+ };
59
88
  async function updatePlan(client, sessionId, steps) {
89
+ if (typeof client.updateAgentSession === "function") {
90
+ const plan = {
91
+ steps: steps.map((step) => ({
92
+ content: step.title,
93
+ status: PLAN_STATUS_MAP[step.status]
94
+ }))
95
+ };
96
+ try {
97
+ await client.updateAgentSession(sessionId, { plan });
98
+ return;
99
+ } catch (err) {
100
+ console.warn("updateAgentSession failed, falling back to legacy plan", err);
101
+ }
102
+ }
60
103
  const planSummary = steps.map((step) => {
61
104
  const icon = step.status === "completed" ? "\u2705" : step.status === "in_progress" ? "\u23F3" : step.status === "failed" ? "\u274C" : "\u2B1C";
62
105
  return `${icon} ${step.title}`;
@@ -68,6 +111,141 @@ ${planSummary}`
68
111
  });
69
112
  }
70
113
 
114
+ // src/integrations/linear/types.ts
115
+ var STALE_SESSION_CHECK_INTERVAL_MS = 20 * 6e4;
116
+ var STALE_SESSION_THRESHOLD_MS = 25 * 6e4;
117
+
118
+ // src/integrations/linear/stale-session-guard.ts
119
+ var DEFAULT_KEEPALIVE_MESSAGE = "Still working \u2014 waiting for specialist output in the collaboration room.";
120
+ var StaleSessionGuard = class {
121
+ store;
122
+ linearClient;
123
+ logger;
124
+ checkIntervalMs;
125
+ staleThresholdMs;
126
+ keepAliveMessage;
127
+ timer = null;
128
+ constructor(options) {
129
+ this.store = options.store;
130
+ this.linearClient = options.linearClient;
131
+ this.logger = options.logger ?? new NoopLogger();
132
+ this.checkIntervalMs = options.checkIntervalMs ?? STALE_SESSION_CHECK_INTERVAL_MS;
133
+ this.staleThresholdMs = options.staleThresholdMs ?? STALE_SESSION_THRESHOLD_MS;
134
+ this.keepAliveMessage = options.keepAliveMessage ?? DEFAULT_KEEPALIVE_MESSAGE;
135
+ }
136
+ /** Start the periodic keepalive check. Safe to call multiple times. */
137
+ start() {
138
+ if (this.timer) {
139
+ return;
140
+ }
141
+ this.logger.info("stale_session_guard.started", {
142
+ checkIntervalMs: this.checkIntervalMs,
143
+ staleThresholdMs: this.staleThresholdMs
144
+ });
145
+ this.timer = setInterval(() => {
146
+ void this.tick();
147
+ }, this.checkIntervalMs);
148
+ if (typeof this.timer === "object" && "unref" in this.timer) {
149
+ this.timer.unref();
150
+ }
151
+ }
152
+ /** Stop the periodic keepalive check. */
153
+ stop() {
154
+ if (!this.timer) {
155
+ return;
156
+ }
157
+ clearInterval(this.timer);
158
+ this.timer = null;
159
+ this.logger.info("stale_session_guard.stopped", {});
160
+ }
161
+ /** Run a single keepalive check. Exposed for testing and manual invocation. */
162
+ async tick() {
163
+ if (!this.store.listActiveSessions) {
164
+ this.logger.warn("stale_session_guard.store_missing_listActiveSessions", {});
165
+ return 0;
166
+ }
167
+ let sessions;
168
+ try {
169
+ sessions = await this.store.listActiveSessions();
170
+ } catch (error) {
171
+ this.logger.error("stale_session_guard.list_sessions_failed", {
172
+ error: error instanceof Error ? error.message : String(error)
173
+ });
174
+ return 0;
175
+ }
176
+ const now = Date.now();
177
+ let kept = 0;
178
+ for (const session of sessions) {
179
+ if (isSessionStale(session, now, this.staleThresholdMs)) {
180
+ try {
181
+ await postThought(this.linearClient, session.linearSessionId, this.keepAliveMessage);
182
+ const activityTimestamp = (/* @__PURE__ */ new Date()).toISOString();
183
+ await this.store.upsert({
184
+ ...session,
185
+ lastLinearActivityAt: activityTimestamp,
186
+ updatedAt: activityTimestamp
187
+ });
188
+ kept += 1;
189
+ this.logger.info("stale_session_guard.keepalive_sent", {
190
+ sessionId: session.linearSessionId,
191
+ lastLinearActivityAt: session.lastLinearActivityAt
192
+ });
193
+ } catch (error) {
194
+ this.logger.warn("stale_session_guard.keepalive_failed", {
195
+ sessionId: session.linearSessionId,
196
+ error: error instanceof Error ? error.message : String(error)
197
+ });
198
+ }
199
+ }
200
+ }
201
+ if (kept > 0) {
202
+ this.logger.info("stale_session_guard.tick_complete", {
203
+ totalSessions: sessions.length,
204
+ keepalivesSent: kept
205
+ });
206
+ }
207
+ return kept;
208
+ }
209
+ };
210
+ function isSessionStale(session, nowMs, thresholdMs) {
211
+ const referenceIso = session.lastLinearActivityAt ?? session.updatedAt;
212
+ const referenceMs = new Date(referenceIso).getTime();
213
+ if (Number.isNaN(referenceMs)) {
214
+ return true;
215
+ }
216
+ return nowMs - referenceMs >= thresholdMs;
217
+ }
218
+ async function sendRecoveryActivityIfStale(input) {
219
+ const thresholdMs = input.staleThresholdMs ?? STALE_SESSION_THRESHOLD_MS;
220
+ if (!isSessionStale(input.session, Date.now(), thresholdMs)) {
221
+ return false;
222
+ }
223
+ try {
224
+ await postThought(
225
+ input.linearClient,
226
+ input.session.linearSessionId,
227
+ "Resuming session \u2014 reconnecting after extended specialist work."
228
+ );
229
+ const now = (/* @__PURE__ */ new Date()).toISOString();
230
+ await input.store.upsert({
231
+ ...input.session,
232
+ lastLinearActivityAt: now,
233
+ updatedAt: now
234
+ });
235
+ input.logger.info("stale_session_guard.recovery_activity_sent", {
236
+ sessionId: input.session.linearSessionId,
237
+ lastLinearActivityAt: input.session.lastLinearActivityAt
238
+ });
239
+ return true;
240
+ } catch (error) {
241
+ input.logger.warn("stale_session_guard.recovery_activity_failed", {
242
+ sessionId: input.session.linearSessionId,
243
+ error: error instanceof Error ? error.message : String(error)
244
+ });
245
+ return false;
246
+ }
247
+ }
248
+
71
249
  // src/integrations/linear/bridge/message.ts
72
250
  function buildBridgeMessage(input) {
73
251
  const header = input.action === "created" ? "[Linear]: Agent session created." : "[Linear]: Agent session updated.";
@@ -87,6 +265,8 @@ function buildBridgeMessage(input) {
87
265
  const issueStateTypeLine = input.issueStateType ? `issue_state_type: ${input.issueStateType}` : "issue_state_type: none";
88
266
  const issueAssigneeLine = input.issueAssigneeName ? `issue_assignee: ${input.issueAssigneeName}` : "issue_assignee: none";
89
267
  const issueAssigneeIdLine = input.issueAssigneeId ? `issue_assignee_id: ${input.issueAssigneeId}` : "issue_assignee_id: none";
268
+ const issueDelegateLine = input.issueDelegateName ? `issue_delegate: ${input.issueDelegateName}` : "issue_delegate: none";
269
+ const issueDelegateIdLine = input.issueDelegateId ? `issue_delegate_id: ${input.issueDelegateId}` : "issue_delegate_id: none";
90
270
  const sessionStatusLine = input.sessionStatus ? `session_status: ${input.sessionStatus}` : "session_status: none";
91
271
  const sessionTypeLine = input.sessionType ? `session_type: ${input.sessionType}` : "session_type: none";
92
272
  const sessionCreatedLine = input.sessionCreatedAt ? `session_created_at: ${input.sessionCreatedAt}` : "session_created_at: none";
@@ -122,6 +302,8 @@ Linear session context:
122
302
  - ${issueStateTypeLine}
123
303
  - ${issueAssigneeLine}
124
304
  - ${issueAssigneeIdLine}
305
+ - ${issueDelegateLine}
306
+ - ${issueDelegateIdLine}
125
307
  - inferred_session_intent: ${input.sessionIntent}
126
308
  - writeback_mode: ${input.writebackMode}
127
309
  - ${appUserLine}
@@ -137,7 +319,7 @@ Bridge responsibilities:
137
319
  - keep Linear updated with meaningful milestones
138
320
  - use linear_post_thought and linear_post_action for meaningful progress updates
139
321
  - call linear_post_response when the work is actually finished
140
- - when you delegate inside Thenvoi, include the relevant ticket context in the room message so the specialist can act without hidden state
322
+ - when you delegate inside Band, include the relevant ticket context in the room message so the specialist can act without hidden state
141
323
 
142
324
  Suggested peers available in the registry right now. They are not in the room yet:
143
325
  ${suggestedPeersLine}
@@ -207,6 +389,11 @@ function extractIssueAssigneeField(issue, field) {
207
389
  const value = assignee?.[field];
208
390
  return typeof value === "string" ? value : null;
209
391
  }
392
+ function extractIssueDelegateField(issue, field) {
393
+ const delegate = extractNestedRecord(issue, "delegate");
394
+ const value = delegate?.[field];
395
+ return typeof value === "string" ? value : null;
396
+ }
210
397
  function firstNonEmpty(...values) {
211
398
  for (const value of values) {
212
399
  if (!value) {
@@ -229,11 +416,13 @@ function extractRecord(value) {
229
416
  }
230
417
 
231
418
  // src/integrations/linear/bridge/handler.ts
419
+ var DEFAULT_APP_BASE_URL = "https://app.band.ai";
232
420
  var SUPPORTED_ACTIONS = /* @__PURE__ */ new Set(["created", "updated", "canceled", "prompted"]);
233
421
  var MAX_PEER_LOOKUP_PAGES = 25;
234
422
  var PEER_PAGE_SIZE = 100;
235
423
  var RECOVERED_ROOM_EVENT_RETRY_LIMIT = 2;
236
424
  var RECOVERED_ROOM_EVENT_RETRY_BASE_DELAY_MS = 1e3;
425
+ var AUTO_START_ELIGIBLE_TYPES = /* @__PURE__ */ new Set(["backlog", "unstarted", "triage"]);
237
426
  function createLinearBridgeRuntime() {
238
427
  return {
239
428
  roomResolutionLocks: /* @__PURE__ */ new Map(),
@@ -298,9 +487,19 @@ async function handleAgentSessionEvent(input, options) {
298
487
  });
299
488
  return;
300
489
  }
490
+ if (action === "updated" && existingBySession) {
491
+ await sendRecoveryActivityIfStale({
492
+ session: existingBySession,
493
+ linearClient: input.deps.linearClient,
494
+ store: input.deps.store,
495
+ logger
496
+ });
497
+ }
498
+ let lastLinearActivityAt = null;
301
499
  if (action === "created" && !options?.skipAcknowledgment) {
302
500
  try {
303
501
  await postThought(input.deps.linearClient, sessionId, "Received session. Setting up workspace...");
502
+ lastLinearActivityAt = (/* @__PURE__ */ new Date()).toISOString();
304
503
  } catch (ackError) {
305
504
  logger.warn("linear_thenvoi_bridge.acknowledgment_failed", {
306
505
  sessionId,
@@ -308,17 +507,59 @@ async function handleAgentSessionEvent(input, options) {
308
507
  });
309
508
  }
310
509
  }
510
+ let delegatePromise;
511
+ if (action === "created" && issueId) {
512
+ const appUserId = input.payload.appUserId;
513
+ if (appUserId) {
514
+ delegatePromise = trySetAgentAsDelegate({
515
+ linearClient: input.deps.linearClient,
516
+ issueId,
517
+ appUserId,
518
+ logger
519
+ }).catch((delegateError) => {
520
+ logger.warn("linear_thenvoi_bridge.auto_delegate_failed", {
521
+ sessionId,
522
+ issueId,
523
+ appUserId,
524
+ error: delegateError instanceof Error ? delegateError.message : String(delegateError)
525
+ });
526
+ return { set: false, delegateName: null };
527
+ });
528
+ }
529
+ }
530
+ let autoStartPromise;
531
+ if (action === "created" && issueId) {
532
+ const originalStateType = extractIssueStateField(input.payload.agentSession.issue, "type");
533
+ const teamId = extractIssueTeamId(input.payload.agentSession.issue);
534
+ if (teamId && originalStateType && AUTO_START_ELIGIBLE_TYPES.has(originalStateType)) {
535
+ autoStartPromise = tryMoveIssueToStarted({
536
+ linearClient: input.deps.linearClient,
537
+ issueId,
538
+ teamId,
539
+ logger
540
+ }).catch((autoStartError) => {
541
+ logger.warn("linear_thenvoi_bridge.auto_start_failed", {
542
+ sessionId,
543
+ issueId,
544
+ teamId,
545
+ error: autoStartError instanceof Error ? autoStartError.message : String(autoStartError)
546
+ });
547
+ return { moved: false, stateId: null, stateName: null };
548
+ });
549
+ }
550
+ }
311
551
  let roomRecord = null;
552
+ let externalUrlPromise;
312
553
  try {
313
554
  const hostAgentHandle = await resolveHostAgentHandle({
314
- thenvoiRest: input.deps.thenvoiRest,
555
+ bandRest: input.deps.bandRest,
315
556
  configuredHostHandle: config.hostAgentHandle,
316
557
  logger,
317
558
  runtime
318
559
  });
319
560
  const existingByIssue = config.roomStrategy === "issue" && issueId ? await input.deps.store.getByIssueId(issueId) : null;
320
561
  roomRecord = await resolveRoomRecord({
321
- thenvoiRest: input.deps.thenvoiRest,
562
+ bandRest: input.deps.bandRest,
322
563
  store: input.deps.store,
323
564
  roomStrategy: config.roomStrategy,
324
565
  sessionId,
@@ -326,6 +567,22 @@ async function handleAgentSessionEvent(input, options) {
326
567
  logger,
327
568
  runtime
328
569
  });
570
+ if (action === "created") {
571
+ const roomId = roomRecord.bandRoomId;
572
+ externalUrlPromise = trySetSessionExternalUrl({
573
+ linearClient: input.deps.linearClient,
574
+ sessionId,
575
+ roomId,
576
+ appBaseUrl: config.bandAppBaseUrl,
577
+ logger
578
+ }).catch((urlError) => {
579
+ logger.warn("linear_thenvoi_bridge.set_external_url_failed", {
580
+ sessionId,
581
+ roomId,
582
+ error: urlError instanceof Error ? urlError.message : String(urlError)
583
+ });
584
+ });
585
+ }
329
586
  const sessionIntent = detectSessionIntent({
330
587
  issueStateType: extractIssueStateField(input.payload.agentSession.issue, "type"),
331
588
  promptContext: input.payload.promptContext,
@@ -334,8 +591,8 @@ async function handleAgentSessionEvent(input, options) {
334
591
  issueDescription: input.payload.agentSession.issue?.description
335
592
  });
336
593
  const suggestedPeerHandles = await selectRelevantPeerHandles({
337
- thenvoiRest: input.deps.thenvoiRest,
338
- roomId: roomRecord.thenvoiRoomId,
594
+ bandRest: input.deps.bandRest,
595
+ roomId: roomRecord.bandRoomId,
339
596
  intent: sessionIntent,
340
597
  hostAgentHandle,
341
598
  planningAgentHandles: input.config.planningAgentHandles,
@@ -343,11 +600,27 @@ async function handleAgentSessionEvent(input, options) {
343
600
  logger
344
601
  });
345
602
  await ensureRoomParticipants({
346
- thenvoiRest: input.deps.thenvoiRest,
347
- roomId: roomRecord.thenvoiRoomId,
603
+ bandRest: input.deps.bandRest,
604
+ roomId: roomRecord.bandRoomId,
348
605
  handles: [hostAgentHandle],
349
606
  logger
350
607
  });
608
+ const delegateResult = await delegatePromise;
609
+ let issueDelegateId = extractIssueDelegateField(input.payload.agentSession.issue, "id");
610
+ let issueDelegateName = extractIssueDelegateField(input.payload.agentSession.issue, "displayName") ?? extractIssueDelegateField(input.payload.agentSession.issue, "name");
611
+ if (delegateResult?.set && input.payload.appUserId) {
612
+ issueDelegateId = input.payload.appUserId;
613
+ issueDelegateName = delegateResult.delegateName ?? input.payload.appUserId;
614
+ }
615
+ const autoStartResult = await autoStartPromise;
616
+ let issueStateId = extractIssueStateField(input.payload.agentSession.issue, "id");
617
+ let issueStateName = extractIssueStateField(input.payload.agentSession.issue, "name");
618
+ let resolvedStateType = extractIssueStateField(input.payload.agentSession.issue, "type");
619
+ if (autoStartResult?.moved) {
620
+ if (autoStartResult.stateId) issueStateId = autoStartResult.stateId;
621
+ if (autoStartResult.stateName) issueStateName = autoStartResult.stateName;
622
+ resolvedStateType = "started";
623
+ }
351
624
  const message = buildBridgeMessage({
352
625
  sessionId,
353
626
  issueId,
@@ -365,11 +638,13 @@ async function handleAgentSessionEvent(input, options) {
365
638
  issueTeamKey: extractIssueTeamKey(input.payload.agentSession.issue),
366
639
  issueTeamName: extractIssueTeamName(input.payload.agentSession.issue),
367
640
  issueTeamId: extractIssueTeamId(input.payload.agentSession.issue),
368
- issueStateId: extractIssueStateField(input.payload.agentSession.issue, "id"),
369
- issueStateName: extractIssueStateField(input.payload.agentSession.issue, "name"),
370
- issueStateType: extractIssueStateField(input.payload.agentSession.issue, "type"),
641
+ issueStateId,
642
+ issueStateName,
643
+ issueStateType: resolvedStateType,
371
644
  issueAssigneeId: extractIssueAssigneeField(input.payload.agentSession.issue, "id"),
372
645
  issueAssigneeName: extractIssueAssigneeField(input.payload.agentSession.issue, "displayName") ?? extractIssueAssigneeField(input.payload.agentSession.issue, "name"),
646
+ issueDelegateId,
647
+ issueDelegateName,
373
648
  commentBody: input.payload.agentSession.comment?.body,
374
649
  commentId: input.payload.agentSession.comment?.id,
375
650
  sessionIntent,
@@ -387,17 +662,17 @@ async function handleAgentSessionEvent(input, options) {
387
662
  linear_issue_id: issueId,
388
663
  linear_prompt_context: input.payload.promptContext ?? null,
389
664
  linear_writeback_mode: config.writebackMode,
390
- linear_bridge: "thenvoi",
665
+ linear_bridge: "band",
391
666
  linear_host_handle: hostAgentHandle
392
667
  };
393
668
  const shouldResetRoomSession = Boolean(
394
- existingByIssue && existingByIssue.linearSessionId !== sessionId && existingByIssue.thenvoiRoomId === roomRecord.thenvoiRoomId && (existingByIssue.status === "completed" || existingByIssue.status === "errored")
669
+ existingByIssue && existingByIssue.linearSessionId !== sessionId && existingByIssue.bandRoomId === roomRecord.bandRoomId && (existingByIssue.status === "completed" || existingByIssue.status === "errored")
395
670
  );
396
- const authenticatedHostHandle = runtime.authenticatedHostHandleCache.get(input.deps.thenvoiRest) ?? null;
671
+ const authenticatedHostHandle = runtime.authenticatedHostHandleCache.get(input.deps.bandRest) ?? null;
397
672
  const canBootstrapDirectly = options?.skipRoomWrite === true || authenticatedHostHandle === null || authenticatedHostHandle === hostAgentHandle;
398
673
  if (!options?.skipRoomWrite || !canBootstrapDirectly) {
399
674
  roomRecord = await forwardBridgeMessage({
400
- thenvoiRest: input.deps.thenvoiRest,
675
+ bandRest: input.deps.bandRest,
401
676
  store: input.deps.store,
402
677
  logger,
403
678
  roomRecord,
@@ -415,7 +690,7 @@ async function handleAgentSessionEvent(input, options) {
415
690
  await enqueueBootstrapRequest(input.deps.store, {
416
691
  eventKey,
417
692
  linearSessionId: sessionId,
418
- thenvoiRoomId: roomRecord.thenvoiRoomId,
693
+ bandRoomId: roomRecord.bandRoomId,
419
694
  expectedContent: message,
420
695
  messageType: "task",
421
696
  senderId: linearActor.id,
@@ -426,19 +701,25 @@ async function handleAgentSessionEvent(input, options) {
426
701
  } : messageMetadata
427
702
  });
428
703
  }
704
+ const now = (/* @__PURE__ */ new Date()).toISOString();
429
705
  await saveSessionRecord(input.deps.store, {
430
706
  ...roomRecord,
431
707
  status: "active",
432
708
  lastEventKey: eventKey,
433
- updatedAt: (/* @__PURE__ */ new Date()).toISOString()
709
+ lastLinearActivityAt: lastLinearActivityAt ?? roomRecord.lastLinearActivityAt ?? now,
710
+ updatedAt: now
434
711
  });
712
+ await externalUrlPromise;
435
713
  logger.info("linear_thenvoi_bridge.message_forwarded", {
436
714
  sessionId,
437
715
  issueId,
438
- roomId: roomRecord.thenvoiRoomId,
716
+ roomId: roomRecord.bandRoomId,
439
717
  action
440
718
  });
441
719
  } catch (error) {
720
+ await externalUrlPromise;
721
+ await delegatePromise;
722
+ await autoStartPromise;
442
723
  try {
443
724
  await postError(
444
725
  input.deps.linearClient,
@@ -484,18 +765,39 @@ async function completeLinearSession(input) {
484
765
  if (!existing) {
485
766
  return;
486
767
  }
768
+ const now = (/* @__PURE__ */ new Date()).toISOString();
487
769
  await saveSessionRecord(input.store, {
488
770
  ...existing,
489
771
  status: "completed",
490
772
  lastEventKey: input.lastEventKey ?? existing.lastEventKey ?? null,
491
- updatedAt: (/* @__PURE__ */ new Date()).toISOString()
773
+ lastLinearActivityAt: now,
774
+ updatedAt: now
775
+ });
776
+ }
777
+ async function trySetSessionExternalUrl(input) {
778
+ if (typeof input.linearClient.agentSessionUpdateExternalUrl !== "function") {
779
+ input.logger.info("linear_thenvoi_bridge.set_external_url_skipped_no_api", {
780
+ sessionId: input.sessionId
781
+ });
782
+ return;
783
+ }
784
+ const base = input.appBaseUrl.replace(/\/+$/, "");
785
+ const roomUrl = `${base}/rooms/${input.roomId}`;
786
+ await input.linearClient.agentSessionUpdateExternalUrl(input.sessionId, {
787
+ externalUrls: [{ label: "View in Band", url: roomUrl }]
788
+ });
789
+ input.logger.info("linear_thenvoi_bridge.external_url_set", {
790
+ sessionId: input.sessionId,
791
+ roomId: input.roomId,
792
+ url: roomUrl
492
793
  });
493
794
  }
494
795
  function normalizeConfig(config) {
495
796
  return {
496
797
  roomStrategy: config.roomStrategy ?? "issue",
497
798
  writebackMode: config.writebackMode ?? "final_only",
498
- hostAgentHandle: normalizeOptionalHandle(config.hostAgentHandle)
799
+ hostAgentHandle: normalizeOptionalHandle(config.hostAgentHandle),
800
+ bandAppBaseUrl: config.bandAppBaseUrl ?? DEFAULT_APP_BASE_URL
499
801
  };
500
802
  }
501
803
  function normalizeOptionalHandle(handle) {
@@ -505,25 +807,82 @@ function normalizeOptionalHandle(handle) {
505
807
  const normalized = stripHandlePrefix(handle);
506
808
  return normalized.length > 0 ? normalized : null;
507
809
  }
810
+ async function trySetAgentAsDelegate(input) {
811
+ const issue = await input.linearClient.issue(input.issueId);
812
+ const existingDelegateId = issue.delegateId;
813
+ if (existingDelegateId) {
814
+ input.logger.info("linear_thenvoi_bridge.delegate_already_set", {
815
+ issueId: input.issueId,
816
+ existingDelegateId
817
+ });
818
+ return { set: false, delegateName: null };
819
+ }
820
+ await input.linearClient.updateIssue(input.issueId, {
821
+ delegateId: input.appUserId
822
+ });
823
+ let delegateName = null;
824
+ try {
825
+ const updated = await input.linearClient.issue(input.issueId);
826
+ const delegate = await updated.delegate;
827
+ delegateName = delegate?.displayName ?? delegate?.name ?? null;
828
+ } catch {
829
+ }
830
+ input.logger.info("linear_thenvoi_bridge.delegate_set", {
831
+ issueId: input.issueId,
832
+ delegateId: input.appUserId
833
+ });
834
+ return { set: true, delegateName };
835
+ }
836
+ async function tryMoveIssueToStarted(input) {
837
+ const response = await input.linearClient.workflowStates({
838
+ filter: {
839
+ team: { id: { eq: input.teamId } },
840
+ type: { eq: "started" }
841
+ }
842
+ });
843
+ const nodes = Array.isArray(response.nodes) ? response.nodes : [];
844
+ const startedStates = [...nodes].sort((a, b) => a.position - b.position);
845
+ const targetState = startedStates[0];
846
+ if (!targetState?.id) {
847
+ input.logger.info("linear_thenvoi_bridge.auto_start_no_started_state", {
848
+ issueId: input.issueId,
849
+ teamId: input.teamId
850
+ });
851
+ return { moved: false, stateId: null, stateName: null };
852
+ }
853
+ await input.linearClient.updateIssue(input.issueId, {
854
+ stateId: targetState.id
855
+ });
856
+ input.logger.info("linear_thenvoi_bridge.auto_start_moved", {
857
+ issueId: input.issueId,
858
+ stateId: targetState.id,
859
+ stateName: targetState.name ?? null
860
+ });
861
+ return {
862
+ moved: true,
863
+ stateId: targetState.id,
864
+ stateName: targetState.name ?? null
865
+ };
866
+ }
508
867
  async function resolveHostAgentHandle(input) {
509
868
  if (input.configuredHostHandle) {
510
- input.runtime.resolvedHostHandleCache.set(input.thenvoiRest, input.configuredHostHandle);
869
+ input.runtime.resolvedHostHandleCache.set(input.bandRest, input.configuredHostHandle);
511
870
  return input.configuredHostHandle;
512
871
  }
513
- const cachedAuthenticated = input.runtime.authenticatedHostHandleCache.get(input.thenvoiRest);
872
+ const cachedAuthenticated = input.runtime.authenticatedHostHandleCache.get(input.bandRest);
514
873
  if (cachedAuthenticated) {
515
- input.runtime.resolvedHostHandleCache.set(input.thenvoiRest, cachedAuthenticated);
874
+ input.runtime.resolvedHostHandleCache.set(input.bandRest, cachedAuthenticated);
516
875
  return cachedAuthenticated;
517
876
  }
518
- const cached = input.runtime.resolvedHostHandleCache.get(input.thenvoiRest);
877
+ const cached = input.runtime.resolvedHostHandleCache.get(input.bandRest);
519
878
  if (cached) {
520
879
  return cached;
521
880
  }
522
- const identity = await input.thenvoiRest.getAgentMe();
881
+ const identity = await input.bandRest.getAgentMe();
523
882
  const authenticatedAgentHandle = normalizeOptionalHandle(identity.handle);
524
883
  if (authenticatedAgentHandle) {
525
- input.runtime.authenticatedHostHandleCache.set(input.thenvoiRest, authenticatedAgentHandle);
526
- input.runtime.resolvedHostHandleCache.set(input.thenvoiRest, authenticatedAgentHandle);
884
+ input.runtime.authenticatedHostHandleCache.set(input.bandRest, authenticatedAgentHandle);
885
+ input.runtime.resolvedHostHandleCache.set(input.bandRest, authenticatedAgentHandle);
527
886
  return authenticatedAgentHandle;
528
887
  }
529
888
  throw new Error(
@@ -557,14 +916,14 @@ async function handleCanceledAction(input) {
557
916
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
558
917
  });
559
918
  if (!input.skipRoomWrite) {
560
- await input.deps.thenvoiRest.createChatEvent(existing.thenvoiRoomId, {
919
+ await input.deps.bandRest.createChatEvent(existing.bandRoomId, {
561
920
  content: "[Linear]: Agent session canceled. Stop in-room execution and await new instructions.",
562
921
  messageType: "task",
563
922
  metadata: {
564
923
  linear_event_action: "canceled",
565
924
  linear_session_id: input.sessionId,
566
925
  linear_issue_id: input.issueId,
567
- linear_bridge: "thenvoi"
926
+ linear_bridge: "band"
568
927
  }
569
928
  });
570
929
  }
@@ -590,15 +949,21 @@ async function handlePromptedAction(input) {
590
949
  });
591
950
  return;
592
951
  }
952
+ await sendRecoveryActivityIfStale({
953
+ session: existing,
954
+ linearClient: input.deps.linearClient,
955
+ store: input.deps.store,
956
+ logger: input.logger
957
+ });
593
958
  const message = `[Linear User Response]: ${userResponse}`;
594
959
  const metadata = {
595
960
  linear_event_action: "prompted",
596
961
  linear_session_id: input.sessionId,
597
- linear_bridge: "thenvoi"
962
+ linear_bridge: "band"
598
963
  };
599
- const canBootstrapDirectly = input.config.hostAgentHandle !== null ? true : Boolean(normalizeOptionalHandle((await input.deps.thenvoiRest.getAgentMe()).handle));
964
+ const canBootstrapDirectly = input.config.hostAgentHandle !== null ? true : Boolean(normalizeOptionalHandle((await input.deps.bandRest.getAgentMe()).handle));
600
965
  if (!input.skipRoomWrite || !canBootstrapDirectly) {
601
- await input.deps.thenvoiRest.createChatEvent(existing.thenvoiRoomId, {
966
+ await input.deps.bandRest.createChatEvent(existing.bandRoomId, {
602
967
  content: message,
603
968
  messageType: "text",
604
969
  metadata
@@ -609,7 +974,7 @@ async function handlePromptedAction(input) {
609
974
  await enqueueBootstrapRequest(input.deps.store, {
610
975
  eventKey: input.eventKey,
611
976
  linearSessionId: input.sessionId,
612
- thenvoiRoomId: existing.thenvoiRoomId,
977
+ bandRoomId: existing.bandRoomId,
613
978
  expectedContent: message,
614
979
  messageType: "text",
615
980
  senderId: linearActor.id,
@@ -625,7 +990,7 @@ async function handlePromptedAction(input) {
625
990
  });
626
991
  input.logger.info("linear_thenvoi_bridge.prompted_forwarded", {
627
992
  sessionId: input.sessionId,
628
- roomId: existing.thenvoiRoomId
993
+ roomId: existing.bandRoomId
629
994
  });
630
995
  }
631
996
  async function enqueueBootstrapRequest(store, request) {
@@ -667,7 +1032,7 @@ async function resolveRoomRecord(input) {
667
1032
  }
668
1033
  async function forwardBridgeMessage(input) {
669
1034
  try {
670
- await input.thenvoiRest.createChatEvent(input.roomRecord.thenvoiRoomId, {
1035
+ await input.bandRest.createChatEvent(input.roomRecord.bandRoomId, {
671
1036
  content: input.message,
672
1037
  messageType: input.messageType,
673
1038
  metadata: input.metadata
@@ -680,26 +1045,26 @@ async function forwardBridgeMessage(input) {
680
1045
  input.logger.warn("linear_thenvoi_bridge.room_forward_recovering_with_fresh_room", {
681
1046
  sessionId: input.sessionId,
682
1047
  issueId: input.issueId,
683
- previousRoomId: input.roomRecord.thenvoiRoomId,
1048
+ previousRoomId: input.roomRecord.bandRoomId,
684
1049
  error: error instanceof Error ? error.message : String(error)
685
1050
  });
686
1051
  const recoveredRoom = await createFreshRoomRecord({
687
- thenvoiRest: input.thenvoiRest,
1052
+ bandRest: input.bandRest,
688
1053
  store: input.store,
689
1054
  sessionId: input.sessionId,
690
1055
  issueId: input.issueId,
691
1056
  logger: input.logger
692
1057
  });
693
1058
  await ensureRoomParticipants({
694
- thenvoiRest: input.thenvoiRest,
695
- roomId: recoveredRoom.thenvoiRoomId,
1059
+ bandRest: input.bandRest,
1060
+ roomId: recoveredRoom.bandRoomId,
696
1061
  handles: [input.hostAgentHandle],
697
1062
  logger: input.logger
698
1063
  });
699
1064
  let attempt = 0;
700
1065
  while (true) {
701
1066
  try {
702
- await input.thenvoiRest.createChatEvent(recoveredRoom.thenvoiRoomId, {
1067
+ await input.bandRest.createChatEvent(recoveredRoom.bandRoomId, {
703
1068
  content: input.message,
704
1069
  messageType: input.messageType,
705
1070
  metadata: input.metadata
@@ -715,7 +1080,7 @@ async function forwardBridgeMessage(input) {
715
1080
  input.logger.warn("linear_thenvoi_bridge.room_recreated_retrying", {
716
1081
  sessionId: input.sessionId,
717
1082
  issueId: input.issueId,
718
- roomId: recoveredRoom.thenvoiRoomId,
1083
+ roomId: recoveredRoom.bandRoomId,
719
1084
  attempt,
720
1085
  delayMs,
721
1086
  error: recoveredError instanceof Error ? recoveredError.message : String(recoveredError)
@@ -745,11 +1110,11 @@ function isRetryableRateLimitError(error) {
745
1110
  }
746
1111
  async function createFreshRoomRecord(input) {
747
1112
  const now = (/* @__PURE__ */ new Date()).toISOString();
748
- const created = await input.thenvoiRest.createChat();
1113
+ const created = await input.bandRest.createChat();
749
1114
  const record = {
750
1115
  linearSessionId: input.sessionId,
751
1116
  linearIssueId: input.issueId,
752
- thenvoiRoomId: created.id,
1117
+ bandRoomId: created.id,
753
1118
  status: "active",
754
1119
  lastEventKey: null,
755
1120
  createdAt: now,
@@ -775,7 +1140,7 @@ async function resolveRoomRecordImpl(input) {
775
1140
  const linkedRecord = {
776
1141
  linearSessionId: input.sessionId,
777
1142
  linearIssueId: input.issueId,
778
- thenvoiRoomId: existingByIssue.thenvoiRoomId,
1143
+ bandRoomId: existingByIssue.bandRoomId,
779
1144
  status: "active",
780
1145
  lastEventKey: existingBySession?.lastEventKey ?? null,
781
1146
  createdAt: existingBySession?.createdAt ?? now2,
@@ -786,11 +1151,11 @@ async function resolveRoomRecordImpl(input) {
786
1151
  }
787
1152
  }
788
1153
  const now = (/* @__PURE__ */ new Date()).toISOString();
789
- const created = await input.thenvoiRest.createChat();
1154
+ const created = await input.bandRest.createChat();
790
1155
  const createdRecord = {
791
1156
  linearSessionId: input.sessionId,
792
1157
  linearIssueId: input.issueId,
793
- thenvoiRoomId: created.id,
1158
+ bandRoomId: created.id,
794
1159
  status: "active",
795
1160
  lastEventKey: null,
796
1161
  createdAt: now,
@@ -811,7 +1176,7 @@ async function ensureRoomParticipants(input) {
811
1176
  }
812
1177
  let existingParticipants;
813
1178
  try {
814
- existingParticipants = await input.thenvoiRest.listChatParticipants(input.roomId);
1179
+ existingParticipants = await input.bandRest.listChatParticipants(input.roomId);
815
1180
  } catch (error) {
816
1181
  if (isRetryableRateLimitError(error)) {
817
1182
  input.logger.warn("linear_thenvoi_bridge.participant_list_rate_limited", {
@@ -831,7 +1196,7 @@ async function ensureRoomParticipants(input) {
831
1196
  }
832
1197
  let peersByHandle;
833
1198
  try {
834
- peersByHandle = await lookupPeerIdsByHandle(input.thenvoiRest, input.roomId, missingHandles);
1199
+ peersByHandle = await lookupPeerIdsByHandle(input.bandRest, input.roomId, missingHandles);
835
1200
  } catch (error) {
836
1201
  if (error instanceof UnsupportedFeatureError) {
837
1202
  input.logger.warn("linear_thenvoi_bridge.peer_lookup_unavailable", {
@@ -853,7 +1218,7 @@ async function ensureRoomParticipants(input) {
853
1218
  continue;
854
1219
  }
855
1220
  try {
856
- await input.thenvoiRest.addChatParticipant(
1221
+ await input.bandRest.addChatParticipant(
857
1222
  input.roomId,
858
1223
  {
859
1224
  participantId,
@@ -872,14 +1237,14 @@ async function ensureRoomParticipants(input) {
872
1237
  }
873
1238
  }
874
1239
  }
875
- async function lookupPeerIdsByHandle(thenvoiRest, roomId, handles) {
876
- if (!thenvoiRest.listPeers) {
1240
+ async function lookupPeerIdsByHandle(bandRest, roomId, handles) {
1241
+ if (!bandRest.listPeers) {
877
1242
  throw new UnsupportedFeatureError("Peer listing is not available in current REST adapter");
878
1243
  }
879
1244
  const targets = new Set(handles);
880
1245
  const found = /* @__PURE__ */ new Map();
881
1246
  for (let page = 1; page <= MAX_PEER_LOOKUP_PAGES && found.size < targets.size; page += 1) {
882
- const response = await thenvoiRest.listPeers({
1247
+ const response = await bandRest.listPeers({
883
1248
  page,
884
1249
  pageSize: PEER_PAGE_SIZE,
885
1250
  notInChat: roomId
@@ -920,12 +1285,12 @@ async function selectRelevantPeerHandles(input) {
920
1285
  });
921
1286
  return configuredHandles;
922
1287
  }
923
- if (!input.thenvoiRest.listPeers) {
1288
+ if (!input.bandRest.listPeers) {
924
1289
  return [];
925
1290
  }
926
1291
  let peers;
927
1292
  try {
928
- const response = await input.thenvoiRest.listPeers({
1293
+ const response = await input.bandRest.listPeers({
929
1294
  page: 1,
930
1295
  pageSize: PEER_PAGE_SIZE,
931
1296
  notInChat: input.roomId
@@ -1056,6 +1421,7 @@ var SqliteSessionRoomStore = class {
1056
1421
  thenvoi_room_id,
1057
1422
  status,
1058
1423
  last_event_key,
1424
+ last_linear_activity_at,
1059
1425
  created_at,
1060
1426
  updated_at
1061
1427
  FROM linear_thenvoi_session_rooms
@@ -1076,6 +1442,7 @@ var SqliteSessionRoomStore = class {
1076
1442
  thenvoi_room_id,
1077
1443
  status,
1078
1444
  last_event_key,
1445
+ last_linear_activity_at,
1079
1446
  created_at,
1080
1447
  updated_at
1081
1448
  FROM linear_thenvoi_session_rooms
@@ -1097,23 +1464,26 @@ var SqliteSessionRoomStore = class {
1097
1464
  thenvoi_room_id,
1098
1465
  status,
1099
1466
  last_event_key,
1467
+ last_linear_activity_at,
1100
1468
  created_at,
1101
1469
  updated_at
1102
- ) VALUES (?, ?, ?, ?, ?, ?, ?)
1470
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
1103
1471
  ON CONFLICT(linear_session_id)
1104
1472
  DO UPDATE SET
1105
1473
  linear_issue_id = excluded.linear_issue_id,
1106
1474
  thenvoi_room_id = excluded.thenvoi_room_id,
1107
1475
  status = excluded.status,
1108
1476
  last_event_key = excluded.last_event_key,
1477
+ last_linear_activity_at = excluded.last_linear_activity_at,
1109
1478
  updated_at = excluded.updated_at
1110
1479
  `
1111
1480
  ).run(
1112
1481
  record.linearSessionId,
1113
1482
  record.linearIssueId,
1114
- record.thenvoiRoomId,
1483
+ record.bandRoomId,
1115
1484
  record.status,
1116
1485
  record.lastEventKey ?? null,
1486
+ record.lastLinearActivityAt ?? null,
1117
1487
  record.createdAt,
1118
1488
  record.updatedAt
1119
1489
  );
@@ -1157,7 +1527,7 @@ var SqliteSessionRoomStore = class {
1157
1527
  ).run(
1158
1528
  request.eventKey,
1159
1529
  request.linearSessionId,
1160
- request.thenvoiRoomId,
1530
+ request.bandRoomId,
1161
1531
  request.expectedContent,
1162
1532
  request.messageType,
1163
1533
  request.metadata ? JSON.stringify(request.metadata) : null,
@@ -1189,7 +1559,7 @@ var SqliteSessionRoomStore = class {
1189
1559
  return rows.map((row) => ({
1190
1560
  eventKey: row.event_key,
1191
1561
  linearSessionId: row.linear_session_id,
1192
- thenvoiRoomId: row.thenvoi_room_id,
1562
+ bandRoomId: row.thenvoi_room_id,
1193
1563
  expectedContent: row.expected_content,
1194
1564
  messageType: row.message_type,
1195
1565
  metadata: this.parseMetadata(row.metadata_json),
@@ -1207,6 +1577,29 @@ var SqliteSessionRoomStore = class {
1207
1577
  `
1208
1578
  ).run((/* @__PURE__ */ new Date()).toISOString(), eventKey);
1209
1579
  }
1580
+ async listActiveSessions() {
1581
+ const db = await this.getDb();
1582
+ const rawRows = db.prepare(
1583
+ `
1584
+ SELECT
1585
+ linear_session_id,
1586
+ linear_issue_id,
1587
+ thenvoi_room_id,
1588
+ status,
1589
+ last_event_key,
1590
+ last_linear_activity_at,
1591
+ created_at,
1592
+ updated_at
1593
+ FROM linear_thenvoi_session_rooms
1594
+ WHERE status IN ('active', 'waiting')
1595
+ ORDER BY updated_at DESC
1596
+ `
1597
+ ).all();
1598
+ if (!Array.isArray(rawRows)) {
1599
+ return [];
1600
+ }
1601
+ return rawRows.map((raw) => parseSessionRoomRow(raw)).filter((row) => row !== null).map((row) => this.toRecord(row));
1602
+ }
1210
1603
  async close() {
1211
1604
  if (!this.dbPromise) {
1212
1605
  return;
@@ -1288,15 +1681,26 @@ var SqliteSessionRoomStore = class {
1288
1681
  throw error;
1289
1682
  }
1290
1683
  }
1684
+ try {
1685
+ db.exec(`
1686
+ ALTER TABLE linear_thenvoi_session_rooms
1687
+ ADD COLUMN last_linear_activity_at TEXT
1688
+ `);
1689
+ } catch (error) {
1690
+ if (!isDuplicateColumnError(error)) {
1691
+ throw error;
1692
+ }
1693
+ }
1291
1694
  return db;
1292
1695
  }
1293
1696
  toRecord(row) {
1294
1697
  return {
1295
1698
  linearSessionId: row.linear_session_id,
1296
1699
  linearIssueId: row.linear_issue_id,
1297
- thenvoiRoomId: row.thenvoi_room_id,
1700
+ bandRoomId: row.thenvoi_room_id,
1298
1701
  status: row.status,
1299
1702
  lastEventKey: row.last_event_key,
1703
+ lastLinearActivityAt: row.last_linear_activity_at,
1300
1704
  createdAt: row.created_at,
1301
1705
  updatedAt: row.updated_at
1302
1706
  };
@@ -1349,20 +1753,22 @@ function parseSessionRoomRow(value) {
1349
1753
  }
1350
1754
  const linearSessionId = asString(row.linear_session_id);
1351
1755
  const linearIssueId = asNullableString(row.linear_issue_id);
1352
- const thenvoiRoomId = asString(row.thenvoi_room_id);
1756
+ const bandRoomId = asString(row.thenvoi_room_id);
1353
1757
  const status = asString(row.status);
1354
1758
  const lastEventKey = asNullableString(row.last_event_key);
1759
+ const lastLinearActivityAt = asNullableString(row.last_linear_activity_at);
1355
1760
  const createdAt = asString(row.created_at);
1356
1761
  const updatedAt = asString(row.updated_at);
1357
- if (!linearSessionId || linearIssueId === void 0 || !thenvoiRoomId || !status || !isSessionStatus(status) || lastEventKey === void 0 || !createdAt || !updatedAt) {
1762
+ if (!linearSessionId || linearIssueId === void 0 || !bandRoomId || !status || !isSessionStatus(status) || lastEventKey === void 0 || lastLinearActivityAt === void 0 || !createdAt || !updatedAt) {
1358
1763
  return null;
1359
1764
  }
1360
1765
  return {
1361
1766
  linear_session_id: linearSessionId,
1362
1767
  linear_issue_id: linearIssueId,
1363
- thenvoi_room_id: thenvoiRoomId,
1768
+ thenvoi_room_id: bandRoomId,
1364
1769
  status,
1365
1770
  last_event_key: lastEventKey,
1771
+ last_linear_activity_at: lastLinearActivityAt,
1366
1772
  created_at: createdAt,
1367
1773
  updated_at: updatedAt
1368
1774
  };
@@ -1380,20 +1786,20 @@ function parseBootstrapRequestRow(value) {
1380
1786
  }
1381
1787
  const eventKey = asString(row.event_key);
1382
1788
  const linearSessionId = asString(row.linear_session_id);
1383
- const thenvoiRoomId = asString(row.thenvoi_room_id);
1789
+ const bandRoomId = asString(row.thenvoi_room_id);
1384
1790
  const expectedContent = asString(row.expected_content);
1385
1791
  const messageType = asString(row.message_type);
1386
1792
  const metadataJson = asNullableString(row.metadata_json);
1387
1793
  const createdAt = asString(row.created_at);
1388
1794
  const expiresAt = asString(row.expires_at);
1389
1795
  const processedAt = asNullableString(row.processed_at);
1390
- if (!eventKey || !linearSessionId || !thenvoiRoomId || !expectedContent || !messageType || metadataJson === void 0 || !createdAt || !expiresAt || processedAt === void 0) {
1796
+ if (!eventKey || !linearSessionId || !bandRoomId || !expectedContent || !messageType || metadataJson === void 0 || !createdAt || !expiresAt || processedAt === void 0) {
1391
1797
  return null;
1392
1798
  }
1393
1799
  return {
1394
1800
  event_key: eventKey,
1395
1801
  linear_session_id: linearSessionId,
1396
- thenvoi_room_id: thenvoiRoomId,
1802
+ thenvoi_room_id: bandRoomId,
1397
1803
  expected_content: expectedContent,
1398
1804
  message_type: messageType,
1399
1805
  metadata_json: metadataJson,
@@ -1408,8 +1814,18 @@ function createSqliteSessionRoomStore(dbPath) {
1408
1814
 
1409
1815
  // src/integrations/linear/tools.ts
1410
1816
  import { z } from "zod";
1817
+ var LOCALHOST_HOSTNAMES = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]"]);
1818
+ function isAllowedAuthUrl(u) {
1819
+ try {
1820
+ const parsed = new URL(u);
1821
+ if (parsed.protocol === "https:") return true;
1822
+ return LOCALHOST_HOSTNAMES.has(parsed.hostname);
1823
+ } catch {
1824
+ return false;
1825
+ }
1826
+ }
1411
1827
  function createLinearTools(options) {
1412
- const { client, store, enableElicitation = true } = options;
1828
+ const { client, store, enableElicitation = true, logger } = options;
1413
1829
  const sessionBodySchema = z.object({
1414
1830
  session_id: z.string().describe("The Linear agent session ID"),
1415
1831
  body: z.string().describe("The message body in Markdown format")
@@ -1421,52 +1837,121 @@ function createLinearTools(options) {
1421
1837
  const requiredIssueIdSchema = issueIdInputSchema;
1422
1838
  const optionalIssueIdSchema = issueIdInputSchema;
1423
1839
  const tools = [];
1424
- const addSessionBodyTool = (name, description, handler) => {
1840
+ const ephemeralSessionBodySchema = sessionBodySchema.extend({
1841
+ ephemeral: z.boolean().optional().describe(
1842
+ '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...".'
1843
+ )
1844
+ });
1845
+ const addSessionBodyTool = (name, description, activityFn, options2) => {
1846
+ const supportsEphemeral = options2?.supportsEphemeral ?? false;
1425
1847
  tools.push({
1426
1848
  name,
1427
- description,
1428
- schema: sessionBodySchema,
1429
- handler
1849
+ description: supportsEphemeral ? description + " Set ephemeral: true for transient status updates that should disappear when the next activity arrives." : description,
1850
+ schema: supportsEphemeral ? ephemeralSessionBodySchema : sessionBodySchema,
1851
+ handler: async (args) => {
1852
+ await activityFn(
1853
+ client,
1854
+ args.session_id,
1855
+ args.body,
1856
+ supportsEphemeral && args.ephemeral === true ? { ephemeral: true } : void 0
1857
+ );
1858
+ return { ok: true };
1859
+ }
1430
1860
  });
1431
1861
  };
1432
1862
  addSessionBodyTool(
1433
1863
  "linear_post_thought",
1434
1864
  "Post a thought to the Linear agent session, visible to the user as internal reasoning.",
1435
- async (args) => {
1436
- await postThought(client, args.session_id, args.body);
1437
- return { ok: true };
1438
- }
1865
+ postThought,
1866
+ { supportsEphemeral: true }
1439
1867
  );
1440
1868
  addSessionBodyTool(
1441
1869
  "linear_post_action",
1442
1870
  "Post an action to the Linear agent session, showing the user what step is being taken.",
1443
- async (args) => {
1444
- await postAction(client, args.session_id, args.body);
1445
- return { ok: true };
1446
- }
1871
+ postAction,
1872
+ { supportsEphemeral: true }
1447
1873
  );
1448
1874
  addSessionBodyTool(
1449
1875
  "linear_post_error",
1450
1876
  "Post an error to the Linear agent session to notify the user of a failure.",
1451
- async (args) => {
1452
- await postError(client, args.session_id, args.body);
1453
- return { ok: true };
1454
- }
1877
+ postError
1455
1878
  );
1456
1879
  if (enableElicitation) {
1457
- addSessionBodyTool(
1458
- "linear_ask_user",
1459
- "Ask the Linear user a question via an elicitation activity.",
1460
- async (args) => {
1461
- await postElicitation(client, args.session_id, args.body);
1880
+ tools.push({
1881
+ name: "linear_ask_user",
1882
+ 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.",
1883
+ schema: z.object({
1884
+ session_id: z.string().describe("The Linear agent session ID"),
1885
+ body: z.string().max(ELICITATION_BODY_MAX_LENGTH).describe("The question to ask, in Markdown format"),
1886
+ options: z.array(z.object({
1887
+ label: z.string().max(SELECT_OPTION_MAX_LENGTH).describe("Display text for the option"),
1888
+ value: z.string().max(SELECT_OPTION_MAX_LENGTH).describe("Value returned when the option is selected")
1889
+ })).min(2).max(20).optional().describe("Clickable options for a select picker (2\u201320 items). Omit for free-text input.")
1890
+ }),
1891
+ handler: async (args) => {
1892
+ const sessionId = args.session_id;
1893
+ const body = args.body;
1894
+ const options2 = args.options;
1895
+ if (options2) {
1896
+ await postSelectElicitation(client, sessionId, body, options2);
1897
+ } else {
1898
+ await postElicitation(client, sessionId, body);
1899
+ }
1462
1900
  return { ok: true };
1463
1901
  }
1464
- );
1902
+ });
1903
+ tools.push({
1904
+ name: "linear_select",
1905
+ 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.",
1906
+ schema: z.object({
1907
+ session_id: z.string().describe("The Linear agent session ID"),
1908
+ body: z.string().describe("The question or prompt shown above the options (Markdown)"),
1909
+ options: z.array(
1910
+ z.object({
1911
+ label: z.string().describe("Display label for this option"),
1912
+ value: z.string().describe("Value returned when this option is selected")
1913
+ })
1914
+ ).min(1).max(25).describe("The selectable options")
1915
+ }),
1916
+ handler: async (args) => {
1917
+ await postSelectElicitation(
1918
+ client,
1919
+ args.session_id,
1920
+ args.body,
1921
+ args.options
1922
+ );
1923
+ return { ok: true };
1924
+ }
1925
+ });
1926
+ tools.push({
1927
+ name: "linear_request_auth",
1928
+ 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.",
1929
+ schema: z.object({
1930
+ session_id: z.string().describe("The Linear agent session ID"),
1931
+ body: z.string().max(ELICITATION_BODY_MAX_LENGTH).describe("Explanation of why authentication is needed, in Markdown format"),
1932
+ url: z.string().url().refine(
1933
+ isAllowedAuthUrl,
1934
+ { message: "URL must use https (http allowed only for localhost)" }
1935
+ ).describe("The authentication URL to open when the user clicks the link button"),
1936
+ provider: z.string().min(1).max(PROVIDER_MAX_LENGTH).optional().describe("Name of the external service (e.g. 'GitHub', 'Slack')")
1937
+ }),
1938
+ handler: async (args) => {
1939
+ await postAuthElicitation(
1940
+ client,
1941
+ args.session_id,
1942
+ args.body,
1943
+ args.url,
1944
+ args.provider
1945
+ );
1946
+ return { ok: true };
1947
+ }
1948
+ });
1465
1949
  }
1466
- addSessionBodyTool(
1467
- "linear_post_response",
1468
- "Post the final response to the Linear agent session and mark the session completed when a store is available.",
1469
- async (args) => {
1950
+ tools.push({
1951
+ name: "linear_post_response",
1952
+ description: "Post the final response to the Linear agent session and mark the session completed when a store is available.",
1953
+ schema: sessionBodySchema,
1954
+ handler: async (args) => {
1470
1955
  await completeLinearSession({
1471
1956
  linearClient: client,
1472
1957
  agentSessionId: args.session_id,
@@ -1475,11 +1960,11 @@ function createLinearTools(options) {
1475
1960
  });
1476
1961
  return { ok: true };
1477
1962
  }
1478
- );
1963
+ });
1479
1964
  tools.push(
1480
1965
  {
1481
1966
  name: "linear_update_plan",
1482
- description: "Update the plan for the Linear agent session, showing progress on each step.",
1967
+ 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.",
1483
1968
  schema: z.object({
1484
1969
  session_id: z.string().describe("The Linear agent session ID"),
1485
1970
  steps: z.array(z.object({
@@ -1504,6 +1989,35 @@ function createLinearTools(options) {
1504
1989
  optionalIssueIdSchema,
1505
1990
  issueCommentLimitSchema
1506
1991
  });
1992
+ if (typeof client.issueRepositorySuggestions === "function") {
1993
+ const suggestRepositories = client.issueRepositorySuggestions.bind(client);
1994
+ tools.push({
1995
+ name: "linear_suggest_repositories",
1996
+ 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.",
1997
+ schema: requiredIssueIdSchema.extend({
1998
+ session_id: z.string().describe("The Linear agent session ID"),
1999
+ repositories: z.array(
2000
+ z.object({
2001
+ hostname: z.string().describe("Hostname of the Git service (e.g. 'github.com')"),
2002
+ repositoryFullName: z.string().describe("Full name in owner/name format (e.g. 'acme/backend')")
2003
+ })
2004
+ ).min(1).max(50).describe("Candidate repositories the agent has access to")
2005
+ }),
2006
+ handler: async (args) => {
2007
+ const issueId = resolveIssueId("linear_suggest_repositories", args);
2008
+ const sessionId = args.session_id;
2009
+ const candidates = args.repositories;
2010
+ const response = await suggestRepositories(
2011
+ candidates,
2012
+ issueId,
2013
+ { agentSessionId: sessionId }
2014
+ );
2015
+ const suggestions = extractRepositorySuggestions(response);
2016
+ return { suggestions };
2017
+ }
2018
+ });
2019
+ }
2020
+ addSessionCreationTools({ tools, client, store, logger });
1507
2021
  return tools;
1508
2022
  }
1509
2023
  function addIssueTools(input) {
@@ -1557,6 +2071,7 @@ function addIssueTools(input) {
1557
2071
  priority: z.number().int().min(0).max(4).optional().describe("Priority 0-4"),
1558
2072
  state_id: z.string().optional().describe("Workflow state ID"),
1559
2073
  assignee_id: z.string().nullable().optional().describe("Assignee user ID, or null to unassign"),
2074
+ delegate_id: z.string().nullable().optional().describe("Agent user ID to delegate the issue to, or null to clear delegate"),
1560
2075
  estimate: z.number().int().optional().describe("Estimate points"),
1561
2076
  due_date: z.string().optional().describe("Due date ISO string")
1562
2077
  }),
@@ -1565,7 +2080,7 @@ function addIssueTools(input) {
1565
2080
  throw new Error("linear_update_issue is unavailable: Linear client does not support updateIssue().");
1566
2081
  }
1567
2082
  const issueId = resolveIssueId("linear_update_issue", args);
1568
- 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;
2083
+ 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;
1569
2084
  if (!hasUpdates) {
1570
2085
  throw new Error("linear_update_issue requires at least one update field.");
1571
2086
  }
@@ -1577,6 +2092,7 @@ function addIssueTools(input) {
1577
2092
  ...args.priority !== void 0 ? { priority: args.priority } : {},
1578
2093
  ...args.state_id !== void 0 ? { stateId: args.state_id } : {},
1579
2094
  ...args.assignee_id !== void 0 ? { assigneeId: args.assignee_id } : {},
2095
+ ...args.delegate_id !== void 0 ? { delegateId: args.delegate_id } : {},
1580
2096
  ...args.estimate !== void 0 ? { estimate: args.estimate } : {},
1581
2097
  ...args.due_date !== void 0 ? { dueDate: args.due_date } : {}
1582
2098
  }
@@ -1607,7 +2123,7 @@ function addIssueTools(input) {
1607
2123
  function assertUuid(toolName, value) {
1608
2124
  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;
1609
2125
  if (!uuidPattern.test(value)) {
1610
- throw new Error(`${toolName} requires the exact Linear issue UUID from the session context. Received "${value}".`);
2126
+ throw new Error(`${toolName} requires the exact Linear UUID from the session context (not the human-readable identifier). Received "${value}".`);
1611
2127
  }
1612
2128
  }
1613
2129
  function resolveIssueId(toolName, args) {
@@ -1650,6 +2166,11 @@ async function readIssue(client, issueId) {
1650
2166
  id: issue.assignee.id ?? null,
1651
2167
  name: issue.assignee.displayName ?? issue.assignee.name ?? null
1652
2168
  } : null,
2169
+ delegate: issue.delegate ? {
2170
+ id: issue.delegate.id ?? null,
2171
+ name: issue.delegate.displayName ?? issue.delegate.name ?? null
2172
+ } : null,
2173
+ delegate_id: issue.delegateId ?? null,
1653
2174
  team: issue.team ? {
1654
2175
  id: issue.team.id ?? null,
1655
2176
  key: issue.team.key ?? null,
@@ -1705,6 +2226,296 @@ async function readWorkflowStates(client, teamId) {
1705
2226
  })).sort((left, right) => (left.position ?? Number.MAX_SAFE_INTEGER) - (right.position ?? Number.MAX_SAFE_INTEGER));
1706
2227
  return { team_id: teamId, states };
1707
2228
  }
2229
+ function addSessionCreationTools(input) {
2230
+ const { tools, client, store, logger } = input;
2231
+ const sessionCreationBaseSchema = z.object({
2232
+ external_link: z.string().url().optional().describe("Optional URL of an external page associated with this session"),
2233
+ room_id: 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.")
2234
+ });
2235
+ const sessionCreationSchema = sessionCreationBaseSchema.extend({
2236
+ issue_id: z.string().uuid().describe("The Linear issue ID (UUID)")
2237
+ });
2238
+ async function persistSessionRoom(sessionId, issueId, roomId) {
2239
+ if (!store || typeof roomId !== "string") return null;
2240
+ try {
2241
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2242
+ await store.upsert({
2243
+ linearSessionId: sessionId,
2244
+ linearIssueId: issueId,
2245
+ bandRoomId: roomId,
2246
+ status: "active",
2247
+ createdAt: now,
2248
+ updatedAt: now
2249
+ });
2250
+ return null;
2251
+ } catch (err) {
2252
+ logger?.warn("linear_tools.session_room_persist_failed", {
2253
+ sessionId,
2254
+ issueId,
2255
+ roomId,
2256
+ error: err instanceof Error ? err.message : String(err)
2257
+ });
2258
+ return "session-room mapping not persisted";
2259
+ }
2260
+ }
2261
+ const createOnIssue = client.agentSessionCreateOnIssue?.bind(client);
2262
+ if (typeof createOnIssue === "function") {
2263
+ tools.push({
2264
+ name: "linear_create_session_on_issue",
2265
+ 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.",
2266
+ schema: sessionCreationSchema,
2267
+ handler: async (args) => {
2268
+ const issueId = args.issue_id;
2269
+ const result = await createOnIssue({
2270
+ issueId,
2271
+ ...typeof args.external_link === "string" ? { externalLink: args.external_link } : {}
2272
+ });
2273
+ const session = extractCreatedSession(result, logger);
2274
+ const warning = await persistSessionRoom(session.id, session.issueId, args.room_id);
2275
+ return { ok: true, session, ...warning ? { warning } : {} };
2276
+ }
2277
+ });
2278
+ }
2279
+ const createOnComment = client.agentSessionCreateOnComment?.bind(client);
2280
+ if (typeof createOnComment === "function") {
2281
+ tools.push({
2282
+ name: "linear_create_session_on_comment",
2283
+ 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.",
2284
+ schema: sessionCreationBaseSchema.extend({
2285
+ comment_id: z.string().uuid().describe("The Linear comment ID (UUID)")
2286
+ }),
2287
+ handler: async (args) => {
2288
+ const commentId = args.comment_id;
2289
+ const result = await createOnComment({
2290
+ commentId,
2291
+ ...typeof args.external_link === "string" ? { externalLink: args.external_link } : {}
2292
+ });
2293
+ const session = extractCreatedSession(result, logger);
2294
+ const warning = await persistSessionRoom(session.id, session.issueId, args.room_id);
2295
+ return { ok: true, session, ...warning ? { warning } : {} };
2296
+ }
2297
+ });
2298
+ }
2299
+ const createIssueFn = client.createIssue?.bind(client);
2300
+ if (typeof createIssueFn === "function") {
2301
+ tools.push({
2302
+ name: "linear_create_issue",
2303
+ 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.",
2304
+ schema: z.object({
2305
+ team_id: z.string().uuid().describe("The Linear team ID to create the issue in"),
2306
+ title: z.string().min(1).describe("Issue title"),
2307
+ description: z.string().optional().describe("Issue description in Markdown"),
2308
+ priority: z.number().int().min(0).max(4).optional().describe("Priority 0-4 (0=none, 1=urgent, 2=high, 3=normal, 4=low)"),
2309
+ assignee_id: z.string().uuid().optional().describe("Assignee user ID (UUID)"),
2310
+ state_id: z.string().uuid().optional().describe("Workflow state ID (UUID)"),
2311
+ label_ids: z.array(z.string().uuid()).optional().describe("Label IDs to attach (UUIDs)")
2312
+ }),
2313
+ handler: async (args) => {
2314
+ const result = await createIssueFn({
2315
+ teamId: args.team_id,
2316
+ title: args.title,
2317
+ ...typeof args.description === "string" ? { description: args.description } : {},
2318
+ ...typeof args.priority === "number" ? { priority: args.priority } : {},
2319
+ ...typeof args.assignee_id === "string" ? { assigneeId: args.assignee_id } : {},
2320
+ ...typeof args.state_id === "string" ? { stateId: args.state_id } : {},
2321
+ ...Array.isArray(args.label_ids) ? { labelIds: args.label_ids } : {}
2322
+ });
2323
+ const issue = extractCreatedIssue(result, logger);
2324
+ return { ok: true, issue };
2325
+ }
2326
+ });
2327
+ }
2328
+ }
2329
+ function extractCreatedSession(result, logger) {
2330
+ const payload = typeof result === "object" && result !== null ? result : {};
2331
+ const session = typeof payload.agentSession === "object" && payload.agentSession !== null ? payload.agentSession : payload;
2332
+ const id = typeof session.id === "string" ? session.id : null;
2333
+ if (!id) {
2334
+ logger?.error("linear_tools.orphan_session_created", {
2335
+ raw: safeStringify(result)
2336
+ });
2337
+ throw new Error("Linear API returned a session without an ID.");
2338
+ }
2339
+ return {
2340
+ id,
2341
+ issueId: typeof session.issueId === "string" ? session.issueId : null,
2342
+ status: typeof session.status === "string" ? session.status : null
2343
+ };
2344
+ }
2345
+ function extractCreatedIssue(result, logger) {
2346
+ const payload = typeof result === "object" && result !== null ? result : {};
2347
+ const issue = typeof payload.issue === "object" && payload.issue !== null ? payload.issue : payload;
2348
+ const id = typeof issue.id === "string" ? issue.id : null;
2349
+ if (!id) {
2350
+ logger?.error("linear_tools.orphan_issue_created", {
2351
+ raw: safeStringify(result)
2352
+ });
2353
+ throw new Error("Linear API returned an issue without an ID.");
2354
+ }
2355
+ return {
2356
+ id,
2357
+ identifier: typeof issue.identifier === "string" ? issue.identifier : null,
2358
+ url: typeof issue.url === "string" ? issue.url : null,
2359
+ title: typeof issue.title === "string" ? issue.title : null
2360
+ };
2361
+ }
2362
+ function safeStringify(value) {
2363
+ try {
2364
+ return JSON.stringify(value);
2365
+ } catch {
2366
+ return String(value);
2367
+ }
2368
+ }
2369
+ function extractRepositorySuggestions(response) {
2370
+ if (response == null || typeof response !== "object") {
2371
+ return [];
2372
+ }
2373
+ const payload = response;
2374
+ if (!Array.isArray(payload.suggestions)) {
2375
+ return [];
2376
+ }
2377
+ return payload.suggestions.filter(
2378
+ (entry) => entry != null && typeof entry === "object" && typeof entry.repositoryFullName === "string"
2379
+ ).map((entry) => ({
2380
+ repositoryFullName: entry.repositoryFullName,
2381
+ hostname: typeof entry.hostname === "string" ? entry.hostname : null,
2382
+ confidence: typeof entry.confidence === "number" ? entry.confidence : 0
2383
+ })).sort((a, b) => b.confidence - a.confidence);
2384
+ }
2385
+
2386
+ // src/integrations/linear/notification.ts
2387
+ var MAX_COMMENT_LENGTH = 4e3;
2388
+ var MAX_ACTOR_NAME_LENGTH = 100;
2389
+ function sanitizeInlineText(text) {
2390
+ return text.replace(/[\x00-\x1f\x7f]/g, " ").trim();
2391
+ }
2392
+ function sanitizeBodyText(text) {
2393
+ return text.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, "").trim();
2394
+ }
2395
+ async function handleAppUserNotification(input) {
2396
+ const { payload, deps, logger, appUserId } = input;
2397
+ const notification = payload.notification;
2398
+ const typename = notification.__typename;
2399
+ switch (typename) {
2400
+ case "IssueUnassignedFromYouNotificationWebhookPayload":
2401
+ await handleIssueUnassigned({
2402
+ notification,
2403
+ deps,
2404
+ logger
2405
+ });
2406
+ return;
2407
+ case "IssueNewCommentNotificationWebhookPayload":
2408
+ await handleIssueNewComment({
2409
+ notification,
2410
+ deps,
2411
+ logger,
2412
+ appUserId
2413
+ });
2414
+ return;
2415
+ case "IssueCommentReactionNotificationWebhookPayload":
2416
+ case "IssueEmojiReactionNotificationWebhookPayload":
2417
+ logReaction({
2418
+ notification,
2419
+ logger
2420
+ });
2421
+ return;
2422
+ default:
2423
+ logger.info("linear_thenvoi_bridge.notification_unhandled", {
2424
+ notificationType: typename
2425
+ });
2426
+ }
2427
+ }
2428
+ async function handleIssueUnassigned(input) {
2429
+ const { notification, deps, logger } = input;
2430
+ const existing = await deps.store.getByIssueId(notification.issueId);
2431
+ if (!existing || existing.status !== "active" && existing.status !== "waiting") {
2432
+ logger.info("linear_thenvoi_bridge.notification_unassigned_no_session", {
2433
+ issueId: notification.issueId,
2434
+ reason: existing ? `session_status_${existing.status}` : "no_active_session"
2435
+ });
2436
+ return;
2437
+ }
2438
+ await deps.bandRest.createChatEvent(existing.bandRoomId, {
2439
+ content: "[Linear]: Issue unassigned from agent. Disengage from active work and await reassignment.",
2440
+ messageType: "task",
2441
+ metadata: {
2442
+ linear_notification_type: "issueUnassignedFromYou",
2443
+ linear_issue_id: notification.issueId,
2444
+ linear_actor_id: notification.actorId ?? null,
2445
+ linear_bridge: "band"
2446
+ }
2447
+ });
2448
+ await deps.store.markCanceled(existing.linearSessionId);
2449
+ logger.info("linear_thenvoi_bridge.notification_unassigned_handled", {
2450
+ issueId: notification.issueId,
2451
+ sessionId: existing.linearSessionId,
2452
+ roomId: existing.bandRoomId
2453
+ });
2454
+ }
2455
+ async function handleIssueNewComment(input) {
2456
+ const { notification, deps, logger, appUserId } = input;
2457
+ if (appUserId && notification.actorId === appUserId) {
2458
+ logger.info("linear_thenvoi_bridge.notification_comment_self_skipped", {
2459
+ issueId: notification.issueId,
2460
+ commentId: notification.commentId,
2461
+ actorId: notification.actorId
2462
+ });
2463
+ return;
2464
+ }
2465
+ const existing = await deps.store.getByIssueId(notification.issueId);
2466
+ if (!existing || existing.status !== "active" && existing.status !== "waiting") {
2467
+ logger.info("linear_thenvoi_bridge.notification_comment_skipped", {
2468
+ issueId: notification.issueId,
2469
+ reason: existing ? `session_status_${existing.status}` : "no_active_session"
2470
+ });
2471
+ return;
2472
+ }
2473
+ let actorName = sanitizeInlineText(notification.actor?.name ?? "");
2474
+ if (actorName.length > MAX_ACTOR_NAME_LENGTH) {
2475
+ actorName = actorName.slice(0, MAX_ACTOR_NAME_LENGTH) + "\u2026";
2476
+ }
2477
+ if (actorName.length === 0) {
2478
+ actorName = "Unknown user";
2479
+ }
2480
+ let commentBody = sanitizeBodyText(notification.comment.body ?? "");
2481
+ if (commentBody.length > MAX_COMMENT_LENGTH) {
2482
+ commentBody = commentBody.slice(0, MAX_COMMENT_LENGTH) + "\u2026";
2483
+ }
2484
+ if (!notification.actor?.name) {
2485
+ logger.info("linear_thenvoi_bridge.notification_comment_actor_fallback", {
2486
+ issueId: notification.issueId,
2487
+ commentId: notification.commentId,
2488
+ actorId: notification.actorId ?? null
2489
+ });
2490
+ }
2491
+ await deps.bandRest.createChatEvent(existing.bandRoomId, {
2492
+ content: `[Linear Comment from ${actorName}]: ${commentBody}`,
2493
+ messageType: "text",
2494
+ metadata: {
2495
+ linear_notification_type: "issueNewComment",
2496
+ linear_issue_id: notification.issueId,
2497
+ linear_comment_id: notification.commentId,
2498
+ linear_actor_id: notification.actorId ?? null,
2499
+ linear_bridge: "band"
2500
+ }
2501
+ });
2502
+ logger.info("linear_thenvoi_bridge.notification_comment_forwarded", {
2503
+ issueId: notification.issueId,
2504
+ commentId: notification.commentId,
2505
+ sessionId: existing.linearSessionId,
2506
+ roomId: existing.bandRoomId
2507
+ });
2508
+ }
2509
+ function logReaction(input) {
2510
+ const { notification, logger } = input;
2511
+ logger.info("linear_thenvoi_bridge.notification_reaction", {
2512
+ notificationType: notification.__typename,
2513
+ issueId: notification.issueId,
2514
+ actorId: notification.actorId ?? null,
2515
+ commentId: "commentId" in notification ? notification.commentId : null,
2516
+ reactionEmoji: notification.reactionEmoji
2517
+ });
2518
+ }
1708
2519
 
1709
2520
  // src/integrations/linear/webhook.ts
1710
2521
  import {
@@ -1844,6 +2655,7 @@ function createLinearWebhookHandler(options) {
1844
2655
  const dispatcher = options.dispatcher ?? createInlineLinearBridgeDispatcher({ logger });
1845
2656
  const webhookClient = new LinearWebhookClient(options.config.linearWebhookSecret);
1846
2657
  const inFlightEventKeys = /* @__PURE__ */ new Set();
2658
+ const processedNotificationIds = /* @__PURE__ */ new Set();
1847
2659
  return async (request, response) => {
1848
2660
  if (request.method !== "POST") {
1849
2661
  logger.warn("linear_thenvoi_bridge.webhook_invalid_method", {
@@ -1865,9 +2677,9 @@ function createLinearWebhookHandler(options) {
1865
2677
  return;
1866
2678
  }
1867
2679
  const rawBody = await readRawBody(request);
1868
- let payload;
2680
+ let parsed;
1869
2681
  try {
1870
- payload = webhookClient.parseData(
2682
+ parsed = webhookClient.parseData(
1871
2683
  rawBody,
1872
2684
  signature,
1873
2685
  timestamp
@@ -1879,13 +2691,111 @@ function createLinearWebhookHandler(options) {
1879
2691
  sendText(response, 400, "Invalid webhook");
1880
2692
  return;
1881
2693
  }
1882
- if (payload.type !== "AgentSessionEvent") {
2694
+ if (parsed.type === "AppUserNotification") {
2695
+ const notificationPayload = parsed;
2696
+ if (!notificationPayload.notification) {
2697
+ logger.warn("linear_thenvoi_bridge.webhook_notification_missing_payload", {
2698
+ appUserId: notificationPayload.appUserId ?? null,
2699
+ organizationId: notificationPayload.organizationId ?? null
2700
+ });
2701
+ sendText(response, 200, "OK");
2702
+ return;
2703
+ }
2704
+ const notificationId = notificationPayload.notification?.id;
2705
+ const notificationType = notificationPayload.notification.__typename;
2706
+ const isDedupable = notificationType !== "IssueCommentReactionNotificationWebhookPayload" && notificationType !== "IssueEmojiReactionNotificationWebhookPayload";
2707
+ if (isDedupable && notificationId && processedNotificationIds.has(notificationId)) {
2708
+ logger.info("linear_thenvoi_bridge.webhook_notification_duplicate_ignored", {
2709
+ notificationId
2710
+ });
2711
+ sendText(response, 200, "OK");
2712
+ return;
2713
+ }
2714
+ logger.info("linear_thenvoi_bridge.webhook_notification_received", {
2715
+ notificationType: notificationType ?? null,
2716
+ notificationId: notificationId ?? null,
2717
+ appUserId: notificationPayload.appUserId,
2718
+ organizationId: notificationPayload.organizationId
2719
+ });
2720
+ try {
2721
+ await handleAppUserNotification({
2722
+ payload: notificationPayload,
2723
+ deps: options.deps,
2724
+ logger,
2725
+ appUserId: notificationPayload.appUserId
2726
+ });
2727
+ if (isDedupable && notificationId) {
2728
+ processedNotificationIds.add(notificationId);
2729
+ if (processedNotificationIds.size > 1e3) {
2730
+ const oldest = processedNotificationIds.values().next().value;
2731
+ if (oldest) processedNotificationIds.delete(oldest);
2732
+ }
2733
+ }
2734
+ } catch (error) {
2735
+ logger.error("linear_thenvoi_bridge.webhook_notification_failed", {
2736
+ notificationType: notificationType ?? null,
2737
+ error: serializeError(error)
2738
+ });
2739
+ }
2740
+ sendText(response, 200, "OK");
2741
+ return;
2742
+ }
2743
+ if (parsed.type === "PermissionChange") {
2744
+ const action = parsed.action;
2745
+ if (normalizeAction2(action) === "teamaccesschanged") {
2746
+ const permPayload = parsed;
2747
+ logger.info("linear_thenvoi_bridge.team_access_changed", {
2748
+ action: permPayload.action,
2749
+ addedTeamIds: permPayload.addedTeamIds,
2750
+ removedTeamIds: permPayload.removedTeamIds,
2751
+ canAccessAllPublicTeams: permPayload.canAccessAllPublicTeams,
2752
+ organizationId: permPayload.organizationId
2753
+ });
2754
+ try {
2755
+ await options.permissionCallbacks?.onTeamAccessChanged?.(permPayload);
2756
+ } catch (error) {
2757
+ logger.error("linear_thenvoi_bridge.team_access_changed_callback_failed", {
2758
+ error: serializeError(error)
2759
+ });
2760
+ }
2761
+ } else {
2762
+ logger.info("linear_thenvoi_bridge.permission_change_event_ignored", {
2763
+ action
2764
+ });
2765
+ }
2766
+ sendText(response, 200, "OK");
2767
+ return;
2768
+ }
2769
+ if (parsed.type === "OAuthApp") {
2770
+ const oauthPayload = parsed;
2771
+ if (normalizeAction2(oauthPayload.action) === "revoked") {
2772
+ logger.warn("linear_thenvoi_bridge.oauth_app_revoked", {
2773
+ oauthClientId: oauthPayload.oauthClientId,
2774
+ organizationId: oauthPayload.organizationId
2775
+ });
2776
+ try {
2777
+ await options.permissionCallbacks?.onOAuthAppRevoked?.(oauthPayload);
2778
+ } catch (error) {
2779
+ logger.error("linear_thenvoi_bridge.oauth_app_revoked_callback_failed", {
2780
+ error: serializeError(error)
2781
+ });
2782
+ }
2783
+ } else {
2784
+ logger.info("linear_thenvoi_bridge.oauth_app_event_ignored", {
2785
+ action: oauthPayload.action
2786
+ });
2787
+ }
2788
+ sendText(response, 200, "OK");
2789
+ return;
2790
+ }
2791
+ if (parsed.type !== "AgentSessionEvent") {
1883
2792
  logger.info("linear_thenvoi_bridge.webhook_ignored_event", {
1884
- type: payload.type
2793
+ type: parsed.type
1885
2794
  });
1886
2795
  sendText(response, 200, "OK");
1887
2796
  return;
1888
2797
  }
2798
+ const payload = parsed;
1889
2799
  const eventKey = getAgentSessionEventKey(payload);
1890
2800
  logger.info("linear_thenvoi_bridge.webhook_received", {
1891
2801
  sessionId: payload.agentSession.id,
@@ -2117,6 +3027,12 @@ var DEFAULT_STATUS_MAPPING = {
2117
3027
  };
2118
3028
  export {
2119
3029
  DEFAULT_STATUS_MAPPING,
3030
+ ELICITATION_BODY_MAX_LENGTH,
3031
+ PROVIDER_MAX_LENGTH,
3032
+ SELECT_OPTION_MAX_LENGTH,
3033
+ STALE_SESSION_CHECK_INTERVAL_MS,
3034
+ STALE_SESSION_THRESHOLD_MS,
3035
+ StaleSessionGuard,
2120
3036
  buildLinearAuthorizationHeader,
2121
3037
  completeLinearSession,
2122
3038
  createInProcessLinearBridgeDispatcher,
@@ -2128,12 +3044,17 @@ export {
2128
3044
  createSqliteSessionRoomStore,
2129
3045
  dedupeHandles,
2130
3046
  handleAgentSessionEvent,
3047
+ handleAppUserNotification,
2131
3048
  isLinearApiKey,
3049
+ isSessionStale,
2132
3050
  postAction,
3051
+ postAuthElicitation,
2133
3052
  postElicitation,
2134
3053
  postError,
2135
3054
  postResponse,
3055
+ postSelectElicitation,
2136
3056
  postThought,
3057
+ sendRecoveryActivityIfStale,
2137
3058
  stripHandlePrefix,
2138
3059
  updatePlan
2139
3060
  };