@axhub/genie 0.2.11 → 0.2.12

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/api-docs.html +2 -2
  2. package/dist/assets/App-Clb2COtW.js +274 -0
  3. package/dist/assets/ImagePlaygroundPage-DqhMSbM8.js +106 -0
  4. package/dist/assets/ImagePlaygroundPage-MEn3NN80.css +1 -0
  5. package/dist/assets/ReviewApp-CDcLYe-u.js +1 -0
  6. package/dist/assets/{_basePickBy-BDnj7-0Z.js → _basePickBy-jUZsM51q.js} +1 -1
  7. package/dist/assets/{_baseUniq-Bl0JKOyl.js → _baseUniq-BXglE6_v.js} +1 -1
  8. package/dist/assets/{arc-DY-4Kev3.js → arc-D-oFCFBv.js} +1 -1
  9. package/dist/assets/{architectureDiagram-2XIMDMQ5-qw7crNVd.js → architectureDiagram-2XIMDMQ5-DC8bAnQt.js} +1 -1
  10. package/dist/assets/{blockDiagram-WCTKOSBZ-B9xg7ep3.js → blockDiagram-WCTKOSBZ-C4semIRc.js} +1 -1
  11. package/dist/assets/{c4Diagram-IC4MRINW-H9xp3ytb.js → c4Diagram-IC4MRINW-FHj1QO3y.js} +1 -1
  12. package/dist/assets/channel-BF4woPXX.js +1 -0
  13. package/dist/assets/{chunk-4BX2VUAB-B3EVDUxI.js → chunk-4BX2VUAB-D-LjsQ_s.js} +1 -1
  14. package/dist/assets/{chunk-55IACEB6-CGv945ef.js → chunk-55IACEB6-DI3j_d7A.js} +1 -1
  15. package/dist/assets/{chunk-FMBD7UC4-uAT4CKWM.js → chunk-FMBD7UC4-BEVnaLFN.js} +1 -1
  16. package/dist/assets/{chunk-JSJVCQXG-Cbvlpkf7.js → chunk-JSJVCQXG-CSxpcErk.js} +1 -1
  17. package/dist/assets/{chunk-KX2RTZJC-CcqIuGat.js → chunk-KX2RTZJC-BbuhDN4h.js} +1 -1
  18. package/dist/assets/{chunk-NQ4KR5QH-CgrcsRuX.js → chunk-NQ4KR5QH-C3x61XQa.js} +1 -1
  19. package/dist/assets/{chunk-QZHKN3VN-Cx0APOoV.js → chunk-QZHKN3VN-DxWOFtPh.js} +1 -1
  20. package/dist/assets/{chunk-WL4C6EOR-BbZirvBk.js → chunk-WL4C6EOR-Bt2OauD2.js} +1 -1
  21. package/dist/assets/classDiagram-VBA2DB6C-D2kHlnQ7.js +1 -0
  22. package/dist/assets/classDiagram-v2-RAHNMMFH-D2kHlnQ7.js +1 -0
  23. package/dist/assets/clone-CqBvwCJW.js +1 -0
  24. package/dist/assets/{cose-bilkent-S5V4N54A-CrvmGFLD.js → cose-bilkent-S5V4N54A-Dexadrue.js} +1 -1
  25. package/dist/assets/{dagre-KLK3FWXG-C-W6VPjS.js → dagre-KLK3FWXG-F9U4X2xC.js} +1 -1
  26. package/dist/assets/{diagram-E7M64L7V-IP2q3bL0.js → diagram-E7M64L7V-B3V17aH3.js} +1 -1
  27. package/dist/assets/{diagram-IFDJBPK2-CQaL-XyV.js → diagram-IFDJBPK2-CdHAmLL1.js} +1 -1
  28. package/dist/assets/{diagram-P4PSJMXO-BxBLThfv.js → diagram-P4PSJMXO-CrTNfk8K.js} +1 -1
  29. package/dist/assets/{erDiagram-INFDFZHY-Dyl7bJTt.js → erDiagram-INFDFZHY-vDh9SWK9.js} +1 -1
  30. package/dist/assets/{flowDiagram-PKNHOUZH-B7NFMgFK.js → flowDiagram-PKNHOUZH-DpltMg7L.js} +1 -1
  31. package/dist/assets/{ganttDiagram-A5KZAMGK-hReWSDu2.js → ganttDiagram-A5KZAMGK-COTk2xur.js} +1 -1
  32. package/dist/assets/{gitGraphDiagram-K3NZZRJ6-gVgcr0ST.js → gitGraphDiagram-K3NZZRJ6-BNV7bvvj.js} +1 -1
  33. package/dist/assets/{graph-DNDiJhTn.js → graph-Dkeg9oys.js} +1 -1
  34. package/dist/assets/{highlighted-body-TPN3WLV5-DclLmTou.js → highlighted-body-TPN3WLV5-DaiQEBwR.js} +1 -1
  35. package/dist/assets/index-DgGmiqsP.css +1 -0
  36. package/dist/assets/index-DvA901Vs.js +2 -0
  37. package/dist/assets/{infoDiagram-LFFYTUFH-CqQOOzDA.js → infoDiagram-LFFYTUFH-CZioW3Gt.js} +1 -1
  38. package/dist/assets/{ishikawaDiagram-PHBUUO56-CZ0iLiHg.js → ishikawaDiagram-PHBUUO56-BbqR3i1B.js} +1 -1
  39. package/dist/assets/{journeyDiagram-4ABVD52K-DdfYKfNh.js → journeyDiagram-4ABVD52K-wfb-WHzl.js} +1 -1
  40. package/dist/assets/{kanban-definition-K7BYSVSG-C5Vf32u6.js → kanban-definition-K7BYSVSG-B3c4y3VN.js} +1 -1
  41. package/dist/assets/{layout-rvTEu2KS.js → layout-Xr9Z2VGF.js} +1 -1
  42. package/dist/assets/{linear-CD9SiYze.js → linear-JBmzAJtl.js} +1 -1
  43. package/dist/assets/{mermaid-O7DHMXV3-OZ8qWWwa.js → mermaid-O7DHMXV3-fDuyNLKe.js} +230 -222
  44. package/dist/assets/{mindmap-definition-YRQLILUH-CQxrLNVc.js → mindmap-definition-YRQLILUH-B5NTN_jD.js} +1 -1
  45. package/dist/assets/{pieDiagram-SKSYHLDU-XgAUByWg.js → pieDiagram-SKSYHLDU-CuO98GVu.js} +1 -1
  46. package/dist/assets/{quadrantDiagram-337W2JSQ-CH16ls7G.js → quadrantDiagram-337W2JSQ-LL3f4vLf.js} +1 -1
  47. package/dist/assets/{requirementDiagram-Z7DCOOCP-B_kQO06L.js → requirementDiagram-Z7DCOOCP-Di-2O6LH.js} +1 -1
  48. package/dist/assets/{sankeyDiagram-WA2Y5GQK-ofe78CyS.js → sankeyDiagram-WA2Y5GQK-9lHqrXqR.js} +1 -1
  49. package/dist/assets/{sequenceDiagram-2WXFIKYE-Ckbxwny6.js → sequenceDiagram-2WXFIKYE-BQu-SoGr.js} +1 -1
  50. package/dist/assets/{stateDiagram-RAJIS63D-DNtzCk14.js → stateDiagram-RAJIS63D-BUxvd2BC.js} +1 -1
  51. package/dist/assets/stateDiagram-v2-FVOUBMTO-CDVexTiR.js +1 -0
  52. package/dist/assets/{timeline-definition-YZTLITO2-zT6CklKt.js → timeline-definition-YZTLITO2-oP47UEU6.js} +1 -1
  53. package/dist/assets/{treemap-KZPCXAKY-y0U2c3xG.js → treemap-KZPCXAKY-BRjDo2aE.js} +1 -1
  54. package/dist/assets/{vendor-codemirror-CMHSJ_9p.js → vendor-codemirror-BiCeS-y4.js} +1 -1
  55. package/dist/assets/{vendor-react-xmA_f8ig.js → vendor-react-DVlYPmi3.js} +1 -1
  56. package/dist/assets/{vennDiagram-LZ73GAT5-xKj3SjYG.js → vennDiagram-LZ73GAT5-DrRqcDqo.js} +1 -1
  57. package/dist/assets/{xychartDiagram-JWTSCODW-Da_qyEoX.js → xychartDiagram-JWTSCODW-DUXrymAi.js} +1 -1
  58. package/dist/index.html +4 -4
  59. package/package.json +25 -6
  60. package/scripts/refresh-acp-default-capabilities.mjs +160 -0
  61. package/server/acp-runtime/client.js +1137 -181
  62. package/server/acp-runtime/command-overrides.js +48 -0
  63. package/server/acp-runtime/index.js +576 -16
  64. package/server/acp-runtime/registry.js +6 -4
  65. package/server/acp-runtime/session-store.js +235 -92
  66. package/server/database/db.js +12 -3
  67. package/server/external-agent/ws.js +212 -11
  68. package/server/index.js +145 -52
  69. package/server/projects-watcher-config.js +4 -0
  70. package/server/projects.js +466 -125
  71. package/server/routes/cc-connect.js +5 -4
  72. package/server/routes/codex.js +24 -0
  73. package/server/routes/commands.js +144 -1
  74. package/server/routes/runs.js +641 -0
  75. package/server/routes/session-core.js +357 -109
  76. package/server/session-core/eventStore.js +0 -121
  77. package/server/session-core/providerAdapters.js +644 -163
  78. package/server/session-core/providerDiscovery.js +66 -38
  79. package/server/session-core/runRegistry.js +244 -0
  80. package/server/session-core/runtimeState.js +75 -3
  81. package/server/session-core/runtimeWriter.js +132 -10
  82. package/server/utils/codexImagePlayground.js +479 -0
  83. package/server/utils/localTerminal.js +56 -0
  84. package/server/utils/shellCommand.js +70 -0
  85. package/shared/acpCapabilities.js +393 -0
  86. package/shared/acpDefaultCapabilities.generated.json +141 -0
  87. package/shared/conversationEvents.js +425 -121
  88. package/dist/assets/App-VH1wNUHs.js +0 -259
  89. package/dist/assets/ReviewApp-D_9EN4TM.js +0 -1
  90. package/dist/assets/channel-CyNUnRfc.js +0 -1
  91. package/dist/assets/classDiagram-VBA2DB6C-DxBtyz2A.js +0 -1
  92. package/dist/assets/classDiagram-v2-RAHNMMFH-DxBtyz2A.js +0 -1
  93. package/dist/assets/clone-C341l3d0.js +0 -1
  94. package/dist/assets/index-DBkz_W_P.css +0 -1
  95. package/dist/assets/index-DdRyoXKh.js +0 -2
  96. package/dist/assets/stateDiagram-v2-FVOUBMTO-B3VPhiE1.js +0 -1
@@ -1,209 +1,690 @@
1
- import { normalizeLegacyHistoryEntries, SUPPORTED_PROVIDERS } from '../../shared/conversationEvents.js';
2
- import { mergeConversationEventHistories, readMirroredConversationEvents } from './eventStore.js';
3
1
  import {
4
- getSessionMessages,
5
- getCodexSessionMessages,
6
- getOpencodeSessionMessages,
7
- getGeminiSessionMessages,
8
- getSessions,
9
- getCodexSessions,
10
- getOpencodeSessions,
11
- getGeminiSessions
12
- } from '../projects.js';
2
+ isConversationEvent,
3
+ SUPPORTED_PROVIDERS
4
+ } from '../../shared/conversationEvents.js';
5
+ import {
6
+ disposeManagedAgentClient,
7
+ getOrStartAgentClient,
8
+ isAgentSessionActive,
9
+ retainSessionAgentClient,
10
+ shouldSkipGeminiAutomaticAcpStartup
11
+ } from '../acp-runtime/index.js';
12
+ import { mergeAgentCommandOverrides } from '../acp-runtime/command-overrides.js';
13
13
  import {
14
14
  findAcpSessionRecord,
15
- listAcpSessions
15
+ listAcpSessions,
16
+ touchAcpSessionRecord
16
17
  } from '../acp-runtime/session-store.js';
17
- import { mergeSessionLists } from './sessionListMerge.js';
18
18
 
19
- async function flattenLegacyMessages(result) {
20
- if (Array.isArray(result)) return result;
21
- return Array.isArray(result?.messages) ? result.messages : [];
19
+ function getAgentCommandOverrides(options = {}) {
20
+ return mergeAgentCommandOverrides(
21
+ process.env.AXHUB_ACP_COMMAND_OVERRIDES,
22
+ options.agentCommandOverrides
23
+ );
24
+ }
25
+
26
+ function createAcpReplayWriter() {
27
+ const events = [];
28
+
29
+ return {
30
+ events,
31
+ send(payload) {
32
+ if (payload?.type === 'conversation-event' && isConversationEvent(payload.event)) {
33
+ events.push(payload.event);
34
+ }
35
+ },
36
+ setSessionId() {},
37
+ getSessionId() {
38
+ return null;
39
+ }
40
+ };
41
+ }
42
+
43
+ const acpSessionEventCache = new Map();
44
+
45
+ function createSessionEventCacheKey(provider, sessionId) {
46
+ const normalizedProvider = normalizeProvider(provider);
47
+ const normalizedSessionId = normalizeSessionId(sessionId);
48
+ if (!normalizedProvider || !normalizedSessionId) {
49
+ return null;
50
+ }
51
+ return `${normalizedProvider}:${normalizedSessionId}`;
52
+ }
53
+
54
+ function cloneJsonValue(value) {
55
+ if (value === undefined) {
56
+ return undefined;
57
+ }
58
+
59
+ return JSON.parse(JSON.stringify(value));
60
+ }
61
+
62
+ function normalizeComparableText(value) {
63
+ return String(value || '').replace(/\r\n/g, '\n').trim();
64
+ }
65
+
66
+ function getEventMessageId(event) {
67
+ const rawValue = event?.payload?.messageId || event?.extensions?.messageId || null;
68
+ return typeof rawValue === 'string' && rawValue.trim() ? rawValue.trim() : null;
69
+ }
70
+
71
+ function getUserMessageText(event) {
72
+ return event?.kind === 'user_message'
73
+ ? normalizeComparableText(event?.payload?.text)
74
+ : '';
75
+ }
76
+
77
+ function isReplayEvent(event) {
78
+ return Boolean(
79
+ event?.extensions?.runtimeReplay === true ||
80
+ event?.rawRef?.sourceType === 'acp-session-loaded-replay'
81
+ );
82
+ }
83
+
84
+ function getReplayOriginalTimestamp(event) {
85
+ const rawValue = event?.extensions?.originalTimestamp || event?.payload?.originalTimestamp || null;
86
+ if (typeof rawValue !== 'string' || !rawValue.trim()) {
87
+ return null;
88
+ }
89
+
90
+ const parsed = Date.parse(rawValue);
91
+ return Number.isFinite(parsed) ? rawValue : null;
92
+ }
93
+
94
+ function getEventSortTime(event) {
95
+ const replayTimestamp = isReplayEvent(event) ? getReplayOriginalTimestamp(event) : null;
96
+ const parsed = Date.parse(replayTimestamp || event?.timestamp || '');
97
+ return Number.isFinite(parsed) ? parsed : 0;
98
+ }
99
+
100
+ function getEventOrderRank(event) {
101
+ if (event?.kind === 'user_message') {
102
+ return 0;
103
+ }
104
+ if (
105
+ event?.kind === 'assistant_text_start' ||
106
+ event?.kind === 'assistant_text_delta' ||
107
+ event?.kind === 'assistant_text_end' ||
108
+ event?.kind === 'reasoning_start' ||
109
+ event?.kind === 'reasoning_delta' ||
110
+ event?.kind === 'reasoning_end'
111
+ ) {
112
+ return 1;
113
+ }
114
+ if (event?.kind === 'tool_call_start' || event?.kind === 'tool_call_input') {
115
+ return 2;
116
+ }
117
+ if (event?.kind === 'tool_call_end' || event?.kind === 'tool_result') {
118
+ return 3;
119
+ }
120
+ return 4;
121
+ }
122
+
123
+ function buildEventSignature(event) {
124
+ return JSON.stringify({
125
+ kind: event.kind,
126
+ provider: event.provider,
127
+ sessionId: event.sessionId,
128
+ payload: event.payload || {},
129
+ extensions: event.extensions || {},
130
+ rawRef: event.rawRef || null
131
+ });
22
132
  }
23
133
 
24
- function hasReplayableTranscriptContent(events = []) {
25
- return (Array.isArray(events) ? events : []).some((event) => (
26
- event?.kind === 'user_message' ||
134
+ function getEventTieBreakerKey(event) {
135
+ if (event?.kind === 'user_message') {
136
+ return getUserMessageText(event);
137
+ }
138
+ if (
139
+ event?.kind === 'assistant_text_start' ||
27
140
  event?.kind === 'assistant_text_delta' ||
28
- event?.kind === 'assistant_content_block' ||
141
+ event?.kind === 'assistant_text_end' ||
142
+ event?.kind === 'reasoning_start' ||
29
143
  event?.kind === 'reasoning_delta' ||
30
- event?.kind === 'tool_call_start' ||
31
- event?.kind === 'tool_result' ||
32
- event?.kind === 'plan_update' ||
33
- event?.kind === 'system_notice'
34
- ));
144
+ event?.kind === 'reasoning_end'
145
+ ) {
146
+ return typeof event?.payload?.messageId === 'string' ? event.payload.messageId : '';
147
+ }
148
+ return '';
35
149
  }
36
150
 
37
- async function normalizeLegacyLoadResult(result, provider, sessionId) {
38
- const messages = await flattenLegacyMessages(result);
39
- const legacyEvents = normalizeLegacyHistoryEntries(messages, provider, sessionId);
40
- const mirroredEvents = await readMirroredConversationEvents(provider, sessionId);
41
- const events = mergeConversationEventHistories(legacyEvents, mirroredEvents);
151
+ function createEventIdentityKey(event) {
152
+ if (event?.eventId) {
153
+ return `id:${event.eventId}`;
154
+ }
155
+
156
+ return `signature:${buildEventSignature(event)}`;
157
+ }
42
158
 
43
- if (Array.isArray(result)) {
44
- return events;
159
+ function findNearestUserTextBeforeIndex(events = [], index = -1) {
160
+ for (let i = index - 1; i >= 0; i -= 1) {
161
+ const userText = getUserMessageText(events[i]);
162
+ if (userText) {
163
+ return userText;
164
+ }
45
165
  }
46
166
 
47
- return {
48
- ...result,
49
- events,
50
- source: 'legacy'
167
+ return '';
168
+ }
169
+
170
+ function shouldTreatUserMessagesAsDuplicates(leftEvent, rightEvent) {
171
+ if (leftEvent?.kind !== 'user_message' || rightEvent?.kind !== 'user_message') {
172
+ return false;
173
+ }
174
+
175
+ const leftText = getUserMessageText(leftEvent);
176
+ const rightText = getUserMessageText(rightEvent);
177
+ if (!leftText || leftText !== rightText) {
178
+ return false;
179
+ }
180
+
181
+ const leftClientRequestId = leftEvent?.extensions?.clientRequestId || null;
182
+ const rightClientRequestId = rightEvent?.extensions?.clientRequestId || null;
183
+ if (leftClientRequestId && rightClientRequestId && leftClientRequestId === rightClientRequestId) {
184
+ return true;
185
+ }
186
+
187
+ return isReplayEvent(leftEvent) || isReplayEvent(rightEvent);
188
+ }
189
+
190
+ function collectAssistantTextGroups(events = []) {
191
+ const groupsByMessageId = new Map();
192
+ const groups = [];
193
+ let lastUserText = '';
194
+
195
+ const ensureGroup = (event) => {
196
+ const messageId = getEventMessageId(event) || `anonymous:${groups.length}`;
197
+ let group = groupsByMessageId.get(messageId);
198
+ if (!group) {
199
+ group = {
200
+ messageId,
201
+ userText: lastUserText,
202
+ events: [],
203
+ text: '',
204
+ };
205
+ groupsByMessageId.set(messageId, group);
206
+ groups.push(group);
207
+ }
208
+ return group;
51
209
  };
210
+
211
+ for (const event of Array.isArray(events) ? events : []) {
212
+ if (!isConversationEvent(event)) {
213
+ continue;
214
+ }
215
+
216
+ const userText = getUserMessageText(event);
217
+ if (userText) {
218
+ lastUserText = userText;
219
+ continue;
220
+ }
221
+
222
+ if (
223
+ event.kind !== 'assistant_text_start' &&
224
+ event.kind !== 'assistant_text_delta' &&
225
+ event.kind !== 'assistant_text_end'
226
+ ) {
227
+ continue;
228
+ }
229
+
230
+ const group = ensureGroup(event);
231
+ group.events.push(event);
232
+ if (event.kind === 'assistant_text_delta') {
233
+ group.text += String(event.payload?.text || '');
234
+ }
235
+ }
236
+
237
+ return groups
238
+ .map((group) => ({
239
+ ...group,
240
+ text: normalizeComparableText(group.text),
241
+ }))
242
+ .filter((group) => group.text);
52
243
  }
53
244
 
54
- function createEventLoadResult(events = [], source = 'acp') {
245
+ function findMatchingUserEventIndex(events, userText) {
246
+ const normalizedUserText = normalizeComparableText(userText);
247
+ if (!normalizedUserText) {
248
+ return -1;
249
+ }
250
+
251
+ return events.findIndex((event) => getUserMessageText(event) === normalizedUserText);
252
+ }
253
+
254
+ function insertEventsAfterMatchingUser(events, userText, eventsToInsert) {
255
+ const insertIndex = findMatchingUserEventIndex(events, userText);
256
+ if (insertIndex < 0) {
257
+ return [...events, ...eventsToInsert];
258
+ }
259
+
260
+ return [
261
+ ...events.slice(0, insertIndex + 1),
262
+ ...eventsToInsert,
263
+ ...events.slice(insertIndex + 1),
264
+ ];
265
+ }
266
+
267
+ function relocateCachedAssistantGroupsAfterMatchingUsers(events = [], cachedEvents = []) {
268
+ let relocatedEvents = Array.isArray(events) ? events : [];
269
+
270
+ for (const cachedGroup of collectAssistantTextGroups(cachedEvents)) {
271
+ const normalizedUserText = normalizeComparableText(cachedGroup.userText);
272
+ if (!normalizedUserText) {
273
+ continue;
274
+ }
275
+
276
+ const groupKeys = new Set(
277
+ cachedGroup.events.map(createEventIdentityKey).filter(Boolean)
278
+ );
279
+ if (groupKeys.size === 0) {
280
+ continue;
281
+ }
282
+
283
+ const groupIndexes = [];
284
+ relocatedEvents.forEach((event, index) => {
285
+ if (groupKeys.has(createEventIdentityKey(event))) {
286
+ groupIndexes.push(index);
287
+ }
288
+ });
289
+ if (groupIndexes.length === 0) {
290
+ continue;
291
+ }
292
+
293
+ const firstGroupIndex = groupIndexes[0];
294
+ if (findNearestUserTextBeforeIndex(relocatedEvents, firstGroupIndex) === normalizedUserText) {
295
+ continue;
296
+ }
297
+
298
+ const groupIndexSet = new Set(groupIndexes);
299
+ const groupEvents = groupIndexes.map((index) => relocatedEvents[index]);
300
+ const eventsWithoutGroup = relocatedEvents.filter((_, index) => !groupIndexSet.has(index));
301
+ relocatedEvents = insertEventsAfterMatchingUser(
302
+ eventsWithoutGroup,
303
+ normalizedUserText,
304
+ groupEvents
305
+ );
306
+ }
307
+
308
+ return relocatedEvents;
309
+ }
310
+
311
+ function mergeCachedAssistantReplayEvents(currentEvents = [], cachedEvents = []) {
312
+ const normalizedCurrentEvents = (Array.isArray(currentEvents) ? currentEvents : []).filter(isConversationEvent);
313
+ const normalizedCachedEvents = (Array.isArray(cachedEvents) ? cachedEvents : []).filter(isConversationEvent);
314
+
315
+ if (normalizedCurrentEvents.length === 0) {
316
+ return normalizedCachedEvents.map(cloneJsonValue);
317
+ }
318
+
319
+ let mergedEvents = normalizedCurrentEvents.map(cloneJsonValue);
320
+ const mergedEventIds = new Set(
321
+ mergedEvents.map((event) => event.eventId).filter(Boolean)
322
+ );
323
+ const mergedEventSignatures = new Set(
324
+ mergedEvents.map(buildEventSignature)
325
+ );
326
+
327
+ for (const cachedEvent of normalizedCachedEvents) {
328
+ if (cachedEvent?.eventId && mergedEventIds.has(cachedEvent.eventId)) {
329
+ continue;
330
+ }
331
+
332
+ if (
333
+ cachedEvent.kind === 'user_message' &&
334
+ mergedEvents.some((event) => shouldTreatUserMessagesAsDuplicates(event, cachedEvent))
335
+ ) {
336
+ continue;
337
+ }
338
+
339
+ const signature = buildEventSignature(cachedEvent);
340
+ if (mergedEventSignatures.has(signature)) {
341
+ continue;
342
+ }
343
+
344
+ mergedEvents.push(cloneJsonValue(cachedEvent));
345
+ if (cachedEvent?.eventId) {
346
+ mergedEventIds.add(cachedEvent.eventId);
347
+ }
348
+ mergedEventSignatures.add(signature);
349
+ }
350
+
351
+ mergedEvents = mergedEvents.map((event, index) => ({ event, index }));
352
+ mergedEvents.sort((leftEntry, rightEntry) => {
353
+ const left = leftEntry.event;
354
+ const right = rightEntry.event;
355
+ const leftTime = getEventSortTime(left);
356
+ const rightTime = getEventSortTime(right);
357
+ if (leftTime !== rightTime) {
358
+ return leftTime - rightTime;
359
+ }
360
+ const rankDelta = getEventOrderRank(left) - getEventOrderRank(right);
361
+ if (rankDelta !== 0) {
362
+ const leftKey = getEventTieBreakerKey(left);
363
+ const rightKey = getEventTieBreakerKey(right);
364
+ if (leftKey && rightKey && leftKey === rightKey) {
365
+ return rankDelta;
366
+ }
367
+ }
368
+ return leftEntry.index - rightEntry.index;
369
+ });
370
+ mergedEvents = mergedEvents.map((entry) => entry.event);
371
+ mergedEvents = relocateCachedAssistantGroupsAfterMatchingUsers(
372
+ mergedEvents,
373
+ normalizedCachedEvents
374
+ );
375
+
376
+ const currentAssistantTexts = new Set(
377
+ collectAssistantTextGroups(mergedEvents)
378
+ .map((group) => group.text)
379
+ .filter(Boolean)
380
+ );
381
+
382
+ for (const cachedGroup of collectAssistantTextGroups(normalizedCachedEvents)) {
383
+ if (currentAssistantTexts.has(cachedGroup.text)) {
384
+ continue;
385
+ }
386
+
387
+ mergedEvents = insertEventsAfterMatchingUser(
388
+ mergedEvents,
389
+ cachedGroup.userText,
390
+ cachedGroup.events.map(cloneJsonValue)
391
+ );
392
+ currentAssistantTexts.add(cachedGroup.text);
393
+ }
394
+
395
+ return mergedEvents;
396
+ }
397
+
398
+ function readCachedSessionEvents(provider, sessionId) {
399
+ const cacheKey = createSessionEventCacheKey(provider, sessionId);
400
+ if (!cacheKey) {
401
+ return [];
402
+ }
403
+
404
+ return (acpSessionEventCache.get(cacheKey) || []).map(cloneJsonValue);
405
+ }
406
+
407
+ function rememberSessionEvents(provider, sessionId, events = []) {
408
+ const cacheKey = createSessionEventCacheKey(provider, sessionId);
409
+ if (!cacheKey) {
410
+ return Array.isArray(events) ? events : [];
411
+ }
412
+
413
+ const mergedEvents = mergeCachedAssistantReplayEvents(events, acpSessionEventCache.get(cacheKey) || []);
414
+ acpSessionEventCache.set(cacheKey, mergedEvents.map(cloneJsonValue));
415
+ return mergedEvents.map(cloneJsonValue);
416
+ }
417
+
418
+ function paginateEventsFromTail(events = [], limit = null, offset = 0) {
55
419
  const normalizedEvents = Array.isArray(events) ? events : [];
420
+ const normalizedLimit = limit === null ? null : Math.max(0, Number(limit) || 0);
421
+ const normalizedOffset = Math.max(0, Number(offset) || 0);
422
+ const total = normalizedEvents.length;
423
+
424
+ if (normalizedLimit === null) {
425
+ return {
426
+ events: normalizedEvents,
427
+ total,
428
+ hasMore: false,
429
+ offset: 0,
430
+ limit: null
431
+ };
432
+ }
433
+
434
+ const endIndex = Math.max(0, total - normalizedOffset);
435
+ const startIndex = Math.max(0, endIndex - normalizedLimit);
436
+ const paginatedEvents = normalizedEvents.slice(startIndex, endIndex);
437
+
56
438
  return {
57
- events: normalizedEvents,
58
- total: normalizedEvents.length,
59
- hasMore: false,
60
- offset: 0,
61
- limit: null,
439
+ events: paginatedEvents,
440
+ total,
441
+ hasMore: total > normalizedOffset + paginatedEvents.length,
442
+ offset: normalizedOffset,
443
+ limit: normalizedLimit
444
+ };
445
+ }
446
+
447
+ function createEventLoadResult(events = [], source = 'acp', options = {}) {
448
+ return {
449
+ ...paginateEventsFromTail(events, options.limit, options.offset),
62
450
  source
63
451
  };
64
452
  }
65
453
 
66
- async function loadAcpEvents(provider, sessionId, nativeHistoryLoader = null) {
67
- const record = await findAcpSessionRecord(sessionId, provider);
68
- if (!record) {
454
+ function getLastEventTimestamp(events = []) {
455
+ const timestamps = (Array.isArray(events) ? events : [])
456
+ .map((event) => Date.parse(event?.timestamp || ''))
457
+ .filter(Number.isFinite);
458
+ if (timestamps.length === 0) {
69
459
  return null;
70
460
  }
461
+ return new Date(Math.max(...timestamps)).toISOString();
462
+ }
71
463
 
72
- const mirroredEvents = await readMirroredConversationEvents(provider, sessionId);
73
- if (hasReplayableTranscriptContent(mirroredEvents)) {
74
- return createEventLoadResult(mirroredEvents, 'acp');
464
+ function getLastUserEventTimestamp(events = []) {
465
+ const timestamps = (Array.isArray(events) ? events : [])
466
+ .filter((event) => event?.kind === 'user_message')
467
+ .map((event) => Date.parse(event?.timestamp || ''))
468
+ .filter(Number.isFinite);
469
+ if (timestamps.length === 0) {
470
+ return null;
75
471
  }
472
+ return new Date(Math.max(...timestamps)).toISOString();
473
+ }
76
474
 
77
- if (typeof nativeHistoryLoader === 'function') {
78
- try {
79
- const nativeResult = await nativeHistoryLoader();
80
- const normalizedResult = await normalizeLegacyLoadResult(nativeResult, provider, sessionId);
81
- if (Array.isArray(normalizedResult)) {
82
- return createEventLoadResult(normalizedResult, 'acp');
83
- }
84
- return {
85
- ...normalizedResult,
86
- source: 'acp'
87
- };
88
- } catch {
89
- // Fall back to the mirrored event store when native history is unavailable.
90
- }
475
+ function isAcpMissingResourceError(error) {
476
+ const message = String(error?.message || error || '').trim().toLowerCase();
477
+ return message === 'resource not found'
478
+ || message.includes('resource not found')
479
+ || message.includes('session not found');
480
+ }
481
+
482
+ function getSessionActivityTime(session) {
483
+ return new Date(
484
+ session?.lastActivity
485
+ || session?.updatedAt
486
+ || session?.updated_at
487
+ || session?.createdAt
488
+ || session?.created_at
489
+ || 0
490
+ ).getTime();
491
+ }
492
+
493
+ function normalizeSessionId(value) {
494
+ const normalized = String(value || '').trim();
495
+ return normalized || null;
496
+ }
497
+
498
+ function normalizeProvider(value) {
499
+ return String(value || '').trim().toLowerCase();
500
+ }
501
+
502
+ const GENERIC_ACP_SESSION_TITLE_PATTERN = /^ACP Session\b/i;
503
+
504
+ function isProbeLikeAcpSession(session) {
505
+ if (!session || session.source !== 'acp') {
506
+ return false;
91
507
  }
92
508
 
93
- return createEventLoadResult(mirroredEvents, 'acp');
509
+ const lastPromptAt = String(session?.lastPromptAt || '').trim();
510
+ if (lastPromptAt) {
511
+ return false;
512
+ }
513
+
514
+ const title = String(session?.title || '').trim();
515
+ const summary = String(session?.summary || '').trim();
516
+
517
+ return GENERIC_ACP_SESSION_TITLE_PATTERN.test(title)
518
+ || GENERIC_ACP_SESSION_TITLE_PATTERN.test(summary);
94
519
  }
95
520
 
96
- const PROVIDER_ADAPTERS = {
97
- claude: {
98
- async listSessions({ projectName, projectPath, limit = 50, offset = 0 }) {
99
- const [result, acpSessions] = await Promise.all([
100
- getSessions(projectName, limit, offset),
101
- listAcpSessions({ provider: 'claude', projectPath: projectPath || null })
102
- ]);
103
- return mergeSessionLists(
104
- (result?.sessions || []).map((session) => ({ ...session, provider: 'claude', source: 'legacy' })),
105
- acpSessions,
106
- { fallbackProvider: 'claude' }
107
- );
108
- },
109
- async loadEvents({ projectName, sessionId, limit = null, offset = 0 }) {
110
- const acpResult = await loadAcpEvents(
111
- 'claude',
112
- sessionId,
113
- () => getSessionMessages(projectName, sessionId, limit, offset),
114
- );
115
- if (acpResult) {
116
- return acpResult;
117
- }
521
+ function createSnapshotOverlay(snapshot) {
522
+ if (!snapshot || typeof snapshot !== 'object') {
523
+ return {};
524
+ }
118
525
 
119
- const rawMessages = await getSessionMessages(projectName, sessionId, limit, offset);
120
- return normalizeLegacyLoadResult(rawMessages, 'claude', sessionId);
121
- }
122
- },
123
- codex: {
124
- async listSessions({ projectPath, limit = 50 }) {
125
- const [sessions, acpSessions] = await Promise.all([
126
- getCodexSessions(projectPath, { limit }),
127
- listAcpSessions({ provider: 'codex', projectPath: projectPath || null })
128
- ]);
129
- return mergeSessionLists(
130
- sessions.map((session) => ({ ...session, provider: 'codex', source: 'legacy' })),
131
- acpSessions,
132
- { fallbackProvider: 'codex' }
133
- );
134
- },
135
- async loadEvents({ sessionId, limit = null, offset = 0 }) {
136
- const acpResult = await loadAcpEvents(
137
- 'codex',
138
- sessionId,
139
- () => getCodexSessionMessages(sessionId, limit, offset),
140
- );
141
- if (acpResult) {
142
- return acpResult;
143
- }
526
+ const overlay = {};
527
+ const copyIfPresent = (targetKey, sourceKey = targetKey) => {
528
+ if (snapshot[sourceKey] !== undefined && snapshot[sourceKey] !== null) {
529
+ overlay[targetKey] = snapshot[sourceKey];
530
+ }
531
+ };
144
532
 
145
- const rawMessages = await getCodexSessionMessages(sessionId, limit, offset);
146
- return normalizeLegacyLoadResult(rawMessages, 'codex', sessionId);
147
- }
148
- },
149
- gemini: {
150
- async listSessions({ projectPath, limit = 50 }) {
151
- const [sessions, acpSessions] = await Promise.all([
152
- getGeminiSessions(projectPath, { limit }),
153
- listAcpSessions({ provider: 'gemini', projectPath: projectPath || null })
154
- ]);
155
- return mergeSessionLists(
156
- sessions.map((session) => ({ ...session, provider: 'gemini', source: 'legacy' })),
157
- acpSessions,
158
- { fallbackProvider: 'gemini' }
159
- );
160
- },
161
- async loadEvents({ sessionId, limit = null, offset = 0 }) {
162
- const acpResult = await loadAcpEvents(
163
- 'gemini',
164
- sessionId,
165
- () => getGeminiSessionMessages(sessionId, limit, offset),
166
- );
167
- if (acpResult) {
168
- return acpResult;
169
- }
533
+ copyIfPresent('title');
534
+ copyIfPresent('summary');
535
+ copyIfPresent('model');
536
+ copyIfPresent('createdAt');
537
+ copyIfPresent('updatedAt');
538
+ copyIfPresent('lastActivity');
539
+ copyIfPresent('lastPromptAt');
540
+ copyIfPresent('closedAt');
541
+ copyIfPresent('isClosed');
542
+ copyIfPresent('projectPath');
543
+ copyIfPresent('cwd', 'projectPath');
170
544
 
171
- const rawMessages = await getGeminiSessionMessages(sessionId, limit, offset);
172
- return normalizeLegacyLoadResult(rawMessages, 'gemini', sessionId);
173
- }
174
- },
175
- opencode: {
176
- async listSessions({ projectPath, limit = 50 }) {
177
- const [sessions, acpSessions] = await Promise.all([
178
- getOpencodeSessions(projectPath, { limit }),
179
- listAcpSessions({ provider: 'opencode', projectPath: projectPath || null })
180
- ]);
181
- return mergeSessionLists(
182
- sessions.map((session) => ({ ...session, provider: 'opencode', source: 'legacy' })),
183
- acpSessions,
184
- { fallbackProvider: 'opencode' }
185
- );
186
- },
187
- async loadEvents({ sessionId, limit = null, offset = 0 }) {
188
- const acpResult = await loadAcpEvents(
189
- 'opencode',
190
- sessionId,
191
- () => getOpencodeSessionMessages(sessionId, limit, offset),
192
- );
193
- if (acpResult) {
194
- return acpResult;
545
+ return overlay;
546
+ }
547
+
548
+ async function listAcpSessionsWithSnapshotOverlay(provider, options = {}) {
549
+ const { projectPath, limit = 50 } = options;
550
+ const snapshots = await listAcpSessions({ provider, projectPath: projectPath || null });
551
+ const sessions = snapshots
552
+ .map((session) => ({
553
+ ...session,
554
+ ...createSnapshotOverlay(session),
555
+ provider,
556
+ __provider: provider,
557
+ source: 'acp',
558
+ runtime: 'acp'
559
+ }))
560
+ .filter((session) => !isProbeLikeAcpSession(session))
561
+ .sort((left, right) => getSessionActivityTime(right) - getSessionActivityTime(left));
562
+
563
+ return limit > 0 ? sessions.slice(0, limit) : sessions;
564
+ }
565
+
566
+ async function loadAcpEvents(provider, sessionId, options = {}) {
567
+ const record = await findAcpSessionRecord(sessionId, provider);
568
+ const projectPath = record?.projectPath || options.projectPath || null;
569
+ const cachedEvents = readCachedSessionEvents(provider, sessionId);
570
+ if (isAgentSessionActive(provider, sessionId)) {
571
+ return createEventLoadResult(cachedEvents, 'acp', options);
572
+ }
573
+
574
+ if (!projectPath) {
575
+ return createEventLoadResult(cachedEvents, 'acp', options);
576
+ }
577
+
578
+ if (await shouldSkipGeminiAutomaticAcpStartup(provider, options)) {
579
+ return createEventLoadResult(cachedEvents, 'acp', options);
580
+ }
581
+
582
+ const writer = createAcpReplayWriter();
583
+ const { client, reused } = await getOrStartAgentClient({
584
+ agentKey: provider,
585
+ writer,
586
+ projectPath,
587
+ sessionId,
588
+ options: {
589
+ agentCommandOverrides: getAgentCommandOverrides(options)
590
+ }
591
+ });
592
+ let shouldRetainClient = false;
593
+ const shouldDisposeOnFailure = reused !== 'session';
594
+
595
+ try {
596
+ if (!client.canLoadSession()) {
597
+ return createEventLoadResult(cachedEvents, 'acp', options);
598
+ }
599
+
600
+ let response;
601
+ try {
602
+ response = await client.loadSession(sessionId, { suppressReplayUpdates: false });
603
+ } catch (error) {
604
+ if (isAcpMissingResourceError(error)) {
605
+ return createEventLoadResult(cachedEvents, 'acp', options);
195
606
  }
607
+ console.warn(`Could not replay ${provider} ACP session ${sessionId}:`, error.message);
608
+ return createEventLoadResult(cachedEvents, 'acp', options);
609
+ }
610
+ shouldRetainClient = true;
611
+
612
+ const mergedEvents = rememberSessionEvents(provider, sessionId, writer.events);
613
+ await touchAcpSessionRecord({
614
+ provider,
615
+ sessionId,
616
+ projectPath,
617
+ title: record?.title || sessionId,
618
+ summary: record?.summary || null,
619
+ model: record?.model || client.model || null,
620
+ lastActivity: getLastEventTimestamp(mergedEvents) || record?.lastActivity || new Date().toISOString(),
621
+ lastPromptAt: getLastUserEventTimestamp(mergedEvents) || record?.lastPromptAt || null,
622
+ isClosed: record?.isClosed ?? false
623
+ });
624
+ const result = createEventLoadResult(mergedEvents, 'acp', options);
196
625
 
197
- const rawMessages = await getOpencodeSessionMessages(sessionId, limit, offset);
198
- return normalizeLegacyLoadResult(rawMessages, 'opencode', sessionId);
626
+ if (response?.tokenUsage) {
627
+ result.tokenUsage = response.tokenUsage;
628
+ }
629
+
630
+ return result;
631
+ } finally {
632
+ if (shouldRetainClient) {
633
+ retainSessionAgentClient({
634
+ provider,
635
+ sessionId,
636
+ client,
637
+ metadata: {
638
+ projectPath
639
+ }
640
+ });
641
+ } else if (shouldDisposeOnFailure) {
642
+ await disposeManagedAgentClient({
643
+ provider,
644
+ sessionId,
645
+ projectPath,
646
+ client
647
+ });
648
+ } else {
649
+ retainSessionAgentClient({
650
+ provider,
651
+ sessionId,
652
+ client,
653
+ metadata: {
654
+ projectPath
655
+ }
656
+ });
199
657
  }
200
658
  }
201
- };
659
+ }
660
+
661
+ function createProviderAdapter(provider) {
662
+ return {
663
+ async listSessions({ projectPath, limit = 50, agentCommandOverrides = {} } = {}) {
664
+ try {
665
+ return await listAcpSessionsWithSnapshotOverlay(provider, { projectPath, limit, agentCommandOverrides });
666
+ } catch (error) {
667
+ console.warn(`Could not load ${provider} ACP sessions for project ${projectPath}:`, error.message);
668
+ return [];
669
+ }
670
+ },
671
+ async loadEvents({ sessionId, projectPath, limit = null, offset = 0, agentCommandOverrides = {} } = {}) {
672
+ return loadAcpEvents(provider, sessionId, { projectPath, limit, offset, agentCommandOverrides });
673
+ }
674
+ };
675
+ }
676
+
677
+ const PROVIDER_ADAPTERS = SUPPORTED_PROVIDERS.reduce((adapters, provider) => {
678
+ adapters[provider] = createProviderAdapter(provider);
679
+ return adapters;
680
+ }, {});
202
681
 
203
682
  export function getProviderAdapter(provider) {
204
- const normalizedProvider = String(provider || '').toLowerCase();
683
+ const normalizedProvider = normalizeProvider(provider);
205
684
  if (!SUPPORTED_PROVIDERS.includes(normalizedProvider)) {
206
685
  throw new Error(`Unsupported provider: ${provider}`);
207
686
  }
208
687
  return PROVIDER_ADAPTERS[normalizedProvider];
209
688
  }
689
+
690
+ export const __test__rememberSessionEvents = rememberSessionEvents;