@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,13 +1,15 @@
1
1
  const ACP_ADAPTER_PACKAGE_RANGES = {
2
- claude: '^0.26.0',
3
- codex: '^0.11.1'
2
+ claude: '^0.37.0',
3
+ codex: '^0.15.0',
4
+ gemini: '^0.43.0',
5
+ opencode: '^1.15.10'
4
6
  };
5
7
 
6
8
  export const AGENT_REGISTRY = {
7
9
  claude: `npx -y @agentclientprotocol/claude-agent-acp@${ACP_ADAPTER_PACKAGE_RANGES.claude}`,
8
10
  codex: `npx @zed-industries/codex-acp@${ACP_ADAPTER_PACKAGE_RANGES.codex}`,
9
- gemini: 'gemini --acp',
10
- opencode: 'npx -y opencode-ai acp'
11
+ gemini: `npx -y @google/gemini-cli@${ACP_ADAPTER_PACKAGE_RANGES.gemini} --acp --skip-trust`,
12
+ opencode: `npx -y opencode-ai@${ACP_ADAPTER_PACKAGE_RANGES.opencode} acp`
11
13
  };
12
14
 
13
15
  const AGENT_ALIASES = {
@@ -1,7 +1,6 @@
1
1
  import fs from 'fs/promises';
2
2
  import os from 'os';
3
3
  import path from 'path';
4
- import { readMirroredConversationEvents } from '../session-core/eventStore.js';
5
4
 
6
5
  function parsePositiveInteger(value, fallback) {
7
6
  const parsed = parseInt(value, 10);
@@ -9,6 +8,10 @@ function parsePositiveInteger(value, fallback) {
9
8
  }
10
9
 
11
10
  const DEFAULT_SESSION_GC_MAX_AGE_DAYS = parsePositiveInteger(process.env.ACP_SESSION_GC_MAX_AGE_DAYS, 30);
11
+ const sessionRecordWriteQueues = new Map();
12
+ let sessionIndexMutationQueue = Promise.resolve();
13
+ const SUPPORTED_SESSION_PROVIDERS = ['claude', 'codex', 'gemini', 'opencode'];
14
+ const SESSION_INDEX_VERSION = 1;
12
15
 
13
16
  function normalizeProvider(value) {
14
17
  return String(value || '').trim().toLowerCase();
@@ -19,13 +22,21 @@ function normalizeSessionId(value) {
19
22
  return normalized || null;
20
23
  }
21
24
 
22
- function getProviderDir(provider) {
23
- const sessionRoot = process.env.AXHUB_GENIE_ACP_SESSION_ROOT
25
+ function getSessionRoot() {
26
+ return process.env.AXHUB_GENIE_ACP_SESSION_ROOT
24
27
  ? path.resolve(process.env.AXHUB_GENIE_ACP_SESSION_ROOT)
25
28
  : path.join(os.homedir(), '.axhub-genie', 'acp-sessions');
29
+ }
30
+
31
+ function getProviderDir(provider) {
32
+ const sessionRoot = getSessionRoot();
26
33
  return path.join(sessionRoot, normalizeProvider(provider));
27
34
  }
28
35
 
36
+ function getSessionIndexFilePath() {
37
+ return path.join(getSessionRoot(), 'index.json');
38
+ }
39
+
29
40
  function getSessionFilePath(provider, sessionId) {
30
41
  return path.join(getProviderDir(provider), `${encodeURIComponent(sessionId)}.json`);
31
42
  }
@@ -42,6 +53,10 @@ async function ensureProviderDir(provider) {
42
53
  await fs.mkdir(getProviderDir(provider), { recursive: true });
43
54
  }
44
55
 
56
+ async function ensureSessionRootDir() {
57
+ await fs.mkdir(getSessionRoot(), { recursive: true });
58
+ }
59
+
45
60
  async function listProviderSessionFiles(provider) {
46
61
  try {
47
62
  const entries = await fs.readdir(getProviderDir(provider), { withFileTypes: true });
@@ -64,6 +79,26 @@ async function readJsonFile(filePath) {
64
79
  if (error?.code === 'ENOENT') {
65
80
  return null;
66
81
  }
82
+ if (error instanceof SyntaxError) {
83
+ return null;
84
+ }
85
+ throw error;
86
+ }
87
+ }
88
+
89
+ async function writeJsonFileAtomically(filePath, content) {
90
+ const tempPath = [
91
+ filePath,
92
+ process.pid,
93
+ Date.now(),
94
+ Math.random().toString(36).slice(2)
95
+ ].join('.') + '.tmp';
96
+
97
+ try {
98
+ await fs.writeFile(tempPath, content, 'utf8');
99
+ await fs.rename(tempPath, filePath);
100
+ } catch (error) {
101
+ await fs.rm(tempPath, { force: true }).catch(() => {});
67
102
  throw error;
68
103
  }
69
104
  }
@@ -73,15 +108,26 @@ function createSessionSummary(record) {
73
108
  return null;
74
109
  }
75
110
 
111
+ const provider = normalizeProvider(record.provider);
112
+ const sessionId = normalizeSessionId(record.sessionId || record.id);
113
+ if (!provider || !sessionId) {
114
+ return null;
115
+ }
116
+
117
+ const projectPath = typeof record.projectPath === 'string' && record.projectPath.trim()
118
+ ? record.projectPath
119
+ : (typeof record.cwd === 'string' && record.cwd.trim() ? record.cwd : null);
120
+
76
121
  return {
77
- id: record.sessionId,
78
- sessionId: record.sessionId,
79
- provider: record.provider,
122
+ id: sessionId,
123
+ sessionId,
124
+ provider,
125
+ __provider: provider,
80
126
  source: 'acp',
81
127
  runtime: 'acp',
82
- projectPath: record.projectPath || null,
83
- cwd: record.projectPath || null,
84
- title: record.title || record.sessionId,
128
+ projectPath,
129
+ cwd: projectPath,
130
+ title: record.title || sessionId,
85
131
  summary: record.summary || null,
86
132
  model: record.model || null,
87
133
  createdAt: record.createdAt || null,
@@ -93,71 +139,132 @@ function createSessionSummary(record) {
93
139
  };
94
140
  }
95
141
 
96
- function extractVisibleUserPrompt(value) {
97
- const text = String(value || '').trim();
98
- if (!text) {
99
- return '';
100
- }
142
+ function normalizeIndexSession(value) {
143
+ return createSessionSummary(value);
144
+ }
101
145
 
102
- if (
103
- text.startsWith('# AGENTS.md instructions for ') ||
104
- text.includes('<environment_context>') ||
105
- text.startsWith('<subagent_notification>') ||
106
- text.startsWith('</subagent_notification>')
107
- ) {
108
- return '';
109
- }
146
+ function getIndexSessionKey(session) {
147
+ const provider = normalizeProvider(session?.provider || session?.__provider);
148
+ const sessionId = normalizeSessionId(session?.sessionId || session?.id);
149
+ return provider && sessionId ? `${provider}\u0000${sessionId}` : null;
150
+ }
110
151
 
111
- if (text.startsWith('[DYNAMIC CONTEXT V1]')) {
112
- const marker = '\n\n[USER MESSAGE]\n';
113
- const markerIndex = text.indexOf(marker);
114
- if (markerIndex >= 0) {
115
- return text.slice(markerIndex + marker.length).trim();
152
+ function uniqueSessionSummaries(sessions = []) {
153
+ const byKey = new Map();
154
+ for (const session of sessions) {
155
+ const summary = normalizeIndexSession(session);
156
+ const key = getIndexSessionKey(summary);
157
+ if (!key) {
158
+ continue;
116
159
  }
117
- return '';
160
+ byKey.set(key, summary);
118
161
  }
162
+ return sortSessionsByActivity(Array.from(byKey.values()));
163
+ }
119
164
 
120
- const dynamicContextMatch = text.match(
121
- /^<dynamic_context(?:\s[^>]*)?>[\s\S]*?<\/dynamic_context>\s*<user_message>\s*([\s\S]*?)\s*<\/user_message>$/i
122
- );
123
- if (dynamicContextMatch) {
124
- return String(dynamicContextMatch[1] || '').trim();
165
+ function normalizeSessionIndex(index) {
166
+ if (!index || typeof index !== 'object' || !Array.isArray(index.sessions)) {
167
+ return null;
125
168
  }
126
169
 
127
- return text;
170
+ return {
171
+ version: SESSION_INDEX_VERSION,
172
+ updatedAt: typeof index.updatedAt === 'string' ? index.updatedAt : null,
173
+ sessions: uniqueSessionSummaries(index.sessions)
174
+ };
128
175
  }
129
176
 
130
- function truncateSessionSummary(text, maxLength = 50) {
131
- const normalizedText = typeof text === 'string' ? text.trim() : '';
132
- if (!normalizedText) {
133
- return null;
134
- }
177
+ async function writeSessionIndex(sessions = []) {
178
+ const index = {
179
+ version: SESSION_INDEX_VERSION,
180
+ updatedAt: new Date().toISOString(),
181
+ sessions: uniqueSessionSummaries(sessions)
182
+ };
135
183
 
136
- return normalizedText.length > maxLength
137
- ? `${normalizedText.slice(0, maxLength)}...`
138
- : normalizedText;
184
+ await ensureSessionRootDir();
185
+ await writeJsonFileAtomically(getSessionIndexFilePath(), `${JSON.stringify(index, null, 2)}\n`);
186
+ return index;
139
187
  }
140
188
 
141
- async function deriveSummaryFromMirroredEvents(record) {
142
- if (!record?.provider || !record?.sessionId) {
143
- return null;
144
- }
189
+ async function readSessionIndex() {
190
+ const index = await readJsonFile(getSessionIndexFilePath());
191
+ return normalizeSessionIndex(index);
192
+ }
145
193
 
146
- const events = await readMirroredConversationEvents(record.provider, record.sessionId);
147
- for (const event of events) {
148
- if (event?.kind !== 'user_message') {
194
+ async function scanSessionRecordSummaries({ provider = null } = {}) {
195
+ const providers = provider ? [normalizeProvider(provider)] : SUPPORTED_SESSION_PROVIDERS;
196
+ const sessions = [];
197
+
198
+ for (const currentProvider of providers) {
199
+ if (!currentProvider) {
149
200
  continue;
150
201
  }
151
202
 
152
- const rawText = event?.payload?.text || event?.payload?.message || '';
153
- const visiblePrompt = extractVisibleUserPrompt(rawText);
154
- const summary = truncateSessionSummary(visiblePrompt);
155
- if (summary) {
156
- return summary;
203
+ const filePaths = await listProviderSessionFiles(currentProvider);
204
+ for (const filePath of filePaths) {
205
+ const record = await readJsonFile(filePath);
206
+ const summary = createSessionSummary(record);
207
+ if (summary) {
208
+ sessions.push(summary);
209
+ }
157
210
  }
158
211
  }
159
212
 
160
- return null;
213
+ return uniqueSessionSummaries(sessions);
214
+ }
215
+
216
+ async function rebuildSessionIndex() {
217
+ const sessions = await scanSessionRecordSummaries();
218
+ return writeSessionIndex(sessions);
219
+ }
220
+
221
+ async function readOrRebuildSessionIndex() {
222
+ const index = await readSessionIndex();
223
+ return index || rebuildSessionIndex();
224
+ }
225
+
226
+ async function queueSessionIndexMutation(operation) {
227
+ const currentMutation = sessionIndexMutationQueue
228
+ .catch(() => {})
229
+ .then(operation);
230
+
231
+ sessionIndexMutationQueue = currentMutation.catch(() => {});
232
+ return currentMutation;
233
+ }
234
+
235
+ async function upsertSessionIndexEntry(record) {
236
+ return queueSessionIndexMutation(async () => {
237
+ const summary = createSessionSummary(record);
238
+ if (!summary) {
239
+ return null;
240
+ }
241
+
242
+ const index = await readOrRebuildSessionIndex();
243
+ const key = getIndexSessionKey(summary);
244
+ const nextSessions = (index.sessions || []).filter((session) => getIndexSessionKey(session) !== key);
245
+ nextSessions.push(summary);
246
+ await writeSessionIndex(nextSessions);
247
+ return summary;
248
+ });
249
+ }
250
+
251
+ export async function removeAcpSessionIndexEntry(provider, sessionId) {
252
+ return queueSessionIndexMutation(async () => {
253
+ const normalizedProvider = normalizeProvider(provider);
254
+ const normalizedSessionId = normalizeSessionId(sessionId);
255
+ if (!normalizedProvider || !normalizedSessionId) {
256
+ return false;
257
+ }
258
+
259
+ const index = await readOrRebuildSessionIndex();
260
+ const removeKey = `${normalizedProvider}\u0000${normalizedSessionId}`;
261
+ const nextSessions = (index.sessions || []).filter((session) => getIndexSessionKey(session) !== removeKey);
262
+ const removed = nextSessions.length !== index.sessions.length;
263
+ if (removed) {
264
+ await writeSessionIndex(nextSessions);
265
+ }
266
+ return removed;
267
+ });
161
268
  }
162
269
 
163
270
  function getSessionActivityTime(record) {
@@ -181,14 +288,7 @@ export async function readAcpSessionRecord(provider, sessionId) {
181
288
  return record && typeof record === 'object' ? record : null;
182
289
  }
183
290
 
184
- export async function writeAcpSessionRecord(record) {
185
- const normalizedProvider = normalizeProvider(record?.provider);
186
- const normalizedSessionId = normalizeSessionId(record?.sessionId);
187
-
188
- if (!normalizedProvider || !normalizedSessionId) {
189
- throw new Error('provider and sessionId are required to persist an ACP session');
190
- }
191
-
291
+ async function writeAcpSessionRecordUnlocked(record, normalizedProvider, normalizedSessionId) {
192
292
  const now = new Date().toISOString();
193
293
  const existing = await readAcpSessionRecord(normalizedProvider, normalizedSessionId);
194
294
  const nextRecord = {
@@ -211,11 +311,37 @@ export async function writeAcpSessionRecord(record) {
211
311
 
212
312
  await ensureProviderDir(normalizedProvider);
213
313
  const filePath = getSessionFilePath(normalizedProvider, normalizedSessionId);
214
- await fs.writeFile(filePath, `${JSON.stringify(nextRecord, null, 2)}\n`, 'utf8');
314
+ await writeJsonFileAtomically(filePath, `${JSON.stringify(nextRecord, null, 2)}\n`);
315
+ await upsertSessionIndexEntry(nextRecord);
215
316
 
216
317
  return nextRecord;
217
318
  }
218
319
 
320
+ export async function writeAcpSessionRecord(record) {
321
+ const normalizedProvider = normalizeProvider(record?.provider);
322
+ const normalizedSessionId = normalizeSessionId(record?.sessionId);
323
+
324
+ if (!normalizedProvider || !normalizedSessionId) {
325
+ throw new Error('provider and sessionId are required to persist an ACP session');
326
+ }
327
+
328
+ const queueKey = `${normalizedProvider}\u0000${normalizedSessionId}`;
329
+ const previousWrite = sessionRecordWriteQueues.get(queueKey) || Promise.resolve();
330
+ const currentWrite = previousWrite
331
+ .catch(() => {})
332
+ .then(() => writeAcpSessionRecordUnlocked(record, normalizedProvider, normalizedSessionId));
333
+
334
+ sessionRecordWriteQueues.set(queueKey, currentWrite);
335
+
336
+ try {
337
+ return await currentWrite;
338
+ } finally {
339
+ if (sessionRecordWriteQueues.get(queueKey) === currentWrite) {
340
+ sessionRecordWriteQueues.delete(queueKey);
341
+ }
342
+ }
343
+ }
344
+
219
345
  export async function touchAcpSessionRecord(record) {
220
346
  return writeAcpSessionRecord(record);
221
347
  }
@@ -232,13 +358,45 @@ export async function closeAcpSessionRecord(provider, sessionId, updates = {}) {
232
358
  });
233
359
  }
234
360
 
361
+ async function deleteAcpSessionRecordUnlocked(normalizedProvider, normalizedSessionId) {
362
+ const existing = await readAcpSessionRecord(normalizedProvider, normalizedSessionId);
363
+ await fs.rm(getSessionFilePath(normalizedProvider, normalizedSessionId), { force: true });
364
+ await removeAcpSessionIndexEntry(normalizedProvider, normalizedSessionId);
365
+ return Boolean(existing);
366
+ }
367
+
368
+ export async function deleteAcpSessionRecord(provider, sessionId) {
369
+ const normalizedProvider = normalizeProvider(provider);
370
+ const normalizedSessionId = normalizeSessionId(sessionId);
371
+
372
+ if (!normalizedProvider || !normalizedSessionId) {
373
+ return false;
374
+ }
375
+
376
+ const queueKey = `${normalizedProvider}\u0000${normalizedSessionId}`;
377
+ const previousWrite = sessionRecordWriteQueues.get(queueKey) || Promise.resolve();
378
+ const currentWrite = previousWrite
379
+ .catch(() => {})
380
+ .then(() => deleteAcpSessionRecordUnlocked(normalizedProvider, normalizedSessionId));
381
+
382
+ sessionRecordWriteQueues.set(queueKey, currentWrite);
383
+
384
+ try {
385
+ return await currentWrite;
386
+ } finally {
387
+ if (sessionRecordWriteQueues.get(queueKey) === currentWrite) {
388
+ sessionRecordWriteQueues.delete(queueKey);
389
+ }
390
+ }
391
+ }
392
+
235
393
  export async function gcOldAcpSessions({ provider = null, maxAgeDays = DEFAULT_SESSION_GC_MAX_AGE_DAYS, includeOpen = false } = {}) {
236
394
  const normalizedMaxAgeDays = Number(maxAgeDays);
237
395
  if (!Number.isFinite(normalizedMaxAgeDays) || normalizedMaxAgeDays <= 0) {
238
396
  return { removed: 0 };
239
397
  }
240
398
 
241
- const providers = provider ? [normalizeProvider(provider)] : ['claude', 'codex', 'gemini', 'opencode'];
399
+ const providers = provider ? [normalizeProvider(provider)] : SUPPORTED_SESSION_PROVIDERS;
242
400
  const cutoffTime = Date.now() - (normalizedMaxAgeDays * 24 * 60 * 60 * 1000);
243
401
  let removed = 0;
244
402
 
@@ -264,6 +422,7 @@ export async function gcOldAcpSessions({ provider = null, maxAgeDays = DEFAULT_S
264
422
  }
265
423
 
266
424
  await fs.rm(filePath, { force: true });
425
+ await removeAcpSessionIndexEntry(currentProvider, record.sessionId).catch(() => {});
267
426
  removed += 1;
268
427
  }
269
428
  }
@@ -273,35 +432,19 @@ export async function gcOldAcpSessions({ provider = null, maxAgeDays = DEFAULT_S
273
432
 
274
433
  export async function listAcpSessions({ provider = null, projectPath = null } = {}) {
275
434
  await gcOldAcpSessions({ provider }).catch(() => {});
276
- const providers = provider ? [normalizeProvider(provider)] : ['claude', 'codex', 'gemini', 'opencode'];
277
- const sessions = [];
278
-
279
- for (const currentProvider of providers) {
280
- if (!currentProvider) {
281
- continue;
435
+ const providers = new Set(provider ? [normalizeProvider(provider)] : SUPPORTED_SESSION_PROVIDERS);
436
+ const index = await readOrRebuildSessionIndex();
437
+ const sessions = (index.sessions || []).filter((session) => {
438
+ if (!providers.has(normalizeProvider(session.provider))) {
439
+ return false;
282
440
  }
283
441
 
284
- const filePaths = await listProviderSessionFiles(currentProvider);
285
- for (const filePath of filePaths) {
286
- const record = await readJsonFile(filePath);
287
- const hydratedRecord = record && !record.summary
288
- ? {
289
- ...record,
290
- summary: await deriveSummaryFromMirroredEvents(record)
291
- }
292
- : record;
293
- const summary = createSessionSummary(hydratedRecord);
294
- if (!summary) {
295
- continue;
296
- }
297
-
298
- if (projectPath && summary.projectPath !== projectPath) {
299
- continue;
300
- }
301
-
302
- sessions.push(summary);
442
+ if (projectPath && session.projectPath !== projectPath) {
443
+ return false;
303
444
  }
304
- }
445
+
446
+ return true;
447
+ });
305
448
 
306
449
  return sortSessionsByActivity(sessions);
307
450
  }
@@ -316,7 +459,7 @@ export async function findAcpSessionRecord(sessionId, provider = null) {
316
459
  return readAcpSessionRecord(provider, normalizedSessionId);
317
460
  }
318
461
 
319
- const providers = ['claude', 'codex', 'gemini', 'opencode'];
462
+ const providers = SUPPORTED_SESSION_PROVIDERS;
320
463
  for (const currentProvider of providers) {
321
464
  const record = await readAcpSessionRecord(currentProvider, normalizedSessionId);
322
465
  if (record) {
@@ -79,9 +79,18 @@ function normalizeState(rawState) {
79
79
 
80
80
  function persistStateSync(nextState) {
81
81
  ensureDataDirectory();
82
- const tempPath = `${DATA_FILE_PATH}.tmp`;
83
- fs.writeFileSync(tempPath, `${JSON.stringify(nextState, null, 2)}\n`, 'utf8');
84
- fs.renameSync(tempPath, DATA_FILE_PATH);
82
+ const tempPath = `${DATA_FILE_PATH}.${process.pid}.${crypto.randomUUID()}.tmp`;
83
+ try {
84
+ fs.writeFileSync(tempPath, `${JSON.stringify(nextState, null, 2)}\n`, 'utf8');
85
+ fs.renameSync(tempPath, DATA_FILE_PATH);
86
+ } catch (error) {
87
+ try {
88
+ fs.rmSync(tempPath, { force: true });
89
+ } catch {
90
+ // Best-effort cleanup for failed atomic writes.
91
+ }
92
+ throw error;
93
+ }
85
94
  stateCache = nextState;
86
95
  }
87
96