cyrus-edge-worker 0.2.21 → 0.2.22

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 (95) hide show
  1. package/dist/ActivityPoster.d.ts +15 -0
  2. package/dist/ActivityPoster.d.ts.map +1 -0
  3. package/dist/ActivityPoster.js +194 -0
  4. package/dist/ActivityPoster.js.map +1 -0
  5. package/dist/AgentSessionManager.d.ts +63 -21
  6. package/dist/AgentSessionManager.d.ts.map +1 -1
  7. package/dist/AgentSessionManager.js +413 -404
  8. package/dist/AgentSessionManager.js.map +1 -1
  9. package/dist/AskUserQuestionHandler.d.ts +3 -2
  10. package/dist/AskUserQuestionHandler.d.ts.map +1 -1
  11. package/dist/AskUserQuestionHandler.js +15 -12
  12. package/dist/AskUserQuestionHandler.js.map +1 -1
  13. package/dist/AttachmentService.d.ts +69 -0
  14. package/dist/AttachmentService.d.ts.map +1 -0
  15. package/dist/AttachmentService.js +369 -0
  16. package/dist/AttachmentService.js.map +1 -0
  17. package/dist/ChatSessionHandler.d.ts +87 -0
  18. package/dist/ChatSessionHandler.d.ts.map +1 -0
  19. package/dist/ChatSessionHandler.js +227 -0
  20. package/dist/ChatSessionHandler.js.map +1 -0
  21. package/dist/ConfigManager.d.ts +91 -0
  22. package/dist/ConfigManager.d.ts.map +1 -0
  23. package/dist/ConfigManager.js +227 -0
  24. package/dist/ConfigManager.js.map +1 -0
  25. package/dist/EdgeWorker.d.ts +152 -120
  26. package/dist/EdgeWorker.d.ts.map +1 -1
  27. package/dist/EdgeWorker.js +1333 -2125
  28. package/dist/EdgeWorker.js.map +1 -1
  29. package/dist/GitService.d.ts +14 -10
  30. package/dist/GitService.d.ts.map +1 -1
  31. package/dist/GitService.js +91 -12
  32. package/dist/GitService.js.map +1 -1
  33. package/dist/GlobalSessionRegistry.d.ts +142 -0
  34. package/dist/GlobalSessionRegistry.d.ts.map +1 -0
  35. package/dist/GlobalSessionRegistry.js +254 -0
  36. package/dist/GlobalSessionRegistry.js.map +1 -0
  37. package/dist/PromptBuilder.d.ts +175 -0
  38. package/dist/PromptBuilder.d.ts.map +1 -0
  39. package/dist/PromptBuilder.js +884 -0
  40. package/dist/PromptBuilder.js.map +1 -0
  41. package/dist/RepositoryRouter.d.ts +3 -2
  42. package/dist/RepositoryRouter.d.ts.map +1 -1
  43. package/dist/RepositoryRouter.js +39 -37
  44. package/dist/RepositoryRouter.js.map +1 -1
  45. package/dist/RunnerSelectionService.d.ts +71 -0
  46. package/dist/RunnerSelectionService.d.ts.map +1 -0
  47. package/dist/RunnerSelectionService.js +392 -0
  48. package/dist/RunnerSelectionService.js.map +1 -0
  49. package/dist/SharedApplicationServer.d.ts +3 -1
  50. package/dist/SharedApplicationServer.d.ts.map +1 -1
  51. package/dist/SharedApplicationServer.js +21 -17
  52. package/dist/SharedApplicationServer.js.map +1 -1
  53. package/dist/SharedWebhookServer.d.ts +3 -1
  54. package/dist/SharedWebhookServer.d.ts.map +1 -1
  55. package/dist/SharedWebhookServer.js +19 -16
  56. package/dist/SharedWebhookServer.js.map +1 -1
  57. package/dist/SlackChatAdapter.d.ts +25 -0
  58. package/dist/SlackChatAdapter.d.ts.map +1 -0
  59. package/dist/SlackChatAdapter.js +143 -0
  60. package/dist/SlackChatAdapter.js.map +1 -0
  61. package/dist/WorktreeIncludeService.d.ts +2 -9
  62. package/dist/WorktreeIncludeService.d.ts.map +1 -1
  63. package/dist/WorktreeIncludeService.js +3 -9
  64. package/dist/WorktreeIncludeService.js.map +1 -1
  65. package/dist/index.d.ts +7 -2
  66. package/dist/index.d.ts.map +1 -1
  67. package/dist/index.js +4 -0
  68. package/dist/index.js.map +1 -1
  69. package/dist/procedures/ProcedureAnalyzer.d.ts +6 -4
  70. package/dist/procedures/ProcedureAnalyzer.d.ts.map +1 -1
  71. package/dist/procedures/ProcedureAnalyzer.js +32 -12
  72. package/dist/procedures/ProcedureAnalyzer.js.map +1 -1
  73. package/dist/procedures/types.d.ts +1 -0
  74. package/dist/procedures/types.d.ts.map +1 -1
  75. package/dist/prompts/graphite-orchestrator.md +4 -2
  76. package/dist/prompts/orchestrator.md +5 -3
  77. package/dist/sinks/IActivitySink.d.ts +60 -0
  78. package/dist/sinks/IActivitySink.d.ts.map +1 -0
  79. package/dist/sinks/IActivitySink.js +2 -0
  80. package/dist/sinks/IActivitySink.js.map +1 -0
  81. package/dist/sinks/LinearActivitySink.d.ts +69 -0
  82. package/dist/sinks/LinearActivitySink.d.ts.map +1 -0
  83. package/dist/sinks/LinearActivitySink.js +111 -0
  84. package/dist/sinks/LinearActivitySink.js.map +1 -0
  85. package/dist/sinks/NoopActivitySink.d.ts +13 -0
  86. package/dist/sinks/NoopActivitySink.d.ts.map +1 -0
  87. package/dist/sinks/NoopActivitySink.js +17 -0
  88. package/dist/sinks/NoopActivitySink.js.map +1 -0
  89. package/dist/sinks/index.d.ts +9 -0
  90. package/dist/sinks/index.d.ts.map +1 -0
  91. package/dist/sinks/index.js +8 -0
  92. package/dist/sinks/index.js.map +1 -0
  93. package/package.json +16 -10
  94. package/prompts/graphite-orchestrator.md +4 -2
  95. package/prompts/orchestrator.md +5 -3
@@ -1,5 +1,5 @@
1
1
  import { EventEmitter } from "node:events";
2
- import { AgentActivitySignal, AgentSessionStatus, AgentSessionType, } from "cyrus-core";
2
+ import { AgentSessionStatus, AgentSessionType, createLogger, } from "cyrus-core";
3
3
  import { DEFAULT_VALIDATION_LOOP_CONFIG, parseValidationResult, renderValidationFixerPrompt, } from "./validation/index.js";
4
4
  /**
5
5
  * Manages Agent Sessions integration with Claude Code SDK
@@ -9,65 +9,135 @@ import { DEFAULT_VALIDATION_LOOP_CONFIG, parseValidationResult, renderValidation
9
9
  * CURRENTLY BEING HANDLED 'per repository'
10
10
  */
11
11
  export class AgentSessionManager extends EventEmitter {
12
- issueTracker;
12
+ logger;
13
+ activitySink;
13
14
  sessions = new Map();
14
- entries = new Map(); // Stores a list of session entries per each session by its linearAgentActivitySessionId
15
+ entries = new Map(); // Stores a list of session entries per each session by its id
15
16
  activeTasksBySession = new Map(); // Maps session ID to active Task tool use ID
16
17
  toolCallsByToolUseId = new Map(); // Track tool calls by their tool_use_id
17
18
  taskSubjectsByToolUseId = new Map(); // Cache TaskCreate subjects by toolUseId until result arrives with task ID
18
19
  taskSubjectsById = new Map(); // Cache task subjects by task ID (e.g., "1" → "Fix login bug")
19
20
  activeStatusActivitiesBySession = new Map(); // Maps session ID to active compacting status activity ID
21
+ stopRequestedSessions = new Set(); // Sessions explicitly stopped by user signal
20
22
  procedureAnalyzer;
21
23
  sharedApplicationServer;
22
24
  getParentSessionId;
23
25
  resumeParentSession;
24
- constructor(issueTracker, getParentSessionId, resumeParentSession, procedureAnalyzer, sharedApplicationServer) {
26
+ constructor(activitySink, getParentSessionId, resumeParentSession, procedureAnalyzer, sharedApplicationServer, logger) {
25
27
  super();
26
- this.issueTracker = issueTracker;
28
+ this.logger = logger ?? createLogger({ component: "AgentSessionManager" });
29
+ this.activitySink = activitySink;
27
30
  this.getParentSessionId = getParentSessionId;
28
31
  this.resumeParentSession = resumeParentSession;
29
32
  this.procedureAnalyzer = procedureAnalyzer;
30
33
  this.sharedApplicationServer = sharedApplicationServer;
31
34
  }
32
35
  /**
33
- * Initialize a Linear agent session from webhook
34
- * The session is already created by Linear, we just need to track it
36
+ * Get a session-scoped logger with context (sessionId, platform, issueIdentifier).
35
37
  */
36
- createLinearAgentSession(linearAgentActivitySessionId, issueId, issueMinimal, workspace) {
37
- console.log(`[AgentSessionManager] Tracking Linear session ${linearAgentActivitySessionId} for issue ${issueId}`);
38
+ sessionLog(sessionId) {
39
+ const session = this.sessions.get(sessionId);
40
+ return this.logger.withContext({
41
+ sessionId,
42
+ platform: session?.issueContext?.trackerId,
43
+ issueIdentifier: session?.issueContext?.issueIdentifier,
44
+ });
45
+ }
46
+ /**
47
+ * Initialize an agent session from webhook
48
+ * The session is already created by the platform, we just need to track it
49
+ *
50
+ * @param sessionId - Internal session ID
51
+ * @param issueId - Issue/PR identifier
52
+ * @param issueMinimal - Minimal issue data
53
+ * @param workspace - Workspace configuration
54
+ * @param platform - Source platform ("linear", "github", "slack"). Defaults to "linear".
55
+ * Only "linear" sessions will have activities streamed to Linear.
56
+ */
57
+ createLinearAgentSession(sessionId, issueId, issueMinimal, workspace, platform = "linear") {
58
+ const log = this.logger.withContext({
59
+ sessionId,
60
+ platform,
61
+ issueIdentifier: issueMinimal.identifier,
62
+ });
63
+ log.info(`Tracking session for issue ${issueId}`);
38
64
  const agentSession = {
39
- linearAgentActivitySessionId,
65
+ id: sessionId,
66
+ // Only Linear sessions have a valid external session ID for posting activities
67
+ externalSessionId: platform === "linear" ? sessionId : undefined,
40
68
  type: AgentSessionType.CommentThread,
41
69
  status: AgentSessionStatus.Active,
42
70
  context: AgentSessionType.CommentThread,
43
71
  createdAt: Date.now(),
44
72
  updatedAt: Date.now(),
45
- issueId,
73
+ issueContext: {
74
+ trackerId: platform,
75
+ issueId: issueId,
76
+ issueIdentifier: issueMinimal.identifier,
77
+ },
78
+ issueId, // Kept for backwards compatibility
46
79
  issue: issueMinimal,
47
80
  workspace: workspace,
48
81
  };
49
82
  // Store locally
50
- this.sessions.set(linearAgentActivitySessionId, agentSession);
51
- this.entries.set(linearAgentActivitySessionId, []);
83
+ this.sessions.set(sessionId, agentSession);
84
+ this.entries.set(sessionId, []);
85
+ return agentSession;
86
+ }
87
+ /**
88
+ * Create an agent session for chat-style platforms (Slack, etc.) that are
89
+ * not tied to a specific issue or repository.
90
+ *
91
+ * Unlike {@link createLinearAgentSession}, this does NOT require issue
92
+ * context — the session lives in a standalone workspace with no issue
93
+ * tracker linkage.
94
+ */
95
+ createChatSession(sessionId, workspace, platform) {
96
+ const log = this.logger.withContext({ sessionId, platform });
97
+ log.info("Creating chat session");
98
+ const agentSession = {
99
+ id: sessionId,
100
+ type: AgentSessionType.CommentThread,
101
+ status: AgentSessionStatus.Active,
102
+ context: AgentSessionType.CommentThread,
103
+ createdAt: Date.now(),
104
+ updatedAt: Date.now(),
105
+ workspace,
106
+ };
107
+ this.sessions.set(sessionId, agentSession);
108
+ this.entries.set(sessionId, []);
52
109
  return agentSession;
53
110
  }
54
111
  /**
55
112
  * Update Agent Session with session ID from system initialization
56
113
  * Automatically detects whether it's Claude or Gemini based on the runner
57
114
  */
58
- updateAgentSessionWithClaudeSessionId(linearAgentActivitySessionId, claudeSystemMessage) {
59
- const linearSession = this.sessions.get(linearAgentActivitySessionId);
115
+ updateAgentSessionWithClaudeSessionId(sessionId, claudeSystemMessage) {
116
+ const linearSession = this.sessions.get(sessionId);
60
117
  if (!linearSession) {
61
- console.warn(`[AgentSessionManager] No Linear session found for linearAgentActivitySessionId ${linearAgentActivitySessionId}`);
118
+ const log = this.sessionLog(sessionId);
119
+ log.warn(`No session found`);
62
120
  return;
63
121
  }
64
122
  // Determine which runner is being used
65
123
  const runner = linearSession.agentRunner;
66
- const isGeminiRunner = runner?.constructor.name === "GeminiRunner";
124
+ const runnerType = runner?.constructor.name === "GeminiRunner"
125
+ ? "gemini"
126
+ : runner?.constructor.name === "CodexRunner"
127
+ ? "codex"
128
+ : runner?.constructor.name === "CursorRunner"
129
+ ? "cursor"
130
+ : "claude";
67
131
  // Update the appropriate session ID based on runner type
68
- if (isGeminiRunner) {
132
+ if (runnerType === "gemini") {
69
133
  linearSession.geminiSessionId = claudeSystemMessage.session_id;
70
134
  }
135
+ else if (runnerType === "codex") {
136
+ linearSession.codexSessionId = claudeSystemMessage.session_id;
137
+ }
138
+ else if (runnerType === "cursor") {
139
+ linearSession.cursorSessionId = claudeSystemMessage.session_id;
140
+ }
71
141
  else {
72
142
  linearSession.claudeSessionId = claudeSystemMessage.session_id;
73
143
  }
@@ -83,7 +153,7 @@ export class AgentSessionManager extends EventEmitter {
83
153
  /**
84
154
  * Create a session entry from user/assistant message (without syncing to Linear)
85
155
  */
86
- async createSessionEntry(linearAgentActivitySessionId, sdkMessage) {
156
+ async createSessionEntry(sessionId, sdkMessage) {
87
157
  // Extract tool info if this is an assistant message
88
158
  const toolInfo = sdkMessage.type === "assistant" ? this.extractToolInfo(sdkMessage) : null;
89
159
  // Extract tool_use_id and error status if this is a user message with tool_result
@@ -97,14 +167,24 @@ export class AgentSessionManager extends EventEmitter {
97
167
  // "error":"rate_limit" field when usage limits are hit
98
168
  const sdkError = sdkMessage.type === "assistant" ? sdkMessage.error : undefined;
99
169
  // Determine which runner is being used
100
- const session = this.sessions.get(linearAgentActivitySessionId);
170
+ const session = this.sessions.get(sessionId);
101
171
  const runner = session?.agentRunner;
102
- const isGeminiRunner = runner?.constructor.name === "GeminiRunner";
172
+ const runnerType = runner?.constructor.name === "GeminiRunner"
173
+ ? "gemini"
174
+ : runner?.constructor.name === "CodexRunner"
175
+ ? "codex"
176
+ : runner?.constructor.name === "CursorRunner"
177
+ ? "cursor"
178
+ : "claude";
103
179
  const sessionEntry = {
104
180
  // Set the appropriate session ID based on runner type
105
- ...(isGeminiRunner
181
+ ...(runnerType === "gemini"
106
182
  ? { geminiSessionId: sdkMessage.session_id }
107
- : { claudeSessionId: sdkMessage.session_id }),
183
+ : runnerType === "codex"
184
+ ? { codexSessionId: sdkMessage.session_id }
185
+ : runnerType === "cursor"
186
+ ? { cursorSessionId: sdkMessage.session_id }
187
+ : { claudeSessionId: sdkMessage.session_id }),
108
188
  type: sdkMessage.type,
109
189
  content: this.extractContent(sdkMessage),
110
190
  metadata: {
@@ -128,35 +208,49 @@ export class AgentSessionManager extends EventEmitter {
128
208
  /**
129
209
  * Complete a session from Claude result message
130
210
  */
131
- async completeSession(linearAgentActivitySessionId, resultMessage) {
132
- const session = this.sessions.get(linearAgentActivitySessionId);
211
+ async completeSession(sessionId, resultMessage) {
212
+ const session = this.sessions.get(sessionId);
133
213
  if (!session) {
134
- console.error(`[AgentSessionManager] No session found for linearAgentActivitySessionId: ${linearAgentActivitySessionId}`);
214
+ const log = this.sessionLog(sessionId);
215
+ log.error(`No session found`);
135
216
  return;
136
217
  }
218
+ const log = this.sessionLog(sessionId);
137
219
  // Clear any active Task when session completes
138
- this.activeTasksBySession.delete(linearAgentActivitySessionId);
220
+ this.activeTasksBySession.delete(sessionId);
139
221
  // Clear tool calls tracking for this session
140
222
  // Note: We should ideally track by session, but for now clearing all is safer
141
223
  // to prevent memory leaks
142
- const status = resultMessage.subtype === "success"
143
- ? AgentSessionStatus.Complete
144
- : AgentSessionStatus.Error;
224
+ const wasStopRequested = this.consumeStopRequest(sessionId);
225
+ const status = wasStopRequested
226
+ ? AgentSessionStatus.Error
227
+ : resultMessage.subtype === "success"
228
+ ? AgentSessionStatus.Complete
229
+ : AgentSessionStatus.Error;
145
230
  // Update session status and metadata
146
- await this.updateSessionStatus(linearAgentActivitySessionId, status, {
231
+ await this.updateSessionStatus(sessionId, status, {
147
232
  totalCostUsd: resultMessage.total_cost_usd,
148
233
  usage: resultMessage.usage,
149
234
  });
150
- // Handle result using procedure routing system
235
+ // Handle result using procedure routing system (skip for sessions without procedures, e.g. Slack)
236
+ if (!this.procedureAnalyzer) {
237
+ log.info(`Session completed (no procedure routing)`);
238
+ return;
239
+ }
240
+ if (wasStopRequested) {
241
+ log.info(`Session ${sessionId} was stopped by user; skipping procedure continuation`);
242
+ return;
243
+ }
151
244
  if ("result" in resultMessage && resultMessage.result) {
152
- await this.handleProcedureCompletion(session, linearAgentActivitySessionId, resultMessage);
245
+ await this.handleProcedureCompletion(session, sessionId, resultMessage);
153
246
  }
154
- else if (resultMessage.subtype !== "success") {
247
+ else if (resultMessage.subtype !== "success" &&
248
+ this.shouldRecoverFromPreviousSubroutine(resultMessage)) {
155
249
  // Error result (e.g. error_max_turns from singleTurn subroutines) — try to
156
250
  // recover from the last completed subroutine's result so the procedure can still complete.
157
251
  const recoveredText = this.procedureAnalyzer?.getLastSubroutineResult(session);
158
252
  if (recoveredText) {
159
- console.log(`[AgentSessionManager] Recovered result from previous subroutine (subtype: ${resultMessage.subtype}), treating as success for procedure completion`);
253
+ log.info(`Recovered result from previous subroutine (subtype: ${resultMessage.subtype}), treating as success for procedure completion`);
160
254
  // Create a synthetic success result for procedure routing
161
255
  const syntheticResult = {
162
256
  ...resultMessage,
@@ -164,30 +258,67 @@ export class AgentSessionManager extends EventEmitter {
164
258
  result: recoveredText,
165
259
  is_error: false,
166
260
  };
167
- await this.handleProcedureCompletion(session, linearAgentActivitySessionId, syntheticResult);
261
+ await this.handleProcedureCompletion(session, sessionId, syntheticResult);
168
262
  }
169
263
  else {
170
- console.warn(`[AgentSessionManager] Error result with no recoverable text (subtype: ${resultMessage.subtype}), posting error to Linear`);
171
- await this.addResultEntry(linearAgentActivitySessionId, resultMessage);
264
+ log.warn(`Error result with no recoverable text (subtype: ${resultMessage.subtype}), posting error to Linear`);
265
+ await this.addResultEntry(sessionId, resultMessage);
172
266
  }
173
267
  }
268
+ else if (resultMessage.subtype !== "success") {
269
+ // Non-recoverable errors (e.g. stop/abort) should not advance procedures.
270
+ await this.addResultEntry(sessionId, resultMessage);
271
+ }
272
+ }
273
+ shouldRecoverFromPreviousSubroutine(resultMessage) {
274
+ if (resultMessage.subtype === "error_max_turns") {
275
+ return true;
276
+ }
277
+ const errorText = [
278
+ resultMessage.subtype,
279
+ ...("errors" in resultMessage && Array.isArray(resultMessage.errors)
280
+ ? resultMessage.errors
281
+ : []),
282
+ "result" in resultMessage && typeof resultMessage.result === "string"
283
+ ? resultMessage.result
284
+ : "",
285
+ ]
286
+ .join(" ")
287
+ .toLowerCase();
288
+ return (errorText.includes("max turn") ||
289
+ errorText.includes("turn limit") ||
290
+ errorText.includes("turns limit"));
291
+ }
292
+ consumeStopRequest(linearAgentActivitySessionId) {
293
+ if (!this.stopRequestedSessions.has(linearAgentActivitySessionId)) {
294
+ return false;
295
+ }
296
+ this.stopRequestedSessions.delete(linearAgentActivitySessionId);
297
+ return true;
298
+ }
299
+ requestSessionStop(linearAgentActivitySessionId) {
300
+ this.stopRequestedSessions.add(linearAgentActivitySessionId);
174
301
  }
175
302
  /**
176
303
  * Handle completion using procedure routing system
177
304
  */
178
- async handleProcedureCompletion(session, linearAgentActivitySessionId, resultMessage) {
305
+ async handleProcedureCompletion(session, sessionId, resultMessage) {
306
+ const log = this.sessionLog(sessionId);
179
307
  if (!this.procedureAnalyzer) {
180
308
  throw new Error("ProcedureAnalyzer not available");
181
309
  }
182
310
  // Check if error occurred
183
311
  if (resultMessage.subtype !== "success") {
184
- console.log(`[AgentSessionManager] Subroutine completed with error, not triggering next subroutine`);
312
+ log.info(`Subroutine completed with error, not triggering next subroutine`);
185
313
  return;
186
314
  }
187
- // Get the session ID (either Claude or Gemini)
188
- const sessionId = session.claudeSessionId || session.geminiSessionId;
189
- if (!sessionId) {
190
- console.error(`[AgentSessionManager] No session ID found for procedure session`);
315
+ // Get the runner session ID (Claude, Gemini, Codex, or Cursor)
316
+ const runnerSessionId = session.claudeSessionId ||
317
+ session.geminiSessionId ||
318
+ session.codexSessionId ||
319
+ session.cursorSessionId;
320
+ if (!runnerSessionId) {
321
+ log.error(`No runner session ID found for procedure session`);
191
322
  return;
192
323
  }
193
324
  // Check if there's a next subroutine
@@ -196,11 +327,11 @@ export class AgentSessionManager extends EventEmitter {
196
327
  // More subroutines to run - check if current subroutine requires approval
197
328
  const currentSubroutine = this.procedureAnalyzer.getCurrentSubroutine(session);
198
329
  if (currentSubroutine?.requiresApproval) {
199
- console.log(`[AgentSessionManager] Current subroutine "${currentSubroutine.name}" requires approval before proceeding`);
330
+ log.info(`Current subroutine "${currentSubroutine.name}" requires approval before proceeding`);
200
331
  // Check if SharedApplicationServer is available
201
332
  if (!this.sharedApplicationServer) {
202
- console.error(`[AgentSessionManager] SharedApplicationServer not available for approval workflow`);
203
- await this.createErrorActivity(linearAgentActivitySessionId, "Approval workflow failed: Server not available");
333
+ log.error(`SharedApplicationServer not available for approval workflow`);
334
+ await this.createErrorActivity(sessionId, "Approval workflow failed: Server not available");
204
335
  return;
205
336
  }
206
337
  // Extract the final result from the completed subroutine
@@ -209,11 +340,11 @@ export class AgentSessionManager extends EventEmitter {
209
340
  : "No result available";
210
341
  try {
211
342
  // Register approval request with server
212
- const approvalRequest = this.sharedApplicationServer.registerApprovalRequest(linearAgentActivitySessionId);
343
+ const approvalRequest = this.sharedApplicationServer.registerApprovalRequest(sessionId);
213
344
  // Post approval elicitation to Linear with auth signal URL
214
345
  const approvalMessage = `The previous step has completed. Please review the result below and approve to continue:\n\n${subroutineResult}`;
215
- await this.createApprovalElicitation(linearAgentActivitySessionId, approvalMessage, approvalRequest.url);
216
- console.log(`[AgentSessionManager] Waiting for approval at URL: ${approvalRequest.url}`);
346
+ await this.createApprovalElicitation(sessionId, approvalMessage, approvalRequest.url);
347
+ log.info(`Waiting for approval at URL: ${approvalRequest.url}`);
217
348
  // Wait for approval with timeout (30 minutes)
218
349
  const approvalTimeout = 30 * 60 * 1000;
219
350
  const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error("Approval timeout")), approvalTimeout));
@@ -222,57 +353,57 @@ export class AgentSessionManager extends EventEmitter {
222
353
  timeoutPromise,
223
354
  ]);
224
355
  if (!approved) {
225
- console.log(`[AgentSessionManager] Approval rejected for session ${linearAgentActivitySessionId}`);
226
- await this.createErrorActivity(linearAgentActivitySessionId, `Workflow stopped: User rejected approval.${feedback ? `\n\nFeedback: ${feedback}` : ""}`);
356
+ log.info(`Approval rejected`);
357
+ await this.createErrorActivity(sessionId, `Workflow stopped: User rejected approval.${feedback ? `\n\nFeedback: ${feedback}` : ""}`);
227
358
  return; // Stop workflow
228
359
  }
229
- console.log(`[AgentSessionManager] Approval granted, continuing to next subroutine`);
360
+ log.info(`Approval granted, continuing to next subroutine`);
230
361
  // Optionally post feedback as a thought
231
362
  if (feedback) {
232
- await this.createThoughtActivity(linearAgentActivitySessionId, `User feedback: ${feedback}`);
363
+ await this.createThoughtActivity(sessionId, `User feedback: ${feedback}`);
233
364
  }
234
365
  // Continue with advancement (fall through to existing code)
235
366
  }
236
367
  catch (error) {
237
368
  const errorMessage = error.message;
238
369
  if (errorMessage === "Approval timeout") {
239
- console.log(`[AgentSessionManager] Approval timed out for session ${linearAgentActivitySessionId}`);
240
- await this.createErrorActivity(linearAgentActivitySessionId, "Workflow stopped: Approval request timed out after 30 minutes.");
370
+ log.info(`Approval timed out`);
371
+ await this.createErrorActivity(sessionId, "Workflow stopped: Approval request timed out after 30 minutes.");
241
372
  }
242
373
  else {
243
- console.error(`[AgentSessionManager] Approval request failed:`, error);
244
- await this.createErrorActivity(linearAgentActivitySessionId, `Workflow stopped: Approval request failed - ${errorMessage}`);
374
+ log.error(`Approval request failed:`, error);
375
+ await this.createErrorActivity(sessionId, `Workflow stopped: Approval request failed - ${errorMessage}`);
245
376
  }
246
377
  return; // Stop workflow
247
378
  }
248
379
  }
249
380
  // Check if current subroutine uses validation loop
250
381
  if (currentSubroutine?.usesValidationLoop) {
251
- const handled = await this.handleValidationLoopCompletion(session, linearAgentActivitySessionId, resultMessage, sessionId, nextSubroutine);
382
+ const handled = await this.handleValidationLoopCompletion(session, sessionId, resultMessage, runnerSessionId, nextSubroutine);
252
383
  if (handled) {
253
384
  return; // Validation loop took over control flow
254
385
  }
255
386
  // If not handled (validation passed or max retries), continue with normal advancement
256
387
  }
257
388
  // Advance procedure state
258
- console.log(`[AgentSessionManager] Subroutine completed, advancing to next: ${nextSubroutine.name}`);
389
+ log.info(`Subroutine completed, advancing to next: ${nextSubroutine.name}`);
259
390
  const subroutineResult = "result" in resultMessage ? resultMessage.result : undefined;
260
- this.procedureAnalyzer.advanceToNextSubroutine(session, sessionId, subroutineResult);
391
+ this.procedureAnalyzer.advanceToNextSubroutine(session, runnerSessionId, subroutineResult);
261
392
  // Emit event for EdgeWorker to handle subroutine transition
262
393
  // This replaces the callback pattern and allows EdgeWorker to subscribe
263
394
  this.emit("subroutineComplete", {
264
- linearAgentActivitySessionId,
395
+ sessionId,
265
396
  session,
266
397
  });
267
398
  }
268
399
  else {
269
400
  // Procedure complete - post final result
270
- console.log(`[AgentSessionManager] All subroutines completed, posting final result to Linear`);
271
- await this.addResultEntry(linearAgentActivitySessionId, resultMessage);
401
+ log.info(`All subroutines completed, posting final result to Linear`);
402
+ await this.addResultEntry(sessionId, resultMessage);
272
403
  // Handle child session completion
273
- const isChildSession = this.getParentSessionId?.(linearAgentActivitySessionId);
404
+ const isChildSession = this.getParentSessionId?.(sessionId);
274
405
  if (isChildSession && this.resumeParentSession) {
275
- await this.handleChildSessionCompletion(linearAgentActivitySessionId, resultMessage);
406
+ await this.handleChildSessionCompletion(sessionId, resultMessage);
276
407
  }
277
408
  }
278
409
  }
@@ -281,7 +412,8 @@ export class AgentSessionManager extends EventEmitter {
281
412
  * Returns true if the validation loop took over control flow (needs fixer or retry)
282
413
  * Returns false if validation passed or max retries reached (continue with normal advancement)
283
414
  */
284
- async handleValidationLoopCompletion(session, linearAgentActivitySessionId, resultMessage, _sessionId, _nextSubroutine) {
415
+ async handleValidationLoopCompletion(session, sessionId, resultMessage, _runnerSessionId, _nextSubroutine) {
416
+ const log = this.sessionLog(sessionId);
285
417
  const maxIterations = DEFAULT_VALIDATION_LOOP_CONFIG.maxIterations;
286
418
  // Get or initialize validation loop state
287
419
  let validationLoop = session.metadata?.procedure?.validationLoop;
@@ -295,13 +427,13 @@ export class AgentSessionManager extends EventEmitter {
295
427
  // Check if we're coming back from the fixer
296
428
  if (validationLoop.inFixerMode) {
297
429
  // Fixer completed, now we need to re-run verifications
298
- console.log(`[AgentSessionManager] Validation fixer completed for iteration ${validationLoop.iteration}, re-running verifications`);
430
+ log.info(`Validation fixer completed for iteration ${validationLoop.iteration}, re-running verifications`);
299
431
  // Clear fixer mode flag
300
432
  validationLoop.inFixerMode = false;
301
433
  this.updateValidationLoopState(session, validationLoop);
302
434
  // Emit event to re-run verifications
303
435
  this.emit("validationLoopRerun", {
304
- linearAgentActivitySessionId,
436
+ sessionId,
305
437
  session,
306
438
  iteration: validationLoop.iteration,
307
439
  });
@@ -322,27 +454,27 @@ export class AgentSessionManager extends EventEmitter {
322
454
  reason: validationResult.reason,
323
455
  timestamp: Date.now(),
324
456
  });
325
- console.log(`[AgentSessionManager] Validation result for iteration ${newIteration}/${maxIterations}: pass=${validationResult.pass}, reason="${validationResult.reason.substring(0, 100)}..."`);
457
+ log.info(`Validation result for iteration ${newIteration}/${maxIterations}: pass=${validationResult.pass}, reason="${validationResult.reason.substring(0, 100)}..."`);
326
458
  // Update state in session
327
459
  this.updateValidationLoopState(session, validationLoop);
328
460
  // Check if validation passed
329
461
  if (validationResult.pass) {
330
- console.log(`[AgentSessionManager] Validation passed after ${newIteration} iteration(s)`);
462
+ log.info(`Validation passed after ${newIteration} iteration(s)`);
331
463
  // Clear validation loop state for next subroutine
332
464
  this.clearValidationLoopState(session);
333
465
  return false; // Continue with normal advancement
334
466
  }
335
467
  // Check if we've exceeded max retries
336
468
  if (newIteration >= maxIterations) {
337
- console.log(`[AgentSessionManager] Validation failed after ${newIteration} iterations, continuing anyway`);
469
+ log.info(`Validation failed after ${newIteration} iterations, continuing anyway`);
338
470
  // Post a thought about the failures
339
- await this.createThoughtActivity(linearAgentActivitySessionId, `Validation loop exhausted after ${newIteration} attempts. Last failure: ${validationResult.reason}`);
471
+ await this.createThoughtActivity(sessionId, `Validation loop exhausted after ${newIteration} attempts. Last failure: ${validationResult.reason}`);
340
472
  // Clear validation loop state for next subroutine
341
473
  this.clearValidationLoopState(session);
342
474
  return false; // Continue with normal advancement
343
475
  }
344
476
  // Validation failed and we have retries left - run the fixer
345
- console.log(`[AgentSessionManager] Validation failed, running fixer (iteration ${newIteration}/${maxIterations})`);
477
+ log.info(`Validation failed, running fixer (iteration ${newIteration}/${maxIterations})`);
346
478
  // Set fixer mode flag
347
479
  validationLoop.inFixerMode = true;
348
480
  this.updateValidationLoopState(session, validationLoop);
@@ -359,7 +491,7 @@ export class AgentSessionManager extends EventEmitter {
359
491
  });
360
492
  // Emit event for EdgeWorker to run the fixer
361
493
  this.emit("validationLoopIteration", {
362
- linearAgentActivitySessionId,
494
+ sessionId,
363
495
  session,
364
496
  fixerPrompt,
365
497
  iteration: newIteration,
@@ -390,76 +522,78 @@ export class AgentSessionManager extends EventEmitter {
390
522
  /**
391
523
  * Handle child session completion and resume parent
392
524
  */
393
- async handleChildSessionCompletion(linearAgentActivitySessionId, resultMessage) {
525
+ async handleChildSessionCompletion(sessionId, resultMessage) {
526
+ const log = this.sessionLog(sessionId);
394
527
  if (!this.getParentSessionId || !this.resumeParentSession) {
395
528
  return;
396
529
  }
397
- const parentAgentSessionId = this.getParentSessionId(linearAgentActivitySessionId);
530
+ const parentAgentSessionId = this.getParentSessionId(sessionId);
398
531
  if (!parentAgentSessionId) {
399
- console.error(`[AgentSessionManager] No parent session ID found for child ${linearAgentActivitySessionId}`);
532
+ log.error(`No parent session ID found for child session`);
400
533
  return;
401
534
  }
402
- console.log(`[AgentSessionManager] Child session ${linearAgentActivitySessionId} completed, resuming parent ${parentAgentSessionId}`);
535
+ log.info(`Child session completed, resuming parent ${parentAgentSessionId}`);
403
536
  try {
404
537
  const childResult = "result" in resultMessage
405
538
  ? resultMessage.result
406
539
  : "No result available";
407
- const promptToParent = `Child agent session ${linearAgentActivitySessionId} completed with result:\n\n${childResult}`;
408
- await this.resumeParentSession(parentAgentSessionId, promptToParent, linearAgentActivitySessionId);
409
- console.log(`[AgentSessionManager] Successfully resumed parent session ${parentAgentSessionId}`);
540
+ const promptToParent = `Child agent session ${sessionId} completed with result:\n\n${childResult}`;
541
+ await this.resumeParentSession(parentAgentSessionId, promptToParent, sessionId);
542
+ log.info(`Successfully resumed parent session ${parentAgentSessionId}`);
410
543
  }
411
544
  catch (error) {
412
- console.error(`[AgentSessionManager] Failed to resume parent session:`, error);
545
+ log.error(`Failed to resume parent session:`, error);
413
546
  }
414
547
  }
415
548
  /**
416
549
  * Handle streaming Claude messages and route to appropriate methods
417
550
  */
418
- async handleClaudeMessage(linearAgentActivitySessionId, message) {
551
+ async handleClaudeMessage(sessionId, message) {
552
+ const log = this.sessionLog(sessionId);
419
553
  try {
420
554
  switch (message.type) {
421
555
  case "system":
422
556
  if (message.subtype === "init") {
423
- this.updateAgentSessionWithClaudeSessionId(linearAgentActivitySessionId, message);
557
+ this.updateAgentSessionWithClaudeSessionId(sessionId, message);
424
558
  // Post model notification
425
559
  const systemMessage = message;
426
560
  if (systemMessage.model) {
427
- await this.postModelNotificationThought(linearAgentActivitySessionId, systemMessage.model);
561
+ await this.postModelNotificationThought(sessionId, systemMessage.model);
428
562
  }
429
563
  }
430
564
  else if (message.subtype === "status") {
431
565
  // Handle status updates (compacting, etc.)
432
- await this.handleStatusMessage(linearAgentActivitySessionId, message);
566
+ await this.handleStatusMessage(sessionId, message);
433
567
  }
434
568
  break;
435
569
  case "user": {
436
- const userEntry = await this.createSessionEntry(linearAgentActivitySessionId, message);
437
- await this.syncEntryToLinear(userEntry, linearAgentActivitySessionId);
570
+ const userEntry = await this.createSessionEntry(sessionId, message);
571
+ await this.syncEntryToActivitySink(userEntry, sessionId);
438
572
  break;
439
573
  }
440
574
  case "assistant": {
441
- const assistantEntry = await this.createSessionEntry(linearAgentActivitySessionId, message);
442
- await this.syncEntryToLinear(assistantEntry, linearAgentActivitySessionId);
575
+ const assistantEntry = await this.createSessionEntry(sessionId, message);
576
+ await this.syncEntryToActivitySink(assistantEntry, sessionId);
443
577
  break;
444
578
  }
445
579
  case "result":
446
- await this.completeSession(linearAgentActivitySessionId, message);
580
+ await this.completeSession(sessionId, message);
447
581
  break;
448
582
  default:
449
- console.warn(`[AgentSessionManager] Unknown message type: ${message.type}`);
583
+ log.warn(`Unknown message type: ${message.type}`);
450
584
  }
451
585
  }
452
586
  catch (error) {
453
- console.error(`[AgentSessionManager] Error handling message:`, error);
587
+ log.error(`Error handling message:`, error);
454
588
  // Mark session as error state
455
- await this.updateSessionStatus(linearAgentActivitySessionId, AgentSessionStatus.Error);
589
+ await this.updateSessionStatus(sessionId, AgentSessionStatus.Error);
456
590
  }
457
591
  }
458
592
  /**
459
593
  * Update session status and metadata
460
594
  */
461
- async updateSessionStatus(linearAgentActivitySessionId, status, additionalMetadata) {
462
- const session = this.sessions.get(linearAgentActivitySessionId);
595
+ async updateSessionStatus(sessionId, status, additionalMetadata) {
596
+ const session = this.sessions.get(sessionId);
463
597
  if (!session)
464
598
  return;
465
599
  session.status = status;
@@ -467,32 +601,51 @@ export class AgentSessionManager extends EventEmitter {
467
601
  if (additionalMetadata) {
468
602
  session.metadata = { ...session.metadata, ...additionalMetadata };
469
603
  }
470
- this.sessions.set(linearAgentActivitySessionId, session);
604
+ this.sessions.set(sessionId, session);
471
605
  }
472
606
  /**
473
607
  * Add result entry from result message
474
608
  */
475
- async addResultEntry(linearAgentActivitySessionId, resultMessage) {
609
+ async addResultEntry(sessionId, resultMessage) {
476
610
  // Determine which runner is being used
477
- const session = this.sessions.get(linearAgentActivitySessionId);
611
+ const session = this.sessions.get(sessionId);
478
612
  const runner = session?.agentRunner;
479
- const isGeminiRunner = runner?.constructor.name === "GeminiRunner";
613
+ const runnerType = runner?.constructor.name === "GeminiRunner"
614
+ ? "gemini"
615
+ : runner?.constructor.name === "CodexRunner"
616
+ ? "codex"
617
+ : runner?.constructor.name === "CursorRunner"
618
+ ? "cursor"
619
+ : "claude";
620
+ // For error results, content may be in errors[] rather than result
621
+ const content = "result" in resultMessage && typeof resultMessage.result === "string"
622
+ ? resultMessage.result
623
+ : resultMessage.is_error &&
624
+ "errors" in resultMessage &&
625
+ Array.isArray(resultMessage.errors) &&
626
+ resultMessage.errors.length > 0
627
+ ? resultMessage.errors.join("\n")
628
+ : "";
480
629
  const resultEntry = {
481
630
  // Set the appropriate session ID based on runner type
482
- ...(isGeminiRunner
631
+ ...(runnerType === "gemini"
483
632
  ? { geminiSessionId: resultMessage.session_id }
484
- : { claudeSessionId: resultMessage.session_id }),
633
+ : runnerType === "codex"
634
+ ? { codexSessionId: resultMessage.session_id }
635
+ : runnerType === "cursor"
636
+ ? { cursorSessionId: resultMessage.session_id }
637
+ : { claudeSessionId: resultMessage.session_id }),
485
638
  type: "result",
486
- content: "result" in resultMessage ? resultMessage.result : "",
639
+ content,
487
640
  metadata: {
488
641
  timestamp: Date.now(),
489
642
  durationMs: resultMessage.duration_ms,
490
643
  isError: resultMessage.is_error,
491
644
  },
492
645
  };
493
- // DON'T store locally - syncEntryToLinear will do it
646
+ // DON'T store locally - syncEntryToActivitySink will do it
494
647
  // Sync to Linear
495
- await this.syncEntryToLinear(resultEntry, linearAgentActivitySessionId);
648
+ await this.syncEntryToActivitySink(resultEntry, sessionId);
496
649
  }
497
650
  /**
498
651
  * Extract content from Claude message
@@ -586,31 +739,32 @@ export class AgentSessionManager extends EventEmitter {
586
739
  };
587
740
  }
588
741
  /**
589
- * Sync Agent Session Entry to Linear (create AgentActivity)
742
+ * Sync session entry to external tracker (create AgentActivity)
590
743
  */
591
- async syncEntryToLinear(entry, linearAgentActivitySessionId) {
744
+ async syncEntryToActivitySink(entry, sessionId) {
745
+ const log = this.sessionLog(sessionId);
592
746
  try {
593
- const session = this.sessions.get(linearAgentActivitySessionId);
747
+ const session = this.sessions.get(sessionId);
594
748
  if (!session) {
595
- console.warn(`[AgentSessionManager] No Linear session for linearAgentActivitySessionId ${linearAgentActivitySessionId}`);
749
+ log.warn(`No session found`);
596
750
  return;
597
751
  }
598
752
  // Store entry locally first
599
- const entries = this.entries.get(linearAgentActivitySessionId) || [];
753
+ const entries = this.entries.get(sessionId) || [];
600
754
  entries.push(entry);
601
- this.entries.set(linearAgentActivitySessionId, entries);
755
+ this.entries.set(sessionId, entries);
602
756
  // Build activity content based on entry type
603
757
  let content;
604
758
  let ephemeral = false;
605
759
  switch (entry.type) {
606
760
  case "user": {
607
- const activeTaskId = this.activeTasksBySession.get(linearAgentActivitySessionId);
761
+ const activeTaskId = this.activeTasksBySession.get(sessionId);
608
762
  if (activeTaskId && activeTaskId === entry.metadata?.toolUseId) {
609
763
  content = {
610
764
  type: "thought",
611
765
  body: `✅ Task Completed\n\n\n\n${entry.content}\n\n---\n\n`,
612
766
  };
613
- this.activeTasksBySession.delete(linearAgentActivitySessionId);
767
+ this.activeTasksBySession.delete(sessionId);
614
768
  }
615
769
  else if (entry.metadata?.toolUseId) {
616
770
  // This is a tool result - create an action activity with the result
@@ -641,7 +795,7 @@ export class AgentSessionManager extends EventEmitter {
641
795
  if (baseToolName === "TaskUpdate" || baseToolName === "TaskGet") {
642
796
  const formatter = session.agentRunner?.getFormatter();
643
797
  if (!formatter) {
644
- console.warn(`[AgentSessionManager] No formatter available for session ${linearAgentActivitySessionId}`);
798
+ log.warn(`No formatter available for session ${sessionId}`);
645
799
  return;
646
800
  }
647
801
  // Try to enrich toolInput with subject from cache or result
@@ -706,7 +860,7 @@ export class AgentSessionManager extends EventEmitter {
706
860
  // Get formatter from runner
707
861
  const formatter = session.agentRunner?.getFormatter();
708
862
  if (!formatter) {
709
- console.warn(`[AgentSessionManager] No formatter available for session ${linearAgentActivitySessionId}`);
863
+ log.warn(`No formatter available`);
710
864
  return;
711
865
  }
712
866
  // Format parameter and result using runner's formatter
@@ -739,7 +893,7 @@ export class AgentSessionManager extends EventEmitter {
739
893
  // Check if this is a subtask with arrow prefix
740
894
  let storedName = toolName;
741
895
  if (entry.metadata?.parentToolUseId) {
742
- const activeTaskId = this.activeTasksBySession.get(linearAgentActivitySessionId);
896
+ const activeTaskId = this.activeTasksBySession.get(sessionId);
743
897
  if (activeTaskId === entry.metadata?.parentToolUseId) {
744
898
  storedName = `↪ ${toolName}`;
745
899
  }
@@ -758,7 +912,7 @@ export class AgentSessionManager extends EventEmitter {
758
912
  // Get formatter from runner
759
913
  const formatter = session.agentRunner?.getFormatter();
760
914
  if (!formatter) {
761
- console.warn(`[AgentSessionManager] No formatter available for session ${linearAgentActivitySessionId}`);
915
+ log.warn(`No formatter available`);
762
916
  return;
763
917
  }
764
918
  const formattedTodos = formatter.formatTodoWriteParameter(entry.content);
@@ -773,7 +927,7 @@ export class AgentSessionManager extends EventEmitter {
773
927
  // Get formatter from runner
774
928
  const formatter = session.agentRunner?.getFormatter();
775
929
  if (!formatter) {
776
- console.warn(`[AgentSessionManager] No formatter available for session ${linearAgentActivitySessionId}`);
930
+ log.warn(`No formatter available for session ${sessionId}`);
777
931
  return;
778
932
  }
779
933
  // Special handling for Task tools - format as thought instead of action
@@ -801,7 +955,7 @@ export class AgentSessionManager extends EventEmitter {
801
955
  // Get formatter from runner
802
956
  const formatter = session.agentRunner?.getFormatter();
803
957
  if (!formatter) {
804
- console.warn(`[AgentSessionManager] No formatter available for session ${linearAgentActivitySessionId}`);
958
+ log.warn(`No formatter available for session ${sessionId}`);
805
959
  return;
806
960
  }
807
961
  // Special handling for ToolSearch - format as thought instead of action
@@ -818,7 +972,7 @@ export class AgentSessionManager extends EventEmitter {
818
972
  // Get formatter from runner
819
973
  const formatter = session.agentRunner?.getFormatter();
820
974
  if (!formatter) {
821
- console.warn(`[AgentSessionManager] No formatter available for session ${linearAgentActivitySessionId}`);
975
+ log.warn(`No formatter available`);
822
976
  return;
823
977
  }
824
978
  // Special handling for Task tool - add start marker and track active task
@@ -827,7 +981,7 @@ export class AgentSessionManager extends EventEmitter {
827
981
  const displayName = toolName;
828
982
  // Track this as the active Task for this session
829
983
  if (entry.metadata?.toolUseId) {
830
- this.activeTasksBySession.set(linearAgentActivitySessionId, entry.metadata.toolUseId);
984
+ this.activeTasksBySession.set(sessionId, entry.metadata.toolUseId);
831
985
  }
832
986
  content = {
833
987
  type: "action",
@@ -842,14 +996,14 @@ export class AgentSessionManager extends EventEmitter {
842
996
  // Get formatter from runner
843
997
  const formatter = session.agentRunner?.getFormatter();
844
998
  if (!formatter) {
845
- console.warn(`[AgentSessionManager] No formatter available for session ${linearAgentActivitySessionId}`);
999
+ log.warn(`No formatter available`);
846
1000
  return;
847
1001
  }
848
1002
  // Other tools - check if they're within an active Task
849
1003
  const toolInput = entry.metadata.toolInput || entry.content;
850
1004
  let displayName = toolName;
851
1005
  if (entry.metadata?.parentToolUseId) {
852
- const activeTaskId = this.activeTasksBySession.get(linearAgentActivitySessionId);
1006
+ const activeTaskId = this.activeTasksBySession.get(sessionId);
853
1007
  if (activeTaskId === entry.metadata?.parentToolUseId) {
854
1008
  displayName = `↪ ${toolName}`;
855
1009
  }
@@ -918,40 +1072,45 @@ export class AgentSessionManager extends EventEmitter {
918
1072
  if (currentSubroutine?.suppressThoughtPosting) {
919
1073
  // Only suppress thoughts and actions, not responses or results
920
1074
  if (content.type === "thought" || content.type === "action") {
921
- console.log(`[AgentSessionManager] Suppressing ${content.type} posting for subroutine "${currentSubroutine.name}"`);
922
- return; // Don't post to Linear
1075
+ log.debug(`Suppressing ${content.type} posting for subroutine "${currentSubroutine.name}"`);
1076
+ return; // Don't post to tracker
923
1077
  }
924
1078
  }
925
- const activityInput = {
926
- agentSessionId: session.linearAgentActivitySessionId, // Use the Linear session ID
927
- content,
928
- ...(ephemeral && { ephemeral: true }),
929
- };
930
- const result = await this.issueTracker.createAgentActivity(activityInput);
931
- if (result.success && result.agentActivity) {
932
- const agentActivity = await result.agentActivity;
933
- entry.linearAgentActivityId = agentActivity.id;
934
- console.log(`[AgentSessionManager] Created ${content.type} activity ${entry.linearAgentActivityId}`);
1079
+ // Ensure we have an external session ID for activity posting
1080
+ if (!session.externalSessionId) {
1081
+ log.debug(`Skipping activity sync - no external session ID (platform: ${session.issueContext?.trackerId || "unknown"})`);
1082
+ return;
935
1083
  }
936
- else {
937
- console.error(`[AgentSessionManager] Failed to create Linear activity:`, result);
1084
+ const options = {};
1085
+ if (ephemeral) {
1086
+ options.ephemeral = true;
1087
+ }
1088
+ const result = await this.activitySink.postActivity(session.externalSessionId, content, options);
1089
+ if (result.activityId) {
1090
+ entry.linearAgentActivityId = result.activityId;
1091
+ if (entry.type === "result") {
1092
+ log.info(`Result message emitted to Linear (activity ${entry.linearAgentActivityId})`);
1093
+ }
1094
+ else {
1095
+ log.debug(`Created ${content.type} activity ${entry.linearAgentActivityId}`);
1096
+ }
938
1097
  }
939
1098
  }
940
1099
  catch (error) {
941
- console.error(`[AgentSessionManager] Failed to sync entry to Linear:`, error);
1100
+ log.error(`Failed to sync entry to activity sink:`, error);
942
1101
  }
943
1102
  }
944
1103
  /**
945
1104
  * Get session by ID
946
1105
  */
947
- getSession(linearAgentActivitySessionId) {
948
- return this.sessions.get(linearAgentActivitySessionId);
1106
+ getSession(sessionId) {
1107
+ return this.sessions.get(sessionId);
949
1108
  }
950
1109
  /**
951
1110
  * Get session entries by session ID
952
1111
  */
953
- getSessionEntries(linearAgentActivitySessionId) {
954
- return this.entries.get(linearAgentActivitySessionId) || [];
1112
+ getSessionEntries(sessionId) {
1113
+ return this.entries.get(sessionId) || [];
955
1114
  }
956
1115
  /**
957
1116
  * Get all active sessions
@@ -962,15 +1121,16 @@ export class AgentSessionManager extends EventEmitter {
962
1121
  /**
963
1122
  * Add or update agent runner for a session
964
1123
  */
965
- addAgentRunner(linearAgentActivitySessionId, agentRunner) {
966
- const session = this.sessions.get(linearAgentActivitySessionId);
1124
+ addAgentRunner(sessionId, agentRunner) {
1125
+ const log = this.sessionLog(sessionId);
1126
+ const session = this.sessions.get(sessionId);
967
1127
  if (!session) {
968
- console.warn(`[AgentSessionManager] No session found for linearAgentActivitySessionId ${linearAgentActivitySessionId}`);
1128
+ log.warn(`No session found`);
969
1129
  return;
970
1130
  }
971
1131
  session.agentRunner = agentRunner;
972
1132
  session.updatedAt = Date.now();
973
- console.log(`[AgentSessionManager] Added agent runner to session ${linearAgentActivitySessionId}`);
1133
+ log.debug(`Added agent runner`);
974
1134
  }
975
1135
  /**
976
1136
  * Get all agent runners
@@ -980,12 +1140,18 @@ export class AgentSessionManager extends EventEmitter {
980
1140
  .map((session) => session.agentRunner)
981
1141
  .filter((runner) => runner !== undefined);
982
1142
  }
1143
+ /**
1144
+ * Resolve the issue ID from a session, checking issueContext first then deprecated issueId.
1145
+ */
1146
+ getSessionIssueId(session) {
1147
+ return session.issueContext?.issueId ?? session.issueId;
1148
+ }
983
1149
  /**
984
1150
  * Get all agent runners for a specific issue
985
1151
  */
986
1152
  getAgentRunnersForIssue(issueId) {
987
1153
  return Array.from(this.sessions.values())
988
- .filter((session) => session.issueId === issueId)
1154
+ .filter((session) => this.getSessionIssueId(session) === issueId)
989
1155
  .map((session) => session.agentRunner)
990
1156
  .filter((runner) => runner !== undefined);
991
1157
  }
@@ -993,15 +1159,23 @@ export class AgentSessionManager extends EventEmitter {
993
1159
  * Get sessions by issue ID
994
1160
  */
995
1161
  getSessionsByIssueId(issueId) {
996
- return Array.from(this.sessions.values()).filter((session) => session.issueId === issueId);
1162
+ return Array.from(this.sessions.values()).filter((session) => this.getSessionIssueId(session) === issueId);
997
1163
  }
998
1164
  /**
999
1165
  * Get active sessions by issue ID
1000
1166
  */
1001
1167
  getActiveSessionsByIssueId(issueId) {
1002
- return Array.from(this.sessions.values()).filter((session) => session.issueId === issueId &&
1168
+ return Array.from(this.sessions.values()).filter((session) => this.getSessionIssueId(session) === issueId &&
1003
1169
  session.status === AgentSessionStatus.Active);
1004
1170
  }
1171
+ /**
1172
+ * Get active sessions where the issue's branch name matches the given branch.
1173
+ * Useful for detecting when multiple sessions share the same worktree.
1174
+ */
1175
+ getActiveSessionsByBranchName(branchName) {
1176
+ return Array.from(this.sessions.values()).filter((session) => session.status === AgentSessionStatus.Active &&
1177
+ session.issue?.branchName === branchName);
1178
+ }
1005
1179
  /**
1006
1180
  * Get all sessions
1007
1181
  */
@@ -1011,193 +1185,97 @@ export class AgentSessionManager extends EventEmitter {
1011
1185
  /**
1012
1186
  * Get agent runner for a specific session
1013
1187
  */
1014
- getAgentRunner(linearAgentActivitySessionId) {
1015
- const session = this.sessions.get(linearAgentActivitySessionId);
1188
+ getAgentRunner(sessionId) {
1189
+ const session = this.sessions.get(sessionId);
1016
1190
  return session?.agentRunner;
1017
1191
  }
1018
1192
  /**
1019
1193
  * Check if an agent runner exists for a session
1020
1194
  */
1021
- hasAgentRunner(linearAgentActivitySessionId) {
1022
- const session = this.sessions.get(linearAgentActivitySessionId);
1195
+ hasAgentRunner(sessionId) {
1196
+ const session = this.sessions.get(sessionId);
1023
1197
  return session?.agentRunner !== undefined;
1024
1198
  }
1025
1199
  /**
1026
- * Create a thought activity
1200
+ * Post an activity to the activity sink for a session.
1201
+ * Consolidates session lookup, externalSessionId guard, try/catch, and logging.
1202
+ *
1203
+ * @returns The activity ID when resolved, `null` otherwise.
1027
1204
  */
1028
- async createThoughtActivity(sessionId, body) {
1205
+ async postActivity(sessionId, input, label) {
1206
+ const log = this.sessionLog(sessionId);
1029
1207
  const session = this.sessions.get(sessionId);
1030
- if (!session || !session.linearAgentActivitySessionId) {
1031
- console.warn(`[AgentSessionManager] No Linear session ID for session ${sessionId}`);
1032
- return;
1208
+ if (!session || !session.externalSessionId) {
1209
+ log.debug(`Skipping ${label} - no external session ID (platform: ${session?.issueContext?.trackerId || "unknown"})`);
1210
+ return null;
1033
1211
  }
1034
1212
  try {
1035
- const result = await this.issueTracker.createAgentActivity({
1036
- agentSessionId: session.linearAgentActivitySessionId,
1037
- content: {
1038
- type: "thought",
1039
- body,
1040
- },
1041
- });
1042
- if (result.success) {
1043
- console.log(`[AgentSessionManager] Created thought activity for session ${sessionId}`);
1213
+ const options = {};
1214
+ if (input.ephemeral !== undefined) {
1215
+ options.ephemeral = input.ephemeral;
1044
1216
  }
1045
- else {
1046
- console.error(`[AgentSessionManager] Failed to create thought activity:`, result);
1217
+ if (input.signal) {
1218
+ options.signal = input.signal;
1219
+ }
1220
+ if (input.signalMetadata) {
1221
+ options.signalMetadata = input.signalMetadata;
1222
+ }
1223
+ const result = await this.activitySink.postActivity(session.externalSessionId, input.content, options);
1224
+ if (result.activityId) {
1225
+ log.debug(`Created ${label} activity ${result.activityId}`);
1226
+ return result.activityId;
1047
1227
  }
1228
+ log.debug(`Created ${label}`);
1229
+ return null;
1048
1230
  }
1049
1231
  catch (error) {
1050
- console.error(`[AgentSessionManager] Error creating thought activity:`, error);
1232
+ log.error(`Error creating ${label}:`, error);
1233
+ return null;
1051
1234
  }
1052
1235
  }
1236
+ /**
1237
+ * Create a thought activity
1238
+ */
1239
+ async createThoughtActivity(sessionId, body) {
1240
+ await this.postActivity(sessionId, { content: { type: "thought", body } }, "thought");
1241
+ }
1053
1242
  /**
1054
1243
  * Create an action activity
1055
1244
  */
1056
1245
  async createActionActivity(sessionId, action, parameter, result) {
1057
- const session = this.sessions.get(sessionId);
1058
- if (!session || !session.linearAgentActivitySessionId) {
1059
- console.warn(`[AgentSessionManager] No Linear session ID for session ${sessionId}`);
1060
- return;
1061
- }
1062
- try {
1063
- const content = {
1064
- type: "action",
1065
- action,
1066
- parameter,
1067
- };
1068
- if (result !== undefined) {
1069
- content.result = result;
1070
- }
1071
- const response = await this.issueTracker.createAgentActivity({
1072
- agentSessionId: session.linearAgentActivitySessionId,
1073
- content,
1074
- });
1075
- if (response.success) {
1076
- console.log(`[AgentSessionManager] Created action activity for session ${sessionId}`);
1077
- }
1078
- else {
1079
- console.error(`[AgentSessionManager] Failed to create action activity:`, response);
1080
- }
1081
- }
1082
- catch (error) {
1083
- console.error(`[AgentSessionManager] Error creating action activity:`, error);
1246
+ const content = { type: "action", action, parameter };
1247
+ if (result !== undefined) {
1248
+ content.result = result;
1084
1249
  }
1250
+ await this.postActivity(sessionId, { content }, "action");
1085
1251
  }
1086
1252
  /**
1087
1253
  * Create a response activity
1088
1254
  */
1089
1255
  async createResponseActivity(sessionId, body) {
1090
- const session = this.sessions.get(sessionId);
1091
- if (!session || !session.linearAgentActivitySessionId) {
1092
- console.warn(`[AgentSessionManager] No Linear session ID for session ${sessionId}`);
1093
- return;
1094
- }
1095
- try {
1096
- const result = await this.issueTracker.createAgentActivity({
1097
- agentSessionId: session.linearAgentActivitySessionId,
1098
- content: {
1099
- type: "response",
1100
- body,
1101
- },
1102
- });
1103
- if (result.success) {
1104
- console.log(`[AgentSessionManager] Created response activity for session ${sessionId}`);
1105
- }
1106
- else {
1107
- console.error(`[AgentSessionManager] Failed to create response activity:`, result);
1108
- }
1109
- }
1110
- catch (error) {
1111
- console.error(`[AgentSessionManager] Error creating response activity:`, error);
1112
- }
1256
+ await this.postActivity(sessionId, { content: { type: "response", body } }, "response");
1113
1257
  }
1114
1258
  /**
1115
1259
  * Create an error activity
1116
1260
  */
1117
1261
  async createErrorActivity(sessionId, body) {
1118
- const session = this.sessions.get(sessionId);
1119
- if (!session || !session.linearAgentActivitySessionId) {
1120
- console.warn(`[AgentSessionManager] No Linear session ID for session ${sessionId}`);
1121
- return;
1122
- }
1123
- try {
1124
- const result = await this.issueTracker.createAgentActivity({
1125
- agentSessionId: session.linearAgentActivitySessionId,
1126
- content: {
1127
- type: "error",
1128
- body,
1129
- },
1130
- });
1131
- if (result.success) {
1132
- console.log(`[AgentSessionManager] Created error activity for session ${sessionId}`);
1133
- }
1134
- else {
1135
- console.error(`[AgentSessionManager] Failed to create error activity:`, result);
1136
- }
1137
- }
1138
- catch (error) {
1139
- console.error(`[AgentSessionManager] Error creating error activity:`, error);
1140
- }
1262
+ await this.postActivity(sessionId, { content: { type: "error", body } }, "error");
1141
1263
  }
1142
1264
  /**
1143
1265
  * Create an elicitation activity
1144
1266
  */
1145
1267
  async createElicitationActivity(sessionId, body) {
1146
- const session = this.sessions.get(sessionId);
1147
- if (!session || !session.linearAgentActivitySessionId) {
1148
- console.warn(`[AgentSessionManager] No Linear session ID for session ${sessionId}`);
1149
- return;
1150
- }
1151
- try {
1152
- const result = await this.issueTracker.createAgentActivity({
1153
- agentSessionId: session.linearAgentActivitySessionId,
1154
- content: {
1155
- type: "elicitation",
1156
- body,
1157
- },
1158
- });
1159
- if (result.success) {
1160
- console.log(`[AgentSessionManager] Created elicitation activity for session ${sessionId}`);
1161
- }
1162
- else {
1163
- console.error(`[AgentSessionManager] Failed to create elicitation activity:`, result);
1164
- }
1165
- }
1166
- catch (error) {
1167
- console.error(`[AgentSessionManager] Error creating elicitation activity:`, error);
1168
- }
1268
+ await this.postActivity(sessionId, { content: { type: "elicitation", body } }, "elicitation");
1169
1269
  }
1170
1270
  /**
1171
1271
  * Create an approval elicitation activity with auth signal
1172
1272
  */
1173
1273
  async createApprovalElicitation(sessionId, body, approvalUrl) {
1174
- const session = this.sessions.get(sessionId);
1175
- if (!session || !session.linearAgentActivitySessionId) {
1176
- console.warn(`[AgentSessionManager] No Linear session ID for session ${sessionId}`);
1177
- return;
1178
- }
1179
- try {
1180
- const result = await this.issueTracker.createAgentActivity({
1181
- agentSessionId: session.linearAgentActivitySessionId,
1182
- content: {
1183
- type: "elicitation",
1184
- body,
1185
- },
1186
- signal: AgentActivitySignal.Auth,
1187
- signalMetadata: {
1188
- url: approvalUrl,
1189
- },
1190
- });
1191
- if (result.success) {
1192
- console.log(`[AgentSessionManager] Created approval elicitation for session ${sessionId} with URL: ${approvalUrl}`);
1193
- }
1194
- else {
1195
- console.error(`[AgentSessionManager] Failed to create approval elicitation:`, result);
1196
- }
1197
- }
1198
- catch (error) {
1199
- console.error(`[AgentSessionManager] Error creating approval elicitation:`, error);
1200
- }
1274
+ await this.postActivity(sessionId, {
1275
+ content: { type: "elicitation", body },
1276
+ signal: "auth",
1277
+ signalMetadata: { url: approvalUrl },
1278
+ }, "approval elicitation");
1201
1279
  }
1202
1280
  /**
1203
1281
  * Clear completed sessions older than specified time
@@ -1207,9 +1285,10 @@ export class AgentSessionManager extends EventEmitter {
1207
1285
  for (const [sessionId, session] of this.sessions.entries()) {
1208
1286
  if ((session.status === "complete" || session.status === "error") &&
1209
1287
  session.updatedAt < cutoff) {
1288
+ const log = this.sessionLog(sessionId);
1210
1289
  this.sessions.delete(sessionId);
1211
1290
  this.entries.delete(sessionId);
1212
- console.log(`[AgentSessionManager] Cleaned up session ${sessionId}`);
1291
+ log.debug(`Cleaned up session`);
1213
1292
  }
1214
1293
  }
1215
1294
  }
@@ -1254,135 +1333,65 @@ export class AgentSessionManager extends EventEmitter {
1254
1333
  }));
1255
1334
  this.entries.set(sessionId, sessionEntries);
1256
1335
  }
1257
- console.log(`[AgentSessionManager] Restored ${this.sessions.size} sessions, ${Object.keys(serializedEntries).length} entry collections`);
1336
+ this.logger.debug(`Restored ${this.sessions.size} sessions, ${Object.keys(serializedEntries).length} entry collections`);
1258
1337
  }
1259
1338
  /**
1260
1339
  * Post a thought about the model being used
1261
1340
  */
1262
- async postModelNotificationThought(linearAgentActivitySessionId, model) {
1263
- try {
1264
- const result = await this.issueTracker.createAgentActivity({
1265
- agentSessionId: linearAgentActivitySessionId,
1266
- content: {
1267
- type: "thought",
1268
- body: `Using model: ${model}`,
1269
- },
1270
- });
1271
- if (result.success) {
1272
- console.log(`[AgentSessionManager] Posted model notification for session ${linearAgentActivitySessionId} (model: ${model})`);
1273
- }
1274
- else {
1275
- console.error(`[AgentSessionManager] Failed to post model notification:`, result);
1276
- }
1277
- }
1278
- catch (error) {
1279
- console.error(`[AgentSessionManager] Error posting model notification:`, error);
1280
- }
1341
+ async postModelNotificationThought(sessionId, model) {
1342
+ await this.postActivity(sessionId, { content: { type: "thought", body: `Using model: ${model}` } }, "model notification");
1281
1343
  }
1282
1344
  /**
1283
1345
  * Post an ephemeral "Analyzing your request..." thought and return the activity ID
1284
1346
  */
1285
- async postAnalyzingThought(linearAgentActivitySessionId) {
1286
- try {
1287
- const result = await this.issueTracker.createAgentActivity({
1288
- agentSessionId: linearAgentActivitySessionId,
1289
- content: {
1290
- type: "thought",
1291
- body: "Analyzing your request…",
1292
- },
1293
- ephemeral: true,
1294
- });
1295
- if (result.success && result.agentActivity) {
1296
- const activity = await result.agentActivity;
1297
- console.log(`[AgentSessionManager] Posted analyzing thought for session ${linearAgentActivitySessionId}`);
1298
- return activity.id;
1299
- }
1300
- else {
1301
- console.error(`[AgentSessionManager] Failed to post analyzing thought:`, result);
1302
- return null;
1303
- }
1304
- }
1305
- catch (error) {
1306
- console.error(`[AgentSessionManager] Error posting analyzing thought:`, error);
1307
- return null;
1308
- }
1347
+ async postAnalyzingThought(sessionId) {
1348
+ return this.postActivity(sessionId, {
1349
+ content: { type: "thought", body: "Analyzing your request…" },
1350
+ ephemeral: true,
1351
+ }, "analyzing thought");
1309
1352
  }
1310
1353
  /**
1311
1354
  * Post the procedure selection result as a non-ephemeral thought
1312
1355
  */
1313
- async postProcedureSelectionThought(linearAgentActivitySessionId, procedureName, classification) {
1314
- try {
1315
- const result = await this.issueTracker.createAgentActivity({
1316
- agentSessionId: linearAgentActivitySessionId,
1317
- content: {
1318
- type: "thought",
1319
- body: `Selected procedure: **${procedureName}** (classified as: ${classification})`,
1320
- },
1321
- ephemeral: false,
1322
- });
1323
- if (result.success) {
1324
- console.log(`[AgentSessionManager] Posted procedure selection for session ${linearAgentActivitySessionId}: ${procedureName}`);
1325
- }
1326
- else {
1327
- console.error(`[AgentSessionManager] Failed to post procedure selection:`, result);
1328
- }
1329
- }
1330
- catch (error) {
1331
- console.error(`[AgentSessionManager] Error posting procedure selection:`, error);
1332
- }
1356
+ async postProcedureSelectionThought(sessionId, procedureName, classification) {
1357
+ await this.postActivity(sessionId, {
1358
+ content: {
1359
+ type: "thought",
1360
+ body: `Selected procedure: **${procedureName}** (classified as: ${classification})`,
1361
+ },
1362
+ ephemeral: false,
1363
+ }, "procedure selection");
1333
1364
  }
1334
1365
  /**
1335
1366
  * Handle status messages (compacting, etc.)
1336
1367
  */
1337
- async handleStatusMessage(linearAgentActivitySessionId, message) {
1338
- const session = this.sessions.get(linearAgentActivitySessionId);
1339
- if (!session || !session.linearAgentActivitySessionId) {
1340
- console.warn(`[AgentSessionManager] No Linear session ID for session ${linearAgentActivitySessionId}`);
1368
+ async handleStatusMessage(sessionId, message) {
1369
+ const session = this.sessions.get(sessionId);
1370
+ if (!session || !session.externalSessionId) {
1371
+ const log = this.sessionLog(sessionId);
1372
+ log.debug(`Skipping status message - no external session ID (platform: ${session?.issueContext?.trackerId || "unknown"})`);
1341
1373
  return;
1342
1374
  }
1343
- try {
1344
- if (message.status === "compacting") {
1345
- // Create an ephemeral thought for the compacting status
1346
- const result = await this.issueTracker.createAgentActivity({
1347
- agentSessionId: session.linearAgentActivitySessionId,
1348
- content: {
1349
- type: "thought",
1350
- body: "Compacting conversation history…",
1351
- },
1352
- ephemeral: true,
1353
- });
1354
- if (result.success && result.agentActivity) {
1355
- const activity = await result.agentActivity;
1356
- // Store the activity ID so we can replace it later
1357
- this.activeStatusActivitiesBySession.set(linearAgentActivitySessionId, activity.id);
1358
- console.log(`[AgentSessionManager] Posted ephemeral compacting status for session ${linearAgentActivitySessionId}`);
1359
- }
1360
- else {
1361
- console.error(`[AgentSessionManager] Failed to post compacting status:`, result);
1362
- }
1363
- }
1364
- else if (message.status === null) {
1365
- // Clear the status - post a non-ephemeral thought to replace the ephemeral one
1366
- const result = await this.issueTracker.createAgentActivity({
1367
- agentSessionId: session.linearAgentActivitySessionId,
1368
- content: {
1369
- type: "thought",
1370
- body: "Conversation history compacted",
1371
- },
1372
- ephemeral: false,
1373
- });
1374
- if (result.success) {
1375
- // Clean up the stored activity ID
1376
- this.activeStatusActivitiesBySession.delete(linearAgentActivitySessionId);
1377
- console.log(`[AgentSessionManager] Posted non-ephemeral status clear for session ${linearAgentActivitySessionId}`);
1378
- }
1379
- else {
1380
- console.error(`[AgentSessionManager] Failed to post status clear:`, result);
1381
- }
1375
+ if (message.status === "compacting") {
1376
+ const activityId = await this.postActivity(sessionId, {
1377
+ content: {
1378
+ type: "thought",
1379
+ body: "Compacting conversation history…",
1380
+ },
1381
+ ephemeral: true,
1382
+ }, "compacting status");
1383
+ if (activityId) {
1384
+ this.activeStatusActivitiesBySession.set(sessionId, activityId);
1382
1385
  }
1383
1386
  }
1384
- catch (error) {
1385
- console.error(`[AgentSessionManager] Error handling status message:`, error);
1387
+ else if (message.status === null) {
1388
+ // Clear the status - post a non-ephemeral thought to replace the ephemeral one
1389
+ await this.postActivity(sessionId, {
1390
+ content: { type: "thought", body: "Conversation history compacted" },
1391
+ ephemeral: false,
1392
+ }, "status clear");
1393
+ // Clean up the stored activity ID regardless — stale IDs do no harm
1394
+ this.activeStatusActivitiesBySession.delete(sessionId);
1386
1395
  }
1387
1396
  }
1388
1397
  }