@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,53 +1,64 @@
1
1
  import express from 'express';
2
- import crypto from 'node:crypto';
3
2
  import path from 'path';
4
3
  import {
5
- getClaudeSessionMetadata,
6
- getCodexSessionMetadata,
7
- getGeminiSessionMetadata,
8
- getOpencodeSessionMetadata,
9
- getProjects,
4
+ getLocalProviderSessionMessages,
5
+ getLocalProviderSessions,
10
6
  getProjectsList
11
7
  } from '../projects.js';
12
8
  import { discoverAllProviders, discoverProvider } from '../session-core/providerDiscovery.js';
13
- import { getProviderAdapter } from '../session-core/providerAdapters.js';
14
- import { findAcpSessionRecord, listAcpSessions } from '../acp-runtime/session-store.js';
15
- import { mergeSessionLists } from '../session-core/sessionListMerge.js';
9
+ import { probeAgentCapabilities } from '../acp-runtime/index.js';
10
+ import { mergeAgentCommandOverrides } from '../acp-runtime/command-overrides.js';
11
+ import { findAcpSessionRecord } from '../acp-runtime/session-store.js';
12
+ import { runRegistry as defaultRunRegistry } from '../session-core/runRegistry.js';
13
+ import { normalizeLegacyHistoryEntries } from '../../shared/conversationEvents.js';
16
14
 
17
15
  const router = express.Router();
16
+ const ASSISTANT_TEXT_EVENT_KINDS = new Set([
17
+ 'assistant_text_start',
18
+ 'assistant_text_delta',
19
+ 'assistant_text_end'
20
+ ]);
18
21
 
19
22
  router.use((req, res, next) => {
20
23
  res.setHeader('X-Runtime-Engine', 'acp');
21
24
  next();
22
25
  });
23
26
 
24
- async function flattenProjectSessions(project) {
27
+ function getRequestAgentCommandOverrides(req) {
28
+ return mergeAgentCommandOverrides(
29
+ process.env.AXHUB_ACP_COMMAND_OVERRIDES,
30
+ req?.get?.('X-Axhub-Acp-Command-Overrides')
31
+ );
32
+ }
33
+
34
+ async function flattenProjectSessions(project, options = {}) {
25
35
  const projectPath = project.fullPath || project.path || null;
26
- const groups = [
27
- { provider: 'claude', items: project.sessions || [] },
28
- { provider: 'codex', items: project.codexSessions || [] },
29
- { provider: 'gemini', items: project.geminiSessions || [] },
30
- { provider: 'opencode', items: project.opencodeSessions || [] }
31
- ];
32
-
33
- const acpSessions = await listAcpSessions({ projectPath });
34
- const legacySessions = groups.flatMap(({ provider, items }) => (
35
- items.map((item) => ({ ...item, provider, __provider: provider, source: item?.source || 'legacy' }))
36
- ));
37
- const normalizedAcpSessions = acpSessions.map((item) => ({
38
- ...item,
39
- provider: item.provider,
40
- __provider: item.provider,
41
- source: 'acp'
42
- }));
43
-
44
- return mergeSessionLists(legacySessions, normalizedAcpSessions);
36
+ const sessionGroups = await Promise.all(
37
+ ['claude', 'codex', 'gemini', 'opencode'].map(async (provider) => {
38
+ return getLocalProviderSessions({
39
+ provider,
40
+ projectName: project.name,
41
+ projectPath,
42
+ limit: 0
43
+ });
44
+ })
45
+ );
46
+
47
+ return sessionGroups
48
+ .flat()
49
+ .sort((left, right) => {
50
+ const leftTime = new Date(left?.lastActivity || left?.updatedAt || left?.createdAt || 0).getTime();
51
+ const rightTime = new Date(right?.lastActivity || right?.updatedAt || right?.createdAt || 0).getTime();
52
+ return rightTime - leftTime;
53
+ });
45
54
  }
46
55
 
47
56
  async function resolveProviderSessionRoute({
48
57
  provider,
49
58
  sessionId,
50
- projectList = null
59
+ projectPath = null,
60
+ projectList = null,
61
+ agentCommandOverrides = {}
51
62
  }) {
52
63
  const normalizedProvider = String(provider || '').trim().toLowerCase();
53
64
  const normalizedSessionId = String(sessionId || '').trim();
@@ -69,8 +80,8 @@ async function resolveProviderSessionRoute({
69
80
  return path.normalize(withoutWindowsLongPathPrefix);
70
81
  };
71
82
 
72
- const findProjectByPath = (projectPath) => {
73
- const normalizedProjectPath = normalizeComparableProjectPath(projectPath);
83
+ const findProjectByPath = (candidateProjectPath) => {
84
+ const normalizedProjectPath = normalizeComparableProjectPath(candidateProjectPath);
74
85
  if (!normalizedProjectPath) {
75
86
  return null;
76
87
  }
@@ -81,81 +92,60 @@ async function resolveProviderSessionRoute({
81
92
  }) || null;
82
93
  };
83
94
 
84
- const acpRecord = await findAcpSessionRecord(normalizedSessionId, normalizedProvider);
85
- const acpProject = findProjectByPath(acpRecord?.projectPath || null);
86
-
87
- let matchedProject = acpProject;
88
- let resolvedSource = acpRecord ? 'acp' : 'legacy';
89
-
90
- if (!matchedProject) {
91
- switch (normalizedProvider) {
92
- case 'claude': {
93
- const metadata = await getClaudeSessionMetadata(normalizedSessionId);
94
- matchedProject = findProjectByPath(metadata?.cwd || null);
95
- break;
96
- }
97
- case 'codex': {
98
- const metadata = await getCodexSessionMetadata(normalizedSessionId);
99
- matchedProject = findProjectByPath(metadata?.cwd || null);
100
- break;
101
- }
102
- case 'gemini': {
103
- const metadata = await getGeminiSessionMetadata(normalizedSessionId);
104
- const projectHash = String(metadata?.projectHash || '').trim();
105
-
106
- if (projectHash) {
107
- matchedProject = projects.find((project) => {
108
- const projectPath = normalizeComparableProjectPath(project.fullPath || project.path || '');
109
- if (!projectPath) {
110
- return false;
111
- }
112
-
113
- const candidateHash = crypto.createHash('sha256').update(projectPath).digest('hex');
114
- return candidateHash === projectHash;
115
- }) || null;
116
- }
117
- break;
118
- }
119
- case 'opencode': {
120
- const metadata = await getOpencodeSessionMetadata(normalizedSessionId);
121
- matchedProject = findProjectByPath(metadata?.cwd || null);
122
- break;
123
- }
124
- default:
125
- break;
95
+ const findProjectByRouteContext = (routeProjectContext) => {
96
+ const matchedByPath = findProjectByPath(routeProjectContext);
97
+ if (matchedByPath) {
98
+ return matchedByPath;
126
99
  }
127
- }
128
100
 
129
- if (!matchedProject) {
130
- return null;
131
- }
101
+ const normalizedContext = normalizeComparableProjectPath(routeProjectContext);
102
+ if (!normalizedContext) {
103
+ return null;
104
+ }
132
105
 
133
- const adapter = getProviderAdapter(normalizedProvider);
134
- let sessions = [];
106
+ return projects.find((project) => [
107
+ project.name,
108
+ project.displayName
109
+ ].some((candidate) => normalizeComparableProjectPath(candidate) === normalizedContext)) || null;
110
+ };
135
111
 
136
- if (normalizedProvider === 'claude') {
137
- sessions = await adapter.listSessions({
138
- projectName: matchedProject.name,
139
- projectPath: matchedProject.fullPath || matchedProject.path,
140
- limit: 1000,
141
- offset: 0
142
- });
143
- } else {
144
- sessions = await adapter.listSessions({
145
- projectPath: matchedProject.fullPath || matchedProject.path,
146
- limit: 0
147
- });
148
- }
112
+ const acpRecord = await findAcpSessionRecord(normalizedSessionId, normalizedProvider);
113
+ const matchedProject = findProjectByPath(acpRecord?.projectPath || null)
114
+ || (!acpRecord ? findProjectByRouteContext(projectPath) : null);
149
115
 
150
- const matchedSession = (Array.isArray(sessions) ? sessions : []).find((session) => session.id === normalizedSessionId);
151
- if (!matchedSession) {
116
+ if (!matchedProject) {
152
117
  return null;
153
118
  }
154
119
 
120
+ const resolvedProjectPath = acpRecord?.projectPath
121
+ || matchedProject.fullPath
122
+ || matchedProject.path
123
+ || projectPath
124
+ || null;
125
+ const resolvedSession = {
126
+ id: normalizedSessionId,
127
+ sessionId: normalizedSessionId,
128
+ provider: normalizedProvider,
129
+ __provider: normalizedProvider,
130
+ source: 'acp',
131
+ runtime: 'acp',
132
+ projectPath: resolvedProjectPath,
133
+ cwd: resolvedProjectPath,
134
+ title: acpRecord?.title || acpRecord?.summary || normalizedSessionId,
135
+ summary: acpRecord?.summary || null,
136
+ model: acpRecord?.model || null,
137
+ createdAt: acpRecord?.createdAt || null,
138
+ updatedAt: acpRecord?.updatedAt || null,
139
+ lastActivity: acpRecord?.lastActivity || acpRecord?.updatedAt || acpRecord?.createdAt || null,
140
+ lastPromptAt: acpRecord?.lastPromptAt || null,
141
+ closedAt: acpRecord?.closedAt || null,
142
+ isClosed: Boolean(acpRecord?.isClosed)
143
+ };
144
+
155
145
  return {
156
146
  provider: normalizedProvider,
157
- source: matchedSession.source || resolvedSource,
158
- session: matchedSession,
147
+ source: resolvedSession.source || 'acp',
148
+ session: resolvedSession,
159
149
  project: {
160
150
  name: matchedProject.name,
161
151
  fullPath: matchedProject.fullPath || matchedProject.path,
@@ -165,6 +155,236 @@ async function resolveProviderSessionRoute({
165
155
  };
166
156
  }
167
157
 
158
+ function normalizeComparableText(value) {
159
+ return String(value || '').replace(/\r\n/g, '\n').trim();
160
+ }
161
+
162
+ function normalizeNullableString(value) {
163
+ return typeof value === 'string' && value.trim() ? value.trim() : null;
164
+ }
165
+
166
+ function getAssistantTextGroupKey(event, index) {
167
+ return normalizeNullableString(event?.payload?.messageId || event?.extensions?.messageId)
168
+ || `event:${event?.eventId || index}`;
169
+ }
170
+
171
+ function collectAssistantTextGroups(events = []) {
172
+ const groups = new Map();
173
+ (Array.isArray(events) ? events : []).forEach((event, index) => {
174
+ if (!ASSISTANT_TEXT_EVENT_KINDS.has(event?.kind)) {
175
+ return;
176
+ }
177
+
178
+ const key = getAssistantTextGroupKey(event, index);
179
+ const group = groups.get(key) || {
180
+ events: [],
181
+ text: ''
182
+ };
183
+ group.events.push(event);
184
+ if (event.kind === 'assistant_text_delta') {
185
+ group.text += String(event.payload?.text || '');
186
+ }
187
+ groups.set(key, group);
188
+ });
189
+ return groups;
190
+ }
191
+
192
+ function isAssistantTextCoveredByRunTexts(text, runAssistantTexts) {
193
+ const normalizedText = normalizeComparableText(text);
194
+ if (!normalizedText || !(runAssistantTexts instanceof Set) || runAssistantTexts.size === 0) {
195
+ return false;
196
+ }
197
+
198
+ if (runAssistantTexts.has(normalizedText)) {
199
+ return true;
200
+ }
201
+
202
+ const parts = Array.from(runAssistantTexts)
203
+ .map((part) => normalizeComparableText(part))
204
+ .filter((part) => part && normalizedText.includes(part))
205
+ .sort((left, right) => right.length - left.length);
206
+
207
+ if (parts.length === 0) {
208
+ return false;
209
+ }
210
+
211
+ const reachable = new Array(normalizedText.length + 1).fill(false);
212
+ reachable[0] = true;
213
+ for (let index = 0; index < normalizedText.length; index += 1) {
214
+ if (!reachable[index]) {
215
+ continue;
216
+ }
217
+ for (const part of parts) {
218
+ if (normalizedText.startsWith(part, index)) {
219
+ reachable[index + part.length] = true;
220
+ }
221
+ }
222
+ }
223
+
224
+ return reachable[normalizedText.length] === true;
225
+ }
226
+
227
+ function collectRunRegistryConversationEvents({ provider, sessionId, runRegistry = defaultRunRegistry } = {}) {
228
+ if (!runRegistry || typeof runRegistry.listSessionRuns !== 'function' || typeof runRegistry.listRunEvents !== 'function') {
229
+ return [];
230
+ }
231
+
232
+ return runRegistry
233
+ .listSessionRuns({ provider, sessionId })
234
+ .flatMap((run) => runRegistry.listRunEvents(run.runId, { after: 0 }))
235
+ .filter((runEvent) => runEvent?.type === 'conversation-event' && runEvent.event)
236
+ .map((runEvent) => ({
237
+ ...runEvent.event,
238
+ provider: runEvent.event.provider || runEvent.provider || provider,
239
+ sessionId: runEvent.event.sessionId || runEvent.sessionId || sessionId
240
+ }));
241
+ }
242
+
243
+ function collectUserMessageTimesByText(events = []) {
244
+ const timesByText = new Map();
245
+ (Array.isArray(events) ? events : []).forEach((event) => {
246
+ if (event?.kind !== 'user_message') {
247
+ return;
248
+ }
249
+
250
+ const text = normalizeComparableText(event.payload?.text);
251
+ if (!text) {
252
+ return;
253
+ }
254
+
255
+ const timestamp = Date.parse(event.timestamp || '');
256
+ const times = timesByText.get(text) || [];
257
+ times.push(Number.isFinite(timestamp) ? timestamp : 0);
258
+ timesByText.set(text, times);
259
+ });
260
+ return timesByText;
261
+ }
262
+
263
+ function isDuplicateAcpUserReplay(event, runUserTimesByText) {
264
+ if (event?.kind !== 'user_message') {
265
+ return false;
266
+ }
267
+
268
+ const text = normalizeComparableText(event.payload?.text);
269
+ if (!text) {
270
+ return false;
271
+ }
272
+
273
+ const runTimes = runUserTimesByText.get(text);
274
+ if (!Array.isArray(runTimes) || runTimes.length === 0) {
275
+ return false;
276
+ }
277
+
278
+ const acpTime = Date.parse(event.timestamp || '');
279
+ if (!Number.isFinite(acpTime)) {
280
+ return true;
281
+ }
282
+
283
+ return runTimes.some((runTime) => runTime <= acpTime);
284
+ }
285
+
286
+ function getConversationEventDedupeKey(event) {
287
+ if (event?.eventId) {
288
+ return `id:${event.eventId}`;
289
+ }
290
+ return [
291
+ 'shape',
292
+ event?.kind || '',
293
+ event?.provider || '',
294
+ event?.sessionId || '',
295
+ event?.timestamp || '',
296
+ JSON.stringify(event?.payload || {})
297
+ ].join('\u0000');
298
+ }
299
+
300
+ function loadLocalSessionEventsResult(result, provider, sessionId) {
301
+ const rawMessages = Array.isArray(result)
302
+ ? result
303
+ : Array.isArray(result?.messages)
304
+ ? result.messages
305
+ : Array.isArray(result?.events)
306
+ ? result.events
307
+ : [];
308
+
309
+ const events = normalizeLegacyHistoryEntries(rawMessages, provider, sessionId);
310
+
311
+ return {
312
+ source: 'native-history',
313
+ events,
314
+ total: typeof result?.total === 'number' ? result.total : rawMessages.length,
315
+ hasMore: typeof result?.hasMore === 'boolean' ? result.hasMore : false,
316
+ offset: typeof result?.offset === 'number' ? result.offset : 0,
317
+ limit: Object.prototype.hasOwnProperty.call(result || {}, 'limit') ? result.limit : null,
318
+ ...(result?.tokenUsage ? { tokenUsage: result.tokenUsage } : {})
319
+ };
320
+ }
321
+
322
+ export function mergeSessionEventsWithRunRegistryEvents(events = [], {
323
+ provider,
324
+ sessionId,
325
+ runRegistry = defaultRunRegistry
326
+ } = {}) {
327
+ const acpEvents = Array.isArray(events) ? events : [];
328
+ const runEvents = collectRunRegistryConversationEvents({ provider, sessionId, runRegistry });
329
+
330
+ if (runEvents.length === 0) {
331
+ return acpEvents;
332
+ }
333
+
334
+ const runAssistantTexts = new Set(
335
+ Array.from(collectAssistantTextGroups(runEvents).values())
336
+ .map((group) => normalizeComparableText(group.text))
337
+ .filter(Boolean)
338
+ );
339
+ const runUserTimesByText = collectUserMessageTimesByText(runEvents);
340
+ const duplicateAcpAssistantEventIds = new Set();
341
+ const duplicateAcpAssistantEventRefs = new Set();
342
+
343
+ for (const group of collectAssistantTextGroups(acpEvents).values()) {
344
+ const text = normalizeComparableText(group.text);
345
+ if (!text || !isAssistantTextCoveredByRunTexts(text, runAssistantTexts)) {
346
+ continue;
347
+ }
348
+ group.events.forEach((event) => {
349
+ if (event?.eventId) {
350
+ duplicateAcpAssistantEventIds.add(event.eventId);
351
+ } else {
352
+ duplicateAcpAssistantEventRefs.add(event);
353
+ }
354
+ });
355
+ }
356
+
357
+ const filteredAcpEvents = acpEvents.filter((event) => !(
358
+ isDuplicateAcpUserReplay(event, runUserTimesByText) ||
359
+ (event?.eventId && duplicateAcpAssistantEventIds.has(event.eventId)) ||
360
+ duplicateAcpAssistantEventRefs.has(event)
361
+ ));
362
+ const ordered = [...filteredAcpEvents, ...runEvents].map((event, index) => ({ event, index }));
363
+ ordered.sort((left, right) => {
364
+ const leftTime = Date.parse(left.event?.timestamp || '');
365
+ const rightTime = Date.parse(right.event?.timestamp || '');
366
+ const normalizedLeftTime = Number.isFinite(leftTime) ? leftTime : 0;
367
+ const normalizedRightTime = Number.isFinite(rightTime) ? rightTime : 0;
368
+ if (normalizedLeftTime !== normalizedRightTime) {
369
+ return normalizedLeftTime - normalizedRightTime;
370
+ }
371
+ return left.index - right.index;
372
+ });
373
+
374
+ const seen = new Set();
375
+ const merged = [];
376
+ for (const { event } of ordered) {
377
+ const key = getConversationEventDedupeKey(event);
378
+ if (seen.has(key)) {
379
+ continue;
380
+ }
381
+ seen.add(key);
382
+ merged.push(event);
383
+ }
384
+
385
+ return merged;
386
+ }
387
+
168
388
  router.get('/providers', async (req, res) => {
169
389
  try {
170
390
  const providers = await discoverAllProviders({ projectPath: req.query.projectPath });
@@ -176,16 +396,33 @@ router.get('/providers', async (req, res) => {
176
396
 
177
397
  router.get('/providers/:provider', async (req, res) => {
178
398
  try {
179
- const result = await discoverProvider(req.params.provider, { projectPath: req.query.projectPath });
399
+ const result = await discoverProvider(req.params.provider, {
400
+ projectPath: req.query.projectPath,
401
+ force: req.query.force
402
+ });
180
403
  res.json({ success: true, ...result });
181
404
  } catch (error) {
182
405
  res.status(500).json({ success: false, error: error.message });
183
406
  }
184
407
  });
185
408
 
409
+ router.get('/providers/:provider/acp-capabilities', async (req, res) => {
410
+ try {
411
+ const agentCommandOverrides = getRequestAgentCommandOverrides(req);
412
+ const result = await probeAgentCapabilities(req.params.provider, {
413
+ projectPath: req.query.projectPath,
414
+ force: req.query.force,
415
+ ...(Object.keys(agentCommandOverrides).length > 0 ? { agentCommandOverrides } : {}),
416
+ });
417
+ res.json({ success: true, provider: req.params.provider, capabilitySnapshot: result });
418
+ } catch (error) {
419
+ res.status(500).json({ success: false, provider: req.params.provider, error: error.message });
420
+ }
421
+ });
422
+
186
423
  router.get('/projects/:projectName/history-index', async (req, res) => {
187
424
  try {
188
- const projects = await getProjects();
425
+ const projects = await getProjectsList();
189
426
  const project = projects.find((entry) => entry.name === req.params.projectName);
190
427
  if (!project) {
191
428
  return res.status(404).json({ success: false, error: 'Project not found' });
@@ -197,7 +434,9 @@ router.get('/projects/:projectName/history-index', async (req, res) => {
197
434
  fullPath: project.fullPath || project.path,
198
435
  displayName: project.displayName || project.name
199
436
  },
200
- sessions: await flattenProjectSessions(project)
437
+ sessions: await flattenProjectSessions(project, {
438
+ agentCommandOverrides: getRequestAgentCommandOverrides(req)
439
+ })
201
440
  });
202
441
  } catch (error) {
203
442
  res.status(500).json({ success: false, error: error.message });
@@ -215,11 +454,13 @@ router.get('/sessions/:provider/:sessionId/resolve', async (req, res) => {
215
454
 
216
455
  const result = await resolveProviderSessionRoute({
217
456
  provider: requestedProvider,
218
- sessionId: requestedSessionId
457
+ sessionId: requestedSessionId,
458
+ projectPath: req.query.projectPath || req.query.cwd || req.query.workdir || req.query.project || req.query.projectName || null,
459
+ agentCommandOverrides: getRequestAgentCommandOverrides(req)
219
460
  });
220
461
 
221
462
  if (!result) {
222
- return res.status(404).json({ success: true, found: false });
463
+ return res.json({ success: true, found: false });
223
464
  }
224
465
 
225
466
  res.json({
@@ -234,19 +475,26 @@ router.get('/sessions/:provider/:sessionId/resolve', async (req, res) => {
234
475
 
235
476
  router.get('/sessions/:provider/:sessionId/events', async (req, res) => {
236
477
  try {
237
- const adapter = getProviderAdapter(req.params.provider);
238
478
  const parsedLimit = req.query.limit ? parseInt(req.query.limit, 10) : null;
239
479
  const parsedOffset = req.query.offset ? parseInt(req.query.offset, 10) : 0;
240
- const result = await adapter.loadEvents({
480
+ const result = loadLocalSessionEventsResult(await getLocalProviderSessionMessages({
481
+ provider: req.params.provider,
241
482
  projectName: req.query.projectName,
242
- projectPath: req.query.projectPath,
243
483
  sessionId: req.params.sessionId,
244
484
  limit: parsedLimit,
245
485
  offset: parsedOffset
246
- });
486
+ }), req.params.provider, req.params.sessionId);
247
487
 
248
- const events = Array.isArray(result) ? result : (result?.events || result?.messages || []);
249
- const total = typeof result?.total === 'number' ? result.total : events.length;
488
+ const rawEvents = Array.isArray(result) ? result : (result?.events || result?.messages || []);
489
+ const events = mergeSessionEventsWithRunRegistryEvents(rawEvents, {
490
+ provider: req.params.provider,
491
+ sessionId: req.params.sessionId,
492
+ runRegistry: defaultRunRegistry
493
+ });
494
+ const total = Math.max(
495
+ typeof result?.total === 'number' ? result.total : rawEvents.length,
496
+ events.length
497
+ );
250
498
  const hasMore = typeof result?.hasMore === 'boolean' ? result.hasMore : false;
251
499
  const offset = typeof result?.offset === 'number' ? result.offset : parsedOffset;
252
500
  const limit = Object.prototype.hasOwnProperty.call(result || {}, 'limit')
@@ -257,7 +505,7 @@ router.get('/sessions/:provider/:sessionId/events', async (req, res) => {
257
505
  success: true,
258
506
  provider: req.params.provider,
259
507
  sessionId: req.params.sessionId,
260
- source: result?.source || 'legacy',
508
+ source: result?.source || 'acp',
261
509
  events,
262
510
  total,
263
511
  hasMore,