cyrus-edge-worker 0.0.26 → 0.0.28
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 +3 -1
- package/dist/AgentSessionManager.d.ts.map +1 -1
- package/dist/AgentSessionManager.js +23 -1
- package/dist/AgentSessionManager.js.map +1 -1
- package/dist/EdgeWorker.d.ts +25 -2
- package/dist/EdgeWorker.d.ts.map +1 -1
- package/dist/EdgeWorker.js +342 -66
- package/dist/EdgeWorker.js.map +1 -1
- package/dist/types.d.ts +14 -6
- package/dist/types.d.ts.map +1 -1
- package/package.json +5 -5
- package/prompts/orchestrator.md +174 -0
package/dist/EdgeWorker.js
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
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, getAllTools, getCoordinatorTools, getReadOnlyTools, getSafeTools, } from "cyrus-claude-runner";
|
|
8
7
|
import { isAgentSessionCreatedWebhook, isAgentSessionPromptedWebhook, isIssueAssignedWebhook, isIssueCommentMentionWebhook, isIssueNewCommentWebhook, isIssueUnassignedWebhook, PersistenceManager, } from "cyrus-core";
|
|
9
8
|
import { NdjsonClient } from "cyrus-ndjson-client";
|
|
10
9
|
import { fileTypeFromBuffer } from "file-type";
|
|
@@ -25,10 +24,15 @@ export class EdgeWorker extends EventEmitter {
|
|
|
25
24
|
ndjsonClients = new Map(); // listeners for webhook events, one per linear token
|
|
26
25
|
persistenceManager;
|
|
27
26
|
sharedApplicationServer;
|
|
27
|
+
cyrusHome;
|
|
28
|
+
childToParentAgentSession = new Map(); // Maps child agentSessionId to parent agentSessionId
|
|
28
29
|
constructor(config) {
|
|
29
30
|
super();
|
|
30
31
|
this.config = config;
|
|
31
|
-
this.
|
|
32
|
+
this.cyrusHome = config.cyrusHome;
|
|
33
|
+
this.persistenceManager = new PersistenceManager(join(this.cyrusHome, "state"));
|
|
34
|
+
console.log(`[EdgeWorker Constructor] Initializing parent-child session mapping system`);
|
|
35
|
+
console.log(`[EdgeWorker Constructor] Parent-child mapping initialized with 0 entries`);
|
|
32
36
|
// Initialize shared application server
|
|
33
37
|
const serverPort = config.serverPort || config.webhookPort || 3456;
|
|
34
38
|
const serverHost = config.serverHost || "localhost";
|
|
@@ -46,8 +50,46 @@ export class EdgeWorker extends EventEmitter {
|
|
|
46
50
|
accessToken: repo.linearToken,
|
|
47
51
|
});
|
|
48
52
|
this.linearClients.set(repo.id, linearClient);
|
|
49
|
-
// Create AgentSessionManager for this repository
|
|
50
|
-
|
|
53
|
+
// Create AgentSessionManager for this repository with parent session lookup and resume callback
|
|
54
|
+
//
|
|
55
|
+
// Note: This pattern works (despite appearing recursive) because:
|
|
56
|
+
// 1. The agentSessionManager variable is captured by the closure after it's assigned
|
|
57
|
+
// 2. JavaScript's variable hoisting means 'agentSessionManager' exists (but is undefined) when the arrow function is created
|
|
58
|
+
// 3. By the time the callback is actually invoked (when a child session completes), agentSessionManager is fully initialized
|
|
59
|
+
// 4. The callback only executes asynchronously, well after the constructor has completed and agentSessionManager is assigned
|
|
60
|
+
//
|
|
61
|
+
// This allows the AgentSessionManager to call back into itself to access its own sessions,
|
|
62
|
+
// enabling child sessions to trigger parent session resumption using the same manager instance.
|
|
63
|
+
const agentSessionManager = new AgentSessionManager(linearClient, (childSessionId) => {
|
|
64
|
+
console.log(`[Parent-Child Lookup] Looking up parent session for child ${childSessionId}`);
|
|
65
|
+
const parentId = this.childToParentAgentSession.get(childSessionId);
|
|
66
|
+
console.log(`[Parent-Child Lookup] Child ${childSessionId} -> Parent ${parentId || "not found"}`);
|
|
67
|
+
return parentId;
|
|
68
|
+
}, async (parentSessionId, prompt) => {
|
|
69
|
+
console.log(`[Parent Session Resume] Child session completed, resuming parent session ${parentSessionId}`);
|
|
70
|
+
// Get the parent session and repository
|
|
71
|
+
// This works because by the time this callback runs, agentSessionManager is fully initialized
|
|
72
|
+
console.log(`[Parent Session Resume] Retrieving parent session ${parentSessionId} from agent session manager`);
|
|
73
|
+
const parentSession = agentSessionManager.getSession(parentSessionId);
|
|
74
|
+
if (!parentSession) {
|
|
75
|
+
console.error(`[Parent Session Resume] Parent session ${parentSessionId} not found in agent session manager`);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
console.log(`[Parent Session Resume] Found parent session - Issue: ${parentSession.issueId}, Workspace: ${parentSession.workspace.path}`);
|
|
79
|
+
await this.postParentResumeAcknowledgment(parentSessionId, repo.id);
|
|
80
|
+
// Resume the parent session with the child's result
|
|
81
|
+
console.log(`[Parent Session Resume] Resuming parent Claude session with child results`);
|
|
82
|
+
try {
|
|
83
|
+
await this.resumeClaudeSession(parentSession, repo, parentSessionId, agentSessionManager, prompt, "", // No attachment manifest for child results
|
|
84
|
+
false);
|
|
85
|
+
console.log(`[Parent Session Resume] Successfully resumed parent session ${parentSessionId} with child results`);
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
console.error(`[Parent Session Resume] Failed to resume parent session ${parentSessionId}:`, error);
|
|
89
|
+
console.error(`[Parent Session Resume] Error context - Parent issue: ${parentSession.issueId}, Repository: ${repo.name}`);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
this.agentSessionManagers.set(repo.id, agentSessionManager);
|
|
51
93
|
}
|
|
52
94
|
}
|
|
53
95
|
// Group repositories by token to minimize NDJSON connections
|
|
@@ -212,17 +254,16 @@ export class EdgeWorker extends EventEmitter {
|
|
|
212
254
|
* Handle webhook events from proxy - now accepts native webhook payloads
|
|
213
255
|
*/
|
|
214
256
|
async handleWebhook(webhook, repos) {
|
|
215
|
-
console.log(`[EdgeWorker] Processing webhook: ${webhook.type}`);
|
|
216
257
|
// Log verbose webhook info if enabled
|
|
217
258
|
if (process.env.CYRUS_WEBHOOK_DEBUG === "true") {
|
|
218
|
-
console.log(`[
|
|
259
|
+
console.log(`[handleWebhook] Full webhook payload:`, JSON.stringify(webhook, null, 2));
|
|
219
260
|
}
|
|
220
261
|
// Find the appropriate repository for this webhook
|
|
221
262
|
const repository = await this.findRepositoryForWebhook(webhook, repos);
|
|
222
263
|
if (!repository) {
|
|
223
|
-
console.log("No repository configured for webhook from workspace", webhook.organizationId);
|
|
224
264
|
if (process.env.CYRUS_WEBHOOK_DEBUG === "true") {
|
|
225
|
-
console.log(
|
|
265
|
+
console.log(`[handleWebhook] No repository configured for webhook from workspace ${webhook.organizationId}`);
|
|
266
|
+
console.log(`[handleWebhook] Available repositories:`, repos.map((r) => ({
|
|
226
267
|
name: r.name,
|
|
227
268
|
workspaceId: r.linearWorkspaceId,
|
|
228
269
|
teamKeys: r.teamKeys,
|
|
@@ -231,20 +272,16 @@ export class EdgeWorker extends EventEmitter {
|
|
|
231
272
|
}
|
|
232
273
|
return;
|
|
233
274
|
}
|
|
234
|
-
console.log(`[EdgeWorker] Webhook matched to repository: ${repository.name}`);
|
|
235
275
|
try {
|
|
236
276
|
// Handle specific webhook types with proper typing
|
|
237
277
|
// NOTE: Traditional webhooks (assigned, comment) are disabled in favor of agent session events
|
|
238
278
|
if (isIssueAssignedWebhook(webhook)) {
|
|
239
|
-
console.log(`[EdgeWorker] Ignoring traditional issue assigned webhook - using agent session events instead`);
|
|
240
279
|
return;
|
|
241
280
|
}
|
|
242
281
|
else if (isIssueCommentMentionWebhook(webhook)) {
|
|
243
|
-
console.log(`[EdgeWorker] Ignoring traditional comment mention webhook - using agent session events instead`);
|
|
244
282
|
return;
|
|
245
283
|
}
|
|
246
284
|
else if (isIssueNewCommentWebhook(webhook)) {
|
|
247
|
-
console.log(`[EdgeWorker] Ignoring traditional new comment webhook - using agent session events instead`);
|
|
248
285
|
return;
|
|
249
286
|
}
|
|
250
287
|
else if (isIssueUnassignedWebhook(webhook)) {
|
|
@@ -258,11 +295,13 @@ export class EdgeWorker extends EventEmitter {
|
|
|
258
295
|
await this.handleUserPostedAgentActivity(webhook, repository);
|
|
259
296
|
}
|
|
260
297
|
else {
|
|
261
|
-
|
|
298
|
+
if (process.env.CYRUS_WEBHOOK_DEBUG === "true") {
|
|
299
|
+
console.log(`[handleWebhook] Unhandled webhook type: ${webhook.action} for repository ${repository.name}`);
|
|
300
|
+
}
|
|
262
301
|
}
|
|
263
302
|
}
|
|
264
303
|
catch (error) {
|
|
265
|
-
console.error(`[
|
|
304
|
+
console.error(`[handleWebhook] Failed to process webhook: ${webhook.action} for repository ${repository.name}`, error);
|
|
266
305
|
// Don't re-throw webhook processing errors to prevent application crashes
|
|
267
306
|
// The error has been logged and individual webhook failures shouldn't crash the entire system
|
|
268
307
|
}
|
|
@@ -437,7 +476,7 @@ export class EdgeWorker extends EventEmitter {
|
|
|
437
476
|
const attachmentResult = await this.downloadIssueAttachments(fullIssue, repository, workspace.path);
|
|
438
477
|
// Pre-create attachments directory even if no attachments exist yet
|
|
439
478
|
const workspaceFolderName = basename(workspace.path);
|
|
440
|
-
const attachmentsDir = join(
|
|
479
|
+
const attachmentsDir = join(this.cyrusHome, workspaceFolderName, "attachments");
|
|
441
480
|
await mkdir(attachmentsDir, { recursive: true });
|
|
442
481
|
// Build allowed directories list - always include attachments directory
|
|
443
482
|
const allowedDirectories = [attachmentsDir];
|
|
@@ -481,13 +520,14 @@ export class EdgeWorker extends EventEmitter {
|
|
|
481
520
|
const sessionData = await this.createLinearAgentSession(linearAgentActivitySessionId, issue, repository, agentSessionManager);
|
|
482
521
|
// Destructure the session data (excluding allowedTools which we'll build with promptType)
|
|
483
522
|
const { session, fullIssue, workspace: _workspace, attachmentResult, attachmentsDir: _attachmentsDir, allowedDirectories, } = sessionData;
|
|
484
|
-
//
|
|
523
|
+
// Fetch labels (needed for both model selection and system prompt determination)
|
|
524
|
+
const labels = await this.fetchIssueLabels(fullIssue);
|
|
525
|
+
// Only determine system prompt for delegation (not mentions)
|
|
485
526
|
let systemPrompt;
|
|
486
527
|
let systemPromptVersion;
|
|
487
528
|
let promptType;
|
|
488
529
|
if (!isMentionTriggered) {
|
|
489
|
-
//
|
|
490
|
-
const labels = await this.fetchIssueLabels(fullIssue);
|
|
530
|
+
// Determine system prompt based on labels (delegation case)
|
|
491
531
|
const systemPromptResult = await this.determineSystemPromptFromLabels(labels, repository);
|
|
492
532
|
systemPrompt = systemPromptResult?.prompt;
|
|
493
533
|
systemPromptVersion = systemPromptResult?.version;
|
|
@@ -504,7 +544,9 @@ export class EdgeWorker extends EventEmitter {
|
|
|
504
544
|
const allowedTools = this.buildAllowedTools(repository, promptType);
|
|
505
545
|
console.log(`[EdgeWorker] Configured allowed tools for ${fullIssue.identifier}:`, allowedTools);
|
|
506
546
|
// Create Claude runner with attachment directory access and optional system prompt
|
|
507
|
-
const runnerConfig = this.buildClaudeRunnerConfig(session, repository, linearAgentActivitySessionId, systemPrompt, allowedTools, allowedDirectories
|
|
547
|
+
const runnerConfig = this.buildClaudeRunnerConfig(session, repository, linearAgentActivitySessionId, agentSessionManager, systemPrompt, allowedTools, allowedDirectories, undefined, // resumeSessionId
|
|
548
|
+
linearAgentActivitySessionId, // Pass current session ID as parent context
|
|
549
|
+
labels);
|
|
508
550
|
const runner = new ClaudeRunner(runnerConfig);
|
|
509
551
|
// Store runner by comment ID
|
|
510
552
|
agentSessionManager.addClaudeRunner(linearAgentActivitySessionId, runner);
|
|
@@ -600,7 +642,7 @@ export class EdgeWorker extends EventEmitter {
|
|
|
600
642
|
}
|
|
601
643
|
// Always set up attachments directory, even if no attachments in current comment
|
|
602
644
|
const workspaceFolderName = basename(session.workspace.path);
|
|
603
|
-
const attachmentsDir = join(
|
|
645
|
+
const attachmentsDir = join(this.cyrusHome, workspaceFolderName, "attachments");
|
|
604
646
|
// Ensure directory exists
|
|
605
647
|
await mkdir(attachmentsDir, { recursive: true });
|
|
606
648
|
let attachmentManifest = "";
|
|
@@ -659,44 +701,9 @@ export class EdgeWorker extends EventEmitter {
|
|
|
659
701
|
existingRunner.addStreamMessage(fullPrompt);
|
|
660
702
|
return; // Exit early - comment has been added to stream
|
|
661
703
|
}
|
|
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;
|
|
704
|
+
// Use the new resumeClaudeSession function
|
|
668
705
|
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);
|
|
706
|
+
await this.resumeClaudeSession(session, repository, linearAgentActivitySessionId, agentSessionManager, promptBody, attachmentManifest, isNewSession);
|
|
700
707
|
}
|
|
701
708
|
catch (error) {
|
|
702
709
|
console.error("Failed to continue conversation:", error);
|
|
@@ -776,7 +783,12 @@ export class EdgeWorker extends EventEmitter {
|
|
|
776
783
|
return undefined;
|
|
777
784
|
}
|
|
778
785
|
// Check each prompt type for matching labels
|
|
779
|
-
const promptTypes = [
|
|
786
|
+
const promptTypes = [
|
|
787
|
+
"debugger",
|
|
788
|
+
"builder",
|
|
789
|
+
"scoper",
|
|
790
|
+
"orchestrator",
|
|
791
|
+
];
|
|
780
792
|
for (const promptType of promptTypes) {
|
|
781
793
|
const promptConfig = repository.labelPrompts[promptType];
|
|
782
794
|
// Handle both old array format and new object format for backward compatibility
|
|
@@ -1255,6 +1267,26 @@ ${newComment ? `New comment to address:\n${newComment.body}\n\n` : ""}Please ana
|
|
|
1255
1267
|
* @param issue Full Linear issue object from Linear SDK
|
|
1256
1268
|
* @param repositoryId Repository ID for Linear client lookup
|
|
1257
1269
|
*/
|
|
1270
|
+
/**
|
|
1271
|
+
* Get the repository configuration for a given session
|
|
1272
|
+
*/
|
|
1273
|
+
getRepositoryForSession(session) {
|
|
1274
|
+
// Find the repository that matches the session's workspace path
|
|
1275
|
+
for (const repo of this.config.repositories) {
|
|
1276
|
+
if (session.workspace.path.includes(repo.workspaceBaseDir)) {
|
|
1277
|
+
return repo;
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
// Fallback: try to find by issue ID in Linear client
|
|
1281
|
+
for (const [repoId, _] of this.linearClients) {
|
|
1282
|
+
const repo = this.config.repositories.find((r) => r.id === repoId);
|
|
1283
|
+
if (repo) {
|
|
1284
|
+
// Additional checks could be added here if needed
|
|
1285
|
+
return repo;
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
return undefined;
|
|
1289
|
+
}
|
|
1258
1290
|
async moveIssueToStartedState(issue, repositoryId) {
|
|
1259
1291
|
try {
|
|
1260
1292
|
const linearClient = this.linearClients.get(repositoryId);
|
|
@@ -1369,7 +1401,7 @@ ${newComment ? `New comment to address:\n${newComment.body}\n\n` : ""}Please ana
|
|
|
1369
1401
|
async downloadIssueAttachments(issue, repository, workspacePath) {
|
|
1370
1402
|
// Create attachments directory in home directory
|
|
1371
1403
|
const workspaceFolderName = basename(workspacePath);
|
|
1372
|
-
const attachmentsDir = join(
|
|
1404
|
+
const attachmentsDir = join(this.cyrusHome, workspaceFolderName, "attachments");
|
|
1373
1405
|
try {
|
|
1374
1406
|
const attachmentMap = {};
|
|
1375
1407
|
const imageMap = {};
|
|
@@ -1713,7 +1745,7 @@ ${newComment ? `New comment to address:\n${newComment.body}\n\n` : ""}Please ana
|
|
|
1713
1745
|
* Build MCP configuration with automatic Linear server injection
|
|
1714
1746
|
*/
|
|
1715
1747
|
buildMcpConfig(repository) {
|
|
1716
|
-
// Always inject the Linear MCP
|
|
1748
|
+
// Always inject the Linear MCP servers with the repository's token
|
|
1717
1749
|
const mcpConfig = {
|
|
1718
1750
|
linear: {
|
|
1719
1751
|
type: "stdio",
|
|
@@ -1723,6 +1755,14 @@ ${newComment ? `New comment to address:\n${newComment.body}\n\n` : ""}Please ana
|
|
|
1723
1755
|
LINEAR_API_TOKEN: repository.linearToken,
|
|
1724
1756
|
},
|
|
1725
1757
|
},
|
|
1758
|
+
"cyrus-mcp-tools": {
|
|
1759
|
+
type: "stdio",
|
|
1760
|
+
command: "npx",
|
|
1761
|
+
args: ["-y", "cyrus-mcp-tools"],
|
|
1762
|
+
env: {
|
|
1763
|
+
LINEAR_API_TOKEN: repository.linearToken,
|
|
1764
|
+
},
|
|
1765
|
+
},
|
|
1726
1766
|
};
|
|
1727
1767
|
return mcpConfig;
|
|
1728
1768
|
}
|
|
@@ -1740,6 +1780,8 @@ ${newComment ? `New comment to address:\n${newComment.body}\n\n` : ""}Please ana
|
|
|
1740
1780
|
return getSafeTools();
|
|
1741
1781
|
case "all":
|
|
1742
1782
|
return getAllTools();
|
|
1783
|
+
case "coordinator":
|
|
1784
|
+
return getCoordinatorTools();
|
|
1743
1785
|
default:
|
|
1744
1786
|
// If it's a string but not a preset, treat it as a single tool
|
|
1745
1787
|
return [preset];
|
|
@@ -1766,18 +1808,137 @@ ${newComment ? `New comment to address:\n${newComment.body}\n\n` : ""}Please ana
|
|
|
1766
1808
|
/**
|
|
1767
1809
|
* Build Claude runner configuration with common settings
|
|
1768
1810
|
*/
|
|
1769
|
-
buildClaudeRunnerConfig(session, repository, linearAgentActivitySessionId, systemPrompt, allowedTools, allowedDirectories, resumeSessionId) {
|
|
1811
|
+
buildClaudeRunnerConfig(session, repository, linearAgentActivitySessionId, agentSessionManager, systemPrompt, allowedTools, allowedDirectories, resumeSessionId, parentAgentSessionId, labels) {
|
|
1812
|
+
// Build hooks configuration
|
|
1813
|
+
const hooks = {
|
|
1814
|
+
PostToolUse: [
|
|
1815
|
+
{
|
|
1816
|
+
matcher: "mcp__cyrus-mcp-tools__linear_agent_session_create",
|
|
1817
|
+
hooks: [
|
|
1818
|
+
async (input, _toolUseID, _options) => {
|
|
1819
|
+
// Check if this is the linear_agent_session_create tool
|
|
1820
|
+
if (input.tool_name ===
|
|
1821
|
+
"mcp__cyrus-mcp-tools__linear_agent_session_create") {
|
|
1822
|
+
const toolResponse = input.tool_response;
|
|
1823
|
+
// The response is an array with a single object containing type and text fields
|
|
1824
|
+
// Parse the JSON from the text field to get the agentSessionId
|
|
1825
|
+
if (Array.isArray(toolResponse) &&
|
|
1826
|
+
toolResponse.length > 0 &&
|
|
1827
|
+
toolResponse[0].type === "text" &&
|
|
1828
|
+
toolResponse[0].text) {
|
|
1829
|
+
try {
|
|
1830
|
+
const responseData = JSON.parse(toolResponse[0].text);
|
|
1831
|
+
const childAgentSessionId = responseData.agentSessionId;
|
|
1832
|
+
// If there's a parent session, create the mapping
|
|
1833
|
+
if (parentAgentSessionId && childAgentSessionId) {
|
|
1834
|
+
console.log(`[Parent-Child Mapping] Creating: child ${childAgentSessionId} -> parent ${parentAgentSessionId}`);
|
|
1835
|
+
this.childToParentAgentSession.set(childAgentSessionId, parentAgentSessionId);
|
|
1836
|
+
console.log(`[Parent-Child Mapping] Successfully created. Total mappings: ${this.childToParentAgentSession.size}`);
|
|
1837
|
+
// Save state after adding new mapping
|
|
1838
|
+
this.savePersistedState().catch((error) => {
|
|
1839
|
+
console.error(`[Parent-Child Mapping] Failed to save state after creating mapping:`, error);
|
|
1840
|
+
});
|
|
1841
|
+
}
|
|
1842
|
+
}
|
|
1843
|
+
catch (error) {
|
|
1844
|
+
console.error(`[Parent-Child Mapping] Failed to parse agentSessionId from tool response:`, error);
|
|
1845
|
+
}
|
|
1846
|
+
}
|
|
1847
|
+
}
|
|
1848
|
+
return { continue: true };
|
|
1849
|
+
},
|
|
1850
|
+
],
|
|
1851
|
+
},
|
|
1852
|
+
{
|
|
1853
|
+
matcher: "mcp__cyrus-mcp-tools__linear_agent_give_feedback",
|
|
1854
|
+
hooks: [
|
|
1855
|
+
async (input, _toolUseID, _options) => {
|
|
1856
|
+
// Check if this is the give_feedback tool
|
|
1857
|
+
if (input.tool_name ===
|
|
1858
|
+
"mcp__cyrus-mcp-tools__linear_agent_give_feedback") {
|
|
1859
|
+
const toolInput = input.tool_input;
|
|
1860
|
+
const childAgentSessionId = toolInput?.agentSessionId;
|
|
1861
|
+
const feedbackMessage = toolInput?.message;
|
|
1862
|
+
if (childAgentSessionId && feedbackMessage) {
|
|
1863
|
+
console.log(`[Give Feedback] Triggering child session resumption: ${childAgentSessionId}`);
|
|
1864
|
+
// Get the child session
|
|
1865
|
+
const childSession = agentSessionManager.getSession(childAgentSessionId);
|
|
1866
|
+
if (!childSession) {
|
|
1867
|
+
console.error(`[Give Feedback] Child session not found: ${childAgentSessionId}`);
|
|
1868
|
+
return { continue: true };
|
|
1869
|
+
}
|
|
1870
|
+
// Find the repository for this session
|
|
1871
|
+
const repo = this.getRepositoryForSession(childSession);
|
|
1872
|
+
if (!repo) {
|
|
1873
|
+
console.error(`[Give Feedback] Repository not found for child session: ${childAgentSessionId}`);
|
|
1874
|
+
return { continue: true };
|
|
1875
|
+
}
|
|
1876
|
+
// Prepare the prompt with the feedback message and child session ID
|
|
1877
|
+
const prompt = `Feedback from parent session regarding child agent session ${childAgentSessionId}:\n\n${feedbackMessage}`;
|
|
1878
|
+
// Resume the child session with the feedback
|
|
1879
|
+
try {
|
|
1880
|
+
await this.resumeClaudeSession(childSession, repo, childAgentSessionId, agentSessionManager, prompt, "", // No attachment manifest for feedback
|
|
1881
|
+
false);
|
|
1882
|
+
console.log(`[Give Feedback] Successfully resumed child session ${childAgentSessionId} with feedback`);
|
|
1883
|
+
}
|
|
1884
|
+
catch (error) {
|
|
1885
|
+
console.error(`[Give Feedback] Failed to resume child session ${childAgentSessionId}:`, error);
|
|
1886
|
+
}
|
|
1887
|
+
}
|
|
1888
|
+
}
|
|
1889
|
+
return { continue: true };
|
|
1890
|
+
},
|
|
1891
|
+
],
|
|
1892
|
+
},
|
|
1893
|
+
],
|
|
1894
|
+
};
|
|
1895
|
+
// Check for model override labels (case-insensitive)
|
|
1896
|
+
let modelOverride;
|
|
1897
|
+
let fallbackModelOverride;
|
|
1898
|
+
if (labels && labels.length > 0) {
|
|
1899
|
+
const lowercaseLabels = labels.map((label) => label.toLowerCase());
|
|
1900
|
+
// Check for model override labels: opus, sonnet, haiku
|
|
1901
|
+
if (lowercaseLabels.includes("opus")) {
|
|
1902
|
+
modelOverride = "opus";
|
|
1903
|
+
console.log(`[EdgeWorker] Model override via label: opus (for session ${linearAgentActivitySessionId})`);
|
|
1904
|
+
}
|
|
1905
|
+
else if (lowercaseLabels.includes("sonnet")) {
|
|
1906
|
+
modelOverride = "sonnet";
|
|
1907
|
+
console.log(`[EdgeWorker] Model override via label: sonnet (for session ${linearAgentActivitySessionId})`);
|
|
1908
|
+
}
|
|
1909
|
+
else if (lowercaseLabels.includes("haiku")) {
|
|
1910
|
+
modelOverride = "haiku";
|
|
1911
|
+
console.log(`[EdgeWorker] Model override via label: haiku (for session ${linearAgentActivitySessionId})`);
|
|
1912
|
+
}
|
|
1913
|
+
// If a model override is found, also set a reasonable fallback
|
|
1914
|
+
if (modelOverride) {
|
|
1915
|
+
// Set fallback to the next lower tier: opus->sonnet, sonnet->haiku, haiku->haiku
|
|
1916
|
+
if (modelOverride === "opus") {
|
|
1917
|
+
fallbackModelOverride = "sonnet";
|
|
1918
|
+
}
|
|
1919
|
+
else if (modelOverride === "sonnet") {
|
|
1920
|
+
fallbackModelOverride = "haiku";
|
|
1921
|
+
}
|
|
1922
|
+
else {
|
|
1923
|
+
fallbackModelOverride = "haiku";
|
|
1924
|
+
}
|
|
1925
|
+
}
|
|
1926
|
+
}
|
|
1770
1927
|
const config = {
|
|
1771
1928
|
workingDirectory: session.workspace.path,
|
|
1772
1929
|
allowedTools,
|
|
1773
1930
|
allowedDirectories,
|
|
1774
1931
|
workspaceName: session.issue?.identifier || session.issueId,
|
|
1932
|
+
cyrusHome: this.cyrusHome,
|
|
1775
1933
|
mcpConfigPath: repository.mcpConfigPath,
|
|
1776
1934
|
mcpConfig: this.buildMcpConfig(repository),
|
|
1777
1935
|
appendSystemPrompt: (systemPrompt || "") + LAST_MESSAGE_MARKER,
|
|
1778
|
-
//
|
|
1779
|
-
model: repository.model || this.config.defaultModel,
|
|
1780
|
-
fallbackModel:
|
|
1936
|
+
// Priority order: label override > repository config > global default
|
|
1937
|
+
model: modelOverride || repository.model || this.config.defaultModel,
|
|
1938
|
+
fallbackModel: fallbackModelOverride ||
|
|
1939
|
+
repository.fallbackModel ||
|
|
1940
|
+
this.config.defaultFallbackModel,
|
|
1941
|
+
hooks,
|
|
1781
1942
|
onMessage: (message) => {
|
|
1782
1943
|
this.handleClaudeMessage(linearAgentActivitySessionId, message, repository.id);
|
|
1783
1944
|
},
|
|
@@ -1823,7 +1984,7 @@ ${newComment ? `New comment to address:\n${newComment.body}\n\n` : ""}Please ana
|
|
|
1823
1984
|
}
|
|
1824
1985
|
// Linear MCP tools that should always be available
|
|
1825
1986
|
// See: https://docs.anthropic.com/en/docs/claude-code/iam#tool-specific-permission-rules
|
|
1826
|
-
const linearMcpTools = ["mcp__linear"];
|
|
1987
|
+
const linearMcpTools = ["mcp__linear", "mcp__cyrus-mcp-tools"];
|
|
1827
1988
|
// Combine and deduplicate
|
|
1828
1989
|
const allTools = [...new Set([...baseTools, ...linearMcpTools])];
|
|
1829
1990
|
console.log(`[EdgeWorker] Tool selection for ${repository.name}: ${allTools.length} tools from ${toolSource}`);
|
|
@@ -1879,9 +2040,12 @@ ${newComment ? `New comment to address:\n${newComment.body}\n\n` : ""}Please ana
|
|
|
1879
2040
|
agentSessions[repositoryId] = serializedState.sessions;
|
|
1880
2041
|
agentSessionEntries[repositoryId] = serializedState.entries;
|
|
1881
2042
|
}
|
|
2043
|
+
// Serialize child to parent agent session mapping
|
|
2044
|
+
const childToParentAgentSession = Object.fromEntries(this.childToParentAgentSession.entries());
|
|
1882
2045
|
return {
|
|
1883
2046
|
agentSessions,
|
|
1884
2047
|
agentSessionEntries,
|
|
2048
|
+
childToParentAgentSession,
|
|
1885
2049
|
};
|
|
1886
2050
|
}
|
|
1887
2051
|
/**
|
|
@@ -1900,6 +2064,11 @@ ${newComment ? `New comment to address:\n${newComment.body}\n\n` : ""}Please ana
|
|
|
1900
2064
|
}
|
|
1901
2065
|
}
|
|
1902
2066
|
}
|
|
2067
|
+
// Restore child to parent agent session mapping
|
|
2068
|
+
if (state.childToParentAgentSession) {
|
|
2069
|
+
this.childToParentAgentSession = new Map(Object.entries(state.childToParentAgentSession));
|
|
2070
|
+
console.log(`[EdgeWorker] Restored ${this.childToParentAgentSession.size} child-to-parent agent session mappings`);
|
|
2071
|
+
}
|
|
1903
2072
|
}
|
|
1904
2073
|
/**
|
|
1905
2074
|
* Post instant acknowledgment thought when agent session is created
|
|
@@ -1930,6 +2099,35 @@ ${newComment ? `New comment to address:\n${newComment.body}\n\n` : ""}Please ana
|
|
|
1930
2099
|
console.error(`[EdgeWorker] Error posting instant acknowledgment:`, error);
|
|
1931
2100
|
}
|
|
1932
2101
|
}
|
|
2102
|
+
/**
|
|
2103
|
+
* Post parent resume acknowledgment thought when parent session is resumed from child
|
|
2104
|
+
*/
|
|
2105
|
+
async postParentResumeAcknowledgment(linearAgentActivitySessionId, repositoryId) {
|
|
2106
|
+
try {
|
|
2107
|
+
const linearClient = this.linearClients.get(repositoryId);
|
|
2108
|
+
if (!linearClient) {
|
|
2109
|
+
console.warn(`[EdgeWorker] No Linear client found for repository ${repositoryId}`);
|
|
2110
|
+
return;
|
|
2111
|
+
}
|
|
2112
|
+
const activityInput = {
|
|
2113
|
+
agentSessionId: linearAgentActivitySessionId,
|
|
2114
|
+
content: {
|
|
2115
|
+
type: "thought",
|
|
2116
|
+
body: "Resuming from child session",
|
|
2117
|
+
},
|
|
2118
|
+
};
|
|
2119
|
+
const result = await linearClient.createAgentActivity(activityInput);
|
|
2120
|
+
if (result.success) {
|
|
2121
|
+
console.log(`[EdgeWorker] Posted parent resumption acknowledgment thought for session ${linearAgentActivitySessionId}`);
|
|
2122
|
+
}
|
|
2123
|
+
else {
|
|
2124
|
+
console.error(`[EdgeWorker] Failed to post parent resumption acknowledgment:`, result);
|
|
2125
|
+
}
|
|
2126
|
+
}
|
|
2127
|
+
catch (error) {
|
|
2128
|
+
console.error(`[EdgeWorker] Error posting parent resumption acknowledgment:`, error);
|
|
2129
|
+
}
|
|
2130
|
+
}
|
|
1933
2131
|
/**
|
|
1934
2132
|
* Post thought about system prompt selection based on labels
|
|
1935
2133
|
*/
|
|
@@ -1977,6 +2175,18 @@ ${newComment ? `New comment to address:\n${newComment.body}\n\n` : ""}Please ana
|
|
|
1977
2175
|
selectedPromptType = "scoper";
|
|
1978
2176
|
triggerLabel = scoperLabel;
|
|
1979
2177
|
}
|
|
2178
|
+
else {
|
|
2179
|
+
// Check orchestrator labels
|
|
2180
|
+
const orchestratorConfig = repository.labelPrompts.orchestrator;
|
|
2181
|
+
const orchestratorLabels = Array.isArray(orchestratorConfig)
|
|
2182
|
+
? orchestratorConfig
|
|
2183
|
+
: orchestratorConfig?.labels;
|
|
2184
|
+
const orchestratorLabel = orchestratorLabels?.find((label) => labels.includes(label));
|
|
2185
|
+
if (orchestratorLabel) {
|
|
2186
|
+
selectedPromptType = "orchestrator";
|
|
2187
|
+
triggerLabel = orchestratorLabel;
|
|
2188
|
+
}
|
|
2189
|
+
}
|
|
1980
2190
|
}
|
|
1981
2191
|
}
|
|
1982
2192
|
}
|
|
@@ -2003,6 +2213,72 @@ ${newComment ? `New comment to address:\n${newComment.body}\n\n` : ""}Please ana
|
|
|
2003
2213
|
console.error(`[EdgeWorker] Error posting system prompt selection thought:`, error);
|
|
2004
2214
|
}
|
|
2005
2215
|
}
|
|
2216
|
+
/**
|
|
2217
|
+
* Resume or create a Claude session with the given prompt
|
|
2218
|
+
* This is the core logic for handling prompted agent activities
|
|
2219
|
+
* @param session The Cyrus agent session
|
|
2220
|
+
* @param repository The repository configuration
|
|
2221
|
+
* @param linearAgentActivitySessionId The Linear agent session ID
|
|
2222
|
+
* @param agentSessionManager The agent session manager
|
|
2223
|
+
* @param promptBody The prompt text to send
|
|
2224
|
+
* @param attachmentManifest Optional attachment manifest
|
|
2225
|
+
* @param isNewSession Whether this is a new session
|
|
2226
|
+
*/
|
|
2227
|
+
async resumeClaudeSession(session, repository, linearAgentActivitySessionId, agentSessionManager, promptBody, attachmentManifest = "", isNewSession = false) {
|
|
2228
|
+
// Check for existing runner
|
|
2229
|
+
const existingRunner = session.claudeRunner;
|
|
2230
|
+
// If there's an existing streaming runner, add to it
|
|
2231
|
+
if (existingRunner?.isStreaming()) {
|
|
2232
|
+
let fullPrompt = promptBody;
|
|
2233
|
+
if (attachmentManifest) {
|
|
2234
|
+
fullPrompt = `${promptBody}\n\n${attachmentManifest}`;
|
|
2235
|
+
}
|
|
2236
|
+
fullPrompt = `${fullPrompt}${LAST_MESSAGE_MARKER}`;
|
|
2237
|
+
existingRunner.addStreamMessage(fullPrompt);
|
|
2238
|
+
return;
|
|
2239
|
+
}
|
|
2240
|
+
// Stop existing runner if it's not streaming
|
|
2241
|
+
if (existingRunner) {
|
|
2242
|
+
existingRunner.stop();
|
|
2243
|
+
}
|
|
2244
|
+
// Determine if we need a new Claude session
|
|
2245
|
+
const needsNewClaudeSession = isNewSession || !session.claudeSessionId;
|
|
2246
|
+
// Fetch full issue details
|
|
2247
|
+
const fullIssue = await this.fetchFullIssueDetails(session.issueId, repository.id);
|
|
2248
|
+
if (!fullIssue) {
|
|
2249
|
+
console.error(`[resumeClaudeSession] Failed to fetch full issue details for ${session.issueId}`);
|
|
2250
|
+
throw new Error(`Failed to fetch full issue details for ${session.issueId}`);
|
|
2251
|
+
}
|
|
2252
|
+
// Fetch issue labels and determine system prompt
|
|
2253
|
+
const labels = await this.fetchIssueLabels(fullIssue);
|
|
2254
|
+
const systemPromptResult = await this.determineSystemPromptFromLabels(labels, repository);
|
|
2255
|
+
const systemPrompt = systemPromptResult?.prompt;
|
|
2256
|
+
const promptType = systemPromptResult?.type;
|
|
2257
|
+
// Build allowed tools list
|
|
2258
|
+
const allowedTools = this.buildAllowedTools(repository, promptType);
|
|
2259
|
+
// Set up attachments directory
|
|
2260
|
+
const workspaceFolderName = basename(session.workspace.path);
|
|
2261
|
+
const attachmentsDir = join(this.cyrusHome, workspaceFolderName, "attachments");
|
|
2262
|
+
await mkdir(attachmentsDir, { recursive: true });
|
|
2263
|
+
const allowedDirectories = [attachmentsDir];
|
|
2264
|
+
// Create runner configuration
|
|
2265
|
+
const runnerConfig = this.buildClaudeRunnerConfig(session, repository, linearAgentActivitySessionId, agentSessionManager, systemPrompt, allowedTools, allowedDirectories, needsNewClaudeSession ? undefined : session.claudeSessionId, linearAgentActivitySessionId, labels);
|
|
2266
|
+
const runner = new ClaudeRunner(runnerConfig);
|
|
2267
|
+
// Store runner
|
|
2268
|
+
agentSessionManager.addClaudeRunner(linearAgentActivitySessionId, runner);
|
|
2269
|
+
// Save state
|
|
2270
|
+
await this.savePersistedState();
|
|
2271
|
+
// Prepare the full prompt
|
|
2272
|
+
const fullPrompt = await this.buildSessionPrompt(isNewSession, fullIssue, repository, promptBody, attachmentManifest);
|
|
2273
|
+
// Start streaming session
|
|
2274
|
+
try {
|
|
2275
|
+
await runner.startStreaming(fullPrompt);
|
|
2276
|
+
}
|
|
2277
|
+
catch (error) {
|
|
2278
|
+
console.error(`[resumeClaudeSession] Failed to start streaming session for ${linearAgentActivitySessionId}:`, error);
|
|
2279
|
+
throw error;
|
|
2280
|
+
}
|
|
2281
|
+
}
|
|
2006
2282
|
/**
|
|
2007
2283
|
* Post instant acknowledgment thought when receiving prompted webhook
|
|
2008
2284
|
*/
|