@band-ai/sdk 0.1.4 → 0.1.10

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 (73) hide show
  1. package/README.md +372 -0
  2. package/dist/{ClaudeSDKAdapter-CRq4R-WB.d.cts → ClaudeSDKAdapter-DfjaI5ke.d.cts} +9 -7
  3. package/dist/{ClaudeSDKAdapter-BHOkjN3X.d.ts → ClaudeSDKAdapter-DonPGJ43.d.ts} +9 -7
  4. package/dist/{acp-server-BdRz9_UN.d.cts → acp-server-CUBXIiG6.d.cts} +2 -2
  5. package/dist/{acp-server-B1Da7L6u.d.ts → acp-server-NZAeg-WO.d.ts} +2 -2
  6. package/dist/adapters.cjs +494 -106
  7. package/dist/adapters.d.cts +12 -12
  8. package/dist/adapters.d.ts +12 -12
  9. package/dist/adapters.js +7 -7
  10. package/dist/{backends-SVMT-7l4.d.cts → backends-DYtEhCZG.d.cts} +7 -2
  11. package/dist/{backends-CzDbiQar.d.ts → backends-oYAb7EU-.d.ts} +7 -2
  12. package/dist/{chunk-MSL6CWHY.js → chunk-2BHHR6YW.js} +17 -16
  13. package/dist/{chunk-EDPKUUJL.js → chunk-6RXWL6TC.js} +9 -1
  14. package/dist/{chunk-SFVKOEQH.js → chunk-DNPVCR3S.js} +92 -14
  15. package/dist/{chunk-I5EZG52P.js → chunk-FP237EAG.js} +424 -136
  16. package/dist/{chunk-RHVH2GZR.js → chunk-J3M3PFFY.js} +81 -31
  17. package/dist/{chunk-GVEQ3RAH.js → chunk-MLROFDKR.js} +132 -23
  18. package/dist/{chunk-INA6YZTZ.js → chunk-RDM4TE4B.js} +32 -30
  19. package/dist/chunk-US226AS4.js +130 -0
  20. package/dist/{chunk-W5NDZSYE.js → chunk-XA4N2JPH.js} +1 -1
  21. package/dist/converters.cjs +469 -13
  22. package/dist/converters.d.cts +5 -5
  23. package/dist/converters.d.ts +5 -5
  24. package/dist/converters.js +3 -2
  25. package/dist/core.cjs +45 -2
  26. package/dist/core.d.cts +4 -3
  27. package/dist/core.d.ts +4 -3
  28. package/dist/core.js +5 -1
  29. package/dist/disconnectReason-IuADl6TH.d.cts +34 -0
  30. package/dist/disconnectReason-IuADl6TH.d.ts +34 -0
  31. package/dist/{dtos-DUpbIu8k.d.cts → dtos-Dd_fTfLn.d.cts} +26 -6
  32. package/dist/{dtos-DUpbIu8k.d.ts → dtos-Dd_fTfLn.d.ts} +26 -6
  33. package/dist/index.cjs +1089 -269
  34. package/dist/index.d.cts +16 -15
  35. package/dist/index.d.ts +16 -15
  36. package/dist/index.js +14 -8
  37. package/dist/linear.cjs +985 -53
  38. package/dist/linear.d.cts +137 -7
  39. package/dist/linear.d.ts +137 -7
  40. package/dist/linear.js +974 -53
  41. package/dist/mcp-claude.cjs +303 -32
  42. package/dist/mcp-claude.d.cts +3 -3
  43. package/dist/mcp-claude.d.ts +3 -3
  44. package/dist/mcp-claude.js +210 -12
  45. package/dist/mcp.cjs +313 -45
  46. package/dist/mcp.d.cts +5 -5
  47. package/dist/mcp.d.ts +5 -5
  48. package/dist/mcp.js +3 -3
  49. package/dist/{opencode-BBcDchcN.d.cts → opencode--iA5-KYk.d.cts} +2 -2
  50. package/dist/{opencode-DthQQUsZ.d.ts → opencode-YNacv-eJ.d.ts} +2 -2
  51. package/dist/{pagination-CcDkVFxK.d.ts → pagination-BBLsYLhT.d.ts} +2 -2
  52. package/dist/{pagination-BPiys10t.d.cts → pagination-CqidD-L_.d.cts} +2 -2
  53. package/dist/{protocols-DrhzOH6K.d.ts → protocols-BF6jamIl.d.ts} +1 -1
  54. package/dist/{protocols-CInhtFpA.d.cts → protocols-D2bTrQ_t.d.cts} +1 -1
  55. package/dist/rest.d.cts +5 -5
  56. package/dist/rest.d.ts +5 -5
  57. package/dist/runtime.cjs +721 -172
  58. package/dist/runtime.d.cts +15 -10
  59. package/dist/runtime.d.ts +15 -10
  60. package/dist/runtime.js +11 -8
  61. package/dist/{schemas-vos1AaBc.d.ts → schemas-B1Fd3oUM.d.ts} +17 -8
  62. package/dist/{schemas-BpEBkq5V.d.cts → schemas-CI0qgcp_.d.cts} +17 -8
  63. package/dist/{sdk-CcnQ5OUC.d.ts → sdk-CyqN1W5g.d.ts} +32 -2
  64. package/dist/{sdk-CwTAxhwI.d.cts → sdk-D2rTDU_o.d.cts} +32 -2
  65. package/dist/{simpleAdapter-DqhvjsGr.d.cts → simpleAdapter-C_f-01Cu.d.cts} +1 -1
  66. package/dist/{simpleAdapter-9OE_p6QW.d.ts → simpleAdapter-qQt-tSz4.d.ts} +1 -1
  67. package/dist/testing.d.cts +3 -3
  68. package/dist/testing.d.ts +3 -3
  69. package/dist/{types-CovcHpYf.d.cts → types-CKqrhGB1.d.cts} +2 -2
  70. package/dist/{types-CxII3XkA.d.ts → types-D3AsUnd0.d.ts} +1 -1
  71. package/dist/{types-7jVzOeOP.d.ts → types-DNySTI_8.d.ts} +2 -2
  72. package/dist/{types-CSHFipIc.d.cts → types-DtMms5Fi.d.cts} +1 -1
  73. package/package.json +22 -21
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
  });
@@ -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}
@@ -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_THENVOI_APP_BASE_URL = "https://app.thenvoi.com";
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,7 +589,49 @@ 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
637
  thenvoiRest: input.deps.thenvoiRest,
@@ -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.thenvoiRoomId;
654
+ externalUrlPromise = trySetSessionExternalUrl({
655
+ linearClient: input.deps.linearClient,
656
+ sessionId,
657
+ roomId,
658
+ appBaseUrl: config.thenvoiAppBaseUrl,
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,
@@ -419,6 +687,22 @@ async function handleAgentSessionEvent(input, options) {
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,
@@ -497,12 +783,15 @@ 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,
@@ -510,6 +799,9 @@ async function handleAgentSessionEvent(input, options) {
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 Thenvoi", 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
+ thenvoiAppBaseUrl: config.thenvoiAppBaseUrl ?? DEFAULT_THENVOI_APP_BASE_URL
570
883
  };
571
884
  }
572
885
  function normalizeOptionalHandle(handle) {
@@ -576,6 +889,63 @@ 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
951
  input.runtime.resolvedHostHandleCache.set(input.thenvoiRest, input.configuredHostHandle);
@@ -661,6 +1031,12 @@ 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",
@@ -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,15 +1546,17 @@ 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(
@@ -1185,6 +1565,7 @@ var SqliteSessionRoomStore = class {
1185
1565
  record.thenvoiRoomId,
1186
1566
  record.status,
1187
1567
  record.lastEventKey ?? null,
1568
+ record.lastLinearActivityAt ?? null,
1188
1569
  record.createdAt,
1189
1570
  record.updatedAt
1190
1571
  );
@@ -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,6 +1763,16 @@ 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) {
@@ -1368,6 +1782,7 @@ var SqliteSessionRoomStore = class {
1368
1782
  thenvoiRoomId: 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
  };
@@ -1423,9 +1838,10 @@ function parseSessionRoomRow(value) {
1423
1838
  const thenvoiRoomId = 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 || !thenvoiRoomId || !status || !isSessionStatus(status) || lastEventKey === void 0 || lastLinearActivityAt === void 0 || !createdAt || !updatedAt) {
1429
1845
  return null;
1430
1846
  }
1431
1847
  return {
@@ -1434,6 +1850,7 @@ function parseSessionRoomRow(value) {
1434
1850
  thenvoi_room_id: thenvoiRoomId,
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
  };
@@ -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 Thenvoi room ID to persist the session-room mapping. Pass this when creating a session from within a Thenvoi 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
+ thenvoiRoomId: 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 Thenvoi 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.thenvoiRest.createChatEvent(existing.thenvoiRoomId, {
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: "thenvoi"
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.thenvoiRoomId
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.thenvoiRest.createChatEvent(existing.thenvoiRoomId, {
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: "thenvoi"
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.thenvoiRoomId
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
  });