@calltelemetry/openclaw-linear 0.8.1 → 0.8.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/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/src/pipeline/webhook.ts +18 -150
package/openclaw.plugin.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"id": "openclaw-linear",
|
|
3
3
|
"name": "Linear Agent",
|
|
4
4
|
"description": "Linear integration with OAuth support, agent pipeline, and webhook-driven AI agent lifecycle",
|
|
5
|
-
"version": "0.8.
|
|
5
|
+
"version": "0.8.2",
|
|
6
6
|
"configSchema": {
|
|
7
7
|
"type": "object",
|
|
8
8
|
"additionalProperties": false,
|
package/package.json
CHANGED
package/src/pipeline/webhook.ts
CHANGED
|
@@ -155,158 +155,14 @@ export async function handleLinearWebhook(
|
|
|
155
155
|
emitDiagnostic(api, { event: "webhook_received", webhookType: payload.type, webhookAction: payload.action });
|
|
156
156
|
|
|
157
157
|
|
|
158
|
-
// ── AppUserNotification —
|
|
158
|
+
// ── AppUserNotification — IGNORED ─────────────────────────────────
|
|
159
|
+
// AppUserNotification duplicates events already handled by the workspace
|
|
160
|
+
// webhook (Comment.create for mentions, Issue.update for assignments).
|
|
161
|
+
// Processing both causes double agent runs. Ack and discard.
|
|
159
162
|
if (payload.type === "AppUserNotification") {
|
|
163
|
+
api.logger.info(`AppUserNotification ignored (duplicate of workspace webhook): ${payload.notification?.type} appUserId=${payload.appUserId}`);
|
|
160
164
|
res.statusCode = 200;
|
|
161
165
|
res.end("ok");
|
|
162
|
-
|
|
163
|
-
const notification = payload.notification;
|
|
164
|
-
const notifType = notification?.type;
|
|
165
|
-
api.logger.info(`AppUserNotification: ${notifType} appUserId=${payload.appUserId}`);
|
|
166
|
-
|
|
167
|
-
const issue = notification?.issue;
|
|
168
|
-
const comment = notification?.comment ?? notification?.parentComment;
|
|
169
|
-
|
|
170
|
-
if (!issue?.id) {
|
|
171
|
-
api.logger.error("AppUserNotification missing issue data");
|
|
172
|
-
return true;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
const linearApi = createLinearApi(api);
|
|
176
|
-
if (!linearApi) {
|
|
177
|
-
api.logger.error("No Linear access token — cannot process agent notification");
|
|
178
|
-
return true;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
const agentId = resolveAgentId(api);
|
|
182
|
-
|
|
183
|
-
// Fetch full issue details
|
|
184
|
-
let enrichedIssue: any = issue;
|
|
185
|
-
try {
|
|
186
|
-
enrichedIssue = await linearApi.getIssueDetails(issue.id);
|
|
187
|
-
} catch (err) {
|
|
188
|
-
api.logger.warn(`Could not fetch issue details: ${err}`);
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
const description = enrichedIssue?.description ?? issue?.description ?? "(no description)";
|
|
192
|
-
const comments = enrichedIssue?.comments?.nodes ?? [];
|
|
193
|
-
const commentSummary = comments
|
|
194
|
-
.slice(-5)
|
|
195
|
-
.map((c: any) => ` - **${c.user?.name ?? "Unknown"}**: ${c.body?.slice(0, 200)}`)
|
|
196
|
-
.join("\n");
|
|
197
|
-
|
|
198
|
-
const notifIssueRef = enrichedIssue?.identifier ?? issue.id;
|
|
199
|
-
const message = [
|
|
200
|
-
`You are an orchestrator responding to a Linear issue notification. Your text output will be automatically posted as a comment on the issue (do NOT post a comment yourself — the handler does it).`,
|
|
201
|
-
``,
|
|
202
|
-
`**Tool access:**`,
|
|
203
|
-
`- \`linearis\` CLI: READ ONLY. You can read issues (\`linearis issues read ${notifIssueRef}\`), list, and search. Do NOT use linearis to update, close, comment, or modify issues.`,
|
|
204
|
-
`- \`code_run\`: Dispatch coding work to a worker. Workers return text — they cannot access linearis.`,
|
|
205
|
-
`- Standard tools: exec, read, edit, write, web_search, etc.`,
|
|
206
|
-
``,
|
|
207
|
-
`**Your role:** Dispatcher. For work requests, use \`code_run\`. You do NOT update issue status — the audit system handles lifecycle.`,
|
|
208
|
-
``,
|
|
209
|
-
`## Issue: ${notifIssueRef} — ${enrichedIssue?.title ?? issue.title ?? "(untitled)"}`,
|
|
210
|
-
`**Status:** ${enrichedIssue?.state?.name ?? "Unknown"} | **Assignee:** ${enrichedIssue?.assignee?.name ?? "Unassigned"}`,
|
|
211
|
-
``,
|
|
212
|
-
`**Description:**`,
|
|
213
|
-
description,
|
|
214
|
-
commentSummary ? `\n**Recent comments:**\n${commentSummary}` : "",
|
|
215
|
-
comment?.body ? `\n**Triggering comment:**\n> ${comment.body}` : "",
|
|
216
|
-
``,
|
|
217
|
-
`Respond concisely. For work requests, dispatch via \`code_run\` and summarize the result.`,
|
|
218
|
-
].filter(Boolean).join("\n");
|
|
219
|
-
|
|
220
|
-
// Dispatch agent with session lifecycle (non-blocking)
|
|
221
|
-
void (async () => {
|
|
222
|
-
const profiles = loadAgentProfiles();
|
|
223
|
-
const defaultProfile = Object.entries(profiles).find(([, p]) => p.isDefault);
|
|
224
|
-
const label = defaultProfile?.[1]?.label ?? profiles[agentId]?.label ?? agentId;
|
|
225
|
-
const avatarUrl = defaultProfile?.[1]?.avatarUrl ?? profiles[agentId]?.avatarUrl;
|
|
226
|
-
let agentSessionId: string | null = null;
|
|
227
|
-
|
|
228
|
-
try {
|
|
229
|
-
// 1. Create agent session (non-fatal)
|
|
230
|
-
const sessionResult = await linearApi.createSessionOnIssue(issue.id);
|
|
231
|
-
agentSessionId = sessionResult.sessionId;
|
|
232
|
-
if (agentSessionId) {
|
|
233
|
-
api.logger.info(`Created agent session ${agentSessionId} for notification`);
|
|
234
|
-
setActiveSession({
|
|
235
|
-
agentSessionId,
|
|
236
|
-
issueIdentifier: enrichedIssue?.identifier ?? issue.id,
|
|
237
|
-
issueId: issue.id,
|
|
238
|
-
agentId,
|
|
239
|
-
startedAt: Date.now(),
|
|
240
|
-
});
|
|
241
|
-
} else {
|
|
242
|
-
api.logger.warn(`Could not create agent session for notification: ${sessionResult.error ?? "unknown"}`);
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
// 2. Emit thought
|
|
246
|
-
if (agentSessionId) {
|
|
247
|
-
await linearApi.emitActivity(agentSessionId, {
|
|
248
|
-
type: "thought",
|
|
249
|
-
body: `Reviewing ${enrichedIssue?.identifier ?? issue.id}...`,
|
|
250
|
-
}).catch(() => {});
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
// 3. Run agent with streaming
|
|
254
|
-
const sessionId = `linear-notif-${notification?.type ?? "unknown"}-${Date.now()}`;
|
|
255
|
-
const { runAgent } = await import("../agent/agent.js");
|
|
256
|
-
const result = await runAgent({
|
|
257
|
-
api,
|
|
258
|
-
agentId,
|
|
259
|
-
sessionId,
|
|
260
|
-
message,
|
|
261
|
-
timeoutMs: 3 * 60_000,
|
|
262
|
-
streaming: agentSessionId ? { linearApi, agentSessionId } : undefined,
|
|
263
|
-
});
|
|
264
|
-
|
|
265
|
-
const responseBody = result.success
|
|
266
|
-
? result.output
|
|
267
|
-
: `Something went wrong while processing this. The system will retry automatically if possible. If this keeps happening, run \`openclaw openclaw-linear doctor\` to check for issues.`;
|
|
268
|
-
|
|
269
|
-
// 5. Post branded comment (fallback to prefix)
|
|
270
|
-
const brandingOpts = avatarUrl
|
|
271
|
-
? { createAsUser: label, displayIconUrl: avatarUrl }
|
|
272
|
-
: undefined;
|
|
273
|
-
|
|
274
|
-
try {
|
|
275
|
-
if (brandingOpts) {
|
|
276
|
-
await linearApi.createComment(issue.id, responseBody, brandingOpts);
|
|
277
|
-
} else {
|
|
278
|
-
await linearApi.createComment(issue.id, `**[${label}]** ${responseBody}`);
|
|
279
|
-
}
|
|
280
|
-
} catch (brandErr) {
|
|
281
|
-
api.logger.warn(`Branded comment failed, falling back to prefix: ${brandErr}`);
|
|
282
|
-
await linearApi.createComment(issue.id, `**[${label}]** ${responseBody}`);
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
// 6. Emit response (closes session)
|
|
286
|
-
if (agentSessionId) {
|
|
287
|
-
const truncated = responseBody.length > 2000
|
|
288
|
-
? responseBody.slice(0, 2000) + "…"
|
|
289
|
-
: responseBody;
|
|
290
|
-
await linearApi.emitActivity(agentSessionId, {
|
|
291
|
-
type: "response",
|
|
292
|
-
body: truncated,
|
|
293
|
-
}).catch(() => {});
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
api.logger.info(`Posted agent response to ${enrichedIssue?.identifier ?? issue.id}`);
|
|
297
|
-
} catch (err) {
|
|
298
|
-
api.logger.error(`AppUserNotification handler error: ${err}`);
|
|
299
|
-
if (agentSessionId) {
|
|
300
|
-
await linearApi.emitActivity(agentSessionId, {
|
|
301
|
-
type: "error",
|
|
302
|
-
body: `Failed to process notification: ${String(err).slice(0, 500)}`,
|
|
303
|
-
}).catch(() => {});
|
|
304
|
-
}
|
|
305
|
-
} finally {
|
|
306
|
-
clearActiveSession(issue.id);
|
|
307
|
-
}
|
|
308
|
-
})();
|
|
309
|
-
|
|
310
166
|
return true;
|
|
311
167
|
}
|
|
312
168
|
|
|
@@ -965,6 +821,14 @@ export async function handleLinearWebhook(
|
|
|
965
821
|
|
|
966
822
|
const agentId = resolveAgentId(api);
|
|
967
823
|
|
|
824
|
+
// Guard: prevent duplicate runs on same issue (also blocks AgentSessionEvent
|
|
825
|
+
// webhooks that arrive from sessions we create during triage)
|
|
826
|
+
if (activeRuns.has(issue.id)) {
|
|
827
|
+
api.logger.info(`Issue.create: ${issue.identifier ?? issue.id} already has active run — skipping triage`);
|
|
828
|
+
return true;
|
|
829
|
+
}
|
|
830
|
+
activeRuns.add(issue.id);
|
|
831
|
+
|
|
968
832
|
// Dispatch triage (non-blocking)
|
|
969
833
|
void (async () => {
|
|
970
834
|
const profiles = loadAgentProfiles();
|
|
@@ -1172,6 +1036,7 @@ export async function handleLinearWebhook(
|
|
|
1172
1036
|
}
|
|
1173
1037
|
} finally {
|
|
1174
1038
|
clearActiveSession(issue.id);
|
|
1039
|
+
activeRuns.delete(issue.id);
|
|
1175
1040
|
}
|
|
1176
1041
|
})();
|
|
1177
1042
|
|
|
@@ -1481,6 +1346,9 @@ async function handleDispatch(
|
|
|
1481
1346
|
}
|
|
1482
1347
|
|
|
1483
1348
|
// 6. Create agent session on Linear
|
|
1349
|
+
// Mark active BEFORE session creation so that any AgentSessionEvent.created
|
|
1350
|
+
// webhook arriving from this call is blocked by the activeRuns guard.
|
|
1351
|
+
activeRuns.add(issue.id);
|
|
1484
1352
|
let agentSessionId: string | undefined;
|
|
1485
1353
|
try {
|
|
1486
1354
|
const sessionResult = await linearApi.createSessionOnIssue(issue.id);
|
|
@@ -1583,7 +1451,7 @@ async function handleDispatch(
|
|
|
1583
1451
|
}
|
|
1584
1452
|
|
|
1585
1453
|
// 11. Run v2 pipeline: worker → audit → verdict (non-blocking)
|
|
1586
|
-
activeRuns
|
|
1454
|
+
// (activeRuns already set in step 6 above)
|
|
1587
1455
|
|
|
1588
1456
|
// Instantiate notifier (Discord, Slack, or both — config-driven)
|
|
1589
1457
|
const notify: NotifyFn = createNotifierFromConfig(pluginConfig, api.runtime, api);
|