@band-ai/sdk 0.1.2
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.
- package/dist/CodexAdapter-A1k_LMdZ.d.ts +1278 -0
- package/dist/CodexAdapter-BbZD-nBd.d.cts +1278 -0
- package/dist/adapters.cjs +10643 -0
- package/dist/adapters.d.cts +217 -0
- package/dist/adapters.d.ts +217 -0
- package/dist/adapters.js +92 -0
- package/dist/backends-BnOLsG7w.d.cts +20 -0
- package/dist/backends-C4n6cKOf.d.ts +20 -0
- package/dist/chunk-3BKAC4OZ.js +44 -0
- package/dist/chunk-5CXIMAH2.js +52 -0
- package/dist/chunk-6AJA2OPC.js +3682 -0
- package/dist/chunk-A3TDK6GP.js +6674 -0
- package/dist/chunk-AVVDPXP4.js +39 -0
- package/dist/chunk-EDPKUUJL.js +177 -0
- package/dist/chunk-FUODYQRE.js +2562 -0
- package/dist/chunk-GVEQ3RAH.js +373 -0
- package/dist/chunk-INA6YZTZ.js +88 -0
- package/dist/chunk-IZPV6QS6.js +115 -0
- package/dist/chunk-LY55KYVI.js +0 -0
- package/dist/chunk-NIAQBANR.js +577 -0
- package/dist/chunk-OD2G5LFQ.js +78 -0
- package/dist/chunk-SFVKOEQH.js +103 -0
- package/dist/chunk-SG4WLD2H.js +1093 -0
- package/dist/chunk-YR3BOQNW.js +39 -0
- package/dist/chunk-ZZJRRBPQ.js +82 -0
- package/dist/claude-CcnQ5OUC.d.ts +53 -0
- package/dist/claude-CwTAxhwI.d.cts +53 -0
- package/dist/config.cjs +164 -0
- package/dist/config.d.cts +19 -0
- package/dist/config.d.ts +19 -0
- package/dist/config.js +9 -0
- package/dist/converters.cjs +1086 -0
- package/dist/converters.d.cts +143 -0
- package/dist/converters.d.ts +143 -0
- package/dist/converters.js +468 -0
- package/dist/core.cjs +185 -0
- package/dist/core.d.cts +22 -0
- package/dist/core.d.ts +22 -0
- package/dist/core.js +24 -0
- package/dist/customTools-BzF0IISO.d.cts +10 -0
- package/dist/customTools-BzF0IISO.d.ts +10 -0
- package/dist/dtos-DUpbIu8k.d.cts +163 -0
- package/dist/dtos-DUpbIu8k.d.ts +163 -0
- package/dist/history-ByessXNq.d.ts +24 -0
- package/dist/history-i6yN7neC.d.cts +24 -0
- package/dist/index.cjs +13914 -0
- package/dist/index.d.cts +63 -0
- package/dist/index.d.ts +63 -0
- package/dist/index.js +198 -0
- package/dist/linear.cjs +2207 -0
- package/dist/linear.d.cts +167 -0
- package/dist/linear.d.ts +167 -0
- package/dist/linear.js +2139 -0
- package/dist/logger-CABQOnlR.d.cts +21 -0
- package/dist/logger-CABQOnlR.d.ts +21 -0
- package/dist/mcp-claude.cjs +584 -0
- package/dist/mcp-claude.d.cts +4 -0
- package/dist/mcp-claude.d.ts +4 -0
- package/dist/mcp-claude.js +11 -0
- package/dist/mcp.cjs +1247 -0
- package/dist/mcp.d.cts +97 -0
- package/dist/mcp.d.ts +97 -0
- package/dist/mcp.js +27 -0
- package/dist/opencode-BBcDchcN.d.cts +159 -0
- package/dist/opencode-DthQQUsZ.d.ts +159 -0
- package/dist/pagination-BPiys10t.d.cts +25 -0
- package/dist/pagination-CcDkVFxK.d.ts +25 -0
- package/dist/protocols-CInhtFpA.d.cts +116 -0
- package/dist/protocols-DrhzOH6K.d.ts +116 -0
- package/dist/rest.cjs +1159 -0
- package/dist/rest.d.cts +144 -0
- package/dist/rest.d.ts +144 -0
- package/dist/rest.js +18 -0
- package/dist/runtime.cjs +5353 -0
- package/dist/runtime.d.cts +276 -0
- package/dist/runtime.d.ts +276 -0
- package/dist/runtime.js +274 -0
- package/dist/schemas-BpEBkq5V.d.cts +592 -0
- package/dist/schemas-vos1AaBc.d.ts +592 -0
- package/dist/sdk-HJUVSSWA.js +10 -0
- package/dist/simpleAdapter-9OE_p6QW.d.ts +29 -0
- package/dist/simpleAdapter-DqhvjsGr.d.cts +29 -0
- package/dist/testing.cjs +217 -0
- package/dist/testing.d.cts +126 -0
- package/dist/testing.d.ts +126 -0
- package/dist/testing.js +186 -0
- package/dist/types-7jVzOeOP.d.ts +446 -0
- package/dist/types-CSHFipIc.d.cts +331 -0
- package/dist/types-CovcHpYf.d.cts +446 -0
- package/dist/types-CxII3XkA.d.ts +331 -0
- package/package.json +189 -0
package/dist/linear.js
ADDED
|
@@ -0,0 +1,2139 @@
|
|
|
1
|
+
import {
|
|
2
|
+
NoopLogger
|
|
3
|
+
} from "./chunk-OD2G5LFQ.js";
|
|
4
|
+
import {
|
|
5
|
+
UnsupportedFeatureError
|
|
6
|
+
} from "./chunk-YR3BOQNW.js";
|
|
7
|
+
|
|
8
|
+
// src/integrations/linear/bridge/handler.ts
|
|
9
|
+
import { LinearDocument as L2 } from "@linear/sdk";
|
|
10
|
+
|
|
11
|
+
// src/integrations/linear/handles.ts
|
|
12
|
+
function stripHandlePrefix(handle) {
|
|
13
|
+
return handle.trim().replace(/^@+/, "").toLowerCase();
|
|
14
|
+
}
|
|
15
|
+
function dedupeHandles(handles) {
|
|
16
|
+
const deduped = [];
|
|
17
|
+
const seen = /* @__PURE__ */ new Set();
|
|
18
|
+
for (const handle of handles) {
|
|
19
|
+
const normalized = stripHandlePrefix(handle);
|
|
20
|
+
if (!normalized || seen.has(normalized)) {
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
seen.add(normalized);
|
|
24
|
+
deduped.push(normalized);
|
|
25
|
+
}
|
|
26
|
+
return deduped;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// src/integrations/linear/activities.ts
|
|
30
|
+
import { LinearDocument as L } from "@linear/sdk";
|
|
31
|
+
async function postActivity(client, sessionId, content) {
|
|
32
|
+
await client.createAgentActivity({
|
|
33
|
+
agentSessionId: sessionId,
|
|
34
|
+
content
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
async function postBodyActivity(client, sessionId, type, body) {
|
|
38
|
+
await postActivity(client, sessionId, { type, body });
|
|
39
|
+
}
|
|
40
|
+
async function postThought(client, sessionId, body) {
|
|
41
|
+
await postBodyActivity(client, sessionId, L.AgentActivityType.Thought, body);
|
|
42
|
+
}
|
|
43
|
+
async function postError(client, sessionId, body) {
|
|
44
|
+
await postBodyActivity(client, sessionId, L.AgentActivityType.Error, body);
|
|
45
|
+
}
|
|
46
|
+
async function postResponse(client, sessionId, body) {
|
|
47
|
+
await postBodyActivity(client, sessionId, L.AgentActivityType.Response, body);
|
|
48
|
+
}
|
|
49
|
+
async function postElicitation(client, sessionId, body) {
|
|
50
|
+
await postBodyActivity(client, sessionId, L.AgentActivityType.Elicitation, body);
|
|
51
|
+
}
|
|
52
|
+
async function postAction(client, sessionId, body) {
|
|
53
|
+
await postActivity(client, sessionId, {
|
|
54
|
+
type: L.AgentActivityType.Action,
|
|
55
|
+
action: body,
|
|
56
|
+
parameter: ""
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
async function updatePlan(client, sessionId, steps) {
|
|
60
|
+
const planSummary = steps.map((step) => {
|
|
61
|
+
const icon = step.status === "completed" ? "\u2705" : step.status === "in_progress" ? "\u23F3" : step.status === "failed" ? "\u274C" : "\u2B1C";
|
|
62
|
+
return `${icon} ${step.title}`;
|
|
63
|
+
}).join("\n");
|
|
64
|
+
await postActivity(client, sessionId, {
|
|
65
|
+
type: L.AgentActivityType.Thought,
|
|
66
|
+
body: `**Plan:**
|
|
67
|
+
${planSummary}`
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// src/integrations/linear/bridge/message.ts
|
|
72
|
+
function buildBridgeMessage(input) {
|
|
73
|
+
const header = input.action === "created" ? "[Linear]: Agent session created." : "[Linear]: Agent session updated.";
|
|
74
|
+
const userRequest = firstNonEmpty(
|
|
75
|
+
input.commentBody,
|
|
76
|
+
input.promptContext,
|
|
77
|
+
input.issueTitle ? `Please handle ${input.issueTitle}.` : null
|
|
78
|
+
) ?? "Please handle this Linear request.";
|
|
79
|
+
const issueIdLine = input.issueId ? `issue_id: ${input.issueId}` : "issue_id: none";
|
|
80
|
+
const issueIdentifierLine = input.issueIdentifier ? `issue_identifier: ${input.issueIdentifier}` : "issue_identifier: none";
|
|
81
|
+
const issueTitleLine = input.issueTitle ? `issue_title: ${input.issueTitle}` : "issue_title: none";
|
|
82
|
+
const issueUrlLine = input.issueUrl ? `issue_url: ${input.issueUrl}` : "issue_url: none";
|
|
83
|
+
const issueTeamLine = firstNonEmpty(input.issueTeamKey, input.issueTeamName) ? `issue_team: ${firstNonEmpty(input.issueTeamKey, input.issueTeamName)}` : "issue_team: none";
|
|
84
|
+
const issueTeamIdLine = input.issueTeamId ? `issue_team_id: ${input.issueTeamId}` : "issue_team_id: none";
|
|
85
|
+
const issueStateLine = firstNonEmpty(input.issueStateName, input.issueStateType) ? `issue_state: ${firstNonEmpty(input.issueStateName, input.issueStateType)}` : "issue_state: none";
|
|
86
|
+
const issueStateIdLine = input.issueStateId ? `issue_state_id: ${input.issueStateId}` : "issue_state_id: none";
|
|
87
|
+
const issueStateTypeLine = input.issueStateType ? `issue_state_type: ${input.issueStateType}` : "issue_state_type: none";
|
|
88
|
+
const issueAssigneeLine = input.issueAssigneeName ? `issue_assignee: ${input.issueAssigneeName}` : "issue_assignee: none";
|
|
89
|
+
const issueAssigneeIdLine = input.issueAssigneeId ? `issue_assignee_id: ${input.issueAssigneeId}` : "issue_assignee_id: none";
|
|
90
|
+
const sessionStatusLine = input.sessionStatus ? `session_status: ${input.sessionStatus}` : "session_status: none";
|
|
91
|
+
const sessionTypeLine = input.sessionType ? `session_type: ${input.sessionType}` : "session_type: none";
|
|
92
|
+
const sessionCreatedLine = input.sessionCreatedAt ? `session_created_at: ${input.sessionCreatedAt}` : "session_created_at: none";
|
|
93
|
+
const sessionUpdatedLine = input.sessionUpdatedAt ? `session_updated_at: ${input.sessionUpdatedAt}` : "session_updated_at: none";
|
|
94
|
+
const appUserLine = input.appUserId ? `app_user_id: ${input.appUserId}` : "app_user_id: none";
|
|
95
|
+
const organizationLine = input.organizationId ? `organization_id: ${input.organizationId}` : "organization_id: none";
|
|
96
|
+
const oauthClientLine = input.oauthClientId ? `oauth_client_id: ${input.oauthClientId}` : "oauth_client_id: none";
|
|
97
|
+
const webhookIdLine = input.webhookId ? `webhook_id: ${input.webhookId}` : "webhook_id: none";
|
|
98
|
+
const webhookTimestampLine = typeof input.webhookTimestamp === "number" ? `webhook_timestamp: ${input.webhookTimestamp}` : "webhook_timestamp: none";
|
|
99
|
+
const commentIdLine = input.commentId ? `comment_id: ${input.commentId}` : "comment_id: none";
|
|
100
|
+
const promptContext = firstNonEmpty(input.promptContext) ?? "none";
|
|
101
|
+
const issueDescription = firstNonEmpty(input.issueDescription) ?? "none";
|
|
102
|
+
const commentBody = firstNonEmpty(input.commentBody) ?? "none";
|
|
103
|
+
const suggestedPeersLine = input.suggestedPeerHandles.length > 0 ? input.suggestedPeerHandles.map((handle) => ` - ${handle}`).join("\n") : " - none";
|
|
104
|
+
return `${userRequest}
|
|
105
|
+
|
|
106
|
+
${header}
|
|
107
|
+
|
|
108
|
+
Linear session context:
|
|
109
|
+
- session_id: ${input.sessionId}
|
|
110
|
+
- ${sessionStatusLine}
|
|
111
|
+
- ${sessionTypeLine}
|
|
112
|
+
- ${sessionCreatedLine}
|
|
113
|
+
- ${sessionUpdatedLine}
|
|
114
|
+
- ${issueIdLine}
|
|
115
|
+
- ${issueIdentifierLine}
|
|
116
|
+
- ${issueTitleLine}
|
|
117
|
+
- ${issueUrlLine}
|
|
118
|
+
- ${issueTeamLine}
|
|
119
|
+
- ${issueTeamIdLine}
|
|
120
|
+
- ${issueStateLine}
|
|
121
|
+
- ${issueStateIdLine}
|
|
122
|
+
- ${issueStateTypeLine}
|
|
123
|
+
- ${issueAssigneeLine}
|
|
124
|
+
- ${issueAssigneeIdLine}
|
|
125
|
+
- inferred_session_intent: ${input.sessionIntent}
|
|
126
|
+
- writeback_mode: ${input.writebackMode}
|
|
127
|
+
- ${appUserLine}
|
|
128
|
+
- ${organizationLine}
|
|
129
|
+
- ${oauthClientLine}
|
|
130
|
+
- ${webhookIdLine}
|
|
131
|
+
- ${webhookTimestampLine}
|
|
132
|
+
|
|
133
|
+
Bridge responsibilities:
|
|
134
|
+
- own orchestration for this Linear session
|
|
135
|
+
- decide whether you can answer alone or need help
|
|
136
|
+
- discover and invite specialists only when needed
|
|
137
|
+
- keep Linear updated with meaningful milestones
|
|
138
|
+
- use linear_post_thought and linear_post_action for meaningful progress updates
|
|
139
|
+
- 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
|
|
141
|
+
|
|
142
|
+
Suggested peers available in the registry right now. They are not in the room yet:
|
|
143
|
+
${suggestedPeersLine}
|
|
144
|
+
|
|
145
|
+
Prompt context:
|
|
146
|
+
${promptContext}
|
|
147
|
+
|
|
148
|
+
Issue description:
|
|
149
|
+
${issueDescription}
|
|
150
|
+
|
|
151
|
+
Linked comment:
|
|
152
|
+
- ${commentIdLine}
|
|
153
|
+
${commentBody}`;
|
|
154
|
+
}
|
|
155
|
+
function extractPromptedResponseBody(payload) {
|
|
156
|
+
if (typeof payload !== "object" || payload === null) {
|
|
157
|
+
return "";
|
|
158
|
+
}
|
|
159
|
+
const record = payload;
|
|
160
|
+
const activity = record.agentActivity;
|
|
161
|
+
if (typeof activity !== "object" || activity === null) {
|
|
162
|
+
return "";
|
|
163
|
+
}
|
|
164
|
+
const content = activity.content;
|
|
165
|
+
if (typeof content !== "object" || content === null) {
|
|
166
|
+
return "";
|
|
167
|
+
}
|
|
168
|
+
const body = content.body;
|
|
169
|
+
return typeof body === "string" ? body : "";
|
|
170
|
+
}
|
|
171
|
+
function detectSessionIntent(input) {
|
|
172
|
+
if (input.issueStateType?.trim().toLowerCase() === "started") {
|
|
173
|
+
return "implementation";
|
|
174
|
+
}
|
|
175
|
+
const explicitDirective = [
|
|
176
|
+
input.promptContext,
|
|
177
|
+
input.commentBody
|
|
178
|
+
].filter((value) => typeof value === "string" && value.trim().length > 0).join(" ").toLowerCase();
|
|
179
|
+
if (/(please implement|implement (?:this|it|now|the)|build (?:this|it|now|the)|code (?:this|it|now|the)|ship (?:it|this)|start coding|start implementation|tighten|adjust|refine)/.test(explicitDirective)) {
|
|
180
|
+
return "implementation";
|
|
181
|
+
}
|
|
182
|
+
return "planning";
|
|
183
|
+
}
|
|
184
|
+
function extractIssueTeamKey(issue) {
|
|
185
|
+
const team = extractNestedRecord(issue, "team");
|
|
186
|
+
return typeof team?.key === "string" ? team.key : null;
|
|
187
|
+
}
|
|
188
|
+
function extractIssueTeamName(issue) {
|
|
189
|
+
const team = extractNestedRecord(issue, "team");
|
|
190
|
+
return typeof team?.name === "string" ? team.name : null;
|
|
191
|
+
}
|
|
192
|
+
function extractIssueTeamId(issue) {
|
|
193
|
+
const issueRecord = extractRecord(issue);
|
|
194
|
+
if (typeof issueRecord?.teamId === "string") {
|
|
195
|
+
return issueRecord.teamId;
|
|
196
|
+
}
|
|
197
|
+
const team = extractNestedRecord(issue, "team");
|
|
198
|
+
return typeof team?.id === "string" ? team.id : null;
|
|
199
|
+
}
|
|
200
|
+
function extractIssueStateField(issue, field) {
|
|
201
|
+
const state = extractNestedRecord(issue, "state");
|
|
202
|
+
const value = state?.[field];
|
|
203
|
+
return typeof value === "string" ? value : null;
|
|
204
|
+
}
|
|
205
|
+
function extractIssueAssigneeField(issue, field) {
|
|
206
|
+
const assignee = extractNestedRecord(issue, "assignee");
|
|
207
|
+
const value = assignee?.[field];
|
|
208
|
+
return typeof value === "string" ? value : null;
|
|
209
|
+
}
|
|
210
|
+
function firstNonEmpty(...values) {
|
|
211
|
+
for (const value of values) {
|
|
212
|
+
if (!value) {
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
const trimmed = value.trim();
|
|
216
|
+
if (trimmed.length > 0) {
|
|
217
|
+
return trimmed;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
function extractNestedRecord(value, key) {
|
|
223
|
+
const record = extractRecord(value);
|
|
224
|
+
const nested = record?.[key];
|
|
225
|
+
return extractRecord(nested);
|
|
226
|
+
}
|
|
227
|
+
function extractRecord(value) {
|
|
228
|
+
return typeof value === "object" && value !== null ? value : null;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// src/integrations/linear/bridge/handler.ts
|
|
232
|
+
var SUPPORTED_ACTIONS = /* @__PURE__ */ new Set(["created", "updated", "canceled", "prompted"]);
|
|
233
|
+
var MAX_PEER_LOOKUP_PAGES = 25;
|
|
234
|
+
var PEER_PAGE_SIZE = 100;
|
|
235
|
+
var RECOVERED_ROOM_EVENT_RETRY_LIMIT = 2;
|
|
236
|
+
var RECOVERED_ROOM_EVENT_RETRY_BASE_DELAY_MS = 1e3;
|
|
237
|
+
function createLinearBridgeRuntime() {
|
|
238
|
+
return {
|
|
239
|
+
roomResolutionLocks: /* @__PURE__ */ new Map(),
|
|
240
|
+
resolvedHostHandleCache: /* @__PURE__ */ new WeakMap(),
|
|
241
|
+
authenticatedHostHandleCache: /* @__PURE__ */ new WeakMap()
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
function getAgentSessionEventKey(payload) {
|
|
245
|
+
return [
|
|
246
|
+
payload.agentSession.id,
|
|
247
|
+
normalizeAction(payload.action) ?? String(payload.action ?? "unknown"),
|
|
248
|
+
payload.agentSession.updatedAt ?? payload.agentSession.createdAt ?? "",
|
|
249
|
+
typeof payload.webhookTimestamp === "number" ? String(payload.webhookTimestamp) : ""
|
|
250
|
+
].join(":");
|
|
251
|
+
}
|
|
252
|
+
async function handleAgentSessionEvent(input, options) {
|
|
253
|
+
const logger = input.deps.logger ?? new NoopLogger();
|
|
254
|
+
const config = normalizeConfig(input.config);
|
|
255
|
+
const action = normalizeAction(input.payload.action);
|
|
256
|
+
const runtime = options?.runtime ?? createLinearBridgeRuntime();
|
|
257
|
+
if (!action) {
|
|
258
|
+
logger.warn("linear_thenvoi_bridge.ignored_unknown_action", {
|
|
259
|
+
action: input.payload.action,
|
|
260
|
+
sessionId: input.payload.agentSession.id
|
|
261
|
+
});
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
const sessionId = input.payload.agentSession.id;
|
|
265
|
+
const issueId = input.payload.agentSession.issueId ?? input.payload.agentSession.issue?.id ?? null;
|
|
266
|
+
const eventKey = options?.expectedEventKey ?? getAgentSessionEventKey(input.payload);
|
|
267
|
+
const existingBySession = await input.deps.store.getBySessionId(sessionId);
|
|
268
|
+
if (existingBySession?.lastEventKey === eventKey) {
|
|
269
|
+
logger.info("linear_thenvoi_bridge.duplicate_event_skipped", {
|
|
270
|
+
sessionId,
|
|
271
|
+
issueId,
|
|
272
|
+
action,
|
|
273
|
+
eventKey
|
|
274
|
+
});
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
if (action === "canceled") {
|
|
278
|
+
await handleCanceledAction({
|
|
279
|
+
deps: input.deps,
|
|
280
|
+
logger,
|
|
281
|
+
sessionId,
|
|
282
|
+
issueId,
|
|
283
|
+
eventKey,
|
|
284
|
+
payloadAction: input.payload.action,
|
|
285
|
+
skipRoomWrite: options?.skipRoomWrite ?? false
|
|
286
|
+
});
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
if (action === "prompted") {
|
|
290
|
+
await handlePromptedAction({
|
|
291
|
+
deps: input.deps,
|
|
292
|
+
config,
|
|
293
|
+
logger,
|
|
294
|
+
sessionId,
|
|
295
|
+
eventKey,
|
|
296
|
+
payload: input.payload,
|
|
297
|
+
skipRoomWrite: options?.skipRoomWrite ?? false
|
|
298
|
+
});
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
if (action === "created" && !options?.skipAcknowledgment) {
|
|
302
|
+
try {
|
|
303
|
+
await postThought(input.deps.linearClient, sessionId, "Received session. Setting up workspace...");
|
|
304
|
+
} catch (ackError) {
|
|
305
|
+
logger.warn("linear_thenvoi_bridge.acknowledgment_failed", {
|
|
306
|
+
sessionId,
|
|
307
|
+
error: ackError instanceof Error ? ackError.message : String(ackError)
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
let roomRecord = null;
|
|
312
|
+
try {
|
|
313
|
+
const hostAgentHandle = await resolveHostAgentHandle({
|
|
314
|
+
thenvoiRest: input.deps.thenvoiRest,
|
|
315
|
+
configuredHostHandle: config.hostAgentHandle,
|
|
316
|
+
logger,
|
|
317
|
+
runtime
|
|
318
|
+
});
|
|
319
|
+
const existingByIssue = config.roomStrategy === "issue" && issueId ? await input.deps.store.getByIssueId(issueId) : null;
|
|
320
|
+
roomRecord = await resolveRoomRecord({
|
|
321
|
+
thenvoiRest: input.deps.thenvoiRest,
|
|
322
|
+
store: input.deps.store,
|
|
323
|
+
roomStrategy: config.roomStrategy,
|
|
324
|
+
sessionId,
|
|
325
|
+
issueId,
|
|
326
|
+
logger,
|
|
327
|
+
runtime
|
|
328
|
+
});
|
|
329
|
+
const sessionIntent = detectSessionIntent({
|
|
330
|
+
issueStateType: extractIssueStateField(input.payload.agentSession.issue, "type"),
|
|
331
|
+
promptContext: input.payload.promptContext,
|
|
332
|
+
commentBody: input.payload.agentSession.comment?.body,
|
|
333
|
+
issueTitle: input.payload.agentSession.issue?.title,
|
|
334
|
+
issueDescription: input.payload.agentSession.issue?.description
|
|
335
|
+
});
|
|
336
|
+
const suggestedPeerHandles = await selectRelevantPeerHandles({
|
|
337
|
+
thenvoiRest: input.deps.thenvoiRest,
|
|
338
|
+
roomId: roomRecord.thenvoiRoomId,
|
|
339
|
+
intent: sessionIntent,
|
|
340
|
+
hostAgentHandle,
|
|
341
|
+
planningAgentHandles: input.config.planningAgentHandles,
|
|
342
|
+
implementationAgentHandles: input.config.implementationAgentHandles,
|
|
343
|
+
logger
|
|
344
|
+
});
|
|
345
|
+
await ensureRoomParticipants({
|
|
346
|
+
thenvoiRest: input.deps.thenvoiRest,
|
|
347
|
+
roomId: roomRecord.thenvoiRoomId,
|
|
348
|
+
handles: [hostAgentHandle],
|
|
349
|
+
logger
|
|
350
|
+
});
|
|
351
|
+
const message = buildBridgeMessage({
|
|
352
|
+
sessionId,
|
|
353
|
+
issueId,
|
|
354
|
+
issueIdentifier: input.payload.agentSession.issue?.identifier,
|
|
355
|
+
sessionStatus: input.payload.agentSession.status,
|
|
356
|
+
sessionType: input.payload.agentSession.type,
|
|
357
|
+
sessionCreatedAt: input.payload.agentSession.createdAt,
|
|
358
|
+
sessionUpdatedAt: input.payload.agentSession.updatedAt,
|
|
359
|
+
action,
|
|
360
|
+
hostHandle: hostAgentHandle,
|
|
361
|
+
promptContext: input.payload.promptContext,
|
|
362
|
+
issueTitle: input.payload.agentSession.issue?.title,
|
|
363
|
+
issueDescription: input.payload.agentSession.issue?.description,
|
|
364
|
+
issueUrl: input.payload.agentSession.issue?.url,
|
|
365
|
+
issueTeamKey: extractIssueTeamKey(input.payload.agentSession.issue),
|
|
366
|
+
issueTeamName: extractIssueTeamName(input.payload.agentSession.issue),
|
|
367
|
+
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"),
|
|
371
|
+
issueAssigneeId: extractIssueAssigneeField(input.payload.agentSession.issue, "id"),
|
|
372
|
+
issueAssigneeName: extractIssueAssigneeField(input.payload.agentSession.issue, "displayName") ?? extractIssueAssigneeField(input.payload.agentSession.issue, "name"),
|
|
373
|
+
commentBody: input.payload.agentSession.comment?.body,
|
|
374
|
+
commentId: input.payload.agentSession.comment?.id,
|
|
375
|
+
sessionIntent,
|
|
376
|
+
suggestedPeerHandles,
|
|
377
|
+
webhookId: input.payload.webhookId,
|
|
378
|
+
webhookTimestamp: input.payload.webhookTimestamp,
|
|
379
|
+
oauthClientId: input.payload.oauthClientId,
|
|
380
|
+
organizationId: input.payload.organizationId,
|
|
381
|
+
appUserId: input.payload.appUserId,
|
|
382
|
+
writebackMode: config.writebackMode
|
|
383
|
+
});
|
|
384
|
+
const messageMetadata = {
|
|
385
|
+
linear_event_action: action,
|
|
386
|
+
linear_session_id: sessionId,
|
|
387
|
+
linear_issue_id: issueId,
|
|
388
|
+
linear_prompt_context: input.payload.promptContext ?? null,
|
|
389
|
+
linear_writeback_mode: config.writebackMode,
|
|
390
|
+
linear_bridge: "thenvoi",
|
|
391
|
+
linear_host_handle: hostAgentHandle
|
|
392
|
+
};
|
|
393
|
+
const shouldResetRoomSession = Boolean(
|
|
394
|
+
existingByIssue && existingByIssue.linearSessionId !== sessionId && existingByIssue.thenvoiRoomId === roomRecord.thenvoiRoomId && (existingByIssue.status === "completed" || existingByIssue.status === "errored")
|
|
395
|
+
);
|
|
396
|
+
const authenticatedHostHandle = runtime.authenticatedHostHandleCache.get(input.deps.thenvoiRest) ?? null;
|
|
397
|
+
const canBootstrapDirectly = options?.skipRoomWrite === true || authenticatedHostHandle === null || authenticatedHostHandle === hostAgentHandle;
|
|
398
|
+
if (!options?.skipRoomWrite || !canBootstrapDirectly) {
|
|
399
|
+
roomRecord = await forwardBridgeMessage({
|
|
400
|
+
thenvoiRest: input.deps.thenvoiRest,
|
|
401
|
+
store: input.deps.store,
|
|
402
|
+
logger,
|
|
403
|
+
roomRecord,
|
|
404
|
+
hostAgentHandle,
|
|
405
|
+
sessionId,
|
|
406
|
+
issueId,
|
|
407
|
+
message,
|
|
408
|
+
messageType: "task",
|
|
409
|
+
metadata: messageMetadata,
|
|
410
|
+
recoveredRoomRetryBaseDelayMs: input.config.recoveredRoomRetryBaseDelayMs
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
if (canBootstrapDirectly) {
|
|
414
|
+
const linearActor = resolveLinearActor(input.payload);
|
|
415
|
+
await enqueueBootstrapRequest(input.deps.store, {
|
|
416
|
+
eventKey,
|
|
417
|
+
linearSessionId: sessionId,
|
|
418
|
+
thenvoiRoomId: roomRecord.thenvoiRoomId,
|
|
419
|
+
expectedContent: message,
|
|
420
|
+
messageType: "task",
|
|
421
|
+
senderId: linearActor.id,
|
|
422
|
+
senderName: linearActor.name,
|
|
423
|
+
metadata: shouldResetRoomSession ? {
|
|
424
|
+
...messageMetadata,
|
|
425
|
+
linear_reset_room_session: true
|
|
426
|
+
} : messageMetadata
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
await saveSessionRecord(input.deps.store, {
|
|
430
|
+
...roomRecord,
|
|
431
|
+
status: "active",
|
|
432
|
+
lastEventKey: eventKey,
|
|
433
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
434
|
+
});
|
|
435
|
+
logger.info("linear_thenvoi_bridge.message_forwarded", {
|
|
436
|
+
sessionId,
|
|
437
|
+
issueId,
|
|
438
|
+
roomId: roomRecord.thenvoiRoomId,
|
|
439
|
+
action
|
|
440
|
+
});
|
|
441
|
+
} catch (error) {
|
|
442
|
+
try {
|
|
443
|
+
await postError(
|
|
444
|
+
input.deps.linearClient,
|
|
445
|
+
sessionId,
|
|
446
|
+
`Bridge error: ${error instanceof Error ? error.message : String(error)}`
|
|
447
|
+
);
|
|
448
|
+
} catch (reportError) {
|
|
449
|
+
logger.warn("linear_thenvoi_bridge.error_reporting_failed", {
|
|
450
|
+
sessionId,
|
|
451
|
+
error: reportError instanceof Error ? reportError.message : String(reportError)
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
if (roomRecord) {
|
|
455
|
+
await saveSessionRecord(input.deps.store, {
|
|
456
|
+
...roomRecord,
|
|
457
|
+
status: "errored",
|
|
458
|
+
lastEventKey: eventKey,
|
|
459
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
460
|
+
});
|
|
461
|
+
} else if (existingBySession) {
|
|
462
|
+
await saveSessionRecord(input.deps.store, {
|
|
463
|
+
...existingBySession,
|
|
464
|
+
status: "errored",
|
|
465
|
+
lastEventKey: eventKey,
|
|
466
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
throw error;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
async function completeLinearSession(input) {
|
|
473
|
+
await input.linearClient.createAgentActivity({
|
|
474
|
+
agentSessionId: input.agentSessionId,
|
|
475
|
+
content: {
|
|
476
|
+
type: L2.AgentActivityType.Response,
|
|
477
|
+
body: input.body
|
|
478
|
+
}
|
|
479
|
+
});
|
|
480
|
+
if (!input.store) {
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
const existing = await input.store.getBySessionId(input.agentSessionId);
|
|
484
|
+
if (!existing) {
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
await saveSessionRecord(input.store, {
|
|
488
|
+
...existing,
|
|
489
|
+
status: "completed",
|
|
490
|
+
lastEventKey: input.lastEventKey ?? existing.lastEventKey ?? null,
|
|
491
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
function normalizeConfig(config) {
|
|
495
|
+
return {
|
|
496
|
+
roomStrategy: config.roomStrategy ?? "issue",
|
|
497
|
+
writebackMode: config.writebackMode ?? "final_only",
|
|
498
|
+
hostAgentHandle: normalizeOptionalHandle(config.hostAgentHandle)
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
function normalizeOptionalHandle(handle) {
|
|
502
|
+
if (!handle) {
|
|
503
|
+
return null;
|
|
504
|
+
}
|
|
505
|
+
const normalized = stripHandlePrefix(handle);
|
|
506
|
+
return normalized.length > 0 ? normalized : null;
|
|
507
|
+
}
|
|
508
|
+
async function resolveHostAgentHandle(input) {
|
|
509
|
+
if (input.configuredHostHandle) {
|
|
510
|
+
input.runtime.resolvedHostHandleCache.set(input.thenvoiRest, input.configuredHostHandle);
|
|
511
|
+
return input.configuredHostHandle;
|
|
512
|
+
}
|
|
513
|
+
const cachedAuthenticated = input.runtime.authenticatedHostHandleCache.get(input.thenvoiRest);
|
|
514
|
+
if (cachedAuthenticated) {
|
|
515
|
+
input.runtime.resolvedHostHandleCache.set(input.thenvoiRest, cachedAuthenticated);
|
|
516
|
+
return cachedAuthenticated;
|
|
517
|
+
}
|
|
518
|
+
const cached = input.runtime.resolvedHostHandleCache.get(input.thenvoiRest);
|
|
519
|
+
if (cached) {
|
|
520
|
+
return cached;
|
|
521
|
+
}
|
|
522
|
+
const identity = await input.thenvoiRest.getAgentMe();
|
|
523
|
+
const authenticatedAgentHandle = normalizeOptionalHandle(identity.handle);
|
|
524
|
+
if (authenticatedAgentHandle) {
|
|
525
|
+
input.runtime.authenticatedHostHandleCache.set(input.thenvoiRest, authenticatedAgentHandle);
|
|
526
|
+
input.runtime.resolvedHostHandleCache.set(input.thenvoiRest, authenticatedAgentHandle);
|
|
527
|
+
return authenticatedAgentHandle;
|
|
528
|
+
}
|
|
529
|
+
throw new Error(
|
|
530
|
+
"Linear bridge could not resolve the host agent handle. Set hostAgentHandle or use a REST adapter whose getAgentMe() returns handle."
|
|
531
|
+
);
|
|
532
|
+
}
|
|
533
|
+
function normalizeAction(action) {
|
|
534
|
+
if (!action) {
|
|
535
|
+
return null;
|
|
536
|
+
}
|
|
537
|
+
const normalized = action.trim().toLowerCase();
|
|
538
|
+
if (!SUPPORTED_ACTIONS.has(normalized)) {
|
|
539
|
+
return null;
|
|
540
|
+
}
|
|
541
|
+
return normalized;
|
|
542
|
+
}
|
|
543
|
+
async function handleCanceledAction(input) {
|
|
544
|
+
const existing = await input.deps.store.getBySessionId(input.sessionId);
|
|
545
|
+
if (!existing) {
|
|
546
|
+
input.logger.info("linear_thenvoi_bridge.session_canceled_without_room", {
|
|
547
|
+
sessionId: input.sessionId,
|
|
548
|
+
issueId: input.issueId,
|
|
549
|
+
action: input.payloadAction
|
|
550
|
+
});
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
await saveSessionRecord(input.deps.store, {
|
|
554
|
+
...existing,
|
|
555
|
+
status: "canceled",
|
|
556
|
+
lastEventKey: input.eventKey,
|
|
557
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
558
|
+
});
|
|
559
|
+
if (!input.skipRoomWrite) {
|
|
560
|
+
await input.deps.thenvoiRest.createChatEvent(existing.thenvoiRoomId, {
|
|
561
|
+
content: "[Linear]: Agent session canceled. Stop in-room execution and await new instructions.",
|
|
562
|
+
messageType: "task",
|
|
563
|
+
metadata: {
|
|
564
|
+
linear_event_action: "canceled",
|
|
565
|
+
linear_session_id: input.sessionId,
|
|
566
|
+
linear_issue_id: input.issueId,
|
|
567
|
+
linear_bridge: "thenvoi"
|
|
568
|
+
}
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
input.logger.info("linear_thenvoi_bridge.session_canceled", {
|
|
572
|
+
sessionId: input.sessionId,
|
|
573
|
+
issueId: input.issueId,
|
|
574
|
+
hadRoom: Boolean(existing),
|
|
575
|
+
action: input.payloadAction
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
async function handlePromptedAction(input) {
|
|
579
|
+
const existing = await input.deps.store.getBySessionId(input.sessionId);
|
|
580
|
+
if (!existing) {
|
|
581
|
+
input.logger.warn("linear_thenvoi_bridge.prompted_no_room", {
|
|
582
|
+
sessionId: input.sessionId
|
|
583
|
+
});
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
const userResponse = extractPromptedResponseBody(input.payload);
|
|
587
|
+
if (!userResponse) {
|
|
588
|
+
input.logger.warn("linear_thenvoi_bridge.prompted_empty_response", {
|
|
589
|
+
sessionId: input.sessionId
|
|
590
|
+
});
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
const message = `[Linear User Response]: ${userResponse}`;
|
|
594
|
+
const metadata = {
|
|
595
|
+
linear_event_action: "prompted",
|
|
596
|
+
linear_session_id: input.sessionId,
|
|
597
|
+
linear_bridge: "thenvoi"
|
|
598
|
+
};
|
|
599
|
+
const canBootstrapDirectly = input.config.hostAgentHandle !== null ? true : Boolean(normalizeOptionalHandle((await input.deps.thenvoiRest.getAgentMe()).handle));
|
|
600
|
+
if (!input.skipRoomWrite || !canBootstrapDirectly) {
|
|
601
|
+
await input.deps.thenvoiRest.createChatEvent(existing.thenvoiRoomId, {
|
|
602
|
+
content: message,
|
|
603
|
+
messageType: "text",
|
|
604
|
+
metadata
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
if (canBootstrapDirectly) {
|
|
608
|
+
const linearActor = resolveLinearActor(input.payload);
|
|
609
|
+
await enqueueBootstrapRequest(input.deps.store, {
|
|
610
|
+
eventKey: input.eventKey,
|
|
611
|
+
linearSessionId: input.sessionId,
|
|
612
|
+
thenvoiRoomId: existing.thenvoiRoomId,
|
|
613
|
+
expectedContent: message,
|
|
614
|
+
messageType: "text",
|
|
615
|
+
senderId: linearActor.id,
|
|
616
|
+
senderName: linearActor.name,
|
|
617
|
+
metadata
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
await saveSessionRecord(input.deps.store, {
|
|
621
|
+
...existing,
|
|
622
|
+
status: "waiting",
|
|
623
|
+
lastEventKey: input.eventKey,
|
|
624
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
625
|
+
});
|
|
626
|
+
input.logger.info("linear_thenvoi_bridge.prompted_forwarded", {
|
|
627
|
+
sessionId: input.sessionId,
|
|
628
|
+
roomId: existing.thenvoiRoomId
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
async function enqueueBootstrapRequest(store, request) {
|
|
632
|
+
const now = /* @__PURE__ */ new Date();
|
|
633
|
+
await store.enqueueBootstrapRequest({
|
|
634
|
+
...request,
|
|
635
|
+
createdAt: now.toISOString(),
|
|
636
|
+
expiresAt: new Date(now.getTime() + 10 * 6e4).toISOString()
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
function resolveLinearActor(payload) {
|
|
640
|
+
const creator = payload.agentSession.creator;
|
|
641
|
+
const creatorName = typeof creator?.name === "string" ? creator.name.trim() : "";
|
|
642
|
+
const creatorId = typeof creator?.id === "string" ? creator.id.trim() : "";
|
|
643
|
+
if (creatorName.length > 0) {
|
|
644
|
+
return {
|
|
645
|
+
id: creatorId.length > 0 ? creatorId : payload.agentSession.creatorId ?? payload.appUserId ?? null,
|
|
646
|
+
name: creatorName
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
return {
|
|
650
|
+
id: payload.agentSession.creatorId ?? payload.appUserId ?? null,
|
|
651
|
+
name: "Linear User"
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
async function resolveRoomRecord(input) {
|
|
655
|
+
const lockScope = input.roomStrategy === "issue" && input.issueId ? `issue:${input.issueId}` : `session:${input.sessionId}`;
|
|
656
|
+
const existing = input.runtime.roomResolutionLocks.get(lockScope);
|
|
657
|
+
if (existing) {
|
|
658
|
+
return existing;
|
|
659
|
+
}
|
|
660
|
+
const promise = resolveRoomRecordImpl(input);
|
|
661
|
+
input.runtime.roomResolutionLocks.set(lockScope, promise);
|
|
662
|
+
try {
|
|
663
|
+
return await promise;
|
|
664
|
+
} finally {
|
|
665
|
+
input.runtime.roomResolutionLocks.delete(lockScope);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
async function forwardBridgeMessage(input) {
|
|
669
|
+
try {
|
|
670
|
+
await input.thenvoiRest.createChatEvent(input.roomRecord.thenvoiRoomId, {
|
|
671
|
+
content: input.message,
|
|
672
|
+
messageType: input.messageType,
|
|
673
|
+
metadata: input.metadata
|
|
674
|
+
});
|
|
675
|
+
return input.roomRecord;
|
|
676
|
+
} catch (error) {
|
|
677
|
+
if (!isRecoverableRoomAccessError(error)) {
|
|
678
|
+
throw error;
|
|
679
|
+
}
|
|
680
|
+
input.logger.warn("linear_thenvoi_bridge.room_forward_recovering_with_fresh_room", {
|
|
681
|
+
sessionId: input.sessionId,
|
|
682
|
+
issueId: input.issueId,
|
|
683
|
+
previousRoomId: input.roomRecord.thenvoiRoomId,
|
|
684
|
+
error: error instanceof Error ? error.message : String(error)
|
|
685
|
+
});
|
|
686
|
+
const recoveredRoom = await createFreshRoomRecord({
|
|
687
|
+
thenvoiRest: input.thenvoiRest,
|
|
688
|
+
store: input.store,
|
|
689
|
+
sessionId: input.sessionId,
|
|
690
|
+
issueId: input.issueId,
|
|
691
|
+
logger: input.logger
|
|
692
|
+
});
|
|
693
|
+
await ensureRoomParticipants({
|
|
694
|
+
thenvoiRest: input.thenvoiRest,
|
|
695
|
+
roomId: recoveredRoom.thenvoiRoomId,
|
|
696
|
+
handles: [input.hostAgentHandle],
|
|
697
|
+
logger: input.logger
|
|
698
|
+
});
|
|
699
|
+
let attempt = 0;
|
|
700
|
+
while (true) {
|
|
701
|
+
try {
|
|
702
|
+
await input.thenvoiRest.createChatEvent(recoveredRoom.thenvoiRoomId, {
|
|
703
|
+
content: input.message,
|
|
704
|
+
messageType: input.messageType,
|
|
705
|
+
metadata: input.metadata
|
|
706
|
+
});
|
|
707
|
+
return recoveredRoom;
|
|
708
|
+
} catch (recoveredError) {
|
|
709
|
+
if (!isRetryableRecoveredRoomEventError(recoveredError) || attempt >= RECOVERED_ROOM_EVENT_RETRY_LIMIT) {
|
|
710
|
+
throw recoveredError;
|
|
711
|
+
}
|
|
712
|
+
attempt += 1;
|
|
713
|
+
const retryBaseDelayMs = input.recoveredRoomRetryBaseDelayMs ?? RECOVERED_ROOM_EVENT_RETRY_BASE_DELAY_MS;
|
|
714
|
+
const delayMs = retryBaseDelayMs * attempt;
|
|
715
|
+
input.logger.warn("linear_thenvoi_bridge.room_recreated_retrying", {
|
|
716
|
+
sessionId: input.sessionId,
|
|
717
|
+
issueId: input.issueId,
|
|
718
|
+
roomId: recoveredRoom.thenvoiRoomId,
|
|
719
|
+
attempt,
|
|
720
|
+
delayMs,
|
|
721
|
+
error: recoveredError instanceof Error ? recoveredError.message : String(recoveredError)
|
|
722
|
+
});
|
|
723
|
+
await sleep(delayMs);
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
function isRecoverableRoomAccessError(error) {
|
|
729
|
+
if (!(error instanceof Error)) {
|
|
730
|
+
return false;
|
|
731
|
+
}
|
|
732
|
+
return /\/api\/v1\/agent\/chats\/.+\/events failed \((403|404)/.test(error.message);
|
|
733
|
+
}
|
|
734
|
+
function isRetryableRecoveredRoomEventError(error) {
|
|
735
|
+
if (!(error instanceof Error)) {
|
|
736
|
+
return false;
|
|
737
|
+
}
|
|
738
|
+
return /\/api\/v1\/agent\/chats\/.+\/events failed \((403|404|429)/.test(error.message);
|
|
739
|
+
}
|
|
740
|
+
function isRetryableRateLimitError(error) {
|
|
741
|
+
if (typeof error === "object" && error !== null && "retryable" in error) {
|
|
742
|
+
return error.retryable === true;
|
|
743
|
+
}
|
|
744
|
+
return error instanceof Error && /\b429\b/.test(error.message);
|
|
745
|
+
}
|
|
746
|
+
async function createFreshRoomRecord(input) {
|
|
747
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
748
|
+
const created = await input.thenvoiRest.createChat();
|
|
749
|
+
const record = {
|
|
750
|
+
linearSessionId: input.sessionId,
|
|
751
|
+
linearIssueId: input.issueId,
|
|
752
|
+
thenvoiRoomId: created.id,
|
|
753
|
+
status: "active",
|
|
754
|
+
lastEventKey: null,
|
|
755
|
+
createdAt: now,
|
|
756
|
+
updatedAt: now
|
|
757
|
+
};
|
|
758
|
+
await input.store.upsert(record);
|
|
759
|
+
input.logger.info("linear_thenvoi_bridge.room_recreated", {
|
|
760
|
+
sessionId: input.sessionId,
|
|
761
|
+
issueId: input.issueId,
|
|
762
|
+
roomId: created.id
|
|
763
|
+
});
|
|
764
|
+
return record;
|
|
765
|
+
}
|
|
766
|
+
async function resolveRoomRecordImpl(input) {
|
|
767
|
+
const existingBySession = await input.store.getBySessionId(input.sessionId);
|
|
768
|
+
if (existingBySession && existingBySession.status !== "canceled") {
|
|
769
|
+
return existingBySession;
|
|
770
|
+
}
|
|
771
|
+
if (input.roomStrategy === "issue" && input.issueId) {
|
|
772
|
+
const existingByIssue = await input.store.getByIssueId(input.issueId);
|
|
773
|
+
if (existingByIssue && existingByIssue.status !== "canceled") {
|
|
774
|
+
const now2 = (/* @__PURE__ */ new Date()).toISOString();
|
|
775
|
+
const linkedRecord = {
|
|
776
|
+
linearSessionId: input.sessionId,
|
|
777
|
+
linearIssueId: input.issueId,
|
|
778
|
+
thenvoiRoomId: existingByIssue.thenvoiRoomId,
|
|
779
|
+
status: "active",
|
|
780
|
+
lastEventKey: existingBySession?.lastEventKey ?? null,
|
|
781
|
+
createdAt: existingBySession?.createdAt ?? now2,
|
|
782
|
+
updatedAt: now2
|
|
783
|
+
};
|
|
784
|
+
await input.store.upsert(linkedRecord);
|
|
785
|
+
return linkedRecord;
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
789
|
+
const created = await input.thenvoiRest.createChat();
|
|
790
|
+
const createdRecord = {
|
|
791
|
+
linearSessionId: input.sessionId,
|
|
792
|
+
linearIssueId: input.issueId,
|
|
793
|
+
thenvoiRoomId: created.id,
|
|
794
|
+
status: "active",
|
|
795
|
+
lastEventKey: null,
|
|
796
|
+
createdAt: now,
|
|
797
|
+
updatedAt: now
|
|
798
|
+
};
|
|
799
|
+
await input.store.upsert(createdRecord);
|
|
800
|
+
input.logger.info("linear_thenvoi_bridge.room_created", {
|
|
801
|
+
sessionId: input.sessionId,
|
|
802
|
+
issueId: input.issueId,
|
|
803
|
+
roomId: created.id,
|
|
804
|
+
roomStrategy: input.roomStrategy
|
|
805
|
+
});
|
|
806
|
+
return createdRecord;
|
|
807
|
+
}
|
|
808
|
+
async function ensureRoomParticipants(input) {
|
|
809
|
+
if (input.handles.length === 0) {
|
|
810
|
+
return;
|
|
811
|
+
}
|
|
812
|
+
let existingParticipants;
|
|
813
|
+
try {
|
|
814
|
+
existingParticipants = await input.thenvoiRest.listChatParticipants(input.roomId);
|
|
815
|
+
} catch (error) {
|
|
816
|
+
if (isRetryableRateLimitError(error)) {
|
|
817
|
+
input.logger.warn("linear_thenvoi_bridge.participant_list_rate_limited", {
|
|
818
|
+
roomId: input.roomId,
|
|
819
|
+
error: error instanceof Error ? error.message : String(error)
|
|
820
|
+
});
|
|
821
|
+
return;
|
|
822
|
+
}
|
|
823
|
+
throw error;
|
|
824
|
+
}
|
|
825
|
+
const existingHandles = new Set(
|
|
826
|
+
existingParticipants.map((participant) => stripHandlePrefix(participant.handle ?? participant.name)).filter((value) => value.length > 0)
|
|
827
|
+
);
|
|
828
|
+
const missingHandles = input.handles.filter((handle) => !existingHandles.has(handle));
|
|
829
|
+
if (missingHandles.length === 0) {
|
|
830
|
+
return;
|
|
831
|
+
}
|
|
832
|
+
let peersByHandle;
|
|
833
|
+
try {
|
|
834
|
+
peersByHandle = await lookupPeerIdsByHandle(input.thenvoiRest, input.roomId, missingHandles);
|
|
835
|
+
} catch (error) {
|
|
836
|
+
if (error instanceof UnsupportedFeatureError) {
|
|
837
|
+
input.logger.warn("linear_thenvoi_bridge.peer_lookup_unavailable", {
|
|
838
|
+
roomId: input.roomId,
|
|
839
|
+
missingHandles,
|
|
840
|
+
reason: error.message
|
|
841
|
+
});
|
|
842
|
+
return;
|
|
843
|
+
}
|
|
844
|
+
throw error;
|
|
845
|
+
}
|
|
846
|
+
for (const handle of missingHandles) {
|
|
847
|
+
const participantId = peersByHandle.get(handle);
|
|
848
|
+
if (!participantId) {
|
|
849
|
+
input.logger.warn("linear_thenvoi_bridge.peer_not_found", {
|
|
850
|
+
roomId: input.roomId,
|
|
851
|
+
handle
|
|
852
|
+
});
|
|
853
|
+
continue;
|
|
854
|
+
}
|
|
855
|
+
try {
|
|
856
|
+
await input.thenvoiRest.addChatParticipant(
|
|
857
|
+
input.roomId,
|
|
858
|
+
{
|
|
859
|
+
participantId,
|
|
860
|
+
role: "member"
|
|
861
|
+
}
|
|
862
|
+
);
|
|
863
|
+
} catch (error) {
|
|
864
|
+
input.logger.warn("linear_thenvoi_bridge.add_participant_failed", {
|
|
865
|
+
roomId: input.roomId,
|
|
866
|
+
handle,
|
|
867
|
+
error: error instanceof Error ? error.message : String(error)
|
|
868
|
+
});
|
|
869
|
+
if (!isRetryableRateLimitError(error)) {
|
|
870
|
+
throw error;
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
async function lookupPeerIdsByHandle(thenvoiRest, roomId, handles) {
|
|
876
|
+
if (!thenvoiRest.listPeers) {
|
|
877
|
+
throw new UnsupportedFeatureError("Peer listing is not available in current REST adapter");
|
|
878
|
+
}
|
|
879
|
+
const targets = new Set(handles);
|
|
880
|
+
const found = /* @__PURE__ */ new Map();
|
|
881
|
+
for (let page = 1; page <= MAX_PEER_LOOKUP_PAGES && found.size < targets.size; page += 1) {
|
|
882
|
+
const response = await thenvoiRest.listPeers({
|
|
883
|
+
page,
|
|
884
|
+
pageSize: PEER_PAGE_SIZE,
|
|
885
|
+
notInChat: roomId
|
|
886
|
+
});
|
|
887
|
+
for (const peer of response.data) {
|
|
888
|
+
const peerId = peer.id;
|
|
889
|
+
if (typeof peerId !== "string" || peerId.length === 0) {
|
|
890
|
+
continue;
|
|
891
|
+
}
|
|
892
|
+
const handleCandidates = [
|
|
893
|
+
typeof peer.handle === "string" ? stripHandlePrefix(peer.handle) : "",
|
|
894
|
+
typeof peer.name === "string" ? stripHandlePrefix(peer.name) : ""
|
|
895
|
+
].filter((value) => value.length > 0);
|
|
896
|
+
for (const candidate of handleCandidates) {
|
|
897
|
+
if (!targets.has(candidate)) {
|
|
898
|
+
continue;
|
|
899
|
+
}
|
|
900
|
+
found.set(candidate, peerId);
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
if (response.data.length < PEER_PAGE_SIZE) {
|
|
904
|
+
break;
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
return found;
|
|
908
|
+
}
|
|
909
|
+
async function selectRelevantPeerHandles(input) {
|
|
910
|
+
const configuredHandles = resolveConfiguredSpecialistHandles(input.intent, {
|
|
911
|
+
planningAgentHandles: input.planningAgentHandles,
|
|
912
|
+
implementationAgentHandles: input.implementationAgentHandles
|
|
913
|
+
}).filter((handle) => handle !== input.hostAgentHandle);
|
|
914
|
+
if (configuredHandles.length > 0) {
|
|
915
|
+
input.logger.info("linear_thenvoi_bridge.peer_prefetch_selected", {
|
|
916
|
+
roomId: input.roomId,
|
|
917
|
+
intent: input.intent,
|
|
918
|
+
handles: configuredHandles,
|
|
919
|
+
source: "configured"
|
|
920
|
+
});
|
|
921
|
+
return configuredHandles;
|
|
922
|
+
}
|
|
923
|
+
if (!input.thenvoiRest.listPeers) {
|
|
924
|
+
return [];
|
|
925
|
+
}
|
|
926
|
+
let peers;
|
|
927
|
+
try {
|
|
928
|
+
const response = await input.thenvoiRest.listPeers({
|
|
929
|
+
page: 1,
|
|
930
|
+
pageSize: PEER_PAGE_SIZE,
|
|
931
|
+
notInChat: input.roomId
|
|
932
|
+
});
|
|
933
|
+
peers = response.data;
|
|
934
|
+
} catch (error) {
|
|
935
|
+
if (error instanceof UnsupportedFeatureError) {
|
|
936
|
+
return [];
|
|
937
|
+
}
|
|
938
|
+
input.logger.warn("linear_thenvoi_bridge.peer_prefetch_failed", {
|
|
939
|
+
roomId: input.roomId,
|
|
940
|
+
intent: input.intent,
|
|
941
|
+
error: error instanceof Error ? error.message : String(error)
|
|
942
|
+
});
|
|
943
|
+
return [];
|
|
944
|
+
}
|
|
945
|
+
const ranked = peers.map((peer) => {
|
|
946
|
+
const handle = normalizeOptionalHandle(typeof peer.handle === "string" ? peer.handle : null);
|
|
947
|
+
return {
|
|
948
|
+
handle,
|
|
949
|
+
score: scorePeerForIntent(peer, input.intent)
|
|
950
|
+
};
|
|
951
|
+
}).filter(
|
|
952
|
+
(candidate) => typeof candidate.handle === "string" && candidate.handle.length > 0 && candidate.handle !== input.hostAgentHandle
|
|
953
|
+
).filter((candidate) => candidate.score > 0).sort((left, right) => right.score - left.score);
|
|
954
|
+
const selected = ranked.slice(0, 2).map((candidate) => candidate.handle);
|
|
955
|
+
if (selected.length > 0) {
|
|
956
|
+
input.logger.info("linear_thenvoi_bridge.peer_prefetch_selected", {
|
|
957
|
+
roomId: input.roomId,
|
|
958
|
+
intent: input.intent,
|
|
959
|
+
handles: selected,
|
|
960
|
+
source: "lookup"
|
|
961
|
+
});
|
|
962
|
+
}
|
|
963
|
+
return selected;
|
|
964
|
+
}
|
|
965
|
+
function resolveConfiguredSpecialistHandles(intent, config) {
|
|
966
|
+
const handles = intent === "implementation" ? config.implementationAgentHandles : config.planningAgentHandles;
|
|
967
|
+
if (!handles || handles.length === 0) {
|
|
968
|
+
return [];
|
|
969
|
+
}
|
|
970
|
+
const configured = handles.map((value) => normalizeOptionalHandle(value)).filter((value) => Boolean(value));
|
|
971
|
+
if (intent === "implementation") {
|
|
972
|
+
return dedupeHandles(configured);
|
|
973
|
+
}
|
|
974
|
+
const ranked = configured.map((handle) => ({
|
|
975
|
+
handle,
|
|
976
|
+
score: /\bplanner\b/.test(handle) ? 3 : /\breviewer\b|\bcodex\b/.test(handle) ? 2 : 1
|
|
977
|
+
})).sort((left, right) => right.score - left.score).slice(0, 2).map((entry) => entry.handle);
|
|
978
|
+
return dedupeHandles(ranked);
|
|
979
|
+
}
|
|
980
|
+
function scorePeerForIntent(peer, intent) {
|
|
981
|
+
const description = typeof peer.description === "string" ? peer.description : "";
|
|
982
|
+
const haystack = [
|
|
983
|
+
typeof peer.name === "string" ? peer.name : "",
|
|
984
|
+
typeof peer.handle === "string" ? peer.handle : "",
|
|
985
|
+
description
|
|
986
|
+
].join(" ").toLowerCase();
|
|
987
|
+
const weightedTerms = intent === "implementation" ? [
|
|
988
|
+
{ pattern: /\bimplementer\b|\bimplementation\b/, score: 10 },
|
|
989
|
+
{ pattern: /\bengineer\b|\bdeveloper\b|\bcoder\b|\bcoding\b/, score: 8 },
|
|
990
|
+
{ pattern: /\bbuild\b|\bfrontend\b|\bui\b/, score: 5 },
|
|
991
|
+
{ pattern: /\breviewer\b|\breview\b|\bplanner\b|\bplan\b/, score: -4 }
|
|
992
|
+
] : [
|
|
993
|
+
{ pattern: /\bplanner\b|\bplanning\b|\bplan\b/, score: 10 },
|
|
994
|
+
{ pattern: /\bclaude\b/, score: 8 },
|
|
995
|
+
{ pattern: /\breviewer\b|\breview\b|\bcodex\b/, score: 7 },
|
|
996
|
+
{ pattern: /\borchestrator\b|\barchitect\b|\bdesign\b|\bscope\b|\bspec\b/, score: 6 },
|
|
997
|
+
{ pattern: /\bimplementer\b|\bimplementation\b/, score: -3 }
|
|
998
|
+
];
|
|
999
|
+
return weightedTerms.reduce((total, term) => term.pattern.test(haystack) ? total + term.score : total, 0);
|
|
1000
|
+
}
|
|
1001
|
+
async function sleep(ms) {
|
|
1002
|
+
await new Promise((resolveDelay) => setTimeout(resolveDelay, ms));
|
|
1003
|
+
}
|
|
1004
|
+
async function saveSessionRecord(store, record) {
|
|
1005
|
+
await store.upsert(record);
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
// src/integrations/linear/client.ts
|
|
1009
|
+
import { LinearClient } from "@linear/sdk";
|
|
1010
|
+
function isLinearApiKey(token) {
|
|
1011
|
+
return token.trim().startsWith("lin_api_");
|
|
1012
|
+
}
|
|
1013
|
+
function createLinearClient(token) {
|
|
1014
|
+
const trimmedToken = token.trim();
|
|
1015
|
+
if (isLinearApiKey(trimmedToken)) {
|
|
1016
|
+
return new LinearClient({
|
|
1017
|
+
apiKey: trimmedToken
|
|
1018
|
+
});
|
|
1019
|
+
}
|
|
1020
|
+
return new LinearClient({
|
|
1021
|
+
accessToken: trimmedToken
|
|
1022
|
+
});
|
|
1023
|
+
}
|
|
1024
|
+
function buildLinearAuthorizationHeader(token) {
|
|
1025
|
+
const trimmedToken = token.trim();
|
|
1026
|
+
if (isLinearApiKey(trimmedToken)) {
|
|
1027
|
+
return trimmedToken;
|
|
1028
|
+
}
|
|
1029
|
+
if (trimmedToken.startsWith("Bearer ")) {
|
|
1030
|
+
return trimmedToken;
|
|
1031
|
+
}
|
|
1032
|
+
return `Bearer ${trimmedToken}`;
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
// src/integrations/linear/store.ts
|
|
1036
|
+
var SESSION_STATUSES = /* @__PURE__ */ new Set([
|
|
1037
|
+
"active",
|
|
1038
|
+
"waiting",
|
|
1039
|
+
"completed",
|
|
1040
|
+
"canceled",
|
|
1041
|
+
"errored"
|
|
1042
|
+
]);
|
|
1043
|
+
var SqliteSessionRoomStore = class {
|
|
1044
|
+
dbPath;
|
|
1045
|
+
dbPromise = null;
|
|
1046
|
+
constructor(dbPath) {
|
|
1047
|
+
this.dbPath = dbPath;
|
|
1048
|
+
}
|
|
1049
|
+
async getBySessionId(sessionId) {
|
|
1050
|
+
const db = await this.getDb();
|
|
1051
|
+
const rawRow = db.prepare(
|
|
1052
|
+
`
|
|
1053
|
+
SELECT
|
|
1054
|
+
linear_session_id,
|
|
1055
|
+
linear_issue_id,
|
|
1056
|
+
thenvoi_room_id,
|
|
1057
|
+
status,
|
|
1058
|
+
last_event_key,
|
|
1059
|
+
created_at,
|
|
1060
|
+
updated_at
|
|
1061
|
+
FROM linear_thenvoi_session_rooms
|
|
1062
|
+
WHERE linear_session_id = ?
|
|
1063
|
+
LIMIT 1
|
|
1064
|
+
`
|
|
1065
|
+
).get(sessionId);
|
|
1066
|
+
const row = parseSessionRoomRow(rawRow);
|
|
1067
|
+
return row ? this.toRecord(row) : null;
|
|
1068
|
+
}
|
|
1069
|
+
async getByIssueId(issueId) {
|
|
1070
|
+
const db = await this.getDb();
|
|
1071
|
+
const rawRow = db.prepare(
|
|
1072
|
+
`
|
|
1073
|
+
SELECT
|
|
1074
|
+
linear_session_id,
|
|
1075
|
+
linear_issue_id,
|
|
1076
|
+
thenvoi_room_id,
|
|
1077
|
+
status,
|
|
1078
|
+
last_event_key,
|
|
1079
|
+
created_at,
|
|
1080
|
+
updated_at
|
|
1081
|
+
FROM linear_thenvoi_session_rooms
|
|
1082
|
+
WHERE linear_issue_id = ? AND status != 'canceled'
|
|
1083
|
+
ORDER BY updated_at DESC
|
|
1084
|
+
LIMIT 1
|
|
1085
|
+
`
|
|
1086
|
+
).get(issueId);
|
|
1087
|
+
const row = parseSessionRoomRow(rawRow);
|
|
1088
|
+
return row ? this.toRecord(row) : null;
|
|
1089
|
+
}
|
|
1090
|
+
async upsert(record) {
|
|
1091
|
+
const db = await this.getDb();
|
|
1092
|
+
db.prepare(
|
|
1093
|
+
`
|
|
1094
|
+
INSERT INTO linear_thenvoi_session_rooms (
|
|
1095
|
+
linear_session_id,
|
|
1096
|
+
linear_issue_id,
|
|
1097
|
+
thenvoi_room_id,
|
|
1098
|
+
status,
|
|
1099
|
+
last_event_key,
|
|
1100
|
+
created_at,
|
|
1101
|
+
updated_at
|
|
1102
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
1103
|
+
ON CONFLICT(linear_session_id)
|
|
1104
|
+
DO UPDATE SET
|
|
1105
|
+
linear_issue_id = excluded.linear_issue_id,
|
|
1106
|
+
thenvoi_room_id = excluded.thenvoi_room_id,
|
|
1107
|
+
status = excluded.status,
|
|
1108
|
+
last_event_key = excluded.last_event_key,
|
|
1109
|
+
updated_at = excluded.updated_at
|
|
1110
|
+
`
|
|
1111
|
+
).run(
|
|
1112
|
+
record.linearSessionId,
|
|
1113
|
+
record.linearIssueId,
|
|
1114
|
+
record.thenvoiRoomId,
|
|
1115
|
+
record.status,
|
|
1116
|
+
record.lastEventKey ?? null,
|
|
1117
|
+
record.createdAt,
|
|
1118
|
+
record.updatedAt
|
|
1119
|
+
);
|
|
1120
|
+
}
|
|
1121
|
+
async markCanceled(sessionId) {
|
|
1122
|
+
const db = await this.getDb();
|
|
1123
|
+
db.prepare(
|
|
1124
|
+
`
|
|
1125
|
+
UPDATE linear_thenvoi_session_rooms
|
|
1126
|
+
SET status = 'canceled', updated_at = ?
|
|
1127
|
+
WHERE linear_session_id = ?
|
|
1128
|
+
`
|
|
1129
|
+
).run((/* @__PURE__ */ new Date()).toISOString(), sessionId);
|
|
1130
|
+
}
|
|
1131
|
+
async enqueueBootstrapRequest(request) {
|
|
1132
|
+
const db = await this.getDb();
|
|
1133
|
+
db.prepare(
|
|
1134
|
+
`
|
|
1135
|
+
INSERT INTO linear_thenvoi_bootstrap_requests (
|
|
1136
|
+
event_key,
|
|
1137
|
+
linear_session_id,
|
|
1138
|
+
thenvoi_room_id,
|
|
1139
|
+
expected_content,
|
|
1140
|
+
message_type,
|
|
1141
|
+
metadata_json,
|
|
1142
|
+
created_at,
|
|
1143
|
+
expires_at,
|
|
1144
|
+
processed_at
|
|
1145
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, NULL)
|
|
1146
|
+
ON CONFLICT(event_key)
|
|
1147
|
+
DO UPDATE SET
|
|
1148
|
+
linear_session_id = excluded.linear_session_id,
|
|
1149
|
+
thenvoi_room_id = excluded.thenvoi_room_id,
|
|
1150
|
+
expected_content = excluded.expected_content,
|
|
1151
|
+
message_type = excluded.message_type,
|
|
1152
|
+
metadata_json = excluded.metadata_json,
|
|
1153
|
+
created_at = excluded.created_at,
|
|
1154
|
+
expires_at = excluded.expires_at,
|
|
1155
|
+
processed_at = NULL
|
|
1156
|
+
`
|
|
1157
|
+
).run(
|
|
1158
|
+
request.eventKey,
|
|
1159
|
+
request.linearSessionId,
|
|
1160
|
+
request.thenvoiRoomId,
|
|
1161
|
+
request.expectedContent,
|
|
1162
|
+
request.messageType,
|
|
1163
|
+
request.metadata ? JSON.stringify(request.metadata) : null,
|
|
1164
|
+
request.createdAt,
|
|
1165
|
+
request.expiresAt
|
|
1166
|
+
);
|
|
1167
|
+
}
|
|
1168
|
+
async listPendingBootstrapRequests(limit = 20) {
|
|
1169
|
+
const db = await this.getDb();
|
|
1170
|
+
const rawRows = db.prepare(
|
|
1171
|
+
`
|
|
1172
|
+
SELECT
|
|
1173
|
+
event_key,
|
|
1174
|
+
linear_session_id,
|
|
1175
|
+
thenvoi_room_id,
|
|
1176
|
+
expected_content,
|
|
1177
|
+
message_type,
|
|
1178
|
+
metadata_json,
|
|
1179
|
+
created_at,
|
|
1180
|
+
expires_at,
|
|
1181
|
+
processed_at
|
|
1182
|
+
FROM linear_thenvoi_bootstrap_requests
|
|
1183
|
+
WHERE processed_at IS NULL AND expires_at > ?
|
|
1184
|
+
ORDER BY created_at ASC
|
|
1185
|
+
LIMIT ?
|
|
1186
|
+
`
|
|
1187
|
+
).all((/* @__PURE__ */ new Date()).toISOString(), limit);
|
|
1188
|
+
const rows = parseBootstrapRequestRows(rawRows);
|
|
1189
|
+
return rows.map((row) => ({
|
|
1190
|
+
eventKey: row.event_key,
|
|
1191
|
+
linearSessionId: row.linear_session_id,
|
|
1192
|
+
thenvoiRoomId: row.thenvoi_room_id,
|
|
1193
|
+
expectedContent: row.expected_content,
|
|
1194
|
+
messageType: row.message_type,
|
|
1195
|
+
metadata: this.parseMetadata(row.metadata_json),
|
|
1196
|
+
createdAt: row.created_at,
|
|
1197
|
+
expiresAt: row.expires_at
|
|
1198
|
+
}));
|
|
1199
|
+
}
|
|
1200
|
+
async markBootstrapRequestProcessed(eventKey) {
|
|
1201
|
+
const db = await this.getDb();
|
|
1202
|
+
db.prepare(
|
|
1203
|
+
`
|
|
1204
|
+
UPDATE linear_thenvoi_bootstrap_requests
|
|
1205
|
+
SET processed_at = ?
|
|
1206
|
+
WHERE event_key = ? AND processed_at IS NULL
|
|
1207
|
+
`
|
|
1208
|
+
).run((/* @__PURE__ */ new Date()).toISOString(), eventKey);
|
|
1209
|
+
}
|
|
1210
|
+
async close() {
|
|
1211
|
+
if (!this.dbPromise) {
|
|
1212
|
+
return;
|
|
1213
|
+
}
|
|
1214
|
+
const db = await this.dbPromise;
|
|
1215
|
+
db.close();
|
|
1216
|
+
this.dbPromise = null;
|
|
1217
|
+
}
|
|
1218
|
+
async getDb() {
|
|
1219
|
+
if (this.dbPromise) {
|
|
1220
|
+
return this.dbPromise;
|
|
1221
|
+
}
|
|
1222
|
+
this.dbPromise = this.initialize();
|
|
1223
|
+
return this.dbPromise;
|
|
1224
|
+
}
|
|
1225
|
+
async initialize() {
|
|
1226
|
+
const module = await import("sqlite").catch((error) => {
|
|
1227
|
+
throw new UnsupportedFeatureError(
|
|
1228
|
+
`SQLite store requires node:sqlite (Node.js 22+). Original error: ${error instanceof Error ? error.message : String(error)}`
|
|
1229
|
+
);
|
|
1230
|
+
});
|
|
1231
|
+
const db = new module.DatabaseSync(this.dbPath);
|
|
1232
|
+
db.exec(`
|
|
1233
|
+
CREATE TABLE IF NOT EXISTS linear_thenvoi_session_rooms (
|
|
1234
|
+
linear_session_id TEXT PRIMARY KEY,
|
|
1235
|
+
linear_issue_id TEXT,
|
|
1236
|
+
thenvoi_room_id TEXT NOT NULL,
|
|
1237
|
+
status TEXT NOT NULL,
|
|
1238
|
+
last_event_key TEXT,
|
|
1239
|
+
created_at TEXT NOT NULL,
|
|
1240
|
+
updated_at TEXT NOT NULL
|
|
1241
|
+
);
|
|
1242
|
+
|
|
1243
|
+
CREATE INDEX IF NOT EXISTS idx_linear_thenvoi_session_rooms_issue_active
|
|
1244
|
+
ON linear_thenvoi_session_rooms (linear_issue_id, status, updated_at);
|
|
1245
|
+
|
|
1246
|
+
CREATE TABLE IF NOT EXISTS linear_thenvoi_bootstrap_requests (
|
|
1247
|
+
event_key TEXT PRIMARY KEY,
|
|
1248
|
+
linear_session_id TEXT NOT NULL,
|
|
1249
|
+
thenvoi_room_id TEXT NOT NULL,
|
|
1250
|
+
expected_content TEXT NOT NULL,
|
|
1251
|
+
message_type TEXT NOT NULL,
|
|
1252
|
+
metadata_json TEXT,
|
|
1253
|
+
created_at TEXT NOT NULL,
|
|
1254
|
+
expires_at TEXT NOT NULL,
|
|
1255
|
+
processed_at TEXT
|
|
1256
|
+
);
|
|
1257
|
+
|
|
1258
|
+
CREATE INDEX IF NOT EXISTS idx_linear_thenvoi_bootstrap_requests_pending
|
|
1259
|
+
ON linear_thenvoi_bootstrap_requests (processed_at, expires_at, created_at);
|
|
1260
|
+
`);
|
|
1261
|
+
try {
|
|
1262
|
+
db.exec(`
|
|
1263
|
+
ALTER TABLE linear_thenvoi_session_rooms
|
|
1264
|
+
ADD COLUMN last_event_key TEXT
|
|
1265
|
+
`);
|
|
1266
|
+
} catch (error) {
|
|
1267
|
+
if (!isDuplicateColumnError(error)) {
|
|
1268
|
+
throw error;
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
try {
|
|
1272
|
+
db.exec(`
|
|
1273
|
+
ALTER TABLE linear_thenvoi_bootstrap_requests
|
|
1274
|
+
ADD COLUMN message_type TEXT NOT NULL DEFAULT 'task'
|
|
1275
|
+
`);
|
|
1276
|
+
} catch (error) {
|
|
1277
|
+
if (!isDuplicateColumnError(error)) {
|
|
1278
|
+
throw error;
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
try {
|
|
1282
|
+
db.exec(`
|
|
1283
|
+
ALTER TABLE linear_thenvoi_bootstrap_requests
|
|
1284
|
+
ADD COLUMN metadata_json TEXT
|
|
1285
|
+
`);
|
|
1286
|
+
} catch (error) {
|
|
1287
|
+
if (!isDuplicateColumnError(error)) {
|
|
1288
|
+
throw error;
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
return db;
|
|
1292
|
+
}
|
|
1293
|
+
toRecord(row) {
|
|
1294
|
+
return {
|
|
1295
|
+
linearSessionId: row.linear_session_id,
|
|
1296
|
+
linearIssueId: row.linear_issue_id,
|
|
1297
|
+
thenvoiRoomId: row.thenvoi_room_id,
|
|
1298
|
+
status: row.status,
|
|
1299
|
+
lastEventKey: row.last_event_key,
|
|
1300
|
+
createdAt: row.created_at,
|
|
1301
|
+
updatedAt: row.updated_at
|
|
1302
|
+
};
|
|
1303
|
+
}
|
|
1304
|
+
parseMetadata(metadataJson) {
|
|
1305
|
+
if (!metadataJson) {
|
|
1306
|
+
return void 0;
|
|
1307
|
+
}
|
|
1308
|
+
try {
|
|
1309
|
+
const parsed = JSON.parse(metadataJson);
|
|
1310
|
+
return asRecord(parsed) ?? void 0;
|
|
1311
|
+
} catch (error) {
|
|
1312
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
1313
|
+
console.warn(`SqliteSessionRoomStore: failed to parse metadata_json: ${msg}`);
|
|
1314
|
+
return void 0;
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
};
|
|
1318
|
+
function asRecord(value) {
|
|
1319
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
1320
|
+
return null;
|
|
1321
|
+
}
|
|
1322
|
+
return value;
|
|
1323
|
+
}
|
|
1324
|
+
function isDuplicateColumnError(error) {
|
|
1325
|
+
if (!(error instanceof Error)) {
|
|
1326
|
+
return false;
|
|
1327
|
+
}
|
|
1328
|
+
return /duplicate column name/i.test(error.message);
|
|
1329
|
+
}
|
|
1330
|
+
function asString(value) {
|
|
1331
|
+
return typeof value === "string" ? value : null;
|
|
1332
|
+
}
|
|
1333
|
+
function asNullableString(value) {
|
|
1334
|
+
if (value === null) {
|
|
1335
|
+
return null;
|
|
1336
|
+
}
|
|
1337
|
+
if (value === void 0) {
|
|
1338
|
+
return void 0;
|
|
1339
|
+
}
|
|
1340
|
+
return asString(value);
|
|
1341
|
+
}
|
|
1342
|
+
function isSessionStatus(value) {
|
|
1343
|
+
return SESSION_STATUSES.has(value);
|
|
1344
|
+
}
|
|
1345
|
+
function parseSessionRoomRow(value) {
|
|
1346
|
+
const row = asRecord(value);
|
|
1347
|
+
if (!row) {
|
|
1348
|
+
return null;
|
|
1349
|
+
}
|
|
1350
|
+
const linearSessionId = asString(row.linear_session_id);
|
|
1351
|
+
const linearIssueId = asNullableString(row.linear_issue_id);
|
|
1352
|
+
const thenvoiRoomId = asString(row.thenvoi_room_id);
|
|
1353
|
+
const status = asString(row.status);
|
|
1354
|
+
const lastEventKey = asNullableString(row.last_event_key);
|
|
1355
|
+
const createdAt = asString(row.created_at);
|
|
1356
|
+
const updatedAt = asString(row.updated_at);
|
|
1357
|
+
if (!linearSessionId || linearIssueId === void 0 || !thenvoiRoomId || !status || !isSessionStatus(status) || lastEventKey === void 0 || !createdAt || !updatedAt) {
|
|
1358
|
+
return null;
|
|
1359
|
+
}
|
|
1360
|
+
return {
|
|
1361
|
+
linear_session_id: linearSessionId,
|
|
1362
|
+
linear_issue_id: linearIssueId,
|
|
1363
|
+
thenvoi_room_id: thenvoiRoomId,
|
|
1364
|
+
status,
|
|
1365
|
+
last_event_key: lastEventKey,
|
|
1366
|
+
created_at: createdAt,
|
|
1367
|
+
updated_at: updatedAt
|
|
1368
|
+
};
|
|
1369
|
+
}
|
|
1370
|
+
function parseBootstrapRequestRows(value) {
|
|
1371
|
+
if (!Array.isArray(value)) {
|
|
1372
|
+
return [];
|
|
1373
|
+
}
|
|
1374
|
+
return value.map((entry) => parseBootstrapRequestRow(entry)).filter((entry) => entry !== null);
|
|
1375
|
+
}
|
|
1376
|
+
function parseBootstrapRequestRow(value) {
|
|
1377
|
+
const row = asRecord(value);
|
|
1378
|
+
if (!row) {
|
|
1379
|
+
return null;
|
|
1380
|
+
}
|
|
1381
|
+
const eventKey = asString(row.event_key);
|
|
1382
|
+
const linearSessionId = asString(row.linear_session_id);
|
|
1383
|
+
const thenvoiRoomId = asString(row.thenvoi_room_id);
|
|
1384
|
+
const expectedContent = asString(row.expected_content);
|
|
1385
|
+
const messageType = asString(row.message_type);
|
|
1386
|
+
const metadataJson = asNullableString(row.metadata_json);
|
|
1387
|
+
const createdAt = asString(row.created_at);
|
|
1388
|
+
const expiresAt = asString(row.expires_at);
|
|
1389
|
+
const processedAt = asNullableString(row.processed_at);
|
|
1390
|
+
if (!eventKey || !linearSessionId || !thenvoiRoomId || !expectedContent || !messageType || metadataJson === void 0 || !createdAt || !expiresAt || processedAt === void 0) {
|
|
1391
|
+
return null;
|
|
1392
|
+
}
|
|
1393
|
+
return {
|
|
1394
|
+
event_key: eventKey,
|
|
1395
|
+
linear_session_id: linearSessionId,
|
|
1396
|
+
thenvoi_room_id: thenvoiRoomId,
|
|
1397
|
+
expected_content: expectedContent,
|
|
1398
|
+
message_type: messageType,
|
|
1399
|
+
metadata_json: metadataJson,
|
|
1400
|
+
created_at: createdAt,
|
|
1401
|
+
expires_at: expiresAt,
|
|
1402
|
+
processed_at: processedAt
|
|
1403
|
+
};
|
|
1404
|
+
}
|
|
1405
|
+
function createSqliteSessionRoomStore(dbPath) {
|
|
1406
|
+
return new SqliteSessionRoomStore(dbPath);
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
// src/integrations/linear/tools.ts
|
|
1410
|
+
import { z } from "zod";
|
|
1411
|
+
function createLinearTools(options) {
|
|
1412
|
+
const { client, store, enableElicitation = true } = options;
|
|
1413
|
+
const sessionBodySchema = z.object({
|
|
1414
|
+
session_id: z.string().describe("The Linear agent session ID"),
|
|
1415
|
+
body: z.string().describe("The message body in Markdown format")
|
|
1416
|
+
});
|
|
1417
|
+
const issueIdInputSchema = z.object({
|
|
1418
|
+
issue_id: z.string().optional().describe("The Linear issue ID (UUID) from the session context")
|
|
1419
|
+
}).passthrough();
|
|
1420
|
+
const issueCommentLimitSchema = z.number().int().min(1).max(50).optional().describe("Maximum number of recent comments to return");
|
|
1421
|
+
const requiredIssueIdSchema = issueIdInputSchema;
|
|
1422
|
+
const optionalIssueIdSchema = issueIdInputSchema;
|
|
1423
|
+
const tools = [];
|
|
1424
|
+
const addSessionBodyTool = (name, description, handler) => {
|
|
1425
|
+
tools.push({
|
|
1426
|
+
name,
|
|
1427
|
+
description,
|
|
1428
|
+
schema: sessionBodySchema,
|
|
1429
|
+
handler
|
|
1430
|
+
});
|
|
1431
|
+
};
|
|
1432
|
+
addSessionBodyTool(
|
|
1433
|
+
"linear_post_thought",
|
|
1434
|
+
"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
|
+
}
|
|
1439
|
+
);
|
|
1440
|
+
addSessionBodyTool(
|
|
1441
|
+
"linear_post_action",
|
|
1442
|
+
"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
|
+
}
|
|
1447
|
+
);
|
|
1448
|
+
addSessionBodyTool(
|
|
1449
|
+
"linear_post_error",
|
|
1450
|
+
"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
|
+
}
|
|
1455
|
+
);
|
|
1456
|
+
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);
|
|
1462
|
+
return { ok: true };
|
|
1463
|
+
}
|
|
1464
|
+
);
|
|
1465
|
+
}
|
|
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) => {
|
|
1470
|
+
await completeLinearSession({
|
|
1471
|
+
linearClient: client,
|
|
1472
|
+
agentSessionId: args.session_id,
|
|
1473
|
+
body: args.body,
|
|
1474
|
+
store
|
|
1475
|
+
});
|
|
1476
|
+
return { ok: true };
|
|
1477
|
+
}
|
|
1478
|
+
);
|
|
1479
|
+
tools.push(
|
|
1480
|
+
{
|
|
1481
|
+
name: "linear_update_plan",
|
|
1482
|
+
description: "Update the plan for the Linear agent session, showing progress on each step.",
|
|
1483
|
+
schema: z.object({
|
|
1484
|
+
session_id: z.string().describe("The Linear agent session ID"),
|
|
1485
|
+
steps: z.array(z.object({
|
|
1486
|
+
title: z.string().describe("Step title"),
|
|
1487
|
+
status: z.enum(["pending", "in_progress", "completed", "failed"]).describe("Step status")
|
|
1488
|
+
})).describe("The plan steps with their current status")
|
|
1489
|
+
}),
|
|
1490
|
+
handler: async (args) => {
|
|
1491
|
+
await updatePlan(
|
|
1492
|
+
client,
|
|
1493
|
+
args.session_id,
|
|
1494
|
+
args.steps
|
|
1495
|
+
);
|
|
1496
|
+
return { ok: true };
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
);
|
|
1500
|
+
addIssueTools({
|
|
1501
|
+
tools,
|
|
1502
|
+
client,
|
|
1503
|
+
requiredIssueIdSchema,
|
|
1504
|
+
optionalIssueIdSchema,
|
|
1505
|
+
issueCommentLimitSchema
|
|
1506
|
+
});
|
|
1507
|
+
return tools;
|
|
1508
|
+
}
|
|
1509
|
+
function addIssueTools(input) {
|
|
1510
|
+
const {
|
|
1511
|
+
tools,
|
|
1512
|
+
client,
|
|
1513
|
+
requiredIssueIdSchema,
|
|
1514
|
+
optionalIssueIdSchema,
|
|
1515
|
+
issueCommentLimitSchema
|
|
1516
|
+
} = input;
|
|
1517
|
+
tools.push(
|
|
1518
|
+
{
|
|
1519
|
+
name: "linear_get_issue",
|
|
1520
|
+
description: "Fetch the current Linear issue details using the exact issue UUID from the session context.",
|
|
1521
|
+
schema: requiredIssueIdSchema,
|
|
1522
|
+
handler: async (args) => {
|
|
1523
|
+
const issueId = resolveIssueId("linear_get_issue", args);
|
|
1524
|
+
return readIssue(client, issueId);
|
|
1525
|
+
}
|
|
1526
|
+
},
|
|
1527
|
+
{
|
|
1528
|
+
name: "linear_list_issue_comments",
|
|
1529
|
+
description: "List recent comments for the current Linear issue using the exact issue UUID from the session context.",
|
|
1530
|
+
schema: requiredIssueIdSchema.extend({ limit: issueCommentLimitSchema }),
|
|
1531
|
+
handler: async (args) => {
|
|
1532
|
+
const issueId = resolveIssueId("linear_list_issue_comments", args);
|
|
1533
|
+
return readIssueComments(client, issueId, typeof args.limit === "number" ? args.limit : 20);
|
|
1534
|
+
}
|
|
1535
|
+
},
|
|
1536
|
+
{
|
|
1537
|
+
name: "linear_list_workflow_states",
|
|
1538
|
+
description: "List workflow states for the current issue's team so the bridge can move the issue between Todo, In Progress, In Review, and Done.",
|
|
1539
|
+
schema: optionalIssueIdSchema.extend({
|
|
1540
|
+
team_id: z.string().optional().describe("The Linear team ID when already known from the issue context")
|
|
1541
|
+
}),
|
|
1542
|
+
handler: async (args) => {
|
|
1543
|
+
const issueId = resolveOptionalIssueId("linear_list_workflow_states", args);
|
|
1544
|
+
const teamId = typeof args.team_id === "string" && args.team_id.trim().length > 0 ? args.team_id : await readIssueTeamId(client, issueId);
|
|
1545
|
+
if (!teamId) {
|
|
1546
|
+
throw new Error("linear_list_workflow_states requires issue_id or team_id.");
|
|
1547
|
+
}
|
|
1548
|
+
return readWorkflowStates(client, teamId);
|
|
1549
|
+
}
|
|
1550
|
+
},
|
|
1551
|
+
{
|
|
1552
|
+
name: "linear_update_issue",
|
|
1553
|
+
description: "Update a Linear issue (title/description/state/assignee/priority/etc.) from the session workflow.",
|
|
1554
|
+
schema: requiredIssueIdSchema.extend({
|
|
1555
|
+
title: z.string().optional().describe("New issue title"),
|
|
1556
|
+
description: z.string().optional().describe("New issue description (Markdown)"),
|
|
1557
|
+
priority: z.number().int().min(0).max(4).optional().describe("Priority 0-4"),
|
|
1558
|
+
state_id: z.string().optional().describe("Workflow state ID"),
|
|
1559
|
+
assignee_id: z.string().nullable().optional().describe("Assignee user ID, or null to unassign"),
|
|
1560
|
+
estimate: z.number().int().optional().describe("Estimate points"),
|
|
1561
|
+
due_date: z.string().optional().describe("Due date ISO string")
|
|
1562
|
+
}),
|
|
1563
|
+
handler: async (args) => {
|
|
1564
|
+
if (typeof client.updateIssue !== "function") {
|
|
1565
|
+
throw new Error("linear_update_issue is unavailable: Linear client does not support updateIssue().");
|
|
1566
|
+
}
|
|
1567
|
+
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;
|
|
1569
|
+
if (!hasUpdates) {
|
|
1570
|
+
throw new Error("linear_update_issue requires at least one update field.");
|
|
1571
|
+
}
|
|
1572
|
+
await client.updateIssue(
|
|
1573
|
+
issueId,
|
|
1574
|
+
{
|
|
1575
|
+
...args.title !== void 0 ? { title: args.title } : {},
|
|
1576
|
+
...args.description !== void 0 ? { description: args.description } : {},
|
|
1577
|
+
...args.priority !== void 0 ? { priority: args.priority } : {},
|
|
1578
|
+
...args.state_id !== void 0 ? { stateId: args.state_id } : {},
|
|
1579
|
+
...args.assignee_id !== void 0 ? { assigneeId: args.assignee_id } : {},
|
|
1580
|
+
...args.estimate !== void 0 ? { estimate: args.estimate } : {},
|
|
1581
|
+
...args.due_date !== void 0 ? { dueDate: args.due_date } : {}
|
|
1582
|
+
}
|
|
1583
|
+
);
|
|
1584
|
+
return { ok: true };
|
|
1585
|
+
}
|
|
1586
|
+
},
|
|
1587
|
+
{
|
|
1588
|
+
name: "linear_add_issue_comment",
|
|
1589
|
+
description: "Add a new comment to a Linear issue.",
|
|
1590
|
+
schema: requiredIssueIdSchema.extend({
|
|
1591
|
+
body: z.string().describe("Comment body (Markdown)")
|
|
1592
|
+
}),
|
|
1593
|
+
handler: async (args) => {
|
|
1594
|
+
if (typeof client.createComment !== "function") {
|
|
1595
|
+
throw new Error("linear_add_issue_comment is unavailable: Linear client does not support createComment().");
|
|
1596
|
+
}
|
|
1597
|
+
const issueId = resolveIssueId("linear_add_issue_comment", args);
|
|
1598
|
+
await client.createComment({
|
|
1599
|
+
issueId,
|
|
1600
|
+
body: args.body
|
|
1601
|
+
});
|
|
1602
|
+
return { ok: true };
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
);
|
|
1606
|
+
}
|
|
1607
|
+
function assertUuid(toolName, value) {
|
|
1608
|
+
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
|
+
if (!uuidPattern.test(value)) {
|
|
1610
|
+
throw new Error(`${toolName} requires the exact Linear issue UUID from the session context. Received "${value}".`);
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
function resolveIssueId(toolName, args) {
|
|
1614
|
+
const issueId = resolveOptionalIssueId(toolName, args);
|
|
1615
|
+
if (!issueId) {
|
|
1616
|
+
throw new Error(`${toolName} requires issue_id.`);
|
|
1617
|
+
}
|
|
1618
|
+
return issueId;
|
|
1619
|
+
}
|
|
1620
|
+
function resolveOptionalIssueId(toolName, args) {
|
|
1621
|
+
if (typeof args.issue_id !== "string" || args.issue_id.length === 0) {
|
|
1622
|
+
return void 0;
|
|
1623
|
+
}
|
|
1624
|
+
assertUuid(toolName, args.issue_id);
|
|
1625
|
+
return args.issue_id;
|
|
1626
|
+
}
|
|
1627
|
+
async function readIssue(client, issueId) {
|
|
1628
|
+
if (typeof client.issue !== "function") {
|
|
1629
|
+
throw new Error("linear_get_issue is unavailable: Linear client does not support issue().");
|
|
1630
|
+
}
|
|
1631
|
+
const issue = await client.issue(issueId);
|
|
1632
|
+
return {
|
|
1633
|
+
issue: {
|
|
1634
|
+
id: issue.id,
|
|
1635
|
+
identifier: issue.identifier ?? null,
|
|
1636
|
+
title: issue.title ?? null,
|
|
1637
|
+
description: issue.description ?? null,
|
|
1638
|
+
url: issue.url ?? null,
|
|
1639
|
+
priority: issue.priority ?? null,
|
|
1640
|
+
estimate: issue.estimate ?? null,
|
|
1641
|
+
due_date: issue.dueDate ?? null,
|
|
1642
|
+
created_at: issue.createdAt ?? null,
|
|
1643
|
+
updated_at: issue.updatedAt ?? null,
|
|
1644
|
+
state: issue.state ? {
|
|
1645
|
+
id: issue.state.id ?? null,
|
|
1646
|
+
name: issue.state.name ?? null,
|
|
1647
|
+
type: issue.state.type ?? null
|
|
1648
|
+
} : null,
|
|
1649
|
+
assignee: issue.assignee ? {
|
|
1650
|
+
id: issue.assignee.id ?? null,
|
|
1651
|
+
name: issue.assignee.displayName ?? issue.assignee.name ?? null
|
|
1652
|
+
} : null,
|
|
1653
|
+
team: issue.team ? {
|
|
1654
|
+
id: issue.team.id ?? null,
|
|
1655
|
+
key: issue.team.key ?? null,
|
|
1656
|
+
name: issue.team.name ?? null
|
|
1657
|
+
} : null
|
|
1658
|
+
}
|
|
1659
|
+
};
|
|
1660
|
+
}
|
|
1661
|
+
async function readIssueComments(client, issueId, limit) {
|
|
1662
|
+
if (typeof client.issue !== "function") {
|
|
1663
|
+
throw new Error("linear_list_issue_comments is unavailable: Linear client does not support issue().");
|
|
1664
|
+
}
|
|
1665
|
+
const issue = await client.issue(issueId);
|
|
1666
|
+
if (typeof issue.comments !== "function") {
|
|
1667
|
+
throw new Error("linear_list_issue_comments is unavailable: issue.comments() is not supported by the Linear client.");
|
|
1668
|
+
}
|
|
1669
|
+
const response = await issue.comments();
|
|
1670
|
+
const comments = (response.nodes ?? []).slice(-limit).map((comment) => ({
|
|
1671
|
+
id: comment.id ?? null,
|
|
1672
|
+
body: comment.body ?? null,
|
|
1673
|
+
created_at: comment.createdAt ?? null,
|
|
1674
|
+
updated_at: comment.updatedAt ?? null,
|
|
1675
|
+
user: comment.user ? {
|
|
1676
|
+
id: comment.user.id ?? null,
|
|
1677
|
+
name: comment.user.displayName ?? comment.user.name ?? null
|
|
1678
|
+
} : null
|
|
1679
|
+
}));
|
|
1680
|
+
return { comments };
|
|
1681
|
+
}
|
|
1682
|
+
async function readIssueTeamId(client, issueId) {
|
|
1683
|
+
if (!issueId) {
|
|
1684
|
+
return null;
|
|
1685
|
+
}
|
|
1686
|
+
if (typeof client.issue !== "function") {
|
|
1687
|
+
throw new Error("linear_list_workflow_states is unavailable: Linear client does not support issue().");
|
|
1688
|
+
}
|
|
1689
|
+
const issue = await client.issue(issueId);
|
|
1690
|
+
return issue.teamId ?? issue.team?.id ?? null;
|
|
1691
|
+
}
|
|
1692
|
+
async function readWorkflowStates(client, teamId) {
|
|
1693
|
+
if (typeof client.workflowStates !== "function") {
|
|
1694
|
+
throw new Error(
|
|
1695
|
+
"linear_list_workflow_states is unavailable: Linear client does not support workflowStates()."
|
|
1696
|
+
);
|
|
1697
|
+
}
|
|
1698
|
+
const response = await client.workflowStates({ teamId });
|
|
1699
|
+
const states = (response.nodes ?? []).map((state) => ({
|
|
1700
|
+
id: state.id ?? null,
|
|
1701
|
+
name: state.name ?? null,
|
|
1702
|
+
type: state.type ?? null,
|
|
1703
|
+
position: state.position ?? null,
|
|
1704
|
+
team_id: state.teamId ?? teamId
|
|
1705
|
+
})).sort((left, right) => (left.position ?? Number.MAX_SAFE_INTEGER) - (right.position ?? Number.MAX_SAFE_INTEGER));
|
|
1706
|
+
return { team_id: teamId, states };
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
// src/integrations/linear/webhook.ts
|
|
1710
|
+
import {
|
|
1711
|
+
LinearWebhookClient,
|
|
1712
|
+
LINEAR_WEBHOOK_SIGNATURE_HEADER,
|
|
1713
|
+
LINEAR_WEBHOOK_TS_HEADER
|
|
1714
|
+
} from "@linear/sdk/webhooks";
|
|
1715
|
+
var DISPATCH_RETRY_LIMIT = 2;
|
|
1716
|
+
var DISPATCH_RETRY_BASE_DELAY_MS = 1e3;
|
|
1717
|
+
function createInlineLinearBridgeDispatcher(options) {
|
|
1718
|
+
const logger = options?.logger ?? new NoopLogger();
|
|
1719
|
+
const runtime = createLinearBridgeRuntime();
|
|
1720
|
+
return {
|
|
1721
|
+
dispatch: async (job) => {
|
|
1722
|
+
const result = await runDispatchAttempt(async () => {
|
|
1723
|
+
await handleAgentSessionEvent(job.input, {
|
|
1724
|
+
skipAcknowledgment: true,
|
|
1725
|
+
expectedEventKey: job.eventKey,
|
|
1726
|
+
runtime: job.runtime ?? runtime
|
|
1727
|
+
});
|
|
1728
|
+
}, {
|
|
1729
|
+
logger,
|
|
1730
|
+
failureEvent: "linear_thenvoi_bridge.dispatch_failed",
|
|
1731
|
+
retryEvent: "linear_thenvoi_bridge.dispatch_retrying",
|
|
1732
|
+
eventKey: job.eventKey,
|
|
1733
|
+
sessionId: job.input.payload.agentSession.id
|
|
1734
|
+
});
|
|
1735
|
+
if (result.ok) {
|
|
1736
|
+
await markBootstrapHandoffProcessed({
|
|
1737
|
+
eventKey: job.eventKey,
|
|
1738
|
+
store: job.input.deps.store,
|
|
1739
|
+
logger,
|
|
1740
|
+
sessionId: job.input.payload.agentSession.id
|
|
1741
|
+
});
|
|
1742
|
+
return;
|
|
1743
|
+
}
|
|
1744
|
+
const failure = {
|
|
1745
|
+
eventKey: job.eventKey,
|
|
1746
|
+
sessionId: job.input.payload.agentSession.id,
|
|
1747
|
+
attempts: result.attempts,
|
|
1748
|
+
error: result.error
|
|
1749
|
+
};
|
|
1750
|
+
await signalTerminalDispatchFailure({
|
|
1751
|
+
logger,
|
|
1752
|
+
job,
|
|
1753
|
+
failure
|
|
1754
|
+
});
|
|
1755
|
+
throw result.error;
|
|
1756
|
+
}
|
|
1757
|
+
};
|
|
1758
|
+
}
|
|
1759
|
+
function createInProcessLinearBridgeDispatcher(options) {
|
|
1760
|
+
const logger = options?.logger ?? new NoopLogger();
|
|
1761
|
+
const runtime = createLinearBridgeRuntime();
|
|
1762
|
+
const queued = /* @__PURE__ */ new Set();
|
|
1763
|
+
const inFlight = /* @__PURE__ */ new Set();
|
|
1764
|
+
const terminalFailures = [];
|
|
1765
|
+
return {
|
|
1766
|
+
isQueued: (eventKey) => queued.has(eventKey),
|
|
1767
|
+
waitForIdle: async () => {
|
|
1768
|
+
let snapshot = [...inFlight];
|
|
1769
|
+
while (snapshot.length > 0) {
|
|
1770
|
+
await Promise.allSettled(snapshot);
|
|
1771
|
+
snapshot = [...inFlight];
|
|
1772
|
+
}
|
|
1773
|
+
if (terminalFailures.length === 0) {
|
|
1774
|
+
return;
|
|
1775
|
+
}
|
|
1776
|
+
const failures = terminalFailures.splice(0, terminalFailures.length);
|
|
1777
|
+
throw new AggregateError(
|
|
1778
|
+
failures.map((failure) => toDispatchFailureError(failure)),
|
|
1779
|
+
`Linear bridge async dispatch failed for ${failures.length} event(s)`
|
|
1780
|
+
);
|
|
1781
|
+
},
|
|
1782
|
+
dispatch: async (job) => {
|
|
1783
|
+
if (queued.has(job.eventKey)) {
|
|
1784
|
+
return;
|
|
1785
|
+
}
|
|
1786
|
+
queued.add(job.eventKey);
|
|
1787
|
+
const dispatchTask = (async () => {
|
|
1788
|
+
await new Promise((resolveMicrotask) => {
|
|
1789
|
+
queueMicrotask(() => resolveMicrotask());
|
|
1790
|
+
});
|
|
1791
|
+
const result = await runDispatchAttempt(async () => {
|
|
1792
|
+
await handleAgentSessionEvent(job.input, {
|
|
1793
|
+
skipAcknowledgment: true,
|
|
1794
|
+
expectedEventKey: job.eventKey,
|
|
1795
|
+
runtime: job.runtime ?? runtime
|
|
1796
|
+
});
|
|
1797
|
+
}, {
|
|
1798
|
+
logger,
|
|
1799
|
+
failureEvent: "linear_thenvoi_bridge.async_dispatch_failed",
|
|
1800
|
+
retryEvent: "linear_thenvoi_bridge.async_dispatch_retrying",
|
|
1801
|
+
eventKey: job.eventKey,
|
|
1802
|
+
sessionId: job.input.payload.agentSession.id
|
|
1803
|
+
});
|
|
1804
|
+
if (result.ok) {
|
|
1805
|
+
await markBootstrapHandoffProcessed({
|
|
1806
|
+
eventKey: job.eventKey,
|
|
1807
|
+
store: job.input.deps.store,
|
|
1808
|
+
logger,
|
|
1809
|
+
sessionId: job.input.payload.agentSession.id
|
|
1810
|
+
});
|
|
1811
|
+
return;
|
|
1812
|
+
}
|
|
1813
|
+
const failure = {
|
|
1814
|
+
eventKey: job.eventKey,
|
|
1815
|
+
sessionId: job.input.payload.agentSession.id,
|
|
1816
|
+
attempts: result.attempts,
|
|
1817
|
+
error: result.error
|
|
1818
|
+
};
|
|
1819
|
+
terminalFailures.push(failure);
|
|
1820
|
+
await signalTerminalDispatchFailure({
|
|
1821
|
+
logger,
|
|
1822
|
+
job,
|
|
1823
|
+
failure
|
|
1824
|
+
});
|
|
1825
|
+
})();
|
|
1826
|
+
inFlight.add(dispatchTask);
|
|
1827
|
+
void dispatchTask.catch((error) => {
|
|
1828
|
+
logger.error("linear_thenvoi_bridge.async_dispatch_failure_signal_crashed", {
|
|
1829
|
+
eventKey: job.eventKey,
|
|
1830
|
+
sessionId: job.input.payload.agentSession.id,
|
|
1831
|
+
error: serializeError(error)
|
|
1832
|
+
});
|
|
1833
|
+
}).finally(() => {
|
|
1834
|
+
queued.delete(job.eventKey);
|
|
1835
|
+
inFlight.delete(dispatchTask);
|
|
1836
|
+
});
|
|
1837
|
+
await dispatchTask;
|
|
1838
|
+
}
|
|
1839
|
+
};
|
|
1840
|
+
}
|
|
1841
|
+
function createLinearWebhookHandler(options) {
|
|
1842
|
+
const logger = options.deps.logger ?? new NoopLogger();
|
|
1843
|
+
const runtime = createLinearBridgeRuntime();
|
|
1844
|
+
const dispatcher = options.dispatcher ?? createInlineLinearBridgeDispatcher({ logger });
|
|
1845
|
+
const webhookClient = new LinearWebhookClient(options.config.linearWebhookSecret);
|
|
1846
|
+
const inFlightEventKeys = /* @__PURE__ */ new Set();
|
|
1847
|
+
return async (request, response) => {
|
|
1848
|
+
if (request.method !== "POST") {
|
|
1849
|
+
logger.warn("linear_thenvoi_bridge.webhook_invalid_method", {
|
|
1850
|
+
method: request.method ?? null
|
|
1851
|
+
});
|
|
1852
|
+
sendText(response, 405, "Method not allowed");
|
|
1853
|
+
return;
|
|
1854
|
+
}
|
|
1855
|
+
const signature = getHeaderValue(request, LINEAR_WEBHOOK_SIGNATURE_HEADER);
|
|
1856
|
+
if (!signature) {
|
|
1857
|
+
logger.warn("linear_thenvoi_bridge.webhook_missing_signature", {});
|
|
1858
|
+
sendText(response, 400, "Missing webhook signature");
|
|
1859
|
+
return;
|
|
1860
|
+
}
|
|
1861
|
+
const timestamp = getHeaderValue(request, LINEAR_WEBHOOK_TS_HEADER);
|
|
1862
|
+
if (!timestamp) {
|
|
1863
|
+
logger.warn("linear_thenvoi_bridge.webhook_missing_timestamp", {});
|
|
1864
|
+
sendText(response, 400, "Missing webhook timestamp");
|
|
1865
|
+
return;
|
|
1866
|
+
}
|
|
1867
|
+
const rawBody = await readRawBody(request);
|
|
1868
|
+
let payload;
|
|
1869
|
+
try {
|
|
1870
|
+
payload = webhookClient.parseData(
|
|
1871
|
+
rawBody,
|
|
1872
|
+
signature,
|
|
1873
|
+
timestamp
|
|
1874
|
+
);
|
|
1875
|
+
} catch (error) {
|
|
1876
|
+
logger.warn("linear_thenvoi_bridge.webhook_invalid_signature", {
|
|
1877
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1878
|
+
});
|
|
1879
|
+
sendText(response, 400, "Invalid webhook");
|
|
1880
|
+
return;
|
|
1881
|
+
}
|
|
1882
|
+
if (payload.type !== "AgentSessionEvent") {
|
|
1883
|
+
logger.info("linear_thenvoi_bridge.webhook_ignored_event", {
|
|
1884
|
+
type: payload.type
|
|
1885
|
+
});
|
|
1886
|
+
sendText(response, 200, "OK");
|
|
1887
|
+
return;
|
|
1888
|
+
}
|
|
1889
|
+
const eventKey = getAgentSessionEventKey(payload);
|
|
1890
|
+
logger.info("linear_thenvoi_bridge.webhook_received", {
|
|
1891
|
+
sessionId: payload.agentSession.id,
|
|
1892
|
+
issueId: payload.agentSession.issue?.id ?? null,
|
|
1893
|
+
action: payload.action,
|
|
1894
|
+
eventKey
|
|
1895
|
+
});
|
|
1896
|
+
const existing = await options.deps.store.getBySessionId(payload.agentSession.id);
|
|
1897
|
+
const alreadyInFlight = inFlightEventKeys.has(eventKey);
|
|
1898
|
+
if (existing?.lastEventKey === eventKey || alreadyInFlight || dispatcher.isQueued?.(eventKey)) {
|
|
1899
|
+
logger.info("linear_thenvoi_bridge.webhook_duplicate_ignored", {
|
|
1900
|
+
sessionId: payload.agentSession.id,
|
|
1901
|
+
eventKey
|
|
1902
|
+
});
|
|
1903
|
+
sendText(response, 200, "OK");
|
|
1904
|
+
return;
|
|
1905
|
+
}
|
|
1906
|
+
inFlightEventKeys.add(eventKey);
|
|
1907
|
+
try {
|
|
1908
|
+
if (normalizeAction2(payload.action) === "created") {
|
|
1909
|
+
try {
|
|
1910
|
+
await postThought(
|
|
1911
|
+
options.deps.linearClient,
|
|
1912
|
+
payload.agentSession.id,
|
|
1913
|
+
"Received session. Setting up workspace..."
|
|
1914
|
+
);
|
|
1915
|
+
} catch (error) {
|
|
1916
|
+
logger.warn("linear_thenvoi_bridge.webhook_acknowledgment_failed", {
|
|
1917
|
+
sessionId: payload.agentSession.id,
|
|
1918
|
+
error
|
|
1919
|
+
});
|
|
1920
|
+
}
|
|
1921
|
+
}
|
|
1922
|
+
await dispatcher.dispatch({
|
|
1923
|
+
eventKey,
|
|
1924
|
+
runtime,
|
|
1925
|
+
input: {
|
|
1926
|
+
payload,
|
|
1927
|
+
config: options.config,
|
|
1928
|
+
deps: options.deps
|
|
1929
|
+
}
|
|
1930
|
+
});
|
|
1931
|
+
} catch (error) {
|
|
1932
|
+
logger.error("linear_thenvoi_bridge.webhook_dispatch_failed", {
|
|
1933
|
+
sessionId: payload.agentSession.id,
|
|
1934
|
+
issueId: payload.agentSession.issue?.id ?? null,
|
|
1935
|
+
action: payload.action,
|
|
1936
|
+
eventKey,
|
|
1937
|
+
error: serializeError(error)
|
|
1938
|
+
});
|
|
1939
|
+
sendText(response, isRetryableDispatchError(error) ? 503 : 500, "Dispatch failed");
|
|
1940
|
+
return;
|
|
1941
|
+
} finally {
|
|
1942
|
+
inFlightEventKeys.delete(eventKey);
|
|
1943
|
+
}
|
|
1944
|
+
logger.info("linear_thenvoi_bridge.webhook_dispatched", {
|
|
1945
|
+
sessionId: payload.agentSession.id,
|
|
1946
|
+
issueId: payload.agentSession.issue?.id ?? null,
|
|
1947
|
+
action: payload.action,
|
|
1948
|
+
eventKey
|
|
1949
|
+
});
|
|
1950
|
+
sendText(response, 200, "OK");
|
|
1951
|
+
};
|
|
1952
|
+
}
|
|
1953
|
+
function normalizeAction2(action) {
|
|
1954
|
+
return typeof action === "string" ? action.trim().toLowerCase() : "";
|
|
1955
|
+
}
|
|
1956
|
+
function getHeaderValue(request, name) {
|
|
1957
|
+
const value = request.headers[name];
|
|
1958
|
+
if (Array.isArray(value)) {
|
|
1959
|
+
return value[0] ?? null;
|
|
1960
|
+
}
|
|
1961
|
+
return typeof value === "string" ? value : null;
|
|
1962
|
+
}
|
|
1963
|
+
async function readRawBody(request) {
|
|
1964
|
+
if (Buffer.isBuffer(request.body)) {
|
|
1965
|
+
return request.body;
|
|
1966
|
+
}
|
|
1967
|
+
if (typeof request.body === "string") {
|
|
1968
|
+
return Buffer.from(request.body);
|
|
1969
|
+
}
|
|
1970
|
+
if (request.body instanceof Uint8Array) {
|
|
1971
|
+
return Buffer.from(request.body);
|
|
1972
|
+
}
|
|
1973
|
+
const chunks = [];
|
|
1974
|
+
for await (const chunk of request) {
|
|
1975
|
+
if (typeof chunk === "string") {
|
|
1976
|
+
chunks.push(Buffer.from(chunk));
|
|
1977
|
+
} else {
|
|
1978
|
+
chunks.push(Buffer.from(chunk));
|
|
1979
|
+
}
|
|
1980
|
+
}
|
|
1981
|
+
return Buffer.concat(chunks);
|
|
1982
|
+
}
|
|
1983
|
+
function sendText(response, statusCode, body) {
|
|
1984
|
+
if (response.headersSent) {
|
|
1985
|
+
return;
|
|
1986
|
+
}
|
|
1987
|
+
response.statusCode = statusCode;
|
|
1988
|
+
response.end(body);
|
|
1989
|
+
}
|
|
1990
|
+
async function runDispatchAttempt(task, context) {
|
|
1991
|
+
const retryLimit = context.retryLimit ?? DISPATCH_RETRY_LIMIT;
|
|
1992
|
+
const retryBaseDelayMs = context.retryBaseDelayMs ?? DISPATCH_RETRY_BASE_DELAY_MS;
|
|
1993
|
+
let attempt = 0;
|
|
1994
|
+
while (true) {
|
|
1995
|
+
try {
|
|
1996
|
+
await task();
|
|
1997
|
+
return { ok: true };
|
|
1998
|
+
} catch (error) {
|
|
1999
|
+
if (!isRetryableDispatchError(error) || attempt >= retryLimit) {
|
|
2000
|
+
context.logger.error(context.failureEvent, {
|
|
2001
|
+
eventKey: context.eventKey,
|
|
2002
|
+
sessionId: context.sessionId,
|
|
2003
|
+
error: serializeError(error)
|
|
2004
|
+
});
|
|
2005
|
+
return {
|
|
2006
|
+
ok: false,
|
|
2007
|
+
attempts: attempt + 1,
|
|
2008
|
+
error
|
|
2009
|
+
};
|
|
2010
|
+
}
|
|
2011
|
+
attempt += 1;
|
|
2012
|
+
context.logger.warn(context.retryEvent, {
|
|
2013
|
+
eventKey: context.eventKey,
|
|
2014
|
+
sessionId: context.sessionId,
|
|
2015
|
+
attempt,
|
|
2016
|
+
delayMs: retryBaseDelayMs * attempt,
|
|
2017
|
+
error: serializeError(error)
|
|
2018
|
+
});
|
|
2019
|
+
await sleep2(retryBaseDelayMs * attempt);
|
|
2020
|
+
}
|
|
2021
|
+
}
|
|
2022
|
+
}
|
|
2023
|
+
async function markBootstrapHandoffProcessed(input) {
|
|
2024
|
+
try {
|
|
2025
|
+
await input.store.markBootstrapRequestProcessed(input.eventKey);
|
|
2026
|
+
} catch (error) {
|
|
2027
|
+
input.logger.warn("linear_thenvoi_bridge.bootstrap_handoff_mark_processed_failed", {
|
|
2028
|
+
eventKey: input.eventKey,
|
|
2029
|
+
sessionId: input.sessionId,
|
|
2030
|
+
error: serializeError(error)
|
|
2031
|
+
});
|
|
2032
|
+
}
|
|
2033
|
+
}
|
|
2034
|
+
async function signalTerminalDispatchFailure(input) {
|
|
2035
|
+
const { logger, job, failure } = input;
|
|
2036
|
+
try {
|
|
2037
|
+
const existing = await job.input.deps.store.getBySessionId(failure.sessionId);
|
|
2038
|
+
if (existing) {
|
|
2039
|
+
await job.input.deps.store.upsert({
|
|
2040
|
+
...existing,
|
|
2041
|
+
status: "errored",
|
|
2042
|
+
lastEventKey: job.eventKey,
|
|
2043
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2044
|
+
});
|
|
2045
|
+
logger.warn("linear_thenvoi_bridge.async_dispatch_terminal_failure_signaled", {
|
|
2046
|
+
eventKey: failure.eventKey,
|
|
2047
|
+
sessionId: failure.sessionId,
|
|
2048
|
+
attempts: failure.attempts,
|
|
2049
|
+
signal: "session_store_status_errored"
|
|
2050
|
+
});
|
|
2051
|
+
return;
|
|
2052
|
+
}
|
|
2053
|
+
} catch (storeError) {
|
|
2054
|
+
logger.error("linear_thenvoi_bridge.async_dispatch_terminal_failure_store_signal_failed", {
|
|
2055
|
+
eventKey: failure.eventKey,
|
|
2056
|
+
sessionId: failure.sessionId,
|
|
2057
|
+
attempts: failure.attempts,
|
|
2058
|
+
dispatchError: serializeError(failure.error),
|
|
2059
|
+
signalError: serializeError(storeError)
|
|
2060
|
+
});
|
|
2061
|
+
}
|
|
2062
|
+
try {
|
|
2063
|
+
await postError(
|
|
2064
|
+
job.input.deps.linearClient,
|
|
2065
|
+
failure.sessionId,
|
|
2066
|
+
"Bridge dispatch failed and could not recover automatically. Retry this session event in Linear."
|
|
2067
|
+
);
|
|
2068
|
+
logger.warn("linear_thenvoi_bridge.async_dispatch_terminal_failure_signaled", {
|
|
2069
|
+
eventKey: failure.eventKey,
|
|
2070
|
+
sessionId: failure.sessionId,
|
|
2071
|
+
attempts: failure.attempts,
|
|
2072
|
+
signal: "linear_activity_error"
|
|
2073
|
+
});
|
|
2074
|
+
} catch (signalError) {
|
|
2075
|
+
logger.error("linear_thenvoi_bridge.async_dispatch_terminal_failure_signal_failed", {
|
|
2076
|
+
eventKey: failure.eventKey,
|
|
2077
|
+
sessionId: failure.sessionId,
|
|
2078
|
+
attempts: failure.attempts,
|
|
2079
|
+
dispatchError: serializeError(failure.error),
|
|
2080
|
+
signalError: serializeError(signalError)
|
|
2081
|
+
});
|
|
2082
|
+
}
|
|
2083
|
+
}
|
|
2084
|
+
function isRetryableDispatchError(error) {
|
|
2085
|
+
return typeof error === "object" && error !== null && "retryable" in error && error.retryable === true;
|
|
2086
|
+
}
|
|
2087
|
+
async function sleep2(ms) {
|
|
2088
|
+
await new Promise((resolveDelay) => setTimeout(resolveDelay, ms));
|
|
2089
|
+
}
|
|
2090
|
+
function serializeError(error) {
|
|
2091
|
+
if (error instanceof Error) {
|
|
2092
|
+
return {
|
|
2093
|
+
name: error.name,
|
|
2094
|
+
message: error.message,
|
|
2095
|
+
stack: error.stack,
|
|
2096
|
+
...typeof error.retryable !== "undefined" ? { retryable: error.retryable } : {}
|
|
2097
|
+
};
|
|
2098
|
+
}
|
|
2099
|
+
return {
|
|
2100
|
+
message: String(error)
|
|
2101
|
+
};
|
|
2102
|
+
}
|
|
2103
|
+
function toDispatchFailureError(failure) {
|
|
2104
|
+
return new Error(
|
|
2105
|
+
`Linear bridge async dispatch failed for event ${failure.eventKey} (session ${failure.sessionId})`,
|
|
2106
|
+
{ cause: failure.error }
|
|
2107
|
+
);
|
|
2108
|
+
}
|
|
2109
|
+
|
|
2110
|
+
// src/integrations/linear/constants.ts
|
|
2111
|
+
var DEFAULT_STATUS_MAPPING = {
|
|
2112
|
+
active: "active",
|
|
2113
|
+
waiting: "waiting",
|
|
2114
|
+
canceled: "canceled",
|
|
2115
|
+
completed: "completed",
|
|
2116
|
+
errored: "errored"
|
|
2117
|
+
};
|
|
2118
|
+
export {
|
|
2119
|
+
DEFAULT_STATUS_MAPPING,
|
|
2120
|
+
buildLinearAuthorizationHeader,
|
|
2121
|
+
completeLinearSession,
|
|
2122
|
+
createInProcessLinearBridgeDispatcher,
|
|
2123
|
+
createInlineLinearBridgeDispatcher,
|
|
2124
|
+
createLinearBridgeRuntime,
|
|
2125
|
+
createLinearClient,
|
|
2126
|
+
createLinearTools,
|
|
2127
|
+
createLinearWebhookHandler,
|
|
2128
|
+
createSqliteSessionRoomStore,
|
|
2129
|
+
dedupeHandles,
|
|
2130
|
+
handleAgentSessionEvent,
|
|
2131
|
+
isLinearApiKey,
|
|
2132
|
+
postAction,
|
|
2133
|
+
postElicitation,
|
|
2134
|
+
postError,
|
|
2135
|
+
postResponse,
|
|
2136
|
+
postThought,
|
|
2137
|
+
stripHandlePrefix,
|
|
2138
|
+
updatePlan
|
|
2139
|
+
};
|