cyrus-edge-worker 0.0.37 → 0.0.38

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.
Files changed (45) hide show
  1. package/dist/AgentSessionManager.d.ts +26 -1
  2. package/dist/AgentSessionManager.d.ts.map +1 -1
  3. package/dist/AgentSessionManager.js +227 -30
  4. package/dist/AgentSessionManager.js.map +1 -1
  5. package/dist/EdgeWorker.d.ts +4 -3
  6. package/dist/EdgeWorker.d.ts.map +1 -1
  7. package/dist/EdgeWorker.js +191 -25
  8. package/dist/EdgeWorker.js.map +1 -1
  9. package/dist/SharedApplicationServer.d.ts +29 -4
  10. package/dist/SharedApplicationServer.d.ts.map +1 -1
  11. package/dist/SharedApplicationServer.js +262 -0
  12. package/dist/SharedApplicationServer.js.map +1 -1
  13. package/dist/index.d.ts +2 -3
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js.map +1 -1
  16. package/dist/procedures/ProcedureRouter.d.ts +60 -0
  17. package/dist/procedures/ProcedureRouter.d.ts.map +1 -0
  18. package/dist/procedures/ProcedureRouter.js +201 -0
  19. package/dist/procedures/ProcedureRouter.js.map +1 -0
  20. package/dist/procedures/index.d.ts +7 -0
  21. package/dist/procedures/index.d.ts.map +1 -0
  22. package/dist/procedures/index.js +7 -0
  23. package/dist/procedures/index.js.map +1 -0
  24. package/dist/procedures/registry.d.ts +76 -0
  25. package/dist/procedures/registry.d.ts.map +1 -0
  26. package/dist/procedures/registry.js +130 -0
  27. package/dist/procedures/registry.js.map +1 -0
  28. package/dist/procedures/types.d.ts +64 -0
  29. package/dist/procedures/types.d.ts.map +1 -0
  30. package/dist/procedures/types.js +5 -0
  31. package/dist/procedures/types.js.map +1 -0
  32. package/dist/prompts/subroutines/concise-summary.md +53 -0
  33. package/dist/prompts/subroutines/debugger-fix.md +108 -0
  34. package/dist/prompts/subroutines/debugger-reproduction.md +106 -0
  35. package/dist/prompts/subroutines/get-approval.md +175 -0
  36. package/dist/prompts/subroutines/git-gh.md +52 -0
  37. package/dist/prompts/subroutines/verbose-summary.md +46 -0
  38. package/dist/prompts/subroutines/verifications.md +46 -0
  39. package/dist/types.d.ts +0 -97
  40. package/dist/types.d.ts.map +1 -1
  41. package/package.json +8 -6
  42. package/prompt-template-v2.md +3 -19
  43. package/prompts/builder.md +1 -23
  44. package/prompts/debugger.md +11 -174
  45. package/prompts/orchestrator.md +41 -64
@@ -4,14 +4,14 @@ import { basename, dirname, extname, join, resolve } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { LinearClient, } from "@linear/sdk";
6
6
  import { watch as chokidarWatch } from "chokidar";
7
- import { ClaudeRunner, createCyrusToolsServer, getAllTools, getCoordinatorTools, getReadOnlyTools, getSafeTools, } from "cyrus-claude-runner";
7
+ import { ClaudeRunner, createCyrusToolsServer, createImageToolsServer, createSoraToolsServer, getAllTools, getCoordinatorTools, getReadOnlyTools, getSafeTools, } from "cyrus-claude-runner";
8
8
  import { isAgentSessionCreatedWebhook, isAgentSessionPromptedWebhook, isIssueAssignedWebhook, isIssueCommentMentionWebhook, isIssueNewCommentWebhook, isIssueUnassignedWebhook, PersistenceManager, } from "cyrus-core";
9
9
  import { LinearWebhookClient } from "cyrus-linear-webhook-client";
10
10
  import { NdjsonClient } from "cyrus-ndjson-client";
11
11
  import { fileTypeFromBuffer } from "file-type";
12
12
  import { AgentSessionManager } from "./AgentSessionManager.js";
13
+ import { ProcedureRouter, } from "./procedures/index.js";
13
14
  import { SharedApplicationServer } from "./SharedApplicationServer.js";
14
- const LAST_MESSAGE_MARKER = "\n\nIMPORTANT: When providing your final summary response, include the special marker ___LAST_MESSAGE_MARKER___ at the very beginning of your message. This marker will be automatically removed before posting.";
15
15
  /**
16
16
  * Unified edge worker that **orchestrates**
17
17
  * capturing Linear webhooks,
@@ -28,6 +28,7 @@ export class EdgeWorker extends EventEmitter {
28
28
  sharedApplicationServer;
29
29
  cyrusHome;
30
30
  childToParentAgentSession = new Map(); // Maps child agentSessionId to parent agentSessionId
31
+ procedureRouter; // Intelligent workflow routing
31
32
  configWatcher; // File watcher for config.json
32
33
  configPath; // Path to config.json file
33
34
  tokenToRepoIds = new Map(); // Maps Linear token to repository IDs using that token
@@ -36,6 +37,12 @@ export class EdgeWorker extends EventEmitter {
36
37
  this.config = config;
37
38
  this.cyrusHome = config.cyrusHome;
38
39
  this.persistenceManager = new PersistenceManager(join(this.cyrusHome, "state"));
40
+ // Initialize procedure router with haiku model for fast classification
41
+ this.procedureRouter = new ProcedureRouter({
42
+ cyrusHome: this.cyrusHome,
43
+ model: "haiku",
44
+ timeoutMs: 10000,
45
+ });
39
46
  console.log(`[EdgeWorker Constructor] Initializing parent-child session mapping system`);
40
47
  console.log(`[EdgeWorker Constructor] Parent-child mapping initialized with 0 entries`);
41
48
  // Initialize shared application server
@@ -72,7 +79,47 @@ export class EdgeWorker extends EventEmitter {
72
79
  return parentId;
73
80
  }, async (parentSessionId, prompt, childSessionId) => {
74
81
  await this.handleResumeParentSession(parentSessionId, prompt, childSessionId, repo, agentSessionManager);
75
- });
82
+ }, async (linearAgentActivitySessionId) => {
83
+ console.log(`[Subroutine Transition] Advancing to next subroutine for session ${linearAgentActivitySessionId}`);
84
+ // Get the session
85
+ const session = agentSessionManager.getSession(linearAgentActivitySessionId);
86
+ if (!session) {
87
+ console.error(`[Subroutine Transition] Session ${linearAgentActivitySessionId} not found`);
88
+ return;
89
+ }
90
+ // Get next subroutine (advancement already handled by AgentSessionManager)
91
+ const nextSubroutine = this.procedureRouter.getCurrentSubroutine(session);
92
+ if (!nextSubroutine) {
93
+ console.log(`[Subroutine Transition] Procedure complete for session ${linearAgentActivitySessionId}`);
94
+ return;
95
+ }
96
+ console.log(`[Subroutine Transition] Next subroutine: ${nextSubroutine.name}`);
97
+ // Load subroutine prompt
98
+ const __filename = fileURLToPath(import.meta.url);
99
+ const __dirname = dirname(__filename);
100
+ const subroutinePromptPath = join(__dirname, "prompts", nextSubroutine.promptPath);
101
+ let subroutinePrompt;
102
+ try {
103
+ subroutinePrompt = await readFile(subroutinePromptPath, "utf-8");
104
+ console.log(`[Subroutine Transition] Loaded ${nextSubroutine.name} subroutine prompt (${subroutinePrompt.length} characters)`);
105
+ }
106
+ catch (error) {
107
+ console.error(`[Subroutine Transition] Failed to load subroutine prompt from ${subroutinePromptPath}:`, error);
108
+ // Fallback to simple prompt
109
+ subroutinePrompt = `Continue with: ${nextSubroutine.description}`;
110
+ }
111
+ // Resume Claude session with subroutine prompt
112
+ try {
113
+ await this.resumeClaudeSession(session, repo, linearAgentActivitySessionId, agentSessionManager, subroutinePrompt, "", // No attachment manifest
114
+ false, // Not a new session
115
+ [], // No additional allowed directories
116
+ nextSubroutine.maxTurns);
117
+ console.log(`[Subroutine Transition] Successfully resumed session for ${nextSubroutine.name} subroutine${nextSubroutine.maxTurns ? ` (maxTurns=${nextSubroutine.maxTurns})` : ""}`);
118
+ }
119
+ catch (error) {
120
+ console.error(`[Subroutine Transition] Failed to resume session for ${nextSubroutine.name} subroutine:`, error);
121
+ }
122
+ }, this.procedureRouter, this.sharedApplicationServer);
76
123
  this.agentSessionManagers.set(repo.id, agentSessionManager);
77
124
  }
78
125
  }
@@ -461,7 +508,8 @@ export class EdgeWorker extends EventEmitter {
461
508
  return this.childToParentAgentSession.get(childSessionId);
462
509
  }, async (parentSessionId, prompt, childSessionId) => {
463
510
  await this.handleResumeParentSession(parentSessionId, prompt, childSessionId, repo, agentSessionManager);
464
- });
511
+ }, undefined, // No resumeNextSubroutine callback for dynamically added repos
512
+ this.procedureRouter, this.sharedApplicationServer);
465
513
  this.agentSessionManagers.set(repo.id, agentSessionManager);
466
514
  // Update token-to-repo mapping
467
515
  const repoIds = this.tokenToRepoIds.get(repo.linearToken) || [];
@@ -1002,8 +1050,62 @@ export class EdgeWorker extends EventEmitter {
1002
1050
  const sessionData = await this.createLinearAgentSession(linearAgentActivitySessionId, issue, repository, agentSessionManager);
1003
1051
  // Destructure the session data (excluding allowedTools which we'll build with promptType)
1004
1052
  const { session, fullIssue, workspace: _workspace, attachmentResult, attachmentsDir: _attachmentsDir, allowedDirectories, } = sessionData;
1005
- // Fetch labels (needed for both model selection and system prompt determination)
1053
+ // Initialize procedure metadata using intelligent routing
1054
+ if (!session.metadata) {
1055
+ session.metadata = {};
1056
+ }
1057
+ // Post ephemeral "Routing..." thought
1058
+ await agentSessionManager.postRoutingThought(linearAgentActivitySessionId);
1059
+ // Fetch labels early (needed for label override check)
1006
1060
  const labels = await this.fetchIssueLabels(fullIssue);
1061
+ // Check for label overrides BEFORE AI routing
1062
+ const debuggerConfig = repository.labelPrompts?.debugger;
1063
+ const debuggerLabels = Array.isArray(debuggerConfig)
1064
+ ? debuggerConfig
1065
+ : debuggerConfig?.labels;
1066
+ const hasDebuggerLabel = debuggerLabels?.some((label) => labels.includes(label));
1067
+ const orchestratorConfig = repository.labelPrompts?.orchestrator;
1068
+ const orchestratorLabels = Array.isArray(orchestratorConfig)
1069
+ ? orchestratorConfig
1070
+ : orchestratorConfig?.labels;
1071
+ const hasOrchestratorLabel = orchestratorLabels?.some((label) => labels.includes(label));
1072
+ let finalProcedure;
1073
+ let finalClassification;
1074
+ // If labels indicate a specific procedure, use that instead of AI routing
1075
+ if (hasDebuggerLabel) {
1076
+ const debuggerProcedure = this.procedureRouter.getProcedure("debugger-full");
1077
+ if (!debuggerProcedure) {
1078
+ throw new Error("debugger-full procedure not found in registry");
1079
+ }
1080
+ finalProcedure = debuggerProcedure;
1081
+ finalClassification = "debugger";
1082
+ console.log(`[EdgeWorker] Using debugger-full procedure due to debugger label (skipping AI routing)`);
1083
+ }
1084
+ else if (hasOrchestratorLabel) {
1085
+ const orchestratorProcedure = this.procedureRouter.getProcedure("orchestrator-full");
1086
+ if (!orchestratorProcedure) {
1087
+ throw new Error("orchestrator-full procedure not found in registry");
1088
+ }
1089
+ finalProcedure = orchestratorProcedure;
1090
+ finalClassification = "orchestrator";
1091
+ console.log(`[EdgeWorker] Using orchestrator-full procedure due to orchestrator label (skipping AI routing)`);
1092
+ }
1093
+ else {
1094
+ // No label override - use AI routing
1095
+ const issueDescription = `${issue.title}\n\n${fullIssue.description || ""}`.trim();
1096
+ const routingDecision = await this.procedureRouter.determineRoutine(issueDescription);
1097
+ finalProcedure = routingDecision.procedure;
1098
+ finalClassification = routingDecision.classification;
1099
+ // Log AI routing decision
1100
+ console.log(`[EdgeWorker] AI routing decision for ${linearAgentActivitySessionId}:`);
1101
+ console.log(` Classification: ${routingDecision.classification}`);
1102
+ console.log(` Procedure: ${finalProcedure.name}`);
1103
+ console.log(` Reasoning: ${routingDecision.reasoning}`);
1104
+ }
1105
+ // Initialize procedure metadata in session with final decision
1106
+ this.procedureRouter.initializeProcedureMetadata(session, finalProcedure);
1107
+ // Post single procedure selection result (replaces ephemeral routing thought)
1108
+ await agentSessionManager.postProcedureSelectionThought(linearAgentActivitySessionId, finalProcedure.name, finalClassification);
1007
1109
  // Only determine system prompt for delegation (not mentions) or when /label-based-prompt is requested
1008
1110
  let systemPrompt;
1009
1111
  let systemPromptVersion;
@@ -1098,6 +1200,7 @@ export class EdgeWorker extends EventEmitter {
1098
1200
  }
1099
1201
  let session = agentSessionManager.getSession(linearAgentActivitySessionId);
1100
1202
  let isNewSession = false;
1203
+ let fullIssue = null;
1101
1204
  if (!session) {
1102
1205
  console.log(`[EdgeWorker] No existing session found for agent activity session ${linearAgentActivitySessionId}, creating new session`);
1103
1206
  isNewSession = true;
@@ -1106,23 +1209,70 @@ export class EdgeWorker extends EventEmitter {
1106
1209
  // Create the session using the shared method
1107
1210
  const sessionData = await this.createLinearAgentSession(linearAgentActivitySessionId, issue, repository, agentSessionManager);
1108
1211
  // Destructure session data for new session
1109
- const { fullIssue: newFullIssue } = sessionData;
1212
+ fullIssue = sessionData.fullIssue;
1110
1213
  session = sessionData.session;
1214
+ console.log(`[EdgeWorker] Created new session ${linearAgentActivitySessionId} (prompted webhook)`);
1111
1215
  // Save state and emit events for new session
1112
1216
  await this.savePersistedState();
1113
- this.emit("session:started", newFullIssue.id, newFullIssue, repository.id);
1114
- this.config.handlers?.onSessionStart?.(newFullIssue.id, newFullIssue, repository.id);
1217
+ this.emit("session:started", fullIssue.id, fullIssue, repository.id);
1218
+ this.config.handlers?.onSessionStart?.(fullIssue.id, fullIssue, repository.id);
1219
+ }
1220
+ else {
1221
+ console.log(`[EdgeWorker] Found existing session ${linearAgentActivitySessionId} for new user prompt`);
1222
+ // Post instant acknowledgment for existing session BEFORE any async work
1223
+ // Check streaming status first to determine the message
1224
+ const isCurrentlyStreaming = session?.claudeRunner?.isStreaming() || false;
1225
+ await this.postInstantPromptedAcknowledgment(linearAgentActivitySessionId, repository.id, isCurrentlyStreaming);
1226
+ // Need to fetch full issue for routing context
1227
+ const linearClient = this.linearClients.get(repository.id);
1228
+ if (linearClient) {
1229
+ try {
1230
+ fullIssue = await linearClient.issue(issue.id);
1231
+ }
1232
+ catch (error) {
1233
+ console.warn(`[EdgeWorker] Failed to fetch full issue for routing: ${issue.id}`, error);
1234
+ // Continue with degraded routing context
1235
+ }
1236
+ }
1237
+ }
1238
+ // Check if runner is actively streaming before routing
1239
+ const existingRunner = session?.claudeRunner;
1240
+ const isStreaming = existingRunner?.isStreaming() || false;
1241
+ // Always route procedure for new comments, UNLESS actively streaming
1242
+ if (!isStreaming) {
1243
+ // Initialize procedure metadata using intelligent routing
1244
+ if (!session.metadata) {
1245
+ session.metadata = {};
1246
+ }
1247
+ // Post ephemeral "Routing..." thought
1248
+ await agentSessionManager.postRoutingThought(linearAgentActivitySessionId);
1249
+ // For prompted events, use the actual prompt content from the user
1250
+ // Combine with issue context for better routing
1251
+ if (!fullIssue) {
1252
+ console.warn(`[EdgeWorker] Routing without full issue details for ${linearAgentActivitySessionId}`);
1253
+ }
1254
+ const promptBody = webhook.agentActivity.content.body;
1255
+ const routingDecision = await this.procedureRouter.determineRoutine(promptBody.trim());
1256
+ const selectedProcedure = routingDecision.procedure;
1257
+ // Initialize procedure metadata in session (resets for each new comment)
1258
+ this.procedureRouter.initializeProcedureMetadata(session, selectedProcedure);
1259
+ // Post procedure selection result (replaces ephemeral routing thought)
1260
+ await agentSessionManager.postProcedureSelectionThought(linearAgentActivitySessionId, selectedProcedure.name, routingDecision.classification);
1261
+ // Log routing decision
1262
+ console.log(`[EdgeWorker] Routing decision for ${linearAgentActivitySessionId} (prompted webhook, ${isNewSession ? "new" : "existing"} session):`);
1263
+ console.log(` Classification: ${routingDecision.classification}`);
1264
+ console.log(` Procedure: ${selectedProcedure.name}`);
1265
+ console.log(` Reasoning: ${routingDecision.reasoning}`);
1266
+ }
1267
+ else {
1268
+ console.log(`[EdgeWorker] Skipping routing for ${linearAgentActivitySessionId} - runner is actively streaming`);
1115
1269
  }
1116
1270
  // Ensure session is not null after creation/retrieval
1117
1271
  if (!session) {
1118
1272
  throw new Error(`Failed to get or create session for agent activity session ${linearAgentActivitySessionId}`);
1119
1273
  }
1120
- // Nothing before this should create latency or be async, so that these remain instant and low-latency for user experience
1121
- const existingRunner = session.claudeRunner;
1122
- if (!isNewSession) {
1123
- // Only post acknowledgment for existing sessions (new sessions already handled it above)
1124
- await this.postInstantPromptedAcknowledgment(linearAgentActivitySessionId, repository.id, existingRunner?.isStreaming() || false);
1125
- }
1274
+ // Acknowledgment already posted above for both new and existing sessions
1275
+ // (before any async routing work to ensure instant user feedback)
1126
1276
  // Get Linear client for this repository
1127
1277
  const linearClient = this.linearClients.get(repository.id);
1128
1278
  if (!linearClient) {
@@ -1186,7 +1336,6 @@ export class EdgeWorker extends EventEmitter {
1186
1336
  if (attachmentManifest) {
1187
1337
  fullPrompt = `${promptBody}\n\n${attachmentManifest}`;
1188
1338
  }
1189
- fullPrompt = `${fullPrompt}${LAST_MESSAGE_MARKER}`;
1190
1339
  existingRunner.addStreamMessage(fullPrompt);
1191
1340
  return; // Exit early - comment has been added to stream
1192
1341
  }
@@ -1416,7 +1565,6 @@ export class EdgeWorker extends EventEmitter {
1416
1565
  console.log(`[EdgeWorker] Adding attachment manifest to label-based prompt, length: ${attachmentManifest.length} characters`);
1417
1566
  prompt = `${prompt}\n\n${attachmentManifest}`;
1418
1567
  }
1419
- prompt = `${prompt}${LAST_MESSAGE_MARKER}`;
1420
1568
  console.log(`[EdgeWorker] Label-based prompt built successfully, length: ${prompt.length} characters`);
1421
1569
  return { prompt, version: templateVersion };
1422
1570
  }
@@ -1460,7 +1608,6 @@ IMPORTANT: You were specifically mentioned in the comment above. Focus on addres
1460
1608
  if (attachmentManifest) {
1461
1609
  prompt = `${prompt}\n\n${attachmentManifest}`;
1462
1610
  }
1463
- prompt = `${prompt}${LAST_MESSAGE_MARKER}`;
1464
1611
  return { prompt };
1465
1612
  }
1466
1613
  catch (error) {
@@ -1781,7 +1928,6 @@ IMPORTANT: Focus specifically on addressing the new comment above. This is a new
1781
1928
  console.log(`[EdgeWorker] Adding repository-specific instruction`);
1782
1929
  prompt = `${prompt}\n\n<repository-specific-instruction>\n${repository.appendInstruction}\n</repository-specific-instruction>`;
1783
1930
  }
1784
- prompt = `${prompt}${LAST_MESSAGE_MARKER}`;
1785
1931
  console.log(`[EdgeWorker] Final prompt length: ${prompt.length} characters`);
1786
1932
  return { prompt, version: templateVersion };
1787
1933
  }
@@ -1805,7 +1951,7 @@ Branch: ${issue.branchName}
1805
1951
  Working directory: ${repository.repositoryPath}
1806
1952
  Base branch: ${baseBranch}
1807
1953
 
1808
- ${newComment ? `New comment to address:\n${newComment.body}\n\n` : ""}Please analyze this issue and help implement a solution. ${LAST_MESSAGE_MARKER}`;
1954
+ ${newComment ? `New comment to address:\n${newComment.body}\n\n` : ""}Please analyze this issue and help implement a solution.`;
1809
1955
  return { prompt: fallbackPrompt, version: undefined };
1810
1956
  }
1811
1957
  }
@@ -2378,6 +2524,20 @@ ${newComment ? `New comment to address:\n${newComment.body}\n\n` : ""}Please ana
2378
2524
  },
2379
2525
  }),
2380
2526
  };
2527
+ // Add OpenAI-based MCP servers if API key is configured
2528
+ if (repository.openaiApiKey) {
2529
+ // Sora video generation tools
2530
+ mcpConfig["sora-tools"] = createSoraToolsServer({
2531
+ apiKey: repository.openaiApiKey,
2532
+ outputDirectory: repository.openaiOutputDirectory,
2533
+ });
2534
+ // GPT Image generation tools
2535
+ mcpConfig["image-tools"] = createImageToolsServer({
2536
+ apiKey: repository.openaiApiKey,
2537
+ outputDirectory: repository.openaiOutputDirectory,
2538
+ });
2539
+ console.log(`[EdgeWorker] Configured OpenAI MCP servers (Sora + GPT Image) for repository: ${repository.name}`);
2540
+ }
2381
2541
  return mcpConfig;
2382
2542
  }
2383
2543
  /**
@@ -2416,13 +2576,13 @@ ${newComment ? `New comment to address:\n${newComment.body}\n\n` : ""}Please ana
2416
2576
  const manifestSuffix = attachmentManifest
2417
2577
  ? `\n\n${attachmentManifest}`
2418
2578
  : "";
2419
- return `${promptBody}${manifestSuffix}${LAST_MESSAGE_MARKER}`;
2579
+ return `${promptBody}${manifestSuffix}`;
2420
2580
  }
2421
2581
  }
2422
2582
  /**
2423
2583
  * Build Claude runner configuration with common settings
2424
2584
  */
2425
- buildClaudeRunnerConfig(session, repository, linearAgentActivitySessionId, systemPrompt, allowedTools, allowedDirectories, disallowedTools, resumeSessionId, labels) {
2585
+ buildClaudeRunnerConfig(session, repository, linearAgentActivitySessionId, systemPrompt, allowedTools, allowedDirectories, disallowedTools, resumeSessionId, labels, maxTurns) {
2426
2586
  // Configure PostToolUse hook for playwright screenshots
2427
2587
  const hooks = {
2428
2588
  PostToolUse: [
@@ -2482,7 +2642,7 @@ ${newComment ? `New comment to address:\n${newComment.body}\n\n` : ""}Please ana
2482
2642
  cyrusHome: this.cyrusHome,
2483
2643
  mcpConfigPath: repository.mcpConfigPath,
2484
2644
  mcpConfig: this.buildMcpConfig(repository, linearAgentActivitySessionId),
2485
- appendSystemPrompt: (systemPrompt || "") + LAST_MESSAGE_MARKER,
2645
+ appendSystemPrompt: systemPrompt || "",
2486
2646
  // Priority order: label override > repository config > global default
2487
2647
  model: modelOverride || repository.model || this.config.defaultModel,
2488
2648
  fallbackModel: fallbackModelOverride ||
@@ -2497,6 +2657,9 @@ ${newComment ? `New comment to address:\n${newComment.body}\n\n` : ""}Please ana
2497
2657
  if (resumeSessionId) {
2498
2658
  config.resumeSessionId = resumeSessionId;
2499
2659
  }
2660
+ if (maxTurns !== undefined) {
2661
+ config.maxTurns = maxTurns;
2662
+ }
2500
2663
  return config;
2501
2664
  }
2502
2665
  /**
@@ -2812,7 +2975,7 @@ ${newComment ? `New comment to address:\n${newComment.body}\n\n` : ""}Please ana
2812
2975
  * @param attachmentManifest Optional attachment manifest
2813
2976
  * @param isNewSession Whether this is a new session
2814
2977
  */
2815
- async resumeClaudeSession(session, repository, linearAgentActivitySessionId, agentSessionManager, promptBody, attachmentManifest = "", isNewSession = false, additionalAllowedDirectories = []) {
2978
+ async resumeClaudeSession(session, repository, linearAgentActivitySessionId, agentSessionManager, promptBody, attachmentManifest = "", isNewSession = false, additionalAllowedDirectories = [], maxTurns) {
2816
2979
  // Check for existing runner
2817
2980
  const existingRunner = session.claudeRunner;
2818
2981
  // If there's an existing streaming runner, add to it
@@ -2821,7 +2984,6 @@ ${newComment ? `New comment to address:\n${newComment.body}\n\n` : ""}Please ana
2821
2984
  if (attachmentManifest) {
2822
2985
  fullPrompt = `${promptBody}\n\n${attachmentManifest}`;
2823
2986
  }
2824
- fullPrompt = `${fullPrompt}${LAST_MESSAGE_MARKER}`;
2825
2987
  existingRunner.addStreamMessage(fullPrompt);
2826
2988
  return;
2827
2989
  }
@@ -2854,7 +3016,11 @@ ${newComment ? `New comment to address:\n${newComment.body}\n\n` : ""}Please ana
2854
3016
  ...additionalAllowedDirectories,
2855
3017
  ];
2856
3018
  // Create runner configuration
2857
- const runnerConfig = this.buildClaudeRunnerConfig(session, repository, linearAgentActivitySessionId, systemPrompt, allowedTools, allowedDirectories, disallowedTools, needsNewClaudeSession ? undefined : session.claudeSessionId, labels);
3019
+ const resumeSessionId = needsNewClaudeSession
3020
+ ? undefined
3021
+ : session.claudeSessionId;
3022
+ const runnerConfig = this.buildClaudeRunnerConfig(session, repository, linearAgentActivitySessionId, systemPrompt, allowedTools, allowedDirectories, disallowedTools, resumeSessionId, labels, // Pass labels for model override
3023
+ maxTurns);
2858
3024
  const runner = new ClaudeRunner(runnerConfig);
2859
3025
  // Store runner
2860
3026
  agentSessionManager.addClaudeRunner(linearAgentActivitySessionId, runner);