cyrus-edge-worker 0.0.27 → 0.0.29
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/AgentSessionManager.d.ts +11 -1
- package/dist/AgentSessionManager.d.ts.map +1 -1
- package/dist/AgentSessionManager.js +37 -1
- package/dist/AgentSessionManager.js.map +1 -1
- package/dist/EdgeWorker.d.ts +26 -3
- package/dist/EdgeWorker.d.ts.map +1 -1
- package/dist/EdgeWorker.js +468 -90
- package/dist/EdgeWorker.js.map +1 -1
- package/dist/types.d.ts +25 -6
- package/dist/types.d.ts.map +1 -1
- package/label-prompt-template.md +14 -1
- package/package.json +6 -5
- package/prompts/orchestrator.md +271 -0
package/dist/EdgeWorker.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { EventEmitter } from "node:events";
|
|
2
2
|
import { mkdir, readdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
3
|
-
import { homedir } from "node:os";
|
|
4
3
|
import { basename, dirname, extname, join, resolve } from "node:path";
|
|
5
4
|
import { fileURLToPath } from "node:url";
|
|
6
5
|
import { LinearClient, } from "@linear/sdk";
|
|
7
|
-
import { ClaudeRunner, getAllTools, getReadOnlyTools, getSafeTools, } from "cyrus-claude-runner";
|
|
6
|
+
import { ClaudeRunner, createCyrusToolsServer, getAllTools, getCoordinatorTools, getReadOnlyTools, getSafeTools, } from "cyrus-claude-runner";
|
|
8
7
|
import { isAgentSessionCreatedWebhook, isAgentSessionPromptedWebhook, isIssueAssignedWebhook, isIssueCommentMentionWebhook, isIssueNewCommentWebhook, isIssueUnassignedWebhook, PersistenceManager, } from "cyrus-core";
|
|
8
|
+
import { LinearWebhookClient } from "cyrus-linear-webhook-client";
|
|
9
9
|
import { NdjsonClient } from "cyrus-ndjson-client";
|
|
10
10
|
import { fileTypeFromBuffer } from "file-type";
|
|
11
11
|
import { AgentSessionManager } from "./AgentSessionManager.js";
|
|
@@ -25,10 +25,15 @@ export class EdgeWorker extends EventEmitter {
|
|
|
25
25
|
ndjsonClients = new Map(); // listeners for webhook events, one per linear token
|
|
26
26
|
persistenceManager;
|
|
27
27
|
sharedApplicationServer;
|
|
28
|
+
cyrusHome;
|
|
29
|
+
childToParentAgentSession = new Map(); // Maps child agentSessionId to parent agentSessionId
|
|
28
30
|
constructor(config) {
|
|
29
31
|
super();
|
|
30
32
|
this.config = config;
|
|
31
|
-
this.
|
|
33
|
+
this.cyrusHome = config.cyrusHome;
|
|
34
|
+
this.persistenceManager = new PersistenceManager(join(this.cyrusHome, "state"));
|
|
35
|
+
console.log(`[EdgeWorker Constructor] Initializing parent-child session mapping system`);
|
|
36
|
+
console.log(`[EdgeWorker Constructor] Parent-child mapping initialized with 0 entries`);
|
|
32
37
|
// Initialize shared application server
|
|
33
38
|
const serverPort = config.serverPort || config.webhookPort || 3456;
|
|
34
39
|
const serverHost = config.serverHost || "localhost";
|
|
@@ -46,8 +51,57 @@ export class EdgeWorker extends EventEmitter {
|
|
|
46
51
|
accessToken: repo.linearToken,
|
|
47
52
|
});
|
|
48
53
|
this.linearClients.set(repo.id, linearClient);
|
|
49
|
-
// Create AgentSessionManager for this repository
|
|
50
|
-
|
|
54
|
+
// Create AgentSessionManager for this repository with parent session lookup and resume callback
|
|
55
|
+
//
|
|
56
|
+
// Note: This pattern works (despite appearing recursive) because:
|
|
57
|
+
// 1. The agentSessionManager variable is captured by the closure after it's assigned
|
|
58
|
+
// 2. JavaScript's variable hoisting means 'agentSessionManager' exists (but is undefined) when the arrow function is created
|
|
59
|
+
// 3. By the time the callback is actually invoked (when a child session completes), agentSessionManager is fully initialized
|
|
60
|
+
// 4. The callback only executes asynchronously, well after the constructor has completed and agentSessionManager is assigned
|
|
61
|
+
//
|
|
62
|
+
// This allows the AgentSessionManager to call back into itself to access its own sessions,
|
|
63
|
+
// enabling child sessions to trigger parent session resumption using the same manager instance.
|
|
64
|
+
const agentSessionManager = new AgentSessionManager(linearClient, (childSessionId) => {
|
|
65
|
+
console.log(`[Parent-Child Lookup] Looking up parent session for child ${childSessionId}`);
|
|
66
|
+
const parentId = this.childToParentAgentSession.get(childSessionId);
|
|
67
|
+
console.log(`[Parent-Child Lookup] Child ${childSessionId} -> Parent ${parentId || "not found"}`);
|
|
68
|
+
return parentId;
|
|
69
|
+
}, async (parentSessionId, prompt, childSessionId) => {
|
|
70
|
+
console.log(`[Parent Session Resume] Child session completed, resuming parent session ${parentSessionId}`);
|
|
71
|
+
// Get the parent session and repository
|
|
72
|
+
// This works because by the time this callback runs, agentSessionManager is fully initialized
|
|
73
|
+
console.log(`[Parent Session Resume] Retrieving parent session ${parentSessionId} from agent session manager`);
|
|
74
|
+
const parentSession = agentSessionManager.getSession(parentSessionId);
|
|
75
|
+
if (!parentSession) {
|
|
76
|
+
console.error(`[Parent Session Resume] Parent session ${parentSessionId} not found in agent session manager`);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
console.log(`[Parent Session Resume] Found parent session - Issue: ${parentSession.issueId}, Workspace: ${parentSession.workspace.path}`);
|
|
80
|
+
// Get the child session to access its workspace path
|
|
81
|
+
const childSession = agentSessionManager.getSession(childSessionId);
|
|
82
|
+
const childWorkspaceDirs = [];
|
|
83
|
+
if (childSession) {
|
|
84
|
+
childWorkspaceDirs.push(childSession.workspace.path);
|
|
85
|
+
console.log(`[Parent Session Resume] Adding child workspace to parent allowed directories: ${childSession.workspace.path}`);
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
console.warn(`[Parent Session Resume] Could not find child session ${childSessionId} to add workspace to parent allowed directories`);
|
|
89
|
+
}
|
|
90
|
+
await this.postParentResumeAcknowledgment(parentSessionId, repo.id);
|
|
91
|
+
// Resume the parent session with the child's result
|
|
92
|
+
console.log(`[Parent Session Resume] Resuming parent Claude session with child results`);
|
|
93
|
+
try {
|
|
94
|
+
await this.resumeClaudeSession(parentSession, repo, parentSessionId, agentSessionManager, prompt, "", // No attachment manifest for child results
|
|
95
|
+
false, // Not a new session
|
|
96
|
+
childWorkspaceDirs);
|
|
97
|
+
console.log(`[Parent Session Resume] Successfully resumed parent session ${parentSessionId} with child results`);
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
console.error(`[Parent Session Resume] Failed to resume parent session ${parentSessionId}:`, error);
|
|
101
|
+
console.error(`[Parent Session Resume] Error context - Parent issue: ${parentSession.issueId}, Repository: ${repo.name}`);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
this.agentSessionManagers.set(repo.id, agentSessionManager);
|
|
51
105
|
}
|
|
52
106
|
}
|
|
53
107
|
// Group repositories by token to minimize NDJSON connections
|
|
@@ -65,7 +119,9 @@ export class EdgeWorker extends EventEmitter {
|
|
|
65
119
|
if (!firstRepo)
|
|
66
120
|
continue;
|
|
67
121
|
const primaryRepoId = firstRepo.id;
|
|
68
|
-
|
|
122
|
+
// Determine which client to use based on environment variable
|
|
123
|
+
const useLinearDirectWebhooks = process.env.LINEAR_DIRECT_WEBHOOKS === "true";
|
|
124
|
+
const clientConfig = {
|
|
69
125
|
proxyUrl: config.proxyUrl,
|
|
70
126
|
token: token,
|
|
71
127
|
name: repos.map((r) => r.name).join(", "), // Pass repository names
|
|
@@ -83,11 +139,20 @@ export class EdgeWorker extends EventEmitter {
|
|
|
83
139
|
onConnect: () => this.handleConnect(primaryRepoId, repos),
|
|
84
140
|
onDisconnect: (reason) => this.handleDisconnect(primaryRepoId, repos, reason),
|
|
85
141
|
onError: (error) => this.handleError(error),
|
|
86
|
-
}
|
|
87
|
-
//
|
|
88
|
-
ndjsonClient
|
|
89
|
-
|
|
90
|
-
|
|
142
|
+
};
|
|
143
|
+
// Create the appropriate client based on configuration
|
|
144
|
+
const ndjsonClient = useLinearDirectWebhooks
|
|
145
|
+
? new LinearWebhookClient({
|
|
146
|
+
...clientConfig,
|
|
147
|
+
onWebhook: (payload) => this.handleWebhook(payload, repos),
|
|
148
|
+
})
|
|
149
|
+
: new NdjsonClient(clientConfig);
|
|
150
|
+
// Set up webhook handler for NdjsonClient (LinearWebhookClient uses onWebhook in constructor)
|
|
151
|
+
if (!useLinearDirectWebhooks) {
|
|
152
|
+
ndjsonClient.on("webhook", (data) => this.handleWebhook(data, repos));
|
|
153
|
+
}
|
|
154
|
+
// Optional heartbeat logging (only for NdjsonClient)
|
|
155
|
+
if (process.env.DEBUG_EDGE === "true" && !useLinearDirectWebhooks) {
|
|
91
156
|
ndjsonClient.on("heartbeat", () => {
|
|
92
157
|
console.log(`❤️ Heartbeat received for token ending in ...${token.slice(-4)}`);
|
|
93
158
|
});
|
|
@@ -212,17 +277,16 @@ export class EdgeWorker extends EventEmitter {
|
|
|
212
277
|
* Handle webhook events from proxy - now accepts native webhook payloads
|
|
213
278
|
*/
|
|
214
279
|
async handleWebhook(webhook, repos) {
|
|
215
|
-
console.log(`[EdgeWorker] Processing webhook: ${webhook.type}`);
|
|
216
280
|
// Log verbose webhook info if enabled
|
|
217
281
|
if (process.env.CYRUS_WEBHOOK_DEBUG === "true") {
|
|
218
|
-
console.log(`[
|
|
282
|
+
console.log(`[handleWebhook] Full webhook payload:`, JSON.stringify(webhook, null, 2));
|
|
219
283
|
}
|
|
220
284
|
// Find the appropriate repository for this webhook
|
|
221
285
|
const repository = await this.findRepositoryForWebhook(webhook, repos);
|
|
222
286
|
if (!repository) {
|
|
223
|
-
console.log("No repository configured for webhook from workspace", webhook.organizationId);
|
|
224
287
|
if (process.env.CYRUS_WEBHOOK_DEBUG === "true") {
|
|
225
|
-
console.log(
|
|
288
|
+
console.log(`[handleWebhook] No repository configured for webhook from workspace ${webhook.organizationId}`);
|
|
289
|
+
console.log(`[handleWebhook] Available repositories:`, repos.map((r) => ({
|
|
226
290
|
name: r.name,
|
|
227
291
|
workspaceId: r.linearWorkspaceId,
|
|
228
292
|
teamKeys: r.teamKeys,
|
|
@@ -231,20 +295,16 @@ export class EdgeWorker extends EventEmitter {
|
|
|
231
295
|
}
|
|
232
296
|
return;
|
|
233
297
|
}
|
|
234
|
-
console.log(`[EdgeWorker] Webhook matched to repository: ${repository.name}`);
|
|
235
298
|
try {
|
|
236
299
|
// Handle specific webhook types with proper typing
|
|
237
300
|
// NOTE: Traditional webhooks (assigned, comment) are disabled in favor of agent session events
|
|
238
301
|
if (isIssueAssignedWebhook(webhook)) {
|
|
239
|
-
console.log(`[EdgeWorker] Ignoring traditional issue assigned webhook - using agent session events instead`);
|
|
240
302
|
return;
|
|
241
303
|
}
|
|
242
304
|
else if (isIssueCommentMentionWebhook(webhook)) {
|
|
243
|
-
console.log(`[EdgeWorker] Ignoring traditional comment mention webhook - using agent session events instead`);
|
|
244
305
|
return;
|
|
245
306
|
}
|
|
246
307
|
else if (isIssueNewCommentWebhook(webhook)) {
|
|
247
|
-
console.log(`[EdgeWorker] Ignoring traditional new comment webhook - using agent session events instead`);
|
|
248
308
|
return;
|
|
249
309
|
}
|
|
250
310
|
else if (isIssueUnassignedWebhook(webhook)) {
|
|
@@ -258,11 +318,13 @@ export class EdgeWorker extends EventEmitter {
|
|
|
258
318
|
await this.handleUserPostedAgentActivity(webhook, repository);
|
|
259
319
|
}
|
|
260
320
|
else {
|
|
261
|
-
|
|
321
|
+
if (process.env.CYRUS_WEBHOOK_DEBUG === "true") {
|
|
322
|
+
console.log(`[handleWebhook] Unhandled webhook type: ${webhook.action} for repository ${repository.name}`);
|
|
323
|
+
}
|
|
262
324
|
}
|
|
263
325
|
}
|
|
264
326
|
catch (error) {
|
|
265
|
-
console.error(`[
|
|
327
|
+
console.error(`[handleWebhook] Failed to process webhook: ${webhook.action} for repository ${repository.name}`, error);
|
|
266
328
|
// Don't re-throw webhook processing errors to prevent application crashes
|
|
267
329
|
// The error has been logged and individual webhook failures shouldn't crash the entire system
|
|
268
330
|
}
|
|
@@ -437,13 +499,14 @@ export class EdgeWorker extends EventEmitter {
|
|
|
437
499
|
const attachmentResult = await this.downloadIssueAttachments(fullIssue, repository, workspace.path);
|
|
438
500
|
// Pre-create attachments directory even if no attachments exist yet
|
|
439
501
|
const workspaceFolderName = basename(workspace.path);
|
|
440
|
-
const attachmentsDir = join(
|
|
502
|
+
const attachmentsDir = join(this.cyrusHome, workspaceFolderName, "attachments");
|
|
441
503
|
await mkdir(attachmentsDir, { recursive: true });
|
|
442
504
|
// Build allowed directories list - always include attachments directory
|
|
443
505
|
const allowedDirectories = [attachmentsDir];
|
|
444
506
|
console.log(`[EdgeWorker] Configured allowed directories for ${fullIssue.identifier}:`, allowedDirectories);
|
|
445
507
|
// Build allowed tools list with Linear MCP tools
|
|
446
508
|
const allowedTools = this.buildAllowedTools(repository);
|
|
509
|
+
const disallowedTools = this.buildDisallowedTools(repository);
|
|
447
510
|
return {
|
|
448
511
|
session,
|
|
449
512
|
fullIssue,
|
|
@@ -452,6 +515,7 @@ export class EdgeWorker extends EventEmitter {
|
|
|
452
515
|
attachmentsDir,
|
|
453
516
|
allowedDirectories,
|
|
454
517
|
allowedTools,
|
|
518
|
+
disallowedTools,
|
|
455
519
|
};
|
|
456
520
|
}
|
|
457
521
|
/**
|
|
@@ -469,6 +533,8 @@ export class EdgeWorker extends EventEmitter {
|
|
|
469
533
|
// HACK: This is required since the comment body is always populated, thus there is no other way to differentiate between the two trigger events
|
|
470
534
|
const AGENT_SESSION_MARKER = "This thread is for an agent session";
|
|
471
535
|
const isMentionTriggered = commentBody && !commentBody.includes(AGENT_SESSION_MARKER);
|
|
536
|
+
// Check if the comment contains the /label-based-prompt command
|
|
537
|
+
const isLabelBasedPromptRequested = commentBody?.includes("/label-based-prompt");
|
|
472
538
|
// Initialize the agent session in AgentSessionManager
|
|
473
539
|
const agentSessionManager = this.agentSessionManagers.get(repository.id);
|
|
474
540
|
if (!agentSessionManager) {
|
|
@@ -481,13 +547,14 @@ export class EdgeWorker extends EventEmitter {
|
|
|
481
547
|
const sessionData = await this.createLinearAgentSession(linearAgentActivitySessionId, issue, repository, agentSessionManager);
|
|
482
548
|
// Destructure the session data (excluding allowedTools which we'll build with promptType)
|
|
483
549
|
const { session, fullIssue, workspace: _workspace, attachmentResult, attachmentsDir: _attachmentsDir, allowedDirectories, } = sessionData;
|
|
484
|
-
//
|
|
550
|
+
// Fetch labels (needed for both model selection and system prompt determination)
|
|
551
|
+
const labels = await this.fetchIssueLabels(fullIssue);
|
|
552
|
+
// Only determine system prompt for delegation (not mentions) or when /label-based-prompt is requested
|
|
485
553
|
let systemPrompt;
|
|
486
554
|
let systemPromptVersion;
|
|
487
555
|
let promptType;
|
|
488
|
-
if (!isMentionTriggered) {
|
|
489
|
-
//
|
|
490
|
-
const labels = await this.fetchIssueLabels(fullIssue);
|
|
556
|
+
if (!isMentionTriggered || isLabelBasedPromptRequested) {
|
|
557
|
+
// Determine system prompt based on labels (delegation case or /label-based-prompt command)
|
|
491
558
|
const systemPromptResult = await this.determineSystemPromptFromLabels(labels, repository);
|
|
492
559
|
systemPrompt = systemPromptResult?.prompt;
|
|
493
560
|
systemPromptVersion = systemPromptResult?.version;
|
|
@@ -502,9 +569,14 @@ export class EdgeWorker extends EventEmitter {
|
|
|
502
569
|
}
|
|
503
570
|
// Build allowed tools list with Linear MCP tools (now with prompt type context)
|
|
504
571
|
const allowedTools = this.buildAllowedTools(repository, promptType);
|
|
572
|
+
const disallowedTools = this.buildDisallowedTools(repository, promptType);
|
|
505
573
|
console.log(`[EdgeWorker] Configured allowed tools for ${fullIssue.identifier}:`, allowedTools);
|
|
574
|
+
if (disallowedTools.length > 0) {
|
|
575
|
+
console.log(`[EdgeWorker] Configured disallowed tools for ${fullIssue.identifier}:`, disallowedTools);
|
|
576
|
+
}
|
|
506
577
|
// Create Claude runner with attachment directory access and optional system prompt
|
|
507
|
-
const runnerConfig = this.buildClaudeRunnerConfig(session, repository, linearAgentActivitySessionId, systemPrompt, allowedTools, allowedDirectories
|
|
578
|
+
const runnerConfig = this.buildClaudeRunnerConfig(session, repository, linearAgentActivitySessionId, systemPrompt, allowedTools, allowedDirectories, disallowedTools, undefined, // resumeSessionId
|
|
579
|
+
labels);
|
|
508
580
|
const runner = new ClaudeRunner(runnerConfig);
|
|
509
581
|
// Store runner by comment ID
|
|
510
582
|
agentSessionManager.addClaudeRunner(linearAgentActivitySessionId, runner);
|
|
@@ -517,11 +589,13 @@ export class EdgeWorker extends EventEmitter {
|
|
|
517
589
|
console.log(`[EdgeWorker] Building initial prompt for issue ${fullIssue.identifier}`);
|
|
518
590
|
try {
|
|
519
591
|
// Choose the appropriate prompt builder based on trigger type and system prompt
|
|
520
|
-
const promptResult = isMentionTriggered
|
|
521
|
-
? await this.
|
|
522
|
-
:
|
|
523
|
-
? await this.
|
|
524
|
-
:
|
|
592
|
+
const promptResult = isMentionTriggered && isLabelBasedPromptRequested
|
|
593
|
+
? await this.buildLabelBasedPrompt(fullIssue, repository, attachmentResult.manifest)
|
|
594
|
+
: isMentionTriggered
|
|
595
|
+
? await this.buildMentionPrompt(fullIssue, agentSession, attachmentResult.manifest)
|
|
596
|
+
: systemPrompt
|
|
597
|
+
? await this.buildLabelBasedPrompt(fullIssue, repository, attachmentResult.manifest)
|
|
598
|
+
: await this.buildPromptV2(fullIssue, repository, undefined, attachmentResult.manifest);
|
|
525
599
|
const { prompt, version: userPromptVersion } = promptResult;
|
|
526
600
|
// Update runner with version information
|
|
527
601
|
if (userPromptVersion || systemPromptVersion) {
|
|
@@ -530,11 +604,13 @@ export class EdgeWorker extends EventEmitter {
|
|
|
530
604
|
systemPromptVersion,
|
|
531
605
|
});
|
|
532
606
|
}
|
|
533
|
-
const promptType = isMentionTriggered
|
|
534
|
-
? "
|
|
535
|
-
:
|
|
536
|
-
? "
|
|
537
|
-
:
|
|
607
|
+
const promptType = isMentionTriggered && isLabelBasedPromptRequested
|
|
608
|
+
? "label-based-prompt-command"
|
|
609
|
+
: isMentionTriggered
|
|
610
|
+
? "mention"
|
|
611
|
+
: systemPrompt
|
|
612
|
+
? "label-based"
|
|
613
|
+
: "fallback";
|
|
538
614
|
console.log(`[EdgeWorker] Initial prompt built successfully using ${promptType} workflow, length: ${prompt.length} characters`);
|
|
539
615
|
console.log(`[EdgeWorker] Starting Claude streaming session`);
|
|
540
616
|
const sessionInfo = await runner.startStreaming(prompt);
|
|
@@ -600,7 +676,7 @@ export class EdgeWorker extends EventEmitter {
|
|
|
600
676
|
}
|
|
601
677
|
// Always set up attachments directory, even if no attachments in current comment
|
|
602
678
|
const workspaceFolderName = basename(session.workspace.path);
|
|
603
|
-
const attachmentsDir = join(
|
|
679
|
+
const attachmentsDir = join(this.cyrusHome, workspaceFolderName, "attachments");
|
|
604
680
|
// Ensure directory exists
|
|
605
681
|
await mkdir(attachmentsDir, { recursive: true });
|
|
606
682
|
let attachmentManifest = "";
|
|
@@ -659,44 +735,9 @@ export class EdgeWorker extends EventEmitter {
|
|
|
659
735
|
existingRunner.addStreamMessage(fullPrompt);
|
|
660
736
|
return; // Exit early - comment has been added to stream
|
|
661
737
|
}
|
|
662
|
-
//
|
|
663
|
-
if (existingRunner) {
|
|
664
|
-
existingRunner.stop();
|
|
665
|
-
}
|
|
666
|
-
// For newly created sessions or sessions without claudeSessionId, create a fresh Claude session
|
|
667
|
-
const needsNewClaudeSession = isNewSession || !session.claudeSessionId;
|
|
738
|
+
// Use the new resumeClaudeSession function
|
|
668
739
|
try {
|
|
669
|
-
|
|
670
|
-
const fullIssue = await this.fetchFullIssueDetails(issue.id, repository.id);
|
|
671
|
-
if (!fullIssue) {
|
|
672
|
-
throw new Error(`Failed to fetch full issue details for ${issue.id}`);
|
|
673
|
-
}
|
|
674
|
-
// Fetch issue labels and determine system prompt (same as in handleAgentSessionCreatedWebhook)
|
|
675
|
-
const labels = await this.fetchIssueLabels(fullIssue);
|
|
676
|
-
const systemPromptResult = await this.determineSystemPromptFromLabels(labels, repository);
|
|
677
|
-
const systemPrompt = systemPromptResult?.prompt;
|
|
678
|
-
const promptType = systemPromptResult?.type;
|
|
679
|
-
// Build allowed tools list with Linear MCP tools (now with prompt type context)
|
|
680
|
-
const allowedTools = this.buildAllowedTools(repository, promptType);
|
|
681
|
-
console.log(`[EdgeWorker] Configured allowed tools for ${issue.identifier} (continued session):`, allowedTools);
|
|
682
|
-
const allowedDirectories = [attachmentsDir];
|
|
683
|
-
// Create runner - resume existing session or start new one
|
|
684
|
-
const runnerConfig = this.buildClaudeRunnerConfig(session, repository, linearAgentActivitySessionId, systemPrompt, allowedTools, allowedDirectories, needsNewClaudeSession ? undefined : session.claudeSessionId);
|
|
685
|
-
const runner = new ClaudeRunner(runnerConfig);
|
|
686
|
-
// Store new runner by comment thread root
|
|
687
|
-
// Store runner by comment ID
|
|
688
|
-
agentSessionManager.addClaudeRunner(linearAgentActivitySessionId, runner);
|
|
689
|
-
// Save state after mapping changes
|
|
690
|
-
await this.savePersistedState();
|
|
691
|
-
// Prepare the prompt - different logic for new vs existing sessions
|
|
692
|
-
const fullPrompt = await this.buildSessionPrompt(isNewSession, fullIssue, repository, promptBody, attachmentManifest);
|
|
693
|
-
if (isNewSession) {
|
|
694
|
-
console.log(`[EdgeWorker] Building initial prompt for new session for issue ${fullIssue.identifier}`);
|
|
695
|
-
}
|
|
696
|
-
// Start streaming session
|
|
697
|
-
const sessionType = needsNewClaudeSession ? "new" : "resumed";
|
|
698
|
-
console.log(`[EdgeWorker] Starting ${sessionType} streaming session for issue ${issue.identifier}`);
|
|
699
|
-
await runner.startStreaming(fullPrompt);
|
|
740
|
+
await this.resumeClaudeSession(session, repository, linearAgentActivitySessionId, agentSessionManager, promptBody, attachmentManifest, isNewSession, []);
|
|
700
741
|
}
|
|
701
742
|
catch (error) {
|
|
702
743
|
console.error("Failed to continue conversation:", error);
|
|
@@ -776,7 +817,12 @@ export class EdgeWorker extends EventEmitter {
|
|
|
776
817
|
return undefined;
|
|
777
818
|
}
|
|
778
819
|
// Check each prompt type for matching labels
|
|
779
|
-
const promptTypes = [
|
|
820
|
+
const promptTypes = [
|
|
821
|
+
"debugger",
|
|
822
|
+
"builder",
|
|
823
|
+
"scoper",
|
|
824
|
+
"orchestrator",
|
|
825
|
+
];
|
|
780
826
|
for (const promptType of promptTypes) {
|
|
781
827
|
const promptConfig = repository.labelPrompts[promptType];
|
|
782
828
|
// Handle both old array format and new object format for backward compatibility
|
|
@@ -833,6 +879,67 @@ export class EdgeWorker extends EventEmitter {
|
|
|
833
879
|
}
|
|
834
880
|
// Determine the base branch considering parent issues
|
|
835
881
|
const baseBranch = await this.determineBaseBranch(issue, repository);
|
|
882
|
+
// Fetch assignee information
|
|
883
|
+
let assigneeId = "";
|
|
884
|
+
let assigneeName = "";
|
|
885
|
+
try {
|
|
886
|
+
if (issue.assigneeId) {
|
|
887
|
+
assigneeId = issue.assigneeId;
|
|
888
|
+
// Fetch the full assignee object to get the name
|
|
889
|
+
const assignee = await issue.assignee;
|
|
890
|
+
if (assignee) {
|
|
891
|
+
assigneeName = assignee.displayName || assignee.name || "";
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
catch (error) {
|
|
896
|
+
console.warn(`[EdgeWorker] Failed to fetch assignee details:`, error);
|
|
897
|
+
}
|
|
898
|
+
// Get LinearClient for this repository
|
|
899
|
+
const linearClient = this.linearClients.get(repository.id);
|
|
900
|
+
if (!linearClient) {
|
|
901
|
+
console.error(`No LinearClient found for repository ${repository.id}`);
|
|
902
|
+
throw new Error(`No LinearClient found for repository ${repository.id}`);
|
|
903
|
+
}
|
|
904
|
+
// Fetch workspace teams and labels
|
|
905
|
+
let workspaceTeams = "";
|
|
906
|
+
let workspaceLabels = "";
|
|
907
|
+
try {
|
|
908
|
+
console.log(`[EdgeWorker] Fetching workspace teams and labels for repository ${repository.id}`);
|
|
909
|
+
// Fetch teams
|
|
910
|
+
const teamsConnection = await linearClient.teams();
|
|
911
|
+
const teamsArray = [];
|
|
912
|
+
for (const team of teamsConnection.nodes) {
|
|
913
|
+
teamsArray.push({
|
|
914
|
+
id: team.id,
|
|
915
|
+
name: team.name,
|
|
916
|
+
key: team.key,
|
|
917
|
+
description: team.description || "",
|
|
918
|
+
color: team.color,
|
|
919
|
+
});
|
|
920
|
+
}
|
|
921
|
+
workspaceTeams = teamsArray
|
|
922
|
+
.map((team) => `- ${team.name} (${team.key}): ${team.id}${team.description ? ` - ${team.description}` : ""}`)
|
|
923
|
+
.join("\n");
|
|
924
|
+
// Fetch labels
|
|
925
|
+
const labelsConnection = await linearClient.issueLabels();
|
|
926
|
+
const labelsArray = [];
|
|
927
|
+
for (const label of labelsConnection.nodes) {
|
|
928
|
+
labelsArray.push({
|
|
929
|
+
id: label.id,
|
|
930
|
+
name: label.name,
|
|
931
|
+
description: label.description || "",
|
|
932
|
+
color: label.color,
|
|
933
|
+
});
|
|
934
|
+
}
|
|
935
|
+
workspaceLabels = labelsArray
|
|
936
|
+
.map((label) => `- ${label.name}: ${label.id}${label.description ? ` - ${label.description}` : ""}`)
|
|
937
|
+
.join("\n");
|
|
938
|
+
console.log(`[EdgeWorker] Fetched ${teamsArray.length} teams and ${labelsArray.length} labels`);
|
|
939
|
+
}
|
|
940
|
+
catch (error) {
|
|
941
|
+
console.warn(`[EdgeWorker] Failed to fetch workspace teams and labels:`, error);
|
|
942
|
+
}
|
|
836
943
|
// Build the simplified prompt with only essential variables
|
|
837
944
|
let prompt = template
|
|
838
945
|
.replace(/{{repository_name}}/g, repository.name)
|
|
@@ -841,7 +948,11 @@ export class EdgeWorker extends EventEmitter {
|
|
|
841
948
|
.replace(/{{issue_identifier}}/g, issue.identifier || "")
|
|
842
949
|
.replace(/{{issue_title}}/g, issue.title || "")
|
|
843
950
|
.replace(/{{issue_description}}/g, issue.description || "No description provided")
|
|
844
|
-
.replace(/{{issue_url}}/g, issue.url || "")
|
|
951
|
+
.replace(/{{issue_url}}/g, issue.url || "")
|
|
952
|
+
.replace(/{{assignee_id}}/g, assigneeId)
|
|
953
|
+
.replace(/{{assignee_name}}/g, assigneeName)
|
|
954
|
+
.replace(/{{workspace_teams}}/g, workspaceTeams)
|
|
955
|
+
.replace(/{{workspace_labels}}/g, workspaceLabels);
|
|
845
956
|
if (attachmentManifest) {
|
|
846
957
|
console.log(`[EdgeWorker] Adding attachment manifest to label-based prompt, length: ${attachmentManifest.length} characters`);
|
|
847
958
|
prompt = `${prompt}\n\n${attachmentManifest}`;
|
|
@@ -1355,7 +1466,8 @@ ${newComment ? `New comment to address:\n${newComment.body}\n\n` : ""}Please ana
|
|
|
1355
1466
|
if (!text)
|
|
1356
1467
|
return [];
|
|
1357
1468
|
// Match URLs that start with https://uploads.linear.app
|
|
1358
|
-
|
|
1469
|
+
// Exclude brackets and parentheses to avoid capturing malformed markdown link syntax
|
|
1470
|
+
const regex = /https:\/\/uploads\.linear\.app\/[a-zA-Z0-9/_.-]+/gi;
|
|
1359
1471
|
const matches = text.match(regex) || [];
|
|
1360
1472
|
// Remove duplicates
|
|
1361
1473
|
return [...new Set(matches)];
|
|
@@ -1369,7 +1481,7 @@ ${newComment ? `New comment to address:\n${newComment.body}\n\n` : ""}Please ana
|
|
|
1369
1481
|
async downloadIssueAttachments(issue, repository, workspacePath) {
|
|
1370
1482
|
// Create attachments directory in home directory
|
|
1371
1483
|
const workspaceFolderName = basename(workspacePath);
|
|
1372
|
-
const attachmentsDir = join(
|
|
1484
|
+
const attachmentsDir = join(this.cyrusHome, workspaceFolderName, "attachments");
|
|
1373
1485
|
try {
|
|
1374
1486
|
const attachmentMap = {};
|
|
1375
1487
|
const imageMap = {};
|
|
@@ -1377,7 +1489,7 @@ ${newComment ? `New comment to address:\n${newComment.body}\n\n` : ""}Please ana
|
|
|
1377
1489
|
let imageCount = 0;
|
|
1378
1490
|
let skippedCount = 0;
|
|
1379
1491
|
let failedCount = 0;
|
|
1380
|
-
const maxAttachments =
|
|
1492
|
+
const maxAttachments = 20;
|
|
1381
1493
|
// Ensure directory exists
|
|
1382
1494
|
await mkdir(attachmentsDir, { recursive: true });
|
|
1383
1495
|
// Extract URLs from issue description
|
|
@@ -1542,7 +1654,7 @@ ${newComment ? `New comment to address:\n${newComment.body}\n\n` : ""}Please ana
|
|
|
1542
1654
|
let newAttachmentCount = 0;
|
|
1543
1655
|
let newImageCount = 0;
|
|
1544
1656
|
let failedCount = 0;
|
|
1545
|
-
const maxAttachments =
|
|
1657
|
+
const maxAttachments = 20;
|
|
1546
1658
|
// Extract URLs from the comment
|
|
1547
1659
|
const urls = this.extractAttachmentUrls(commentBody);
|
|
1548
1660
|
if (urls.length === 0) {
|
|
@@ -1710,10 +1822,10 @@ ${newComment ? `New comment to address:\n${newComment.body}\n\n` : ""}Please ana
|
|
|
1710
1822
|
return manifest;
|
|
1711
1823
|
}
|
|
1712
1824
|
/**
|
|
1713
|
-
* Build MCP configuration with automatic Linear server injection
|
|
1825
|
+
* Build MCP configuration with automatic Linear server injection and inline cyrus tools
|
|
1714
1826
|
*/
|
|
1715
|
-
buildMcpConfig(repository) {
|
|
1716
|
-
// Always inject the Linear MCP
|
|
1827
|
+
buildMcpConfig(repository, parentSessionId) {
|
|
1828
|
+
// Always inject the Linear MCP servers with the repository's token
|
|
1717
1829
|
const mcpConfig = {
|
|
1718
1830
|
linear: {
|
|
1719
1831
|
type: "stdio",
|
|
@@ -1723,6 +1835,58 @@ ${newComment ? `New comment to address:\n${newComment.body}\n\n` : ""}Please ana
|
|
|
1723
1835
|
LINEAR_API_TOKEN: repository.linearToken,
|
|
1724
1836
|
},
|
|
1725
1837
|
},
|
|
1838
|
+
"cyrus-tools": createCyrusToolsServer(repository.linearToken, {
|
|
1839
|
+
parentSessionId,
|
|
1840
|
+
onSessionCreated: (childSessionId, parentId) => {
|
|
1841
|
+
console.log(`[EdgeWorker] Agent session created: ${childSessionId}, mapping to parent ${parentId}`);
|
|
1842
|
+
// Map child to parent session
|
|
1843
|
+
this.childToParentAgentSession.set(childSessionId, parentId);
|
|
1844
|
+
console.log(`[EdgeWorker] Parent-child mapping updated: ${this.childToParentAgentSession.size} mappings`);
|
|
1845
|
+
},
|
|
1846
|
+
onFeedbackDelivery: async (childSessionId, message) => {
|
|
1847
|
+
console.log(`[EdgeWorker] Processing feedback delivery to child session ${childSessionId}`);
|
|
1848
|
+
// Find the repository containing the child session
|
|
1849
|
+
// We need to search all repositories for this child session
|
|
1850
|
+
let childRepo;
|
|
1851
|
+
let childAgentSessionManager;
|
|
1852
|
+
for (const [repoId, manager] of this.agentSessionManagers) {
|
|
1853
|
+
if (manager.hasClaudeRunner(childSessionId)) {
|
|
1854
|
+
childRepo = this.repositories.get(repoId);
|
|
1855
|
+
childAgentSessionManager = manager;
|
|
1856
|
+
break;
|
|
1857
|
+
}
|
|
1858
|
+
}
|
|
1859
|
+
if (!childRepo || !childAgentSessionManager) {
|
|
1860
|
+
console.error(`[EdgeWorker] Child session ${childSessionId} not found in any repository`);
|
|
1861
|
+
return false;
|
|
1862
|
+
}
|
|
1863
|
+
// Get the child session
|
|
1864
|
+
const childSession = childAgentSessionManager.getSession(childSessionId);
|
|
1865
|
+
if (!childSession) {
|
|
1866
|
+
console.error(`[EdgeWorker] Child session ${childSessionId} not found`);
|
|
1867
|
+
return false;
|
|
1868
|
+
}
|
|
1869
|
+
console.log(`[EdgeWorker] Found child session - Issue: ${childSession.issueId}`);
|
|
1870
|
+
// Format the feedback as a prompt for the child session with enhanced markdown formatting
|
|
1871
|
+
const feedbackPrompt = `## Received feedback from orchestrator\n\n---\n\n${message}\n\n---`;
|
|
1872
|
+
// Resume the CHILD session with the feedback from the parent
|
|
1873
|
+
// Important: We don't await the full session completion to avoid timeouts.
|
|
1874
|
+
// The feedback is delivered immediately when the session starts, so we can
|
|
1875
|
+
// return success right away while the session continues in the background.
|
|
1876
|
+
this.resumeClaudeSession(childSession, childRepo, childSessionId, childAgentSessionManager, feedbackPrompt, "", // No attachment manifest for feedback
|
|
1877
|
+
false, // Not a new session
|
|
1878
|
+
[])
|
|
1879
|
+
.then(() => {
|
|
1880
|
+
console.log(`[EdgeWorker] Child session ${childSessionId} completed processing feedback`);
|
|
1881
|
+
})
|
|
1882
|
+
.catch((error) => {
|
|
1883
|
+
console.error(`[EdgeWorker] Failed to complete child session with feedback:`, error);
|
|
1884
|
+
});
|
|
1885
|
+
// Return success immediately after initiating the session
|
|
1886
|
+
console.log(`[EdgeWorker] Feedback delivered successfully to child session ${childSessionId}`);
|
|
1887
|
+
return true;
|
|
1888
|
+
},
|
|
1889
|
+
}),
|
|
1726
1890
|
};
|
|
1727
1891
|
return mcpConfig;
|
|
1728
1892
|
}
|
|
@@ -1740,6 +1904,8 @@ ${newComment ? `New comment to address:\n${newComment.body}\n\n` : ""}Please ana
|
|
|
1740
1904
|
return getSafeTools();
|
|
1741
1905
|
case "all":
|
|
1742
1906
|
return getAllTools();
|
|
1907
|
+
case "coordinator":
|
|
1908
|
+
return getCoordinatorTools();
|
|
1743
1909
|
default:
|
|
1744
1910
|
// If it's a string but not a preset, treat it as a single tool
|
|
1745
1911
|
return [preset];
|
|
@@ -1766,18 +1932,73 @@ ${newComment ? `New comment to address:\n${newComment.body}\n\n` : ""}Please ana
|
|
|
1766
1932
|
/**
|
|
1767
1933
|
* Build Claude runner configuration with common settings
|
|
1768
1934
|
*/
|
|
1769
|
-
buildClaudeRunnerConfig(session, repository, linearAgentActivitySessionId, systemPrompt, allowedTools, allowedDirectories, resumeSessionId) {
|
|
1935
|
+
buildClaudeRunnerConfig(session, repository, linearAgentActivitySessionId, systemPrompt, allowedTools, allowedDirectories, disallowedTools, resumeSessionId, labels) {
|
|
1936
|
+
// Configure PostToolUse hook for playwright screenshots
|
|
1937
|
+
const hooks = {
|
|
1938
|
+
PostToolUse: [
|
|
1939
|
+
{
|
|
1940
|
+
matcher: "playwright_screenshot",
|
|
1941
|
+
hooks: [
|
|
1942
|
+
async (input, _toolUseID, { signal: _signal }) => {
|
|
1943
|
+
const postToolUseInput = input;
|
|
1944
|
+
console.log(`Tool ${postToolUseInput.tool_name} completed with response:`, postToolUseInput.tool_response);
|
|
1945
|
+
return {
|
|
1946
|
+
continue: true,
|
|
1947
|
+
additionalContext: "Screenshot taken successfully. You should use the Read tool to view the screenshot file to analyze the visual content.",
|
|
1948
|
+
};
|
|
1949
|
+
},
|
|
1950
|
+
],
|
|
1951
|
+
},
|
|
1952
|
+
],
|
|
1953
|
+
};
|
|
1954
|
+
// Check for model override labels (case-insensitive)
|
|
1955
|
+
let modelOverride;
|
|
1956
|
+
let fallbackModelOverride;
|
|
1957
|
+
if (labels && labels.length > 0) {
|
|
1958
|
+
const lowercaseLabels = labels.map((label) => label.toLowerCase());
|
|
1959
|
+
// Check for model override labels: opus, sonnet, haiku
|
|
1960
|
+
if (lowercaseLabels.includes("opus")) {
|
|
1961
|
+
modelOverride = "opus";
|
|
1962
|
+
console.log(`[EdgeWorker] Model override via label: opus (for session ${linearAgentActivitySessionId})`);
|
|
1963
|
+
}
|
|
1964
|
+
else if (lowercaseLabels.includes("sonnet")) {
|
|
1965
|
+
modelOverride = "sonnet";
|
|
1966
|
+
console.log(`[EdgeWorker] Model override via label: sonnet (for session ${linearAgentActivitySessionId})`);
|
|
1967
|
+
}
|
|
1968
|
+
else if (lowercaseLabels.includes("haiku")) {
|
|
1969
|
+
modelOverride = "haiku";
|
|
1970
|
+
console.log(`[EdgeWorker] Model override via label: haiku (for session ${linearAgentActivitySessionId})`);
|
|
1971
|
+
}
|
|
1972
|
+
// If a model override is found, also set a reasonable fallback
|
|
1973
|
+
if (modelOverride) {
|
|
1974
|
+
// Set fallback to the next lower tier: opus->sonnet, sonnet->haiku, haiku->sonnet
|
|
1975
|
+
if (modelOverride === "opus") {
|
|
1976
|
+
fallbackModelOverride = "sonnet";
|
|
1977
|
+
}
|
|
1978
|
+
else if (modelOverride === "sonnet") {
|
|
1979
|
+
fallbackModelOverride = "haiku";
|
|
1980
|
+
}
|
|
1981
|
+
else {
|
|
1982
|
+
fallbackModelOverride = "sonnet"; // haiku falls back to sonnet since same model retry doesn't help
|
|
1983
|
+
}
|
|
1984
|
+
}
|
|
1985
|
+
}
|
|
1770
1986
|
const config = {
|
|
1771
1987
|
workingDirectory: session.workspace.path,
|
|
1772
1988
|
allowedTools,
|
|
1989
|
+
disallowedTools,
|
|
1773
1990
|
allowedDirectories,
|
|
1774
1991
|
workspaceName: session.issue?.identifier || session.issueId,
|
|
1992
|
+
cyrusHome: this.cyrusHome,
|
|
1775
1993
|
mcpConfigPath: repository.mcpConfigPath,
|
|
1776
|
-
mcpConfig: this.buildMcpConfig(repository),
|
|
1994
|
+
mcpConfig: this.buildMcpConfig(repository, linearAgentActivitySessionId),
|
|
1777
1995
|
appendSystemPrompt: (systemPrompt || "") + LAST_MESSAGE_MARKER,
|
|
1778
|
-
//
|
|
1779
|
-
model: repository.model || this.config.defaultModel,
|
|
1780
|
-
fallbackModel:
|
|
1996
|
+
// Priority order: label override > repository config > global default
|
|
1997
|
+
model: modelOverride || repository.model || this.config.defaultModel,
|
|
1998
|
+
fallbackModel: fallbackModelOverride ||
|
|
1999
|
+
repository.fallbackModel ||
|
|
2000
|
+
this.config.defaultFallbackModel,
|
|
2001
|
+
hooks,
|
|
1781
2002
|
onMessage: (message) => {
|
|
1782
2003
|
this.handleClaudeMessage(linearAgentActivitySessionId, message, repository.id);
|
|
1783
2004
|
},
|
|
@@ -1788,6 +2009,44 @@ ${newComment ? `New comment to address:\n${newComment.body}\n\n` : ""}Please ana
|
|
|
1788
2009
|
}
|
|
1789
2010
|
return config;
|
|
1790
2011
|
}
|
|
2012
|
+
/**
|
|
2013
|
+
* Build disallowed tools list following the same hierarchy as allowed tools
|
|
2014
|
+
*/
|
|
2015
|
+
buildDisallowedTools(repository, promptType) {
|
|
2016
|
+
let disallowedTools = [];
|
|
2017
|
+
let toolSource = "";
|
|
2018
|
+
// Priority order (same as allowedTools):
|
|
2019
|
+
// 1. Repository-specific prompt type configuration
|
|
2020
|
+
if (promptType && repository.labelPrompts?.[promptType]?.disallowedTools) {
|
|
2021
|
+
disallowedTools = repository.labelPrompts[promptType].disallowedTools;
|
|
2022
|
+
toolSource = `repository label prompt (${promptType})`;
|
|
2023
|
+
}
|
|
2024
|
+
// 2. Global prompt type defaults
|
|
2025
|
+
else if (promptType &&
|
|
2026
|
+
this.config.promptDefaults?.[promptType]?.disallowedTools) {
|
|
2027
|
+
disallowedTools = this.config.promptDefaults[promptType].disallowedTools;
|
|
2028
|
+
toolSource = `global prompt defaults (${promptType})`;
|
|
2029
|
+
}
|
|
2030
|
+
// 3. Repository-level disallowed tools
|
|
2031
|
+
else if (repository.disallowedTools) {
|
|
2032
|
+
disallowedTools = repository.disallowedTools;
|
|
2033
|
+
toolSource = "repository configuration";
|
|
2034
|
+
}
|
|
2035
|
+
// 4. Global default disallowed tools
|
|
2036
|
+
else if (this.config.defaultDisallowedTools) {
|
|
2037
|
+
disallowedTools = this.config.defaultDisallowedTools;
|
|
2038
|
+
toolSource = "global defaults";
|
|
2039
|
+
}
|
|
2040
|
+
// 5. No defaults for disallowedTools (as per requirements)
|
|
2041
|
+
else {
|
|
2042
|
+
disallowedTools = [];
|
|
2043
|
+
toolSource = "none (no defaults)";
|
|
2044
|
+
}
|
|
2045
|
+
if (disallowedTools.length > 0) {
|
|
2046
|
+
console.log(`[EdgeWorker] Disallowed tools for ${repository.name}: ${disallowedTools.length} tools from ${toolSource}`);
|
|
2047
|
+
}
|
|
2048
|
+
return disallowedTools;
|
|
2049
|
+
}
|
|
1791
2050
|
/**
|
|
1792
2051
|
* Build allowed tools list with Linear MCP tools automatically included
|
|
1793
2052
|
*/
|
|
@@ -1823,7 +2082,7 @@ ${newComment ? `New comment to address:\n${newComment.body}\n\n` : ""}Please ana
|
|
|
1823
2082
|
}
|
|
1824
2083
|
// Linear MCP tools that should always be available
|
|
1825
2084
|
// See: https://docs.anthropic.com/en/docs/claude-code/iam#tool-specific-permission-rules
|
|
1826
|
-
const linearMcpTools = ["mcp__linear"];
|
|
2085
|
+
const linearMcpTools = ["mcp__linear", "mcp__cyrus-tools"];
|
|
1827
2086
|
// Combine and deduplicate
|
|
1828
2087
|
const allTools = [...new Set([...baseTools, ...linearMcpTools])];
|
|
1829
2088
|
console.log(`[EdgeWorker] Tool selection for ${repository.name}: ${allTools.length} tools from ${toolSource}`);
|
|
@@ -1879,9 +2138,12 @@ ${newComment ? `New comment to address:\n${newComment.body}\n\n` : ""}Please ana
|
|
|
1879
2138
|
agentSessions[repositoryId] = serializedState.sessions;
|
|
1880
2139
|
agentSessionEntries[repositoryId] = serializedState.entries;
|
|
1881
2140
|
}
|
|
2141
|
+
// Serialize child to parent agent session mapping
|
|
2142
|
+
const childToParentAgentSession = Object.fromEntries(this.childToParentAgentSession.entries());
|
|
1882
2143
|
return {
|
|
1883
2144
|
agentSessions,
|
|
1884
2145
|
agentSessionEntries,
|
|
2146
|
+
childToParentAgentSession,
|
|
1885
2147
|
};
|
|
1886
2148
|
}
|
|
1887
2149
|
/**
|
|
@@ -1900,6 +2162,11 @@ ${newComment ? `New comment to address:\n${newComment.body}\n\n` : ""}Please ana
|
|
|
1900
2162
|
}
|
|
1901
2163
|
}
|
|
1902
2164
|
}
|
|
2165
|
+
// Restore child to parent agent session mapping
|
|
2166
|
+
if (state.childToParentAgentSession) {
|
|
2167
|
+
this.childToParentAgentSession = new Map(Object.entries(state.childToParentAgentSession));
|
|
2168
|
+
console.log(`[EdgeWorker] Restored ${this.childToParentAgentSession.size} child-to-parent agent session mappings`);
|
|
2169
|
+
}
|
|
1903
2170
|
}
|
|
1904
2171
|
/**
|
|
1905
2172
|
* Post instant acknowledgment thought when agent session is created
|
|
@@ -1930,6 +2197,35 @@ ${newComment ? `New comment to address:\n${newComment.body}\n\n` : ""}Please ana
|
|
|
1930
2197
|
console.error(`[EdgeWorker] Error posting instant acknowledgment:`, error);
|
|
1931
2198
|
}
|
|
1932
2199
|
}
|
|
2200
|
+
/**
|
|
2201
|
+
* Post parent resume acknowledgment thought when parent session is resumed from child
|
|
2202
|
+
*/
|
|
2203
|
+
async postParentResumeAcknowledgment(linearAgentActivitySessionId, repositoryId) {
|
|
2204
|
+
try {
|
|
2205
|
+
const linearClient = this.linearClients.get(repositoryId);
|
|
2206
|
+
if (!linearClient) {
|
|
2207
|
+
console.warn(`[EdgeWorker] No Linear client found for repository ${repositoryId}`);
|
|
2208
|
+
return;
|
|
2209
|
+
}
|
|
2210
|
+
const activityInput = {
|
|
2211
|
+
agentSessionId: linearAgentActivitySessionId,
|
|
2212
|
+
content: {
|
|
2213
|
+
type: "thought",
|
|
2214
|
+
body: "Resuming from child session",
|
|
2215
|
+
},
|
|
2216
|
+
};
|
|
2217
|
+
const result = await linearClient.createAgentActivity(activityInput);
|
|
2218
|
+
if (result.success) {
|
|
2219
|
+
console.log(`[EdgeWorker] Posted parent resumption acknowledgment thought for session ${linearAgentActivitySessionId}`);
|
|
2220
|
+
}
|
|
2221
|
+
else {
|
|
2222
|
+
console.error(`[EdgeWorker] Failed to post parent resumption acknowledgment:`, result);
|
|
2223
|
+
}
|
|
2224
|
+
}
|
|
2225
|
+
catch (error) {
|
|
2226
|
+
console.error(`[EdgeWorker] Error posting parent resumption acknowledgment:`, error);
|
|
2227
|
+
}
|
|
2228
|
+
}
|
|
1933
2229
|
/**
|
|
1934
2230
|
* Post thought about system prompt selection based on labels
|
|
1935
2231
|
*/
|
|
@@ -1977,6 +2273,18 @@ ${newComment ? `New comment to address:\n${newComment.body}\n\n` : ""}Please ana
|
|
|
1977
2273
|
selectedPromptType = "scoper";
|
|
1978
2274
|
triggerLabel = scoperLabel;
|
|
1979
2275
|
}
|
|
2276
|
+
else {
|
|
2277
|
+
// Check orchestrator labels
|
|
2278
|
+
const orchestratorConfig = repository.labelPrompts.orchestrator;
|
|
2279
|
+
const orchestratorLabels = Array.isArray(orchestratorConfig)
|
|
2280
|
+
? orchestratorConfig
|
|
2281
|
+
: orchestratorConfig?.labels;
|
|
2282
|
+
const orchestratorLabel = orchestratorLabels?.find((label) => labels.includes(label));
|
|
2283
|
+
if (orchestratorLabel) {
|
|
2284
|
+
selectedPromptType = "orchestrator";
|
|
2285
|
+
triggerLabel = orchestratorLabel;
|
|
2286
|
+
}
|
|
2287
|
+
}
|
|
1980
2288
|
}
|
|
1981
2289
|
}
|
|
1982
2290
|
}
|
|
@@ -2003,6 +2311,76 @@ ${newComment ? `New comment to address:\n${newComment.body}\n\n` : ""}Please ana
|
|
|
2003
2311
|
console.error(`[EdgeWorker] Error posting system prompt selection thought:`, error);
|
|
2004
2312
|
}
|
|
2005
2313
|
}
|
|
2314
|
+
/**
|
|
2315
|
+
* Resume or create a Claude session with the given prompt
|
|
2316
|
+
* This is the core logic for handling prompted agent activities
|
|
2317
|
+
* @param session The Cyrus agent session
|
|
2318
|
+
* @param repository The repository configuration
|
|
2319
|
+
* @param linearAgentActivitySessionId The Linear agent session ID
|
|
2320
|
+
* @param agentSessionManager The agent session manager
|
|
2321
|
+
* @param promptBody The prompt text to send
|
|
2322
|
+
* @param attachmentManifest Optional attachment manifest
|
|
2323
|
+
* @param isNewSession Whether this is a new session
|
|
2324
|
+
*/
|
|
2325
|
+
async resumeClaudeSession(session, repository, linearAgentActivitySessionId, agentSessionManager, promptBody, attachmentManifest = "", isNewSession = false, additionalAllowedDirectories = []) {
|
|
2326
|
+
// Check for existing runner
|
|
2327
|
+
const existingRunner = session.claudeRunner;
|
|
2328
|
+
// If there's an existing streaming runner, add to it
|
|
2329
|
+
if (existingRunner?.isStreaming()) {
|
|
2330
|
+
let fullPrompt = promptBody;
|
|
2331
|
+
if (attachmentManifest) {
|
|
2332
|
+
fullPrompt = `${promptBody}\n\n${attachmentManifest}`;
|
|
2333
|
+
}
|
|
2334
|
+
fullPrompt = `${fullPrompt}${LAST_MESSAGE_MARKER}`;
|
|
2335
|
+
existingRunner.addStreamMessage(fullPrompt);
|
|
2336
|
+
return;
|
|
2337
|
+
}
|
|
2338
|
+
// Stop existing runner if it's not streaming
|
|
2339
|
+
if (existingRunner) {
|
|
2340
|
+
existingRunner.stop();
|
|
2341
|
+
}
|
|
2342
|
+
// Determine if we need a new Claude session
|
|
2343
|
+
const needsNewClaudeSession = isNewSession || !session.claudeSessionId;
|
|
2344
|
+
// Fetch full issue details
|
|
2345
|
+
const fullIssue = await this.fetchFullIssueDetails(session.issueId, repository.id);
|
|
2346
|
+
if (!fullIssue) {
|
|
2347
|
+
console.error(`[resumeClaudeSession] Failed to fetch full issue details for ${session.issueId}`);
|
|
2348
|
+
throw new Error(`Failed to fetch full issue details for ${session.issueId}`);
|
|
2349
|
+
}
|
|
2350
|
+
// Fetch issue labels and determine system prompt
|
|
2351
|
+
const labels = await this.fetchIssueLabels(fullIssue);
|
|
2352
|
+
const systemPromptResult = await this.determineSystemPromptFromLabels(labels, repository);
|
|
2353
|
+
const systemPrompt = systemPromptResult?.prompt;
|
|
2354
|
+
const promptType = systemPromptResult?.type;
|
|
2355
|
+
// Build allowed tools list
|
|
2356
|
+
const allowedTools = this.buildAllowedTools(repository, promptType);
|
|
2357
|
+
const disallowedTools = this.buildDisallowedTools(repository, promptType);
|
|
2358
|
+
// Set up attachments directory
|
|
2359
|
+
const workspaceFolderName = basename(session.workspace.path);
|
|
2360
|
+
const attachmentsDir = join(this.cyrusHome, workspaceFolderName, "attachments");
|
|
2361
|
+
await mkdir(attachmentsDir, { recursive: true });
|
|
2362
|
+
const allowedDirectories = [
|
|
2363
|
+
attachmentsDir,
|
|
2364
|
+
...additionalAllowedDirectories,
|
|
2365
|
+
];
|
|
2366
|
+
// Create runner configuration
|
|
2367
|
+
const runnerConfig = this.buildClaudeRunnerConfig(session, repository, linearAgentActivitySessionId, systemPrompt, allowedTools, allowedDirectories, disallowedTools, needsNewClaudeSession ? undefined : session.claudeSessionId, labels);
|
|
2368
|
+
const runner = new ClaudeRunner(runnerConfig);
|
|
2369
|
+
// Store runner
|
|
2370
|
+
agentSessionManager.addClaudeRunner(linearAgentActivitySessionId, runner);
|
|
2371
|
+
// Save state
|
|
2372
|
+
await this.savePersistedState();
|
|
2373
|
+
// Prepare the full prompt
|
|
2374
|
+
const fullPrompt = await this.buildSessionPrompt(isNewSession, fullIssue, repository, promptBody, attachmentManifest);
|
|
2375
|
+
// Start streaming session
|
|
2376
|
+
try {
|
|
2377
|
+
await runner.startStreaming(fullPrompt);
|
|
2378
|
+
}
|
|
2379
|
+
catch (error) {
|
|
2380
|
+
console.error(`[resumeClaudeSession] Failed to start streaming session for ${linearAgentActivitySessionId}:`, error);
|
|
2381
|
+
throw error;
|
|
2382
|
+
}
|
|
2383
|
+
}
|
|
2006
2384
|
/**
|
|
2007
2385
|
* Post instant acknowledgment thought when receiving prompted webhook
|
|
2008
2386
|
*/
|