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.
- package/README.md +293 -0
- package/dist/ActivityPoster.d.ts +15 -0
- package/dist/ActivityPoster.d.ts.map +1 -0
- package/dist/ActivityPoster.js +194 -0
- package/dist/ActivityPoster.js.map +1 -0
- package/dist/AgentSessionManager.d.ts +280 -0
- package/dist/AgentSessionManager.d.ts.map +1 -0
- package/dist/AgentSessionManager.js +1412 -0
- package/dist/AgentSessionManager.js.map +1 -0
- package/dist/AskUserQuestionHandler.d.ts +97 -0
- package/dist/AskUserQuestionHandler.d.ts.map +1 -0
- package/dist/AskUserQuestionHandler.js +206 -0
- package/dist/AskUserQuestionHandler.js.map +1 -0
- package/dist/AttachmentService.d.ts +69 -0
- package/dist/AttachmentService.d.ts.map +1 -0
- package/dist/AttachmentService.js +369 -0
- package/dist/AttachmentService.js.map +1 -0
- package/dist/ChatSessionHandler.d.ts +87 -0
- package/dist/ChatSessionHandler.d.ts.map +1 -0
- package/dist/ChatSessionHandler.js +231 -0
- package/dist/ChatSessionHandler.js.map +1 -0
- package/dist/ConfigManager.d.ts +91 -0
- package/dist/ConfigManager.d.ts.map +1 -0
- package/dist/ConfigManager.js +227 -0
- package/dist/ConfigManager.js.map +1 -0
- package/dist/EdgeWorker.d.ts +670 -0
- package/dist/EdgeWorker.d.ts.map +1 -0
- package/dist/EdgeWorker.js +3801 -0
- package/dist/EdgeWorker.js.map +1 -0
- package/dist/GitService.d.ts +39 -0
- package/dist/GitService.d.ts.map +1 -0
- package/dist/GitService.js +432 -0
- package/dist/GitService.js.map +1 -0
- package/dist/GlobalSessionRegistry.d.ts +142 -0
- package/dist/GlobalSessionRegistry.d.ts.map +1 -0
- package/dist/GlobalSessionRegistry.js +254 -0
- package/dist/GlobalSessionRegistry.js.map +1 -0
- package/dist/PromptBuilder.d.ts +175 -0
- package/dist/PromptBuilder.d.ts.map +1 -0
- package/dist/PromptBuilder.js +884 -0
- package/dist/PromptBuilder.js.map +1 -0
- package/dist/RepositoryRouter.d.ts +152 -0
- package/dist/RepositoryRouter.d.ts.map +1 -0
- package/dist/RepositoryRouter.js +480 -0
- package/dist/RepositoryRouter.js.map +1 -0
- package/dist/RunnerSelectionService.d.ts +62 -0
- package/dist/RunnerSelectionService.d.ts.map +1 -0
- package/dist/RunnerSelectionService.js +379 -0
- package/dist/RunnerSelectionService.js.map +1 -0
- package/dist/SharedApplicationServer.d.ts +107 -0
- package/dist/SharedApplicationServer.d.ts.map +1 -0
- package/dist/SharedApplicationServer.js +247 -0
- package/dist/SharedApplicationServer.js.map +1 -0
- package/dist/SharedWebhookServer.d.ts +39 -0
- package/dist/SharedWebhookServer.d.ts.map +1 -0
- package/dist/SharedWebhookServer.js +150 -0
- package/dist/SharedWebhookServer.js.map +1 -0
- package/dist/SlackChatAdapter.d.ts +25 -0
- package/dist/SlackChatAdapter.d.ts.map +1 -0
- package/dist/SlackChatAdapter.js +143 -0
- package/dist/SlackChatAdapter.js.map +1 -0
- package/dist/UserAccessControl.d.ts +69 -0
- package/dist/UserAccessControl.d.ts.map +1 -0
- package/dist/UserAccessControl.js +171 -0
- package/dist/UserAccessControl.js.map +1 -0
- package/dist/WorktreeIncludeService.d.ts +32 -0
- package/dist/WorktreeIncludeService.d.ts.map +1 -0
- package/dist/WorktreeIncludeService.js +123 -0
- package/dist/WorktreeIncludeService.js.map +1 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -0
- package/dist/label-prompt-template.md +27 -0
- package/dist/procedures/ProcedureAnalyzer.d.ts +69 -0
- package/dist/procedures/ProcedureAnalyzer.d.ts.map +1 -0
- package/dist/procedures/ProcedureAnalyzer.js +271 -0
- package/dist/procedures/ProcedureAnalyzer.js.map +1 -0
- package/dist/procedures/index.d.ts +7 -0
- package/dist/procedures/index.d.ts.map +1 -0
- package/dist/procedures/index.js +7 -0
- package/dist/procedures/index.js.map +1 -0
- package/dist/procedures/registry.d.ts +156 -0
- package/dist/procedures/registry.d.ts.map +1 -0
- package/dist/procedures/registry.js +240 -0
- package/dist/procedures/registry.js.map +1 -0
- package/dist/procedures/types.d.ts +103 -0
- package/dist/procedures/types.d.ts.map +1 -0
- package/dist/procedures/types.js +5 -0
- package/dist/procedures/types.js.map +1 -0
- package/dist/prompt-assembly/types.d.ts +80 -0
- package/dist/prompt-assembly/types.d.ts.map +1 -0
- package/dist/prompt-assembly/types.js +8 -0
- package/dist/prompt-assembly/types.js.map +1 -0
- package/dist/prompts/builder.md +191 -0
- package/dist/prompts/debugger.md +128 -0
- package/dist/prompts/graphite-orchestrator.md +362 -0
- package/dist/prompts/orchestrator.md +290 -0
- package/dist/prompts/scoper.md +95 -0
- package/dist/prompts/standard-issue-assigned-user-prompt.md +33 -0
- package/dist/prompts/subroutines/changelog-update.md +79 -0
- package/dist/prompts/subroutines/coding-activity.md +12 -0
- package/dist/prompts/subroutines/concise-summary.md +67 -0
- package/dist/prompts/subroutines/debugger-fix.md +92 -0
- package/dist/prompts/subroutines/debugger-reproduction.md +74 -0
- package/dist/prompts/subroutines/full-delegation.md +68 -0
- package/dist/prompts/subroutines/get-approval.md +175 -0
- package/dist/prompts/subroutines/gh-pr.md +80 -0
- package/dist/prompts/subroutines/git-commit.md +37 -0
- package/dist/prompts/subroutines/plan-summary.md +21 -0
- package/dist/prompts/subroutines/preparation.md +16 -0
- package/dist/prompts/subroutines/question-answer.md +8 -0
- package/dist/prompts/subroutines/question-investigation.md +8 -0
- package/dist/prompts/subroutines/release-execution.md +81 -0
- package/dist/prompts/subroutines/release-summary.md +60 -0
- package/dist/prompts/subroutines/user-testing-summary.md +87 -0
- package/dist/prompts/subroutines/user-testing.md +48 -0
- package/dist/prompts/subroutines/validation-fixer.md +56 -0
- package/dist/prompts/subroutines/verbose-summary.md +46 -0
- package/dist/prompts/subroutines/verifications.md +77 -0
- package/dist/prompts/todolist-system-prompt-extension.md +15 -0
- package/dist/sinks/IActivitySink.d.ts +60 -0
- package/dist/sinks/IActivitySink.d.ts.map +1 -0
- package/dist/sinks/IActivitySink.js +2 -0
- package/dist/sinks/IActivitySink.js.map +1 -0
- package/dist/sinks/LinearActivitySink.d.ts +69 -0
- package/dist/sinks/LinearActivitySink.d.ts.map +1 -0
- package/dist/sinks/LinearActivitySink.js +111 -0
- package/dist/sinks/LinearActivitySink.js.map +1 -0
- package/dist/sinks/NoopActivitySink.d.ts +13 -0
- package/dist/sinks/NoopActivitySink.d.ts.map +1 -0
- package/dist/sinks/NoopActivitySink.js +17 -0
- package/dist/sinks/NoopActivitySink.js.map +1 -0
- package/dist/sinks/index.d.ts +9 -0
- package/dist/sinks/index.d.ts.map +1 -0
- package/dist/sinks/index.js +8 -0
- package/dist/sinks/index.js.map +1 -0
- package/dist/types.d.ts +32 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/validation/ValidationLoopController.d.ts +54 -0
- package/dist/validation/ValidationLoopController.d.ts.map +1 -0
- package/dist/validation/ValidationLoopController.js +242 -0
- package/dist/validation/ValidationLoopController.js.map +1 -0
- package/dist/validation/index.d.ts +7 -0
- package/dist/validation/index.d.ts.map +1 -0
- package/dist/validation/index.js +7 -0
- package/dist/validation/index.js.map +1 -0
- package/dist/validation/types.d.ts +82 -0
- package/dist/validation/types.d.ts.map +1 -0
- package/dist/validation/types.js +29 -0
- package/dist/validation/types.js.map +1 -0
- package/label-prompt-template.md +27 -0
- package/package.json +56 -0
- package/prompt-template.md +116 -0
- package/prompts/builder.md +191 -0
- package/prompts/debugger.md +128 -0
- package/prompts/graphite-orchestrator.md +362 -0
- package/prompts/orchestrator.md +290 -0
- package/prompts/scoper.md +95 -0
- package/prompts/standard-issue-assigned-user-prompt.md +33 -0
- 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
|