@straiffi/archon 1.3.1 → 1.3.3

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 (94) hide show
  1. package/dist/client/assets/{TestsDialog-DelI4dOJ.js → TestsDialog-XvTa9G9O.js} +1 -1
  2. package/dist/client/assets/{architectureDiagram-3BPJPVTR-Cn7GlwQg.js → architectureDiagram-3BPJPVTR-DGA6BOBZ.js} +1 -1
  3. package/dist/client/assets/{blockDiagram-GPEHLZMM-Dbt3i35a.js → blockDiagram-GPEHLZMM-DxNTme2C.js} +1 -1
  4. package/dist/client/assets/{c4Diagram-AAUBKEIU-BCNsqu0u.js → c4Diagram-AAUBKEIU-BVtVwcm1.js} +1 -1
  5. package/dist/client/assets/channel-DesU8BSU.js +1 -0
  6. package/dist/client/assets/{chunk-2J33WTMH-B9c8AzpT.js → chunk-2J33WTMH-kjE402ke.js} +1 -1
  7. package/dist/client/assets/{chunk-3OPIFGDE-D-qpTyaE.js → chunk-3OPIFGDE-ZhKa-iUJ.js} +1 -1
  8. package/dist/client/assets/{chunk-5ZQYHXKU-bBV3HPCk.js → chunk-5ZQYHXKU-DD-CFUFz.js} +1 -1
  9. package/dist/client/assets/{chunk-727SXJPM-InkxsEhY.js → chunk-727SXJPM-tYKnuL4a.js} +1 -1
  10. package/dist/client/assets/{chunk-AQP2D5EJ-DZZL694W.js → chunk-AQP2D5EJ-DTWw4ohg.js} +1 -1
  11. package/dist/client/assets/{chunk-CSCIHK7Q-BuAgybUd.js → chunk-CSCIHK7Q-nqMgQFJj.js} +1 -1
  12. package/dist/client/assets/{chunk-KSCS5N6A-B9fgatSq.js → chunk-KSCS5N6A-CG7nuzWr.js} +1 -1
  13. package/dist/client/assets/{chunk-L5ZTLDWV-L-7z2xOe.js → chunk-L5ZTLDWV-8_YeOc_Q.js} +1 -1
  14. package/dist/client/assets/{chunk-LZXEDZCA-hrScZnrx.js → chunk-LZXEDZCA-Baibx6Tw.js} +2 -2
  15. package/dist/client/assets/{chunk-ND2GUHAM-d2HjiRzD.js → chunk-ND2GUHAM-B6WTUT1I.js} +1 -1
  16. package/dist/client/assets/{chunk-NZK2D7GU-C6cn05ZM.js → chunk-NZK2D7GU-D_9rHSdg.js} +1 -1
  17. package/dist/client/assets/{chunk-O5CBEL6O-BAjbtmB_.js → chunk-O5CBEL6O-C7gZvogK.js} +1 -1
  18. package/dist/client/assets/{chunk-WU5MYG2G-By4sIjXN.js → chunk-WU5MYG2G-5mFJEt4y.js} +1 -1
  19. package/dist/client/assets/classDiagram-4FO5ZUOK-BXN2p5zx.js +1 -0
  20. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-C4KSVWqM.js +1 -0
  21. package/dist/client/assets/{dagre-BM42HDAG-_iu0KXos.js → dagre-BM42HDAG-C4bt9HKe.js} +1 -1
  22. package/dist/client/assets/{diagram-2AECGRRQ-DyBAakP6.js → diagram-2AECGRRQ-ChyO8KAx.js} +1 -1
  23. package/dist/client/assets/{diagram-5GNKFQAL-xD8YOWp-.js → diagram-5GNKFQAL-Pv5v9f8J.js} +1 -1
  24. package/dist/client/assets/{diagram-KO2AKTUF-BQDiusQc.js → diagram-KO2AKTUF-D01uZAEx.js} +1 -1
  25. package/dist/client/assets/{diagram-LMA3HP47-BRu1DXcK.js → diagram-LMA3HP47-SgsZHK1n.js} +1 -1
  26. package/dist/client/assets/{diagram-OG6HWLK6-C3A1wt8S.js → diagram-OG6HWLK6-NoXWNvb9.js} +1 -1
  27. package/dist/client/assets/{erDiagram-TEJ5UH35-C_fWil4q.js → erDiagram-TEJ5UH35-CILc_dlJ.js} +1 -1
  28. package/dist/client/assets/{flowDiagram-I6XJVG4X-CKsgXWad.js → flowDiagram-I6XJVG4X-DrPbsQ2p.js} +1 -1
  29. package/dist/client/assets/{ganttDiagram-6RSMTGT7-DRhmLmdi.js → ganttDiagram-6RSMTGT7-CZJImvrO.js} +1 -1
  30. package/dist/client/assets/{gitGraphDiagram-PVQCEYII-C34Vr5C1.js → gitGraphDiagram-PVQCEYII-DOV6sM0r.js} +1 -1
  31. package/dist/client/assets/index-BA7rzT42.css +2 -0
  32. package/dist/client/assets/{index-DENrWubs.js → index-BxKx-nzY.js} +102 -102
  33. package/dist/client/assets/{infoDiagram-5YYISTIA-Bkbr_hQH.js → infoDiagram-5YYISTIA-CrlDAG9K.js} +1 -1
  34. package/dist/client/assets/{ishikawaDiagram-YF4QCWOH-CEXsy2hU.js → ishikawaDiagram-YF4QCWOH-BlzVoHRp.js} +1 -1
  35. package/dist/client/assets/{journeyDiagram-JHISSGLW-RuKnL83f.js → journeyDiagram-JHISSGLW-CzCVPcHP.js} +1 -1
  36. package/dist/client/assets/{kanban-definition-UN3LZRKU-DrlhCmkA.js → kanban-definition-UN3LZRKU-BL53KDiy.js} +1 -1
  37. package/dist/client/assets/{line-CGdxyPh9.js → line-BVzc-l73.js} +1 -1
  38. package/dist/client/assets/{mermaid-parser.core-CbzywbCW.js → mermaid-parser.core-Bex-oQWq.js} +1 -1
  39. package/dist/client/assets/{mermaid.core-09FBpi2R.js → mermaid.core-BtD6cM5K.js} +3 -3
  40. package/dist/client/assets/{mindmap-definition-RKZ34NQL-CXrsI5b1.js → mindmap-definition-RKZ34NQL-jl3kudNu.js} +1 -1
  41. package/dist/client/assets/{pieDiagram-4H26LBE5-HbOpep9N.js → pieDiagram-4H26LBE5-CH992aFg.js} +1 -1
  42. package/dist/client/assets/{quadrantDiagram-W4KKPZXB-BWw-ZWMn.js → quadrantDiagram-W4KKPZXB-CzXkWlOp.js} +1 -1
  43. package/dist/client/assets/{requirementDiagram-4Y6WPE33-CpscsQdI.js → requirementDiagram-4Y6WPE33-DJg-bHHs.js} +1 -1
  44. package/dist/client/assets/{sankeyDiagram-5OEKKPKP-BoV5tyAf.js → sankeyDiagram-5OEKKPKP-DitY__t-.js} +1 -1
  45. package/dist/client/assets/{sequenceDiagram-3UESZ5HK-CBcDRtjR.js → sequenceDiagram-3UESZ5HK-DRPu-NkL.js} +1 -1
  46. package/dist/client/assets/{stateDiagram-AJRCARHV-Df9lcs70.js → stateDiagram-AJRCARHV--Q4rPa1d.js} +1 -1
  47. package/dist/client/assets/stateDiagram-v2-BHNVJYJU-D7AQwkBW.js +1 -0
  48. package/dist/client/assets/{timeline-definition-PNZ67QCA-BFv1VICg.js → timeline-definition-PNZ67QCA-CEWh_YRb.js} +1 -1
  49. package/dist/client/assets/{vennDiagram-CIIHVFJN-DPRtn-XH.js → vennDiagram-CIIHVFJN-CeH6-kWE.js} +1 -1
  50. package/dist/client/assets/{wardleyDiagram-YWT4CUSO-CDiint0P.js → wardleyDiagram-YWT4CUSO-DUHb2UPZ.js} +1 -1
  51. package/dist/client/assets/{xychartDiagram-2RQKCTM6-ELyFdgmG.js → xychartDiagram-2RQKCTM6-DP2fabhc.js} +1 -1
  52. package/dist/client/index.html +2 -2
  53. package/dist/server/db.js +8 -0
  54. package/dist/server/db.js.map +1 -1
  55. package/dist/server/index.js +11 -2
  56. package/dist/server/index.js.map +1 -1
  57. package/dist/server/lib/agent.js +110 -20
  58. package/dist/server/lib/agent.js.map +1 -1
  59. package/dist/server/lib/chatMessages.js +6 -6
  60. package/dist/server/lib/chatMessages.js.map +1 -1
  61. package/dist/server/lib/chatOperations.js +13 -2
  62. package/dist/server/lib/chatOperations.js.map +1 -1
  63. package/dist/server/lib/chats.js +32 -4
  64. package/dist/server/lib/chats.js.map +1 -1
  65. package/dist/server/lib/specArtifact.js +17 -0
  66. package/dist/server/lib/specArtifact.js.map +1 -1
  67. package/dist/server/lib/specDraft.js +19 -0
  68. package/dist/server/lib/specDraft.js.map +1 -1
  69. package/dist/server/lib/specMessages.js +8 -6
  70. package/dist/server/lib/specMessages.js.map +1 -1
  71. package/dist/server/lib/ticketBatch.js +1 -0
  72. package/dist/server/lib/ticketBatch.js.map +1 -1
  73. package/dist/server/lib/ticketMessages.js +6 -6
  74. package/dist/server/lib/ticketMessages.js.map +1 -1
  75. package/dist/server/workers/build.js +2 -1
  76. package/dist/server/workers/build.js.map +1 -1
  77. package/dist/server/workers/chat.js +2 -0
  78. package/dist/server/workers/chat.js.map +1 -1
  79. package/dist/server/workers/followUp.js +2 -1
  80. package/dist/server/workers/followUp.js.map +1 -1
  81. package/dist/server/workers/plan.js +34 -6
  82. package/dist/server/workers/plan.js.map +1 -1
  83. package/dist/server/workers/planFollowUp.js +3 -2
  84. package/dist/server/workers/planFollowUp.js.map +1 -1
  85. package/dist/server/workers/review.js +2 -1
  86. package/dist/server/workers/review.js.map +1 -1
  87. package/dist/server/workers/specSession.js +27 -3
  88. package/dist/server/workers/specSession.js.map +1 -1
  89. package/package.json +1 -1
  90. package/dist/client/assets/channel-BmkYY3c1.js +0 -1
  91. package/dist/client/assets/classDiagram-4FO5ZUOK-B9B9lIAo.js +0 -1
  92. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-DXR72HYe.js +0 -1
  93. package/dist/client/assets/index-iElL-crz.css +0 -2
  94. package/dist/client/assets/stateDiagram-v2-BHNVJYJU-FIWIctC4.js +0 -1
@@ -182,8 +182,13 @@ const getErrorMessage = (error) => {
182
182
  };
183
183
  export const formatAgentErrorForTranscript = (error) => {
184
184
  const rawMessage = getErrorMessage(error);
185
- const cleaned = rawMessage.replace(/^Agent exited with code \S+\n/, '').trim();
186
- return cleaned || rawMessage;
185
+ const prefixMatch = rawMessage.match(/^(Agent exited with code \S+)\n(.*)/s);
186
+ if (!prefixMatch) {
187
+ return rawMessage;
188
+ }
189
+ const [, prefix, logContent] = prefixMatch;
190
+ const trimmedLog = logContent.trim();
191
+ return trimmedLog || prefix;
187
192
  };
188
193
  const getPersistedChatState = (chatSessionId) => {
189
194
  const row = db.prepare(GET_PERSISTED_CHAT_STATE_QUERY).get(chatSessionId);
@@ -915,9 +920,14 @@ const parseOpencodeJsonEvent = (event, cwd) => {
915
920
  };
916
921
  }
917
922
  if (event.type === 'error') {
918
- const text = event.part?.text?.trim()
919
- || (typeof event.error === 'string' ? event.error.trim() : '')
920
- || '';
923
+ const errorText = typeof event.error === 'string'
924
+ ? event.error.trim()
925
+ : event.error && typeof event.error === 'object'
926
+ ? (event.error.data?.message?.trim()
927
+ || event.error.message?.trim()
928
+ || JSON.stringify(event.error))
929
+ : '';
930
+ const text = event.part?.text?.trim() || errorText || '';
921
931
  if (text) {
922
932
  return {
923
933
  logFragment: `**Error:** ${text}`,
@@ -968,6 +978,18 @@ const parseOpencodeJsonLine = (line, cwd) => {
968
978
  }
969
979
  return { logFragment: null, responseFragment: null };
970
980
  };
981
+ const buildResponseTiming = ({ startTime, firstFinalAnswerTime, isStopped, }) => {
982
+ if (!startTime) {
983
+ return null;
984
+ }
985
+ const totalMs = Date.now() - startTime;
986
+ if (isStopped || firstFinalAnswerTime === null) {
987
+ return { totalMs };
988
+ }
989
+ const thinkingMs = firstFinalAnswerTime - startTime;
990
+ const responseMs = Date.now() - firstFinalAnswerTime;
991
+ return { totalMs, thinkingMs, responseMs };
992
+ };
971
993
  const isModelResolutionError = (message) => {
972
994
  if (typeof message !== 'string') {
973
995
  return false;
@@ -1095,6 +1117,8 @@ export const runAgent = ({ ticketId, tool, prompt, cwd, io, model, variant = nul
1095
1117
  let sessionLookupTimer = null;
1096
1118
  let hasResolvedOpencodeSessionId = Boolean(resumeSessionId);
1097
1119
  let settled = false;
1120
+ let startTime = null;
1121
+ let firstFinalAnswerTime = null;
1098
1122
  const buildState = trackBuildProcess
1099
1123
  ? { child: null, stopRequested: false, sessionLookupTimer: null }
1100
1124
  : null;
@@ -1161,7 +1185,7 @@ export const runAgent = ({ ticketId, tool, prompt, cwd, io, model, variant = nul
1161
1185
  ]
1162
1186
  ];
1163
1187
  })()
1164
- : ['opencode', ['run', ...(opencodeAgentMode === 'plan' ? ['--agent', 'plan'] : []), '--format', 'json', ...(opencodeAgentMode !== 'plan' ? ['--thinking'] : []), ...(resumeSessionId ? ['--session', resumeSessionId] : []), ...(resolvedModel ? ['--model', resolvedModel] : []), ...(variant ? ['--variant', variant] : []), prompt]];
1188
+ : ['opencode', ['run', ...(opencodeAgentMode === 'plan' ? ['--agent', 'plan'] : []), '--format', 'json', '--thinking', ...(resumeSessionId ? ['--session', resumeSessionId] : []), ...(resolvedModel ? ['--model', resolvedModel] : []), ...(variant ? ['--variant', variant] : []), prompt]];
1165
1189
  updateStatus(ticketId, 'running', null, io);
1166
1190
  if (tool === 'claude' && sessionId) {
1167
1191
  updateSessionId(ticketId, sessionId, sessionField, io);
@@ -1179,6 +1203,7 @@ export const runAgent = ({ ticketId, tool, prompt, cwd, io, model, variant = nul
1179
1203
  buildState.child = child;
1180
1204
  activeBuildProcesses.set(ticketId, buildState);
1181
1205
  }
1206
+ startTime = Date.now();
1182
1207
  if (tool === 'claude') {
1183
1208
  const logLines = [];
1184
1209
  const responseLines = [];
@@ -1220,24 +1245,25 @@ export const runAgent = ({ ticketId, tool, prompt, cwd, io, model, variant = nul
1220
1245
  const finalLog = logLines.join('\n\n') || errOutput;
1221
1246
  const finalResponse = responseLines.join('\n\n') || errOutput;
1222
1247
  const modelError = isModelResolutionError(finalLog);
1248
+ const timing = startTime ? { totalMs: Date.now() - startTime } : null;
1223
1249
  if (buildState?.stopRequested) {
1224
1250
  updateStatus(ticketId, 'stopped', finalLog, io, finalResponse);
1225
- resolveOnce(finalResponse);
1251
+ resolveOnce({ response: finalResponse, responseTiming: timing });
1226
1252
  return;
1227
1253
  }
1228
1254
  if (code === 0 && !modelError) {
1229
1255
  updateStatus(ticketId, 'done', finalLog, io, finalResponse, { emit: emitSuccessUpdate });
1230
- resolveOnce(finalResponse);
1256
+ resolveOnce({ response: finalResponse, responseTiming: timing });
1231
1257
  return;
1232
1258
  }
1233
1259
  updateStatus(ticketId, 'error', finalLog + (errOutput ? '\n\n' + errOutput : ''), io, finalResponse);
1234
- rejectOnce(new Error(`Agent exited with code ${code ?? 'null'}\n${finalLog}`));
1260
+ rejectOnce(new Error(`Agent exited with code ${code ?? 'null'}\n${finalLog}${errOutput !== finalLog && errOutput ? '\n\n' + errOutput : ''}`));
1235
1261
  });
1236
1262
  child.on('error', err => {
1237
1263
  progressUpdater.cancel();
1238
1264
  if (buildState?.stopRequested) {
1239
1265
  updateStatus(ticketId, 'stopped', null, io);
1240
- resolveOnce(null);
1266
+ resolveOnce({ response: null, responseTiming: null });
1241
1267
  return;
1242
1268
  }
1243
1269
  updateStatus(ticketId, 'error', getErrorMessage(err), io);
@@ -1257,6 +1283,21 @@ export const runAgent = ({ ticketId, tool, prompt, cwd, io, model, variant = nul
1257
1283
  const lines = text.split('\n');
1258
1284
  remainder = lines.pop() ?? '';
1259
1285
  for (const line of lines) {
1286
+ if (firstFinalAnswerTime === null && startTime !== null) {
1287
+ const trimmedLine = line.trim();
1288
+ if (trimmedLine.startsWith('{')) {
1289
+ try {
1290
+ const event = JSON.parse(trimmedLine);
1291
+ if (event.type === 'text' && event.part?.type === 'text' && event.part.text) {
1292
+ const phase = event.part.metadata?.openai?.phase;
1293
+ if (!phase || phase === 'final_answer') {
1294
+ firstFinalAnswerTime = Date.now();
1295
+ }
1296
+ }
1297
+ }
1298
+ catch { /* best-effort timing */ }
1299
+ }
1300
+ }
1260
1301
  const { logFragment, responseFragment } = parseOpencodeJsonLine(line);
1261
1302
  if (logFragment !== null) {
1262
1303
  logLines.push(logFragment);
@@ -1299,18 +1340,19 @@ export const runAgent = ({ ticketId, tool, prompt, cwd, io, model, variant = nul
1299
1340
  const log = logLines.join('\n\n') || stderrOutput;
1300
1341
  const finalResponse = responseLines.join('\n\n') || stderrOutput;
1301
1342
  const modelError = isModelResolutionError(log);
1343
+ const timing = buildResponseTiming({ startTime, firstFinalAnswerTime, isStopped: Boolean(buildState?.stopRequested) });
1302
1344
  if (buildState?.stopRequested) {
1303
1345
  updateStatus(ticketId, 'stopped', log, io, finalResponse);
1304
- resolveOnce(finalResponse);
1346
+ resolveOnce({ response: finalResponse, responseTiming: timing });
1305
1347
  return;
1306
1348
  }
1307
1349
  if (code === 0 && !modelError) {
1308
1350
  updateStatus(ticketId, 'done', log, io, finalResponse, { emit: emitSuccessUpdate });
1309
- resolveOnce(finalResponse);
1351
+ resolveOnce({ response: finalResponse, responseTiming: timing });
1310
1352
  return;
1311
1353
  }
1312
1354
  updateStatus(ticketId, 'error', log, io, finalResponse);
1313
- rejectOnce(new Error(`Agent exited with code ${code ?? 'null'}\n${log}`));
1355
+ rejectOnce(new Error(`Agent exited with code ${code ?? 'null'}\n${log}${stderrOutput !== log && stderrOutput ? '\n\n' + stderrOutput : ''}`));
1314
1356
  });
1315
1357
  child.on('error', err => {
1316
1358
  progressUpdater.cancel();
@@ -1320,7 +1362,7 @@ export const runAgent = ({ ticketId, tool, prompt, cwd, io, model, variant = nul
1320
1362
  }
1321
1363
  if (buildState?.stopRequested) {
1322
1364
  updateStatus(ticketId, 'stopped', null, io);
1323
- resolveOnce(null);
1365
+ resolveOnce({ response: null, responseTiming: null });
1324
1366
  return;
1325
1367
  }
1326
1368
  updateStatus(ticketId, 'error', getErrorMessage(err), io);
@@ -1335,6 +1377,8 @@ export const runChatAgent = ({ chatSessionId, tool, prompt, files = [], cwd, io,
1335
1377
  let spawnTimestamp = null;
1336
1378
  let opencodeMetadataRefresher = null;
1337
1379
  let settled = false;
1380
+ let startTime = null;
1381
+ let firstFinalAnswerTime = null;
1338
1382
  let usageSnapshot = EMPTY_CHAT_USAGE_SNAPSHOT;
1339
1383
  const chatState = {
1340
1384
  child: null,
@@ -1383,7 +1427,7 @@ export const runChatAgent = ({ chatSessionId, tool, prompt, files = [], cwd, io,
1383
1427
  ]
1384
1428
  ];
1385
1429
  })()
1386
- : ['opencode', ['run', ...(opencodeAgentMode === 'plan' ? ['--agent', 'plan'] : []), '--format', 'json', ...(opencodeAgentMode === 'build' ? ['--thinking'] : []), ...(resumeSessionId ? ['--session', resumeSessionId] : []), ...(resolvedModel ? ['--model', resolvedModel] : []), ...(variant ? ['--variant', variant] : []), ...files.flatMap(filePath => ['--file', filePath]), ...(files.length > 0 ? ['--'] : []), prompt]];
1430
+ : ['opencode', ['run', ...(opencodeAgentMode === 'plan' ? ['--agent', 'plan'] : []), '--format', 'json', '--thinking', ...(resumeSessionId ? ['--session', resumeSessionId] : []), ...(resolvedModel ? ['--model', resolvedModel] : []), ...(variant ? ['--variant', variant] : []), ...files.flatMap(filePath => ['--file', filePath]), ...(files.length > 0 ? ['--'] : []), prompt]];
1387
1431
  updateChatSessionStatus(chatSessionId, 'running', null, io);
1388
1432
  if (tool === 'claude' && sessionId) {
1389
1433
  updateChatSessionId(chatSessionId, tool, sessionId, io);
@@ -1412,6 +1456,7 @@ export const runChatAgent = ({ chatSessionId, tool, prompt, files = [], cwd, io,
1412
1456
  });
1413
1457
  chatState.child = child;
1414
1458
  activeChatProcesses.set(chatSessionId, chatState);
1459
+ startTime = Date.now();
1415
1460
  if (tool === 'claude') {
1416
1461
  const logLines = [];
1417
1462
  const responseLines = [];
@@ -1465,6 +1510,7 @@ export const runChatAgent = ({ chatSessionId, tool, prompt, files = [], cwd, io,
1465
1510
  completionStatus: 'stopped',
1466
1511
  finalResponse: preservedOutput.response,
1467
1512
  usageSnapshot,
1513
+ responseTiming: buildResponseTiming({ startTime, firstFinalAnswerTime, isStopped: true }),
1468
1514
  });
1469
1515
  return;
1470
1516
  }
@@ -1480,11 +1526,12 @@ export const runChatAgent = ({ chatSessionId, tool, prompt, files = [], cwd, io,
1480
1526
  completionStatus: 'done',
1481
1527
  finalResponse: finalOutput.response,
1482
1528
  usageSnapshot,
1529
+ responseTiming: buildResponseTiming({ startTime, firstFinalAnswerTime, isStopped: false }),
1483
1530
  });
1484
1531
  return;
1485
1532
  }
1486
1533
  updateChatSessionStatus(chatSessionId, 'error', finalLog + (errOutput ? '\n\n' + errOutput : ''), io, finalResponse);
1487
- rejectOnce(new Error(`Agent exited with code ${code ?? 'null'}\n${finalLog}`));
1534
+ rejectOnce(new Error(`Agent exited with code ${code ?? 'null'}\n${finalLog}${errOutput !== finalLog && errOutput ? '\n\n' + errOutput : ''}`));
1488
1535
  });
1489
1536
  child.on('error', err => {
1490
1537
  progressUpdater.cancel();
@@ -1501,6 +1548,7 @@ export const runChatAgent = ({ chatSessionId, tool, prompt, files = [], cwd, io,
1501
1548
  completionStatus: 'stopped',
1502
1549
  finalResponse: preservedOutput.response,
1503
1550
  usageSnapshot,
1551
+ responseTiming: buildResponseTiming({ startTime, firstFinalAnswerTime, isStopped: true }),
1504
1552
  });
1505
1553
  return;
1506
1554
  }
@@ -1522,6 +1570,21 @@ export const runChatAgent = ({ chatSessionId, tool, prompt, files = [], cwd, io,
1522
1570
  remainder = lines.pop() ?? '';
1523
1571
  for (const line of lines) {
1524
1572
  usageSnapshot = mergeUsageFromJsonLine(usageSnapshot, line);
1573
+ if (firstFinalAnswerTime === null && startTime !== null) {
1574
+ const trimmedLine = line.trim();
1575
+ if (trimmedLine.startsWith('{')) {
1576
+ try {
1577
+ const event = JSON.parse(trimmedLine);
1578
+ if (event.type === 'text' && event.part?.type === 'text' && event.part.text) {
1579
+ const phase = event.part.metadata?.openai?.phase;
1580
+ if (!phase || phase === 'final_answer') {
1581
+ firstFinalAnswerTime = Date.now();
1582
+ }
1583
+ }
1584
+ }
1585
+ catch { /* best-effort timing */ }
1586
+ }
1587
+ }
1525
1588
  const { logFragment, responseFragment } = parseOpencodeJsonLine(line, cwd);
1526
1589
  if (logFragment !== null) {
1527
1590
  logLines.push(logFragment);
@@ -1578,6 +1641,7 @@ export const runChatAgent = ({ chatSessionId, tool, prompt, files = [], cwd, io,
1578
1641
  completionStatus: 'stopped',
1579
1642
  finalResponse: preservedOutput?.response ?? null,
1580
1643
  usageSnapshot,
1644
+ responseTiming: buildResponseTiming({ startTime, firstFinalAnswerTime, isStopped: true }),
1581
1645
  });
1582
1646
  return;
1583
1647
  }
@@ -1600,11 +1664,12 @@ export const runChatAgent = ({ chatSessionId, tool, prompt, files = [], cwd, io,
1600
1664
  completionStatus: 'done',
1601
1665
  finalResponse: finalOutput.response,
1602
1666
  usageSnapshot,
1667
+ responseTiming: buildResponseTiming({ startTime, firstFinalAnswerTime, isStopped: false }),
1603
1668
  });
1604
1669
  return;
1605
1670
  }
1606
1671
  updateChatSessionStatus(chatSessionId, 'error', log, io, finalResponse);
1607
- rejectOnce(new Error(`Agent exited with code ${code ?? 'null'}\n${log}`));
1672
+ rejectOnce(new Error(`Agent exited with code ${code ?? 'null'}\n${log}${stderrOutput !== log && stderrOutput ? '\n\n' + stderrOutput : ''}`));
1608
1673
  });
1609
1674
  child.on('error', err => {
1610
1675
  progressUpdater.cancel();
@@ -1621,6 +1686,7 @@ export const runChatAgent = ({ chatSessionId, tool, prompt, files = [], cwd, io,
1621
1686
  completionStatus: 'stopped',
1622
1687
  finalResponse: preservedOutput.response,
1623
1688
  usageSnapshot,
1689
+ responseTiming: buildResponseTiming({ startTime, firstFinalAnswerTime, isStopped: true }),
1624
1690
  });
1625
1691
  return;
1626
1692
  }
@@ -1636,6 +1702,8 @@ export const runSpecAgent = ({ specSessionId, tool, prompt, files = [], cwd, io,
1636
1702
  let spawnTimestamp = null;
1637
1703
  let opencodeMetadataRefresher = null;
1638
1704
  let settled = false;
1705
+ let startTime = null;
1706
+ let firstFinalAnswerTime = null;
1639
1707
  let usageSnapshot = EMPTY_CHAT_USAGE_SNAPSHOT;
1640
1708
  const specState = {
1641
1709
  child: null,
@@ -1673,7 +1741,7 @@ export const runSpecAgent = ({ specSessionId, tool, prompt, files = [], cwd, io,
1673
1741
  ],
1674
1742
  ];
1675
1743
  })()
1676
- : ['opencode', ['run', '--agent', 'plan', '--format', 'json', ...(resumeSessionId ? ['--session', resumeSessionId] : []), ...(resolvedModel ? ['--model', resolvedModel] : []), ...(variant ? ['--variant', variant] : []), ...files.flatMap(filePath => ['--file', filePath]), ...(files.length > 0 ? ['--'] : []), prompt]];
1744
+ : ['opencode', ['run', '--agent', 'plan', '--format', 'json', '--thinking', ...(resumeSessionId ? ['--session', resumeSessionId] : []), ...(resolvedModel ? ['--model', resolvedModel] : []), ...(variant ? ['--variant', variant] : []), ...files.flatMap(filePath => ['--file', filePath]), ...(files.length > 0 ? ['--'] : []), prompt]];
1677
1745
  updateSpecSessionStatusLocal(specSessionId, 'running', null, io);
1678
1746
  if (tool === 'claude' && sessionId) {
1679
1747
  updateSpecSessionIdLocal(specSessionId, tool, sessionId, io);
@@ -1702,6 +1770,7 @@ export const runSpecAgent = ({ specSessionId, tool, prompt, files = [], cwd, io,
1702
1770
  });
1703
1771
  specState.child = child;
1704
1772
  activeSpecProcesses.set(specSessionId, specState);
1773
+ startTime = Date.now();
1705
1774
  if (tool === 'claude') {
1706
1775
  const logLines = [];
1707
1776
  const responseLines = [];
@@ -1755,6 +1824,7 @@ export const runSpecAgent = ({ specSessionId, tool, prompt, files = [], cwd, io,
1755
1824
  completionStatus: 'stopped',
1756
1825
  finalResponse: preservedOutput.response,
1757
1826
  usageSnapshot,
1827
+ responseTiming: buildResponseTiming({ startTime, firstFinalAnswerTime, isStopped: true }),
1758
1828
  });
1759
1829
  return;
1760
1830
  }
@@ -1770,11 +1840,12 @@ export const runSpecAgent = ({ specSessionId, tool, prompt, files = [], cwd, io,
1770
1840
  completionStatus: 'done',
1771
1841
  finalResponse: finalOutput.response,
1772
1842
  usageSnapshot,
1843
+ responseTiming: buildResponseTiming({ startTime, firstFinalAnswerTime, isStopped: false }),
1773
1844
  });
1774
1845
  return;
1775
1846
  }
1776
1847
  updateSpecSessionStatusLocal(specSessionId, 'error', finalLog + (errOutput ? '\n\n' + errOutput : ''), io, finalResponse);
1777
- rejectOnce(new Error(`Agent exited with code ${code ?? 'null'}\n${finalLog}`));
1848
+ rejectOnce(new Error(`Agent exited with code ${code ?? 'null'}\n${finalLog}${errOutput !== finalLog && errOutput ? '\n\n' + errOutput : ''}`));
1778
1849
  });
1779
1850
  child.on('error', err => {
1780
1851
  progressUpdater.cancel();
@@ -1791,6 +1862,7 @@ export const runSpecAgent = ({ specSessionId, tool, prompt, files = [], cwd, io,
1791
1862
  completionStatus: 'stopped',
1792
1863
  finalResponse: preservedOutput.response,
1793
1864
  usageSnapshot,
1865
+ responseTiming: buildResponseTiming({ startTime, firstFinalAnswerTime, isStopped: true }),
1794
1866
  });
1795
1867
  return;
1796
1868
  }
@@ -1812,6 +1884,21 @@ export const runSpecAgent = ({ specSessionId, tool, prompt, files = [], cwd, io,
1812
1884
  remainder = lines.pop() ?? '';
1813
1885
  for (const line of lines) {
1814
1886
  usageSnapshot = mergeUsageFromJsonLine(usageSnapshot, line);
1887
+ if (firstFinalAnswerTime === null && startTime !== null) {
1888
+ const trimmedLine = line.trim();
1889
+ if (trimmedLine.startsWith('{')) {
1890
+ try {
1891
+ const event = JSON.parse(trimmedLine);
1892
+ if (event.type === 'text' && event.part?.type === 'text' && event.part.text) {
1893
+ const phase = event.part.metadata?.openai?.phase;
1894
+ if (!phase || phase === 'final_answer') {
1895
+ firstFinalAnswerTime = Date.now();
1896
+ }
1897
+ }
1898
+ }
1899
+ catch { /* best-effort timing */ }
1900
+ }
1901
+ }
1815
1902
  const { logFragment, responseFragment } = parseOpencodeJsonLine(line, cwd);
1816
1903
  if (logFragment !== null) {
1817
1904
  logLines.push(logFragment);
@@ -1868,6 +1955,7 @@ export const runSpecAgent = ({ specSessionId, tool, prompt, files = [], cwd, io,
1868
1955
  completionStatus: 'stopped',
1869
1956
  finalResponse: preservedOutput?.response ?? null,
1870
1957
  usageSnapshot,
1958
+ responseTiming: buildResponseTiming({ startTime, firstFinalAnswerTime, isStopped: true }),
1871
1959
  });
1872
1960
  return;
1873
1961
  }
@@ -1890,11 +1978,12 @@ export const runSpecAgent = ({ specSessionId, tool, prompt, files = [], cwd, io,
1890
1978
  completionStatus: 'done',
1891
1979
  finalResponse: finalOutput.response,
1892
1980
  usageSnapshot,
1981
+ responseTiming: buildResponseTiming({ startTime, firstFinalAnswerTime, isStopped: false }),
1893
1982
  });
1894
1983
  return;
1895
1984
  }
1896
1985
  updateSpecSessionStatusLocal(specSessionId, 'error', log, io, finalResponse);
1897
- rejectOnce(new Error(`Agent exited with code ${code ?? 'null'}\n${log}`));
1986
+ rejectOnce(new Error(`Agent exited with code ${code ?? 'null'}\n${log}${stderrOutput !== log && stderrOutput ? '\n\n' + stderrOutput : ''}`));
1898
1987
  });
1899
1988
  child.on('error', err => {
1900
1989
  progressUpdater.cancel();
@@ -1911,6 +2000,7 @@ export const runSpecAgent = ({ specSessionId, tool, prompt, files = [], cwd, io,
1911
2000
  completionStatus: 'stopped',
1912
2001
  finalResponse: preservedOutput.response,
1913
2002
  usageSnapshot,
2003
+ responseTiming: buildResponseTiming({ startTime, firstFinalAnswerTime, isStopped: true }),
1914
2004
  });
1915
2005
  return;
1916
2006
  }