cyrus-edge-worker 0.0.39 → 0.2.0-rc

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 (39) hide show
  1. package/dist/AgentSessionManager.d.ts.map +1 -1
  2. package/dist/AgentSessionManager.js +2 -4
  3. package/dist/AgentSessionManager.js.map +1 -1
  4. package/dist/EdgeWorker.d.ts +83 -32
  5. package/dist/EdgeWorker.d.ts.map +1 -1
  6. package/dist/EdgeWorker.js +618 -457
  7. package/dist/EdgeWorker.js.map +1 -1
  8. package/dist/SharedApplicationServer.d.ts +21 -63
  9. package/dist/SharedApplicationServer.d.ts.map +1 -1
  10. package/dist/SharedApplicationServer.js +93 -764
  11. package/dist/SharedApplicationServer.js.map +1 -1
  12. package/dist/procedures/ProcedureRouter.d.ts.map +1 -1
  13. package/dist/procedures/ProcedureRouter.js +11 -2
  14. package/dist/procedures/ProcedureRouter.js.map +1 -1
  15. package/dist/procedures/registry.d.ts +29 -0
  16. package/dist/procedures/registry.d.ts.map +1 -1
  17. package/dist/procedures/registry.js +45 -8
  18. package/dist/procedures/registry.js.map +1 -1
  19. package/dist/procedures/types.d.ts +1 -1
  20. package/dist/procedures/types.d.ts.map +1 -1
  21. package/dist/prompt-assembly/types.d.ts +81 -0
  22. package/dist/prompt-assembly/types.d.ts.map +1 -0
  23. package/dist/prompt-assembly/types.js +8 -0
  24. package/dist/prompt-assembly/types.js.map +1 -0
  25. package/dist/prompts/subroutines/coding-activity.md +10 -0
  26. package/dist/prompts/subroutines/concise-summary.md +16 -2
  27. package/dist/prompts/subroutines/debugger-fix.md +8 -25
  28. package/dist/prompts/subroutines/debugger-reproduction.md +11 -44
  29. package/dist/prompts/subroutines/git-gh.md +9 -6
  30. package/dist/prompts/subroutines/plan-summary.md +21 -0
  31. package/dist/prompts/subroutines/preparation.md +16 -0
  32. package/dist/prompts/subroutines/question-answer.md +8 -0
  33. package/dist/prompts/subroutines/question-investigation.md +8 -0
  34. package/dist/prompts/subroutines/verifications.md +9 -6
  35. package/package.json +8 -6
  36. package/prompts/orchestrator.md +9 -1
  37. package/prompts/standard-issue-assigned-user-prompt.md +33 -0
  38. package/prompts/todolist-system-prompt-extension.md +15 -0
  39. package/prompt-template-v2.md +0 -89
@@ -5,9 +5,9 @@ import { fileURLToPath } from "node:url";
5
5
  import { LinearClient, } from "@linear/sdk";
6
6
  import { watch as chokidarWatch } from "chokidar";
7
7
  import { ClaudeRunner, createCyrusToolsServer, createImageToolsServer, createSoraToolsServer, getAllTools, getCoordinatorTools, getReadOnlyTools, getSafeTools, } from "cyrus-claude-runner";
8
- import { isAgentSessionCreatedWebhook, isAgentSessionPromptedWebhook, isIssueAssignedWebhook, isIssueCommentMentionWebhook, isIssueNewCommentWebhook, isIssueUnassignedWebhook, PersistenceManager, } from "cyrus-core";
9
- import { LinearWebhookClient } from "cyrus-linear-webhook-client";
10
- import { NdjsonClient } from "cyrus-ndjson-client";
8
+ import { ConfigUpdater } from "cyrus-config-updater";
9
+ import { DEFAULT_PROXY_URL, isAgentSessionCreatedWebhook, isAgentSessionPromptedWebhook, isIssueAssignedWebhook, isIssueCommentMentionWebhook, isIssueNewCommentWebhook, isIssueUnassignedWebhook, PersistenceManager, resolvePath, } from "cyrus-core";
10
+ import { LinearEventTransport } from "cyrus-linear-event-transport";
11
11
  import { fileTypeFromBuffer } from "file-type";
12
12
  import { AgentSessionManager } from "./AgentSessionManager.js";
13
13
  import { ProcedureRouter, } from "./procedures/index.js";
@@ -23,7 +23,8 @@ export class EdgeWorker extends EventEmitter {
23
23
  repositories = new Map(); // repository 'id' (internal, stored in config.json) mapped to the full repo config
24
24
  agentSessionManagers = new Map(); // Maps repository ID to AgentSessionManager, which manages ClaudeRunners for a repo
25
25
  linearClients = new Map(); // one linear client per 'repository'
26
- ndjsonClients = new Map(); // listeners for webhook events, one per linear token
26
+ linearEventTransport = null; // Single event transport for webhook delivery
27
+ configUpdater = null; // Single config updater for configuration updates
27
28
  persistenceManager;
28
29
  sharedApplicationServer;
29
30
  cyrusHome;
@@ -31,7 +32,6 @@ export class EdgeWorker extends EventEmitter {
31
32
  procedureRouter; // Intelligent workflow routing
32
33
  configWatcher; // File watcher for config.json
33
34
  configPath; // Path to config.json file
34
- tokenToRepoIds = new Map(); // Maps Linear token to repository IDs using that token
35
35
  constructor(config) {
36
36
  super();
37
37
  this.config = config;
@@ -48,15 +48,28 @@ export class EdgeWorker extends EventEmitter {
48
48
  // Initialize shared application server
49
49
  const serverPort = config.serverPort || config.webhookPort || 3456;
50
50
  const serverHost = config.serverHost || "localhost";
51
- this.sharedApplicationServer = new SharedApplicationServer(serverPort, serverHost, config.ngrokAuthToken, config.proxyUrl);
52
- // Register OAuth callback handler if provided
53
- if (config.handlers?.onOAuthCallback) {
54
- this.sharedApplicationServer.registerOAuthCallbackHandler(config.handlers.onOAuthCallback);
55
- }
56
- // Initialize repositories
51
+ this.sharedApplicationServer = new SharedApplicationServer(serverPort, serverHost);
52
+ // Initialize repositories with path resolution
57
53
  for (const repo of config.repositories) {
58
54
  if (repo.isActive !== false) {
59
- this.repositories.set(repo.id, repo);
55
+ // Resolve paths that may contain tilde (~) prefix
56
+ const resolvedRepo = {
57
+ ...repo,
58
+ repositoryPath: resolvePath(repo.repositoryPath),
59
+ workspaceBaseDir: resolvePath(repo.workspaceBaseDir),
60
+ mcpConfigPath: Array.isArray(repo.mcpConfigPath)
61
+ ? repo.mcpConfigPath.map(resolvePath)
62
+ : repo.mcpConfigPath
63
+ ? resolvePath(repo.mcpConfigPath)
64
+ : undefined,
65
+ promptTemplatePath: repo.promptTemplatePath
66
+ ? resolvePath(repo.promptTemplatePath)
67
+ : undefined,
68
+ openaiOutputDirectory: repo.openaiOutputDirectory
69
+ ? resolvePath(repo.openaiOutputDirectory)
70
+ : undefined,
71
+ };
72
+ this.repositories.set(repo.id, resolvedRepo);
60
73
  // Create Linear client for this repository's workspace
61
74
  const linearClient = new LinearClient({
62
75
  accessToken: repo.linearToken,
@@ -123,77 +136,7 @@ export class EdgeWorker extends EventEmitter {
123
136
  this.agentSessionManagers.set(repo.id, agentSessionManager);
124
137
  }
125
138
  }
126
- // Group repositories by token to minimize NDJSON connections
127
- const tokenToRepos = new Map();
128
- for (const repo of this.repositories.values()) {
129
- const repos = tokenToRepos.get(repo.linearToken) || [];
130
- repos.push(repo);
131
- tokenToRepos.set(repo.linearToken, repos);
132
- // Track token-to-repo-id mapping for dynamic config updates
133
- const repoIds = this.tokenToRepoIds.get(repo.linearToken) || [];
134
- if (!repoIds.includes(repo.id)) {
135
- repoIds.push(repo.id);
136
- }
137
- this.tokenToRepoIds.set(repo.linearToken, repoIds);
138
- }
139
- // Create one NDJSON client per unique token using shared application server
140
- for (const [token, repos] of tokenToRepos) {
141
- if (!repos || repos.length === 0)
142
- continue;
143
- const firstRepo = repos[0];
144
- if (!firstRepo)
145
- continue;
146
- const primaryRepoId = firstRepo.id;
147
- // Determine which client to use based on environment variable
148
- const useLinearDirectWebhooks = process.env.LINEAR_DIRECT_WEBHOOKS?.toLowerCase().trim() === "true";
149
- const clientConfig = {
150
- proxyUrl: config.proxyUrl,
151
- token: token,
152
- name: repos.map((r) => r.name).join(", "), // Pass repository names
153
- transport: "webhook",
154
- // Use shared application server instead of individual servers
155
- useExternalWebhookServer: true,
156
- externalWebhookServer: this.sharedApplicationServer,
157
- webhookPort: serverPort, // All clients use same port
158
- webhookPath: "/webhook",
159
- webhookHost: serverHost,
160
- ...(config.baseUrl && { webhookBaseUrl: config.baseUrl }),
161
- // Legacy fallback support
162
- ...(!config.baseUrl &&
163
- config.webhookBaseUrl && { webhookBaseUrl: config.webhookBaseUrl }),
164
- onConnect: () => this.handleConnect(primaryRepoId, repos),
165
- onDisconnect: (reason) => this.handleDisconnect(primaryRepoId, repos, reason),
166
- onError: (error) => this.handleError(error),
167
- };
168
- // Create the appropriate client based on configuration
169
- const ndjsonClient = useLinearDirectWebhooks
170
- ? new LinearWebhookClient({
171
- ...clientConfig,
172
- onWebhook: (payload) => {
173
- // Get fresh repositories for this token to avoid stale closures
174
- const freshRepos = this.getRepositoriesForToken(token);
175
- this.handleWebhook(payload, freshRepos);
176
- },
177
- })
178
- : new NdjsonClient(clientConfig);
179
- // Set up webhook handler for NdjsonClient (LinearWebhookClient uses onWebhook in constructor)
180
- if (!useLinearDirectWebhooks) {
181
- ndjsonClient.on("webhook", (data) => {
182
- // Get fresh repositories for this token to avoid stale closures
183
- const freshRepos = this.getRepositoriesForToken(token);
184
- this.handleWebhook(data, freshRepos);
185
- });
186
- }
187
- // Optional heartbeat logging (only for NdjsonClient)
188
- if (process.env.DEBUG_EDGE === "true" && !useLinearDirectWebhooks) {
189
- ndjsonClient.on("heartbeat", () => {
190
- console.log(`❤️ Heartbeat received for token ending in ...${token.slice(-4)}`);
191
- });
192
- }
193
- // Store with the first repo's ID as the key (for error messages)
194
- // But also store the token mapping for lookup
195
- this.ndjsonClients.set(primaryRepoId, ndjsonClient);
196
- }
139
+ // Components will be initialized and registered in start() method before server starts
197
140
  }
198
141
  /**
199
142
  * Start the edge worker
@@ -205,52 +148,53 @@ export class EdgeWorker extends EventEmitter {
205
148
  if (this.configPath) {
206
149
  this.startConfigWatcher();
207
150
  }
208
- // Start shared application server first
151
+ // Initialize and register components BEFORE starting server (routes must be registered before listen())
152
+ await this.initializeComponents();
153
+ // Start shared application server (this also starts Cloudflare tunnel if CLOUDFLARE_TOKEN is set)
209
154
  await this.sharedApplicationServer.start();
210
- // Connect all NDJSON clients
211
- const connections = Array.from(this.ndjsonClients.entries()).map(async ([repoId, client]) => {
212
- try {
213
- await client.connect();
214
- }
215
- catch (error) {
216
- const repoConfig = this.config.repositories.find((r) => r.id === repoId);
217
- const repoName = repoConfig?.name || repoId;
218
- // Check if it's an authentication error
219
- if (error.isAuthError || error.code === "LINEAR_AUTH_FAILED") {
220
- console.error(`\n❌ Linear authentication failed for repository: ${repoName}`);
221
- console.error(` Workspace: ${repoConfig?.linearWorkspaceName || repoConfig?.linearWorkspaceId || "Unknown"}`);
222
- console.error(` Error: ${error.message}`);
223
- console.error(`\n To fix this issue:`);
224
- console.error(` 1. Run: cyrus refresh-token`);
225
- console.error(` 2. Complete the OAuth flow in your browser`);
226
- console.error(` 3. The configuration will be automatically updated\n`);
227
- console.error(` You can also check all tokens with: cyrus check-tokens\n`);
228
- // Continue with other repositories instead of failing completely
229
- return { repoId, success: false, error };
230
- }
231
- // For other errors, still log but with less guidance
232
- console.error(`\n❌ Failed to connect repository: ${repoName}`);
233
- console.error(` Error: ${error.message}\n`);
234
- return { repoId, success: false, error };
235
- }
236
- return { repoId, success: true };
155
+ }
156
+ /**
157
+ * Initialize and register components (routes) before server starts
158
+ */
159
+ async initializeComponents() {
160
+ // Get the first active repository for configuration
161
+ const firstRepo = Array.from(this.repositories.values())[0];
162
+ if (!firstRepo) {
163
+ throw new Error("No active repositories configured");
164
+ }
165
+ // 1. Create and register LinearEventTransport
166
+ const useDirectWebhooks = process.env.LINEAR_DIRECT_WEBHOOKS?.toLowerCase() === "true";
167
+ const verificationMode = useDirectWebhooks ? "direct" : "proxy";
168
+ // Get appropriate secret based on mode
169
+ const secret = useDirectWebhooks
170
+ ? process.env.LINEAR_WEBHOOK_SECRET || ""
171
+ : process.env.CYRUS_API_KEY || "";
172
+ this.linearEventTransport = new LinearEventTransport({
173
+ fastifyServer: this.sharedApplicationServer.getFastifyInstance(),
174
+ verificationMode,
175
+ secret,
237
176
  });
238
- const results = await Promise.all(connections);
239
- const failures = results.filter((r) => !r.success);
240
- if (failures.length === this.ndjsonClients.size) {
241
- // All connections failed
242
- throw new Error("Failed to connect any repositories. Please check your configuration and Linear tokens.");
243
- }
244
- else if (failures.length > 0) {
245
- // Some connections failed
246
- console.warn(`\n⚠️ Connected ${results.length - failures.length} out of ${results.length} repositories`);
247
- console.warn(` The following repositories could not be connected:`);
248
- failures.forEach((f) => {
249
- const repoConfig = this.config.repositories.find((r) => r.id === f.repoId);
250
- console.warn(` - ${repoConfig?.name || f.repoId}`);
251
- });
252
- console.warn(`\n Cyrus will continue running with the available repositories.\n`);
253
- }
177
+ // Listen for webhook events
178
+ this.linearEventTransport.on("webhook", (payload) => {
179
+ // Get all active repositories for webhook handling
180
+ const repos = Array.from(this.repositories.values());
181
+ this.handleWebhook(payload, repos);
182
+ });
183
+ // Listen for errors
184
+ this.linearEventTransport.on("error", (error) => {
185
+ this.handleError(error);
186
+ });
187
+ // Register the /webhook endpoint
188
+ this.linearEventTransport.register();
189
+ console.log(`✅ Linear event transport registered (${verificationMode} mode)`);
190
+ console.log(` Webhook endpoint: ${this.sharedApplicationServer.getWebhookUrl()}`);
191
+ // 2. Create and register ConfigUpdater
192
+ this.configUpdater = new ConfigUpdater(this.sharedApplicationServer.getFastifyInstance(), this.cyrusHome, process.env.CYRUS_API_KEY || "");
193
+ // Register config update routes
194
+ this.configUpdater.register();
195
+ console.log("✅ Config updater registered");
196
+ console.log(" Routes: /api/update/cyrus-config, /api/update/cyrus-env,");
197
+ console.log(" /api/update/repository, /api/test-mcp, /api/configure-mcp");
254
198
  }
255
199
  /**
256
200
  * Stop the edge worker
@@ -285,11 +229,10 @@ export class EdgeWorker extends EventEmitter {
285
229
  }
286
230
  }
287
231
  }
288
- // Disconnect all NDJSON clients
289
- for (const client of this.ndjsonClients.values()) {
290
- client.disconnect();
291
- }
292
- // Stop shared application server
232
+ // Clear event transport (no explicit cleanup needed, routes are removed when server stops)
233
+ this.linearEventTransport = null;
234
+ this.configUpdater = null;
235
+ // Stop shared application server (this also stops Cloudflare tunnel if running)
293
236
  await this.sharedApplicationServer.stop();
294
237
  }
295
238
  /**
@@ -298,21 +241,6 @@ export class EdgeWorker extends EventEmitter {
298
241
  setConfigPath(configPath) {
299
242
  this.configPath = configPath;
300
243
  }
301
- /**
302
- * Get fresh list of repositories for a given Linear token
303
- * This ensures webhook handlers always work with current repository state
304
- */
305
- getRepositoriesForToken(token) {
306
- const repoIds = this.tokenToRepoIds.get(token) || [];
307
- const repos = [];
308
- for (const repoId of repoIds) {
309
- const repo = this.repositories.get(repoId);
310
- if (repo) {
311
- repos.push(repo);
312
- }
313
- }
314
- return repos;
315
- }
316
244
  /**
317
245
  * Handle resuming a parent session when a child session completes
318
246
  * This is the core logic used by the resume parent session callback
@@ -339,13 +267,38 @@ export class EdgeWorker extends EventEmitter {
339
267
  console.warn(`[Parent Session Resume] Could not find child session ${childSessionId} to add workspace to parent allowed directories`);
340
268
  }
341
269
  await this.postParentResumeAcknowledgment(parentSessionId, repo.id);
342
- // Resume the parent session with the child's result
343
- console.log(`[Parent Session Resume] Resuming parent Claude session with child results`);
270
+ // Post thought to Linear showing child result receipt
271
+ const linearClient = this.linearClients.get(repo.id);
272
+ if (linearClient && childSession) {
273
+ const childIssueIdentifier = childSession.issue?.identifier || childSession.issueId;
274
+ const resultThought = `Received result from sub-issue ${childIssueIdentifier}:\n\n---\n\n${prompt}\n\n---`;
275
+ try {
276
+ const result = await linearClient.createAgentActivity({
277
+ agentSessionId: parentSessionId,
278
+ content: {
279
+ type: "thought",
280
+ body: resultThought,
281
+ },
282
+ });
283
+ if (result.success) {
284
+ console.log(`[Parent Session Resume] Posted child result receipt thought for parent session ${parentSessionId}`);
285
+ }
286
+ else {
287
+ console.error(`[Parent Session Resume] Failed to post child result receipt thought:`, result);
288
+ }
289
+ }
290
+ catch (error) {
291
+ console.error(`[Parent Session Resume] Error posting child result receipt thought:`, error);
292
+ }
293
+ }
294
+ // Use centralized streaming check and routing logic
295
+ console.log(`[Parent Session Resume] Handling child result for parent session ${parentSessionId}`);
344
296
  try {
345
- await this.resumeClaudeSession(parentSession, repo, parentSessionId, agentSessionManager, prompt, "", // No attachment manifest for child results
297
+ await this.handlePromptWithStreamingCheck(parentSession, repo, parentSessionId, agentSessionManager, prompt, "", // No attachment manifest for child results
346
298
  false, // Not a new session
347
- childWorkspaceDirs);
348
- console.log(`[Parent Session Resume] Successfully resumed parent session ${parentSessionId} with child results`);
299
+ childWorkspaceDirs, // Add child workspace directories to parent's allowed directories
300
+ "parent resume from child");
301
+ console.log(`[Parent Session Resume] Successfully handled child result for parent session ${parentSessionId}`);
349
302
  }
350
303
  catch (error) {
351
304
  console.error(`[Parent Session Resume] Failed to resume parent session ${parentSessionId}:`, error);
@@ -496,8 +449,25 @@ export class EdgeWorker extends EventEmitter {
496
449
  }
497
450
  try {
498
451
  console.log(`➕ Adding repository: ${repo.name} (${repo.id})`);
452
+ // Resolve paths that may contain tilde (~) prefix
453
+ const resolvedRepo = {
454
+ ...repo,
455
+ repositoryPath: resolvePath(repo.repositoryPath),
456
+ workspaceBaseDir: resolvePath(repo.workspaceBaseDir),
457
+ mcpConfigPath: Array.isArray(repo.mcpConfigPath)
458
+ ? repo.mcpConfigPath.map(resolvePath)
459
+ : repo.mcpConfigPath
460
+ ? resolvePath(repo.mcpConfigPath)
461
+ : undefined,
462
+ promptTemplatePath: repo.promptTemplatePath
463
+ ? resolvePath(repo.promptTemplatePath)
464
+ : undefined,
465
+ openaiOutputDirectory: repo.openaiOutputDirectory
466
+ ? resolvePath(repo.openaiOutputDirectory)
467
+ : undefined,
468
+ };
499
469
  // Add to internal map
500
- this.repositories.set(repo.id, repo);
470
+ this.repositories.set(repo.id, resolvedRepo);
501
471
  // Create Linear client
502
472
  const linearClient = new LinearClient({
503
473
  accessToken: repo.linearToken,
@@ -511,14 +481,6 @@ export class EdgeWorker extends EventEmitter {
511
481
  }, undefined, // No resumeNextSubroutine callback for dynamically added repos
512
482
  this.procedureRouter, this.sharedApplicationServer);
513
483
  this.agentSessionManagers.set(repo.id, agentSessionManager);
514
- // Update token-to-repo mapping
515
- const repoIds = this.tokenToRepoIds.get(repo.linearToken) || [];
516
- if (!repoIds.includes(repo.id)) {
517
- repoIds.push(repo.id);
518
- }
519
- this.tokenToRepoIds.set(repo.linearToken, repoIds);
520
- // Set up webhook listener
521
- await this.setupWebhookListener(repo);
522
484
  console.log(`✅ Repository added successfully: ${repo.name}`);
523
485
  }
524
486
  catch (error) {
@@ -538,8 +500,25 @@ export class EdgeWorker extends EventEmitter {
538
500
  continue;
539
501
  }
540
502
  console.log(`🔄 Updating repository: ${repo.name} (${repo.id})`);
503
+ // Resolve paths that may contain tilde (~) prefix
504
+ const resolvedRepo = {
505
+ ...repo,
506
+ repositoryPath: resolvePath(repo.repositoryPath),
507
+ workspaceBaseDir: resolvePath(repo.workspaceBaseDir),
508
+ mcpConfigPath: Array.isArray(repo.mcpConfigPath)
509
+ ? repo.mcpConfigPath.map(resolvePath)
510
+ : repo.mcpConfigPath
511
+ ? resolvePath(repo.mcpConfigPath)
512
+ : undefined,
513
+ promptTemplatePath: repo.promptTemplatePath
514
+ ? resolvePath(repo.promptTemplatePath)
515
+ : undefined,
516
+ openaiOutputDirectory: repo.openaiOutputDirectory
517
+ ? resolvePath(repo.openaiOutputDirectory)
518
+ : undefined,
519
+ };
541
520
  // Update stored config
542
- this.repositories.set(repo.id, repo);
521
+ this.repositories.set(repo.id, resolvedRepo);
543
522
  // If token changed, recreate Linear client
544
523
  if (oldRepo.linearToken !== repo.linearToken) {
545
524
  console.log(` 🔑 Token changed, recreating Linear client`);
@@ -547,22 +526,6 @@ export class EdgeWorker extends EventEmitter {
547
526
  accessToken: repo.linearToken,
548
527
  });
549
528
  this.linearClients.set(repo.id, linearClient);
550
- // Update token mapping
551
- const oldRepoIds = this.tokenToRepoIds.get(oldRepo.linearToken) || [];
552
- const filteredOldIds = oldRepoIds.filter((id) => id !== repo.id);
553
- if (filteredOldIds.length > 0) {
554
- this.tokenToRepoIds.set(oldRepo.linearToken, filteredOldIds);
555
- }
556
- else {
557
- this.tokenToRepoIds.delete(oldRepo.linearToken);
558
- }
559
- const newRepoIds = this.tokenToRepoIds.get(repo.linearToken) || [];
560
- if (!newRepoIds.includes(repo.id)) {
561
- newRepoIds.push(repo.id);
562
- }
563
- this.tokenToRepoIds.set(repo.linearToken, newRepoIds);
564
- // Reconnect webhook if needed
565
- await this.reconnectWebhook(oldRepo, repo);
566
529
  }
567
530
  // If active status changed
568
531
  if (oldRepo.isActive !== repo.isActive) {
@@ -571,7 +534,6 @@ export class EdgeWorker extends EventEmitter {
571
534
  }
572
535
  else {
573
536
  console.log(` ▶️ Repository reactivated`);
574
- await this.setupWebhookListener(repo);
575
537
  }
576
538
  }
577
539
  console.log(`✅ Repository updated successfully: ${repo.name}`);
@@ -626,17 +588,6 @@ export class EdgeWorker extends EventEmitter {
626
588
  this.repositories.delete(repo.id);
627
589
  this.linearClients.delete(repo.id);
628
590
  this.agentSessionManagers.delete(repo.id);
629
- // Update token mapping
630
- const repoIds = this.tokenToRepoIds.get(repo.linearToken) || [];
631
- const filteredIds = repoIds.filter((id) => id !== repo.id);
632
- if (filteredIds.length > 0) {
633
- this.tokenToRepoIds.set(repo.linearToken, filteredIds);
634
- }
635
- else {
636
- this.tokenToRepoIds.delete(repo.linearToken);
637
- }
638
- // Clean up webhook listener if no other repos use the same token
639
- await this.cleanupWebhookIfUnused(repo);
640
591
  console.log(`✅ Repository removed successfully: ${repo.name}`);
641
592
  }
642
593
  catch (error) {
@@ -644,115 +595,6 @@ export class EdgeWorker extends EventEmitter {
644
595
  }
645
596
  }
646
597
  }
647
- /**
648
- * Set up webhook listener for a repository
649
- */
650
- async setupWebhookListener(repo) {
651
- // Check if we already have a client for this token
652
- const existingRepoIds = this.tokenToRepoIds.get(repo.linearToken) || [];
653
- const existingClient = existingRepoIds.length > 0
654
- ? this.ndjsonClients.get(existingRepoIds[0] || "")
655
- : null;
656
- if (existingClient) {
657
- console.log(` ℹ️ Reusing existing webhook connection for token ...${repo.linearToken.slice(-4)}`);
658
- return;
659
- }
660
- // Create new NDJSON client for this token
661
- const serverPort = this.config.serverPort || this.config.webhookPort || 3456;
662
- const serverHost = this.config.serverHost || "localhost";
663
- const useLinearDirectWebhooks = process.env.LINEAR_DIRECT_WEBHOOKS?.toLowerCase().trim() === "true";
664
- const clientConfig = {
665
- proxyUrl: this.config.proxyUrl,
666
- token: repo.linearToken,
667
- name: repo.name,
668
- transport: "webhook",
669
- useExternalWebhookServer: true,
670
- externalWebhookServer: this.sharedApplicationServer,
671
- webhookPort: serverPort,
672
- webhookPath: "/webhook",
673
- webhookHost: serverHost,
674
- ...(this.config.baseUrl && { webhookBaseUrl: this.config.baseUrl }),
675
- ...(!this.config.baseUrl &&
676
- this.config.webhookBaseUrl && {
677
- webhookBaseUrl: this.config.webhookBaseUrl,
678
- }),
679
- onConnect: () => this.handleConnect(repo.id, [repo]),
680
- onDisconnect: (reason) => this.handleDisconnect(repo.id, [repo], reason),
681
- onError: (error) => this.handleError(error),
682
- };
683
- const ndjsonClient = useLinearDirectWebhooks
684
- ? new LinearWebhookClient({
685
- ...clientConfig,
686
- onWebhook: (payload) => {
687
- // Get fresh repositories for this token to avoid stale closures
688
- const freshRepos = this.getRepositoriesForToken(repo.linearToken);
689
- this.handleWebhook(payload, freshRepos);
690
- },
691
- })
692
- : new NdjsonClient(clientConfig);
693
- if (!useLinearDirectWebhooks) {
694
- ndjsonClient.on("webhook", (data) => {
695
- // Get fresh repositories for this token to avoid stale closures
696
- const freshRepos = this.getRepositoriesForToken(repo.linearToken);
697
- this.handleWebhook(data, freshRepos);
698
- });
699
- }
700
- this.ndjsonClients.set(repo.id, ndjsonClient);
701
- // Connect the client
702
- try {
703
- await ndjsonClient.connect();
704
- console.log(` ✅ Webhook listener connected for ${repo.name}`);
705
- }
706
- catch (error) {
707
- console.error(` ❌ Failed to connect webhook listener:`, error);
708
- }
709
- }
710
- /**
711
- * Reconnect webhook when token changes
712
- */
713
- async reconnectWebhook(oldRepo, newRepo) {
714
- console.log(` 🔌 Reconnecting webhook due to token change`);
715
- // Disconnect old client if no other repos use it
716
- await this.cleanupWebhookIfUnused(oldRepo);
717
- // Set up new connection
718
- await this.setupWebhookListener(newRepo);
719
- }
720
- /**
721
- * Clean up webhook listener if no other repositories use the token
722
- */
723
- async cleanupWebhookIfUnused(repo) {
724
- const repoIds = this.tokenToRepoIds.get(repo.linearToken) || [];
725
- const otherRepos = repoIds.filter((id) => id !== repo.id);
726
- if (otherRepos.length === 0) {
727
- // No other repos use this token, safe to disconnect
728
- const client = this.ndjsonClients.get(repo.id);
729
- if (client) {
730
- console.log(` 🔌 Disconnecting webhook for token ...${repo.linearToken.slice(-4)}`);
731
- client.disconnect();
732
- this.ndjsonClients.delete(repo.id);
733
- }
734
- }
735
- else {
736
- console.log(` ℹ️ Token still used by ${otherRepos.length} other repository(ies), keeping connection`);
737
- }
738
- }
739
- /**
740
- * Handle connection established
741
- */
742
- handleConnect(clientId, repos) {
743
- // Get the token for backward compatibility with events
744
- const token = repos[0]?.linearToken || clientId;
745
- this.emit("connected", token);
746
- // Connection logged by CLI app event handler
747
- }
748
- /**
749
- * Handle disconnection
750
- */
751
- handleDisconnect(clientId, repos, reason) {
752
- // Get the token for backward compatibility with events
753
- const token = repos[0]?.linearToken || clientId;
754
- this.emit("disconnected", token, reason);
755
- }
756
598
  /**
757
599
  * Handle errors
758
600
  */
@@ -1106,71 +948,66 @@ export class EdgeWorker extends EventEmitter {
1106
948
  this.procedureRouter.initializeProcedureMetadata(session, finalProcedure);
1107
949
  // Post single procedure selection result (replaces ephemeral routing thought)
1108
950
  await agentSessionManager.postProcedureSelectionThought(linearAgentActivitySessionId, finalProcedure.name, finalClassification);
1109
- // Only determine system prompt for delegation (not mentions) or when /label-based-prompt is requested
1110
- let systemPrompt;
1111
- let systemPromptVersion;
1112
- let promptType;
1113
- if (!isMentionTriggered || isLabelBasedPromptRequested) {
1114
- // Determine system prompt based on labels (delegation case or /label-based-prompt command)
1115
- const systemPromptResult = await this.determineSystemPromptFromLabels(labels, repository);
1116
- systemPrompt = systemPromptResult?.prompt;
1117
- systemPromptVersion = systemPromptResult?.version;
1118
- promptType = systemPromptResult?.type;
1119
- // Post thought about system prompt selection
1120
- if (systemPrompt) {
1121
- await this.postSystemPromptSelectionThought(linearAgentActivitySessionId, labels, repository.id);
1122
- }
1123
- }
1124
- else {
1125
- console.log(`[EdgeWorker] Skipping system prompt for mention-triggered session ${linearAgentActivitySessionId}`);
1126
- }
1127
- // Build allowed tools list with Linear MCP tools (now with prompt type context)
1128
- const allowedTools = this.buildAllowedTools(repository, promptType);
1129
- const disallowedTools = this.buildDisallowedTools(repository, promptType);
1130
- console.log(`[EdgeWorker] Configured allowed tools for ${fullIssue.identifier}:`, allowedTools);
1131
- if (disallowedTools.length > 0) {
1132
- console.log(`[EdgeWorker] Configured disallowed tools for ${fullIssue.identifier}:`, disallowedTools);
1133
- }
1134
- // Create Claude runner with attachment directory access and optional system prompt
1135
- const runnerConfig = this.buildClaudeRunnerConfig(session, repository, linearAgentActivitySessionId, systemPrompt, allowedTools, allowedDirectories, disallowedTools, undefined, // resumeSessionId
1136
- labels);
1137
- const runner = new ClaudeRunner(runnerConfig);
1138
- // Store runner by comment ID
1139
- agentSessionManager.addClaudeRunner(linearAgentActivitySessionId, runner);
1140
- // Save state after mapping changes
1141
- await this.savePersistedState();
1142
- // Emit events using full Linear issue
1143
- this.emit("session:started", fullIssue.id, fullIssue, repository.id);
1144
- this.config.handlers?.onSessionStart?.(fullIssue.id, fullIssue, repository.id);
1145
951
  // Build and start Claude with initial prompt using full issue (streaming mode)
1146
952
  console.log(`[EdgeWorker] Building initial prompt for issue ${fullIssue.identifier}`);
1147
953
  try {
1148
- // Choose the appropriate prompt builder based on trigger type and system prompt
1149
- const promptResult = isMentionTriggered && isLabelBasedPromptRequested
1150
- ? await this.buildLabelBasedPrompt(fullIssue, repository, attachmentResult.manifest, guidance)
1151
- : isMentionTriggered
1152
- ? await this.buildMentionPrompt(fullIssue, agentSession, attachmentResult.manifest, guidance)
1153
- : systemPrompt
1154
- ? await this.buildLabelBasedPrompt(fullIssue, repository, attachmentResult.manifest, guidance)
1155
- : await this.buildPromptV2(fullIssue, repository, undefined, attachmentResult.manifest, guidance);
1156
- const { prompt, version: userPromptVersion } = promptResult;
1157
- // Update runner with version information
1158
- if (userPromptVersion || systemPromptVersion) {
954
+ // Create input for unified prompt assembly
955
+ const input = {
956
+ session,
957
+ fullIssue,
958
+ repository,
959
+ userComment: commentBody || "", // Empty for delegation, present for mentions
960
+ attachmentManifest: attachmentResult.manifest,
961
+ guidance,
962
+ agentSession,
963
+ labels,
964
+ isNewSession: true,
965
+ isStreaming: false, // Not yet streaming
966
+ isMentionTriggered: isMentionTriggered || false,
967
+ isLabelBasedPromptRequested: isLabelBasedPromptRequested || false,
968
+ };
969
+ // Use unified prompt assembly
970
+ const assembly = await this.assemblePrompt(input);
971
+ // Get systemPromptVersion for tracking (TODO: add to PromptAssembly metadata)
972
+ let systemPromptVersion;
973
+ let promptType;
974
+ if (!isMentionTriggered || isLabelBasedPromptRequested) {
975
+ const systemPromptResult = await this.determineSystemPromptFromLabels(labels, repository);
976
+ systemPromptVersion = systemPromptResult?.version;
977
+ promptType = systemPromptResult?.type;
978
+ // Post thought about system prompt selection
979
+ if (assembly.systemPrompt) {
980
+ await this.postSystemPromptSelectionThought(linearAgentActivitySessionId, labels, repository.id);
981
+ }
982
+ }
983
+ // Build allowed tools list with Linear MCP tools (now with prompt type context)
984
+ const allowedTools = this.buildAllowedTools(repository, promptType);
985
+ const disallowedTools = this.buildDisallowedTools(repository, promptType);
986
+ console.log(`[EdgeWorker] Configured allowed tools for ${fullIssue.identifier}:`, allowedTools);
987
+ if (disallowedTools.length > 0) {
988
+ console.log(`[EdgeWorker] Configured disallowed tools for ${fullIssue.identifier}:`, disallowedTools);
989
+ }
990
+ // Create Claude runner with system prompt from assembly
991
+ const runnerConfig = this.buildClaudeRunnerConfig(session, repository, linearAgentActivitySessionId, assembly.systemPrompt, allowedTools, allowedDirectories, disallowedTools, undefined, // resumeSessionId
992
+ labels);
993
+ const runner = new ClaudeRunner(runnerConfig);
994
+ // Store runner by comment ID
995
+ agentSessionManager.addClaudeRunner(linearAgentActivitySessionId, runner);
996
+ // Save state after mapping changes
997
+ await this.savePersistedState();
998
+ // Emit events using full Linear issue
999
+ this.emit("session:started", fullIssue.id, fullIssue, repository.id);
1000
+ this.config.handlers?.onSessionStart?.(fullIssue.id, fullIssue, repository.id);
1001
+ // Update runner with version information (if available)
1002
+ if (systemPromptVersion) {
1159
1003
  runner.updatePromptVersions({
1160
- userPromptVersion,
1161
1004
  systemPromptVersion,
1162
1005
  });
1163
1006
  }
1164
- const promptType = isMentionTriggered && isLabelBasedPromptRequested
1165
- ? "label-based-prompt-command"
1166
- : isMentionTriggered
1167
- ? "mention"
1168
- : systemPrompt
1169
- ? "label-based"
1170
- : "fallback";
1171
- console.log(`[EdgeWorker] Initial prompt built successfully using ${promptType} workflow, length: ${prompt.length} characters`);
1007
+ // Log metadata for debugging
1008
+ console.log(`[EdgeWorker] Initial prompt built successfully - components: ${assembly.metadata.components.join(", ")}, type: ${assembly.metadata.promptType}, length: ${assembly.userPrompt.length} characters`);
1172
1009
  console.log(`[EdgeWorker] Starting Claude streaming session`);
1173
- const sessionInfo = await runner.startStreaming(prompt);
1010
+ const sessionInfo = await runner.startStreaming(assembly.userPrompt);
1174
1011
  console.log(`[EdgeWorker] Claude streaming session started: ${sessionInfo.sessionId}`);
1175
1012
  // Note: AgentSessionManager will be initialized automatically when the first system message
1176
1013
  // is received via handleClaudeMessage() callback
@@ -1235,38 +1072,8 @@ export class EdgeWorker extends EventEmitter {
1235
1072
  }
1236
1073
  }
1237
1074
  }
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`);
1269
- }
1075
+ // Note: Routing and streaming check happens later in handlePromptWithStreamingCheck
1076
+ // after attachments are processed
1270
1077
  // Ensure session is not null after creation/retrieval
1271
1078
  if (!session) {
1272
1079
  throw new Error(`Failed to get or create session for agent activity session ${linearAgentActivitySessionId}`);
@@ -1285,6 +1092,8 @@ export class EdgeWorker extends EventEmitter {
1285
1092
  // Ensure directory exists
1286
1093
  await mkdir(attachmentsDir, { recursive: true });
1287
1094
  let attachmentManifest = "";
1095
+ let commentAuthor;
1096
+ let commentTimestamp;
1288
1097
  try {
1289
1098
  const result = await linearClient.client.rawRequest(`
1290
1099
  query GetComment($id: String!) {
@@ -1295,16 +1104,27 @@ export class EdgeWorker extends EventEmitter {
1295
1104
  updatedAt
1296
1105
  user {
1297
1106
  name
1107
+ displayName
1108
+ email
1298
1109
  id
1299
1110
  }
1300
1111
  }
1301
1112
  }
1302
1113
  `, { id: commentId });
1114
+ // Extract comment data
1115
+ const comment = result.data.comment;
1116
+ // Extract comment metadata for multi-player context
1117
+ if (comment) {
1118
+ const user = comment.user;
1119
+ commentAuthor =
1120
+ user?.displayName || user?.name || user?.email || "Unknown";
1121
+ commentTimestamp = comment.createdAt || new Date().toISOString();
1122
+ }
1303
1123
  // Count existing attachments
1304
1124
  const existingFiles = await readdir(attachmentsDir).catch(() => []);
1305
1125
  const existingAttachmentCount = existingFiles.filter((file) => file.startsWith("attachment_") || file.startsWith("image_")).length;
1306
1126
  // Download new attachments from the comment
1307
- const downloadResult = await this.downloadCommentAttachments(result.data.comment.body, attachmentsDir, repository.linearToken, existingAttachmentCount);
1127
+ const downloadResult = await this.downloadCommentAttachments(comment.body, attachmentsDir, repository.linearToken, existingAttachmentCount);
1308
1128
  if (downloadResult.totalNewAttachments > 0) {
1309
1129
  attachmentManifest = this.generateNewAttachmentManifest(downloadResult);
1310
1130
  }
@@ -1318,6 +1138,7 @@ export class EdgeWorker extends EventEmitter {
1318
1138
  if (stopSignal) {
1319
1139
  console.log(`[EdgeWorker] Received stop signal for agent activity session ${linearAgentActivitySessionId}`);
1320
1140
  // Stop the existing runner if it's active
1141
+ const existingRunner = session.claudeRunner;
1321
1142
  if (existingRunner) {
1322
1143
  existingRunner.stop();
1323
1144
  console.log(`[EdgeWorker] Stopped Claude session for agent activity session ${linearAgentActivitySessionId}`);
@@ -1327,34 +1148,13 @@ export class EdgeWorker extends EventEmitter {
1327
1148
  await agentSessionManager.createResponseActivity(linearAgentActivitySessionId, stopConfirmation);
1328
1149
  return; // Exit early - stop signal handled
1329
1150
  }
1330
- // Check if there's an existing runner for this comment thread
1331
- if (existingRunner?.isStreaming()) {
1332
- // Add comment with attachment manifest to existing stream
1333
- console.log(`[EdgeWorker] Adding comment to existing stream for agent activity session ${linearAgentActivitySessionId}`);
1334
- // Append attachment manifest to the prompt if we have one
1335
- let fullPrompt = promptBody;
1336
- if (attachmentManifest) {
1337
- fullPrompt = `${promptBody}\n\n${attachmentManifest}`;
1338
- }
1339
- existingRunner.addStreamMessage(fullPrompt);
1340
- return; // Exit early - comment has been added to stream
1341
- }
1342
- // Use the new resumeClaudeSession function
1151
+ // Use centralized streaming check and routing logic
1343
1152
  try {
1344
- await this.resumeClaudeSession(session, repository, linearAgentActivitySessionId, agentSessionManager, promptBody, attachmentManifest, isNewSession, []);
1153
+ await this.handlePromptWithStreamingCheck(session, repository, linearAgentActivitySessionId, agentSessionManager, promptBody, attachmentManifest, isNewSession, [], // No additional allowed directories for regular continuation
1154
+ `prompted webhook (${isNewSession ? "new" : "existing"} session)`, commentAuthor, commentTimestamp);
1345
1155
  }
1346
1156
  catch (error) {
1347
- console.error("Failed to continue conversation:", error);
1348
- // Remove any partially created session
1349
- // this.sessionManager.removeSession(threadRootCommentId)
1350
- // this.commentToRepo.delete(threadRootCommentId)
1351
- // this.commentToIssue.delete(threadRootCommentId)
1352
- // // Start fresh for root comments, or fall back to issue assignment
1353
- // if (isRootComment) {
1354
- // await this.handleNewRootComment(issue, comment, repository)
1355
- // } else {
1356
- // await this.handleIssueAssigned(issue, repository)
1357
- // }
1157
+ console.error("Failed to handle prompted webhook:", error);
1358
1158
  }
1359
1159
  }
1360
1160
  /**
@@ -1585,10 +1385,12 @@ export class EdgeWorker extends EventEmitter {
1585
1385
  async buildMentionPrompt(issue, agentSession, attachmentManifest = "", guidance) {
1586
1386
  try {
1587
1387
  console.log(`[EdgeWorker] Building mention prompt for issue ${issue.identifier}`);
1588
- // Get the mention comment body
1388
+ // Get the mention comment metadata
1589
1389
  const mentionContent = agentSession.comment?.body || "";
1590
- // Build a simple prompt focused on the mention
1591
- let prompt = `You were mentioned in a Linear comment. Please help with the following request.
1390
+ const authorName = agentSession.creator?.name || agentSession.creator?.id || "Unknown";
1391
+ const timestamp = agentSession.createdAt || new Date().toISOString();
1392
+ // Build a focused prompt with comment metadata
1393
+ let prompt = `You were mentioned in a Linear comment on this issue:
1592
1394
 
1593
1395
  <linear_issue>
1594
1396
  <id>${issue.id}</id>
@@ -1597,11 +1399,15 @@ export class EdgeWorker extends EventEmitter {
1597
1399
  <url>${issue.url}</url>
1598
1400
  </linear_issue>
1599
1401
 
1600
- <mention_request>
1402
+ <mention_comment>
1403
+ <author>${authorName}</author>
1404
+ <timestamp>${timestamp}</timestamp>
1405
+ <content>
1601
1406
  ${mentionContent}
1602
- </mention_request>
1407
+ </content>
1408
+ </mention_comment>
1603
1409
 
1604
- IMPORTANT: You were specifically mentioned in the comment above. Focus on addressing the specific question or request in the mention. You can use the Linear MCP tools to fetch additional context about the issue if needed.`;
1410
+ Focus on addressing the specific request in the mention. You can use the Linear MCP tools to fetch additional context if needed.`;
1605
1411
  // Append agent guidance if present
1606
1412
  prompt += this.formatAgentGuidance(guidance);
1607
1413
  // Append attachment manifest if any
@@ -1817,17 +1623,17 @@ ${reply.body}
1817
1623
  * @param guidance Optional agent guidance rules from Linear
1818
1624
  * @returns Formatted prompt string
1819
1625
  */
1820
- async buildPromptV2(issue, repository, newComment, attachmentManifest = "", guidance) {
1821
- console.log(`[EdgeWorker] buildPromptV2 called for issue ${issue.identifier}${newComment ? " with new comment" : ""}`);
1626
+ async buildIssueContextPrompt(issue, repository, newComment, attachmentManifest = "", guidance) {
1627
+ console.log(`[EdgeWorker] buildIssueContextPrompt called for issue ${issue.identifier}${newComment ? " with new comment" : ""}`);
1822
1628
  try {
1823
1629
  // Use custom template if provided (repository-specific takes precedence)
1824
1630
  let templatePath = repository.promptTemplatePath ||
1825
1631
  this.config.features?.promptTemplatePath;
1826
- // If no custom template, use the v2 template
1632
+ // If no custom template, use the standard issue assigned user prompt template
1827
1633
  if (!templatePath) {
1828
1634
  const __filename = fileURLToPath(import.meta.url);
1829
1635
  const __dirname = dirname(__filename);
1830
- templatePath = resolve(__dirname, "../prompt-template-v2.md");
1636
+ templatePath = resolve(__dirname, "../prompts/standard-issue-assigned-user-prompt.md");
1831
1637
  }
1832
1638
  // Load the template
1833
1639
  console.log(`[EdgeWorker] Loading prompt template from: ${templatePath}`);
@@ -1913,8 +1719,8 @@ IMPORTANT: Focus specifically on addressing the new comment above. This is a new
1913
1719
  .replace(/{{new_comment_content}}/g, newComment.body || "");
1914
1720
  }
1915
1721
  else {
1916
- // Remove the new comment section entirely
1917
- prompt = prompt.replace(/{{#if new_comment}}[\s\S]*?{{\/if}}/g, "");
1722
+ // Remove the new comment section entirely (including preceding newlines)
1723
+ prompt = prompt.replace(/\n*{{#if new_comment}}[\s\S]*?{{\/if}}/g, "");
1918
1724
  }
1919
1725
  // Append agent guidance if present
1920
1726
  prompt += this.formatAgentGuidance(guidance);
@@ -1960,29 +1766,28 @@ ${newComment ? `New comment to address:\n${newComment.body}\n\n` : ""}Please ana
1960
1766
  */
1961
1767
  getConnectionStatus() {
1962
1768
  const status = new Map();
1963
- for (const [repoId, client] of this.ndjsonClients) {
1964
- status.set(repoId, client.isConnected());
1769
+ // Single event transport is "connected" if it exists
1770
+ if (this.linearEventTransport) {
1771
+ // Mark all repositories as connected since they share the single transport
1772
+ for (const repoId of this.repositories.keys()) {
1773
+ status.set(repoId, true);
1774
+ }
1965
1775
  }
1966
1776
  return status;
1967
1777
  }
1968
1778
  /**
1969
- * Get NDJSON client by token (for testing purposes)
1779
+ * Get event transport (for testing purposes)
1970
1780
  * @internal
1971
1781
  */
1972
- _getClientByToken(token) {
1973
- for (const [repoId, client] of this.ndjsonClients) {
1974
- const repo = this.repositories.get(repoId);
1975
- if (repo?.linearToken === token) {
1976
- return client;
1977
- }
1978
- }
1979
- return undefined;
1782
+ _getClientByToken(_token) {
1783
+ // Return the single shared event transport
1784
+ return this.linearEventTransport;
1980
1785
  }
1981
1786
  /**
1982
1787
  * Start OAuth flow using the shared application server
1983
1788
  */
1984
1789
  async startOAuthFlow(proxyUrl) {
1985
- const oauthProxyUrl = proxyUrl || this.config.proxyUrl;
1790
+ const oauthProxyUrl = proxyUrl || this.config.proxyUrl || DEFAULT_PROXY_URL;
1986
1791
  return this.sharedApplicationServer.startOAuthFlow(oauthProxyUrl);
1987
1792
  }
1988
1793
  /**
@@ -2481,6 +2286,8 @@ ${newComment ? `New comment to address:\n${newComment.body}\n\n` : ""}Please ana
2481
2286
  },
2482
2287
  onFeedbackDelivery: async (childSessionId, message) => {
2483
2288
  console.log(`[EdgeWorker] Processing feedback delivery to child session ${childSessionId}`);
2289
+ // Find the parent session ID for context
2290
+ const parentSessionId = this.childToParentAgentSession.get(childSessionId);
2484
2291
  // Find the repository containing the child session
2485
2292
  // We need to search all repositories for this child session
2486
2293
  let childRepo;
@@ -2503,22 +2310,62 @@ ${newComment ? `New comment to address:\n${newComment.body}\n\n` : ""}Please ana
2503
2310
  return false;
2504
2311
  }
2505
2312
  console.log(`[EdgeWorker] Found child session - Issue: ${childSession.issueId}`);
2313
+ // Get parent session info for better context in the thought
2314
+ let parentIssueId;
2315
+ if (parentSessionId) {
2316
+ // Find parent session across all repositories
2317
+ for (const manager of this.agentSessionManagers.values()) {
2318
+ const parentSession = manager.getSession(parentSessionId);
2319
+ if (parentSession) {
2320
+ parentIssueId =
2321
+ parentSession.issue?.identifier || parentSession.issueId;
2322
+ break;
2323
+ }
2324
+ }
2325
+ }
2326
+ // Post thought to Linear showing feedback receipt
2327
+ const linearClient = this.linearClients.get(childRepo.id);
2328
+ if (linearClient) {
2329
+ const feedbackThought = parentIssueId
2330
+ ? `Received feedback from orchestrator (${parentIssueId}):\n\n---\n\n${message}\n\n---`
2331
+ : `Received feedback from orchestrator:\n\n---\n\n${message}\n\n---`;
2332
+ try {
2333
+ const result = await linearClient.createAgentActivity({
2334
+ agentSessionId: childSessionId,
2335
+ content: {
2336
+ type: "thought",
2337
+ body: feedbackThought,
2338
+ },
2339
+ });
2340
+ if (result.success) {
2341
+ console.log(`[EdgeWorker] Posted feedback receipt thought for child session ${childSessionId}`);
2342
+ }
2343
+ else {
2344
+ console.error(`[EdgeWorker] Failed to post feedback receipt thought:`, result);
2345
+ }
2346
+ }
2347
+ catch (error) {
2348
+ console.error(`[EdgeWorker] Error posting feedback receipt thought:`, error);
2349
+ }
2350
+ }
2506
2351
  // Format the feedback as a prompt for the child session with enhanced markdown formatting
2507
2352
  const feedbackPrompt = `## Received feedback from orchestrator\n\n---\n\n${message}\n\n---`;
2508
- // Resume the CHILD session with the feedback from the parent
2353
+ // Use centralized streaming check and routing logic
2509
2354
  // Important: We don't await the full session completion to avoid timeouts.
2510
2355
  // The feedback is delivered immediately when the session starts, so we can
2511
2356
  // return success right away while the session continues in the background.
2512
- this.resumeClaudeSession(childSession, childRepo, childSessionId, childAgentSessionManager, feedbackPrompt, "", // No attachment manifest for feedback
2357
+ console.log(`[EdgeWorker] Handling feedback delivery to child session ${childSessionId}`);
2358
+ this.handlePromptWithStreamingCheck(childSession, childRepo, childSessionId, childAgentSessionManager, feedbackPrompt, "", // No attachment manifest for feedback
2513
2359
  false, // Not a new session
2514
- [])
2360
+ [], // No additional allowed directories for feedback
2361
+ "give feedback to child")
2515
2362
  .then(() => {
2516
2363
  console.log(`[EdgeWorker] Child session ${childSessionId} completed processing feedback`);
2517
2364
  })
2518
2365
  .catch((error) => {
2519
- console.error(`[EdgeWorker] Failed to complete child session with feedback:`, error);
2366
+ console.error(`[EdgeWorker] Failed to process feedback in child session:`, error);
2520
2367
  });
2521
- // Return success immediately after initiating the session
2368
+ // Return success immediately after initiating the handling
2522
2369
  console.log(`[EdgeWorker] Feedback delivered successfully to child session ${childSessionId}`);
2523
2370
  return true;
2524
2371
  },
@@ -2562,22 +2409,264 @@ ${newComment ? `New comment to address:\n${newComment.body}\n\n` : ""}Please ana
2562
2409
  }
2563
2410
  }
2564
2411
  /**
2565
- * Build prompt for a session - handles both new and existing sessions
2412
+ * Build the complete prompt for a session - shows full prompt assembly in one place
2413
+ *
2414
+ * New session prompt structure:
2415
+ * 1. Issue context (from buildIssueContextPrompt)
2416
+ * 2. Initial subroutine prompt (if procedure initialized)
2417
+ * 3. User comment
2418
+ *
2419
+ * Existing session prompt structure:
2420
+ * 1. User comment
2421
+ * 2. Attachment manifest (if present)
2566
2422
  */
2567
- async buildSessionPrompt(isNewSession, fullIssue, repository, promptBody, attachmentManifest) {
2568
- if (isNewSession) {
2569
- // For completely new sessions, create a complete initial prompt
2570
- const promptResult = await this.buildPromptV2(fullIssue, repository, undefined, attachmentManifest);
2571
- // Add the user's comment to the initial prompt
2572
- return `${promptResult.prompt}\n\nUser comment: ${promptBody}`;
2423
+ async buildSessionPrompt(isNewSession, session, fullIssue, repository, promptBody, attachmentManifest, commentAuthor, commentTimestamp) {
2424
+ // Fetch labels for system prompt determination
2425
+ const labels = await this.fetchIssueLabels(fullIssue);
2426
+ // Create input for unified prompt assembly
2427
+ const input = {
2428
+ session,
2429
+ fullIssue,
2430
+ repository,
2431
+ userComment: promptBody,
2432
+ commentAuthor,
2433
+ commentTimestamp,
2434
+ attachmentManifest,
2435
+ isNewSession,
2436
+ isStreaming: false, // This path is only for non-streaming prompts
2437
+ labels,
2438
+ };
2439
+ // Use unified prompt assembly
2440
+ const assembly = await this.assemblePrompt(input);
2441
+ // Log metadata for debugging
2442
+ console.log(`[EdgeWorker] Built prompt - components: ${assembly.metadata.components.join(", ")}, type: ${assembly.metadata.promptType}`);
2443
+ return assembly.userPrompt;
2444
+ }
2445
+ /**
2446
+ * Assemble a complete prompt - unified entry point for all prompt building
2447
+ * This method contains all prompt assembly logic in one place
2448
+ */
2449
+ async assemblePrompt(input) {
2450
+ // If actively streaming, just pass through the comment
2451
+ if (input.isStreaming) {
2452
+ return this.buildStreamingPrompt(input);
2453
+ }
2454
+ // If new session, build full prompt with all components
2455
+ if (input.isNewSession) {
2456
+ return this.buildNewSessionPrompt(input);
2457
+ }
2458
+ // Existing session continuation - just user comment + attachments
2459
+ return this.buildContinuationPrompt(input);
2460
+ }
2461
+ /**
2462
+ * Build prompt for actively streaming session - pass through user comment as-is
2463
+ */
2464
+ buildStreamingPrompt(input) {
2465
+ const components = ["user-comment"];
2466
+ if (input.attachmentManifest) {
2467
+ components.push("attachment-manifest");
2468
+ }
2469
+ const parts = [input.userComment];
2470
+ if (input.attachmentManifest) {
2471
+ parts.push(input.attachmentManifest);
2472
+ }
2473
+ return {
2474
+ systemPrompt: undefined,
2475
+ userPrompt: parts.join("\n\n"),
2476
+ metadata: {
2477
+ components,
2478
+ promptType: "continuation",
2479
+ isNewSession: false,
2480
+ isStreaming: true,
2481
+ },
2482
+ };
2483
+ }
2484
+ /**
2485
+ * Build prompt for new session - includes issue context, subroutine prompt, and user comment
2486
+ */
2487
+ async buildNewSessionPrompt(input) {
2488
+ const components = [];
2489
+ const parts = [];
2490
+ // 1. Determine system prompt from labels
2491
+ // Only for delegation (not mentions) or when /label-based-prompt is requested
2492
+ let labelBasedSystemPrompt;
2493
+ if (!input.isMentionTriggered || input.isLabelBasedPromptRequested) {
2494
+ labelBasedSystemPrompt = await this.determineSystemPromptForAssembly(input.labels || [], input.repository);
2495
+ }
2496
+ // 2. Determine system prompt based on prompt type
2497
+ // Label-based: Use only the label-based system prompt
2498
+ // Fallback: Use scenarios system prompt (shared instructions)
2499
+ let systemPrompt;
2500
+ if (labelBasedSystemPrompt) {
2501
+ // Use label-based system prompt as-is (no shared instructions)
2502
+ systemPrompt = labelBasedSystemPrompt;
2573
2503
  }
2574
2504
  else {
2575
- // For existing sessions, just use the comment with attachment manifest
2576
- const manifestSuffix = attachmentManifest
2577
- ? `\n\n${attachmentManifest}`
2578
- : "";
2579
- return `${promptBody}${manifestSuffix}`;
2505
+ // Use scenarios system prompt for fallback cases
2506
+ const sharedInstructions = await this.loadSharedInstructions();
2507
+ systemPrompt = sharedInstructions;
2508
+ }
2509
+ // 3. Build issue context using appropriate builder
2510
+ // Use label-based prompt ONLY if we have a label-based system prompt
2511
+ const promptType = this.determinePromptType(input, !!labelBasedSystemPrompt);
2512
+ const issueContext = await this.buildIssueContextForPromptAssembly(input.fullIssue, input.repository, promptType, input.attachmentManifest, input.guidance, input.agentSession);
2513
+ parts.push(issueContext.prompt);
2514
+ components.push("issue-context");
2515
+ // 4. Load and append initial subroutine prompt
2516
+ const currentSubroutine = this.procedureRouter.getCurrentSubroutine(input.session);
2517
+ let subroutineName;
2518
+ if (currentSubroutine) {
2519
+ const subroutinePrompt = await this.loadSubroutinePrompt(currentSubroutine);
2520
+ if (subroutinePrompt) {
2521
+ parts.push(subroutinePrompt);
2522
+ components.push("subroutine-prompt");
2523
+ subroutineName = currentSubroutine.name;
2524
+ }
2525
+ }
2526
+ // 5. Add user comment (if present)
2527
+ // Skip for mention-triggered prompts since the comment is already in the mention block
2528
+ if (input.userComment.trim() && !input.isMentionTriggered) {
2529
+ // If we have author/timestamp metadata, include it for multi-player context
2530
+ if (input.commentAuthor || input.commentTimestamp) {
2531
+ const author = input.commentAuthor || "Unknown";
2532
+ const timestamp = input.commentTimestamp || new Date().toISOString();
2533
+ parts.push(`<user_comment>
2534
+ <author>${author}</author>
2535
+ <timestamp>${timestamp}</timestamp>
2536
+ <content>
2537
+ ${input.userComment}
2538
+ </content>
2539
+ </user_comment>`);
2540
+ }
2541
+ else {
2542
+ // Legacy format without metadata
2543
+ parts.push(`<user_comment>\n${input.userComment}\n</user_comment>`);
2544
+ }
2545
+ components.push("user-comment");
2546
+ }
2547
+ // 6. Add guidance rules (if present)
2548
+ if (input.guidance && input.guidance.length > 0) {
2549
+ components.push("guidance-rules");
2550
+ }
2551
+ return {
2552
+ systemPrompt,
2553
+ userPrompt: parts.join("\n\n"),
2554
+ metadata: {
2555
+ components,
2556
+ subroutineName,
2557
+ promptType,
2558
+ isNewSession: true,
2559
+ isStreaming: false,
2560
+ },
2561
+ };
2562
+ }
2563
+ /**
2564
+ * Build prompt for existing session continuation - user comment and attachments only
2565
+ */
2566
+ buildContinuationPrompt(input) {
2567
+ const components = ["user-comment"];
2568
+ if (input.attachmentManifest) {
2569
+ components.push("attachment-manifest");
2570
+ }
2571
+ // Wrap comment in XML with author and timestamp for multi-player context
2572
+ const author = input.commentAuthor || "Unknown";
2573
+ const timestamp = input.commentTimestamp || new Date().toISOString();
2574
+ const commentXml = `<new_comment>
2575
+ <author>${author}</author>
2576
+ <timestamp>${timestamp}</timestamp>
2577
+ <content>
2578
+ ${input.userComment}
2579
+ </content>
2580
+ </new_comment>`;
2581
+ const parts = [commentXml];
2582
+ if (input.attachmentManifest) {
2583
+ parts.push(input.attachmentManifest);
2580
2584
  }
2585
+ return {
2586
+ systemPrompt: undefined,
2587
+ userPrompt: parts.join("\n\n"),
2588
+ metadata: {
2589
+ components,
2590
+ promptType: "continuation",
2591
+ isNewSession: false,
2592
+ isStreaming: false,
2593
+ },
2594
+ };
2595
+ }
2596
+ /**
2597
+ * Determine the prompt type based on input flags and system prompt availability
2598
+ */
2599
+ determinePromptType(input, hasSystemPrompt) {
2600
+ if (input.isMentionTriggered && input.isLabelBasedPromptRequested) {
2601
+ return "label-based-prompt-command";
2602
+ }
2603
+ if (input.isMentionTriggered) {
2604
+ return "mention";
2605
+ }
2606
+ if (hasSystemPrompt) {
2607
+ return "label-based";
2608
+ }
2609
+ return "fallback";
2610
+ }
2611
+ /**
2612
+ * Load a subroutine prompt file
2613
+ * Extracted helper to make prompt assembly more readable
2614
+ */
2615
+ async loadSubroutinePrompt(subroutine) {
2616
+ const __filename = fileURLToPath(import.meta.url);
2617
+ const __dirname = dirname(__filename);
2618
+ const subroutinePromptPath = join(__dirname, "prompts", subroutine.promptPath);
2619
+ try {
2620
+ const prompt = await readFile(subroutinePromptPath, "utf-8");
2621
+ console.log(`[EdgeWorker] Loaded ${subroutine.name} subroutine prompt (${prompt.length} characters)`);
2622
+ return prompt;
2623
+ }
2624
+ catch (error) {
2625
+ console.warn(`[EdgeWorker] Failed to load subroutine prompt from ${subroutinePromptPath}:`, error);
2626
+ return null;
2627
+ }
2628
+ }
2629
+ /**
2630
+ * Load shared instructions that get appended to all system prompts
2631
+ */
2632
+ async loadSharedInstructions() {
2633
+ const __filename = fileURLToPath(import.meta.url);
2634
+ const __dirname = dirname(__filename);
2635
+ const instructionsPath = join(__dirname, "..", "prompts", "todolist-system-prompt-extension.md");
2636
+ try {
2637
+ const instructions = await readFile(instructionsPath, "utf-8");
2638
+ return instructions;
2639
+ }
2640
+ catch (error) {
2641
+ console.error(`[EdgeWorker] Failed to load shared instructions from ${instructionsPath}:`, error);
2642
+ return ""; // Return empty string if file can't be loaded
2643
+ }
2644
+ }
2645
+ /**
2646
+ * Adapter method for prompt assembly - extracts just the prompt string
2647
+ */
2648
+ async determineSystemPromptForAssembly(labels, repository) {
2649
+ const result = await this.determineSystemPromptFromLabels(labels, repository);
2650
+ return result?.prompt;
2651
+ }
2652
+ /**
2653
+ * Adapter method for prompt assembly - routes to appropriate issue context builder
2654
+ */
2655
+ async buildIssueContextForPromptAssembly(issue, repository, promptType, attachmentManifest, guidance, agentSession) {
2656
+ // Delegate to appropriate builder based on promptType
2657
+ if (promptType === "mention") {
2658
+ if (!agentSession) {
2659
+ throw new Error("agentSession is required for mention-triggered prompts");
2660
+ }
2661
+ return this.buildMentionPrompt(issue, agentSession, attachmentManifest, guidance);
2662
+ }
2663
+ if (promptType === "label-based" ||
2664
+ promptType === "label-based-prompt-command") {
2665
+ return this.buildLabelBasedPrompt(issue, repository, attachmentManifest, guidance);
2666
+ }
2667
+ // Fallback to standard issue context
2668
+ return this.buildIssueContextPrompt(issue, repository, undefined, // No new comment for initial prompt assembly
2669
+ attachmentManifest, guidance);
2581
2670
  }
2582
2671
  /**
2583
2672
  * Build Claude runner configuration with common settings
@@ -2879,6 +2968,78 @@ ${newComment ? `New comment to address:\n${newComment.body}\n\n` : ""}Please ana
2879
2968
  console.error(`[EdgeWorker] Error posting parent resumption acknowledgment:`, error);
2880
2969
  }
2881
2970
  }
2971
+ /**
2972
+ * Re-route procedure for a session (used when resuming from child or give feedback)
2973
+ * This ensures the currentSubroutine is reset to avoid suppression issues
2974
+ */
2975
+ async rerouteProcedureForSession(session, linearAgentActivitySessionId, agentSessionManager, promptBody) {
2976
+ // Initialize procedure metadata using intelligent routing
2977
+ if (!session.metadata) {
2978
+ session.metadata = {};
2979
+ }
2980
+ // Post ephemeral "Routing..." thought
2981
+ await agentSessionManager.postRoutingThought(linearAgentActivitySessionId);
2982
+ // Route based on the prompt content
2983
+ const routingDecision = await this.procedureRouter.determineRoutine(promptBody.trim());
2984
+ const selectedProcedure = routingDecision.procedure;
2985
+ // Initialize procedure metadata in session (resets currentSubroutine)
2986
+ this.procedureRouter.initializeProcedureMetadata(session, selectedProcedure);
2987
+ // Post procedure selection result (replaces ephemeral routing thought)
2988
+ await agentSessionManager.postProcedureSelectionThought(linearAgentActivitySessionId, selectedProcedure.name, routingDecision.classification);
2989
+ // Log routing decision
2990
+ console.log(`[EdgeWorker] Routing decision for ${linearAgentActivitySessionId}:`);
2991
+ console.log(` Classification: ${routingDecision.classification}`);
2992
+ console.log(` Procedure: ${selectedProcedure.name}`);
2993
+ console.log(` Reasoning: ${routingDecision.reasoning}`);
2994
+ }
2995
+ /**
2996
+ * Handle prompt with streaming check - centralized logic for all input types
2997
+ *
2998
+ * This method implements the unified pattern for handling prompts:
2999
+ * 1. Check if runner is actively streaming
3000
+ * 2. Route procedure if NOT streaming (resets currentSubroutine)
3001
+ * 3. Add to stream if streaming, OR resume session if not
3002
+ *
3003
+ * @param session The Cyrus agent session
3004
+ * @param repository Repository configuration
3005
+ * @param linearAgentActivitySessionId Linear agent activity session ID
3006
+ * @param agentSessionManager Agent session manager instance
3007
+ * @param promptBody The prompt text to send
3008
+ * @param attachmentManifest Optional attachment manifest to append
3009
+ * @param isNewSession Whether this is a new session
3010
+ * @param additionalAllowedDirs Additional directories to allow access to
3011
+ * @param logContext Context string for logging (e.g., "prompted webhook", "parent resume")
3012
+ * @returns true if message was added to stream, false if session was resumed
3013
+ */
3014
+ async handlePromptWithStreamingCheck(session, repository, linearAgentActivitySessionId, agentSessionManager, promptBody, attachmentManifest, isNewSession, additionalAllowedDirs, logContext, commentAuthor, commentTimestamp) {
3015
+ // Check if runner is actively streaming before routing
3016
+ const existingRunner = session.claudeRunner;
3017
+ const isStreaming = existingRunner?.isStreaming() || false;
3018
+ // Always route procedure for new input, UNLESS actively streaming
3019
+ if (!isStreaming) {
3020
+ await this.rerouteProcedureForSession(session, linearAgentActivitySessionId, agentSessionManager, promptBody);
3021
+ console.log(`[EdgeWorker] Routed procedure for ${logContext}`);
3022
+ }
3023
+ else {
3024
+ console.log(`[EdgeWorker] Skipping routing for ${linearAgentActivitySessionId} (${logContext}) - runner is actively streaming`);
3025
+ }
3026
+ // Handle streaming case - add message to existing stream
3027
+ if (existingRunner?.isStreaming()) {
3028
+ console.log(`[EdgeWorker] Adding prompt to existing stream for ${linearAgentActivitySessionId} (${logContext})`);
3029
+ // Append attachment manifest to the prompt if we have one
3030
+ let fullPrompt = promptBody;
3031
+ if (attachmentManifest) {
3032
+ fullPrompt = `${promptBody}\n\n${attachmentManifest}`;
3033
+ }
3034
+ existingRunner.addStreamMessage(fullPrompt);
3035
+ return true; // Message added to stream
3036
+ }
3037
+ // Not streaming - resume/start session
3038
+ console.log(`[EdgeWorker] Resuming Claude session for ${linearAgentActivitySessionId} (${logContext})`);
3039
+ await this.resumeClaudeSession(session, repository, linearAgentActivitySessionId, agentSessionManager, promptBody, attachmentManifest, isNewSession, additionalAllowedDirs, undefined, // maxTurns
3040
+ commentAuthor, commentTimestamp);
3041
+ return false; // Session was resumed
3042
+ }
2882
3043
  /**
2883
3044
  * Post thought about system prompt selection based on labels
2884
3045
  */
@@ -2975,7 +3136,7 @@ ${newComment ? `New comment to address:\n${newComment.body}\n\n` : ""}Please ana
2975
3136
  * @param attachmentManifest Optional attachment manifest
2976
3137
  * @param isNewSession Whether this is a new session
2977
3138
  */
2978
- async resumeClaudeSession(session, repository, linearAgentActivitySessionId, agentSessionManager, promptBody, attachmentManifest = "", isNewSession = false, additionalAllowedDirectories = [], maxTurns) {
3139
+ async resumeClaudeSession(session, repository, linearAgentActivitySessionId, agentSessionManager, promptBody, attachmentManifest = "", isNewSession = false, additionalAllowedDirectories = [], maxTurns, commentAuthor, commentTimestamp) {
2979
3140
  // Check for existing runner
2980
3141
  const existingRunner = session.claudeRunner;
2981
3142
  // If there's an existing streaming runner, add to it
@@ -3027,7 +3188,7 @@ ${newComment ? `New comment to address:\n${newComment.body}\n\n` : ""}Please ana
3027
3188
  // Save state
3028
3189
  await this.savePersistedState();
3029
3190
  // Prepare the full prompt
3030
- const fullPrompt = await this.buildSessionPrompt(isNewSession, fullIssue, repository, promptBody, attachmentManifest);
3191
+ const fullPrompt = await this.buildSessionPrompt(isNewSession, session, fullIssue, repository, promptBody, attachmentManifest, commentAuthor, commentTimestamp);
3031
3192
  // Start streaming session
3032
3193
  try {
3033
3194
  await runner.startStreaming(fullPrompt);