@straiffi/archon 1.0.0

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 (124) hide show
  1. package/README.md +224 -0
  2. package/dist/cli.js +216 -0
  3. package/dist/client/assets/index-8_-boBBA.css +2 -0
  4. package/dist/client/assets/index-s_jjeqha.js +176 -0
  5. package/dist/client/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
  6. package/dist/client/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
  7. package/dist/client/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
  8. package/dist/client/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
  9. package/dist/client/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
  10. package/dist/client/favicon.svg +62 -0
  11. package/dist/client/icons.svg +24 -0
  12. package/dist/client/index.html +14 -0
  13. package/dist/server/db.js +764 -0
  14. package/dist/server/db.js.map +1 -0
  15. package/dist/server/index.js +5134 -0
  16. package/dist/server/index.js.map +1 -0
  17. package/dist/server/lib/agent.js +1302 -0
  18. package/dist/server/lib/agent.js.map +1 -0
  19. package/dist/server/lib/buildChains.js +2 -0
  20. package/dist/server/lib/buildChains.js.map +1 -0
  21. package/dist/server/lib/buildFlow.js +59 -0
  22. package/dist/server/lib/buildFlow.js.map +1 -0
  23. package/dist/server/lib/buildSequences.js +599 -0
  24. package/dist/server/lib/buildSequences.js.map +1 -0
  25. package/dist/server/lib/bundleActivity.js +95 -0
  26. package/dist/server/lib/bundleActivity.js.map +1 -0
  27. package/dist/server/lib/bundlePullRequests.js +126 -0
  28. package/dist/server/lib/bundlePullRequests.js.map +1 -0
  29. package/dist/server/lib/chatMessages.js +60 -0
  30. package/dist/server/lib/chatMessages.js.map +1 -0
  31. package/dist/server/lib/chatTargets.js +123 -0
  32. package/dist/server/lib/chatTargets.js.map +1 -0
  33. package/dist/server/lib/chatTicketProposals.js +180 -0
  34. package/dist/server/lib/chatTicketProposals.js.map +1 -0
  35. package/dist/server/lib/chats.js +279 -0
  36. package/dist/server/lib/chats.js.map +1 -0
  37. package/dist/server/lib/config.js +3 -0
  38. package/dist/server/lib/config.js.map +1 -0
  39. package/dist/server/lib/cors.js +30 -0
  40. package/dist/server/lib/cors.js.map +1 -0
  41. package/dist/server/lib/directoryPicker.js +174 -0
  42. package/dist/server/lib/directoryPicker.js.map +1 -0
  43. package/dist/server/lib/git.js +1284 -0
  44. package/dist/server/lib/git.js.map +1 -0
  45. package/dist/server/lib/integrations/github.js +511 -0
  46. package/dist/server/lib/integrations/github.js.map +1 -0
  47. package/dist/server/lib/integrations/index.js +162 -0
  48. package/dist/server/lib/integrations/index.js.map +1 -0
  49. package/dist/server/lib/integrations/jira.js +283 -0
  50. package/dist/server/lib/integrations/jira.js.map +1 -0
  51. package/dist/server/lib/integrations/planning.js +27 -0
  52. package/dist/server/lib/integrations/planning.js.map +1 -0
  53. package/dist/server/lib/integrations/types.js +2 -0
  54. package/dist/server/lib/integrations/types.js.map +1 -0
  55. package/dist/server/lib/lightweightPrompt.js +88 -0
  56. package/dist/server/lib/lightweightPrompt.js.map +1 -0
  57. package/dist/server/lib/models.js +219 -0
  58. package/dist/server/lib/models.js.map +1 -0
  59. package/dist/server/lib/preview.js +377 -0
  60. package/dist/server/lib/preview.js.map +1 -0
  61. package/dist/server/lib/previewProxy.js +659 -0
  62. package/dist/server/lib/previewProxy.js.map +1 -0
  63. package/dist/server/lib/projectAutoConfig.js +682 -0
  64. package/dist/server/lib/projectAutoConfig.js.map +1 -0
  65. package/dist/server/lib/projectFileSuggestions.js +133 -0
  66. package/dist/server/lib/projectFileSuggestions.js.map +1 -0
  67. package/dist/server/lib/projectMemory.js +1519 -0
  68. package/dist/server/lib/projectMemory.js.map +1 -0
  69. package/dist/server/lib/projectMemoryPrompt.js +390 -0
  70. package/dist/server/lib/projectMemoryPrompt.js.map +1 -0
  71. package/dist/server/lib/projectMemoryScan.js +681 -0
  72. package/dist/server/lib/projectMemoryScan.js.map +1 -0
  73. package/dist/server/lib/projectMemorySuggestions.js +166 -0
  74. package/dist/server/lib/projectMemorySuggestions.js.map +1 -0
  75. package/dist/server/lib/projectMemoryTransfer.js +958 -0
  76. package/dist/server/lib/projectMemoryTransfer.js.map +1 -0
  77. package/dist/server/lib/projects.js +569 -0
  78. package/dist/server/lib/projects.js.map +1 -0
  79. package/dist/server/lib/promptSkills.js +28 -0
  80. package/dist/server/lib/promptSkills.js.map +1 -0
  81. package/dist/server/lib/queue.js +15 -0
  82. package/dist/server/lib/queue.js.map +1 -0
  83. package/dist/server/lib/reviewFindings.js +390 -0
  84. package/dist/server/lib/reviewFindings.js.map +1 -0
  85. package/dist/server/lib/run.js +416 -0
  86. package/dist/server/lib/run.js.map +1 -0
  87. package/dist/server/lib/runtimePaths.js +93 -0
  88. package/dist/server/lib/runtimePaths.js.map +1 -0
  89. package/dist/server/lib/shell.js +27 -0
  90. package/dist/server/lib/shell.js.map +1 -0
  91. package/dist/server/lib/skills.js +124 -0
  92. package/dist/server/lib/skills.js.map +1 -0
  93. package/dist/server/lib/startDev.js +18 -0
  94. package/dist/server/lib/startDev.js.map +1 -0
  95. package/dist/server/lib/staticClient.js +80 -0
  96. package/dist/server/lib/staticClient.js.map +1 -0
  97. package/dist/server/lib/terminal.js +366 -0
  98. package/dist/server/lib/terminal.js.map +1 -0
  99. package/dist/server/lib/ticketDependencies.js +174 -0
  100. package/dist/server/lib/ticketDependencies.js.map +1 -0
  101. package/dist/server/lib/ticketMessages.js +65 -0
  102. package/dist/server/lib/ticketMessages.js.map +1 -0
  103. package/dist/server/lib/ticketOpenQuestions.js +128 -0
  104. package/dist/server/lib/ticketOpenQuestions.js.map +1 -0
  105. package/dist/server/lib/ticketUndo.js +549 -0
  106. package/dist/server/lib/ticketUndo.js.map +1 -0
  107. package/dist/server/lib/tickets.js +981 -0
  108. package/dist/server/lib/tickets.js.map +1 -0
  109. package/dist/server/lib/types.js +2 -0
  110. package/dist/server/lib/types.js.map +1 -0
  111. package/dist/server/package.json +3 -0
  112. package/dist/server/workers/build.js +229 -0
  113. package/dist/server/workers/build.js.map +1 -0
  114. package/dist/server/workers/chat.js +190 -0
  115. package/dist/server/workers/chat.js.map +1 -0
  116. package/dist/server/workers/followUp.js +204 -0
  117. package/dist/server/workers/followUp.js.map +1 -0
  118. package/dist/server/workers/plan.js +1130 -0
  119. package/dist/server/workers/plan.js.map +1 -0
  120. package/dist/server/workers/planFollowUp.js +360 -0
  121. package/dist/server/workers/planFollowUp.js.map +1 -0
  122. package/dist/server/workers/review.js +167 -0
  123. package/dist/server/workers/review.js.map +1 -0
  124. package/package.json +40 -0
@@ -0,0 +1,1302 @@
1
+ import { spawn } from 'child_process';
2
+ import { randomUUID } from 'crypto';
3
+ import { isAbsolute, join, relative, resolve, sep } from 'path';
4
+ import Database from 'better-sqlite3';
5
+ import db from '../db.js';
6
+ import { getChatSession, updateChatSessionUsageSnapshot } from './chats.js';
7
+ import { normalizeModelId } from './models.js';
8
+ import { resolveOpencodeDbPath } from './runtimePaths.js';
9
+ import { markTicketDescriptionVisibleInBundle } from './ticketMessages.js';
10
+ import { getTicket } from './tickets.js';
11
+ const OPENCODE_PLACEHOLDER_TITLE_PATTERN = /^New session\b/i;
12
+ const OPENCODE_SESSION_METADATA_LOOKUP_DELAY_MS = 1500;
13
+ const AGENT_PROGRESS_UPDATE_INTERVAL_MS = 100;
14
+ const OPENCODE_SESSION_BY_ID_QUERY = 'SELECT id, title FROM session WHERE id = ? LIMIT 1';
15
+ const OPENCODE_SESSION_BY_CREATED_AT_QUERY = 'SELECT id, title FROM session WHERE time_created > ? ORDER BY time_created DESC LIMIT 1';
16
+ const OPENCODE_COMPLETED_ASSISTANT_MESSAGE_USAGE_QUERY = "SELECT data FROM message WHERE session_id = ? AND json_extract(data, '$.role') = 'assistant' AND json_extract(data, '$.time.completed') IS NOT NULL ORDER BY time_created DESC LIMIT 1";
17
+ const OPENCODE_COMPLETED_ASSISTANT_MESSAGE_USAGE_SINCE_QUERY = "SELECT data FROM message WHERE session_id = ? AND time_created >= ? AND json_extract(data, '$.role') = 'assistant' AND json_extract(data, '$.time.completed') IS NOT NULL ORDER BY time_created DESC LIMIT 1";
18
+ const OPENCODE_STEP_FINISH_USAGE_QUERY = "SELECT data FROM part WHERE session_id = ? AND json_extract(data, '$.type') = 'step-finish' ORDER BY time_created DESC LIMIT 1";
19
+ const OPENCODE_STEP_FINISH_USAGE_SINCE_QUERY = "SELECT data FROM part WHERE session_id = ? AND time_created >= ? AND json_extract(data, '$.type') = 'step-finish' ORDER BY time_created DESC LIMIT 1";
20
+ const GET_PERSISTED_CHAT_STATE_QUERY = 'SELECT agent_log, streaming_response, opencode_session_id FROM chat_sessions WHERE id = ?';
21
+ const EMPTY_CHAT_USAGE_SNAPSHOT = {
22
+ usage_snapshot_context_window: null,
23
+ usage_snapshot_max_output_tokens: null,
24
+ usage_snapshot_total_tokens: null,
25
+ usage_snapshot_input_tokens: null,
26
+ usage_snapshot_output_tokens: null,
27
+ usage_snapshot_cache_read_tokens: null,
28
+ usage_snapshot_reasoning_tokens: null,
29
+ };
30
+ const toFiniteNumber = (value) => {
31
+ return typeof value === 'number' && Number.isFinite(value)
32
+ ? value
33
+ : null;
34
+ };
35
+ const getValueAtPath = (value, path) => {
36
+ let current = value;
37
+ for (const segment of path) {
38
+ if (!current || typeof current !== 'object' || !(segment in current)) {
39
+ return null;
40
+ }
41
+ current = current[segment];
42
+ }
43
+ return current;
44
+ };
45
+ const getNumberAtPaths = (value, paths) => {
46
+ for (const path of paths) {
47
+ const candidate = toFiniteNumber(getValueAtPath(value, path));
48
+ if (candidate !== null) {
49
+ return candidate;
50
+ }
51
+ }
52
+ return null;
53
+ };
54
+ const mergeChatUsageSnapshot = (current, next) => {
55
+ return {
56
+ usage_snapshot_context_window: next.usage_snapshot_context_window ?? current.usage_snapshot_context_window,
57
+ usage_snapshot_max_output_tokens: next.usage_snapshot_max_output_tokens ?? current.usage_snapshot_max_output_tokens,
58
+ usage_snapshot_total_tokens: next.usage_snapshot_total_tokens ?? current.usage_snapshot_total_tokens,
59
+ usage_snapshot_input_tokens: next.usage_snapshot_input_tokens ?? current.usage_snapshot_input_tokens,
60
+ usage_snapshot_output_tokens: next.usage_snapshot_output_tokens ?? current.usage_snapshot_output_tokens,
61
+ usage_snapshot_cache_read_tokens: next.usage_snapshot_cache_read_tokens ?? current.usage_snapshot_cache_read_tokens,
62
+ usage_snapshot_reasoning_tokens: next.usage_snapshot_reasoning_tokens ?? current.usage_snapshot_reasoning_tokens,
63
+ };
64
+ };
65
+ const extractChatUsageSnapshot = (value) => {
66
+ if (!value || typeof value !== 'object') {
67
+ return {};
68
+ }
69
+ return {
70
+ usage_snapshot_context_window: getNumberAtPaths(value, [
71
+ ['contextWindow'],
72
+ ['context_window'],
73
+ ['result', 'contextWindow'],
74
+ ['result', 'context_window'],
75
+ ['limit', 'context'],
76
+ ['limits', 'context'],
77
+ ]),
78
+ usage_snapshot_max_output_tokens: getNumberAtPaths(value, [
79
+ ['maxOutputTokens'],
80
+ ['max_output_tokens'],
81
+ ['result', 'maxOutputTokens'],
82
+ ['result', 'max_output_tokens'],
83
+ ['limit', 'output'],
84
+ ['limits', 'output'],
85
+ ]),
86
+ usage_snapshot_total_tokens: getNumberAtPaths(value, [
87
+ ['tokens', 'total'],
88
+ ['usage', 'tokens', 'total'],
89
+ ['usage', 'total'],
90
+ ['usage', 'totalTokens'],
91
+ ['result', 'tokens', 'total'],
92
+ ['result', 'usage', 'tokens', 'total'],
93
+ ['result', 'usage', 'total'],
94
+ ['result', 'usage', 'totalTokens'],
95
+ ]),
96
+ usage_snapshot_input_tokens: getNumberAtPaths(value, [
97
+ ['tokens', 'input'],
98
+ ['usage', 'tokens', 'input'],
99
+ ['usage', 'input'],
100
+ ['usage', 'inputTokens'],
101
+ ['result', 'tokens', 'input'],
102
+ ['result', 'usage', 'tokens', 'input'],
103
+ ['result', 'usage', 'input'],
104
+ ['result', 'usage', 'inputTokens'],
105
+ ]),
106
+ usage_snapshot_output_tokens: getNumberAtPaths(value, [
107
+ ['tokens', 'output'],
108
+ ['usage', 'tokens', 'output'],
109
+ ['usage', 'output'],
110
+ ['usage', 'outputTokens'],
111
+ ['result', 'tokens', 'output'],
112
+ ['result', 'usage', 'tokens', 'output'],
113
+ ['result', 'usage', 'output'],
114
+ ['result', 'usage', 'outputTokens'],
115
+ ]),
116
+ usage_snapshot_cache_read_tokens: getNumberAtPaths(value, [
117
+ ['tokens', 'cache', 'read'],
118
+ ['usage', 'tokens', 'cacheRead'],
119
+ ['usage', 'cacheRead'],
120
+ ['usage', 'cacheReadTokens'],
121
+ ['result', 'tokens', 'cache', 'read'],
122
+ ['result', 'usage', 'tokens', 'cacheRead'],
123
+ ['result', 'usage', 'cacheRead'],
124
+ ['result', 'usage', 'cacheReadTokens'],
125
+ ]),
126
+ usage_snapshot_reasoning_tokens: getNumberAtPaths(value, [
127
+ ['tokens', 'reasoning'],
128
+ ['usage', 'tokens', 'reasoning'],
129
+ ['usage', 'reasoning'],
130
+ ['usage', 'reasoningTokens'],
131
+ ['result', 'tokens', 'reasoning'],
132
+ ['result', 'usage', 'tokens', 'reasoning'],
133
+ ['result', 'usage', 'reasoning'],
134
+ ['result', 'usage', 'reasoningTokens'],
135
+ ]),
136
+ };
137
+ };
138
+ const mergeUsageFromJsonLine = (current, line) => {
139
+ const trimmedLine = line.trim();
140
+ if (!trimmedLine.startsWith('{')) {
141
+ return current;
142
+ }
143
+ const { jsonObjects } = extractLeadingJsonObjects(trimmedLine);
144
+ let next = current;
145
+ for (const jsonObject of jsonObjects) {
146
+ try {
147
+ next = mergeChatUsageSnapshot(next, extractChatUsageSnapshot(JSON.parse(jsonObject)));
148
+ }
149
+ catch {
150
+ continue;
151
+ }
152
+ }
153
+ return next;
154
+ };
155
+ const hasUsageSnapshot = (value) => {
156
+ return Object.values(value).some(candidate => candidate !== null && candidate !== undefined);
157
+ };
158
+ const hasMatchingUsageSnapshot = (current, next) => {
159
+ return current.usage_snapshot_context_window === next.usage_snapshot_context_window
160
+ && current.usage_snapshot_max_output_tokens === next.usage_snapshot_max_output_tokens
161
+ && current.usage_snapshot_total_tokens === next.usage_snapshot_total_tokens
162
+ && current.usage_snapshot_input_tokens === next.usage_snapshot_input_tokens
163
+ && current.usage_snapshot_output_tokens === next.usage_snapshot_output_tokens
164
+ && current.usage_snapshot_cache_read_tokens === next.usage_snapshot_cache_read_tokens
165
+ && current.usage_snapshot_reasoning_tokens === next.usage_snapshot_reasoning_tokens;
166
+ };
167
+ const activeBuildProcesses = new Map();
168
+ const activeChatProcesses = new Map();
169
+ const getErrorMessage = (error) => {
170
+ return error instanceof Error ? error.message : String(error);
171
+ };
172
+ const getPersistedChatState = (chatSessionId) => {
173
+ const row = db.prepare(GET_PERSISTED_CHAT_STATE_QUERY).get(chatSessionId);
174
+ if (!row) {
175
+ return null;
176
+ }
177
+ return {
178
+ agentLog: row.agent_log ?? null,
179
+ streamingResponse: row.streaming_response ?? null,
180
+ opencodeSessionId: row.opencode_session_id ?? null,
181
+ };
182
+ };
183
+ const preferNonEmptyChatValue = (value, fallback) => {
184
+ if (typeof value === 'string' && value.trim()) {
185
+ return value;
186
+ }
187
+ if (typeof fallback === 'string' && fallback.trim()) {
188
+ return fallback;
189
+ }
190
+ return null;
191
+ };
192
+ const getPreservedChatOutput = ({ chatSessionId, log, response, }) => {
193
+ const persistedState = getPersistedChatState(chatSessionId);
194
+ return {
195
+ log: preferNonEmptyChatValue(log, persistedState?.agentLog ?? null),
196
+ response: preferNonEmptyChatValue(response, persistedState?.streamingResponse ?? null),
197
+ opencodeSessionId: persistedState?.opencodeSessionId ?? null,
198
+ };
199
+ };
200
+ const updateStatus = (ticketId, status, log, io, streamingResponse = null, options = {}) => {
201
+ db.prepare('UPDATE tickets SET agent_status = ?, agent_log = ?, streaming_response = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(status, log ?? null, streamingResponse ?? null, ticketId);
202
+ const updatedTicket = getTicket(ticketId);
203
+ if (updatedTicket?.worktree_bundle_id && updatedTicket.state !== 'plan') {
204
+ markTicketDescriptionVisibleInBundle(ticketId);
205
+ }
206
+ if (options.emit === false) {
207
+ return;
208
+ }
209
+ if (updatedTicket) {
210
+ io.emit('ticket:updated', getTicket(ticketId) ?? updatedTicket);
211
+ }
212
+ };
213
+ const updateSessionId = (ticketId, sessionId, sessionField, io) => {
214
+ db.prepare(`UPDATE tickets SET ${sessionField} = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`).run(sessionId, ticketId);
215
+ const updatedTicket = getTicket(ticketId);
216
+ if (sessionField === 'session_id'
217
+ && updatedTicket?.worktree_bundle_id
218
+ && updatedTicket.state !== 'plan') {
219
+ markTicketDescriptionVisibleInBundle(ticketId);
220
+ }
221
+ if (updatedTicket) {
222
+ io.emit('ticket:updated', getTicket(ticketId) ?? updatedTicket);
223
+ }
224
+ };
225
+ const updateChatSessionStatus = (chatSessionId, status, log, io, streamingResponse = null) => {
226
+ db.prepare('UPDATE chat_sessions SET agent_status = ?, agent_log = ?, streaming_response = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(status, log ?? null, streamingResponse ?? null, chatSessionId);
227
+ const chatSession = getChatSession(chatSessionId, { includeMessages: true });
228
+ if (chatSession) {
229
+ io.emit('chat-session:updated', chatSession);
230
+ }
231
+ };
232
+ const updateChatSessionId = (chatSessionId, tool, sessionId, io) => {
233
+ const sessionField = tool === 'claude' ? 'claude_session_id' : 'opencode_session_id';
234
+ db.prepare(`UPDATE chat_sessions SET ${sessionField} = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`).run(sessionId, chatSessionId);
235
+ const chatSession = getChatSession(chatSessionId, { includeMessages: true });
236
+ if (chatSession) {
237
+ io.emit('chat-session:updated', chatSession);
238
+ }
239
+ };
240
+ const createThrottledProgressUpdater = (flushUpdate) => {
241
+ let timer = null;
242
+ let hasPendingUpdate = false;
243
+ const clearTimer = () => {
244
+ if (timer === null) {
245
+ return;
246
+ }
247
+ clearTimeout(timer);
248
+ timer = null;
249
+ };
250
+ return {
251
+ schedule: () => {
252
+ hasPendingUpdate = true;
253
+ if (timer !== null) {
254
+ return;
255
+ }
256
+ timer = setTimeout(() => {
257
+ timer = null;
258
+ if (!hasPendingUpdate) {
259
+ return;
260
+ }
261
+ hasPendingUpdate = false;
262
+ flushUpdate();
263
+ }, AGENT_PROGRESS_UPDATE_INTERVAL_MS);
264
+ timer.unref?.();
265
+ },
266
+ cancel: () => {
267
+ hasPendingUpdate = false;
268
+ clearTimer();
269
+ },
270
+ };
271
+ };
272
+ const openOpencodeDatabase = () => new Database(resolveOpencodeDbPath(), { readonly: true });
273
+ const readOpencodeSession = (statement, value) => {
274
+ const opencodeDb = openOpencodeDatabase();
275
+ try {
276
+ const row = opencodeDb.prepare(statement).get(value);
277
+ if (!row?.id) {
278
+ return null;
279
+ }
280
+ const normalizedTitle = typeof row.title === 'string'
281
+ ? row.title.trim() || null
282
+ : null;
283
+ const title = normalizedTitle && !OPENCODE_PLACEHOLDER_TITLE_PATTERN.test(normalizedTitle)
284
+ ? normalizedTitle
285
+ : null;
286
+ return {
287
+ id: row.id,
288
+ title,
289
+ };
290
+ }
291
+ finally {
292
+ opencodeDb.close();
293
+ }
294
+ };
295
+ const readOpencodeUsageSnapshot = ({ sessionId, spawnTimestamp = null, }) => {
296
+ const opencodeDb = openOpencodeDatabase();
297
+ try {
298
+ const completedAssistantMessage = (spawnTimestamp === null
299
+ ? opencodeDb.prepare(OPENCODE_COMPLETED_ASSISTANT_MESSAGE_USAGE_QUERY).get(sessionId)
300
+ : opencodeDb.prepare(OPENCODE_COMPLETED_ASSISTANT_MESSAGE_USAGE_SINCE_QUERY).get(sessionId, spawnTimestamp));
301
+ if (typeof completedAssistantMessage?.data === 'string') {
302
+ try {
303
+ const parsedUsage = extractChatUsageSnapshot(JSON.parse(completedAssistantMessage.data));
304
+ if (hasUsageSnapshot(parsedUsage)) {
305
+ return mergeChatUsageSnapshot(EMPTY_CHAT_USAGE_SNAPSHOT, parsedUsage);
306
+ }
307
+ }
308
+ catch {
309
+ // Fall through to the step-finish lookup below.
310
+ }
311
+ }
312
+ const latestStepFinish = (spawnTimestamp === null
313
+ ? opencodeDb.prepare(OPENCODE_STEP_FINISH_USAGE_QUERY).get(sessionId)
314
+ : opencodeDb.prepare(OPENCODE_STEP_FINISH_USAGE_SINCE_QUERY).get(sessionId, spawnTimestamp));
315
+ if (typeof latestStepFinish?.data === 'string') {
316
+ try {
317
+ const parsedUsage = extractChatUsageSnapshot(JSON.parse(latestStepFinish.data));
318
+ if (hasUsageSnapshot(parsedUsage)) {
319
+ return mergeChatUsageSnapshot(EMPTY_CHAT_USAGE_SNAPSHOT, parsedUsage);
320
+ }
321
+ }
322
+ catch {
323
+ return EMPTY_CHAT_USAGE_SNAPSHOT;
324
+ }
325
+ }
326
+ return EMPTY_CHAT_USAGE_SNAPSHOT;
327
+ }
328
+ finally {
329
+ opencodeDb.close();
330
+ }
331
+ };
332
+ const resolveOpencodeUsageSnapshot = ({ current, sessionId, spawnTimestamp = null, }) => {
333
+ try {
334
+ return mergeChatUsageSnapshot(current, readOpencodeUsageSnapshot({ sessionId, spawnTimestamp }));
335
+ }
336
+ catch {
337
+ return current;
338
+ }
339
+ };
340
+ export const refreshChatSessionUsageSnapshot = (chatSessionId, options = {}) => {
341
+ const chatSession = getChatSession(chatSessionId, { includeMessages: options.includeMessages });
342
+ if (!chatSession || chatSession.tool !== 'opencode' || !chatSession.opencode_session_id) {
343
+ return chatSession;
344
+ }
345
+ let nextUsageSnapshot;
346
+ try {
347
+ nextUsageSnapshot = readOpencodeUsageSnapshot({
348
+ sessionId: chatSession.opencode_session_id,
349
+ });
350
+ }
351
+ catch {
352
+ return chatSession;
353
+ }
354
+ if (hasMatchingUsageSnapshot(chatSession, nextUsageSnapshot)) {
355
+ return chatSession;
356
+ }
357
+ updateChatSessionUsageSnapshot(chatSessionId, nextUsageSnapshot);
358
+ return getChatSession(chatSessionId, { includeMessages: options.includeMessages });
359
+ };
360
+ const updateChatSessionOpencodeMetadata = (chatSessionId, sessionId, title, io) => {
361
+ db.prepare('UPDATE chat_sessions SET opencode_session_id = ?, title = COALESCE(?, title), updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(sessionId, title, chatSessionId);
362
+ const chatSession = getChatSession(chatSessionId, { includeMessages: true });
363
+ if (chatSession) {
364
+ io.emit('chat-session:updated', chatSession);
365
+ }
366
+ };
367
+ const createOpencodeMetadataRefresher = ({ chatSessionId, io, spawnTimestamp, initialSessionId = null, }) => {
368
+ let lookupTimer = null;
369
+ let stopped = false;
370
+ let knownSessionId = initialSessionId;
371
+ let lastAppliedSessionId = null;
372
+ let lastAppliedTitle = null;
373
+ const clearLookupTimer = () => {
374
+ if (!lookupTimer) {
375
+ return;
376
+ }
377
+ clearTimeout(lookupTimer);
378
+ lookupTimer = null;
379
+ };
380
+ const readCurrentSession = () => {
381
+ if (knownSessionId) {
382
+ return readOpencodeSession(OPENCODE_SESSION_BY_ID_QUERY, knownSessionId);
383
+ }
384
+ if (spawnTimestamp === null) {
385
+ return null;
386
+ }
387
+ return readOpencodeSession(OPENCODE_SESSION_BY_CREATED_AT_QUERY, spawnTimestamp);
388
+ };
389
+ const refresh = () => {
390
+ if (stopped) {
391
+ return { hasSession: false, hasResolvedTitle: false, sessionId: knownSessionId };
392
+ }
393
+ try {
394
+ const session = readCurrentSession();
395
+ if (!session) {
396
+ return { hasSession: false, hasResolvedTitle: false, sessionId: knownSessionId };
397
+ }
398
+ knownSessionId = session.id;
399
+ if (session.id !== lastAppliedSessionId || session.title !== lastAppliedTitle) {
400
+ updateChatSessionOpencodeMetadata(chatSessionId, session.id, session.title, io);
401
+ lastAppliedSessionId = session.id;
402
+ lastAppliedTitle = session.title;
403
+ }
404
+ return {
405
+ hasSession: true,
406
+ hasResolvedTitle: session.title !== null,
407
+ sessionId: session.id,
408
+ };
409
+ }
410
+ catch {
411
+ return { hasSession: false, hasResolvedTitle: false, sessionId: knownSessionId };
412
+ }
413
+ };
414
+ const scheduleRetry = () => {
415
+ if (stopped) {
416
+ return;
417
+ }
418
+ clearLookupTimer();
419
+ lookupTimer = setTimeout(() => {
420
+ const result = refresh();
421
+ if (!result.hasResolvedTitle) {
422
+ scheduleRetry();
423
+ }
424
+ }, OPENCODE_SESSION_METADATA_LOOKUP_DELAY_MS);
425
+ };
426
+ return {
427
+ refresh,
428
+ scheduleRetry,
429
+ stop: () => {
430
+ stopped = true;
431
+ clearLookupTimer();
432
+ },
433
+ };
434
+ };
435
+ const TOOL_CALL_PREVIEW_MAX_LENGTH = 100;
436
+ const APPLY_PATCH_START_MARKER = '[[archon-apply-patch]]';
437
+ const APPLY_PATCH_END_MARKER = '[[/archon-apply-patch]]';
438
+ const toDisplayPath = (filePath, cwd) => {
439
+ if (!cwd || !isAbsolute(filePath)) {
440
+ return filePath;
441
+ }
442
+ const resolvedCwd = resolve(cwd);
443
+ const resolvedFilePath = resolve(filePath);
444
+ if (resolvedFilePath === resolvedCwd) {
445
+ return filePath;
446
+ }
447
+ const cwdPrefix = `${resolvedCwd}${sep}`;
448
+ if (!resolvedFilePath.startsWith(cwdPrefix)) {
449
+ return filePath;
450
+ }
451
+ return relative(resolvedCwd, resolvedFilePath);
452
+ };
453
+ const formatApplyPatchToolCall = (input, cwd) => {
454
+ const patchText = typeof input?.patchText === 'string'
455
+ ? input.patchText.trim()
456
+ : null;
457
+ if (!patchText) {
458
+ return 'apply_patch';
459
+ }
460
+ const displayLines = [APPLY_PATCH_START_MARKER];
461
+ const lines = patchText.replace(/\r\n/g, '\n').split('\n');
462
+ let currentFilePath = null;
463
+ let lastDisplayedFileIndex = -1;
464
+ for (const line of lines) {
465
+ const trimmedLine = line.trimEnd();
466
+ const fileMatch = trimmedLine.match(/^\*\*\* (Update|Add|Delete) File: (.+)$/);
467
+ if (fileMatch) {
468
+ currentFilePath = toDisplayPath(fileMatch[2].trim(), cwd);
469
+ displayLines.push(currentFilePath);
470
+ lastDisplayedFileIndex = displayLines.length - 1;
471
+ continue;
472
+ }
473
+ const moveToMatch = trimmedLine.match(/^\*\*\* Move to: (.+)$/);
474
+ if (moveToMatch && currentFilePath && lastDisplayedFileIndex >= 0) {
475
+ const movedToPath = toDisplayPath(moveToMatch[1].trim(), cwd);
476
+ displayLines[lastDisplayedFileIndex] = `${currentFilePath} -> ${movedToPath}`;
477
+ currentFilePath = movedToPath;
478
+ continue;
479
+ }
480
+ if (/^[ +\\-]/.test(line)) {
481
+ displayLines.push(line);
482
+ }
483
+ }
484
+ if (displayLines.length === 1) {
485
+ return 'apply_patch';
486
+ }
487
+ displayLines.push(APPLY_PATCH_END_MARKER);
488
+ return displayLines.join('\n');
489
+ };
490
+ const formatToolCall = (name, input, cwd) => {
491
+ if (name.toLowerCase() === 'apply_patch') {
492
+ return formatApplyPatchToolCall(input, cwd);
493
+ }
494
+ const val = {
495
+ Read: input?.file_path,
496
+ Write: input?.file_path,
497
+ Edit: input?.file_path,
498
+ Bash: input?.command,
499
+ Glob: input?.pattern,
500
+ Grep: input?.pattern,
501
+ WebFetch: input?.url,
502
+ WebSearch: input?.query,
503
+ Task: input?.description,
504
+ NotebookEdit: input?.notebook_path,
505
+ }[name] ?? Object.values(input ?? {}).find(v => typeof v === 'string');
506
+ if (typeof val !== 'string' || val.length === 0) {
507
+ return `_${name}_`;
508
+ }
509
+ const display = val.length > TOOL_CALL_PREVIEW_MAX_LENGTH ? val.slice(0, TOOL_CALL_PREVIEW_MAX_LENGTH) + '…' : val;
510
+ return `_${name}_ \`${display}\``;
511
+ };
512
+ const parseClaudeStreamJsonLine = (line, cwd) => {
513
+ const trimmed = line.trim();
514
+ if (!trimmed) {
515
+ return { logFragment: null, responseFragment: null };
516
+ }
517
+ let event;
518
+ try {
519
+ event = JSON.parse(trimmed);
520
+ }
521
+ catch {
522
+ return { logFragment: trimmed, responseFragment: trimmed };
523
+ }
524
+ if (event.type === 'assistant' && Array.isArray(event.message?.content)) {
525
+ const logParts = [];
526
+ const responseParts = [];
527
+ for (const block of event.message.content) {
528
+ if (block.type === 'text' && block.text) {
529
+ const text = block.text.trim();
530
+ if (text) {
531
+ logParts.push(text);
532
+ responseParts.push(text);
533
+ }
534
+ }
535
+ else if (block.type === 'tool_use' && block.name) {
536
+ logParts.push(formatToolCall(block.name, block.input, cwd));
537
+ }
538
+ }
539
+ return {
540
+ logFragment: logParts.length > 0 ? logParts.join('\n\n') : null,
541
+ responseFragment: responseParts.length > 0 ? responseParts.join('\n\n') : null,
542
+ };
543
+ }
544
+ return { logFragment: null, responseFragment: null };
545
+ };
546
+ const formatOpencodeToolUse = (event, cwd) => {
547
+ const toolName = event.part?.tool;
548
+ if (!toolName) {
549
+ return null;
550
+ }
551
+ return formatToolCall(toolName, event.part?.state?.input, cwd);
552
+ };
553
+ const formatOpencodeStepTitle = (event) => {
554
+ const title = event.part?.title?.trim();
555
+ return title || null;
556
+ };
557
+ const extractLeadingJsonObject = (line) => {
558
+ if (!line.startsWith('{')) {
559
+ return null;
560
+ }
561
+ let depth = 0;
562
+ let inString = false;
563
+ let escaping = false;
564
+ for (let index = 0; index < line.length; index += 1) {
565
+ const char = line[index];
566
+ if (inString) {
567
+ if (escaping) {
568
+ escaping = false;
569
+ continue;
570
+ }
571
+ if (char === '\\') {
572
+ escaping = true;
573
+ continue;
574
+ }
575
+ if (char === '"') {
576
+ inString = false;
577
+ }
578
+ continue;
579
+ }
580
+ if (char === '"') {
581
+ inString = true;
582
+ continue;
583
+ }
584
+ if (char === '{') {
585
+ depth += 1;
586
+ continue;
587
+ }
588
+ if (char === '}') {
589
+ depth -= 1;
590
+ if (depth === 0) {
591
+ return line.slice(0, index + 1);
592
+ }
593
+ }
594
+ }
595
+ return null;
596
+ };
597
+ const extractLeadingJsonObjects = (line) => {
598
+ const jsonObjects = [];
599
+ let remaining = line.trim();
600
+ while (remaining.startsWith('{')) {
601
+ const jsonObject = extractLeadingJsonObject(remaining);
602
+ if (!jsonObject) {
603
+ break;
604
+ }
605
+ jsonObjects.push(jsonObject);
606
+ remaining = remaining.slice(jsonObject.length).trim();
607
+ }
608
+ return {
609
+ jsonObjects,
610
+ remainder: remaining,
611
+ };
612
+ };
613
+ const parseOpencodeJsonEvent = (event, cwd) => {
614
+ if (event.type === 'text' && event.part?.type === 'text' && event.part.text) {
615
+ const text = event.part.text.trim();
616
+ const phase = event.part.metadata?.openai?.phase;
617
+ return {
618
+ logFragment: text || null,
619
+ responseFragment: text && (!phase || phase === 'final_answer') ? text : null,
620
+ };
621
+ }
622
+ if (event.type === 'reasoning' && event.part?.type === 'reasoning') {
623
+ const text = event.part.text?.trim() ?? '';
624
+ return {
625
+ logFragment: text || null,
626
+ responseFragment: null,
627
+ };
628
+ }
629
+ if (event.type === 'tool_use') {
630
+ const toolFragment = formatOpencodeToolUse(event, cwd);
631
+ return {
632
+ logFragment: toolFragment,
633
+ responseFragment: null,
634
+ };
635
+ }
636
+ if (event.type === 'step_start') {
637
+ return {
638
+ logFragment: formatOpencodeStepTitle(event),
639
+ responseFragment: null,
640
+ };
641
+ }
642
+ if (event.type === 'step_finish') {
643
+ return {
644
+ logFragment: null,
645
+ responseFragment: null,
646
+ };
647
+ }
648
+ return { logFragment: null, responseFragment: null };
649
+ };
650
+ const parseOpencodeJsonLine = (line, cwd) => {
651
+ const trimmed = line.trim();
652
+ if (!trimmed) {
653
+ return { logFragment: null, responseFragment: null };
654
+ }
655
+ const { jsonObjects, remainder } = extractLeadingJsonObjects(trimmed);
656
+ if (jsonObjects.length === 0) {
657
+ if (trimmed.startsWith('{')) {
658
+ return { logFragment: null, responseFragment: null };
659
+ }
660
+ return { logFragment: trimmed, responseFragment: trimmed };
661
+ }
662
+ const logFragments = [];
663
+ const responseFragments = [];
664
+ for (const jsonObject of jsonObjects) {
665
+ let event;
666
+ try {
667
+ event = JSON.parse(jsonObject);
668
+ }
669
+ catch {
670
+ continue;
671
+ }
672
+ const { logFragment, responseFragment } = parseOpencodeJsonEvent(event, cwd);
673
+ if (logFragment !== null) {
674
+ logFragments.push(logFragment);
675
+ }
676
+ if (responseFragment !== null) {
677
+ responseFragments.push(responseFragment);
678
+ }
679
+ }
680
+ if (logFragments.length > 0 || responseFragments.length > 0) {
681
+ return {
682
+ logFragment: logFragments.length > 0 ? logFragments.join('\n\n') : null,
683
+ responseFragment: responseFragments.length > 0 ? responseFragments.join('\n\n') : null,
684
+ };
685
+ }
686
+ if (remainder) {
687
+ return { logFragment: remainder, responseFragment: remainder };
688
+ }
689
+ return { logFragment: null, responseFragment: null };
690
+ };
691
+ const isModelResolutionError = (message) => {
692
+ if (typeof message !== 'string') {
693
+ return false;
694
+ }
695
+ return /ProviderModelNotFoundError|Model not found/i.test(message);
696
+ };
697
+ const isProcessAlive = (proc) => {
698
+ if (!proc) {
699
+ return false;
700
+ }
701
+ return proc.exitCode === null && proc.signalCode === null;
702
+ };
703
+ const killProcess = (proc, signal) => {
704
+ if (!isProcessAlive(proc)) {
705
+ return;
706
+ }
707
+ if (proc.pid && process.platform !== 'win32') {
708
+ try {
709
+ process.kill(-proc.pid, signal);
710
+ return;
711
+ }
712
+ catch (error) {
713
+ if (error.code === 'ESRCH') {
714
+ return;
715
+ }
716
+ }
717
+ }
718
+ try {
719
+ proc.kill(signal);
720
+ }
721
+ catch {
722
+ }
723
+ };
724
+ const waitForProcessExit = async (proc, timeoutMs) => {
725
+ const deadline = Date.now() + timeoutMs;
726
+ await new Promise(resolve => {
727
+ const check = () => {
728
+ if (!isProcessAlive(proc) || Date.now() >= deadline) {
729
+ resolve();
730
+ }
731
+ else {
732
+ setTimeout(check, 100);
733
+ }
734
+ };
735
+ setTimeout(check, 100);
736
+ });
737
+ };
738
+ const untrackBuildProcess = (ticketId, state) => {
739
+ if (activeBuildProcesses.get(ticketId) === state) {
740
+ activeBuildProcesses.delete(ticketId);
741
+ }
742
+ if (state.sessionLookupTimer) {
743
+ clearTimeout(state.sessionLookupTimer);
744
+ state.sessionLookupTimer = null;
745
+ }
746
+ };
747
+ const untrackChatProcess = (chatSessionId, state) => {
748
+ if (activeChatProcesses.get(chatSessionId) === state) {
749
+ activeChatProcesses.delete(chatSessionId);
750
+ }
751
+ state.metadataRefresher?.stop();
752
+ state.metadataRefresher = null;
753
+ };
754
+ export const isBuildAgentRunning = (ticketId) => activeBuildProcesses.has(ticketId);
755
+ export const stopAllBuildAgents = async () => {
756
+ const ticketIds = [...activeBuildProcesses.keys()];
757
+ await Promise.all(ticketIds.map(stopBuildAgent));
758
+ };
759
+ export const stopBuildAgent = async (ticketId) => {
760
+ const state = activeBuildProcesses.get(ticketId);
761
+ if (!state?.child) {
762
+ return false;
763
+ }
764
+ state.stopRequested = true;
765
+ killProcess(state.child, 'SIGTERM');
766
+ await waitForProcessExit(state.child, 5000);
767
+ if (isProcessAlive(state.child)) {
768
+ killProcess(state.child, 'SIGKILL');
769
+ await waitForProcessExit(state.child, 1000);
770
+ }
771
+ return true;
772
+ };
773
+ export const stopChatAgent = async (chatSessionId) => {
774
+ const state = activeChatProcesses.get(chatSessionId);
775
+ if (!state?.child) {
776
+ return false;
777
+ }
778
+ state.stopRequested = true;
779
+ state.metadataRefresher?.stop();
780
+ killProcess(state.child, 'SIGTERM');
781
+ await waitForProcessExit(state.child, 5000);
782
+ if (isProcessAlive(state.child)) {
783
+ killProcess(state.child, 'SIGKILL');
784
+ await waitForProcessExit(state.child, 1000);
785
+ }
786
+ return true;
787
+ };
788
+ export const runAgent = ({ ticketId, tool, prompt, cwd, io, model, variant = null, resumeSessionId = null, sessionField = 'session_id', trackBuildProcess = false, emitSuccessUpdate = true, }) => {
789
+ const resolvedModel = normalizeModelId(model);
790
+ return new Promise((resolve, reject) => {
791
+ let sessionId = null;
792
+ let spawnTimestamp = null;
793
+ let sessionLookupTimer = null;
794
+ let hasResolvedOpencodeSessionId = Boolean(resumeSessionId);
795
+ let settled = false;
796
+ const buildState = trackBuildProcess
797
+ ? { child: null, stopRequested: false, sessionLookupTimer: null }
798
+ : null;
799
+ const settle = (handler) => (value) => {
800
+ if (settled) {
801
+ return;
802
+ }
803
+ settled = true;
804
+ if (sessionLookupTimer) {
805
+ clearTimeout(sessionLookupTimer);
806
+ sessionLookupTimer = null;
807
+ }
808
+ if (trackBuildProcess && buildState) {
809
+ untrackBuildProcess(ticketId, buildState);
810
+ }
811
+ handler(value);
812
+ };
813
+ const resolveOnce = settle(resolve);
814
+ const rejectOnce = settle(reject);
815
+ const attemptOpencodeSessionLookup = () => {
816
+ if (tool !== 'opencode' || hasResolvedOpencodeSessionId || spawnTimestamp === null) {
817
+ return hasResolvedOpencodeSessionId;
818
+ }
819
+ try {
820
+ const session = readOpencodeSession(OPENCODE_SESSION_BY_CREATED_AT_QUERY, spawnTimestamp);
821
+ if (session?.id) {
822
+ hasResolvedOpencodeSessionId = true;
823
+ updateSessionId(ticketId, session.id, sessionField, io);
824
+ }
825
+ }
826
+ catch {
827
+ // Ticket session metadata is best-effort.
828
+ }
829
+ return hasResolvedOpencodeSessionId;
830
+ };
831
+ const [cmd, args] = tool === 'claude'
832
+ ? (() => {
833
+ sessionId = resumeSessionId || randomUUID();
834
+ return [
835
+ 'claude',
836
+ [
837
+ '--dangerously-skip-permissions',
838
+ '--print',
839
+ '--verbose',
840
+ '--output-format',
841
+ 'stream-json',
842
+ resumeSessionId ? '--resume' : '--session-id',
843
+ sessionId,
844
+ prompt,
845
+ ]
846
+ ];
847
+ })()
848
+ : ['opencode', ['run', '--format', 'json', '--thinking', ...(resumeSessionId ? ['--session', resumeSessionId] : []), ...(resolvedModel ? ['--model', resolvedModel] : []), ...(variant ? ['--variant', variant] : []), prompt]];
849
+ updateStatus(ticketId, 'running', null, io);
850
+ if (tool === 'claude' && sessionId) {
851
+ updateSessionId(ticketId, sessionId, sessionField, io);
852
+ }
853
+ if (tool === 'opencode') {
854
+ spawnTimestamp = Date.now();
855
+ }
856
+ const child = spawn(cmd, args, {
857
+ cwd,
858
+ stdio: ['ignore', 'pipe', 'pipe'],
859
+ env: process.env,
860
+ detached: process.platform !== 'win32',
861
+ });
862
+ if (trackBuildProcess && buildState) {
863
+ buildState.child = child;
864
+ activeBuildProcesses.set(ticketId, buildState);
865
+ }
866
+ if (tool === 'claude') {
867
+ const logLines = [];
868
+ const responseLines = [];
869
+ let remainder = '';
870
+ const stderrChunks = [];
871
+ const progressUpdater = createThrottledProgressUpdater(() => {
872
+ updateStatus(ticketId, 'running', logLines.join('\n\n'), io, responseLines.join('\n\n'));
873
+ });
874
+ child.stdout?.on('data', (chunk) => {
875
+ const text = remainder + chunk.toString();
876
+ const lines = text.split('\n');
877
+ remainder = lines.pop() ?? '';
878
+ for (const line of lines) {
879
+ const { logFragment, responseFragment } = parseClaudeStreamJsonLine(line, cwd);
880
+ if (logFragment !== null) {
881
+ logLines.push(logFragment);
882
+ }
883
+ if (responseFragment !== null) {
884
+ responseLines.push(responseFragment);
885
+ }
886
+ if (logFragment !== null || responseFragment !== null) {
887
+ progressUpdater.schedule();
888
+ }
889
+ }
890
+ });
891
+ child.stderr?.on('data', (chunk) => stderrChunks.push(chunk));
892
+ child.on('close', code => {
893
+ progressUpdater.cancel();
894
+ if (remainder.trim()) {
895
+ const { logFragment, responseFragment } = parseClaudeStreamJsonLine(remainder, cwd);
896
+ if (logFragment !== null) {
897
+ logLines.push(logFragment);
898
+ }
899
+ if (responseFragment !== null) {
900
+ responseLines.push(responseFragment);
901
+ }
902
+ }
903
+ const errOutput = Buffer.concat(stderrChunks).toString();
904
+ const finalLog = logLines.join('\n\n') || errOutput;
905
+ const finalResponse = responseLines.join('\n\n') || errOutput;
906
+ const modelError = isModelResolutionError(finalLog);
907
+ if (buildState?.stopRequested) {
908
+ updateStatus(ticketId, 'stopped', finalLog, io, finalResponse);
909
+ resolveOnce(finalResponse);
910
+ return;
911
+ }
912
+ if (code === 0 && !modelError) {
913
+ updateStatus(ticketId, 'done', finalLog, io, finalResponse, { emit: emitSuccessUpdate });
914
+ resolveOnce(finalResponse);
915
+ return;
916
+ }
917
+ updateStatus(ticketId, 'error', finalLog + (errOutput ? '\n\n' + errOutput : ''), io, finalResponse);
918
+ rejectOnce(new Error(`Agent exited with code ${code ?? 'null'}\n${finalLog}`));
919
+ });
920
+ child.on('error', err => {
921
+ progressUpdater.cancel();
922
+ if (buildState?.stopRequested) {
923
+ updateStatus(ticketId, 'stopped', null, io);
924
+ resolveOnce(null);
925
+ return;
926
+ }
927
+ updateStatus(ticketId, 'error', getErrorMessage(err), io);
928
+ rejectOnce(err instanceof Error ? err : new Error(getErrorMessage(err)));
929
+ });
930
+ return;
931
+ }
932
+ const logLines = [];
933
+ const responseLines = [];
934
+ let remainder = '';
935
+ const stderrChunks = [];
936
+ const progressUpdater = createThrottledProgressUpdater(() => {
937
+ updateStatus(ticketId, 'running', logLines.join('\n\n'), io, responseLines.join('\n\n'));
938
+ });
939
+ child.stdout?.on('data', (chunk) => {
940
+ const text = remainder + chunk.toString();
941
+ const lines = text.split('\n');
942
+ remainder = lines.pop() ?? '';
943
+ for (const line of lines) {
944
+ const { logFragment, responseFragment } = parseOpencodeJsonLine(line);
945
+ if (logFragment !== null) {
946
+ logLines.push(logFragment);
947
+ }
948
+ if (responseFragment !== null) {
949
+ responseLines.push(responseFragment);
950
+ }
951
+ if (logFragment !== null || responseFragment !== null) {
952
+ progressUpdater.schedule();
953
+ }
954
+ }
955
+ });
956
+ child.stderr?.on('data', (chunk) => stderrChunks.push(chunk));
957
+ if (tool === 'opencode' && !resumeSessionId) {
958
+ sessionLookupTimer = setTimeout(() => {
959
+ attemptOpencodeSessionLookup();
960
+ sessionLookupTimer = null;
961
+ }, 1500);
962
+ if (trackBuildProcess && buildState) {
963
+ buildState.sessionLookupTimer = sessionLookupTimer;
964
+ }
965
+ }
966
+ child.on('close', code => {
967
+ progressUpdater.cancel();
968
+ const resolvedSessionOnClose = attemptOpencodeSessionLookup();
969
+ if (sessionLookupTimer && (resolvedSessionOnClose || hasResolvedOpencodeSessionId)) {
970
+ clearTimeout(sessionLookupTimer);
971
+ sessionLookupTimer = null;
972
+ }
973
+ if (remainder.trim()) {
974
+ const { logFragment, responseFragment } = parseOpencodeJsonLine(remainder);
975
+ if (logFragment !== null) {
976
+ logLines.push(logFragment);
977
+ }
978
+ if (responseFragment !== null) {
979
+ responseLines.push(responseFragment);
980
+ }
981
+ }
982
+ const stderrOutput = Buffer.concat(stderrChunks).toString();
983
+ const log = logLines.join('\n\n') || stderrOutput;
984
+ const finalResponse = responseLines.join('\n\n') || stderrOutput;
985
+ const modelError = isModelResolutionError(log);
986
+ if (buildState?.stopRequested) {
987
+ updateStatus(ticketId, 'stopped', log, io, finalResponse);
988
+ resolveOnce(finalResponse);
989
+ return;
990
+ }
991
+ if (code === 0 && !modelError) {
992
+ updateStatus(ticketId, 'done', log, io, finalResponse, { emit: emitSuccessUpdate });
993
+ resolveOnce(finalResponse);
994
+ return;
995
+ }
996
+ updateStatus(ticketId, 'error', log, io, finalResponse);
997
+ rejectOnce(new Error(`Agent exited with code ${code ?? 'null'}\n${log}`));
998
+ });
999
+ child.on('error', err => {
1000
+ progressUpdater.cancel();
1001
+ if (sessionLookupTimer && hasResolvedOpencodeSessionId) {
1002
+ clearTimeout(sessionLookupTimer);
1003
+ sessionLookupTimer = null;
1004
+ }
1005
+ if (buildState?.stopRequested) {
1006
+ updateStatus(ticketId, 'stopped', null, io);
1007
+ resolveOnce(null);
1008
+ return;
1009
+ }
1010
+ updateStatus(ticketId, 'error', getErrorMessage(err), io);
1011
+ rejectOnce(err instanceof Error ? err : new Error(getErrorMessage(err)));
1012
+ });
1013
+ });
1014
+ };
1015
+ export const runChatAgent = ({ chatSessionId, tool, prompt, cwd, io, model, variant = null, resumeSessionId = null, opencodeAgentMode = 'build', }) => {
1016
+ const resolvedModel = normalizeModelId(model);
1017
+ return new Promise((resolve, reject) => {
1018
+ let sessionId = null;
1019
+ let spawnTimestamp = null;
1020
+ let opencodeMetadataRefresher = null;
1021
+ let settled = false;
1022
+ let usageSnapshot = EMPTY_CHAT_USAGE_SNAPSHOT;
1023
+ const chatState = {
1024
+ child: null,
1025
+ stopRequested: false,
1026
+ metadataRefresher: null,
1027
+ };
1028
+ const settle = (handler) => (value) => {
1029
+ if (settled) {
1030
+ return;
1031
+ }
1032
+ settled = true;
1033
+ opencodeMetadataRefresher?.stop();
1034
+ untrackChatProcess(chatSessionId, chatState);
1035
+ handler(value);
1036
+ };
1037
+ const resolveOnce = settle(resolve);
1038
+ const rejectOnce = settle(reject);
1039
+ const [cmd, args] = tool === 'claude'
1040
+ ? (() => {
1041
+ sessionId = resumeSessionId || randomUUID();
1042
+ return [
1043
+ 'claude',
1044
+ [
1045
+ '--dangerously-skip-permissions',
1046
+ '--print',
1047
+ '--verbose',
1048
+ '--output-format',
1049
+ 'stream-json',
1050
+ resumeSessionId ? '--resume' : '--session-id',
1051
+ sessionId,
1052
+ prompt,
1053
+ ]
1054
+ ];
1055
+ })()
1056
+ : ['opencode', ['run', ...(opencodeAgentMode === 'plan' ? ['--agent', 'plan'] : []), '--format', 'json', ...(opencodeAgentMode === 'build' ? ['--thinking'] : []), ...(resumeSessionId ? ['--session', resumeSessionId] : []), ...(resolvedModel ? ['--model', resolvedModel] : []), ...(variant ? ['--variant', variant] : []), prompt]];
1057
+ updateChatSessionStatus(chatSessionId, 'running', null, io);
1058
+ if (tool === 'claude' && sessionId) {
1059
+ updateChatSessionId(chatSessionId, tool, sessionId, io);
1060
+ }
1061
+ if (tool === 'opencode') {
1062
+ spawnTimestamp = Date.now();
1063
+ opencodeMetadataRefresher = createOpencodeMetadataRefresher({
1064
+ chatSessionId,
1065
+ io,
1066
+ spawnTimestamp,
1067
+ initialSessionId: resumeSessionId,
1068
+ });
1069
+ chatState.metadataRefresher = opencodeMetadataRefresher;
1070
+ if (resumeSessionId) {
1071
+ const result = opencodeMetadataRefresher.refresh();
1072
+ if (!result.hasResolvedTitle) {
1073
+ opencodeMetadataRefresher.scheduleRetry();
1074
+ }
1075
+ }
1076
+ }
1077
+ const child = spawn(cmd, args, {
1078
+ cwd,
1079
+ stdio: ['ignore', 'pipe', 'pipe'],
1080
+ env: process.env,
1081
+ detached: process.platform !== 'win32',
1082
+ });
1083
+ chatState.child = child;
1084
+ activeChatProcesses.set(chatSessionId, chatState);
1085
+ if (tool === 'claude') {
1086
+ const logLines = [];
1087
+ const responseLines = [];
1088
+ let remainder = '';
1089
+ const stderrChunks = [];
1090
+ const progressUpdater = createThrottledProgressUpdater(() => {
1091
+ updateChatSessionStatus(chatSessionId, 'running', logLines.join('\n\n'), io, responseLines.join('\n\n'));
1092
+ });
1093
+ child.stdout?.on('data', (chunk) => {
1094
+ const text = remainder + chunk.toString();
1095
+ const lines = text.split('\n');
1096
+ remainder = lines.pop() ?? '';
1097
+ for (const line of lines) {
1098
+ const { logFragment, responseFragment } = parseClaudeStreamJsonLine(line);
1099
+ usageSnapshot = mergeUsageFromJsonLine(usageSnapshot, line);
1100
+ if (logFragment !== null) {
1101
+ logLines.push(logFragment);
1102
+ }
1103
+ if (responseFragment !== null) {
1104
+ responseLines.push(responseFragment);
1105
+ }
1106
+ if (logFragment !== null || responseFragment !== null) {
1107
+ progressUpdater.schedule();
1108
+ }
1109
+ }
1110
+ });
1111
+ child.stderr?.on('data', (chunk) => stderrChunks.push(chunk));
1112
+ child.on('close', code => {
1113
+ progressUpdater.cancel();
1114
+ if (remainder.trim()) {
1115
+ usageSnapshot = mergeUsageFromJsonLine(usageSnapshot, remainder);
1116
+ const { logFragment, responseFragment } = parseClaudeStreamJsonLine(remainder);
1117
+ if (logFragment !== null) {
1118
+ logLines.push(logFragment);
1119
+ }
1120
+ if (responseFragment !== null) {
1121
+ responseLines.push(responseFragment);
1122
+ }
1123
+ }
1124
+ const errOutput = Buffer.concat(stderrChunks).toString();
1125
+ const finalLog = logLines.join('\n\n') || errOutput;
1126
+ const finalResponse = responseLines.join('\n\n') || errOutput;
1127
+ if (chatState.stopRequested) {
1128
+ const preservedOutput = getPreservedChatOutput({
1129
+ chatSessionId,
1130
+ log: finalLog,
1131
+ response: finalResponse,
1132
+ });
1133
+ updateChatSessionStatus(chatSessionId, 'running', preservedOutput.log, io, preservedOutput.response);
1134
+ resolveOnce({
1135
+ completionStatus: 'stopped',
1136
+ finalResponse: preservedOutput.response,
1137
+ usageSnapshot,
1138
+ });
1139
+ return;
1140
+ }
1141
+ const modelError = isModelResolutionError(finalLog);
1142
+ if (code === 0 && !modelError) {
1143
+ const finalOutput = getPreservedChatOutput({
1144
+ chatSessionId,
1145
+ log: finalLog,
1146
+ response: finalResponse,
1147
+ });
1148
+ updateChatSessionStatus(chatSessionId, 'running', finalOutput.log, io, finalOutput.response);
1149
+ resolveOnce({
1150
+ completionStatus: 'done',
1151
+ finalResponse: finalOutput.response,
1152
+ usageSnapshot,
1153
+ });
1154
+ return;
1155
+ }
1156
+ updateChatSessionStatus(chatSessionId, 'error', finalLog + (errOutput ? '\n\n' + errOutput : ''), io, finalResponse);
1157
+ rejectOnce(new Error(`Agent exited with code ${code ?? 'null'}\n${finalLog}`));
1158
+ });
1159
+ child.on('error', err => {
1160
+ progressUpdater.cancel();
1161
+ if (chatState.stopRequested) {
1162
+ const log = logLines.join('\n\n') || null;
1163
+ const response = responseLines.join('\n\n') || null;
1164
+ const preservedOutput = getPreservedChatOutput({
1165
+ chatSessionId,
1166
+ log,
1167
+ response,
1168
+ });
1169
+ updateChatSessionStatus(chatSessionId, 'running', preservedOutput.log, io, preservedOutput.response);
1170
+ resolveOnce({
1171
+ completionStatus: 'stopped',
1172
+ finalResponse: preservedOutput.response,
1173
+ usageSnapshot,
1174
+ });
1175
+ return;
1176
+ }
1177
+ updateChatSessionStatus(chatSessionId, 'error', getErrorMessage(err), io);
1178
+ rejectOnce(err instanceof Error ? err : new Error(getErrorMessage(err)));
1179
+ });
1180
+ return;
1181
+ }
1182
+ const logLines = [];
1183
+ const responseLines = [];
1184
+ let remainder = '';
1185
+ const stderrChunks = [];
1186
+ const progressUpdater = createThrottledProgressUpdater(() => {
1187
+ updateChatSessionStatus(chatSessionId, 'running', logLines.join('\n\n'), io, responseLines.join('\n\n'));
1188
+ });
1189
+ child.stdout?.on('data', (chunk) => {
1190
+ const text = remainder + chunk.toString();
1191
+ const lines = text.split('\n');
1192
+ remainder = lines.pop() ?? '';
1193
+ for (const line of lines) {
1194
+ usageSnapshot = mergeUsageFromJsonLine(usageSnapshot, line);
1195
+ const { logFragment, responseFragment } = parseOpencodeJsonLine(line, cwd);
1196
+ if (logFragment !== null) {
1197
+ logLines.push(logFragment);
1198
+ }
1199
+ if (responseFragment !== null) {
1200
+ responseLines.push(responseFragment);
1201
+ }
1202
+ if (logFragment !== null || responseFragment !== null) {
1203
+ progressUpdater.schedule();
1204
+ }
1205
+ }
1206
+ });
1207
+ child.stderr?.on('data', (chunk) => stderrChunks.push(chunk));
1208
+ if (!resumeSessionId) {
1209
+ opencodeMetadataRefresher?.scheduleRetry();
1210
+ }
1211
+ child.on('close', code => {
1212
+ progressUpdater.cancel();
1213
+ if (remainder.trim()) {
1214
+ usageSnapshot = mergeUsageFromJsonLine(usageSnapshot, remainder);
1215
+ const { logFragment, responseFragment } = parseOpencodeJsonLine(remainder, cwd);
1216
+ if (logFragment !== null) {
1217
+ logLines.push(logFragment);
1218
+ }
1219
+ if (responseFragment !== null) {
1220
+ responseLines.push(responseFragment);
1221
+ }
1222
+ }
1223
+ const stderrOutput = Buffer.concat(stderrChunks).toString();
1224
+ const log = logLines.join('\n\n') || stderrOutput;
1225
+ const finalResponse = responseLines.join('\n\n') || stderrOutput;
1226
+ const preservedOutput = chatState.stopRequested
1227
+ ? getPreservedChatOutput({
1228
+ chatSessionId,
1229
+ log,
1230
+ response: finalResponse,
1231
+ })
1232
+ : null;
1233
+ const metadataResult = opencodeMetadataRefresher?.refresh();
1234
+ const resolvedOpencodeSessionId = metadataResult?.sessionId
1235
+ ?? preservedOutput?.opencodeSessionId
1236
+ ?? getChatSession(chatSessionId)?.opencode_session_id
1237
+ ?? resumeSessionId;
1238
+ if (chatState.stopRequested) {
1239
+ if (resolvedOpencodeSessionId) {
1240
+ usageSnapshot = resolveOpencodeUsageSnapshot({
1241
+ current: usageSnapshot,
1242
+ sessionId: resolvedOpencodeSessionId,
1243
+ spawnTimestamp,
1244
+ });
1245
+ }
1246
+ updateChatSessionStatus(chatSessionId, 'running', preservedOutput?.log ?? null, io, preservedOutput?.response ?? null);
1247
+ resolveOnce({
1248
+ completionStatus: 'stopped',
1249
+ finalResponse: preservedOutput?.response ?? null,
1250
+ usageSnapshot,
1251
+ });
1252
+ return;
1253
+ }
1254
+ const modelError = isModelResolutionError(log);
1255
+ if (code === 0 && !modelError) {
1256
+ const finalOutput = getPreservedChatOutput({
1257
+ chatSessionId,
1258
+ log,
1259
+ response: finalResponse,
1260
+ });
1261
+ if (resolvedOpencodeSessionId && spawnTimestamp !== null) {
1262
+ usageSnapshot = resolveOpencodeUsageSnapshot({
1263
+ current: usageSnapshot,
1264
+ sessionId: resolvedOpencodeSessionId,
1265
+ spawnTimestamp,
1266
+ });
1267
+ }
1268
+ updateChatSessionStatus(chatSessionId, 'running', finalOutput.log, io, finalOutput.response);
1269
+ resolveOnce({
1270
+ completionStatus: 'done',
1271
+ finalResponse: finalOutput.response,
1272
+ usageSnapshot,
1273
+ });
1274
+ return;
1275
+ }
1276
+ updateChatSessionStatus(chatSessionId, 'error', log, io, finalResponse);
1277
+ rejectOnce(new Error(`Agent exited with code ${code ?? 'null'}\n${log}`));
1278
+ });
1279
+ child.on('error', err => {
1280
+ progressUpdater.cancel();
1281
+ if (chatState.stopRequested) {
1282
+ const log = logLines.join('\n\n') || null;
1283
+ const response = responseLines.join('\n\n') || null;
1284
+ const preservedOutput = getPreservedChatOutput({
1285
+ chatSessionId,
1286
+ log,
1287
+ response,
1288
+ });
1289
+ updateChatSessionStatus(chatSessionId, 'running', preservedOutput.log, io, preservedOutput.response);
1290
+ resolveOnce({
1291
+ completionStatus: 'stopped',
1292
+ finalResponse: preservedOutput.response,
1293
+ usageSnapshot,
1294
+ });
1295
+ return;
1296
+ }
1297
+ updateChatSessionStatus(chatSessionId, 'error', getErrorMessage(err), io);
1298
+ rejectOnce(err instanceof Error ? err : new Error(getErrorMessage(err)));
1299
+ });
1300
+ });
1301
+ };
1302
+ //# sourceMappingURL=agent.js.map