@calltelemetry/openclaw-linear 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +339 -365
- package/index.ts +50 -2
- package/openclaw.plugin.json +7 -1
- package/package.json +3 -2
- package/src/active-session.ts +66 -0
- package/src/agent.ts +173 -1
- package/src/auth.ts +6 -2
- package/src/claude-tool.ts +280 -0
- package/src/cli-shared.ts +75 -0
- package/src/cli.ts +39 -0
- package/src/client.ts +1 -0
- package/src/code-tool.ts +202 -0
- package/src/codex-tool.ts +240 -0
- package/src/codex-worktree.ts +264 -0
- package/src/gemini-tool.ts +238 -0
- package/src/orchestration-tools.ts +134 -0
- package/src/pipeline.ts +68 -10
- package/src/tools.ts +29 -79
- package/src/webhook.ts +321 -90
package/src/webhook.ts
CHANGED
|
@@ -3,7 +3,9 @@ import { readFileSync } from "node:fs";
|
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
5
5
|
import { LinearAgentApi, resolveLinearToken } from "./linear-api.js";
|
|
6
|
-
|
|
6
|
+
// Pipeline is used from Issue.update delegation handler (not from agent session chat)
|
|
7
|
+
// import { runFullPipeline, resumePipeline, type PipelineContext } from "./pipeline.js";
|
|
8
|
+
import { setActiveSession, clearActiveSession } from "./active-session.js";
|
|
7
9
|
|
|
8
10
|
// ── Agent profiles (loaded from config, no hardcoded names) ───────
|
|
9
11
|
interface AgentProfile {
|
|
@@ -49,8 +51,8 @@ function resolveAgentFromAlias(alias: string, profiles: Record<string, AgentProf
|
|
|
49
51
|
return null;
|
|
50
52
|
}
|
|
51
53
|
|
|
52
|
-
//
|
|
53
|
-
const
|
|
54
|
+
// Track issues with active agent runs to prevent concurrent duplicate runs.
|
|
55
|
+
const activeRuns = new Set<string>();
|
|
54
56
|
|
|
55
57
|
// Dedup: track recently processed keys to avoid double-handling
|
|
56
58
|
const recentlyProcessed = new Map<string, number>();
|
|
@@ -212,6 +214,13 @@ export async function handleLinearWebhook(
|
|
|
212
214
|
agentSessionId = sessionResult.sessionId;
|
|
213
215
|
if (agentSessionId) {
|
|
214
216
|
api.logger.info(`Created agent session ${agentSessionId} for notification`);
|
|
217
|
+
setActiveSession({
|
|
218
|
+
agentSessionId,
|
|
219
|
+
issueIdentifier: enrichedIssue?.identifier ?? issue.id,
|
|
220
|
+
issueId: issue.id,
|
|
221
|
+
agentId,
|
|
222
|
+
startedAt: Date.now(),
|
|
223
|
+
});
|
|
215
224
|
} else {
|
|
216
225
|
api.logger.warn(`Could not create agent session for notification: ${sessionResult.error ?? "unknown"}`);
|
|
217
226
|
}
|
|
@@ -220,20 +229,11 @@ export async function handleLinearWebhook(
|
|
|
220
229
|
if (agentSessionId) {
|
|
221
230
|
await linearApi.emitActivity(agentSessionId, {
|
|
222
231
|
type: "thought",
|
|
223
|
-
body: `Reviewing
|
|
232
|
+
body: `Reviewing ${enrichedIssue?.identifier ?? issue.id}...`,
|
|
224
233
|
}).catch(() => {});
|
|
225
234
|
}
|
|
226
235
|
|
|
227
|
-
// 3.
|
|
228
|
-
if (agentSessionId) {
|
|
229
|
-
await linearApi.emitActivity(agentSessionId, {
|
|
230
|
-
type: "action",
|
|
231
|
-
action: "Processing notification",
|
|
232
|
-
parameter: notifType ?? "unknown",
|
|
233
|
-
}).catch(() => {});
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
// 4. Run agent
|
|
236
|
+
// 3. Run agent with streaming
|
|
237
237
|
const sessionId = `linear-notif-${notification?.type ?? "unknown"}-${Date.now()}`;
|
|
238
238
|
const { runAgent } = await import("./agent.js");
|
|
239
239
|
const result = await runAgent({
|
|
@@ -242,6 +242,7 @@ export async function handleLinearWebhook(
|
|
|
242
242
|
sessionId,
|
|
243
243
|
message,
|
|
244
244
|
timeoutMs: 3 * 60_000,
|
|
245
|
+
streaming: agentSessionId ? { linearApi, agentSessionId } : undefined,
|
|
245
246
|
});
|
|
246
247
|
|
|
247
248
|
const responseBody = result.success
|
|
@@ -284,13 +285,18 @@ export async function handleLinearWebhook(
|
|
|
284
285
|
body: `Failed to process notification: ${String(err).slice(0, 500)}`,
|
|
285
286
|
}).catch(() => {});
|
|
286
287
|
}
|
|
288
|
+
} finally {
|
|
289
|
+
clearActiveSession(issue.id);
|
|
287
290
|
}
|
|
288
291
|
})();
|
|
289
292
|
|
|
290
293
|
return true;
|
|
291
294
|
}
|
|
292
295
|
|
|
293
|
-
// ── AgentSessionEvent.created —
|
|
296
|
+
// ── AgentSessionEvent.created — direct agent run ─────────────────
|
|
297
|
+
// User chatted with @ctclaw in Linear's agent session. Run the agent
|
|
298
|
+
// DIRECTLY with the user's message. The plan→implement→audit pipeline
|
|
299
|
+
// is only triggered from Issue.update delegation, not from chat.
|
|
294
300
|
if (
|
|
295
301
|
(payload.type === "AgentSessionEvent" && payload.action === "created") ||
|
|
296
302
|
(payload.type === "AgentSession" && payload.action === "create")
|
|
@@ -307,7 +313,7 @@ export async function handleLinearWebhook(
|
|
|
307
313
|
return true;
|
|
308
314
|
}
|
|
309
315
|
|
|
310
|
-
// Dedup: skip if we already handled this session
|
|
316
|
+
// Dedup: skip if we already handled this session
|
|
311
317
|
if (wasRecentlyProcessed(`session:${session.id}`)) {
|
|
312
318
|
api.logger.info(`AgentSession ${session.id} already handled — skipping`);
|
|
313
319
|
return true;
|
|
@@ -315,47 +321,147 @@ export async function handleLinearWebhook(
|
|
|
315
321
|
|
|
316
322
|
const linearApi = createLinearApi(api);
|
|
317
323
|
if (!linearApi) {
|
|
318
|
-
api.logger.error("No Linear access token configured
|
|
324
|
+
api.logger.error("No Linear access token configured");
|
|
319
325
|
return true;
|
|
320
326
|
}
|
|
321
327
|
|
|
322
328
|
const agentId = resolveAgentId(api);
|
|
323
|
-
|
|
324
329
|
const previousComments = payload.previousComments ?? [];
|
|
325
330
|
const guidance = payload.guidance;
|
|
326
331
|
|
|
327
332
|
api.logger.info(`AgentSession created: ${session.id} for issue ${issue?.identifier ?? issue?.id} (comments: ${previousComments.length}, guidance: ${guidance ? "yes" : "no"})`);
|
|
328
333
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
334
|
+
// Guard: skip if an agent run is already active for this issue
|
|
335
|
+
if (activeRuns.has(issue.id)) {
|
|
336
|
+
api.logger.info(`Agent already running for ${issue?.identifier ?? issue?.id} — skipping session ${session.id}`);
|
|
337
|
+
return true;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Extract the user's latest message from previousComments
|
|
341
|
+
// The last comment is the most recent user message
|
|
342
|
+
const lastComment = previousComments.length > 0
|
|
343
|
+
? previousComments[previousComments.length - 1]
|
|
344
|
+
: null;
|
|
345
|
+
const userMessage = lastComment?.body ?? guidance ?? "";
|
|
346
|
+
|
|
347
|
+
// Fetch full issue details
|
|
348
|
+
let enrichedIssue: any = issue;
|
|
349
|
+
try {
|
|
350
|
+
enrichedIssue = await linearApi.getIssueDetails(issue.id);
|
|
351
|
+
} catch (err) {
|
|
352
|
+
api.logger.warn(`Could not fetch issue details: ${err}`);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const description = enrichedIssue?.description ?? issue?.description ?? "(no description)";
|
|
356
|
+
|
|
357
|
+
// Build conversation context from previous comments
|
|
358
|
+
const commentContext = previousComments
|
|
359
|
+
.slice(-5)
|
|
360
|
+
.map((c: any) => `**${c.user?.name ?? c.actorName ?? "User"}**: ${(c.body ?? "").slice(0, 300)}`)
|
|
361
|
+
.join("\n\n");
|
|
362
|
+
|
|
363
|
+
const message = [
|
|
364
|
+
`You are an AI agent responding in a Linear issue session. Your text output will be posted as activities visible to the user.`,
|
|
365
|
+
`You have access to tools including \`code_run\` for coding tasks, \`spawn_agent\`/\`ask_agent\` for delegation, and standard tools (exec, read, edit, write, web_search, etc.). To manage Linear issues (update status, close, assign, comment, etc.) use the \`linearis\` CLI via exec — e.g. \`linearis issues update API-123 --status done\` to close an issue, \`linearis issues update API-123 --status "In Progress"\` to change status, \`linearis issues list\` to list issues. Run \`linearis usage\` for full command reference.`,
|
|
366
|
+
``,
|
|
367
|
+
`## Issue: ${enrichedIssue?.identifier ?? issue.identifier ?? issue.id} — ${enrichedIssue?.title ?? issue.title ?? "(untitled)"}`,
|
|
368
|
+
`**Status:** ${enrichedIssue?.state?.name ?? "Unknown"} | **Assignee:** ${enrichedIssue?.assignee?.name ?? "Unassigned"}`,
|
|
369
|
+
``,
|
|
370
|
+
`**Description:**`,
|
|
371
|
+
description,
|
|
372
|
+
commentContext ? `\n**Conversation:**\n${commentContext}` : "",
|
|
373
|
+
userMessage ? `\n**Latest message:**\n> ${userMessage}` : "",
|
|
374
|
+
``,
|
|
375
|
+
`Respond to the user's request. If they ask you to write code or make changes, use the \`code_run\` tool. Be concise and action-oriented.`,
|
|
376
|
+
].filter(Boolean).join("\n");
|
|
377
|
+
|
|
378
|
+
// Run agent directly (non-blocking)
|
|
379
|
+
activeRuns.add(issue.id);
|
|
344
380
|
void (async () => {
|
|
345
|
-
const
|
|
346
|
-
const
|
|
347
|
-
|
|
348
|
-
|
|
381
|
+
const profiles = loadAgentProfiles();
|
|
382
|
+
const defaultProfile = Object.entries(profiles).find(([, p]) => p.isDefault);
|
|
383
|
+
const label = defaultProfile?.[1]?.label ?? profiles[agentId]?.label ?? agentId;
|
|
384
|
+
|
|
385
|
+
// Register active session for tool resolution (code_run, etc.)
|
|
386
|
+
setActiveSession({
|
|
387
|
+
agentSessionId: session.id,
|
|
388
|
+
issueIdentifier: enrichedIssue?.identifier ?? issue.identifier ?? issue.id,
|
|
389
|
+
issueId: issue.id,
|
|
390
|
+
agentId,
|
|
391
|
+
startedAt: Date.now(),
|
|
349
392
|
});
|
|
350
|
-
|
|
351
|
-
|
|
393
|
+
|
|
394
|
+
try {
|
|
395
|
+
// Emit initial thought
|
|
396
|
+
await linearApi.emitActivity(session.id, {
|
|
397
|
+
type: "thought",
|
|
398
|
+
body: `Processing request for ${enrichedIssue?.identifier ?? issue.id}...`,
|
|
399
|
+
}).catch(() => {});
|
|
400
|
+
|
|
401
|
+
// Run agent with streaming to Linear
|
|
402
|
+
const sessionId = `linear-session-${session.id}`;
|
|
403
|
+
const { runAgent } = await import("./agent.js");
|
|
404
|
+
const result = await runAgent({
|
|
405
|
+
api,
|
|
406
|
+
agentId,
|
|
407
|
+
sessionId,
|
|
408
|
+
message,
|
|
409
|
+
timeoutMs: 5 * 60_000,
|
|
410
|
+
streaming: {
|
|
411
|
+
linearApi,
|
|
412
|
+
agentSessionId: session.id,
|
|
413
|
+
},
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
const responseBody = result.success
|
|
417
|
+
? result.output
|
|
418
|
+
: `I encountered an error processing this request. Please try again.`;
|
|
419
|
+
|
|
420
|
+
// Post as comment
|
|
421
|
+
const avatarUrl = defaultProfile?.[1]?.avatarUrl ?? profiles[agentId]?.avatarUrl;
|
|
422
|
+
const brandingOpts = avatarUrl
|
|
423
|
+
? { createAsUser: label, displayIconUrl: avatarUrl }
|
|
424
|
+
: undefined;
|
|
425
|
+
|
|
426
|
+
try {
|
|
427
|
+
if (brandingOpts) {
|
|
428
|
+
await linearApi.createComment(issue.id, responseBody, brandingOpts);
|
|
429
|
+
} else {
|
|
430
|
+
await linearApi.createComment(issue.id, `**[${label}]** ${responseBody}`);
|
|
431
|
+
}
|
|
432
|
+
} catch (brandErr) {
|
|
433
|
+
api.logger.warn(`Branded comment failed: ${brandErr}`);
|
|
434
|
+
await linearApi.createComment(issue.id, `**[${label}]** ${responseBody}`);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Emit response (closes session)
|
|
438
|
+
const truncated = responseBody.length > 2000
|
|
439
|
+
? responseBody.slice(0, 2000) + "\u2026"
|
|
440
|
+
: responseBody;
|
|
441
|
+
await linearApi.emitActivity(session.id, {
|
|
442
|
+
type: "response",
|
|
443
|
+
body: truncated,
|
|
444
|
+
}).catch(() => {});
|
|
445
|
+
|
|
446
|
+
api.logger.info(`Posted agent response to ${enrichedIssue?.identifier ?? issue.id} (session ${session.id})`);
|
|
447
|
+
} catch (err) {
|
|
448
|
+
api.logger.error(`AgentSession handler error: ${err}`);
|
|
449
|
+
await linearApi.emitActivity(session.id, {
|
|
450
|
+
type: "error",
|
|
451
|
+
body: `Failed: ${String(err).slice(0, 500)}`,
|
|
452
|
+
}).catch(() => {});
|
|
453
|
+
} finally {
|
|
454
|
+
clearActiveSession(issue.id);
|
|
455
|
+
activeRuns.delete(issue.id);
|
|
352
456
|
}
|
|
353
457
|
})();
|
|
354
458
|
|
|
355
459
|
return true;
|
|
356
460
|
}
|
|
357
461
|
|
|
358
|
-
// ── AgentSession.prompted — user
|
|
462
|
+
// ── AgentSession.prompted — follow-up user messages in existing sessions
|
|
463
|
+
// Also fires when we emit activities (feedback loop). Use activeRuns guard
|
|
464
|
+
// and webhookId dedup to distinguish user follow-ups from our own emissions.
|
|
359
465
|
if (
|
|
360
466
|
(payload.type === "AgentSessionEvent" && payload.action === "prompted") ||
|
|
361
467
|
(payload.type === "AgentSession" && payload.action === "prompted")
|
|
@@ -364,55 +470,160 @@ export async function handleLinearWebhook(
|
|
|
364
470
|
res.end("ok");
|
|
365
471
|
|
|
366
472
|
const session = payload.agentSession ?? payload.data;
|
|
367
|
-
|
|
368
|
-
|
|
473
|
+
const issue = session?.issue ?? payload.issue;
|
|
474
|
+
const activity = payload.agentActivity;
|
|
475
|
+
|
|
476
|
+
if (!session?.id || !issue?.id) {
|
|
477
|
+
api.logger.info(`AgentSession prompted: missing session or issue — ignoring`);
|
|
369
478
|
return true;
|
|
370
479
|
}
|
|
371
480
|
|
|
372
|
-
|
|
481
|
+
// If an agent run is already active for this issue, this is feedback from
|
|
482
|
+
// our own activity emissions — ignore to prevent loops.
|
|
483
|
+
if (activeRuns.has(issue.id)) {
|
|
484
|
+
api.logger.info(`AgentSession prompted: ${session.id} issue=${issue?.identifier ?? issue?.id} — agent active, ignoring (feedback)`);
|
|
485
|
+
return true;
|
|
486
|
+
}
|
|
373
487
|
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
488
|
+
// Dedup by webhookId
|
|
489
|
+
const webhookId = payload.webhookId;
|
|
490
|
+
if (webhookId && wasRecentlyProcessed(`webhook:${webhookId}`)) {
|
|
491
|
+
api.logger.info(`AgentSession prompted: webhook ${webhookId} already processed — skipping`);
|
|
492
|
+
return true;
|
|
493
|
+
}
|
|
377
494
|
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
495
|
+
// Extract user message from the activity or prompt context
|
|
496
|
+
const promptContext = payload.promptContext;
|
|
497
|
+
const userMessage =
|
|
498
|
+
activity?.content?.body ??
|
|
499
|
+
activity?.body ??
|
|
500
|
+
promptContext?.message ??
|
|
501
|
+
promptContext ??
|
|
502
|
+
"";
|
|
503
|
+
|
|
504
|
+
if (!userMessage || typeof userMessage !== "string" || userMessage.trim().length === 0) {
|
|
505
|
+
api.logger.info(`AgentSession prompted: ${session.id} — no user message found, ignoring`);
|
|
506
|
+
return true;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
const linearApi = createLinearApi(api);
|
|
510
|
+
if (!linearApi) {
|
|
511
|
+
api.logger.error("No Linear access token configured");
|
|
512
|
+
return true;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
api.logger.info(`AgentSession prompted (follow-up): ${session.id} issue=${issue?.identifier ?? issue?.id} message="${userMessage.slice(0, 80)}..."`);
|
|
385
516
|
|
|
386
|
-
|
|
517
|
+
const agentId = resolveAgentId(api);
|
|
387
518
|
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
519
|
+
// Run agent for follow-up (non-blocking)
|
|
520
|
+
activeRuns.add(issue.id);
|
|
521
|
+
void (async () => {
|
|
522
|
+
const profiles = loadAgentProfiles();
|
|
523
|
+
const defaultProfile = Object.entries(profiles).find(([, p]) => p.isDefault);
|
|
524
|
+
const label = defaultProfile?.[1]?.label ?? profiles[agentId]?.label ?? agentId;
|
|
525
|
+
|
|
526
|
+
// Fetch full issue details for context
|
|
527
|
+
let enrichedIssue: any = issue;
|
|
528
|
+
try {
|
|
529
|
+
enrichedIssue = await linearApi.getIssueDetails(issue.id);
|
|
530
|
+
} catch (err) {
|
|
531
|
+
api.logger.warn(`Could not fetch issue details: ${err}`);
|
|
532
|
+
}
|
|
391
533
|
|
|
392
|
-
const
|
|
393
|
-
|
|
394
|
-
|
|
534
|
+
const description = enrichedIssue?.description ?? issue?.description ?? "(no description)";
|
|
535
|
+
|
|
536
|
+
// Build context from recent comments
|
|
537
|
+
const recentComments = enrichedIssue?.comments?.nodes ?? [];
|
|
538
|
+
const commentContext = recentComments
|
|
539
|
+
.slice(-5)
|
|
540
|
+
.map((c: any) => `**${c.user?.name ?? "User"}**: ${(c.body ?? "").slice(0, 300)}`)
|
|
541
|
+
.join("\n\n");
|
|
542
|
+
|
|
543
|
+
const message = [
|
|
544
|
+
`You are an AI agent responding in a Linear issue session. Your text output will be posted as activities visible to the user.`,
|
|
545
|
+
`You have access to tools including \`code_run\` for coding tasks, \`spawn_agent\`/\`ask_agent\` for delegation, and standard tools (exec, read, edit, write, web_search, etc.). To manage Linear issues (update status, close, assign, comment, etc.) use the \`linearis\` CLI via exec — e.g. \`linearis issues update API-123 --status done\` to close an issue, \`linearis issues update API-123 --status "In Progress"\` to change status, \`linearis issues list\` to list issues. Run \`linearis usage\` for full command reference.`,
|
|
546
|
+
``,
|
|
547
|
+
`## Issue: ${enrichedIssue?.identifier ?? issue.identifier ?? issue.id} — ${enrichedIssue?.title ?? issue.title ?? "(untitled)"}`,
|
|
548
|
+
`**Status:** ${enrichedIssue?.state?.name ?? "Unknown"} | **Assignee:** ${enrichedIssue?.assignee?.name ?? "Unassigned"}`,
|
|
549
|
+
``,
|
|
550
|
+
`**Description:**`,
|
|
551
|
+
description,
|
|
552
|
+
commentContext ? `\n**Recent conversation:**\n${commentContext}` : "",
|
|
553
|
+
`\n**User's follow-up message:**\n> ${userMessage}`,
|
|
554
|
+
``,
|
|
555
|
+
`Respond to the user's follow-up. Be concise and action-oriented.`,
|
|
556
|
+
].filter(Boolean).join("\n");
|
|
557
|
+
|
|
558
|
+
setActiveSession({
|
|
395
559
|
agentSessionId: session.id,
|
|
560
|
+
issueIdentifier: enrichedIssue?.identifier ?? issue.identifier ?? issue.id,
|
|
561
|
+
issueId: issue.id,
|
|
396
562
|
agentId,
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
identifier: issue.identifier ?? issue.id,
|
|
400
|
-
title: issue.title ?? "(untitled)",
|
|
401
|
-
description: issue.description,
|
|
402
|
-
},
|
|
403
|
-
promptContext: session.context,
|
|
404
|
-
};
|
|
405
|
-
|
|
406
|
-
// Start fresh pipeline since we lost the plan
|
|
407
|
-
void runFullPipeline(ctx);
|
|
408
|
-
return true;
|
|
409
|
-
}
|
|
563
|
+
startedAt: Date.now(),
|
|
564
|
+
});
|
|
410
565
|
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
566
|
+
try {
|
|
567
|
+
await linearApi.emitActivity(session.id, {
|
|
568
|
+
type: "thought",
|
|
569
|
+
body: `Processing follow-up for ${enrichedIssue?.identifier ?? issue.id}...`,
|
|
570
|
+
}).catch(() => {});
|
|
571
|
+
|
|
572
|
+
const sessionId = `linear-session-${session.id}`;
|
|
573
|
+
const { runAgent } = await import("./agent.js");
|
|
574
|
+
const result = await runAgent({
|
|
575
|
+
api,
|
|
576
|
+
agentId,
|
|
577
|
+
sessionId,
|
|
578
|
+
message,
|
|
579
|
+
timeoutMs: 5 * 60_000,
|
|
580
|
+
streaming: {
|
|
581
|
+
linearApi,
|
|
582
|
+
agentSessionId: session.id,
|
|
583
|
+
},
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
const responseBody = result.success
|
|
587
|
+
? result.output
|
|
588
|
+
: `I encountered an error processing this request. Please try again.`;
|
|
589
|
+
|
|
590
|
+
const avatarUrl = defaultProfile?.[1]?.avatarUrl ?? profiles[agentId]?.avatarUrl;
|
|
591
|
+
const brandingOpts = avatarUrl
|
|
592
|
+
? { createAsUser: label, displayIconUrl: avatarUrl }
|
|
593
|
+
: undefined;
|
|
594
|
+
|
|
595
|
+
try {
|
|
596
|
+
if (brandingOpts) {
|
|
597
|
+
await linearApi.createComment(issue.id, responseBody, brandingOpts);
|
|
598
|
+
} else {
|
|
599
|
+
await linearApi.createComment(issue.id, `**[${label}]** ${responseBody}`);
|
|
600
|
+
}
|
|
601
|
+
} catch (brandErr) {
|
|
602
|
+
api.logger.warn(`Branded comment failed: ${brandErr}`);
|
|
603
|
+
await linearApi.createComment(issue.id, `**[${label}]** ${responseBody}`);
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
const truncated = responseBody.length > 2000
|
|
607
|
+
? responseBody.slice(0, 2000) + "\u2026"
|
|
608
|
+
: responseBody;
|
|
609
|
+
await linearApi.emitActivity(session.id, {
|
|
610
|
+
type: "response",
|
|
611
|
+
body: truncated,
|
|
612
|
+
}).catch(() => {});
|
|
613
|
+
|
|
614
|
+
api.logger.info(`Posted follow-up response to ${enrichedIssue?.identifier ?? issue.id} (session ${session.id})`);
|
|
615
|
+
} catch (err) {
|
|
616
|
+
api.logger.error(`AgentSession prompted handler error: ${err}`);
|
|
617
|
+
await linearApi.emitActivity(session.id, {
|
|
618
|
+
type: "error",
|
|
619
|
+
body: `Failed: ${String(err).slice(0, 500)}`,
|
|
620
|
+
}).catch(() => {});
|
|
621
|
+
} finally {
|
|
622
|
+
clearActiveSession(issue.id);
|
|
623
|
+
activeRuns.delete(issue.id);
|
|
624
|
+
}
|
|
625
|
+
})();
|
|
414
626
|
|
|
415
|
-
void resumePipeline(stored.ctx, stored.plan);
|
|
416
627
|
return true;
|
|
417
628
|
}
|
|
418
629
|
|
|
@@ -526,6 +737,14 @@ export async function handleLinearWebhook(
|
|
|
526
737
|
agentSessionId = sessionResult.sessionId;
|
|
527
738
|
if (agentSessionId) {
|
|
528
739
|
api.logger.info(`Created agent session ${agentSessionId} for @${mentionedAgent}`);
|
|
740
|
+
// Register active session so code_run can resolve it automatically
|
|
741
|
+
setActiveSession({
|
|
742
|
+
agentSessionId,
|
|
743
|
+
issueIdentifier: enrichedIssue.identifier ?? issue.id,
|
|
744
|
+
issueId: issue.id,
|
|
745
|
+
agentId: mentionedAgent,
|
|
746
|
+
startedAt: Date.now(),
|
|
747
|
+
});
|
|
529
748
|
} else {
|
|
530
749
|
api.logger.warn(`Could not create agent session for @${mentionedAgent}: ${sessionResult.error ?? "unknown"} — falling back to flat comment`);
|
|
531
750
|
}
|
|
@@ -534,20 +753,11 @@ export async function handleLinearWebhook(
|
|
|
534
753
|
if (agentSessionId) {
|
|
535
754
|
await linearApi.emitActivity(agentSessionId, {
|
|
536
755
|
type: "thought",
|
|
537
|
-
body: `
|
|
538
|
-
}).catch(() => {});
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
// 3. Emit action
|
|
542
|
-
if (agentSessionId) {
|
|
543
|
-
await linearApi.emitActivity(agentSessionId, {
|
|
544
|
-
type: "action",
|
|
545
|
-
action: "Processing mention",
|
|
546
|
-
parameter: `@${alias} by ${commentor}`,
|
|
756
|
+
body: `Reviewing ${enrichedIssue.identifier ?? issue.id}...`,
|
|
547
757
|
}).catch(() => {});
|
|
548
758
|
}
|
|
549
759
|
|
|
550
|
-
//
|
|
760
|
+
// 3. Run agent subprocess with streaming
|
|
551
761
|
const sessionId = `linear-comment-${comment.id ?? Date.now()}`;
|
|
552
762
|
const { runAgent } = await import("./agent.js");
|
|
553
763
|
const result = await runAgent({
|
|
@@ -556,6 +766,7 @@ export async function handleLinearWebhook(
|
|
|
556
766
|
sessionId,
|
|
557
767
|
message: taskMessage,
|
|
558
768
|
timeoutMs: 3 * 60_000,
|
|
769
|
+
streaming: agentSessionId ? { linearApi, agentSessionId } : undefined,
|
|
559
770
|
});
|
|
560
771
|
|
|
561
772
|
const responseBody = result.success
|
|
@@ -592,13 +803,14 @@ export async function handleLinearWebhook(
|
|
|
592
803
|
api.logger.info(`Posted @${mentionedAgent} response to ${issue.identifier ?? issue.id}`);
|
|
593
804
|
} catch (err) {
|
|
594
805
|
api.logger.error(`Comment mention handler error: ${err}`);
|
|
595
|
-
// 7. Emit error activity if session exists
|
|
596
806
|
if (agentSessionId) {
|
|
597
807
|
await linearApi.emitActivity(agentSessionId, {
|
|
598
808
|
type: "error",
|
|
599
809
|
body: `Failed to process mention: ${String(err).slice(0, 500)}`,
|
|
600
810
|
}).catch(() => {});
|
|
601
811
|
}
|
|
812
|
+
} finally {
|
|
813
|
+
clearActiveSession(issue.id);
|
|
602
814
|
}
|
|
603
815
|
})();
|
|
604
816
|
|
|
@@ -727,9 +939,15 @@ export async function handleLinearWebhook(
|
|
|
727
939
|
const sessionResult = await linearApi.createSessionOnIssue(issue.id);
|
|
728
940
|
agentSessionId = sessionResult.sessionId;
|
|
729
941
|
if (agentSessionId) {
|
|
730
|
-
// Mark session as processed so AgentSessionEvent handler skips it
|
|
731
942
|
wasRecentlyProcessed(`session:${agentSessionId}`);
|
|
732
943
|
api.logger.info(`Created agent session ${agentSessionId} for ${trigger}`);
|
|
944
|
+
setActiveSession({
|
|
945
|
+
agentSessionId,
|
|
946
|
+
issueIdentifier: enrichedIssue?.identifier ?? issue.identifier ?? issue.id,
|
|
947
|
+
issueId: issue.id,
|
|
948
|
+
agentId,
|
|
949
|
+
startedAt: Date.now(),
|
|
950
|
+
});
|
|
733
951
|
} else {
|
|
734
952
|
api.logger.warn(`Could not create agent session for assignment: ${sessionResult.error ?? "unknown"}`);
|
|
735
953
|
}
|
|
@@ -757,6 +975,7 @@ export async function handleLinearWebhook(
|
|
|
757
975
|
sessionId,
|
|
758
976
|
message,
|
|
759
977
|
timeoutMs: 3 * 60_000,
|
|
978
|
+
streaming: agentSessionId ? { linearApi, agentSessionId } : undefined,
|
|
760
979
|
});
|
|
761
980
|
|
|
762
981
|
const responseBody = result.success
|
|
@@ -838,6 +1057,8 @@ export async function handleLinearWebhook(
|
|
|
838
1057
|
body: `Failed to process assignment: ${String(err).slice(0, 500)}`,
|
|
839
1058
|
}).catch(() => {});
|
|
840
1059
|
}
|
|
1060
|
+
} finally {
|
|
1061
|
+
clearActiveSession(issue.id);
|
|
841
1062
|
}
|
|
842
1063
|
})();
|
|
843
1064
|
|
|
@@ -904,6 +1125,13 @@ export async function handleLinearWebhook(
|
|
|
904
1125
|
if (agentSessionId) {
|
|
905
1126
|
wasRecentlyProcessed(`session:${agentSessionId}`);
|
|
906
1127
|
api.logger.info(`Created agent session ${agentSessionId} for Issue.create triage`);
|
|
1128
|
+
setActiveSession({
|
|
1129
|
+
agentSessionId,
|
|
1130
|
+
issueIdentifier: enrichedIssue?.identifier ?? issue.identifier ?? issue.id,
|
|
1131
|
+
issueId: issue.id,
|
|
1132
|
+
agentId,
|
|
1133
|
+
startedAt: Date.now(),
|
|
1134
|
+
});
|
|
907
1135
|
}
|
|
908
1136
|
|
|
909
1137
|
if (agentSessionId) {
|
|
@@ -964,6 +1192,7 @@ export async function handleLinearWebhook(
|
|
|
964
1192
|
sessionId,
|
|
965
1193
|
message,
|
|
966
1194
|
timeoutMs: 3 * 60_000,
|
|
1195
|
+
streaming: agentSessionId ? { linearApi, agentSessionId } : undefined,
|
|
967
1196
|
});
|
|
968
1197
|
|
|
969
1198
|
const responseBody = result.success
|
|
@@ -1047,6 +1276,8 @@ export async function handleLinearWebhook(
|
|
|
1047
1276
|
body: `Failed to triage: ${String(err).slice(0, 500)}`,
|
|
1048
1277
|
}).catch(() => {});
|
|
1049
1278
|
}
|
|
1279
|
+
} finally {
|
|
1280
|
+
clearActiveSession(issue.id);
|
|
1050
1281
|
}
|
|
1051
1282
|
})();
|
|
1052
1283
|
|