@straiffi/archon 1.8.13 → 1.8.14

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 (96) hide show
  1. package/dist/client/assets/{ActivityView-DD1Fom-k.js → ActivityView-DYDYyqIN.js} +1 -1
  2. package/dist/client/assets/{GitHubActionsDialog-aDf_6y6m.js → GitHubActionsDialog-jhi4GOyV.js} +1 -1
  3. package/dist/client/assets/{RepositoryGraphDialog-B4Ij2xmA.js → RepositoryGraphDialog-BZV0vHu6.js} +1 -1
  4. package/dist/client/assets/{VerificationDialog-BBCRKTdr.js → VerificationDialog-Sc6e8AKH.js} +1 -1
  5. package/dist/client/assets/{architectureDiagram-3BPJPVTR-DW3kiEe6.js → architectureDiagram-3BPJPVTR-DFbki8mi.js} +1 -1
  6. package/dist/client/assets/{blockDiagram-GPEHLZMM-BVWrspTj.js → blockDiagram-GPEHLZMM-BZFehG8P.js} +1 -1
  7. package/dist/client/assets/{c4Diagram-AAUBKEIU-CDqXjYR1.js → c4Diagram-AAUBKEIU-FVDXRu1t.js} +1 -1
  8. package/dist/client/assets/channel-D1kOpezG.js +1 -0
  9. package/dist/client/assets/{chunk-2J33WTMH-Db4bg8nj.js → chunk-2J33WTMH-4tuVZaT0.js} +1 -1
  10. package/dist/client/assets/{chunk-3OPIFGDE-DqziKs5m.js → chunk-3OPIFGDE-DSn14S35.js} +1 -1
  11. package/dist/client/assets/{chunk-5ZQYHXKU-wdN7gQNo.js → chunk-5ZQYHXKU-Cwx1KNjg.js} +1 -1
  12. package/dist/client/assets/{chunk-727SXJPM-Dj3aKc06.js → chunk-727SXJPM-Odza4hes.js} +1 -1
  13. package/dist/client/assets/{chunk-AQP2D5EJ-CAlE_C_J.js → chunk-AQP2D5EJ-BmVoQT7M.js} +1 -1
  14. package/dist/client/assets/{chunk-CSCIHK7Q-KstW8JRv.js → chunk-CSCIHK7Q-Bg7Ktx0B.js} +1 -1
  15. package/dist/client/assets/{chunk-KSCS5N6A-DrY-9bi9.js → chunk-KSCS5N6A-2tnhar2x.js} +1 -1
  16. package/dist/client/assets/{chunk-L5ZTLDWV-C6fw_Dab.js → chunk-L5ZTLDWV-dAyg8qMJ.js} +1 -1
  17. package/dist/client/assets/{chunk-LZXEDZCA-DaZP5hZq.js → chunk-LZXEDZCA-0gElxrlR.js} +2 -2
  18. package/dist/client/assets/{chunk-ND2GUHAM-ChYXM7G3.js → chunk-ND2GUHAM-Bu-8Lz91.js} +1 -1
  19. package/dist/client/assets/{chunk-NZK2D7GU-JajrDJgw.js → chunk-NZK2D7GU-CInZFdzd.js} +1 -1
  20. package/dist/client/assets/{chunk-O5CBEL6O-C4HoCf_q.js → chunk-O5CBEL6O-rAzpvdNR.js} +1 -1
  21. package/dist/client/assets/{chunk-WU5MYG2G-BdjAiDOI.js → chunk-WU5MYG2G-DOOw2vq7.js} +1 -1
  22. package/dist/client/assets/classDiagram-4FO5ZUOK-C253nwny.js +1 -0
  23. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-DFgDA23g.js +1 -0
  24. package/dist/client/assets/{dagre-BM42HDAG-CpXupRCy.js → dagre-BM42HDAG-Dgua1iuh.js} +1 -1
  25. package/dist/client/assets/{diagram-2AECGRRQ-DyopuXBv.js → diagram-2AECGRRQ-odacwlwu.js} +1 -1
  26. package/dist/client/assets/{diagram-5GNKFQAL-C8lS8Vro.js → diagram-5GNKFQAL-BiKjFmJ1.js} +1 -1
  27. package/dist/client/assets/{diagram-KO2AKTUF-CsyC_xgK.js → diagram-KO2AKTUF-CmYcvFri.js} +1 -1
  28. package/dist/client/assets/{diagram-LMA3HP47-DWsQdA71.js → diagram-LMA3HP47-ncx0RBuX.js} +1 -1
  29. package/dist/client/assets/{diagram-OG6HWLK6-YkNa_Tt6.js → diagram-OG6HWLK6-DbchKIzw.js} +1 -1
  30. package/dist/client/assets/{erDiagram-TEJ5UH35-VmMz5G_L.js → erDiagram-TEJ5UH35-CMnpvRyQ.js} +1 -1
  31. package/dist/client/assets/{flowDiagram-I6XJVG4X-DZAu6IJl.js → flowDiagram-I6XJVG4X-Bi02_PK5.js} +1 -1
  32. package/dist/client/assets/{ganttDiagram-6RSMTGT7-CHJRz_2M.js → ganttDiagram-6RSMTGT7-BiS992US.js} +1 -1
  33. package/dist/client/assets/{gitGraphDiagram-PVQCEYII-BfA1qYTR.js → gitGraphDiagram-PVQCEYII-Cc1hrTsT.js} +1 -1
  34. package/dist/client/assets/{index-B4x8ZZ8N.js → index-B1gdJKwK.js} +90 -90
  35. package/dist/client/assets/{infoDiagram-5YYISTIA-CyqqEYvx.js → infoDiagram-5YYISTIA-BKVfqJCe.js} +1 -1
  36. package/dist/client/assets/{ishikawaDiagram-YF4QCWOH-DnXMvxPm.js → ishikawaDiagram-YF4QCWOH-B-IbFOty.js} +1 -1
  37. package/dist/client/assets/{journeyDiagram-JHISSGLW-DBumlehf.js → journeyDiagram-JHISSGLW-DAZBgAQW.js} +1 -1
  38. package/dist/client/assets/{kanban-definition-UN3LZRKU-D-QvfQhh.js → kanban-definition-UN3LZRKU-Bkr_fXev.js} +1 -1
  39. package/dist/client/assets/{line-BgwSCyaP.js → line-CROuuXDO.js} +1 -1
  40. package/dist/client/assets/{mermaid-parser.core-qDA1puIK.js → mermaid-parser.core-DrBw_Rsc.js} +1 -1
  41. package/dist/client/assets/{mermaid.core-CFM-fuJi.js → mermaid.core-pPd6OYYj.js} +3 -3
  42. package/dist/client/assets/{mindmap-definition-RKZ34NQL-B93RAITk.js → mindmap-definition-RKZ34NQL-BXSd3TZU.js} +1 -1
  43. package/dist/client/assets/{pieDiagram-4H26LBE5-C9arDpCE.js → pieDiagram-4H26LBE5-DoT7PIZC.js} +1 -1
  44. package/dist/client/assets/{quadrantDiagram-W4KKPZXB-Tma_RsUu.js → quadrantDiagram-W4KKPZXB-ycZkLowm.js} +1 -1
  45. package/dist/client/assets/{requirementDiagram-4Y6WPE33-C6GCxpqT.js → requirementDiagram-4Y6WPE33-BPkO1yLu.js} +1 -1
  46. package/dist/client/assets/{sankeyDiagram-5OEKKPKP-CY8ukFyG.js → sankeyDiagram-5OEKKPKP-D7AvhhCw.js} +1 -1
  47. package/dist/client/assets/{sequenceDiagram-3UESZ5HK-B6MbhJm7.js → sequenceDiagram-3UESZ5HK-C982swnH.js} +1 -1
  48. package/dist/client/assets/{stateDiagram-AJRCARHV-Bjy1dBVk.js → stateDiagram-AJRCARHV-xvJ3u8lc.js} +1 -1
  49. package/dist/client/assets/stateDiagram-v2-BHNVJYJU-BKQG8NCn.js +1 -0
  50. package/dist/client/assets/{timeline-definition-PNZ67QCA-KrbaHcGf.js → timeline-definition-PNZ67QCA-D89qjzb5.js} +1 -1
  51. package/dist/client/assets/{vennDiagram-CIIHVFJN-DQ-5gbga.js → vennDiagram-CIIHVFJN-CoKeC__u.js} +1 -1
  52. package/dist/client/assets/{wardleyDiagram-YWT4CUSO-Cwpu-hvf.js → wardleyDiagram-YWT4CUSO-DKhjzvSE.js} +1 -1
  53. package/dist/client/assets/{xychartDiagram-2RQKCTM6-DoP4HxQE.js → xychartDiagram-2RQKCTM6-DFn9ass1.js} +1 -1
  54. package/dist/client/index.html +1 -1
  55. package/dist/server/index.js +26 -5
  56. package/dist/server/index.js.map +1 -1
  57. package/dist/server/lib/agent.js +171 -170
  58. package/dist/server/lib/agent.js.map +1 -1
  59. package/dist/server/lib/agentProvider.js +64 -0
  60. package/dist/server/lib/agentProvider.js.map +1 -0
  61. package/dist/server/lib/agentRuns.js +1 -0
  62. package/dist/server/lib/agentRuns.js.map +1 -1
  63. package/dist/server/lib/automationProgression.js +6 -2
  64. package/dist/server/lib/automationProgression.js.map +1 -1
  65. package/dist/server/lib/automationResumeDecision.js +4 -3
  66. package/dist/server/lib/automationResumeDecision.js.map +1 -1
  67. package/dist/server/lib/claudeAgentProvider.js +48 -0
  68. package/dist/server/lib/claudeAgentProvider.js.map +1 -0
  69. package/dist/server/lib/executionEnvironment.js +20 -0
  70. package/dist/server/lib/executionEnvironment.js.map +1 -0
  71. package/dist/server/lib/hostExecutionEnvironment.js +77 -0
  72. package/dist/server/lib/hostExecutionEnvironment.js.map +1 -0
  73. package/dist/server/lib/lightweightPrompt.js +17 -18
  74. package/dist/server/lib/lightweightPrompt.js.map +1 -1
  75. package/dist/server/lib/opencodeAgentProvider.js +139 -0
  76. package/dist/server/lib/opencodeAgentProvider.js.map +1 -0
  77. package/dist/server/lib/opencodeSessionOperations.js +348 -0
  78. package/dist/server/lib/opencodeSessionOperations.js.map +1 -0
  79. package/dist/server/lib/opencodeTaskSessions.js +13 -225
  80. package/dist/server/lib/opencodeTaskSessions.js.map +1 -1
  81. package/dist/server/lib/projectAutoConfig.js +22 -19
  82. package/dist/server/lib/projectAutoConfig.js.map +1 -1
  83. package/dist/server/lib/reviewFindings.js +15 -0
  84. package/dist/server/lib/reviewFindings.js.map +1 -1
  85. package/dist/server/lib/types.js.map +1 -1
  86. package/dist/server/workers/followUp.js +10 -6
  87. package/dist/server/workers/followUp.js.map +1 -1
  88. package/dist/server/workers/plan.js +5 -21
  89. package/dist/server/workers/plan.js.map +1 -1
  90. package/dist/server/workers/review.js +33 -14
  91. package/dist/server/workers/review.js.map +1 -1
  92. package/package.json +1 -1
  93. package/dist/client/assets/channel-C14KP1Bm.js +0 -1
  94. package/dist/client/assets/classDiagram-4FO5ZUOK-CR20Gg9e.js +0 -1
  95. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-B-N-i15d.js +0 -1
  96. package/dist/client/assets/stateDiagram-v2-BHNVJYJU-asf2fsPC.js +0 -1
@@ -2,73 +2,29 @@ import { spawn } from 'child_process';
2
2
  import { randomUUID } from 'crypto';
3
3
  import { readFileSync } from 'fs';
4
4
  import { extname, isAbsolute, join, relative, resolve, sep } from 'path';
5
- import Database from 'better-sqlite3';
6
5
  import db from '../db.js';
7
6
  import { getChatSession, updateChatSessionUsageSnapshot } from './chats.js';
8
7
  import { normalizeClaudeModelId, normalizeModelId } from './models.js';
9
8
  import { createOpencodeTaskSessionReader, parseOpencodeTaskEnvelope } from './opencodeTaskSessions.js';
9
+ import { hostOpencodeSessionOperations } from './opencodeSessionOperations.js';
10
+ import { hostExecutionEnvironment } from './hostExecutionEnvironment.js';
11
+ import { claudeAgentProvider } from './claudeAgentProvider.js';
12
+ import { buildOpencodeSpecReviewInvocation, createOpencodeExitError, createOpencodeRecoveryTracker, interpretOpencodeRecoveryEvent, isSuccessfulOpencodeExit, opencodeAgentProvider, } from './opencodeAgentProvider.js';
10
13
  import { appendClaudeHeadlessPlanPrompt, buildClaudeHeadlessPlanArgs } from './planModeQuestions.js';
11
14
  import { formatGenericToolCall } from './genericToolCall.js';
12
15
  import { getSpecSession, updateSpecSessionResumeId, updateSpecSessionStatus, updateSpecSessionTitle, updateSpecSessionUsageSnapshot } from './specs.js';
13
16
  import { updateSpecReviewRunStatus, updateSpecReviewRunStreaming, updateSpecReviewRunUsage } from './specReviews.js';
14
17
  import { createAgentRun, emitAgentRunsUpdated, finalizeAgentRun, getNextAgentRunIndex, listAgentRunsForExecutionRun, listAgentRunsForParent, updateAgentRunLabel, updateAgentRunSessionId, updateAgentRunStatus } from './agentRuns.js';
15
- import { resolveOpencodeDbPath } from './runtimePaths.js';
16
18
  import { markTicketDescriptionVisibleInBundle } from './ticketMessages.js';
17
19
  import { getTicket } from './tickets.js';
18
20
  import { createTranscriptRunSnapshots } from './transcriptRuns.js';
19
21
  import { createWorkspaceProcessEnvironment } from './workspaceProcessEnvironment.js';
20
- import { OpencodeModelResolutionError, parseOpencodeError, parseOpencodeStderrModelResolutionError, } from './opencodeErrors.js';
22
+ import { OpencodeModelResolutionError, parseOpencodeStderrModelResolutionError, } from './opencodeErrors.js';
21
23
  import { extractLeadingOpencodeJsonObjects } from './opencodePromptOutput.js';
22
24
  import { assertAgentSpawnPermit, AgentSpawnPermitRevokedError } from './agentSpawnPermits.js';
23
25
  const withTranscriptRuns = (value, transcriptRuns) => {
24
26
  return transcriptRuns ? { ...value, transcriptRuns } : value;
25
27
  };
26
- const createOpencodeRecoveryTracker = () => {
27
- let sawError = false;
28
- let latestEvent = null;
29
- let recoveredFinalAnswerFragments = [];
30
- let latestError = null;
31
- let modelResolutionError = null;
32
- return {
33
- consume: events => {
34
- let shouldResetResponse = false;
35
- for (const event of events ?? []) {
36
- latestEvent = event.type;
37
- if (event.type === 'error') {
38
- sawError = true;
39
- latestError = event.error;
40
- if (event.error?.isModelResolutionError) {
41
- modelResolutionError = event.error;
42
- }
43
- recoveredFinalAnswerFragments = [];
44
- shouldResetResponse = true;
45
- continue;
46
- }
47
- if (sawError) {
48
- recoveredFinalAnswerFragments.push(event.text);
49
- }
50
- }
51
- return shouldResetResponse;
52
- },
53
- hasRecoveredFinalAnswer: () => (sawError
54
- && latestEvent === 'final_answer'
55
- && recoveredFinalAnswerFragments.length > 0),
56
- hasUnrecoveredError: () => latestEvent === 'error',
57
- getModelResolutionError: () => modelResolutionError,
58
- getUnrecoveredError: () => latestEvent === 'error' ? latestError : null,
59
- getRecoveredResponse: () => recoveredFinalAnswerFragments.join('\n\n'),
60
- };
61
- };
62
- const isSuccessfulOpencodeExit = (code, stderrModelError, recoveryTracker) => (!stderrModelError
63
- && !recoveryTracker.getModelResolutionError()
64
- && !recoveryTracker.hasUnrecoveredError()
65
- && (code === 0 || recoveryTracker.hasRecoveredFinalAnswer()));
66
- const createOpencodeExitError = (message, stderrModelError, recoveryTracker) => {
67
- const structuredError = recoveryTracker.getModelResolutionError() ?? recoveryTracker.getUnrecoveredError();
68
- return structuredError?.isModelResolutionError || stderrModelError
69
- ? new OpencodeModelResolutionError(message)
70
- : new Error(message);
71
- };
72
28
  export class AgentContinuationRejectedError extends Error {
73
29
  constructor(message) {
74
30
  super(message);
@@ -91,7 +47,6 @@ export const getAgentErrorTranscriptRuns = (error) => {
91
47
  const transcriptRuns = error.transcriptRuns;
92
48
  return Array.isArray(transcriptRuns) && transcriptRuns.length > 0 ? transcriptRuns : null;
93
49
  };
94
- const OPENCODE_PLACEHOLDER_TITLE_PATTERN = /^New session\b/i;
95
50
  const OPENCODE_SESSION_METADATA_LOOKUP_DELAY_MS = 1500;
96
51
  const AGENT_PROGRESS_UPDATE_INTERVAL_MS = 100;
97
52
  const resolveAgentModel = (tool, model) => {
@@ -157,13 +112,6 @@ const buildClaudeArgs = (mode, model, resumeSessionId, sessionId) => {
157
112
  sessionId,
158
113
  ];
159
114
  };
160
- const OPENCODE_SESSION_BY_ID_QUERY = 'SELECT id, title FROM session WHERE id = ? LIMIT 1';
161
- const OPENCODE_SESSION_BY_CREATED_AT_QUERY = 'SELECT id, title FROM session WHERE time_created > ? ORDER BY time_created DESC LIMIT 1';
162
- 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";
163
- 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";
164
- 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";
165
- 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";
166
- const OPENCODE_SESSION_COST_QUERY = 'SELECT cost FROM session WHERE id = ?';
167
115
  const GET_PERSISTED_CHAT_STATE_QUERY = 'SELECT agent_log, streaming_response, opencode_session_id FROM chat_sessions WHERE id = ?';
168
116
  const GET_PERSISTED_SPEC_STATE_QUERY = 'SELECT agent_log, streaming_response, opencode_session_id FROM spec_sessions WHERE id = ?';
169
117
  export const EMPTY_CHAT_USAGE_SNAPSHOT = {
@@ -327,9 +275,6 @@ export const mergeUsageFromJsonLine = (current, line) => {
327
275
  }
328
276
  return next;
329
277
  };
330
- const hasUsageSnapshot = (value) => {
331
- return Object.values(value).some(candidate => candidate !== null && candidate !== undefined);
332
- };
333
278
  const hasMatchingUsageSnapshot = (current, next) => {
334
279
  return current.usage_snapshot_context_window === next.usage_snapshot_context_window
335
280
  && current.usage_snapshot_max_output_tokens === next.usage_snapshot_max_output_tokens
@@ -500,67 +445,12 @@ const createThrottledProgressUpdater = (flushUpdate) => {
500
445
  },
501
446
  };
502
447
  };
503
- const openOpencodeDatabase = () => new Database(resolveOpencodeDbPath(), { readonly: true });
504
- const readOpencodeSession = (statement, value) => {
505
- const opencodeDb = openOpencodeDatabase();
506
- try {
507
- const row = opencodeDb.prepare(statement).get(value);
508
- if (!row?.id) {
509
- return null;
510
- }
511
- const normalizedTitle = typeof row.title === 'string'
512
- ? row.title.trim() || null
513
- : null;
514
- const title = normalizedTitle && !OPENCODE_PLACEHOLDER_TITLE_PATTERN.test(normalizedTitle)
515
- ? normalizedTitle
516
- : null;
517
- return {
518
- id: row.id,
519
- title,
520
- };
521
- }
522
- finally {
523
- opencodeDb.close();
524
- }
525
- };
526
448
  export const readOpencodeUsageSnapshot = ({ sessionId, spawnTimestamp = null, }) => {
527
- const opencodeDb = openOpencodeDatabase();
528
- try {
529
- const costRow = opencodeDb.prepare(OPENCODE_SESSION_COST_QUERY).get(sessionId);
530
- const sessionCost = typeof costRow?.cost === 'number' ? { usage_snapshot_cost: costRow.cost } : {};
531
- const completedAssistantMessage = (spawnTimestamp === null
532
- ? opencodeDb.prepare(OPENCODE_COMPLETED_ASSISTANT_MESSAGE_USAGE_QUERY).get(sessionId)
533
- : opencodeDb.prepare(OPENCODE_COMPLETED_ASSISTANT_MESSAGE_USAGE_SINCE_QUERY).get(sessionId, spawnTimestamp));
534
- if (typeof completedAssistantMessage?.data === 'string') {
535
- try {
536
- const parsedUsage = extractChatUsageSnapshot(JSON.parse(completedAssistantMessage.data));
537
- if (hasUsageSnapshot(parsedUsage)) {
538
- return mergeChatUsageSnapshot(mergeChatUsageSnapshot(EMPTY_CHAT_USAGE_SNAPSHOT, parsedUsage), sessionCost);
539
- }
540
- }
541
- catch {
542
- // Fall through to the step-finish lookup below.
543
- }
544
- }
545
- const latestStepFinish = (spawnTimestamp === null
546
- ? opencodeDb.prepare(OPENCODE_STEP_FINISH_USAGE_QUERY).get(sessionId)
547
- : opencodeDb.prepare(OPENCODE_STEP_FINISH_USAGE_SINCE_QUERY).get(sessionId, spawnTimestamp));
548
- if (typeof latestStepFinish?.data === 'string') {
549
- try {
550
- const parsedUsage = extractChatUsageSnapshot(JSON.parse(latestStepFinish.data));
551
- if (hasUsageSnapshot(parsedUsage)) {
552
- return mergeChatUsageSnapshot(mergeChatUsageSnapshot(EMPTY_CHAT_USAGE_SNAPSHOT, parsedUsage), sessionCost);
553
- }
554
- }
555
- catch {
556
- return mergeChatUsageSnapshot(EMPTY_CHAT_USAGE_SNAPSHOT, sessionCost);
557
- }
558
- }
559
- return mergeChatUsageSnapshot(EMPTY_CHAT_USAGE_SNAPSHOT, sessionCost);
560
- }
561
- finally {
562
- opencodeDb.close();
449
+ const snapshot = hostOpencodeSessionOperations.readUsageSnapshot(sessionId, spawnTimestamp);
450
+ if (!snapshot) {
451
+ throw new Error('OpenCode usage snapshot read failed');
563
452
  }
453
+ return snapshot;
564
454
  };
565
455
  const resolveOpencodeUsageSnapshot = ({ current, sessionId, spawnTimestamp = null, }) => {
566
456
  try {
@@ -655,12 +545,12 @@ const createOpencodeMetadataRefresher = ({ chatSessionId, io, spawnTimestamp, in
655
545
  };
656
546
  const readCurrentSession = () => {
657
547
  if (knownSessionId) {
658
- return readOpencodeSession(OPENCODE_SESSION_BY_ID_QUERY, knownSessionId);
548
+ return hostOpencodeSessionOperations.findSessionById(knownSessionId);
659
549
  }
660
550
  if (spawnTimestamp === null) {
661
551
  return null;
662
552
  }
663
- return readOpencodeSession(OPENCODE_SESSION_BY_CREATED_AT_QUERY, spawnTimestamp);
553
+ return hostOpencodeSessionOperations.findLatestSessionAfter(spawnTimestamp);
664
554
  };
665
555
  const refresh = () => {
666
556
  if (stopped) {
@@ -723,12 +613,12 @@ const createSpecOpencodeMetadataRefresher = ({ specSessionId, io, spawnTimestamp
723
613
  };
724
614
  const readCurrentSession = () => {
725
615
  if (knownSessionId) {
726
- return readOpencodeSession(OPENCODE_SESSION_BY_ID_QUERY, knownSessionId);
616
+ return hostOpencodeSessionOperations.findSessionById(knownSessionId);
727
617
  }
728
618
  if (spawnTimestamp === null) {
729
619
  return null;
730
620
  }
731
- return readOpencodeSession(OPENCODE_SESSION_BY_CREATED_AT_QUERY, spawnTimestamp);
621
+ return hostOpencodeSessionOperations.findLatestSessionAfter(spawnTimestamp);
732
622
  };
733
623
  const refresh = () => {
734
624
  if (stopped) {
@@ -1094,15 +984,14 @@ const parseOpencodeJsonEvent = (event, cwd) => {
1094
984
  if (event.type === 'text' && event.part?.type === 'text' && event.part.text) {
1095
985
  const text = event.part.text.trim();
1096
986
  const phase = event.part.metadata?.openai?.phase;
987
+ const recoveryEvent = interpretOpencodeRecoveryEvent(event.type, event.error, text, phase);
1097
988
  return {
1098
989
  logFragment: text || null,
1099
990
  responseFragment: text && (!phase || phase === 'final_answer') ? text : null,
1100
991
  subagentEvents: [],
1101
992
  providerSessionId,
1102
993
  modelResolutionError: false,
1103
- recoveryEvents: text && phase === 'final_answer'
1104
- ? [{ type: 'final_answer', text }]
1105
- : [],
994
+ recoveryEvents: recoveryEvent ? [recoveryEvent] : [],
1106
995
  };
1107
996
  }
1108
997
  if (event.type === 'reasoning' && event.part?.type === 'reasoning') {
@@ -1186,14 +1075,15 @@ const parseOpencodeJsonEvent = (event, cwd) => {
1186
1075
  };
1187
1076
  }
1188
1077
  if (event.type === 'error') {
1189
- const parsedError = parseOpencodeError(event.error, event.part?.text);
1078
+ const recoveryEvent = interpretOpencodeRecoveryEvent(event.type, event.error, event.part?.text, undefined);
1079
+ const parsedError = recoveryEvent?.type === 'error' ? recoveryEvent.error : null;
1190
1080
  const text = parsedError?.message ?? '';
1191
1081
  return {
1192
1082
  logFragment: text ? `**Error:** ${text}` : null,
1193
1083
  responseFragment: null,
1194
1084
  subagentEvents: [],
1195
1085
  providerSessionId,
1196
- recoveryEvents: [{ type: 'error', error: parsedError }],
1086
+ recoveryEvents: recoveryEvent ? [recoveryEvent] : [],
1197
1087
  };
1198
1088
  }
1199
1089
  return emptyParsedStreamLine();
@@ -1724,6 +1614,16 @@ const killProcess = (proc, signal) => {
1724
1614
  catch {
1725
1615
  }
1726
1616
  };
1617
+ const interruptProcess = (state, signal) => {
1618
+ if (!isProcessAlive(state.child)) {
1619
+ return;
1620
+ }
1621
+ if (state.interrupt) {
1622
+ state.interrupt(signal);
1623
+ return;
1624
+ }
1625
+ killProcess(state.child, signal);
1626
+ };
1727
1627
  const waitForProcessExit = async (proc, timeoutMs) => {
1728
1628
  const deadline = Date.now() + timeoutMs;
1729
1629
  await new Promise(resolve => {
@@ -1757,6 +1657,7 @@ const untrackChatProcess = (chatSessionId, state) => {
1757
1657
  export const registerChatProcessPlaceholder = (chatSessionId) => {
1758
1658
  const state = {
1759
1659
  child: null,
1660
+ interrupt: null,
1760
1661
  researchChildren: new Set(),
1761
1662
  stopRequested: false,
1762
1663
  metadataRefresher: null,
@@ -1765,7 +1666,7 @@ export const registerChatProcessPlaceholder = (chatSessionId) => {
1765
1666
  return state;
1766
1667
  };
1767
1668
  export const registerBuildProcessPlaceholder = (ticketId) => {
1768
- const state = { child: null, stopRequested: false, sessionLookupTimer: null };
1669
+ const state = { child: null, interrupt: null, stopRequested: false, sessionLookupTimer: null };
1769
1670
  activeBuildProcesses.set(ticketId, state);
1770
1671
  return state;
1771
1672
  };
@@ -1796,6 +1697,7 @@ const untrackSpecProcess = (specSessionId, state) => {
1796
1697
  export const registerSpecProcessPlaceholder = (specSessionId) => {
1797
1698
  const state = {
1798
1699
  child: null,
1700
+ interrupt: null,
1799
1701
  researchChildren: new Set(),
1800
1702
  stopRequested: false,
1801
1703
  metadataRefresher: null,
@@ -1819,6 +1721,7 @@ const untrackSpecReviewProcess = (specReviewRunId, state) => {
1819
1721
  export const registerSpecReviewProcessPlaceholder = (specReviewRunId) => {
1820
1722
  const state = {
1821
1723
  child: null,
1724
+ interrupt: null,
1822
1725
  researchChildren: new Set(),
1823
1726
  stopRequested: false,
1824
1727
  metadataRefresher: null,
@@ -1864,10 +1767,20 @@ export const stopBuildAgent = async (ticketId) => {
1864
1767
  if (!state.child) {
1865
1768
  return true;
1866
1769
  }
1867
- killProcess(state.child, 'SIGTERM');
1770
+ if (state.interrupt) {
1771
+ state.interrupt('SIGTERM');
1772
+ }
1773
+ else {
1774
+ killProcess(state.child, 'SIGTERM');
1775
+ }
1868
1776
  await waitForProcessExit(state.child, 5000);
1869
1777
  if (isProcessAlive(state.child)) {
1870
- killProcess(state.child, 'SIGKILL');
1778
+ if (state.interrupt) {
1779
+ state.interrupt('SIGKILL');
1780
+ }
1781
+ else {
1782
+ killProcess(state.child, 'SIGKILL');
1783
+ }
1871
1784
  await waitForProcessExit(state.child, 1000);
1872
1785
  }
1873
1786
  return true;
@@ -1943,15 +1856,15 @@ export const stopSpecReviewAgent = async (specReviewRunId) => {
1943
1856
  if (!state.child) {
1944
1857
  return true;
1945
1858
  }
1946
- killProcess(state.child, 'SIGTERM');
1859
+ interruptProcess(state, 'SIGTERM');
1947
1860
  await waitForProcessExit(state.child, 5000);
1948
1861
  if (isProcessAlive(state.child)) {
1949
- killProcess(state.child, 'SIGKILL');
1862
+ interruptProcess(state, 'SIGKILL');
1950
1863
  await waitForProcessExit(state.child, 1000);
1951
1864
  }
1952
1865
  return true;
1953
1866
  };
1954
- export const runAgent = ({ ticketId, tool, prompt, cwd, io, model, variant = null, resumeSessionId = null, sessionField = 'session_id', trackBuildProcess = false, reviewProcessState, emitSuccessUpdate = true, opencodeAgentMode, runStream, executionContext, persistSessionIdToTicket = false, spawnPermitId, }) => {
1867
+ export const runAgent = ({ ticketId, tool, prompt, cwd, io, model, variant = null, resumeSessionId = null, sessionField = 'session_id', trackBuildProcess = false, reviewProcessState, emitSuccessUpdate = true, opencodeAgentMode, runStream, executionContext, persistSessionIdToTicket = false, spawnPermitId, executionProvider = hostExecutionEnvironment, }) => {
1955
1868
  const resolvedModel = resolveAgentModel(tool, model);
1956
1869
  const subagentTracker = createSubagentRunTracker({
1957
1870
  parentType: 'ticket',
@@ -2049,7 +1962,7 @@ export const runAgent = ({ ticketId, tool, prompt, cwd, io, model, variant = nul
2049
1962
  return hasResolvedOpencodeSessionId;
2050
1963
  }
2051
1964
  try {
2052
- const session = readOpencodeSession(OPENCODE_SESSION_BY_CREATED_AT_QUERY, spawnTimestamp);
1965
+ const session = hostOpencodeSessionOperations.findLatestSessionAfter(spawnTimestamp);
2053
1966
  if (session?.id) {
2054
1967
  bindResolvedOpencodeSession(session.id);
2055
1968
  }
@@ -2059,15 +1972,19 @@ export const runAgent = ({ ticketId, tool, prompt, cwd, io, model, variant = nul
2059
1972
  }
2060
1973
  return hasResolvedOpencodeSessionId;
2061
1974
  };
2062
- const [cmd, args] = tool === 'claude'
2063
- ? (() => {
2064
- sessionId = resumeSessionId || randomUUID();
2065
- return [
2066
- 'claude',
2067
- buildClaudeArgs(opencodeAgentMode, resolvedModel, resumeSessionId, sessionId),
2068
- ];
2069
- })()
2070
- : ['opencode', ['run', ...(opencodeAgentMode === 'plan' ? ['--agent', 'plan'] : []), '--format', 'json', '--thinking', ...(resumeSessionId ? ['--session', resumeSessionId] : []), ...(resolvedModel ? ['--model', resolvedModel] : []), ...(variant ? ['--variant', variant] : []), prompt]];
1975
+ const agentProvider = tool === 'claude'
1976
+ ? claudeAgentProvider
1977
+ : opencodeAgentProvider;
1978
+ const invocation = agentProvider.buildStreamingInvocation({
1979
+ cwd,
1980
+ prompt,
1981
+ model: resolvedModel,
1982
+ variant,
1983
+ mode: opencodeAgentMode,
1984
+ resumeSessionId,
1985
+ sessionId: randomUUID(),
1986
+ });
1987
+ sessionId = invocation.sessionId;
2071
1988
  writeRunStatus('running', null);
2072
1989
  if (tool === 'claude' && sessionId) {
2073
1990
  writeRunSessionId(sessionId);
@@ -2076,6 +1993,8 @@ export const runAgent = ({ ticketId, tool, prompt, cwd, io, model, variant = nul
2076
1993
  spawnTimestamp = Date.now();
2077
1994
  }
2078
1995
  let child;
1996
+ let interrupt;
1997
+ let placementFailure;
2079
1998
  try {
2080
1999
  if (buildState?.stopRequested) {
2081
2000
  throw new AgentSpawnPermitRevokedError();
@@ -2083,28 +2002,37 @@ export const runAgent = ({ ticketId, tool, prompt, cwd, io, model, variant = nul
2083
2002
  if (spawnPermitId && executionContext) {
2084
2003
  assertAgentSpawnPermit(spawnPermitId, executionContext.runId);
2085
2004
  }
2086
- child = spawn(cmd, args, {
2005
+ const handle = executionProvider.execute({
2006
+ target: {
2007
+ kind: 'worktree',
2008
+ id: ticketId,
2009
+ workspacePath: cwd,
2010
+ revision: 'working-tree',
2011
+ },
2012
+ command: invocation.command,
2013
+ args: invocation.args,
2087
2014
  cwd,
2088
- stdio: tool === 'claude' ? ['pipe', 'pipe', 'pipe'] : ['ignore', 'pipe', 'pipe'],
2015
+ stdio: invocation.stdio,
2089
2016
  env: createWorkspaceProcessEnvironment(cwd, { PWD: cwd }),
2090
2017
  detached: process.platform !== 'win32',
2091
2018
  });
2019
+ child = handle.child;
2020
+ interrupt = handle.interrupt;
2021
+ placementFailure = handle.placementFailure;
2092
2022
  }
2093
2023
  catch (error) {
2094
- const resolvedError = error instanceof Error ? error : new Error(String(error));
2024
+ const resolvedError = agentProvider.classifyExecutionFailure(error);
2095
2025
  const transcriptRuns = subagentTracker.finalize('error', resolvedError.message, null);
2096
2026
  rejectOnce(attachTranscriptRunsToError(resolvedError, transcriptRuns));
2097
2027
  return;
2098
2028
  }
2099
2029
  reviewChild = child;
2100
- if (tool === 'claude') {
2101
- const claudePrompt = opencodeAgentMode === 'plan'
2102
- ? appendClaudeHeadlessPlanPrompt(prompt)
2103
- : prompt;
2104
- writeClaudeStdin(child, buildClaudeStdinMessage(claudePrompt, [], cwd));
2030
+ if (invocation.stdin !== null) {
2031
+ writeClaudeStdin(child, invocation.stdin);
2105
2032
  }
2106
2033
  if (trackBuildProcess && buildState) {
2107
2034
  buildState.child = child;
2035
+ buildState.interrupt = interrupt;
2108
2036
  activeBuildProcesses.set(ticketId, buildState);
2109
2037
  }
2110
2038
  if (reviewProcessState) {
@@ -2113,6 +2041,14 @@ export const runAgent = ({ ticketId, tool, prompt, cwd, io, model, variant = nul
2113
2041
  killProcess(child, 'SIGTERM');
2114
2042
  }
2115
2043
  }
2044
+ void placementFailure.then(failure => {
2045
+ if (!failure || settled) {
2046
+ return;
2047
+ }
2048
+ const resolvedError = agentProvider.classifyExecutionFailure(failure);
2049
+ const transcriptRuns = finalizeRun('error', getErrorMessage(resolvedError), null);
2050
+ rejectOnce(attachTranscriptRunsToError(resolvedError, transcriptRuns));
2051
+ });
2116
2052
  startTime = Date.now();
2117
2053
  if (tool === 'opencode' && spawnTimestamp !== null) {
2118
2054
  opencodeSubagentMonitor = createOpencodeSubagentMonitor({
@@ -2207,8 +2143,9 @@ export const runAgent = ({ ticketId, tool, prompt, cwd, io, model, variant = nul
2207
2143
  resolveOnce(withTranscriptRuns({ response: null, responseTiming: null, usageSnapshot }, transcriptRuns));
2208
2144
  return;
2209
2145
  }
2210
- const transcriptRuns = finalizeRun('error', getErrorMessage(err), null);
2211
- rejectOnce(attachTranscriptRunsToError(err, transcriptRuns));
2146
+ const resolvedError = agentProvider.classifyExecutionFailure(err);
2147
+ const transcriptRuns = finalizeRun('error', getErrorMessage(resolvedError), null);
2148
+ rejectOnce(attachTranscriptRunsToError(resolvedError, transcriptRuns));
2212
2149
  });
2213
2150
  return;
2214
2151
  }
@@ -2321,8 +2258,9 @@ export const runAgent = ({ ticketId, tool, prompt, cwd, io, model, variant = nul
2321
2258
  resolveOnce(withTranscriptRuns({ response: null, responseTiming: null }, transcriptRuns));
2322
2259
  return;
2323
2260
  }
2324
- const transcriptRuns = finalizeRun('error', getErrorMessage(err), null);
2325
- rejectOnce(attachTranscriptRunsToError(err, transcriptRuns));
2261
+ const resolvedError = agentProvider.classifyExecutionFailure(err);
2262
+ const transcriptRuns = finalizeRun('error', getErrorMessage(resolvedError), null);
2263
+ rejectOnce(attachTranscriptRunsToError(resolvedError, transcriptRuns));
2326
2264
  });
2327
2265
  });
2328
2266
  };
@@ -3127,6 +3065,7 @@ export const runSpecReviewAgent = ({ specReviewRunId, specSessionId, tool, promp
3127
3065
  });
3128
3066
  return new Promise((resolve, reject) => {
3129
3067
  let spawnTimestamp = null;
3068
+ let opencodeSessionId = null;
3130
3069
  let opencodeSubagentMonitor = null;
3131
3070
  let settled = false;
3132
3071
  let startTime = null;
@@ -3134,6 +3073,7 @@ export const runSpecReviewAgent = ({ specReviewRunId, specSessionId, tool, promp
3134
3073
  let usageSnapshot = EMPTY_CHAT_USAGE_SNAPSHOT;
3135
3074
  const reviewState = registeredState ?? {
3136
3075
  child: null,
3076
+ interrupt: null,
3137
3077
  researchChildren: new Set(),
3138
3078
  stopRequested: false,
3139
3079
  metadataRefresher: null,
@@ -3149,21 +3089,56 @@ export const runSpecReviewAgent = ({ specReviewRunId, specSessionId, tool, promp
3149
3089
  };
3150
3090
  const resolveOnce = settle(resolve);
3151
3091
  const rejectOnce = settle(reject);
3152
- const [cmd, args] = tool === 'claude'
3153
- ? [
3154
- 'claude',
3155
- buildClaudeArgs('plan', resolvedModel, null, randomUUID()),
3156
- ]
3157
- : ['opencode', ['run', '--agent', 'plan', '--format', 'json', '--thinking', ...(resolvedModel ? ['--model', resolvedModel] : []), ...(variant ? ['--variant', variant] : []), ...files.flatMap(filePath => ['--file', filePath]), ...(files.length > 0 ? ['--'] : []), prompt]];
3092
+ const [cmd, args] = [
3093
+ 'claude',
3094
+ buildClaudeArgs('plan', resolvedModel, null, randomUUID()),
3095
+ ];
3158
3096
  updateSpecReviewRunStreaming(specReviewRunId, null);
3159
- const child = spawn(cmd, args, {
3160
- cwd,
3161
- stdio: tool === 'claude' ? ['pipe', 'pipe', 'pipe'] : ['ignore', 'pipe', 'pipe'],
3162
- env: createWorkspaceProcessEnvironment(cwd, { PWD: cwd }),
3163
- detached: process.platform !== 'win32',
3164
- });
3097
+ const env = createWorkspaceProcessEnvironment(cwd, { PWD: cwd });
3098
+ let child;
3099
+ if (tool === 'opencode') {
3100
+ const invocation = buildOpencodeSpecReviewInvocation({ prompt, files, model: resolvedModel, variant });
3101
+ try {
3102
+ const handle = hostExecutionEnvironment.execute({
3103
+ target: {
3104
+ kind: 'repository',
3105
+ id: cwd,
3106
+ workspacePath: cwd,
3107
+ revision: 'working-tree',
3108
+ },
3109
+ command: invocation.command,
3110
+ args: invocation.args,
3111
+ cwd,
3112
+ stdio: invocation.stdio,
3113
+ env,
3114
+ detached: process.platform !== 'win32',
3115
+ });
3116
+ child = handle.child;
3117
+ reviewState.interrupt = handle.interrupt;
3118
+ void handle.placementFailure.then(failure => {
3119
+ if (failure) {
3120
+ rejectOnce(opencodeAgentProvider.classifyExecutionFailure(failure));
3121
+ }
3122
+ });
3123
+ }
3124
+ catch (error) {
3125
+ rejectOnce(opencodeAgentProvider.classifyExecutionFailure(error));
3126
+ return;
3127
+ }
3128
+ }
3129
+ else {
3130
+ child = spawn(cmd, args, {
3131
+ cwd,
3132
+ stdio: ['pipe', 'pipe', 'pipe'],
3133
+ env,
3134
+ detached: process.platform !== 'win32',
3135
+ });
3136
+ }
3165
3137
  reviewState.child = child;
3166
3138
  activeSpecReviewProcesses.set(specReviewRunId, reviewState);
3139
+ if (reviewState.stopRequested) {
3140
+ interruptProcess(reviewState, 'SIGTERM');
3141
+ }
3167
3142
  if (tool === 'claude') {
3168
3143
  writeClaudeStdin(child, buildClaudeStdinMessage(appendClaudeHeadlessPlanPrompt(prompt), files, cwd));
3169
3144
  }
@@ -3287,6 +3262,7 @@ export const runSpecReviewAgent = ({ specReviewRunId, specSessionId, tool, promp
3287
3262
  const parsed = parseOpencodeJsonLine(line, cwd);
3288
3263
  const { logFragment, responseFragment, subagentEvents, providerSessionId } = parsed;
3289
3264
  opencodeSubagentMonitor?.setParentSessionId(providerSessionId);
3265
+ opencodeSessionId = providerSessionId ?? opencodeSessionId;
3290
3266
  subagentTracker.handle(subagentEvents);
3291
3267
  if (appendOpencodeTranscriptLine({ parsed, logLines, responseLines, recoveryTracker })) {
3292
3268
  firstFinalAnswerTime = null;
@@ -3304,6 +3280,7 @@ export const runSpecReviewAgent = ({ specReviewRunId, specSessionId, tool, promp
3304
3280
  usageSnapshot = mergeUsageFromJsonLine(usageSnapshot, remainder);
3305
3281
  const parsed = parseOpencodeJsonLine(remainder, cwd);
3306
3282
  opencodeSubagentMonitor?.setParentSessionId(parsed.providerSessionId);
3283
+ opencodeSessionId = parsed.providerSessionId ?? opencodeSessionId;
3307
3284
  subagentTracker.handle(parsed.subagentEvents);
3308
3285
  if (appendOpencodeTranscriptLine({ parsed, logLines, responseLines, recoveryTracker })) {
3309
3286
  firstFinalAnswerTime = null;
@@ -3315,6 +3292,13 @@ export const runSpecReviewAgent = ({ specReviewRunId, specSessionId, tool, promp
3315
3292
  const finalResponse = recoveryTracker.hasRecoveredFinalAnswer()
3316
3293
  ? recoveredResponse
3317
3294
  : responseLines.join('\n\n') || stderrOutput;
3295
+ if (opencodeSessionId && spawnTimestamp !== null) {
3296
+ usageSnapshot = resolveOpencodeUsageSnapshot({
3297
+ current: usageSnapshot,
3298
+ sessionId: opencodeSessionId,
3299
+ spawnTimestamp,
3300
+ });
3301
+ }
3318
3302
  if (reviewState.stopRequested) {
3319
3303
  updateSpecReviewRunStreaming(specReviewRunId, finalResponse || null);
3320
3304
  subagentTracker.finalize('stopped', log || null, finalResponse || null);
@@ -3344,8 +3328,25 @@ export const runSpecReviewAgent = ({ specReviewRunId, specSessionId, tool, promp
3344
3328
  });
3345
3329
  child.on('error', err => {
3346
3330
  progressUpdater.cancel();
3347
- subagentTracker.finalize(reviewState.stopRequested ? 'stopped' : 'error', getErrorMessage(err), null);
3348
- rejectOnce(err instanceof Error ? err : new Error(getErrorMessage(err)));
3331
+ if (reviewState.stopRequested) {
3332
+ const processError = getErrorMessage(err);
3333
+ const finalLog = logLines.join('\n\n') || processError;
3334
+ const finalResponse = recoveryTracker.hasRecoveredFinalAnswer()
3335
+ ? recoveryTracker.getRecoveredResponse()
3336
+ : responseLines.join('\n\n') || null;
3337
+ updateSpecReviewRunStreaming(specReviewRunId, finalResponse);
3338
+ subagentTracker.finalize('stopped', finalLog || null, finalResponse);
3339
+ resolveOnce({
3340
+ completionStatus: 'stopped',
3341
+ finalResponse,
3342
+ usageSnapshot,
3343
+ responseTiming: buildResponseTiming({ startTime, firstFinalAnswerTime, isStopped: true }),
3344
+ });
3345
+ return;
3346
+ }
3347
+ const failure = opencodeAgentProvider.classifyExecutionFailure(err);
3348
+ subagentTracker.finalize('error', failure.message, null);
3349
+ rejectOnce(failure);
3349
3350
  });
3350
3351
  });
3351
3352
  };