sylas-edge-worker 0.2.21

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