cyrus-edge-worker 0.2.2 → 0.2.4
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 +37 -40
- package/dist/AgentSessionManager.d.ts.map +1 -1
- package/dist/AgentSessionManager.js +127 -425
- package/dist/AgentSessionManager.js.map +1 -1
- package/dist/EdgeWorker.d.ts +29 -11
- package/dist/EdgeWorker.d.ts.map +1 -1
- package/dist/EdgeWorker.js +435 -290
- package/dist/EdgeWorker.js.map +1 -1
- package/dist/RepositoryRouter.d.ts +5 -6
- package/dist/RepositoryRouter.d.ts.map +1 -1
- package/dist/RepositoryRouter.js +17 -13
- package/dist/RepositoryRouter.js.map +1 -1
- package/dist/procedures/ProcedureRouter.d.ts +5 -3
- package/dist/procedures/ProcedureRouter.d.ts.map +1 -1
- package/dist/procedures/ProcedureRouter.js +25 -11
- package/dist/procedures/ProcedureRouter.js.map +1 -1
- package/dist/procedures/registry.d.ts +5 -5
- package/dist/procedures/registry.js +5 -5
- package/dist/procedures/registry.js.map +1 -1
- package/dist/procedures/types.d.ts +3 -2
- package/dist/procedures/types.d.ts.map +1 -1
- package/dist/prompt-assembly/types.d.ts +5 -6
- package/dist/prompt-assembly/types.d.ts.map +1 -1
- package/dist/types.d.ts +5 -6
- package/dist/types.d.ts.map +1 -1
- package/package.json +9 -8
package/dist/EdgeWorker.js
CHANGED
|
@@ -2,12 +2,13 @@ import { EventEmitter } from "node:events";
|
|
|
2
2
|
import { mkdir, readdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
3
3
|
import { basename, dirname, extname, join, resolve } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
-
import { LinearClient
|
|
5
|
+
import { LinearClient } from "@linear/sdk";
|
|
6
6
|
import { watch as chokidarWatch } from "chokidar";
|
|
7
7
|
import { AbortError, ClaudeRunner, createCyrusToolsServer, createImageToolsServer, createSoraToolsServer, getAllTools, getCoordinatorTools, getReadOnlyTools, getSafeTools, } from "cyrus-claude-runner";
|
|
8
8
|
import { ConfigUpdater } from "cyrus-config-updater";
|
|
9
9
|
import { DEFAULT_PROXY_URL, isAgentSessionCreatedWebhook, isAgentSessionPromptedWebhook, isIssueAssignedWebhook, isIssueCommentMentionWebhook, isIssueNewCommentWebhook, isIssueUnassignedWebhook, PersistenceManager, resolvePath, } from "cyrus-core";
|
|
10
|
-
import {
|
|
10
|
+
import { GeminiRunner } from "cyrus-gemini-runner";
|
|
11
|
+
import { LinearEventTransport, LinearIssueTrackerService, } from "cyrus-linear-event-transport";
|
|
11
12
|
import { fileTypeFromBuffer } from "file-type";
|
|
12
13
|
import { AgentSessionManager } from "./AgentSessionManager.js";
|
|
13
14
|
import { ProcedureRouter, } from "./procedures/index.js";
|
|
@@ -22,8 +23,8 @@ import { SharedApplicationServer } from "./SharedApplicationServer.js";
|
|
|
22
23
|
export class EdgeWorker extends EventEmitter {
|
|
23
24
|
config;
|
|
24
25
|
repositories = new Map(); // repository 'id' (internal, stored in config.json) mapped to the full repo config
|
|
25
|
-
agentSessionManagers = new Map(); // Maps repository ID to AgentSessionManager, which manages
|
|
26
|
-
|
|
26
|
+
agentSessionManagers = new Map(); // Maps repository ID to AgentSessionManager, which manages agent runners for a repo
|
|
27
|
+
issueTrackers = new Map(); // one issue tracker per 'repository'
|
|
27
28
|
linearEventTransport = null; // Single event transport for webhook delivery
|
|
28
29
|
configUpdater = null; // Single config updater for configuration updates
|
|
29
30
|
persistenceManager;
|
|
@@ -40,29 +41,27 @@ export class EdgeWorker extends EventEmitter {
|
|
|
40
41
|
this.config = config;
|
|
41
42
|
this.cyrusHome = config.cyrusHome;
|
|
42
43
|
this.persistenceManager = new PersistenceManager(join(this.cyrusHome, "state"));
|
|
43
|
-
// Initialize procedure router with haiku
|
|
44
|
+
// Initialize procedure router with haiku for fast classification
|
|
45
|
+
// Default to claude runner
|
|
44
46
|
this.procedureRouter = new ProcedureRouter({
|
|
45
47
|
cyrusHome: this.cyrusHome,
|
|
46
48
|
model: "haiku",
|
|
47
|
-
timeoutMs:
|
|
49
|
+
timeoutMs: 100000,
|
|
50
|
+
runnerType: "claude", // Use Claude by default
|
|
48
51
|
});
|
|
49
52
|
// Initialize repository router with dependencies
|
|
50
53
|
const repositoryRouterDeps = {
|
|
51
54
|
fetchIssueLabels: async (issueId, workspaceId) => {
|
|
52
|
-
//
|
|
53
|
-
const
|
|
54
|
-
if (!
|
|
55
|
-
console.warn(`[EdgeWorker] No Linear client found for workspace ${workspaceId}`);
|
|
55
|
+
// Find repository for this workspace
|
|
56
|
+
const repo = Array.from(this.repositories.values()).find((r) => r.linearWorkspaceId === workspaceId);
|
|
57
|
+
if (!repo)
|
|
56
58
|
return [];
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
return await this.fetchIssueLabels(issue);
|
|
61
|
-
}
|
|
62
|
-
catch (error) {
|
|
63
|
-
console.error(`[EdgeWorker] Failed to fetch issue labels for ${issueId}:`, error);
|
|
59
|
+
// Get issue tracker for this repository
|
|
60
|
+
const issueTracker = this.issueTrackers.get(repo.id);
|
|
61
|
+
if (!issueTracker)
|
|
64
62
|
return [];
|
|
65
|
-
|
|
63
|
+
// Use platform-agnostic getIssueLabels method
|
|
64
|
+
return await issueTracker.getIssueLabels(issueId);
|
|
66
65
|
},
|
|
67
66
|
hasActiveSession: (issueId, repositoryId) => {
|
|
68
67
|
const sessionManager = this.agentSessionManagers.get(repositoryId);
|
|
@@ -71,8 +70,8 @@ export class EdgeWorker extends EventEmitter {
|
|
|
71
70
|
const activeSessions = sessionManager.getActiveSessionsByIssueId(issueId);
|
|
72
71
|
return activeSessions.length > 0;
|
|
73
72
|
},
|
|
74
|
-
|
|
75
|
-
return this.
|
|
73
|
+
getIssueTracker: (workspaceId) => {
|
|
74
|
+
return this.getIssueTrackerForWorkspace(workspaceId);
|
|
76
75
|
},
|
|
77
76
|
};
|
|
78
77
|
this.repositoryRouter = new RepositoryRouter(repositoryRouterDeps);
|
|
@@ -103,11 +102,12 @@ export class EdgeWorker extends EventEmitter {
|
|
|
103
102
|
: undefined,
|
|
104
103
|
};
|
|
105
104
|
this.repositories.set(repo.id, resolvedRepo);
|
|
106
|
-
// Create
|
|
105
|
+
// Create issue tracker for this repository's workspace
|
|
107
106
|
const linearClient = new LinearClient({
|
|
108
107
|
accessToken: repo.linearToken,
|
|
109
108
|
});
|
|
110
|
-
|
|
109
|
+
const issueTracker = new LinearIssueTrackerService(linearClient);
|
|
110
|
+
this.issueTrackers.set(repo.id, issueTracker);
|
|
111
111
|
// Create AgentSessionManager for this repository with parent session lookup and resume callback
|
|
112
112
|
//
|
|
113
113
|
// Note: This pattern works (despite appearing recursive) because:
|
|
@@ -118,54 +118,18 @@ export class EdgeWorker extends EventEmitter {
|
|
|
118
118
|
//
|
|
119
119
|
// This allows the AgentSessionManager to call back into itself to access its own sessions,
|
|
120
120
|
// enabling child sessions to trigger parent session resumption using the same manager instance.
|
|
121
|
-
const agentSessionManager = new AgentSessionManager(
|
|
121
|
+
const agentSessionManager = new AgentSessionManager(issueTracker, (childSessionId) => {
|
|
122
122
|
console.log(`[Parent-Child Lookup] Looking up parent session for child ${childSessionId}`);
|
|
123
123
|
const parentId = this.childToParentAgentSession.get(childSessionId);
|
|
124
124
|
console.log(`[Parent-Child Lookup] Child ${childSessionId} -> Parent ${parentId || "not found"}`);
|
|
125
125
|
return parentId;
|
|
126
126
|
}, async (parentSessionId, prompt, childSessionId) => {
|
|
127
127
|
await this.handleResumeParentSession(parentSessionId, prompt, childSessionId, repo, agentSessionManager);
|
|
128
|
-
}, async (linearAgentActivitySessionId) => {
|
|
129
|
-
console.log(`[Subroutine Transition] Advancing to next subroutine for session ${linearAgentActivitySessionId}`);
|
|
130
|
-
// Get the session
|
|
131
|
-
const session = agentSessionManager.getSession(linearAgentActivitySessionId);
|
|
132
|
-
if (!session) {
|
|
133
|
-
console.error(`[Subroutine Transition] Session ${linearAgentActivitySessionId} not found`);
|
|
134
|
-
return;
|
|
135
|
-
}
|
|
136
|
-
// Get next subroutine (advancement already handled by AgentSessionManager)
|
|
137
|
-
const nextSubroutine = this.procedureRouter.getCurrentSubroutine(session);
|
|
138
|
-
if (!nextSubroutine) {
|
|
139
|
-
console.log(`[Subroutine Transition] Procedure complete for session ${linearAgentActivitySessionId}`);
|
|
140
|
-
return;
|
|
141
|
-
}
|
|
142
|
-
console.log(`[Subroutine Transition] Next subroutine: ${nextSubroutine.name}`);
|
|
143
|
-
// Load subroutine prompt
|
|
144
|
-
let subroutinePrompt;
|
|
145
|
-
try {
|
|
146
|
-
subroutinePrompt = await this.loadSubroutinePrompt(nextSubroutine, this.config.linearWorkspaceSlug);
|
|
147
|
-
if (!subroutinePrompt) {
|
|
148
|
-
// Fallback if loadSubroutinePrompt returns null
|
|
149
|
-
subroutinePrompt = `Continue with: ${nextSubroutine.description}`;
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
catch (error) {
|
|
153
|
-
console.error(`[Subroutine Transition] Failed to load subroutine prompt:`, error);
|
|
154
|
-
// Fallback to simple prompt
|
|
155
|
-
subroutinePrompt = `Continue with: ${nextSubroutine.description}`;
|
|
156
|
-
}
|
|
157
|
-
// Resume Claude session with subroutine prompt
|
|
158
|
-
try {
|
|
159
|
-
await this.resumeClaudeSession(session, repo, linearAgentActivitySessionId, agentSessionManager, subroutinePrompt, "", // No attachment manifest
|
|
160
|
-
false, // Not a new session
|
|
161
|
-
[], // No additional allowed directories
|
|
162
|
-
nextSubroutine.maxTurns);
|
|
163
|
-
console.log(`[Subroutine Transition] Successfully resumed session for ${nextSubroutine.name} subroutine${nextSubroutine.maxTurns ? ` (maxTurns=${nextSubroutine.maxTurns})` : ""}`);
|
|
164
|
-
}
|
|
165
|
-
catch (error) {
|
|
166
|
-
console.error(`[Subroutine Transition] Failed to resume session for ${nextSubroutine.name} subroutine:`, error);
|
|
167
|
-
}
|
|
168
128
|
}, this.procedureRouter, this.sharedApplicationServer);
|
|
129
|
+
// Subscribe to subroutine completion events
|
|
130
|
+
agentSessionManager.on("subroutineComplete", async ({ linearAgentActivitySessionId, session }) => {
|
|
131
|
+
await this.handleSubroutineTransition(linearAgentActivitySessionId, session, repo, agentSessionManager);
|
|
132
|
+
});
|
|
169
133
|
this.agentSessionManagers.set(repo.id, agentSessionManager);
|
|
170
134
|
}
|
|
171
135
|
}
|
|
@@ -208,10 +172,10 @@ export class EdgeWorker extends EventEmitter {
|
|
|
208
172
|
secret,
|
|
209
173
|
});
|
|
210
174
|
// Listen for webhook events
|
|
211
|
-
this.linearEventTransport.on("
|
|
175
|
+
this.linearEventTransport.on("event", (event) => {
|
|
212
176
|
// Get all active repositories for webhook handling
|
|
213
177
|
const repos = Array.from(this.repositories.values());
|
|
214
|
-
this.handleWebhook(
|
|
178
|
+
this.handleWebhook(event, repos);
|
|
215
179
|
});
|
|
216
180
|
// Listen for errors
|
|
217
181
|
this.linearEventTransport.on("error", (error) => {
|
|
@@ -246,13 +210,13 @@ export class EdgeWorker extends EventEmitter {
|
|
|
246
210
|
catch (error) {
|
|
247
211
|
console.error("❌ Failed to save EdgeWorker state during shutdown:", error);
|
|
248
212
|
}
|
|
249
|
-
// get all
|
|
250
|
-
const
|
|
213
|
+
// get all agent runners
|
|
214
|
+
const agentRunners = [];
|
|
251
215
|
for (const agentSessionManager of this.agentSessionManagers.values()) {
|
|
252
|
-
|
|
216
|
+
agentRunners.push(...agentSessionManager.getAllAgentRunners());
|
|
253
217
|
}
|
|
254
|
-
// Kill all
|
|
255
|
-
for (const runner of
|
|
218
|
+
// Kill all agent processes with null checking
|
|
219
|
+
for (const runner of agentRunners) {
|
|
256
220
|
if (runner) {
|
|
257
221
|
try {
|
|
258
222
|
runner.stop();
|
|
@@ -301,12 +265,12 @@ export class EdgeWorker extends EventEmitter {
|
|
|
301
265
|
}
|
|
302
266
|
await this.postParentResumeAcknowledgment(parentSessionId, repo.id);
|
|
303
267
|
// Post thought to Linear showing child result receipt
|
|
304
|
-
const
|
|
305
|
-
if (
|
|
268
|
+
const issueTracker = this.issueTrackers.get(repo.id);
|
|
269
|
+
if (issueTracker && childSession) {
|
|
306
270
|
const childIssueIdentifier = childSession.issue?.identifier || childSession.issueId;
|
|
307
271
|
const resultThought = `Received result from sub-issue ${childIssueIdentifier}:\n\n---\n\n${prompt}\n\n---`;
|
|
308
272
|
try {
|
|
309
|
-
const result = await
|
|
273
|
+
const result = await issueTracker.createAgentActivity({
|
|
310
274
|
agentSessionId: parentSessionId,
|
|
311
275
|
content: {
|
|
312
276
|
type: "thought",
|
|
@@ -338,6 +302,45 @@ export class EdgeWorker extends EventEmitter {
|
|
|
338
302
|
console.error(`[Parent Session Resume] Error context - Parent issue: ${parentSession.issueId}, Repository: ${repo.name}`);
|
|
339
303
|
}
|
|
340
304
|
}
|
|
305
|
+
/**
|
|
306
|
+
* Handle subroutine transition when a subroutine completes
|
|
307
|
+
* This is triggered by the AgentSessionManager's 'subroutineComplete' event
|
|
308
|
+
*/
|
|
309
|
+
async handleSubroutineTransition(linearAgentActivitySessionId, session, repo, agentSessionManager) {
|
|
310
|
+
console.log(`[Subroutine Transition] Handling subroutine completion for session ${linearAgentActivitySessionId}`);
|
|
311
|
+
// Get next subroutine (advancement already handled by AgentSessionManager)
|
|
312
|
+
const nextSubroutine = this.procedureRouter.getCurrentSubroutine(session);
|
|
313
|
+
if (!nextSubroutine) {
|
|
314
|
+
console.log(`[Subroutine Transition] Procedure complete for session ${linearAgentActivitySessionId}`);
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
console.log(`[Subroutine Transition] Next subroutine: ${nextSubroutine.name}`);
|
|
318
|
+
// Load subroutine prompt
|
|
319
|
+
let subroutinePrompt;
|
|
320
|
+
try {
|
|
321
|
+
subroutinePrompt = await this.loadSubroutinePrompt(nextSubroutine, this.config.linearWorkspaceSlug);
|
|
322
|
+
if (!subroutinePrompt) {
|
|
323
|
+
// Fallback if loadSubroutinePrompt returns null
|
|
324
|
+
subroutinePrompt = `Continue with: ${nextSubroutine.description}`;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
catch (error) {
|
|
328
|
+
console.error(`[Subroutine Transition] Failed to load subroutine prompt:`, error);
|
|
329
|
+
// Fallback to simple prompt
|
|
330
|
+
subroutinePrompt = `Continue with: ${nextSubroutine.description}`;
|
|
331
|
+
}
|
|
332
|
+
// Resume Claude session with subroutine prompt
|
|
333
|
+
try {
|
|
334
|
+
await this.resumeAgentSession(session, repo, linearAgentActivitySessionId, agentSessionManager, subroutinePrompt, "", // No attachment manifest
|
|
335
|
+
false, // Not a new session
|
|
336
|
+
[], // No additional allowed directories
|
|
337
|
+
nextSubroutine?.singleTurn ? 1 : undefined);
|
|
338
|
+
console.log(`[Subroutine Transition] Successfully resumed session for ${nextSubroutine.name} subroutine${nextSubroutine.singleTurn ? " (singleTurn)" : ""}`);
|
|
339
|
+
}
|
|
340
|
+
catch (error) {
|
|
341
|
+
console.error(`[Subroutine Transition] Failed to resume session for ${nextSubroutine.name} subroutine:`, error);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
341
344
|
/**
|
|
342
345
|
* Start watching config file for changes
|
|
343
346
|
*/
|
|
@@ -502,18 +505,22 @@ export class EdgeWorker extends EventEmitter {
|
|
|
502
505
|
};
|
|
503
506
|
// Add to internal map
|
|
504
507
|
this.repositories.set(repo.id, resolvedRepo);
|
|
505
|
-
// Create
|
|
508
|
+
// Create issue tracker
|
|
506
509
|
const linearClient = new LinearClient({
|
|
507
510
|
accessToken: repo.linearToken,
|
|
508
511
|
});
|
|
509
|
-
|
|
512
|
+
const issueTracker = new LinearIssueTrackerService(linearClient);
|
|
513
|
+
this.issueTrackers.set(repo.id, issueTracker);
|
|
510
514
|
// Create AgentSessionManager with same pattern as constructor
|
|
511
|
-
const agentSessionManager = new AgentSessionManager(
|
|
515
|
+
const agentSessionManager = new AgentSessionManager(issueTracker, (childSessionId) => {
|
|
512
516
|
return this.childToParentAgentSession.get(childSessionId);
|
|
513
517
|
}, async (parentSessionId, prompt, childSessionId) => {
|
|
514
518
|
await this.handleResumeParentSession(parentSessionId, prompt, childSessionId, repo, agentSessionManager);
|
|
515
|
-
},
|
|
516
|
-
|
|
519
|
+
}, this.procedureRouter, this.sharedApplicationServer);
|
|
520
|
+
// Subscribe to subroutine completion events
|
|
521
|
+
agentSessionManager.on("subroutineComplete", async ({ linearAgentActivitySessionId, session }) => {
|
|
522
|
+
await this.handleSubroutineTransition(linearAgentActivitySessionId, session, repo, agentSessionManager);
|
|
523
|
+
});
|
|
517
524
|
this.agentSessionManagers.set(repo.id, agentSessionManager);
|
|
518
525
|
console.log(`✅ Repository added successfully: ${repo.name}`);
|
|
519
526
|
}
|
|
@@ -553,13 +560,14 @@ export class EdgeWorker extends EventEmitter {
|
|
|
553
560
|
};
|
|
554
561
|
// Update stored config
|
|
555
562
|
this.repositories.set(repo.id, resolvedRepo);
|
|
556
|
-
// If token changed, recreate
|
|
563
|
+
// If token changed, recreate issue tracker
|
|
557
564
|
if (oldRepo.linearToken !== repo.linearToken) {
|
|
558
|
-
console.log(` 🔑 Token changed, recreating
|
|
565
|
+
console.log(` 🔑 Token changed, recreating issue tracker`);
|
|
559
566
|
const linearClient = new LinearClient({
|
|
560
567
|
accessToken: repo.linearToken,
|
|
561
568
|
});
|
|
562
|
-
|
|
569
|
+
const issueTracker = new LinearIssueTrackerService(linearClient);
|
|
570
|
+
this.issueTrackers.set(repo.id, issueTracker);
|
|
563
571
|
}
|
|
564
572
|
// If active status changed
|
|
565
573
|
if (oldRepo.isActive !== repo.isActive) {
|
|
@@ -593,17 +601,17 @@ export class EdgeWorker extends EventEmitter {
|
|
|
593
601
|
for (const session of activeSessions) {
|
|
594
602
|
try {
|
|
595
603
|
console.log(` 🛑 Stopping session for issue ${session.issueId}`);
|
|
596
|
-
// Get the
|
|
597
|
-
const runner = manager?.
|
|
604
|
+
// Get the agent runner for this session
|
|
605
|
+
const runner = manager?.getAgentRunner(session.linearAgentActivitySessionId);
|
|
598
606
|
if (runner) {
|
|
599
|
-
// Stop the
|
|
607
|
+
// Stop the agent process
|
|
600
608
|
runner.stop();
|
|
601
609
|
console.log(` ✅ Stopped Claude runner for session ${session.linearAgentActivitySessionId}`);
|
|
602
610
|
}
|
|
603
611
|
// Post cancellation message to Linear
|
|
604
|
-
const
|
|
605
|
-
if (
|
|
606
|
-
await
|
|
612
|
+
const issueTracker = this.issueTrackers.get(repo.id);
|
|
613
|
+
if (issueTracker) {
|
|
614
|
+
await issueTracker.createAgentActivity({
|
|
607
615
|
agentSessionId: session.linearAgentActivitySessionId,
|
|
608
616
|
content: {
|
|
609
617
|
type: "response",
|
|
@@ -620,7 +628,7 @@ export class EdgeWorker extends EventEmitter {
|
|
|
620
628
|
}
|
|
621
629
|
// Remove repository from all maps
|
|
622
630
|
this.repositories.delete(repo.id);
|
|
623
|
-
this.
|
|
631
|
+
this.issueTrackers.delete(repo.id);
|
|
624
632
|
this.agentSessionManagers.delete(repo.id);
|
|
625
633
|
console.log(`✅ Repository removed successfully: ${repo.name}`);
|
|
626
634
|
}
|
|
@@ -688,6 +696,10 @@ export class EdgeWorker extends EventEmitter {
|
|
|
688
696
|
* Handle issue unassignment webhook
|
|
689
697
|
*/
|
|
690
698
|
async handleIssueUnassignedWebhook(webhook) {
|
|
699
|
+
if (!webhook.notification.issue) {
|
|
700
|
+
console.warn("[EdgeWorker] Received issue unassignment webhook without issue");
|
|
701
|
+
return;
|
|
702
|
+
}
|
|
691
703
|
const issueId = webhook.notification.issue.id;
|
|
692
704
|
// Get cached repository (unassignment should only happen on issues with active sessions)
|
|
693
705
|
const repository = this.getCachedRepository(issueId);
|
|
@@ -703,12 +715,12 @@ export class EdgeWorker extends EventEmitter {
|
|
|
703
715
|
await this.handleIssueUnassigned(webhook.notification.issue, repository);
|
|
704
716
|
}
|
|
705
717
|
/**
|
|
706
|
-
* Get
|
|
718
|
+
* Get issue tracker for a workspace by finding first repository with that workspace ID
|
|
707
719
|
*/
|
|
708
|
-
|
|
720
|
+
getIssueTrackerForWorkspace(workspaceId) {
|
|
709
721
|
for (const [repoId, repo] of this.repositories) {
|
|
710
722
|
if (repo.linearWorkspaceId === workspaceId) {
|
|
711
|
-
return this.
|
|
723
|
+
return this.issueTrackers.get(repoId);
|
|
712
724
|
}
|
|
713
725
|
}
|
|
714
726
|
return undefined;
|
|
@@ -751,7 +763,10 @@ export class EdgeWorker extends EventEmitter {
|
|
|
751
763
|
const attachmentsDir = join(this.cyrusHome, workspaceFolderName, "attachments");
|
|
752
764
|
await mkdir(attachmentsDir, { recursive: true });
|
|
753
765
|
// Build allowed directories list - always include attachments directory
|
|
754
|
-
const allowedDirectories = [
|
|
766
|
+
const allowedDirectories = [
|
|
767
|
+
attachmentsDir,
|
|
768
|
+
repository.repositoryPath,
|
|
769
|
+
];
|
|
755
770
|
console.log(`[EdgeWorker] Configured allowed directories for ${fullIssue.identifier}:`, allowedDirectories);
|
|
756
771
|
// Build allowed tools list with Linear MCP tools
|
|
757
772
|
const allowedTools = this.buildAllowedTools(repository);
|
|
@@ -811,17 +826,21 @@ export class EdgeWorker extends EventEmitter {
|
|
|
811
826
|
// Post agent activity showing auto-matched routing
|
|
812
827
|
await this.postRepositorySelectionActivity(webhook.agentSession.id, repository.id, repository.name, routingMethod);
|
|
813
828
|
}
|
|
829
|
+
if (!webhook.agentSession.issue) {
|
|
830
|
+
console.warn("[EdgeWorker] Agent session created webhook missing issue");
|
|
831
|
+
return;
|
|
832
|
+
}
|
|
814
833
|
console.log(`[EdgeWorker] Handling agent session created: ${webhook.agentSession.issue.identifier}`);
|
|
815
834
|
const { agentSession, guidance } = webhook;
|
|
816
835
|
const commentBody = agentSession.comment?.body;
|
|
817
|
-
// Initialize
|
|
818
|
-
await this.
|
|
836
|
+
// Initialize agent runner using shared logic
|
|
837
|
+
await this.initializeAgentRunner(agentSession, repository, guidance, commentBody);
|
|
819
838
|
}
|
|
820
839
|
/**
|
|
821
840
|
|
|
822
841
|
/**
|
|
823
|
-
* Initialize and start
|
|
824
|
-
* This method contains the shared logic for creating
|
|
842
|
+
* Initialize and start agent runner for an agent session
|
|
843
|
+
* This method contains the shared logic for creating an agent runner that both
|
|
825
844
|
* handleAgentSessionCreatedWebhook and handleUserPromptedAgentActivity use.
|
|
826
845
|
*
|
|
827
846
|
* @param agentSession The Linear agent session
|
|
@@ -829,9 +848,13 @@ export class EdgeWorker extends EventEmitter {
|
|
|
829
848
|
* @param guidance Optional guidance rules from Linear
|
|
830
849
|
* @param commentBody Optional comment body (for mentions)
|
|
831
850
|
*/
|
|
832
|
-
async
|
|
851
|
+
async initializeAgentRunner(agentSession, repository, guidance, commentBody) {
|
|
833
852
|
const linearAgentActivitySessionId = agentSession.id;
|
|
834
853
|
const { issue } = agentSession;
|
|
854
|
+
if (!issue) {
|
|
855
|
+
console.warn("[EdgeWorker] Cannot initialize Claude runner without issue");
|
|
856
|
+
return;
|
|
857
|
+
}
|
|
835
858
|
// Log guidance if present
|
|
836
859
|
if (guidance && guidance.length > 0) {
|
|
837
860
|
console.log(`[EdgeWorker] Agent guidance received: ${guidance.length} rule(s)`);
|
|
@@ -931,7 +954,7 @@ export class EdgeWorker extends EventEmitter {
|
|
|
931
954
|
repository,
|
|
932
955
|
userComment: commentBody || "", // Empty for delegation, present for mentions
|
|
933
956
|
attachmentManifest: attachmentResult.manifest,
|
|
934
|
-
guidance,
|
|
957
|
+
guidance: guidance || undefined,
|
|
935
958
|
agentSession,
|
|
936
959
|
labels,
|
|
937
960
|
isNewSession: true,
|
|
@@ -962,28 +985,47 @@ export class EdgeWorker extends EventEmitter {
|
|
|
962
985
|
if (disallowedTools.length > 0) {
|
|
963
986
|
console.log(`[EdgeWorker] Configured disallowed tools for ${fullIssue.identifier}:`, disallowedTools);
|
|
964
987
|
}
|
|
965
|
-
//
|
|
966
|
-
const
|
|
967
|
-
|
|
968
|
-
|
|
988
|
+
// Get current subroutine to check for singleTurn mode
|
|
989
|
+
const currentSubroutine = this.procedureRouter.getCurrentSubroutine(session);
|
|
990
|
+
// Create agent runner with system prompt from assembly
|
|
991
|
+
// buildAgentRunnerConfig now determines runner type from labels internally
|
|
992
|
+
const { config: runnerConfig, runnerType } = this.buildAgentRunnerConfig(session, repository, linearAgentActivitySessionId, assembly.systemPrompt, allowedTools, allowedDirectories, disallowedTools, undefined, // resumeSessionId
|
|
993
|
+
labels, // Pass labels for runner selection and model override
|
|
994
|
+
undefined, // maxTurns
|
|
995
|
+
currentSubroutine?.singleTurn);
|
|
996
|
+
console.log(`[EdgeWorker] Label-based runner selection for new session: ${runnerType} (session ${linearAgentActivitySessionId})`);
|
|
997
|
+
const runner = runnerType === "claude"
|
|
998
|
+
? new ClaudeRunner(runnerConfig)
|
|
999
|
+
: new GeminiRunner(runnerConfig);
|
|
969
1000
|
// Store runner by comment ID
|
|
970
|
-
agentSessionManager.
|
|
1001
|
+
agentSessionManager.addAgentRunner(linearAgentActivitySessionId, runner);
|
|
971
1002
|
// Save state after mapping changes
|
|
972
1003
|
await this.savePersistedState();
|
|
973
1004
|
// Emit events using full Linear issue
|
|
974
1005
|
this.emit("session:started", fullIssue.id, fullIssue, repository.id);
|
|
975
1006
|
this.config.handlers?.onSessionStart?.(fullIssue.id, fullIssue, repository.id);
|
|
976
1007
|
// Update runner with version information (if available)
|
|
977
|
-
|
|
1008
|
+
// Note: updatePromptVersions is specific to ClaudeRunner
|
|
1009
|
+
if (systemPromptVersion &&
|
|
1010
|
+
"updatePromptVersions" in runner &&
|
|
1011
|
+
typeof runner.updatePromptVersions === "function") {
|
|
978
1012
|
runner.updatePromptVersions({
|
|
979
1013
|
systemPromptVersion,
|
|
980
1014
|
});
|
|
981
1015
|
}
|
|
982
1016
|
// Log metadata for debugging
|
|
983
1017
|
console.log(`[EdgeWorker] Initial prompt built successfully - components: ${assembly.metadata.components.join(", ")}, type: ${assembly.metadata.promptType}, length: ${assembly.userPrompt.length} characters`);
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
1018
|
+
// Start session - use streaming mode if supported for ability to add messages later
|
|
1019
|
+
if (runner.supportsStreamingInput && runner.startStreaming) {
|
|
1020
|
+
console.log(`[EdgeWorker] Starting streaming session`);
|
|
1021
|
+
const sessionInfo = await runner.startStreaming(assembly.userPrompt);
|
|
1022
|
+
console.log(`[EdgeWorker] Streaming session started: ${sessionInfo.sessionId}`);
|
|
1023
|
+
}
|
|
1024
|
+
else {
|
|
1025
|
+
console.log(`[EdgeWorker] Starting non-streaming session`);
|
|
1026
|
+
const sessionInfo = await runner.start(assembly.userPrompt);
|
|
1027
|
+
console.log(`[EdgeWorker] Non-streaming session started: ${sessionInfo.sessionId}`);
|
|
1028
|
+
}
|
|
987
1029
|
// Note: AgentSessionManager will be initialized automatically when the first system message
|
|
988
1030
|
// is received via handleClaudeMessage() callback
|
|
989
1031
|
}
|
|
@@ -1021,13 +1063,13 @@ export class EdgeWorker extends EventEmitter {
|
|
|
1021
1063
|
return;
|
|
1022
1064
|
}
|
|
1023
1065
|
// Stop the existing runner if it's active
|
|
1024
|
-
const existingRunner = foundSession.
|
|
1066
|
+
const existingRunner = foundSession.agentRunner;
|
|
1025
1067
|
if (existingRunner) {
|
|
1026
1068
|
existingRunner.stop();
|
|
1027
|
-
console.log(`[EdgeWorker] Stopped
|
|
1069
|
+
console.log(`[EdgeWorker] Stopped agent session for agent activity session ${agentSessionId}`);
|
|
1028
1070
|
}
|
|
1029
1071
|
// Post confirmation
|
|
1030
|
-
const issueTitle = issue
|
|
1072
|
+
const issueTitle = issue?.title || "this issue";
|
|
1031
1073
|
const stopConfirmation = `I've stopped working on ${issueTitle} as requested.\n\n**Stop Signal:** Received from ${webhook.agentSession.creator?.name || "user"}\n**Action Taken:** All ongoing work has been halted`;
|
|
1032
1074
|
await foundManager.createResponseActivity(agentSessionId, stopConfirmation);
|
|
1033
1075
|
}
|
|
@@ -1043,6 +1085,14 @@ export class EdgeWorker extends EventEmitter {
|
|
|
1043
1085
|
const { agentSession, agentActivity, guidance } = webhook;
|
|
1044
1086
|
const commentBody = agentSession.comment?.body;
|
|
1045
1087
|
const agentSessionId = agentSession.id;
|
|
1088
|
+
if (!agentActivity) {
|
|
1089
|
+
console.warn("[EdgeWorker] Cannot handle repository selection without agentActivity");
|
|
1090
|
+
return;
|
|
1091
|
+
}
|
|
1092
|
+
if (!agentSession.issue) {
|
|
1093
|
+
console.warn("[EdgeWorker] Cannot handle repository selection without issue");
|
|
1094
|
+
return;
|
|
1095
|
+
}
|
|
1046
1096
|
const userMessage = agentActivity.content.body;
|
|
1047
1097
|
console.log(`[EdgeWorker] Processing repository selection response: "${userMessage}"`);
|
|
1048
1098
|
// Get the selected repository (or fallback)
|
|
@@ -1056,9 +1106,9 @@ export class EdgeWorker extends EventEmitter {
|
|
|
1056
1106
|
this.repositoryRouter.getIssueRepositoryCache().set(issueId, repository.id);
|
|
1057
1107
|
// Post agent activity showing user-selected repository
|
|
1058
1108
|
await this.postRepositorySelectionActivity(agentSessionId, repository.id, repository.name, "user-selected");
|
|
1059
|
-
console.log(`[EdgeWorker] Initializing
|
|
1060
|
-
// Initialize
|
|
1061
|
-
await this.
|
|
1109
|
+
console.log(`[EdgeWorker] Initializing agent runner after repository selection: ${agentSession.issue.identifier} -> ${repository.name}`);
|
|
1110
|
+
// Initialize agent runner with the selected repository
|
|
1111
|
+
await this.initializeAgentRunner(agentSession, repository, guidance, commentBody);
|
|
1062
1112
|
}
|
|
1063
1113
|
/**
|
|
1064
1114
|
* Handle normal prompted activity (existing session continuation)
|
|
@@ -1068,6 +1118,14 @@ export class EdgeWorker extends EventEmitter {
|
|
|
1068
1118
|
const { agentSession } = webhook;
|
|
1069
1119
|
const linearAgentActivitySessionId = agentSession.id;
|
|
1070
1120
|
const { issue } = agentSession;
|
|
1121
|
+
if (!issue) {
|
|
1122
|
+
console.warn("[EdgeWorker] Cannot handle prompted activity without issue");
|
|
1123
|
+
return;
|
|
1124
|
+
}
|
|
1125
|
+
if (!webhook.agentActivity) {
|
|
1126
|
+
console.warn("[EdgeWorker] Cannot handle prompted activity without agentActivity");
|
|
1127
|
+
return;
|
|
1128
|
+
}
|
|
1071
1129
|
const commentId = webhook.agentActivity.sourceCommentId;
|
|
1072
1130
|
// Initialize the agent session in AgentSessionManager
|
|
1073
1131
|
const agentSessionManager = this.agentSessionManagers.get(repository.id);
|
|
@@ -1097,14 +1155,14 @@ export class EdgeWorker extends EventEmitter {
|
|
|
1097
1155
|
else {
|
|
1098
1156
|
console.log(`[EdgeWorker] Found existing session ${linearAgentActivitySessionId} for new user prompt`);
|
|
1099
1157
|
// Post instant acknowledgment for existing session BEFORE any async work
|
|
1100
|
-
// Check streaming
|
|
1101
|
-
const isCurrentlyStreaming = session?.
|
|
1158
|
+
// Check if runner is currently running (streaming is Claude-specific, use isRunning for both)
|
|
1159
|
+
const isCurrentlyStreaming = session?.agentRunner?.isRunning() || false;
|
|
1102
1160
|
await this.postInstantPromptedAcknowledgment(linearAgentActivitySessionId, repository.id, isCurrentlyStreaming);
|
|
1103
1161
|
// Need to fetch full issue for routing context
|
|
1104
|
-
const
|
|
1105
|
-
if (
|
|
1162
|
+
const issueTracker = this.issueTrackers.get(repository.id);
|
|
1163
|
+
if (issueTracker) {
|
|
1106
1164
|
try {
|
|
1107
|
-
fullIssue = await
|
|
1165
|
+
fullIssue = await issueTracker.fetchIssue(issue.id);
|
|
1108
1166
|
}
|
|
1109
1167
|
catch (error) {
|
|
1110
1168
|
console.warn(`[EdgeWorker] Failed to fetch full issue for routing: ${issue.id}`, error);
|
|
@@ -1120,10 +1178,10 @@ export class EdgeWorker extends EventEmitter {
|
|
|
1120
1178
|
}
|
|
1121
1179
|
// Acknowledgment already posted above for both new and existing sessions
|
|
1122
1180
|
// (before any async routing work to ensure instant user feedback)
|
|
1123
|
-
// Get
|
|
1124
|
-
const
|
|
1125
|
-
if (!
|
|
1126
|
-
console.error("Unexpected: There was no
|
|
1181
|
+
// Get issue tracker for this repository
|
|
1182
|
+
const issueTracker = this.issueTrackers.get(repository.id);
|
|
1183
|
+
if (!issueTracker) {
|
|
1184
|
+
console.error("Unexpected: There was no IssueTrackerService for the repository with id", repository.id);
|
|
1127
1185
|
return;
|
|
1128
1186
|
}
|
|
1129
1187
|
// Always set up attachments directory, even if no attachments in current comment
|
|
@@ -1134,37 +1192,34 @@ export class EdgeWorker extends EventEmitter {
|
|
|
1134
1192
|
let attachmentManifest = "";
|
|
1135
1193
|
let commentAuthor;
|
|
1136
1194
|
let commentTimestamp;
|
|
1195
|
+
if (!commentId) {
|
|
1196
|
+
console.warn("[EdgeWorker] No comment ID provided for attachment handling");
|
|
1197
|
+
}
|
|
1137
1198
|
try {
|
|
1138
|
-
const
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
id
|
|
1142
|
-
body
|
|
1143
|
-
createdAt
|
|
1144
|
-
updatedAt
|
|
1145
|
-
user {
|
|
1146
|
-
name
|
|
1147
|
-
displayName
|
|
1148
|
-
email
|
|
1149
|
-
id
|
|
1150
|
-
}
|
|
1151
|
-
}
|
|
1152
|
-
}
|
|
1153
|
-
`, { id: commentId });
|
|
1154
|
-
// Extract comment data
|
|
1155
|
-
const comment = result.data.comment;
|
|
1199
|
+
const comment = commentId
|
|
1200
|
+
? await issueTracker.fetchComment(commentId)
|
|
1201
|
+
: null;
|
|
1156
1202
|
// Extract comment metadata for multi-player context
|
|
1157
1203
|
if (comment) {
|
|
1158
|
-
const user = comment.user;
|
|
1204
|
+
const user = await comment.user;
|
|
1159
1205
|
commentAuthor =
|
|
1160
1206
|
user?.displayName || user?.name || user?.email || "Unknown";
|
|
1161
|
-
commentTimestamp = comment.createdAt
|
|
1207
|
+
commentTimestamp = comment.createdAt
|
|
1208
|
+
? comment.createdAt.toISOString()
|
|
1209
|
+
: new Date().toISOString();
|
|
1162
1210
|
}
|
|
1163
1211
|
// Count existing attachments
|
|
1164
1212
|
const existingFiles = await readdir(attachmentsDir).catch(() => []);
|
|
1165
1213
|
const existingAttachmentCount = existingFiles.filter((file) => file.startsWith("attachment_") || file.startsWith("image_")).length;
|
|
1166
1214
|
// Download new attachments from the comment
|
|
1167
|
-
const downloadResult =
|
|
1215
|
+
const downloadResult = comment
|
|
1216
|
+
? await this.downloadCommentAttachments(comment.body, attachmentsDir, repository.linearToken, existingAttachmentCount)
|
|
1217
|
+
: {
|
|
1218
|
+
totalNewAttachments: 0,
|
|
1219
|
+
newAttachmentMap: {},
|
|
1220
|
+
newImageMap: {},
|
|
1221
|
+
failedCount: 0,
|
|
1222
|
+
};
|
|
1168
1223
|
if (downloadResult.totalNewAttachments > 0) {
|
|
1169
1224
|
attachmentManifest = this.generateNewAttachmentManifest(downloadResult);
|
|
1170
1225
|
}
|
|
@@ -1196,7 +1251,7 @@ export class EdgeWorker extends EventEmitter {
|
|
|
1196
1251
|
// Branch 1: Handle stop signal (checked FIRST, before any routing work)
|
|
1197
1252
|
// Per CLAUDE.md: "an agentSession MUST already exist" for stop signals
|
|
1198
1253
|
// IMPORTANT: Stop signals do NOT require repository lookup
|
|
1199
|
-
if (webhook.agentActivity
|
|
1254
|
+
if (webhook.agentActivity?.signal === "stop") {
|
|
1200
1255
|
await this.handleStopSignal(webhook);
|
|
1201
1256
|
return;
|
|
1202
1257
|
}
|
|
@@ -1235,12 +1290,12 @@ export class EdgeWorker extends EventEmitter {
|
|
|
1235
1290
|
console.log("No agentSessionManager for unassigned issue, so no sessions to stop");
|
|
1236
1291
|
return;
|
|
1237
1292
|
}
|
|
1238
|
-
// Get all
|
|
1239
|
-
const
|
|
1240
|
-
// Stop all
|
|
1241
|
-
const activeThreadCount =
|
|
1242
|
-
for (const runner of
|
|
1243
|
-
console.log(`[EdgeWorker] Stopping
|
|
1293
|
+
// Get all agent runners for this specific issue
|
|
1294
|
+
const agentRunners = agentSessionManager.getAgentRunnersForIssue(issue.id);
|
|
1295
|
+
// Stop all agent runners for this issue
|
|
1296
|
+
const activeThreadCount = agentRunners.length;
|
|
1297
|
+
for (const runner of agentRunners) {
|
|
1298
|
+
console.log(`[EdgeWorker] Stopping agent runner for issue ${issue.identifier}`);
|
|
1244
1299
|
runner.stop();
|
|
1245
1300
|
}
|
|
1246
1301
|
// Post ONE farewell comment on the issue (not in any thread) if there were active sessions
|
|
@@ -1284,6 +1339,86 @@ export class EdgeWorker extends EventEmitter {
|
|
|
1284
1339
|
return [];
|
|
1285
1340
|
}
|
|
1286
1341
|
}
|
|
1342
|
+
/**
|
|
1343
|
+
* Determine runner type and model from issue labels.
|
|
1344
|
+
* Returns the runner type ("claude" or "gemini"), optional model override, and fallback model.
|
|
1345
|
+
*
|
|
1346
|
+
* Label priority (case-insensitive):
|
|
1347
|
+
* - Gemini labels: gemini, gemini-2.5-pro, gemini-2.5-flash, gemini-2.5-flash-lite, gemini-3-pro, gemini-3-pro-preview
|
|
1348
|
+
* - Claude labels: claude, sonnet, opus
|
|
1349
|
+
*
|
|
1350
|
+
* If no runner label is found, defaults to claude.
|
|
1351
|
+
*/
|
|
1352
|
+
determineRunnerFromLabels(labels) {
|
|
1353
|
+
if (!labels || labels.length === 0) {
|
|
1354
|
+
return {
|
|
1355
|
+
runnerType: "claude",
|
|
1356
|
+
modelOverride: "sonnet",
|
|
1357
|
+
fallbackModelOverride: "haiku",
|
|
1358
|
+
};
|
|
1359
|
+
}
|
|
1360
|
+
const lowercaseLabels = labels.map((label) => label.toLowerCase());
|
|
1361
|
+
// Check for Gemini labels first
|
|
1362
|
+
if (lowercaseLabels.includes("gemini-2.5-pro") ||
|
|
1363
|
+
lowercaseLabels.includes("gemini-2.5")) {
|
|
1364
|
+
return {
|
|
1365
|
+
runnerType: "gemini",
|
|
1366
|
+
modelOverride: "gemini-2.5-pro",
|
|
1367
|
+
fallbackModelOverride: "gemini-2.5-flash",
|
|
1368
|
+
};
|
|
1369
|
+
}
|
|
1370
|
+
if (lowercaseLabels.includes("gemini-2.5-flash")) {
|
|
1371
|
+
return {
|
|
1372
|
+
runnerType: "gemini",
|
|
1373
|
+
modelOverride: "gemini-2.5-flash",
|
|
1374
|
+
fallbackModelOverride: "gemini-2.5-flash-lite",
|
|
1375
|
+
};
|
|
1376
|
+
}
|
|
1377
|
+
if (lowercaseLabels.includes("gemini-2.5-flash-lite")) {
|
|
1378
|
+
return {
|
|
1379
|
+
runnerType: "gemini",
|
|
1380
|
+
modelOverride: "gemini-2.5-flash-lite",
|
|
1381
|
+
fallbackModelOverride: "gemini-2.5-flash-lite",
|
|
1382
|
+
};
|
|
1383
|
+
}
|
|
1384
|
+
if (lowercaseLabels.includes("gemini-3") ||
|
|
1385
|
+
lowercaseLabels.includes("gemini-3-pro") ||
|
|
1386
|
+
lowercaseLabels.includes("gemini-3-pro-preview")) {
|
|
1387
|
+
return {
|
|
1388
|
+
runnerType: "gemini",
|
|
1389
|
+
modelOverride: "gemini-3-pro-preview",
|
|
1390
|
+
fallbackModelOverride: "gemini-2.5-pro",
|
|
1391
|
+
};
|
|
1392
|
+
}
|
|
1393
|
+
if (lowercaseLabels.includes("gemini")) {
|
|
1394
|
+
return {
|
|
1395
|
+
runnerType: "gemini",
|
|
1396
|
+
modelOverride: "gemini-2.5-pro",
|
|
1397
|
+
fallbackModelOverride: "gemini-2.5-flash",
|
|
1398
|
+
};
|
|
1399
|
+
}
|
|
1400
|
+
// Check for Claude labels
|
|
1401
|
+
if (lowercaseLabels.includes("opus")) {
|
|
1402
|
+
return {
|
|
1403
|
+
runnerType: "claude",
|
|
1404
|
+
modelOverride: "opus",
|
|
1405
|
+
fallbackModelOverride: "sonnet",
|
|
1406
|
+
};
|
|
1407
|
+
}
|
|
1408
|
+
if (lowercaseLabels.includes("sonnet")) {
|
|
1409
|
+
return {
|
|
1410
|
+
runnerType: "claude",
|
|
1411
|
+
modelOverride: "sonnet",
|
|
1412
|
+
fallbackModelOverride: "haiku",
|
|
1413
|
+
};
|
|
1414
|
+
}
|
|
1415
|
+
// Default to claude if no runner labels found
|
|
1416
|
+
return {
|
|
1417
|
+
runnerType: "claude",
|
|
1418
|
+
modelOverride: "sonnet",
|
|
1419
|
+
fallbackModelOverride: "haiku",
|
|
1420
|
+
};
|
|
1421
|
+
}
|
|
1287
1422
|
/**
|
|
1288
1423
|
* Determine system prompt based on issue labels and repository configuration
|
|
1289
1424
|
*/
|
|
@@ -1373,10 +1508,10 @@ export class EdgeWorker extends EventEmitter {
|
|
|
1373
1508
|
console.warn(`[EdgeWorker] Failed to fetch assignee details:`, error);
|
|
1374
1509
|
}
|
|
1375
1510
|
// Get LinearClient for this repository
|
|
1376
|
-
const
|
|
1377
|
-
if (!
|
|
1378
|
-
console.error(`No
|
|
1379
|
-
throw new Error(`No
|
|
1511
|
+
const issueTracker = this.issueTrackers.get(repository.id);
|
|
1512
|
+
if (!issueTracker) {
|
|
1513
|
+
console.error(`No IssueTrackerService found for repository ${repository.id}`);
|
|
1514
|
+
throw new Error(`No IssueTrackerService found for repository ${repository.id}`);
|
|
1380
1515
|
}
|
|
1381
1516
|
// Fetch workspace teams and labels
|
|
1382
1517
|
let workspaceTeams = "";
|
|
@@ -1384,7 +1519,7 @@ export class EdgeWorker extends EventEmitter {
|
|
|
1384
1519
|
try {
|
|
1385
1520
|
console.log(`[EdgeWorker] Fetching workspace teams and labels for repository ${repository.id}`);
|
|
1386
1521
|
// Fetch teams
|
|
1387
|
-
const teamsConnection = await
|
|
1522
|
+
const teamsConnection = await issueTracker.fetchTeams();
|
|
1388
1523
|
const teamsArray = [];
|
|
1389
1524
|
for (const team of teamsConnection.nodes) {
|
|
1390
1525
|
teamsArray.push({
|
|
@@ -1399,7 +1534,7 @@ export class EdgeWorker extends EventEmitter {
|
|
|
1399
1534
|
.map((team) => `- ${team.name} (${team.key}): ${team.id}${team.description ? ` - ${team.description}` : ""}`)
|
|
1400
1535
|
.join("\n");
|
|
1401
1536
|
// Fetch labels
|
|
1402
|
-
const labelsConnection = await
|
|
1537
|
+
const labelsConnection = await issueTracker.fetchLabels();
|
|
1403
1538
|
const labelsArray = [];
|
|
1404
1539
|
for (const label of labelsConnection.nodes) {
|
|
1405
1540
|
labelsArray.push({
|
|
@@ -1721,14 +1856,12 @@ ${reply.body}
|
|
|
1721
1856
|
// Determine the base branch considering parent issues
|
|
1722
1857
|
const baseBranch = await this.determineBaseBranch(issue, repository);
|
|
1723
1858
|
// Get formatted comment threads
|
|
1724
|
-
const
|
|
1859
|
+
const issueTracker = this.issueTrackers.get(repository.id);
|
|
1725
1860
|
let commentThreads = "No comments yet.";
|
|
1726
|
-
if (
|
|
1861
|
+
if (issueTracker && issue.id) {
|
|
1727
1862
|
try {
|
|
1728
1863
|
console.log(`[EdgeWorker] Fetching comments for issue ${issue.identifier}`);
|
|
1729
|
-
const comments = await
|
|
1730
|
-
filter: { issue: { id: { eq: issue.id } } },
|
|
1731
|
-
});
|
|
1864
|
+
const comments = await issueTracker.fetchComments(issue.id);
|
|
1732
1865
|
const commentNodes = comments.nodes;
|
|
1733
1866
|
if (commentNodes.length > 0) {
|
|
1734
1867
|
commentThreads = await this.formatCommentThreads(commentNodes);
|
|
@@ -1771,11 +1904,9 @@ IMPORTANT: Focus specifically on addressing the new comment above. This is a new
|
|
|
1771
1904
|
// Now replace the new comment variables
|
|
1772
1905
|
// We'll need to fetch the comment author
|
|
1773
1906
|
let authorName = "Unknown";
|
|
1774
|
-
if (
|
|
1907
|
+
if (issueTracker) {
|
|
1775
1908
|
try {
|
|
1776
|
-
const fullComment = await
|
|
1777
|
-
id: newComment.id,
|
|
1778
|
-
});
|
|
1909
|
+
const fullComment = await issueTracker.fetchComment(newComment.id);
|
|
1779
1910
|
const user = await fullComment.user;
|
|
1780
1911
|
authorName =
|
|
1781
1912
|
user?.displayName || user?.name || user?.email || "Unknown";
|
|
@@ -1876,13 +2007,13 @@ ${newComment ? `New comment to address:\n${newComment.body}\n\n` : ""}Please ana
|
|
|
1876
2007
|
/**
|
|
1877
2008
|
* Move issue to started state when assigned
|
|
1878
2009
|
* @param issue Full Linear issue object from Linear SDK
|
|
1879
|
-
* @param repositoryId Repository ID for
|
|
2010
|
+
* @param repositoryId Repository ID for issue tracker lookup
|
|
1880
2011
|
*/
|
|
1881
2012
|
async moveIssueToStartedState(issue, repositoryId) {
|
|
1882
2013
|
try {
|
|
1883
|
-
const
|
|
1884
|
-
if (!
|
|
1885
|
-
console.warn(`No
|
|
2014
|
+
const issueTracker = this.issueTrackers.get(repositoryId);
|
|
2015
|
+
if (!issueTracker) {
|
|
2016
|
+
console.warn(`No issue tracker found for repository ${repositoryId}, skipping state update`);
|
|
1886
2017
|
return;
|
|
1887
2018
|
}
|
|
1888
2019
|
// Check if issue is already in a started state
|
|
@@ -1898,9 +2029,7 @@ ${newComment ? `New comment to address:\n${newComment.body}\n\n` : ""}Please ana
|
|
|
1898
2029
|
return;
|
|
1899
2030
|
}
|
|
1900
2031
|
// Get available workflow states for the issue's team
|
|
1901
|
-
const teamStates = await
|
|
1902
|
-
filter: { team: { id: { eq: team.id } } },
|
|
1903
|
-
});
|
|
2032
|
+
const teamStates = await issueTracker.fetchWorkflowStates(team.id);
|
|
1904
2033
|
const states = teamStates;
|
|
1905
2034
|
// Find all states with type "started" and pick the one with lowest position
|
|
1906
2035
|
// This ensures we pick "In Progress" over "In Review" when both have type "started"
|
|
@@ -1916,7 +2045,7 @@ ${newComment ? `New comment to address:\n${newComment.body}\n\n` : ""}Please ana
|
|
|
1916
2045
|
console.warn(`Issue ${issue.identifier} has no ID, skipping state update`);
|
|
1917
2046
|
return;
|
|
1918
2047
|
}
|
|
1919
|
-
await
|
|
2048
|
+
await issueTracker.updateIssue(issue.id, {
|
|
1920
2049
|
stateId: startedState.id,
|
|
1921
2050
|
});
|
|
1922
2051
|
console.log(`✅ Successfully moved issue ${issue.identifier} to ${startedState.name} state`);
|
|
@@ -1931,35 +2060,33 @@ ${newComment ? `New comment to address:\n${newComment.body}\n\n` : ""}Please ana
|
|
|
1931
2060
|
*/
|
|
1932
2061
|
// private async postInitialComment(issueId: string, repositoryId: string): Promise<void> {
|
|
1933
2062
|
// const body = "I'm getting started right away."
|
|
1934
|
-
// // Get the
|
|
1935
|
-
// const
|
|
1936
|
-
// if (!
|
|
1937
|
-
// throw new Error(`No
|
|
2063
|
+
// // Get the issue tracker for this repository
|
|
2064
|
+
// const issueTracker = this.issueTrackers.get(repositoryId)
|
|
2065
|
+
// if (!issueTracker) {
|
|
2066
|
+
// throw new Error(`No issue tracker found for repository ${repositoryId}`)
|
|
1938
2067
|
// }
|
|
1939
2068
|
// const commentData = {
|
|
1940
|
-
// issueId,
|
|
1941
2069
|
// body
|
|
1942
2070
|
// }
|
|
1943
|
-
// await
|
|
2071
|
+
// await issueTracker.createComment(commentData)
|
|
1944
2072
|
// }
|
|
1945
2073
|
/**
|
|
1946
2074
|
* Post a comment to Linear
|
|
1947
2075
|
*/
|
|
1948
2076
|
async postComment(issueId, body, repositoryId, parentId) {
|
|
1949
|
-
// Get the
|
|
1950
|
-
const
|
|
1951
|
-
if (!
|
|
1952
|
-
throw new Error(`No
|
|
2077
|
+
// Get the issue tracker for this repository
|
|
2078
|
+
const issueTracker = this.issueTrackers.get(repositoryId);
|
|
2079
|
+
if (!issueTracker) {
|
|
2080
|
+
throw new Error(`No issue tracker found for repository ${repositoryId}`);
|
|
1953
2081
|
}
|
|
1954
|
-
const
|
|
1955
|
-
issueId,
|
|
2082
|
+
const commentInput = {
|
|
1956
2083
|
body,
|
|
1957
2084
|
};
|
|
1958
2085
|
// Add parent ID if provided (for reply)
|
|
1959
2086
|
if (parentId) {
|
|
1960
|
-
|
|
2087
|
+
commentInput.parentId = parentId;
|
|
1961
2088
|
}
|
|
1962
|
-
await
|
|
2089
|
+
await issueTracker.createComment(issueId, commentInput);
|
|
1963
2090
|
}
|
|
1964
2091
|
/**
|
|
1965
2092
|
* Format todos as Linear checklist markdown
|
|
@@ -2008,10 +2135,10 @@ ${newComment ? `New comment to address:\n${newComment.body}\n\n` : ""}Please ana
|
|
|
2008
2135
|
const descriptionUrls = this.extractAttachmentUrls(issue.description || "");
|
|
2009
2136
|
// Extract URLs from comments if available
|
|
2010
2137
|
const commentUrls = [];
|
|
2011
|
-
const
|
|
2138
|
+
const issueTracker = this.issueTrackers.get(repository.id);
|
|
2012
2139
|
// Fetch native Linear attachments (e.g., Sentry links)
|
|
2013
2140
|
const nativeAttachments = [];
|
|
2014
|
-
if (
|
|
2141
|
+
if (issueTracker && issue.id) {
|
|
2015
2142
|
try {
|
|
2016
2143
|
// Fetch native attachments using Linear SDK
|
|
2017
2144
|
console.log(`[EdgeWorker] Fetching native attachments for issue ${issue.identifier}`);
|
|
@@ -2030,9 +2157,7 @@ ${newComment ? `New comment to address:\n${newComment.body}\n\n` : ""}Please ana
|
|
|
2030
2157
|
console.error("Failed to fetch native attachments:", error);
|
|
2031
2158
|
}
|
|
2032
2159
|
try {
|
|
2033
|
-
const comments = await
|
|
2034
|
-
filter: { issue: { id: { eq: issue.id } } },
|
|
2035
|
-
});
|
|
2160
|
+
const comments = await issueTracker.fetchComments(issue.id);
|
|
2036
2161
|
const commentNodes = comments.nodes;
|
|
2037
2162
|
for (const comment of commentNodes) {
|
|
2038
2163
|
const urls = this.extractAttachmentUrls(comment.body);
|
|
@@ -2364,7 +2489,7 @@ ${newComment ? `New comment to address:\n${newComment.body}\n\n` : ""}Please ana
|
|
|
2364
2489
|
let childRepo;
|
|
2365
2490
|
let childAgentSessionManager;
|
|
2366
2491
|
for (const [repoId, manager] of this.agentSessionManagers) {
|
|
2367
|
-
if (manager.
|
|
2492
|
+
if (manager.hasAgentRunner(childSessionId)) {
|
|
2368
2493
|
childRepo = this.repositories.get(repoId);
|
|
2369
2494
|
childAgentSessionManager = manager;
|
|
2370
2495
|
break;
|
|
@@ -2395,13 +2520,13 @@ ${newComment ? `New comment to address:\n${newComment.body}\n\n` : ""}Please ana
|
|
|
2395
2520
|
}
|
|
2396
2521
|
}
|
|
2397
2522
|
// Post thought to Linear showing feedback receipt
|
|
2398
|
-
const
|
|
2399
|
-
if (
|
|
2523
|
+
const issueTracker = this.issueTrackers.get(childRepo.id);
|
|
2524
|
+
if (issueTracker) {
|
|
2400
2525
|
const feedbackThought = parentIssueId
|
|
2401
2526
|
? `Received feedback from orchestrator (${parentIssueId}):\n\n---\n\n${message}\n\n---`
|
|
2402
2527
|
: `Received feedback from orchestrator:\n\n---\n\n${message}\n\n---`;
|
|
2403
2528
|
try {
|
|
2404
|
-
const result = await
|
|
2529
|
+
const result = await issueTracker.createAgentActivity({
|
|
2405
2530
|
agentSessionId: childSessionId,
|
|
2406
2531
|
content: {
|
|
2407
2532
|
type: "thought",
|
|
@@ -2748,9 +2873,11 @@ ${input.userComment}
|
|
|
2748
2873
|
attachmentManifest, guidance);
|
|
2749
2874
|
}
|
|
2750
2875
|
/**
|
|
2751
|
-
* Build
|
|
2876
|
+
* Build agent runner configuration with common settings.
|
|
2877
|
+
* Also determines which runner type to use based on labels.
|
|
2878
|
+
* @returns Object containing the runner config and runner type to use
|
|
2752
2879
|
*/
|
|
2753
|
-
|
|
2880
|
+
buildAgentRunnerConfig(session, repository, linearAgentActivitySessionId, systemPrompt, allowedTools, allowedDirectories, disallowedTools, resumeSessionId, labels, maxTurns, singleTurn) {
|
|
2754
2881
|
// Configure PostToolUse hook for playwright screenshots
|
|
2755
2882
|
const hooks = {
|
|
2756
2883
|
PostToolUse: [
|
|
@@ -2769,38 +2896,30 @@ ${input.userComment}
|
|
|
2769
2896
|
},
|
|
2770
2897
|
],
|
|
2771
2898
|
};
|
|
2772
|
-
//
|
|
2773
|
-
|
|
2774
|
-
let
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
else if (modelOverride === "sonnet") {
|
|
2797
|
-
fallbackModelOverride = "haiku";
|
|
2798
|
-
}
|
|
2799
|
-
else {
|
|
2800
|
-
fallbackModelOverride = "sonnet"; // haiku falls back to sonnet since same model retry doesn't help
|
|
2801
|
-
}
|
|
2802
|
-
}
|
|
2803
|
-
}
|
|
2899
|
+
// Determine runner type and model override from labels
|
|
2900
|
+
const runnerSelection = this.determineRunnerFromLabels(labels || []);
|
|
2901
|
+
let runnerType = runnerSelection.runnerType;
|
|
2902
|
+
let modelOverride = runnerSelection.modelOverride;
|
|
2903
|
+
let fallbackModelOverride = runnerSelection.fallbackModelOverride;
|
|
2904
|
+
// If the labels have changed, and we are resuming a session. Use the existing runner for the session.
|
|
2905
|
+
if (session.claudeSessionId && runnerType !== "claude") {
|
|
2906
|
+
runnerType = "claude";
|
|
2907
|
+
modelOverride = "sonnet";
|
|
2908
|
+
fallbackModelOverride = "haiku";
|
|
2909
|
+
}
|
|
2910
|
+
else if (session.geminiSessionId && runnerType !== "gemini") {
|
|
2911
|
+
runnerType = "gemini";
|
|
2912
|
+
modelOverride = "gemini-2.5-pro";
|
|
2913
|
+
fallbackModelOverride = "gemini-2.5-flash";
|
|
2914
|
+
}
|
|
2915
|
+
// Log model override if found
|
|
2916
|
+
if (modelOverride) {
|
|
2917
|
+
console.log(`[EdgeWorker] Model override via label: ${modelOverride} (for session ${linearAgentActivitySessionId})`);
|
|
2918
|
+
}
|
|
2919
|
+
// Convert singleTurn flag to effective maxTurns value
|
|
2920
|
+
const effectiveMaxTurns = singleTurn ? 1 : maxTurns;
|
|
2921
|
+
// Determine final model name with singleTurn suffix for Gemini
|
|
2922
|
+
const finalModel = modelOverride || repository.model || this.config.defaultModel;
|
|
2804
2923
|
const config = {
|
|
2805
2924
|
workingDirectory: session.workspace.path,
|
|
2806
2925
|
allowedTools,
|
|
@@ -2812,7 +2931,7 @@ ${input.userComment}
|
|
|
2812
2931
|
mcpConfig: this.buildMcpConfig(repository, linearAgentActivitySessionId),
|
|
2813
2932
|
appendSystemPrompt: systemPrompt || "",
|
|
2814
2933
|
// Priority order: label override > repository config > global default
|
|
2815
|
-
model:
|
|
2934
|
+
model: finalModel,
|
|
2816
2935
|
fallbackModel: fallbackModelOverride ||
|
|
2817
2936
|
repository.fallbackModel ||
|
|
2818
2937
|
this.config.defaultFallbackModel,
|
|
@@ -2825,10 +2944,13 @@ ${input.userComment}
|
|
|
2825
2944
|
if (resumeSessionId) {
|
|
2826
2945
|
config.resumeSessionId = resumeSessionId;
|
|
2827
2946
|
}
|
|
2828
|
-
if (
|
|
2829
|
-
config.maxTurns =
|
|
2947
|
+
if (effectiveMaxTurns !== undefined) {
|
|
2948
|
+
config.maxTurns = effectiveMaxTurns;
|
|
2949
|
+
if (singleTurn) {
|
|
2950
|
+
console.log(`[EdgeWorker] Applied singleTurn maxTurns=1 (for session ${linearAgentActivitySessionId})`);
|
|
2951
|
+
}
|
|
2830
2952
|
}
|
|
2831
|
-
return config;
|
|
2953
|
+
return { config, runnerType };
|
|
2832
2954
|
}
|
|
2833
2955
|
/**
|
|
2834
2956
|
* Build disallowed tools list following the same hierarchy as allowed tools
|
|
@@ -3024,9 +3146,9 @@ ${input.userComment}
|
|
|
3024
3146
|
*/
|
|
3025
3147
|
async postInstantAcknowledgment(linearAgentActivitySessionId, repositoryId) {
|
|
3026
3148
|
try {
|
|
3027
|
-
const
|
|
3028
|
-
if (!
|
|
3029
|
-
console.warn(`[EdgeWorker] No
|
|
3149
|
+
const issueTracker = this.issueTrackers.get(repositoryId);
|
|
3150
|
+
if (!issueTracker) {
|
|
3151
|
+
console.warn(`[EdgeWorker] No issue tracker found for repository ${repositoryId}`);
|
|
3030
3152
|
return;
|
|
3031
3153
|
}
|
|
3032
3154
|
const activityInput = {
|
|
@@ -3036,7 +3158,7 @@ ${input.userComment}
|
|
|
3036
3158
|
body: "I've received your request and I'm starting to work on it. Let me analyze the issue and prepare my approach.",
|
|
3037
3159
|
},
|
|
3038
3160
|
};
|
|
3039
|
-
const result = await
|
|
3161
|
+
const result = await issueTracker.createAgentActivity(activityInput);
|
|
3040
3162
|
if (result.success) {
|
|
3041
3163
|
console.log(`[EdgeWorker] Posted instant acknowledgment thought for session ${linearAgentActivitySessionId}`);
|
|
3042
3164
|
}
|
|
@@ -3053,9 +3175,9 @@ ${input.userComment}
|
|
|
3053
3175
|
*/
|
|
3054
3176
|
async postParentResumeAcknowledgment(linearAgentActivitySessionId, repositoryId) {
|
|
3055
3177
|
try {
|
|
3056
|
-
const
|
|
3057
|
-
if (!
|
|
3058
|
-
console.warn(`[EdgeWorker] No
|
|
3178
|
+
const issueTracker = this.issueTrackers.get(repositoryId);
|
|
3179
|
+
if (!issueTracker) {
|
|
3180
|
+
console.warn(`[EdgeWorker] No issue tracker found for repository ${repositoryId}`);
|
|
3059
3181
|
return;
|
|
3060
3182
|
}
|
|
3061
3183
|
const activityInput = {
|
|
@@ -3065,7 +3187,7 @@ ${input.userComment}
|
|
|
3065
3187
|
body: "Resuming from child session",
|
|
3066
3188
|
},
|
|
3067
3189
|
};
|
|
3068
|
-
const result = await
|
|
3190
|
+
const result = await issueTracker.createAgentActivity(activityInput);
|
|
3069
3191
|
if (result.success) {
|
|
3070
3192
|
console.log(`[EdgeWorker] Posted parent resumption acknowledgment thought for session ${linearAgentActivitySessionId}`);
|
|
3071
3193
|
}
|
|
@@ -3083,9 +3205,9 @@ ${input.userComment}
|
|
|
3083
3205
|
*/
|
|
3084
3206
|
async postRepositorySelectionActivity(linearAgentActivitySessionId, repositoryId, repositoryName, selectionMethod) {
|
|
3085
3207
|
try {
|
|
3086
|
-
const
|
|
3087
|
-
if (!
|
|
3088
|
-
console.warn(`[EdgeWorker] No
|
|
3208
|
+
const issueTracker = this.issueTrackers.get(repositoryId);
|
|
3209
|
+
if (!issueTracker) {
|
|
3210
|
+
console.warn(`[EdgeWorker] No issue tracker found for repository ${repositoryId}`);
|
|
3089
3211
|
return;
|
|
3090
3212
|
}
|
|
3091
3213
|
let methodDisplay;
|
|
@@ -3117,7 +3239,7 @@ ${input.userComment}
|
|
|
3117
3239
|
body: `Repository "${repositoryName}" has been ${methodDisplay}.`,
|
|
3118
3240
|
},
|
|
3119
3241
|
};
|
|
3120
|
-
const result = await
|
|
3242
|
+
const result = await issueTracker.createAgentActivity(activityInput);
|
|
3121
3243
|
if (result.success) {
|
|
3122
3244
|
console.log(`[EdgeWorker] Posted repository selection activity for session ${linearAgentActivitySessionId} (${selectionMethod})`);
|
|
3123
3245
|
}
|
|
@@ -3141,11 +3263,11 @@ ${input.userComment}
|
|
|
3141
3263
|
// Post ephemeral "Routing..." thought
|
|
3142
3264
|
await agentSessionManager.postRoutingThought(linearAgentActivitySessionId);
|
|
3143
3265
|
// Fetch full issue and labels to check for Orchestrator label override
|
|
3144
|
-
const
|
|
3266
|
+
const issueTracker = this.issueTrackers.get(repository.id);
|
|
3145
3267
|
let hasOrchestratorLabel = false;
|
|
3146
|
-
if (
|
|
3268
|
+
if (issueTracker) {
|
|
3147
3269
|
try {
|
|
3148
|
-
const fullIssue = await
|
|
3270
|
+
const fullIssue = await issueTracker.fetchIssue(session.issueId);
|
|
3149
3271
|
const labels = await this.fetchIssueLabels(fullIssue);
|
|
3150
3272
|
// Check for Orchestrator label (same logic as initial routing)
|
|
3151
3273
|
const orchestratorConfig = repository.labelPrompts?.orchestrator;
|
|
@@ -3208,19 +3330,21 @@ ${input.userComment}
|
|
|
3208
3330
|
* @returns true if message was added to stream, false if session was resumed
|
|
3209
3331
|
*/
|
|
3210
3332
|
async handlePromptWithStreamingCheck(session, repository, linearAgentActivitySessionId, agentSessionManager, promptBody, attachmentManifest, isNewSession, additionalAllowedDirs, logContext, commentAuthor, commentTimestamp) {
|
|
3211
|
-
// Check if runner is actively
|
|
3212
|
-
const existingRunner = session.
|
|
3213
|
-
const
|
|
3214
|
-
// Always route procedure for new input, UNLESS actively
|
|
3215
|
-
if (!
|
|
3333
|
+
// Check if runner is actively running before routing
|
|
3334
|
+
const existingRunner = session.agentRunner;
|
|
3335
|
+
const isRunning = existingRunner?.isRunning() || false;
|
|
3336
|
+
// Always route procedure for new input, UNLESS actively running
|
|
3337
|
+
if (!isRunning) {
|
|
3216
3338
|
await this.rerouteProcedureForSession(session, linearAgentActivitySessionId, agentSessionManager, promptBody, repository);
|
|
3217
3339
|
console.log(`[EdgeWorker] Routed procedure for ${logContext}`);
|
|
3218
3340
|
}
|
|
3219
3341
|
else {
|
|
3220
|
-
console.log(`[EdgeWorker] Skipping routing for ${linearAgentActivitySessionId} (${logContext}) - runner is actively
|
|
3342
|
+
console.log(`[EdgeWorker] Skipping routing for ${linearAgentActivitySessionId} (${logContext}) - runner is actively running`);
|
|
3221
3343
|
}
|
|
3222
|
-
// Handle
|
|
3223
|
-
if (existingRunner?.
|
|
3344
|
+
// Handle running case - add message to existing stream (if supported)
|
|
3345
|
+
if (existingRunner?.isRunning() &&
|
|
3346
|
+
existingRunner.supportsStreamingInput &&
|
|
3347
|
+
existingRunner.addStreamMessage) {
|
|
3224
3348
|
console.log(`[EdgeWorker] Adding prompt to existing stream for ${linearAgentActivitySessionId} (${logContext})`);
|
|
3225
3349
|
// Append attachment manifest to the prompt if we have one
|
|
3226
3350
|
let fullPrompt = promptBody;
|
|
@@ -3232,7 +3356,7 @@ ${input.userComment}
|
|
|
3232
3356
|
}
|
|
3233
3357
|
// Not streaming - resume/start session
|
|
3234
3358
|
console.log(`[EdgeWorker] Resuming Claude session for ${linearAgentActivitySessionId} (${logContext})`);
|
|
3235
|
-
await this.
|
|
3359
|
+
await this.resumeAgentSession(session, repository, linearAgentActivitySessionId, agentSessionManager, promptBody, attachmentManifest, isNewSession, additionalAllowedDirs, undefined, // maxTurns
|
|
3236
3360
|
commentAuthor, commentTimestamp);
|
|
3237
3361
|
return false; // Session was resumed
|
|
3238
3362
|
}
|
|
@@ -3241,9 +3365,9 @@ ${input.userComment}
|
|
|
3241
3365
|
*/
|
|
3242
3366
|
async postSystemPromptSelectionThought(linearAgentActivitySessionId, labels, repositoryId) {
|
|
3243
3367
|
try {
|
|
3244
|
-
const
|
|
3245
|
-
if (!
|
|
3246
|
-
console.warn(`[EdgeWorker] No
|
|
3368
|
+
const issueTracker = this.issueTrackers.get(repositoryId);
|
|
3369
|
+
if (!issueTracker) {
|
|
3370
|
+
console.warn(`[EdgeWorker] No issue tracker found for repository ${repositoryId}`);
|
|
3247
3371
|
return;
|
|
3248
3372
|
}
|
|
3249
3373
|
// Determine which prompt type was selected and which label triggered it
|
|
@@ -3309,7 +3433,7 @@ ${input.userComment}
|
|
|
3309
3433
|
body: `Entering '${selectedPromptType}' mode because of the '${triggerLabel}' label. I'll follow the ${selectedPromptType} process...`,
|
|
3310
3434
|
},
|
|
3311
3435
|
};
|
|
3312
|
-
const result = await
|
|
3436
|
+
const result = await issueTracker.createAgentActivity(activityInput);
|
|
3313
3437
|
if (result.success) {
|
|
3314
3438
|
console.log(`[EdgeWorker] Posted system prompt selection thought for session ${linearAgentActivitySessionId} (${selectedPromptType} mode)`);
|
|
3315
3439
|
}
|
|
@@ -3322,7 +3446,7 @@ ${input.userComment}
|
|
|
3322
3446
|
}
|
|
3323
3447
|
}
|
|
3324
3448
|
/**
|
|
3325
|
-
* Resume or create
|
|
3449
|
+
* Resume or create an Agent session with the given prompt
|
|
3326
3450
|
* This is the core logic for handling prompted agent activities
|
|
3327
3451
|
* @param session The Cyrus agent session
|
|
3328
3452
|
* @param repository The repository configuration
|
|
@@ -3332,11 +3456,13 @@ ${input.userComment}
|
|
|
3332
3456
|
* @param attachmentManifest Optional attachment manifest
|
|
3333
3457
|
* @param isNewSession Whether this is a new session
|
|
3334
3458
|
*/
|
|
3335
|
-
async
|
|
3459
|
+
async resumeAgentSession(session, repository, linearAgentActivitySessionId, agentSessionManager, promptBody, attachmentManifest = "", isNewSession = false, additionalAllowedDirectories = [], maxTurns, commentAuthor, commentTimestamp) {
|
|
3336
3460
|
// Check for existing runner
|
|
3337
|
-
const existingRunner = session.
|
|
3338
|
-
// If there's an existing
|
|
3339
|
-
if (existingRunner?.
|
|
3461
|
+
const existingRunner = session.agentRunner;
|
|
3462
|
+
// If there's an existing running runner that supports streaming, add to it
|
|
3463
|
+
if (existingRunner?.isRunning() &&
|
|
3464
|
+
existingRunner.supportsStreamingInput &&
|
|
3465
|
+
existingRunner.addStreamMessage) {
|
|
3340
3466
|
let fullPrompt = promptBody;
|
|
3341
3467
|
if (attachmentManifest) {
|
|
3342
3468
|
fullPrompt = `${promptBody}\n\n${attachmentManifest}`;
|
|
@@ -3344,20 +3470,23 @@ ${input.userComment}
|
|
|
3344
3470
|
existingRunner.addStreamMessage(fullPrompt);
|
|
3345
3471
|
return;
|
|
3346
3472
|
}
|
|
3347
|
-
// Stop existing runner if it's not
|
|
3473
|
+
// Stop existing runner if it's not running
|
|
3348
3474
|
if (existingRunner) {
|
|
3349
3475
|
existingRunner.stop();
|
|
3350
3476
|
}
|
|
3351
|
-
// Determine if we need a new Claude session
|
|
3352
|
-
const needsNewClaudeSession = isNewSession || !session.claudeSessionId;
|
|
3353
3477
|
// Fetch full issue details
|
|
3354
3478
|
const fullIssue = await this.fetchFullIssueDetails(session.issueId, repository.id);
|
|
3355
3479
|
if (!fullIssue) {
|
|
3356
|
-
console.error(`[
|
|
3480
|
+
console.error(`[resumeAgentSession] Failed to fetch full issue details for ${session.issueId}`);
|
|
3357
3481
|
throw new Error(`Failed to fetch full issue details for ${session.issueId}`);
|
|
3358
3482
|
}
|
|
3359
|
-
// Fetch issue labels
|
|
3483
|
+
// Fetch issue labels early to determine runner type
|
|
3360
3484
|
const labels = await this.fetchIssueLabels(fullIssue);
|
|
3485
|
+
// Determine which runner to use based on existing session IDs
|
|
3486
|
+
const hasClaudeSession = !isNewSession && Boolean(session.claudeSessionId);
|
|
3487
|
+
const hasGeminiSession = !isNewSession && Boolean(session.geminiSessionId);
|
|
3488
|
+
const needsNewSession = isNewSession || (!hasClaudeSession && !hasGeminiSession);
|
|
3489
|
+
// Fetch system prompt based on labels
|
|
3361
3490
|
const systemPromptResult = await this.determineSystemPromptFromLabels(labels, repository);
|
|
3362
3491
|
const systemPrompt = systemPromptResult?.prompt;
|
|
3363
3492
|
const promptType = systemPromptResult?.type;
|
|
@@ -3372,27 +3501,43 @@ ${input.userComment}
|
|
|
3372
3501
|
await mkdir(attachmentsDir, { recursive: true });
|
|
3373
3502
|
const allowedDirectories = [
|
|
3374
3503
|
attachmentsDir,
|
|
3504
|
+
repository.repositoryPath,
|
|
3375
3505
|
...additionalAllowedDirectories,
|
|
3376
3506
|
];
|
|
3377
|
-
//
|
|
3378
|
-
const
|
|
3507
|
+
// Get current subroutine to check for singleTurn mode
|
|
3508
|
+
const currentSubroutine = this.procedureRouter.getCurrentSubroutine(session);
|
|
3509
|
+
const resumeSessionId = needsNewSession
|
|
3379
3510
|
? undefined
|
|
3380
|
-
: session.claudeSessionId
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
|
|
3511
|
+
: session.claudeSessionId
|
|
3512
|
+
? session.claudeSessionId
|
|
3513
|
+
: session.geminiSessionId;
|
|
3514
|
+
// Create runner configuration
|
|
3515
|
+
// buildAgentRunnerConfig determines runner type from labels for new sessions
|
|
3516
|
+
// For existing sessions, we still need labels for model override but ignore runner type
|
|
3517
|
+
const { config: runnerConfig, runnerType } = this.buildAgentRunnerConfig(session, repository, linearAgentActivitySessionId, systemPrompt, allowedTools, allowedDirectories, disallowedTools, resumeSessionId, labels, // Always pass labels to preserve model override
|
|
3518
|
+
maxTurns, // Pass maxTurns if specified
|
|
3519
|
+
currentSubroutine?.singleTurn);
|
|
3520
|
+
// Create the appropriate runner based on session state
|
|
3521
|
+
const runner = runnerType === "claude"
|
|
3522
|
+
? new ClaudeRunner(runnerConfig)
|
|
3523
|
+
: new GeminiRunner(runnerConfig);
|
|
3384
3524
|
// Store runner
|
|
3385
|
-
agentSessionManager.
|
|
3525
|
+
agentSessionManager.addAgentRunner(linearAgentActivitySessionId, runner);
|
|
3386
3526
|
// Save state
|
|
3387
3527
|
await this.savePersistedState();
|
|
3388
3528
|
// Prepare the full prompt
|
|
3389
3529
|
const fullPrompt = await this.buildSessionPrompt(isNewSession, session, fullIssue, repository, promptBody, attachmentManifest, commentAuthor, commentTimestamp);
|
|
3390
|
-
// Start streaming
|
|
3530
|
+
// Start session - use streaming mode if supported for ability to add messages later
|
|
3391
3531
|
try {
|
|
3392
|
-
|
|
3532
|
+
if (runner.supportsStreamingInput && runner.startStreaming) {
|
|
3533
|
+
await runner.startStreaming(fullPrompt);
|
|
3534
|
+
}
|
|
3535
|
+
else {
|
|
3536
|
+
await runner.start(fullPrompt);
|
|
3537
|
+
}
|
|
3393
3538
|
}
|
|
3394
3539
|
catch (error) {
|
|
3395
|
-
console.error(`[
|
|
3540
|
+
console.error(`[resumeAgentSession] Failed to start streaming session for ${linearAgentActivitySessionId}:`, error);
|
|
3396
3541
|
throw error;
|
|
3397
3542
|
}
|
|
3398
3543
|
}
|
|
@@ -3401,9 +3546,9 @@ ${input.userComment}
|
|
|
3401
3546
|
*/
|
|
3402
3547
|
async postInstantPromptedAcknowledgment(linearAgentActivitySessionId, repositoryId, isStreaming) {
|
|
3403
3548
|
try {
|
|
3404
|
-
const
|
|
3405
|
-
if (!
|
|
3406
|
-
console.warn(`[EdgeWorker] No
|
|
3549
|
+
const issueTracker = this.issueTrackers.get(repositoryId);
|
|
3550
|
+
if (!issueTracker) {
|
|
3551
|
+
console.warn(`[EdgeWorker] No issue tracker found for repository ${repositoryId}`);
|
|
3407
3552
|
return;
|
|
3408
3553
|
}
|
|
3409
3554
|
const message = isStreaming
|
|
@@ -3416,7 +3561,7 @@ ${input.userComment}
|
|
|
3416
3561
|
body: message,
|
|
3417
3562
|
},
|
|
3418
3563
|
};
|
|
3419
|
-
const result = await
|
|
3564
|
+
const result = await issueTracker.createAgentActivity(activityInput);
|
|
3420
3565
|
if (result.success) {
|
|
3421
3566
|
console.log(`[EdgeWorker] Posted instant prompted acknowledgment thought for session ${linearAgentActivitySessionId} (streaming: ${isStreaming})`);
|
|
3422
3567
|
}
|
|
@@ -3432,14 +3577,14 @@ ${input.userComment}
|
|
|
3432
3577
|
* Fetch complete issue details from Linear API
|
|
3433
3578
|
*/
|
|
3434
3579
|
async fetchFullIssueDetails(issueId, repositoryId) {
|
|
3435
|
-
const
|
|
3436
|
-
if (!
|
|
3437
|
-
console.warn(`[EdgeWorker] No
|
|
3580
|
+
const issueTracker = this.issueTrackers.get(repositoryId);
|
|
3581
|
+
if (!issueTracker) {
|
|
3582
|
+
console.warn(`[EdgeWorker] No issue tracker found for repository ${repositoryId}`);
|
|
3438
3583
|
return null;
|
|
3439
3584
|
}
|
|
3440
3585
|
try {
|
|
3441
3586
|
console.log(`[EdgeWorker] Fetching full issue details for ${issueId}`);
|
|
3442
|
-
const fullIssue = await
|
|
3587
|
+
const fullIssue = await issueTracker.fetchIssue(issueId);
|
|
3443
3588
|
console.log(`[EdgeWorker] Successfully fetched issue details for ${issueId}`);
|
|
3444
3589
|
// Check if issue has a parent
|
|
3445
3590
|
try {
|