@roamcode.ai/server 1.0.0 → 1.0.1

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 (99) hide show
  1. package/dist/index.d.ts +2406 -82
  2. package/dist/index.js +8314 -44
  3. package/dist/managed-update-helper.js +621 -26
  4. package/dist/mcp-send.js +76 -90
  5. package/dist/start.d.ts +591 -9
  6. package/dist/start.js +7629 -353
  7. package/package.json +3 -3
  8. package/dist/auth.d.ts +0 -63
  9. package/dist/auth.js +0 -133
  10. package/dist/claude-auth-service.d.ts +0 -76
  11. package/dist/claude-auth-service.js +0 -217
  12. package/dist/claude-latest-service.d.ts +0 -34
  13. package/dist/claude-latest-service.js +0 -61
  14. package/dist/config.d.ts +0 -78
  15. package/dist/config.js +0 -59
  16. package/dist/data-dir.d.ts +0 -42
  17. package/dist/data-dir.js +0 -70
  18. package/dist/diag.d.ts +0 -43
  19. package/dist/diag.js +0 -83
  20. package/dist/fs-service.d.ts +0 -90
  21. package/dist/fs-service.js +0 -290
  22. package/dist/managed-runtime.d.ts +0 -51
  23. package/dist/managed-runtime.js +0 -411
  24. package/dist/managed-update-helper.d.ts +0 -2
  25. package/dist/mcp-send.d.ts +0 -33
  26. package/dist/origin-check.d.ts +0 -37
  27. package/dist/origin-check.js +0 -101
  28. package/dist/pane-status.d.ts +0 -61
  29. package/dist/pane-status.js +0 -145
  30. package/dist/providers/claude-metadata-service.d.ts +0 -58
  31. package/dist/providers/claude-metadata-service.js +0 -352
  32. package/dist/providers/claude-provider.d.ts +0 -11
  33. package/dist/providers/claude-provider.js +0 -166
  34. package/dist/providers/codex-activity.d.ts +0 -21
  35. package/dist/providers/codex-activity.js +0 -122
  36. package/dist/providers/codex-app-server-client.d.ts +0 -90
  37. package/dist/providers/codex-app-server-client.js +0 -485
  38. package/dist/providers/codex-latest-service.d.ts +0 -50
  39. package/dist/providers/codex-latest-service.js +0 -174
  40. package/dist/providers/codex-metadata-service.d.ts +0 -161
  41. package/dist/providers/codex-metadata-service.js +0 -686
  42. package/dist/providers/codex-profile-client.d.ts +0 -16
  43. package/dist/providers/codex-profile-client.js +0 -52
  44. package/dist/providers/codex-profile-security.d.ts +0 -23
  45. package/dist/providers/codex-profile-security.js +0 -161
  46. package/dist/providers/codex-provider.d.ts +0 -15
  47. package/dist/providers/codex-provider.js +0 -174
  48. package/dist/providers/codex-thread-coordinator.d.ts +0 -18
  49. package/dist/providers/codex-thread-coordinator.js +0 -93
  50. package/dist/providers/codex-thread-persistence.d.ts +0 -9
  51. package/dist/providers/codex-thread-persistence.js +0 -45
  52. package/dist/providers/codex-thread-resolver.d.ts +0 -59
  53. package/dist/providers/codex-thread-resolver.js +0 -322
  54. package/dist/providers/options.d.ts +0 -7
  55. package/dist/providers/options.js +0 -155
  56. package/dist/providers/provider-artifacts.d.ts +0 -3
  57. package/dist/providers/provider-artifacts.js +0 -30
  58. package/dist/providers/registry.d.ts +0 -7
  59. package/dist/providers/registry.js +0 -23
  60. package/dist/providers/types.d.ts +0 -95
  61. package/dist/providers/types.js +0 -8
  62. package/dist/push-dispatch.d.ts +0 -81
  63. package/dist/push-dispatch.js +0 -100
  64. package/dist/push-store.d.ts +0 -25
  65. package/dist/push-store.js +0 -79
  66. package/dist/rate-limit.d.ts +0 -52
  67. package/dist/rate-limit.js +0 -72
  68. package/dist/server-config.d.ts +0 -60
  69. package/dist/server-config.js +0 -77
  70. package/dist/service-install.d.ts +0 -60
  71. package/dist/service-install.js +0 -221
  72. package/dist/session-defaults.d.ts +0 -26
  73. package/dist/session-defaults.js +0 -60
  74. package/dist/session-store.d.ts +0 -81
  75. package/dist/session-store.js +0 -654
  76. package/dist/static-routes.d.ts +0 -101
  77. package/dist/static-routes.js +0 -188
  78. package/dist/terminal-capability.d.ts +0 -5
  79. package/dist/terminal-capability.js +0 -27
  80. package/dist/terminal-manager.d.ts +0 -224
  81. package/dist/terminal-manager.js +0 -917
  82. package/dist/terminal-process.d.ts +0 -85
  83. package/dist/terminal-process.js +0 -238
  84. package/dist/terminal-shared.d.ts +0 -36
  85. package/dist/terminal-shared.js +0 -43
  86. package/dist/tmux-list.d.ts +0 -11
  87. package/dist/tmux-list.js +0 -39
  88. package/dist/transport.d.ts +0 -123
  89. package/dist/transport.js +0 -1559
  90. package/dist/updater.d.ts +0 -161
  91. package/dist/updater.js +0 -451
  92. package/dist/usage-service.d.ts +0 -118
  93. package/dist/usage-service.js +0 -173
  94. package/dist/vapid.d.ts +0 -17
  95. package/dist/vapid.js +0 -31
  96. package/dist/web-push-send.d.ts +0 -20
  97. package/dist/web-push-send.js +0 -21
  98. package/dist/ws-ticket.d.ts +0 -47
  99. package/dist/ws-ticket.js +0 -62
@@ -1,654 +0,0 @@
1
- import { createRequire } from "node:module";
2
- import { parseProviderOptions } from "./providers/options.js";
3
- import { normalizeSessionDefaults, SessionDefaultsConflictError, } from "./session-defaults.js";
4
- const require = createRequire(import.meta.url);
5
- const concreteSessionStores = new WeakSet();
6
- /** Internal trust check used by security-sensitive capabilities; only stores opened by this module are accepted. */
7
- export function isConcreteSessionStore(store) {
8
- return concreteSessionStores.has(store);
9
- }
10
- function brandSessionStore(store) {
11
- concreteSessionStores.add(store);
12
- return store;
13
- }
14
- /** Parse the stored spawn_args JSON back into a string[] — tolerant: a NULL, malformed, or non-array value
15
- * (an ancient row, a hand-edited DB) yields undefined so the session simply respawns flag-less, never throws. */
16
- function parseSpawnArgs(raw) {
17
- if (typeof raw !== "string" || raw.length === 0)
18
- return undefined;
19
- try {
20
- const v = JSON.parse(raw);
21
- return Array.isArray(v) && v.every((x) => typeof x === "string") ? v : undefined;
22
- }
23
- catch {
24
- return undefined;
25
- }
26
- }
27
- function legacyRowToSession(r) {
28
- const spawnArgs = parseSpawnArgs(r.spawn_args);
29
- return {
30
- provider: "claude",
31
- id: r.id,
32
- cwd: r.cwd,
33
- dangerouslySkip: r.dangerously_skip === 1,
34
- status: r.status,
35
- createdAt: r.created_at,
36
- lastActivityAt: r.last_activity_at,
37
- mode: "terminal",
38
- // Only carry a REAL name — NULL/"" stays absent so consumers can `?? cwd` and toEqual-style tests
39
- // of unnamed rows don't grow a noise field.
40
- ...(typeof r.name === "string" && r.name.length > 0 ? { name: r.name } : {}),
41
- ...(spawnArgs && spawnArgs.length > 0 ? { spawnArgs } : {}),
42
- };
43
- }
44
- function parseCodexOptions(raw) {
45
- if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
46
- throw new Error("Invalid Codex launch options");
47
- }
48
- const { provider, ...options } = raw;
49
- if (provider !== "codex")
50
- throw new Error("Invalid Codex launch options provider");
51
- return parseProviderOptions("codex", options);
52
- }
53
- function validateCodexOptions(options) {
54
- return parseCodexOptions(options);
55
- }
56
- function validateProviderSessionId(value) {
57
- if (value === undefined)
58
- return undefined;
59
- if (typeof value !== "string" ||
60
- value.trim().length === 0 ||
61
- value.length > 2048 ||
62
- /[\p{Cc}\p{Zl}\p{Zp}]/u.test(value)) {
63
- throw new Error("Invalid provider session id");
64
- }
65
- return value;
66
- }
67
- function validateIntegrationStatus(value) {
68
- if (typeof value !== "object" || value === null || Array.isArray(value)) {
69
- throw new Error("Invalid integration status");
70
- }
71
- const record = value;
72
- const keys = Object.keys(record);
73
- if (keys.some((key) => !["attachments", "activity", "detail"].includes(key)) ||
74
- (record.attachments !== "ready" && record.attachments !== "degraded") ||
75
- (record.activity !== "ready" && record.activity !== "degraded") ||
76
- (record.detail !== undefined && (typeof record.detail !== "string" || record.detail.length > 2048))) {
77
- throw new Error("Invalid integration status");
78
- }
79
- return {
80
- attachments: record.attachments,
81
- activity: record.activity,
82
- ...(typeof record.detail === "string" && record.detail.length > 0 ? { detail: record.detail } : {}),
83
- };
84
- }
85
- function parseIntegrationStatus(raw) {
86
- if (raw === null || raw.length === 0)
87
- return undefined;
88
- return validateIntegrationStatus(JSON.parse(raw));
89
- }
90
- function providerRowToSession(r) {
91
- try {
92
- if (r.provider !== "codex")
93
- return undefined;
94
- const launchOptions = parseCodexOptions(JSON.parse(r.launch_options_json));
95
- const integrationStatus = parseIntegrationStatus(r.integration_status_json);
96
- return {
97
- provider: "codex",
98
- id: r.id,
99
- cwd: r.cwd,
100
- status: r.status,
101
- createdAt: r.created_at,
102
- lastActivityAt: r.last_activity_at,
103
- mode: "terminal",
104
- launchOptions,
105
- ...(typeof r.name === "string" && r.name.length > 0 ? { name: r.name } : {}),
106
- ...(typeof r.provider_session_id === "string" && r.provider_session_id.length > 0
107
- ? { providerSessionId: r.provider_session_id }
108
- : {}),
109
- ...(integrationStatus ? { integrationStatus } : {}),
110
- };
111
- }
112
- catch {
113
- // Keep corrupt rows in SQLite for diagnostics, but never surface them to a launcher.
114
- return undefined;
115
- }
116
- }
117
- function compareSessions(a, b) {
118
- return a.createdAt - b.createdAt || a.id.localeCompare(b.id);
119
- }
120
- function cloneSession(session) {
121
- if (session.provider === "claude") {
122
- return { ...session, ...(session.spawnArgs ? { spawnArgs: [...session.spawnArgs] } : {}) };
123
- }
124
- return {
125
- ...session,
126
- launchOptions: {
127
- ...session.launchOptions,
128
- ...(session.launchOptions.addDirs ? { addDirs: [...session.launchOptions.addDirs] } : {}),
129
- },
130
- ...(session.integrationStatus ? { integrationStatus: { ...session.integrationStatus } } : {}),
131
- };
132
- }
133
- function cloneStoredSessionDefaults(value) {
134
- return {
135
- defaults: normalizeSessionDefaults(value.defaults),
136
- revision: value.revision,
137
- updatedAt: value.updatedAt,
138
- };
139
- }
140
- function appSettingRowToSessionDefaults(row) {
141
- if (!row)
142
- return undefined;
143
- try {
144
- return {
145
- defaults: normalizeSessionDefaults(JSON.parse(row.value_json)),
146
- revision: row.revision,
147
- updatedAt: row.updated_at,
148
- };
149
- }
150
- catch {
151
- return undefined;
152
- }
153
- }
154
- /**
155
- * In-memory fallback used when the native better-sqlite3 module cannot load
156
- * (no toolchain / unsupported platform) so the server still boots. NOT durable
157
- * across process restarts — surfaced as a diagnostic by the caller.
158
- */
159
- function inMemoryStore() {
160
- const map = new Map();
161
- const provisionalProviderSessionIds = new Map();
162
- let sessionDefaults;
163
- const write = (s) => {
164
- provisionalProviderSessionIds.delete(s.id);
165
- if (s.provider === "codex") {
166
- const launchOptions = validateCodexOptions(s.launchOptions);
167
- const integrationStatus = s.integrationStatus ? validateIntegrationStatus(s.integrationStatus) : undefined;
168
- const providerSessionId = validateProviderSessionId(s.providerSessionId);
169
- map.set(s.id, cloneSession({
170
- ...s,
171
- mode: "terminal",
172
- launchOptions,
173
- ...(providerSessionId ? { providerSessionId } : {}),
174
- ...(integrationStatus ? { integrationStatus } : {}),
175
- }));
176
- return;
177
- }
178
- if (s.provider !== "claude")
179
- throw new Error("Unknown session provider");
180
- map.set(s.id, cloneSession({ ...s, mode: "terminal" }));
181
- };
182
- return {
183
- claimNew: (s) => {
184
- if (map.has(s.id))
185
- throw new Error(`Session id ${s.id} already exists`);
186
- write(s);
187
- },
188
- upsert: (s) => {
189
- const existing = map.get(s.id);
190
- if (existing && existing.provider !== s.provider) {
191
- throw new Error(`Session id ${s.id} already belongs to ${existing.provider}`);
192
- }
193
- write(s);
194
- },
195
- has: (id) => map.has(id),
196
- get: (id) => {
197
- const v = map.get(id);
198
- return v ? cloneSession(v) : undefined;
199
- },
200
- list: () => [...map.values()].map(cloneSession).sort(compareSessions),
201
- setStatus: (id, status) => {
202
- const v = map.get(id);
203
- if (v)
204
- v.status = status;
205
- },
206
- touch: (id, at) => {
207
- const v = map.get(id);
208
- if (v)
209
- v.lastActivityAt = at;
210
- },
211
- setName: (id, name) => {
212
- const v = map.get(id);
213
- if (!v)
214
- return;
215
- if (name === undefined)
216
- delete v.name; // clear = the field goes back to absent, mirroring NULL
217
- else
218
- v.name = name;
219
- },
220
- setProviderSessionId: (id, value) => {
221
- const v = map.get(id);
222
- if (!v || v.provider !== "codex")
223
- return;
224
- const providerSessionId = validateProviderSessionId(value);
225
- provisionalProviderSessionIds.delete(id);
226
- if (providerSessionId === undefined)
227
- delete v.providerSessionId;
228
- else
229
- v.providerSessionId = providerSessionId;
230
- },
231
- markProvisionalProviderSessionId: (id, value) => {
232
- const v = map.get(id);
233
- const providerSessionId = validateProviderSessionId(value);
234
- if (!v || v.provider !== "codex" || v.providerSessionId !== undefined || provisionalProviderSessionIds.has(id)) {
235
- throw new Error("Provisional provider identity unavailable");
236
- }
237
- provisionalProviderSessionIds.set(id, providerSessionId);
238
- },
239
- clearProvisionalProviderSessionId: (id, value) => {
240
- if (provisionalProviderSessionIds.get(id) !== value)
241
- throw new Error("Provisional provider identity changed");
242
- provisionalProviderSessionIds.delete(id);
243
- },
244
- commitProvisionalProviderSessionId: (id, value) => {
245
- const v = map.get(id);
246
- if (!v ||
247
- v.provider !== "codex" ||
248
- v.providerSessionId !== undefined ||
249
- provisionalProviderSessionIds.get(id) !== value) {
250
- throw new Error("Provisional provider identity changed");
251
- }
252
- v.providerSessionId = value;
253
- provisionalProviderSessionIds.delete(id);
254
- },
255
- getSessionDefaults: () => (sessionDefaults ? cloneStoredSessionDefaults(sessionDefaults) : undefined),
256
- putSessionDefaults: (defaults, expectedRevision, updatedAt) => {
257
- if ((sessionDefaults?.revision ?? 0) !== expectedRevision) {
258
- throw new SessionDefaultsConflictError(sessionDefaults ? cloneStoredSessionDefaults(sessionDefaults) : undefined);
259
- }
260
- sessionDefaults = {
261
- defaults: normalizeSessionDefaults(defaults),
262
- revision: expectedRevision + 1,
263
- updatedAt,
264
- };
265
- return cloneStoredSessionDefaults(sessionDefaults);
266
- },
267
- delete: (id) => {
268
- provisionalProviderSessionIds.delete(id);
269
- map.delete(id);
270
- },
271
- close: () => {
272
- provisionalProviderSessionIds.clear();
273
- map.clear();
274
- sessionDefaults = undefined;
275
- },
276
- mode: "memory-fallback",
277
- };
278
- }
279
- export function openSessionStore(opts) {
280
- let Database;
281
- try {
282
- // Dynamic require keeps the native dep out of the module graph until needed
283
- // and lets us fall back gracefully if the build is missing. Injectable so tests force the fallback.
284
- if (opts.loadDatabase) {
285
- Database = opts.loadDatabase();
286
- }
287
- else {
288
- const mod = require("better-sqlite3");
289
- Database = (mod.default ?? mod);
290
- }
291
- }
292
- catch {
293
- return brandSessionStore(inMemoryStore());
294
- }
295
- const db = new Database(opts.dbPath);
296
- db.pragma("journal_mode = WAL");
297
- db.pragma("busy_timeout = 5000");
298
- db.exec(`
299
- CREATE TABLE IF NOT EXISTS sessions (
300
- id TEXT PRIMARY KEY,
301
- cwd TEXT NOT NULL,
302
- dangerously_skip INTEGER NOT NULL DEFAULT 0,
303
- status TEXT NOT NULL,
304
- created_at INTEGER NOT NULL,
305
- last_activity_at INTEGER NOT NULL,
306
- mode TEXT NOT NULL DEFAULT 'terminal',
307
- name TEXT
308
- )
309
- `);
310
- // TERMINAL-ONLY MIGRATION: a chat-era DB carries dead columns (model, effort, permission_mode,
311
- // display_name, context_window) and possibly stale mode!='terminal' rows the app never surfaces.
312
- // Drop the dead columns and prune the stale rows so the schema matches the terminal-only model. All
313
- // guarded (best-effort): on a FRESH DB the columns don't exist ("no such column"); on an ancient SQLite
314
- // DROP COLUMN may be unsupported — either way we swallow and boot with the columns simply unused.
315
- for (const col of ["model", "effort", "permission_mode", "display_name", "context_window"]) {
316
- try {
317
- db.exec(`ALTER TABLE sessions DROP COLUMN ${col}`);
318
- }
319
- catch {
320
- // column already gone (fresh DB) or DROP COLUMN unsupported — harmless, leave it be
321
- }
322
- }
323
- // A pre-`mode` (Plan-5) DB lacks the column entirely; add it so the prune + queries below work.
324
- try {
325
- db.exec("ALTER TABLE sessions ADD COLUMN mode TEXT NOT NULL DEFAULT 'terminal'");
326
- }
327
- catch {
328
- // column already exists — nothing to do
329
- }
330
- // Session-name migration: a pre-name DB gains the nullable column; existing rows read back name-less
331
- // (NULL → the field stays absent), so old sessions are simply "unnamed" — no backfill needed.
332
- try {
333
- db.exec("ALTER TABLE sessions ADD COLUMN name TEXT");
334
- }
335
- catch {
336
- // column already exists — nothing to do
337
- }
338
- // spawn-args migration: the user's chosen spawn flags (JSON), so a RESTART can respawn an ended session
339
- // with the same model/effort/permission/danger/add-dir. A pre-spawn_args DB gains the nullable column;
340
- // existing rows read back NULL → those sessions respawn flag-less (the old behavior), no backfill needed.
341
- try {
342
- db.exec("ALTER TABLE sessions ADD COLUMN spawn_args TEXT");
343
- }
344
- catch {
345
- // column already exists — nothing to do
346
- }
347
- // Prune leftover non-terminal (chat) rows — never adopted or surfaced by the terminal app, just cruft.
348
- try {
349
- db.exec("DELETE FROM sessions WHERE mode IS NOT NULL AND mode != 'terminal'");
350
- }
351
- catch {
352
- // best-effort — never block boot on the prune
353
- }
354
- // Rollback-safe provider migration: Codex rows live only in this additive table. The legacy `sessions`
355
- // table remains byte-for-byte compatible with older builds, which continue to see Claude rows only.
356
- db.exec(`
357
- CREATE TABLE IF NOT EXISTS provider_sessions (
358
- id TEXT PRIMARY KEY,
359
- provider TEXT NOT NULL CHECK (provider = 'codex'),
360
- cwd TEXT NOT NULL,
361
- status TEXT NOT NULL,
362
- created_at INTEGER NOT NULL,
363
- last_activity_at INTEGER NOT NULL,
364
- name TEXT,
365
- provider_session_id TEXT,
366
- provisional_provider_session_id TEXT,
367
- launch_options_json TEXT NOT NULL,
368
- integration_status_json TEXT
369
- );
370
- CREATE INDEX IF NOT EXISTS provider_sessions_activity_idx
371
- ON provider_sessions(last_activity_at DESC);
372
-
373
- CREATE TABLE IF NOT EXISTS app_settings (
374
- key TEXT PRIMARY KEY,
375
- value_json TEXT NOT NULL,
376
- revision INTEGER NOT NULL CHECK (revision > 0),
377
- updated_at INTEGER NOT NULL
378
- );
379
- `);
380
- try {
381
- db.exec("ALTER TABLE provider_sessions ADD COLUMN provisional_provider_session_id TEXT");
382
- }
383
- catch {
384
- // Column already exists on fresh/current databases.
385
- }
386
- // A previous process can only leave provisional state by terminating before cross-check/rollback.
387
- // It is never resumable and is safe to discard before this process accepts work.
388
- db.exec("UPDATE provider_sessions SET provisional_provider_session_id = NULL");
389
- const legacyUpsertStmt = db.prepare(`
390
- INSERT INTO sessions (id, cwd, dangerously_skip, status, created_at, last_activity_at, mode, name, spawn_args)
391
- VALUES (@id, @cwd, @dangerously_skip, @status, @created_at, @last_activity_at, @mode, @name, @spawn_args)
392
- ON CONFLICT(id) DO UPDATE SET
393
- cwd=excluded.cwd, dangerously_skip=excluded.dangerously_skip,
394
- status=excluded.status, created_at=excluded.created_at, last_activity_at=excluded.last_activity_at,
395
- mode=excluded.mode, name=excluded.name, spawn_args=excluded.spawn_args
396
- `);
397
- const providerUpsertStmt = db.prepare(`
398
- INSERT INTO provider_sessions (
399
- id, provider, cwd, status, created_at, last_activity_at, name,
400
- provider_session_id, provisional_provider_session_id, launch_options_json, integration_status_json
401
- ) VALUES (
402
- @id, @provider, @cwd, @status, @created_at, @last_activity_at, @name,
403
- @provider_session_id, NULL, @launch_options_json, @integration_status_json
404
- )
405
- ON CONFLICT(id) DO UPDATE SET
406
- provider=excluded.provider, cwd=excluded.cwd, status=excluded.status,
407
- created_at=excluded.created_at, last_activity_at=excluded.last_activity_at,
408
- name=excluded.name, provider_session_id=excluded.provider_session_id,
409
- provisional_provider_session_id=NULL, launch_options_json=excluded.launch_options_json,
410
- integration_status_json=excluded.integration_status_json
411
- `);
412
- const legacyInsertStmt = db.prepare(`
413
- INSERT INTO sessions (id, cwd, dangerously_skip, status, created_at, last_activity_at, mode, name, spawn_args)
414
- VALUES (@id, @cwd, @dangerously_skip, @status, @created_at, @last_activity_at, @mode, @name, @spawn_args)
415
- `);
416
- const providerInsertStmt = db.prepare(`
417
- INSERT INTO provider_sessions (
418
- id, provider, cwd, status, created_at, last_activity_at, name,
419
- provider_session_id, provisional_provider_session_id, launch_options_json, integration_status_json
420
- ) VALUES (
421
- @id, @provider, @cwd, @status, @created_at, @last_activity_at, @name,
422
- @provider_session_id, NULL, @launch_options_json, @integration_status_json
423
- )
424
- `);
425
- const legacyGetStmt = db.prepare("SELECT * FROM sessions WHERE id = ?");
426
- const providerGetStmt = db.prepare("SELECT * FROM provider_sessions WHERE id = ?");
427
- const legacyListStmt = db.prepare("SELECT * FROM sessions");
428
- const providerListStmt = db.prepare("SELECT * FROM provider_sessions");
429
- const legacyStatusStmt = db.prepare("UPDATE sessions SET status = ? WHERE id = ?");
430
- const providerStatusStmt = db.prepare("UPDATE provider_sessions SET status = ? WHERE id = ?");
431
- const legacyTouchStmt = db.prepare("UPDATE sessions SET last_activity_at = ? WHERE id = ?");
432
- const providerTouchStmt = db.prepare("UPDATE provider_sessions SET last_activity_at = ? WHERE id = ?");
433
- const legacyNameStmt = db.prepare("UPDATE sessions SET name = ? WHERE id = ?");
434
- const providerNameStmt = db.prepare("UPDATE provider_sessions SET name = ? WHERE id = ?");
435
- const providerSessionIdStmt = db.prepare("UPDATE provider_sessions SET provider_session_id = ?, provisional_provider_session_id = NULL WHERE id = ?");
436
- const providerMarkProvisionalStmt = db.prepare(`
437
- UPDATE provider_sessions
438
- SET provisional_provider_session_id = ?
439
- WHERE id = ? AND provider_session_id IS NULL AND provisional_provider_session_id IS NULL
440
- `);
441
- const providerClearProvisionalStmt = db.prepare(`
442
- UPDATE provider_sessions
443
- SET provisional_provider_session_id = NULL
444
- WHERE id = ? AND provider_session_id IS NULL AND provisional_provider_session_id = ?
445
- `);
446
- const providerCommitProvisionalStmt = db.prepare(`
447
- UPDATE provider_sessions
448
- SET provider_session_id = ?, provisional_provider_session_id = NULL
449
- WHERE id = ? AND provider_session_id IS NULL AND provisional_provider_session_id = ?
450
- `);
451
- const legacyDeleteStmt = db.prepare("DELETE FROM sessions WHERE id = ?");
452
- const providerDeleteStmt = db.prepare("DELETE FROM provider_sessions WHERE id = ?");
453
- const sessionDefaultsGetStmt = db.prepare("SELECT * FROM app_settings WHERE key = 'session_defaults'");
454
- const sessionDefaultsPutStmt = db.prepare(`
455
- INSERT INTO app_settings (key, value_json, revision, updated_at)
456
- VALUES ('session_defaults', ?, ?, ?)
457
- ON CONFLICT(key) DO UPDATE SET
458
- value_json=excluded.value_json, revision=excluded.revision, updated_at=excluded.updated_at
459
- `);
460
- const ownerOf = (id) => {
461
- const hasLegacy = legacyGetStmt.get(id) !== undefined;
462
- const hasProvider = providerGetStmt.get(id) !== undefined;
463
- // A cross-table id collision is ambiguous. Fail closed instead of mutating or launching the wrong owner.
464
- if (hasLegacy === hasProvider)
465
- return undefined;
466
- return hasLegacy ? "claude" : "codex";
467
- };
468
- const upsertLegacyAtomically = db.transaction((s) => {
469
- if (providerGetStmt.get(s.id) !== undefined) {
470
- throw new Error(`Session id ${s.id} already belongs to codex`);
471
- }
472
- legacyUpsertStmt.run({
473
- id: s.id,
474
- cwd: s.cwd,
475
- dangerously_skip: s.dangerouslySkip ? 1 : 0,
476
- status: s.status,
477
- created_at: s.createdAt,
478
- last_activity_at: s.lastActivityAt,
479
- mode: s.mode ?? "terminal",
480
- name: s.name ?? null,
481
- spawn_args: s.spawnArgs && s.spawnArgs.length > 0 ? JSON.stringify(s.spawnArgs) : null,
482
- });
483
- });
484
- const upsertProviderAtomically = db.transaction((s, launchOptions, integrationStatus, providerSessionId) => {
485
- if (legacyGetStmt.get(s.id) !== undefined) {
486
- throw new Error(`Session id ${s.id} already belongs to claude`);
487
- }
488
- providerUpsertStmt.run({
489
- id: s.id,
490
- provider: "codex",
491
- cwd: s.cwd,
492
- status: s.status,
493
- created_at: s.createdAt,
494
- last_activity_at: s.lastActivityAt,
495
- name: s.name ?? null,
496
- provider_session_id: providerSessionId ?? null,
497
- launch_options_json: JSON.stringify(launchOptions),
498
- integration_status_json: integrationStatus ? JSON.stringify(integrationStatus) : null,
499
- });
500
- });
501
- const claimLegacyAtomically = db.transaction((s) => {
502
- if (legacyGetStmt.get(s.id) !== undefined || providerGetStmt.get(s.id) !== undefined) {
503
- throw new Error(`Session id ${s.id} already exists`);
504
- }
505
- legacyInsertStmt.run({
506
- id: s.id,
507
- cwd: s.cwd,
508
- dangerously_skip: s.dangerouslySkip ? 1 : 0,
509
- status: s.status,
510
- created_at: s.createdAt,
511
- last_activity_at: s.lastActivityAt,
512
- mode: s.mode ?? "terminal",
513
- name: s.name ?? null,
514
- spawn_args: s.spawnArgs && s.spawnArgs.length > 0 ? JSON.stringify(s.spawnArgs) : null,
515
- });
516
- });
517
- const claimProviderAtomically = db.transaction((s, launchOptions, integrationStatus, providerSessionId) => {
518
- if (legacyGetStmt.get(s.id) !== undefined || providerGetStmt.get(s.id) !== undefined) {
519
- throw new Error(`Session id ${s.id} already exists`);
520
- }
521
- providerInsertStmt.run({
522
- id: s.id,
523
- provider: "codex",
524
- cwd: s.cwd,
525
- status: s.status,
526
- created_at: s.createdAt,
527
- last_activity_at: s.lastActivityAt,
528
- name: s.name ?? null,
529
- provider_session_id: providerSessionId ?? null,
530
- launch_options_json: JSON.stringify(launchOptions),
531
- integration_status_json: integrationStatus ? JSON.stringify(integrationStatus) : null,
532
- });
533
- });
534
- const putSessionDefaultsAtomically = db.transaction((defaults, expectedRevision, updatedAt) => {
535
- const row = sessionDefaultsGetStmt.get();
536
- const current = appSettingRowToSessionDefaults(row);
537
- if (row !== undefined && current === undefined) {
538
- throw new SessionDefaultsConflictError(undefined);
539
- }
540
- if ((row?.revision ?? 0) !== expectedRevision) {
541
- throw new SessionDefaultsConflictError(current ? cloneStoredSessionDefaults(current) : undefined);
542
- }
543
- const stored = {
544
- defaults,
545
- revision: (row?.revision ?? 0) + 1,
546
- updatedAt,
547
- };
548
- sessionDefaultsPutStmt.run(JSON.stringify(defaults), stored.revision, updatedAt);
549
- return cloneStoredSessionDefaults(stored);
550
- });
551
- return brandSessionStore({
552
- claimNew: (s) => {
553
- if (s.provider === "claude") {
554
- claimLegacyAtomically.immediate(s);
555
- return;
556
- }
557
- if (s.provider !== "codex")
558
- throw new Error("Unknown session provider");
559
- const launchOptions = validateCodexOptions(s.launchOptions);
560
- const integrationStatus = s.integrationStatus ? validateIntegrationStatus(s.integrationStatus) : undefined;
561
- const providerSessionId = validateProviderSessionId(s.providerSessionId);
562
- claimProviderAtomically.immediate(s, launchOptions, integrationStatus, providerSessionId);
563
- },
564
- upsert: (s) => {
565
- if (s.provider === "claude") {
566
- upsertLegacyAtomically.immediate(s);
567
- return;
568
- }
569
- if (s.provider !== "codex")
570
- throw new Error("Unknown session provider");
571
- const launchOptions = validateCodexOptions(s.launchOptions);
572
- const integrationStatus = s.integrationStatus ? validateIntegrationStatus(s.integrationStatus) : undefined;
573
- const providerSessionId = validateProviderSessionId(s.providerSessionId);
574
- upsertProviderAtomically.immediate(s, launchOptions, integrationStatus, providerSessionId);
575
- },
576
- has: (id) => legacyGetStmt.get(id) !== undefined || providerGetStmt.get(id) !== undefined,
577
- get: (id) => {
578
- const owner = ownerOf(id);
579
- if (owner === "claude") {
580
- return legacyRowToSession(legacyGetStmt.get(id));
581
- }
582
- if (owner === "codex") {
583
- return providerRowToSession(providerGetStmt.get(id));
584
- }
585
- return undefined;
586
- },
587
- list: () => {
588
- const legacyRows = legacyListStmt.all();
589
- const providerRows = providerListStmt.all();
590
- const providerIds = new Set(providerRows.map((row) => row.id));
591
- const ambiguousIds = new Set(legacyRows.filter((row) => providerIds.has(row.id)).map((row) => row.id));
592
- const legacy = legacyRows.filter((row) => !ambiguousIds.has(row.id)).map(legacyRowToSession);
593
- const provider = providerRows
594
- .filter((row) => !ambiguousIds.has(row.id))
595
- .map(providerRowToSession)
596
- .filter((session) => session !== undefined);
597
- return [...legacy, ...provider].sort(compareSessions);
598
- },
599
- setStatus: (id, status) => {
600
- const owner = ownerOf(id);
601
- if (owner === "claude")
602
- legacyStatusStmt.run(status, id);
603
- else if (owner === "codex")
604
- providerStatusStmt.run(status, id);
605
- },
606
- touch: (id, at) => {
607
- const owner = ownerOf(id);
608
- if (owner === "claude")
609
- legacyTouchStmt.run(at, id);
610
- else if (owner === "codex")
611
- providerTouchStmt.run(at, id);
612
- },
613
- setName: (id, name) => {
614
- const owner = ownerOf(id);
615
- if (owner === "claude")
616
- legacyNameStmt.run(name ?? null, id);
617
- else if (owner === "codex")
618
- providerNameStmt.run(name ?? null, id);
619
- },
620
- setProviderSessionId: (id, value) => {
621
- if (ownerOf(id) !== "codex")
622
- return;
623
- providerSessionIdStmt.run(validateProviderSessionId(value) ?? null, id);
624
- },
625
- markProvisionalProviderSessionId: (id, value) => {
626
- const providerSessionId = validateProviderSessionId(value);
627
- if (providerMarkProvisionalStmt.run(providerSessionId, id).changes !== 1) {
628
- throw new Error("Provisional provider identity unavailable");
629
- }
630
- },
631
- clearProvisionalProviderSessionId: (id, value) => {
632
- if (providerClearProvisionalStmt.run(id, validateProviderSessionId(value)).changes !== 1) {
633
- throw new Error("Provisional provider identity changed");
634
- }
635
- },
636
- commitProvisionalProviderSessionId: (id, value) => {
637
- const providerSessionId = validateProviderSessionId(value);
638
- if (providerCommitProvisionalStmt.run(providerSessionId, id, providerSessionId).changes !== 1) {
639
- throw new Error("Provisional provider identity changed");
640
- }
641
- },
642
- getSessionDefaults: () => appSettingRowToSessionDefaults(sessionDefaultsGetStmt.get()),
643
- putSessionDefaults: (defaults, expectedRevision, updatedAt) => putSessionDefaultsAtomically.immediate(normalizeSessionDefaults(defaults), expectedRevision, updatedAt),
644
- delete: (id) => {
645
- const owner = ownerOf(id);
646
- if (owner === "claude")
647
- legacyDeleteStmt.run(id);
648
- else if (owner === "codex")
649
- providerDeleteStmt.run(id);
650
- },
651
- close: () => db.close(),
652
- mode: "sqlite",
653
- });
654
- }