@mobvibe/cli 0.1.7 → 0.1.8

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.
package/dist/index.js CHANGED
@@ -1,47 +1,10 @@
1
+ // @bun
2
+ var __require = import.meta.require;
3
+
1
4
  // src/index.ts
5
+ import { Database as Database3 } from "bun:sqlite";
2
6
  import { Command } from "commander";
3
7
 
4
- // src/auth/login.ts
5
- import * as readline from "readline/promises";
6
-
7
- // src/lib/logger.ts
8
- import pino from "pino";
9
- var LOG_LEVEL = process.env.LOG_LEVEL ?? "info";
10
- var isPretty = process.env.NODE_ENV !== "production";
11
- var redact = {
12
- paths: [
13
- "req.headers.authorization",
14
- "req.headers.cookie",
15
- "req.headers['x-api-key']",
16
- "headers.authorization",
17
- "headers.cookie",
18
- "headers['x-api-key']",
19
- "apiKey",
20
- "token"
21
- ],
22
- censor: "[redacted]"
23
- };
24
- var transport = isPretty ? {
25
- target: "pino-pretty",
26
- options: {
27
- colorize: true,
28
- translateTime: "SYS:standard",
29
- ignore: "pid,hostname"
30
- }
31
- } : void 0;
32
- var logger = pino(
33
- {
34
- level: LOG_LEVEL,
35
- redact,
36
- base: { service: "mobvibe-cli" },
37
- serializers: {
38
- err: pino.stdSerializers.err,
39
- error: pino.stdSerializers.err
40
- }
41
- },
42
- transport ? pino.transport(transport) : void 0
43
- );
44
-
45
8
  // src/auth/credentials.ts
46
9
  import fs from "fs/promises";
47
10
  import os from "os";
@@ -55,7 +18,7 @@ async function loadCredentials() {
55
18
  try {
56
19
  const data = await fs.readFile(CREDENTIALS_FILE, "utf8");
57
20
  const credentials = JSON.parse(data);
58
- if (!credentials.apiKey) {
21
+ if (!credentials.masterSecret) {
59
22
  return null;
60
23
  }
61
24
  return credentials;
@@ -65,25 +28,19 @@ async function loadCredentials() {
65
28
  }
66
29
  async function saveCredentials(credentials) {
67
30
  await ensureMobvibeDir();
68
- await fs.writeFile(
69
- CREDENTIALS_FILE,
70
- JSON.stringify(credentials, null, 2),
71
- { mode: 384 }
72
- // Read/write only for owner
73
- );
31
+ await fs.writeFile(CREDENTIALS_FILE, JSON.stringify(credentials, null, 2), { mode: 384 });
74
32
  }
75
33
  async function deleteCredentials() {
76
34
  try {
77
35
  await fs.unlink(CREDENTIALS_FILE);
78
- } catch {
79
- }
36
+ } catch {}
80
37
  }
81
- async function getApiKey() {
82
- if (process.env.MOBVIBE_API_KEY) {
83
- return process.env.MOBVIBE_API_KEY;
38
+ async function getMasterSecret() {
39
+ if (process.env.MOBVIBE_MASTER_SECRET) {
40
+ return process.env.MOBVIBE_MASTER_SECRET;
84
41
  }
85
42
  const credentials = await loadCredentials();
86
- return credentials?.apiKey;
43
+ return credentials?.masterSecret;
87
44
  }
88
45
  var DEFAULT_GATEWAY_URL = "https://mobvibe.zeabur.app";
89
46
  async function getGatewayUrl() {
@@ -98,37 +55,195 @@ async function getGatewayUrl() {
98
55
  }
99
56
 
100
57
  // src/auth/login.ts
58
+ import os2 from "os";
59
+ import * as readline from "readline/promises";
60
+ import { Writable } from "stream";
61
+ import {
62
+ deriveAuthKeyPair,
63
+ generateMasterSecret,
64
+ getSodium,
65
+ initCrypto
66
+ } from "@mobvibe/shared";
67
+
68
+ // src/lib/logger.ts
69
+ import pino from "pino";
70
+ var LOG_LEVEL = process.env.LOG_LEVEL ?? "info";
71
+ var isPretty = true;
72
+ var redact = {
73
+ paths: [
74
+ "req.headers.authorization",
75
+ "req.headers.cookie",
76
+ "req.headers['x-api-key']",
77
+ "headers.authorization",
78
+ "headers.cookie",
79
+ "headers['x-api-key']",
80
+ "apiKey",
81
+ "token"
82
+ ],
83
+ censor: "[redacted]"
84
+ };
85
+ var transport = isPretty ? {
86
+ target: "pino-pretty",
87
+ options: {
88
+ colorize: true,
89
+ translateTime: "SYS:standard",
90
+ ignore: "pid,hostname"
91
+ }
92
+ } : undefined;
93
+ var logger = pino({
94
+ level: LOG_LEVEL,
95
+ redact,
96
+ base: { service: "mobvibe-cli" },
97
+ serializers: {
98
+ err: pino.stdSerializers.err,
99
+ error: pino.stdSerializers.err
100
+ }
101
+ }, transport ? pino.transport(transport) : undefined);
102
+
103
+ // src/auth/login.ts
104
+ function readPassword(prompt) {
105
+ return new Promise((resolve, reject) => {
106
+ process.stdout.write(prompt);
107
+ const chars = [];
108
+ if (!process.stdin.isTTY) {
109
+ const rl = readline.createInterface({
110
+ input: process.stdin,
111
+ output: new Writable({ write: (_c, _e, cb) => cb() })
112
+ });
113
+ rl.question("").then((answer) => {
114
+ rl.close();
115
+ process.stdout.write(`
116
+ `);
117
+ resolve(answer);
118
+ }, reject);
119
+ return;
120
+ }
121
+ process.stdin.setRawMode(true);
122
+ process.stdin.resume();
123
+ process.stdin.setEncoding("utf8");
124
+ const onData = (key) => {
125
+ for (const ch of key) {
126
+ const code = ch.charCodeAt(0);
127
+ if (ch === "\r" || ch === `
128
+ `) {
129
+ process.stdin.setRawMode(false);
130
+ process.stdin.pause();
131
+ process.stdin.removeListener("data", onData);
132
+ process.stdout.write(`
133
+ `);
134
+ resolve(chars.join(""));
135
+ return;
136
+ }
137
+ if (code === 3) {
138
+ process.stdin.setRawMode(false);
139
+ process.stdin.pause();
140
+ process.stdin.removeListener("data", onData);
141
+ process.stdout.write(`
142
+ `);
143
+ reject(new Error("User cancelled"));
144
+ return;
145
+ }
146
+ if (code === 127 || code === 8) {
147
+ if (chars.length > 0) {
148
+ chars.pop();
149
+ process.stdout.write("\b \b");
150
+ }
151
+ } else if (code >= 32) {
152
+ chars.push(ch);
153
+ process.stdout.write("*");
154
+ }
155
+ }
156
+ };
157
+ process.stdin.on("data", onData);
158
+ });
159
+ }
101
160
  async function login() {
161
+ await initCrypto();
162
+ const sodium = getSodium();
102
163
  logger.info("login_prompt_start");
103
- console.log("To get an API key:");
104
- console.log(" 1. Open the Mobvibe WebUI in your browser");
105
- console.log(" 2. Go to Settings (gear icon) -> API Keys");
106
- console.log(" 3. Click 'Create API Key' and copy it");
107
- console.log(" 4. Paste the API key below\n");
164
+ console.log(`Mobvibe E2EE Login
165
+ `);
108
166
  const rl = readline.createInterface({
109
167
  input: process.stdin,
110
168
  output: process.stdout
111
169
  });
112
170
  try {
113
- const apiKey = await rl.question("Paste your API key: ");
114
- if (!apiKey.trim()) {
115
- logger.warn("login_missing_api_key");
116
- return { success: false, error: "No API key provided" };
171
+ const email = await rl.question("Email: ");
172
+ if (!email.trim()) {
173
+ return { success: false, error: "No email provided" };
174
+ }
175
+ rl.close();
176
+ const password = await readPassword("Password: ");
177
+ if (!password.trim()) {
178
+ return { success: false, error: "No password provided" };
179
+ }
180
+ const gatewayUrl = await getGatewayUrl();
181
+ console.log(`
182
+ Signing in...`);
183
+ const signInResponse = await fetch(`${gatewayUrl}/api/auth/sign-in/email`, {
184
+ method: "POST",
185
+ headers: { "Content-Type": "application/json" },
186
+ body: JSON.stringify({
187
+ email: email.trim(),
188
+ password: password.trim()
189
+ })
190
+ });
191
+ if (!signInResponse.ok) {
192
+ const body = await signInResponse.text();
193
+ logger.warn({ status: signInResponse.status, body }, "login_sign_in_failed");
194
+ return {
195
+ success: false,
196
+ error: `Sign-in failed (${signInResponse.status}): ${body}`
197
+ };
198
+ }
199
+ const setCookieHeaders = signInResponse.headers.getSetCookie?.() ?? [];
200
+ const cookieHeader = setCookieHeaders.map((c) => c.split(";")[0]).join("; ");
201
+ if (!cookieHeader) {
202
+ return {
203
+ success: false,
204
+ error: "No session cookie received from sign-in"
205
+ };
117
206
  }
118
- if (!apiKey.trim().startsWith("mbk_")) {
119
- logger.warn("login_invalid_api_key_format");
207
+ const masterSecret = generateMasterSecret();
208
+ const authKeyPair = deriveAuthKeyPair(masterSecret);
209
+ const publicKeyBase64 = sodium.to_base64(authKeyPair.publicKey, sodium.base64_variants.ORIGINAL);
210
+ console.log("Registering device...");
211
+ const registerResponse = await fetch(`${gatewayUrl}/auth/device/register`, {
212
+ method: "POST",
213
+ headers: {
214
+ "Content-Type": "application/json",
215
+ Cookie: cookieHeader
216
+ },
217
+ body: JSON.stringify({
218
+ publicKey: publicKeyBase64,
219
+ deviceName: os2.hostname()
220
+ })
221
+ });
222
+ if (!registerResponse.ok) {
223
+ const body = await registerResponse.text();
224
+ logger.warn({ status: registerResponse.status, body }, "login_device_register_failed");
120
225
  return {
121
226
  success: false,
122
- error: "Invalid API key format (should start with mbk_)"
227
+ error: `Device registration failed (${registerResponse.status}): ${body}`
123
228
  };
124
229
  }
230
+ const masterSecretBase64 = sodium.to_base64(masterSecret, sodium.base64_variants.ORIGINAL);
125
231
  const credentials = {
126
- apiKey: apiKey.trim(),
232
+ masterSecret: masterSecretBase64,
127
233
  createdAt: Date.now()
128
234
  };
129
235
  await saveCredentials(credentials);
130
236
  logger.info("login_credentials_saved");
131
- console.log("\nAPI key saved!");
237
+ console.log(`
238
+ Login successful!`);
239
+ console.log(`
240
+ WARNING: The master secret below will appear in your terminal history.`);
241
+ console.log(" Clear your terminal after copying it, or use 'mobvibe e2ee show' later.");
242
+ console.log(`
243
+ Your master secret (for pairing WebUI/Tauri devices):`);
244
+ console.log(` ${masterSecretBase64}`);
245
+ console.log(`
246
+ Keep this secret safe. You can view it again with 'mobvibe e2ee show'.`);
132
247
  console.log("Run 'mobvibe start' to connect to the gateway.");
133
248
  return { success: true };
134
249
  } finally {
@@ -143,9 +258,14 @@ async function logout() {
143
258
  async function loginStatus() {
144
259
  const credentials = await loadCredentials();
145
260
  if (credentials) {
261
+ await initCrypto();
262
+ const sodium = getSodium();
263
+ const masterSecret = sodium.from_base64(credentials.masterSecret, sodium.base64_variants.ORIGINAL);
264
+ const authKeyPair = deriveAuthKeyPair(masterSecret);
265
+ const pubKeyBase64 = sodium.to_base64(authKeyPair.publicKey, sodium.base64_variants.ORIGINAL);
146
266
  logger.info("login_status_logged_in");
147
- console.log("Status: Logged in");
148
- console.log(`API key: ${credentials.apiKey.slice(0, 12)}...`);
267
+ console.log("Status: Logged in (E2EE)");
268
+ console.log(`Auth public key: ${pubKeyBase64.slice(0, 16)}...`);
149
269
  console.log(`Saved: ${new Date(credentials.createdAt).toLocaleString()}`);
150
270
  } else {
151
271
  logger.info("login_status_logged_out");
@@ -155,7 +275,7 @@ async function loginStatus() {
155
275
  }
156
276
 
157
277
  // src/config.ts
158
- import os2 from "os";
278
+ import os3 from "os";
159
279
  import path3 from "path";
160
280
 
161
281
  // src/config-loader.ts
@@ -182,14 +302,14 @@ var validateAgentConfig = (agent, index) => {
182
302
  id: record.id.trim(),
183
303
  command: record.command.trim()
184
304
  };
185
- if (record.label !== void 0) {
305
+ if (record.label !== undefined) {
186
306
  if (typeof record.label !== "string") {
187
307
  errors.push(`${prefix}.label: must be a string`);
188
308
  } else if (record.label.trim().length > 0) {
189
309
  validated.label = record.label.trim();
190
310
  }
191
311
  }
192
- if (record.args !== void 0) {
312
+ if (record.args !== undefined) {
193
313
  if (!Array.isArray(record.args)) {
194
314
  errors.push(`${prefix}.args: must be an array of strings`);
195
315
  } else {
@@ -205,7 +325,7 @@ var validateAgentConfig = (agent, index) => {
205
325
  }
206
326
  }
207
327
  }
208
- if (record.env !== void 0) {
328
+ if (record.env !== undefined) {
209
329
  if (typeof record.env !== "object" || record.env === null) {
210
330
  errors.push(`${prefix}.env: must be an object`);
211
331
  } else {
@@ -238,13 +358,13 @@ var validateUserConfig = (data) => {
238
358
  }
239
359
  const record = data;
240
360
  const config = {};
241
- if (record.agents !== void 0) {
361
+ if (record.agents !== undefined) {
242
362
  if (!Array.isArray(record.agents)) {
243
363
  errors.push("agents: must be an array");
244
364
  } else {
245
365
  const validAgents = [];
246
- const seenIds = /* @__PURE__ */ new Set();
247
- for (let i = 0; i < record.agents.length; i++) {
366
+ const seenIds = new Set;
367
+ for (let i = 0;i < record.agents.length; i++) {
248
368
  const result = validateAgentConfig(record.agents[i], i);
249
369
  errors.push(...result.errors);
250
370
  if (result.valid) {
@@ -261,7 +381,7 @@ var validateUserConfig = (data) => {
261
381
  }
262
382
  }
263
383
  }
264
- if (record.defaultAgentId !== void 0) {
384
+ if (record.defaultAgentId !== undefined) {
265
385
  if (typeof record.defaultAgentId !== "string") {
266
386
  errors.push("defaultAgentId: must be a string");
267
387
  } else if (record.defaultAgentId.trim().length > 0) {
@@ -309,6 +429,14 @@ var loadUserConfig = async (homePath) => {
309
429
  };
310
430
 
311
431
  // src/config.ts
432
+ var DEFAULT_COMPACTION_CONFIG = {
433
+ enabled: false,
434
+ ackedEventRetentionDays: 7,
435
+ keepLatestRevisionsCount: 2,
436
+ runOnStartup: false,
437
+ runIntervalHours: 24,
438
+ minEventsToKeep: 1000
439
+ };
312
440
  var DEFAULT_OPENCODE_BACKEND = {
313
441
  id: "opencode",
314
442
  label: "opencode",
@@ -316,10 +444,10 @@ var DEFAULT_OPENCODE_BACKEND = {
316
444
  args: ["acp"]
317
445
  };
318
446
  var generateMachineId = () => {
319
- const hostname = os2.hostname();
320
- const platform = os2.platform();
321
- const arch = os2.arch();
322
- const username = os2.userInfo().username;
447
+ const hostname = os3.hostname();
448
+ const platform = os3.platform();
449
+ const arch = os3.arch();
450
+ const username = os3.userInfo().username;
323
451
  return `${hostname}-${platform}-${arch}-${username}`;
324
452
  };
325
453
  var userAgentToBackendConfig = (agent) => ({
@@ -331,92 +459,786 @@ var userAgentToBackendConfig = (agent) => ({
331
459
  });
332
460
  var mergeBackends = (defaultBackend, userAgents) => {
333
461
  if (!userAgents || userAgents.length === 0) {
334
- return { backends: [defaultBackend], defaultId: defaultBackend.id };
462
+ return [defaultBackend];
335
463
  }
336
464
  const userOpencode = userAgents.find((a) => a.id === "opencode");
337
465
  if (userOpencode) {
338
- return {
339
- backends: userAgents.map(userAgentToBackendConfig),
340
- defaultId: userAgents[0].id
341
- };
466
+ return userAgents.map(userAgentToBackendConfig);
342
467
  }
343
- return {
344
- backends: [defaultBackend, ...userAgents.map(userAgentToBackendConfig)],
345
- defaultId: defaultBackend.id
346
- };
468
+ return [defaultBackend, ...userAgents.map(userAgentToBackendConfig)];
347
469
  };
348
470
  var getCliConfig = async () => {
349
471
  const env = process.env;
350
- const homePath = env.MOBVIBE_HOME ?? path3.join(os2.homedir(), ".mobvibe");
472
+ const homePath = env.MOBVIBE_HOME ?? path3.join(os3.homedir(), ".mobvibe");
351
473
  const userConfigResult = await loadUserConfig(homePath);
352
474
  if (userConfigResult.errors.length > 0) {
353
475
  for (const error of userConfigResult.errors) {
354
476
  logger.warn({ configPath: userConfigResult.path, error }, "config_error");
355
477
  }
356
478
  }
357
- const { backends, defaultId } = mergeBackends(
358
- DEFAULT_OPENCODE_BACKEND,
359
- userConfigResult.config?.agents
360
- );
361
- const resolvedDefaultId = userConfigResult.config?.defaultAgentId && backends.some((b) => b.id === userConfigResult.config?.defaultAgentId) ? userConfigResult.config.defaultAgentId : defaultId;
479
+ const backends = mergeBackends(DEFAULT_OPENCODE_BACKEND, userConfigResult.config?.agents);
362
480
  const gatewayUrl = await getGatewayUrl();
363
481
  return {
364
482
  gatewayUrl,
365
483
  acpBackends: backends,
366
- defaultAcpBackendId: resolvedDefaultId,
367
484
  clientName: env.MOBVIBE_ACP_CLIENT_NAME ?? "mobvibe-cli",
368
485
  clientVersion: env.MOBVIBE_ACP_CLIENT_VERSION ?? "0.0.0",
369
486
  homePath,
370
487
  logPath: path3.join(homePath, "logs"),
371
488
  pidFile: path3.join(homePath, "daemon.pid"),
489
+ walDbPath: path3.join(homePath, "events.db"),
372
490
  machineId: env.MOBVIBE_MACHINE_ID ?? generateMachineId(),
373
- hostname: os2.hostname(),
374
- platform: os2.platform(),
491
+ hostname: os3.hostname(),
492
+ platform: os3.platform(),
375
493
  userConfigPath: userConfigResult.path,
376
- userConfigErrors: userConfigResult.errors.length > 0 ? userConfigResult.errors : void 0
494
+ userConfigErrors: userConfigResult.errors.length > 0 ? userConfigResult.errors : undefined,
495
+ compaction: {
496
+ ...DEFAULT_COMPACTION_CONFIG,
497
+ enabled: env.MOBVIBE_COMPACTION_ENABLED === "true"
498
+ }
377
499
  };
378
500
  };
379
501
 
380
502
  // src/daemon/daemon.ts
503
+ import { Database as Database2 } from "bun:sqlite";
381
504
  import { spawn as spawn2 } from "child_process";
382
- import fs5 from "fs/promises";
383
- import path6 from "path";
505
+ import fs6 from "fs/promises";
506
+ import path7 from "path";
507
+ import { getSodium as getSodium3, initCrypto as initCrypto2 } from "@mobvibe/shared";
384
508
 
385
509
  // src/acp/session-manager.ts
386
510
  import { randomUUID as randomUUID2 } from "crypto";
387
511
  import { EventEmitter as EventEmitter2 } from "events";
388
- import fs3 from "fs/promises";
512
+ import fs4 from "fs/promises";
513
+ import {
514
+ AppError,
515
+ createErrorDetail as createErrorDetail2
516
+ } from "@mobvibe/shared";
389
517
 
390
- // ../../packages/shared/dist/types/errors.js
391
- var createErrorDetail = (input) => ({
392
- ...input
393
- });
394
- var isProtocolMismatch = (error) => {
395
- if (error instanceof Error) {
396
- return /protocol/i.test(error.message);
518
+ // src/wal/compactor.ts
519
+ class WalCompactor {
520
+ config;
521
+ db;
522
+ activeSessionIds = new Set;
523
+ stmtGetAllSessions;
524
+ stmtGetSessionRevisions;
525
+ stmtDeleteAckedEvents;
526
+ stmtDeleteOldRevisionEvents;
527
+ stmtCountEvents;
528
+ stmtLogCompaction;
529
+ constructor(_walStore, config, db) {
530
+ this.config = config;
531
+ this.db = db;
532
+ this.stmtGetAllSessions = this.db.query(`
533
+ SELECT DISTINCT session_id FROM sessions
534
+ `);
535
+ this.stmtGetSessionRevisions = this.db.query(`
536
+ SELECT DISTINCT revision FROM session_events
537
+ WHERE session_id = $sessionId
538
+ ORDER BY revision DESC
539
+ `);
540
+ this.stmtDeleteAckedEvents = this.db.query(`
541
+ DELETE FROM session_events
542
+ WHERE session_id = $sessionId
543
+ AND revision = $revision
544
+ AND acked_at IS NOT NULL
545
+ AND acked_at < $olderThan
546
+ AND id NOT IN (
547
+ SELECT id FROM session_events
548
+ WHERE session_id = $sessionId AND revision = $revision
549
+ ORDER BY seq DESC
550
+ LIMIT $minKeep
551
+ )
552
+ `);
553
+ this.stmtDeleteOldRevisionEvents = this.db.query(`
554
+ DELETE FROM session_events
555
+ WHERE session_id = $sessionId
556
+ AND revision = $revision
557
+ `);
558
+ this.stmtCountEvents = this.db.query(`
559
+ SELECT COUNT(*) as count FROM session_events
560
+ WHERE session_id = $sessionId AND revision = $revision
561
+ `);
562
+ this.stmtLogCompaction = this.db.query(`
563
+ INSERT INTO compaction_log (session_id, revision, operation, events_affected, started_at, completed_at)
564
+ VALUES ($sessionId, $revision, $operation, $eventsAffected, $startedAt, $completedAt)
565
+ `);
566
+ }
567
+ markSessionActive(sessionId) {
568
+ this.activeSessionIds.add(sessionId);
569
+ }
570
+ markSessionInactive(sessionId) {
571
+ this.activeSessionIds.delete(sessionId);
572
+ }
573
+ shouldSkipSession(sessionId) {
574
+ return this.activeSessionIds.has(sessionId);
575
+ }
576
+ async compactAll(options) {
577
+ const startTime = performance.now();
578
+ const stats = [];
579
+ const skipped = [];
580
+ const sessions = this.stmtGetAllSessions.all();
581
+ for (const { session_id: sessionId } of sessions) {
582
+ if (this.shouldSkipSession(sessionId)) {
583
+ skipped.push(sessionId);
584
+ continue;
585
+ }
586
+ try {
587
+ const sessionStats = await this.compactSession(sessionId, options);
588
+ stats.push(sessionStats);
589
+ } catch (error) {
590
+ logger.error({ err: error, sessionId }, "compaction_session_error");
591
+ }
592
+ }
593
+ const totalDurationMs = performance.now() - startTime;
594
+ const totalDeleted = stats.reduce((sum, s) => sum + s.ackedEventsDeleted + s.oldRevisionsDeleted, 0);
595
+ if (!options?.dryRun && totalDeleted > 0) {
596
+ try {
597
+ this.db.exec("VACUUM");
598
+ logger.info({ totalDeleted }, "compaction_vacuum_complete");
599
+ } catch (error) {
600
+ logger.error({ err: error }, "compaction_vacuum_error");
601
+ }
602
+ }
603
+ logger.info({
604
+ sessionsCompacted: stats.length,
605
+ sessionsSkipped: skipped.length,
606
+ totalDeleted,
607
+ totalDurationMs
608
+ }, "compaction_complete");
609
+ return { stats, totalDurationMs, skipped };
610
+ }
611
+ async compactSession(sessionId, options) {
612
+ const startTime = performance.now();
613
+ let ackedEventsDeleted = 0;
614
+ let oldRevisionsDeleted = 0;
615
+ const revisions = this.stmtGetSessionRevisions.all({
616
+ $sessionId: sessionId
617
+ });
618
+ if (revisions.length === 0) {
619
+ return {
620
+ sessionId,
621
+ ackedEventsDeleted: 0,
622
+ oldRevisionsDeleted: 0,
623
+ durationMs: performance.now() - startTime
624
+ };
625
+ }
626
+ const revisionsToKeep = revisions.slice(0, this.config.keepLatestRevisionsCount).map((r) => r.revision);
627
+ const ackedCutoff = new Date;
628
+ ackedCutoff.setDate(ackedCutoff.getDate() - this.config.ackedEventRetentionDays);
629
+ for (const { revision } of revisions) {
630
+ if (!revisionsToKeep.includes(revision)) {
631
+ const countResult = this.stmtCountEvents.get({
632
+ $sessionId: sessionId,
633
+ $revision: revision
634
+ });
635
+ if (!options?.dryRun) {
636
+ const result = this.stmtDeleteOldRevisionEvents.run({
637
+ $sessionId: sessionId,
638
+ $revision: revision
639
+ });
640
+ oldRevisionsDeleted += result.changes;
641
+ this.logCompaction(sessionId, revision, "delete_old_revision", result.changes);
642
+ } else {
643
+ oldRevisionsDeleted += countResult.count;
644
+ }
645
+ logger.debug({ sessionId, revision, count: countResult.count }, "compaction_delete_old_revision");
646
+ continue;
647
+ }
648
+ if (!options?.dryRun) {
649
+ const result = this.stmtDeleteAckedEvents.run({
650
+ $sessionId: sessionId,
651
+ $revision: revision,
652
+ $olderThan: ackedCutoff.toISOString(),
653
+ $minKeep: this.config.minEventsToKeep
654
+ });
655
+ ackedEventsDeleted += result.changes;
656
+ if (result.changes > 0) {
657
+ this.logCompaction(sessionId, revision, "delete_acked_events", result.changes);
658
+ logger.debug({ sessionId, revision, deleted: result.changes }, "compaction_delete_acked");
659
+ }
660
+ }
661
+ }
662
+ const durationMs = performance.now() - startTime;
663
+ if (ackedEventsDeleted > 0 || oldRevisionsDeleted > 0) {
664
+ logger.info({ sessionId, ackedEventsDeleted, oldRevisionsDeleted, durationMs }, "compaction_session_complete");
665
+ }
666
+ return {
667
+ sessionId,
668
+ ackedEventsDeleted,
669
+ oldRevisionsDeleted,
670
+ durationMs
671
+ };
397
672
  }
398
- return false;
399
- };
400
- var AppError = class extends Error {
401
- detail;
402
- status;
403
- constructor(detail, status = 500) {
404
- super(detail.message);
405
- this.detail = detail;
406
- this.status = status;
673
+ logCompaction(sessionId, revision, operation, eventsAffected) {
674
+ const now = new Date().toISOString();
675
+ this.stmtLogCompaction.run({
676
+ $sessionId: sessionId,
677
+ $revision: revision,
678
+ $operation: operation,
679
+ $eventsAffected: eventsAffected,
680
+ $startedAt: now,
681
+ $completedAt: now
682
+ });
407
683
  }
408
- };
684
+ }
685
+ // src/wal/migrations.ts
686
+ var MIGRATIONS = [
687
+ {
688
+ version: 1,
689
+ up: `
690
+ -- Sessions table to track session metadata and current revision
691
+ CREATE TABLE IF NOT EXISTS sessions (
692
+ session_id TEXT PRIMARY KEY,
693
+ machine_id TEXT NOT NULL,
694
+ backend_id TEXT NOT NULL,
695
+ current_revision INTEGER NOT NULL DEFAULT 1,
696
+ cwd TEXT,
697
+ title TEXT,
698
+ created_at TEXT NOT NULL,
699
+ updated_at TEXT NOT NULL
700
+ );
701
+
702
+ -- Session events WAL table
703
+ CREATE TABLE IF NOT EXISTS session_events (
704
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
705
+ session_id TEXT NOT NULL,
706
+ revision INTEGER NOT NULL,
707
+ seq INTEGER NOT NULL,
708
+ kind TEXT NOT NULL,
709
+ payload TEXT NOT NULL,
710
+ created_at TEXT NOT NULL,
711
+ acked_at TEXT,
712
+ UNIQUE (session_id, revision, seq)
713
+ );
714
+
715
+ -- Index for querying events by session and revision
716
+ CREATE INDEX IF NOT EXISTS idx_session_events_session_revision
717
+ ON session_events (session_id, revision, seq);
718
+
719
+ -- Index for querying unacked events
720
+ CREATE INDEX IF NOT EXISTS idx_session_events_unacked
721
+ ON session_events (session_id, revision, acked_at)
722
+ WHERE acked_at IS NULL;
723
+
724
+ -- Schema version tracking
725
+ CREATE TABLE IF NOT EXISTS schema_version (
726
+ version INTEGER PRIMARY KEY
727
+ );
728
+ `
729
+ },
730
+ {
731
+ version: 2,
732
+ up: `
733
+ -- Discovered sessions table for persisting sessions found via discoverSessions()
734
+ CREATE TABLE IF NOT EXISTS discovered_sessions (
735
+ session_id TEXT PRIMARY KEY,
736
+ backend_id TEXT NOT NULL,
737
+ cwd TEXT,
738
+ title TEXT,
739
+ agent_updated_at TEXT, -- agent-reported update time
740
+ discovered_at TEXT NOT NULL,
741
+ last_verified_at TEXT, -- last time cwd was verified to exist
742
+ is_stale INTEGER DEFAULT 0 -- marked stale when cwd no longer exists
743
+ );
744
+
745
+ CREATE INDEX IF NOT EXISTS idx_discovered_sessions_backend
746
+ ON discovered_sessions (backend_id);
747
+
748
+ -- Add agent_updated_at to sessions table
749
+ ALTER TABLE sessions ADD COLUMN agent_updated_at TEXT;
750
+ `
751
+ },
752
+ {
753
+ version: 3,
754
+ up: `
755
+ -- Compaction support
756
+ ALTER TABLE session_events ADD COLUMN compacted_at TEXT;
409
757
 
758
+ -- Index for finding acked events eligible for cleanup
759
+ CREATE INDEX IF NOT EXISTS idx_session_events_acked_at
760
+ ON session_events (session_id, revision, acked_at)
761
+ WHERE acked_at IS NOT NULL;
762
+
763
+ -- Index for finding events by kind (for chunk consolidation)
764
+ CREATE INDEX IF NOT EXISTS idx_session_events_kind
765
+ ON session_events (session_id, revision, kind);
766
+
767
+ -- Compaction operation log
768
+ CREATE TABLE IF NOT EXISTS compaction_log (
769
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
770
+ session_id TEXT NOT NULL,
771
+ revision INTEGER,
772
+ operation TEXT NOT NULL,
773
+ events_affected INTEGER NOT NULL,
774
+ started_at TEXT NOT NULL,
775
+ completed_at TEXT
776
+ );
777
+ `
778
+ },
779
+ {
780
+ version: 4,
781
+ up: `
782
+ -- Archived sessions table (local archive state)
783
+ CREATE TABLE IF NOT EXISTS archived_session_ids (
784
+ session_id TEXT PRIMARY KEY,
785
+ archived_at TEXT NOT NULL
786
+ );
787
+ `
788
+ }
789
+ ];
790
+ function runMigrations(db) {
791
+ db.exec("PRAGMA journal_mode = WAL");
792
+ db.exec("PRAGMA synchronous = NORMAL");
793
+ let currentVersion = 0;
794
+ try {
795
+ const result = db.query("SELECT MAX(version) as version FROM schema_version").get();
796
+ currentVersion = result?.version ?? 0;
797
+ } catch {}
798
+ for (const migration of MIGRATIONS) {
799
+ if (migration.version > currentVersion) {
800
+ db.exec(migration.up);
801
+ db.exec(`INSERT INTO schema_version (version) VALUES (${migration.version})`);
802
+ }
803
+ }
804
+ }
805
+ // src/wal/seq-generator.ts
806
+ class SeqGenerator {
807
+ sequences = new Map;
808
+ buildKey(sessionId, revision) {
809
+ return `${sessionId}:${revision}`;
810
+ }
811
+ initialize(sessionId, revision, lastSeq) {
812
+ const key = this.buildKey(sessionId, revision);
813
+ this.sequences.set(key, lastSeq);
814
+ }
815
+ next(sessionId, revision) {
816
+ const key = this.buildKey(sessionId, revision);
817
+ const current = this.sequences.get(key) ?? 0;
818
+ const next = current + 1;
819
+ this.sequences.set(key, next);
820
+ return next;
821
+ }
822
+ current(sessionId, revision) {
823
+ const key = this.buildKey(sessionId, revision);
824
+ return this.sequences.get(key) ?? 0;
825
+ }
826
+ reset(sessionId, revision) {
827
+ const key = this.buildKey(sessionId, revision);
828
+ this.sequences.set(key, 0);
829
+ }
830
+ clearSession(sessionId) {
831
+ for (const key of this.sequences.keys()) {
832
+ if (key.startsWith(`${sessionId}:`)) {
833
+ this.sequences.delete(key);
834
+ }
835
+ }
836
+ }
837
+ }
838
+ // src/wal/wal-store.ts
839
+ import { Database } from "bun:sqlite";
840
+ import fs3 from "fs";
841
+ import path4 from "path";
842
+ var DEFAULT_QUERY_LIMIT = 100;
843
+
844
+ class WalStore {
845
+ db;
846
+ seqGenerator = new SeqGenerator;
847
+ stmtGetSession;
848
+ stmtInsertSession;
849
+ stmtUpdateSession;
850
+ stmtInsertEvent;
851
+ stmtQueryEvents;
852
+ stmtQueryUnackedEvents;
853
+ stmtAckEvents;
854
+ stmtIncrementRevision;
855
+ stmtGetMaxSeq;
856
+ stmtUpsertDiscoveredSession;
857
+ stmtGetDiscoveredSessions;
858
+ stmtGetDiscoveredSessionsByBackend;
859
+ stmtMarkDiscoveredSessionStale;
860
+ stmtDeleteStaleDiscoveredSessions;
861
+ stmtDeleteSessionEvents;
862
+ stmtDeleteSession;
863
+ stmtInsertArchivedSession;
864
+ stmtIsArchived;
865
+ stmtGetArchivedSessionIds;
866
+ constructor(dbPath) {
867
+ const dir = path4.dirname(dbPath);
868
+ if (!fs3.existsSync(dir)) {
869
+ fs3.mkdirSync(dir, { recursive: true });
870
+ }
871
+ this.db = new Database(dbPath);
872
+ runMigrations(this.db);
873
+ this.stmtGetSession = this.db.query(`
874
+ SELECT session_id, machine_id, backend_id, current_revision, cwd, title, created_at, updated_at
875
+ FROM sessions
876
+ WHERE session_id = $sessionId
877
+ `);
878
+ this.stmtInsertSession = this.db.query(`
879
+ INSERT INTO sessions (session_id, machine_id, backend_id, current_revision, cwd, title, created_at, updated_at)
880
+ VALUES ($sessionId, $machineId, $backendId, 1, $cwd, $title, $createdAt, $updatedAt)
881
+ `);
882
+ this.stmtUpdateSession = this.db.query(`
883
+ UPDATE sessions
884
+ SET cwd = COALESCE($cwd, cwd),
885
+ title = COALESCE($title, title),
886
+ updated_at = $updatedAt
887
+ WHERE session_id = $sessionId
888
+ `);
889
+ this.stmtInsertEvent = this.db.query(`
890
+ INSERT INTO session_events (session_id, revision, seq, kind, payload, created_at)
891
+ VALUES ($sessionId, $revision, $seq, $kind, $payload, $createdAt)
892
+ `);
893
+ this.stmtQueryEvents = this.db.query(`
894
+ SELECT id, session_id, revision, seq, kind, payload, created_at, acked_at
895
+ FROM session_events
896
+ WHERE session_id = $sessionId
897
+ AND revision = $revision
898
+ AND seq > $afterSeq
899
+ ORDER BY seq ASC
900
+ LIMIT $limit
901
+ `);
902
+ this.stmtQueryUnackedEvents = this.db.query(`
903
+ SELECT id, session_id, revision, seq, kind, payload, created_at, acked_at
904
+ FROM session_events
905
+ WHERE session_id = $sessionId
906
+ AND revision = $revision
907
+ AND acked_at IS NULL
908
+ ORDER BY seq ASC
909
+ `);
910
+ this.stmtAckEvents = this.db.query(`
911
+ UPDATE session_events
912
+ SET acked_at = $ackedAt
913
+ WHERE session_id = $sessionId
914
+ AND revision = $revision
915
+ AND seq <= $upToSeq
916
+ AND acked_at IS NULL
917
+ `);
918
+ this.stmtIncrementRevision = this.db.query(`
919
+ UPDATE sessions
920
+ SET current_revision = current_revision + 1,
921
+ updated_at = $updatedAt
922
+ WHERE session_id = $sessionId
923
+ RETURNING current_revision
924
+ `);
925
+ this.stmtGetMaxSeq = this.db.query(`
926
+ SELECT MAX(seq) as max_seq
927
+ FROM session_events
928
+ WHERE session_id = $sessionId AND revision = $revision
929
+ `);
930
+ this.stmtUpsertDiscoveredSession = this.db.query(`
931
+ INSERT INTO discovered_sessions (
932
+ session_id, backend_id, cwd, title, agent_updated_at,
933
+ discovered_at, last_verified_at, is_stale
934
+ ) VALUES (
935
+ $sessionId, $backendId, $cwd, $title, $agentUpdatedAt,
936
+ $discoveredAt, $lastVerifiedAt, 0
937
+ )
938
+ ON CONFLICT (session_id) DO UPDATE SET
939
+ backend_id = $backendId,
940
+ cwd = COALESCE($cwd, discovered_sessions.cwd),
941
+ title = COALESCE($title, discovered_sessions.title),
942
+ agent_updated_at = COALESCE($agentUpdatedAt, discovered_sessions.agent_updated_at),
943
+ last_verified_at = $lastVerifiedAt,
944
+ is_stale = 0
945
+ `);
946
+ this.stmtGetDiscoveredSessions = this.db.query(`
947
+ SELECT d.session_id, d.backend_id, d.cwd, d.title, d.agent_updated_at,
948
+ d.discovered_at, d.last_verified_at, d.is_stale
949
+ FROM discovered_sessions d
950
+ LEFT JOIN archived_session_ids a ON d.session_id = a.session_id
951
+ WHERE d.is_stale = 0 AND a.session_id IS NULL
952
+ ORDER BY d.discovered_at DESC
953
+ `);
954
+ this.stmtGetDiscoveredSessionsByBackend = this.db.query(`
955
+ SELECT d.session_id, d.backend_id, d.cwd, d.title, d.agent_updated_at,
956
+ d.discovered_at, d.last_verified_at, d.is_stale
957
+ FROM discovered_sessions d
958
+ LEFT JOIN archived_session_ids a ON d.session_id = a.session_id
959
+ WHERE d.backend_id = $backendId AND d.is_stale = 0 AND a.session_id IS NULL
960
+ ORDER BY d.discovered_at DESC
961
+ `);
962
+ this.stmtMarkDiscoveredSessionStale = this.db.query(`
963
+ UPDATE discovered_sessions
964
+ SET is_stale = 1
965
+ WHERE session_id = $sessionId
966
+ `);
967
+ this.stmtDeleteStaleDiscoveredSessions = this.db.query(`
968
+ DELETE FROM discovered_sessions
969
+ WHERE is_stale = 1 AND discovered_at < $olderThan
970
+ `);
971
+ this.stmtDeleteSessionEvents = this.db.query(`
972
+ DELETE FROM session_events WHERE session_id = $sessionId
973
+ `);
974
+ this.stmtDeleteSession = this.db.query(`
975
+ DELETE FROM sessions WHERE session_id = $sessionId
976
+ `);
977
+ this.stmtInsertArchivedSession = this.db.query(`
978
+ INSERT OR IGNORE INTO archived_session_ids (session_id, archived_at)
979
+ VALUES ($sessionId, $archivedAt)
980
+ `);
981
+ this.stmtIsArchived = this.db.query(`
982
+ SELECT 1 FROM archived_session_ids WHERE session_id = $sessionId
983
+ `);
984
+ this.stmtGetArchivedSessionIds = this.db.query(`
985
+ SELECT session_id FROM archived_session_ids
986
+ `);
987
+ }
988
+ ensureSession(params) {
989
+ const now = new Date().toISOString();
990
+ const existing = this.stmtGetSession.get({
991
+ $sessionId: params.sessionId
992
+ });
993
+ logger.debug({ sessionId: params.sessionId, exists: !!existing }, "wal_ensure_session");
994
+ if (existing) {
995
+ this.stmtUpdateSession.run({
996
+ $sessionId: params.sessionId,
997
+ $cwd: params.cwd ?? null,
998
+ $title: params.title ?? null,
999
+ $updatedAt: now
1000
+ });
1001
+ const maxSeq = this.getMaxSeq(params.sessionId, existing.current_revision);
1002
+ this.seqGenerator.initialize(params.sessionId, existing.current_revision, maxSeq);
1003
+ logger.debug({
1004
+ sessionId: params.sessionId,
1005
+ revision: existing.current_revision,
1006
+ maxSeq
1007
+ }, "wal_session_existing");
1008
+ return { revision: existing.current_revision };
1009
+ }
1010
+ this.stmtInsertSession.run({
1011
+ $sessionId: params.sessionId,
1012
+ $machineId: params.machineId,
1013
+ $backendId: params.backendId,
1014
+ $cwd: params.cwd ?? null,
1015
+ $title: params.title ?? null,
1016
+ $createdAt: now,
1017
+ $updatedAt: now
1018
+ });
1019
+ this.seqGenerator.initialize(params.sessionId, 1, 0);
1020
+ logger.info({ sessionId: params.sessionId, revision: 1 }, "wal_session_created");
1021
+ return { revision: 1 };
1022
+ }
1023
+ getSession(sessionId) {
1024
+ const row = this.stmtGetSession.get({
1025
+ $sessionId: sessionId
1026
+ });
1027
+ if (!row)
1028
+ return null;
1029
+ return this.rowToSession(row);
1030
+ }
1031
+ appendEvent(params) {
1032
+ const seq = this.seqGenerator.next(params.sessionId, params.revision);
1033
+ const now = new Date().toISOString();
1034
+ logger.debug({
1035
+ sessionId: params.sessionId,
1036
+ revision: params.revision,
1037
+ seq,
1038
+ kind: params.kind
1039
+ }, "wal_append_event");
1040
+ this.stmtInsertEvent.run({
1041
+ $sessionId: params.sessionId,
1042
+ $revision: params.revision,
1043
+ $seq: seq,
1044
+ $kind: params.kind,
1045
+ $payload: JSON.stringify(params.payload),
1046
+ $createdAt: now
1047
+ });
1048
+ const lastId = this.db.query("SELECT last_insert_rowid() as id").get();
1049
+ logger.info({
1050
+ sessionId: params.sessionId,
1051
+ revision: params.revision,
1052
+ seq,
1053
+ kind: params.kind,
1054
+ eventId: lastId.id
1055
+ }, "wal_event_appended");
1056
+ return {
1057
+ id: lastId.id,
1058
+ sessionId: params.sessionId,
1059
+ revision: params.revision,
1060
+ seq,
1061
+ kind: params.kind,
1062
+ payload: params.payload,
1063
+ createdAt: now
1064
+ };
1065
+ }
1066
+ queryEvents(params) {
1067
+ logger.debug({
1068
+ sessionId: params.sessionId,
1069
+ revision: params.revision,
1070
+ afterSeq: params.afterSeq ?? 0,
1071
+ limit: params.limit ?? DEFAULT_QUERY_LIMIT
1072
+ }, "wal_query_events");
1073
+ const rows = this.stmtQueryEvents.all({
1074
+ $sessionId: params.sessionId,
1075
+ $revision: params.revision,
1076
+ $afterSeq: params.afterSeq ?? 0,
1077
+ $limit: params.limit ?? DEFAULT_QUERY_LIMIT
1078
+ });
1079
+ logger.debug({
1080
+ sessionId: params.sessionId,
1081
+ revision: params.revision,
1082
+ count: rows.length,
1083
+ seqRange: rows.length > 0 ? `${rows[0].seq}-${rows[rows.length - 1].seq}` : "empty"
1084
+ }, "wal_query_events_result");
1085
+ return rows.map((row) => this.rowToEvent(row));
1086
+ }
1087
+ getUnackedEvents(sessionId, revision) {
1088
+ const rows = this.stmtQueryUnackedEvents.all({
1089
+ $sessionId: sessionId,
1090
+ $revision: revision
1091
+ });
1092
+ return rows.map((row) => this.rowToEvent(row));
1093
+ }
1094
+ ackEvents(sessionId, revision, upToSeq) {
1095
+ logger.debug({ sessionId, revision, upToSeq }, "wal_ack_events");
1096
+ const result = this.stmtAckEvents.run({
1097
+ $sessionId: sessionId,
1098
+ $revision: revision,
1099
+ $upToSeq: upToSeq,
1100
+ $ackedAt: new Date().toISOString()
1101
+ });
1102
+ logger.debug({ sessionId, revision, upToSeq, changes: result.changes }, "wal_ack_events_result");
1103
+ }
1104
+ incrementRevision(sessionId) {
1105
+ logger.info({ sessionId }, "wal_increment_revision");
1106
+ const result = this.stmtIncrementRevision.get({
1107
+ $sessionId: sessionId,
1108
+ $updatedAt: new Date().toISOString()
1109
+ });
1110
+ if (!result) {
1111
+ logger.error({ sessionId }, "wal_increment_revision_session_not_found");
1112
+ throw new Error(`Session not found: ${sessionId}`);
1113
+ }
1114
+ this.seqGenerator.reset(sessionId, result.current_revision);
1115
+ logger.info({ sessionId, newRevision: result.current_revision }, "wal_revision_incremented");
1116
+ return result.current_revision;
1117
+ }
1118
+ getCurrentSeq(sessionId, revision) {
1119
+ return this.seqGenerator.current(sessionId, revision);
1120
+ }
1121
+ saveDiscoveredSessions(sessions) {
1122
+ const now = new Date().toISOString();
1123
+ for (const session of sessions) {
1124
+ this.stmtUpsertDiscoveredSession.run({
1125
+ $sessionId: session.sessionId,
1126
+ $backendId: session.backendId,
1127
+ $cwd: session.cwd ?? null,
1128
+ $title: session.title ?? null,
1129
+ $agentUpdatedAt: session.agentUpdatedAt ?? null,
1130
+ $discoveredAt: session.discoveredAt,
1131
+ $lastVerifiedAt: now
1132
+ });
1133
+ }
1134
+ }
1135
+ getDiscoveredSessions(backendId) {
1136
+ let rows;
1137
+ if (backendId) {
1138
+ rows = this.stmtGetDiscoveredSessionsByBackend.all({
1139
+ $backendId: backendId
1140
+ });
1141
+ } else {
1142
+ rows = this.stmtGetDiscoveredSessions.all();
1143
+ }
1144
+ return rows.map((row) => this.rowToDiscoveredSession(row));
1145
+ }
1146
+ markDiscoveredSessionStale(sessionId) {
1147
+ this.stmtMarkDiscoveredSessionStale.run({
1148
+ $sessionId: sessionId
1149
+ });
1150
+ }
1151
+ deleteStaleDiscoveredSessions(olderThan) {
1152
+ const result = this.stmtDeleteStaleDiscoveredSessions.run({
1153
+ $olderThan: olderThan.toISOString()
1154
+ });
1155
+ return result.changes;
1156
+ }
1157
+ archiveSession(sessionId) {
1158
+ this.stmtDeleteSessionEvents.run({ $sessionId: sessionId });
1159
+ this.stmtDeleteSession.run({ $sessionId: sessionId });
1160
+ this.stmtInsertArchivedSession.run({
1161
+ $sessionId: sessionId,
1162
+ $archivedAt: new Date().toISOString()
1163
+ });
1164
+ }
1165
+ bulkArchiveSessions(sessionIds) {
1166
+ let count = 0;
1167
+ for (const sessionId of sessionIds) {
1168
+ this.archiveSession(sessionId);
1169
+ count++;
1170
+ }
1171
+ return count;
1172
+ }
1173
+ isArchived(sessionId) {
1174
+ const row = this.stmtIsArchived.get({ $sessionId: sessionId });
1175
+ return row !== null;
1176
+ }
1177
+ getArchivedSessionIds() {
1178
+ const rows = this.stmtGetArchivedSessionIds.all();
1179
+ return rows.map((r) => r.session_id);
1180
+ }
1181
+ close() {
1182
+ this.db.close();
1183
+ }
1184
+ getMaxSeq(sessionId, revision) {
1185
+ const result = this.stmtGetMaxSeq.get({
1186
+ $sessionId: sessionId,
1187
+ $revision: revision
1188
+ });
1189
+ return result?.max_seq ?? 0;
1190
+ }
1191
+ rowToSession(row) {
1192
+ return {
1193
+ sessionId: row.session_id,
1194
+ machineId: row.machine_id,
1195
+ backendId: row.backend_id,
1196
+ currentRevision: row.current_revision,
1197
+ cwd: row.cwd ?? undefined,
1198
+ title: row.title ?? undefined,
1199
+ createdAt: row.created_at,
1200
+ updatedAt: row.updated_at
1201
+ };
1202
+ }
1203
+ rowToEvent(row) {
1204
+ return {
1205
+ id: row.id,
1206
+ sessionId: row.session_id,
1207
+ revision: row.revision,
1208
+ seq: row.seq,
1209
+ kind: row.kind,
1210
+ payload: JSON.parse(row.payload),
1211
+ createdAt: row.created_at,
1212
+ ackedAt: row.acked_at ?? undefined
1213
+ };
1214
+ }
1215
+ rowToDiscoveredSession(row) {
1216
+ return {
1217
+ sessionId: row.session_id,
1218
+ backendId: row.backend_id,
1219
+ cwd: row.cwd ?? undefined,
1220
+ title: row.title ?? undefined,
1221
+ agentUpdatedAt: row.agent_updated_at ?? undefined,
1222
+ discoveredAt: row.discovered_at,
1223
+ lastVerifiedAt: row.last_verified_at ?? undefined,
1224
+ isStale: row.is_stale === 1
1225
+ };
1226
+ }
1227
+ }
410
1228
  // src/acp/acp-connection.ts
411
1229
  import { spawn } from "child_process";
412
1230
  import { randomUUID } from "crypto";
413
1231
  import { EventEmitter } from "events";
414
- import { Readable, Writable } from "stream";
1232
+ import { Readable, Writable as Writable2 } from "stream";
415
1233
  import {
416
1234
  ClientSideConnection,
417
1235
  ndJsonStream,
418
1236
  PROTOCOL_VERSION
419
1237
  } from "@agentclientprotocol/sdk";
1238
+ import {
1239
+ createErrorDetail,
1240
+ isProtocolMismatch
1241
+ } from "@mobvibe/shared";
420
1242
  var getErrorMessage = (error) => {
421
1243
  if (error instanceof Error) {
422
1244
  return error.message;
@@ -520,10 +1342,9 @@ var sliceOutputToLimit = (value, limit) => {
520
1342
  }
521
1343
  return sliced.subarray(start).toString("utf8");
522
1344
  };
523
- var AcpConnection = class {
524
- constructor(options) {
525
- this.options = options;
526
- }
1345
+
1346
+ class AcpConnection {
1347
+ options;
527
1348
  connection;
528
1349
  process;
529
1350
  closedPromise;
@@ -533,11 +1354,14 @@ var AcpConnection = class {
533
1354
  sessionId;
534
1355
  agentInfo;
535
1356
  agentCapabilities;
536
- sessionUpdateEmitter = new EventEmitter();
537
- statusEmitter = new EventEmitter();
538
- terminalOutputEmitter = new EventEmitter();
1357
+ sessionUpdateEmitter = new EventEmitter;
1358
+ statusEmitter = new EventEmitter;
1359
+ terminalOutputEmitter = new EventEmitter;
539
1360
  permissionHandler;
540
- terminals = /* @__PURE__ */ new Map();
1361
+ terminals = new Map;
1362
+ constructor(options) {
1363
+ this.options = options;
1364
+ }
541
1365
  getStatus() {
542
1366
  return {
543
1367
  backendId: this.options.backend.id,
@@ -554,52 +1378,32 @@ var AcpConnection = class {
554
1378
  getAgentInfo() {
555
1379
  return this.agentInfo;
556
1380
  }
557
- /**
558
- * Get the agent's session capabilities.
559
- */
560
1381
  getSessionCapabilities() {
561
1382
  return {
562
1383
  list: this.agentCapabilities?.sessionCapabilities?.list != null,
563
1384
  load: this.agentCapabilities?.loadSession === true
564
1385
  };
565
1386
  }
566
- /**
567
- * Check if the agent supports session/list.
568
- */
569
1387
  supportsSessionList() {
570
1388
  return this.agentCapabilities?.sessionCapabilities?.list != null;
571
1389
  }
572
- /**
573
- * Check if the agent supports session/load.
574
- */
575
1390
  supportsSessionLoad() {
576
1391
  return this.agentCapabilities?.loadSession === true;
577
1392
  }
578
- /**
579
- * List sessions from the agent (session/list).
580
- * @param params Optional filter parameters
581
- * @returns List of session info from the agent
582
- */
583
1393
  async listSessions(params) {
584
1394
  if (!this.supportsSessionList()) {
585
1395
  return { sessions: [] };
586
1396
  }
587
1397
  const connection = await this.ensureReady();
588
1398
  const response = await connection.unstable_listSessions({
589
- cursor: params?.cursor ?? void 0,
590
- cwd: params?.cwd ?? void 0
1399
+ cursor: params?.cursor ?? undefined,
1400
+ cwd: params?.cwd ?? undefined
591
1401
  });
592
1402
  return {
593
1403
  sessions: response.sessions,
594
- nextCursor: response.nextCursor ?? void 0
1404
+ nextCursor: response.nextCursor ?? undefined
595
1405
  };
596
1406
  }
597
- /**
598
- * Load a historical session with message history replay (session/load).
599
- * @param sessionId The session ID to load
600
- * @param cwd The working directory
601
- * @returns Load session response with modes/models state
602
- */
603
1407
  async loadSession(sessionId, cwd) {
604
1408
  if (!this.supportsSessionLoad()) {
605
1409
  throw new Error("Agent does not support session/load capability");
@@ -644,35 +1448,28 @@ var AcpConnection = class {
644
1448
  return;
645
1449
  }
646
1450
  this.updateStatus("connecting");
647
- this.agentInfo = void 0;
1451
+ this.agentInfo = undefined;
648
1452
  try {
649
1453
  const env = this.options.backend.envOverrides ? { ...process.env, ...this.options.backend.envOverrides } : process.env;
650
- const child = spawn(
651
- this.options.backend.command,
652
- this.options.backend.args,
653
- {
654
- stdio: ["pipe", "pipe", "pipe"],
655
- env
656
- }
657
- );
1454
+ const child = spawn(this.options.backend.command, this.options.backend.args, {
1455
+ stdio: ["pipe", "pipe", "pipe"],
1456
+ env
1457
+ });
658
1458
  this.process = child;
659
- this.sessionId = void 0;
1459
+ this.sessionId = undefined;
660
1460
  child.stderr.pipe(process.stderr);
661
- const input = Writable.toWeb(child.stdin);
1461
+ const input = Writable2.toWeb(child.stdin);
662
1462
  const output = Readable.toWeb(child.stdout);
663
1463
  const stream = ndJsonStream(input, output);
664
- const connection = new ClientSideConnection(
665
- () => buildClient({
666
- onSessionUpdate: (notification) => this.emitSessionUpdate(notification),
667
- onRequestPermission: (params) => this.handlePermissionRequest(params),
668
- onCreateTerminal: (params) => this.createTerminal(params),
669
- onTerminalOutput: (params) => this.getTerminalOutput(params),
670
- onWaitForTerminalExit: (params) => this.waitForTerminalExit(params),
671
- onKillTerminal: (params) => this.killTerminal(params),
672
- onReleaseTerminal: (params) => this.releaseTerminal(params)
673
- }),
674
- stream
675
- );
1464
+ const connection = new ClientSideConnection(() => buildClient({
1465
+ onSessionUpdate: (notification) => this.emitSessionUpdate(notification),
1466
+ onRequestPermission: (params) => this.handlePermissionRequest(params),
1467
+ onCreateTerminal: (params) => this.createTerminal(params),
1468
+ onTerminalOutput: (params) => this.getTerminalOutput(params),
1469
+ onWaitForTerminalExit: (params) => this.waitForTerminalExit(params),
1470
+ onKillTerminal: (params) => this.killTerminal(params),
1471
+ onReleaseTerminal: (params) => this.releaseTerminal(params)
1472
+ }), stream);
676
1473
  this.connection = connection;
677
1474
  child.once("error", (error) => {
678
1475
  if (this.state === "stopped") {
@@ -684,16 +1481,10 @@ var AcpConnection = class {
684
1481
  if (this.state === "stopped") {
685
1482
  return;
686
1483
  }
687
- this.updateStatus(
688
- "error",
689
- buildProcessExitError(formatExitMessage(code, signal))
690
- );
1484
+ this.updateStatus("error", buildProcessExitError(formatExitMessage(code, signal)));
691
1485
  });
692
1486
  this.closedPromise = connection.closed.catch((error) => {
693
- this.updateStatus(
694
- "error",
695
- buildConnectionClosedError(getErrorMessage(error))
696
- );
1487
+ this.updateStatus("error", buildConnectionClosedError(getErrorMessage(error)));
697
1488
  });
698
1489
  const initializeResponse = await connection.initialize({
699
1490
  protocolVersion: PROTOCOL_VERSION,
@@ -703,9 +1494,9 @@ var AcpConnection = class {
703
1494
  },
704
1495
  clientCapabilities: { terminal: true }
705
1496
  });
706
- this.agentInfo = initializeResponse.agentInfo ?? void 0;
707
- this.agentCapabilities = initializeResponse.agentCapabilities ?? void 0;
708
- this.connectedAt = /* @__PURE__ */ new Date();
1497
+ this.agentInfo = initializeResponse.agentInfo ?? undefined;
1498
+ this.agentCapabilities = initializeResponse.agentCapabilities ?? undefined;
1499
+ this.connectedAt = new Date;
709
1500
  this.updateStatus("ready");
710
1501
  } catch (error) {
711
1502
  this.updateStatus("error", buildConnectError(error));
@@ -715,10 +1506,7 @@ var AcpConnection = class {
715
1506
  }
716
1507
  async createSession(options) {
717
1508
  const connection = await this.ensureReady();
718
- const response = await this.createSessionInternal(
719
- connection,
720
- options?.cwd ?? process.cwd()
721
- );
1509
+ const response = await this.createSessionInternal(connection, options?.cwd ?? process.cwd());
722
1510
  this.sessionId = response.sessionId;
723
1511
  return response;
724
1512
  }
@@ -740,9 +1528,7 @@ var AcpConnection = class {
740
1528
  }
741
1529
  async createTerminal(params) {
742
1530
  const outputLimit = typeof params.outputByteLimit === "number" && params.outputByteLimit > 0 ? Math.floor(params.outputByteLimit) : 1024 * 1024;
743
- const resolvedEnv = params.env ? Object.fromEntries(
744
- params.env.map((envVar) => [envVar.name, envVar.value])
745
- ) : void 0;
1531
+ const resolvedEnv = params.env ? Object.fromEntries(params.env.map((envVar) => [envVar.name, envVar.value])) : undefined;
746
1532
  const terminalId = randomUUID();
747
1533
  const record = {
748
1534
  sessionId: params.sessionId,
@@ -757,7 +1543,7 @@ var AcpConnection = class {
757
1543
  };
758
1544
  this.terminals.set(terminalId, record);
759
1545
  const child = spawn(params.command, params.args ?? [], {
760
- cwd: params.cwd ?? void 0,
1546
+ cwd: params.cwd ?? undefined,
761
1547
  env: resolvedEnv ? { ...process.env, ...resolvedEnv } : process.env
762
1548
  });
763
1549
  child.once("error", (error) => {
@@ -777,8 +1563,7 @@ var AcpConnection = class {
777
1563
  });
778
1564
  });
779
1565
  record.process = child;
780
- let resolveExit = () => {
781
- };
1566
+ let resolveExit = () => {};
782
1567
  record.onExit = new Promise((resolve) => {
783
1568
  resolveExit = resolve;
784
1569
  });
@@ -789,20 +1574,14 @@ var AcpConnection = class {
789
1574
  return;
790
1575
  }
791
1576
  const combinedOutput = record.output.output + delta;
792
- record.output.truncated = isOutputOverLimit(
793
- combinedOutput,
794
- record.outputByteLimit
795
- );
796
- record.output.output = sliceOutputToLimit(
797
- combinedOutput,
798
- record.outputByteLimit
799
- );
1577
+ record.output.truncated = isOutputOverLimit(combinedOutput, record.outputByteLimit);
1578
+ record.output.output = sliceOutputToLimit(combinedOutput, record.outputByteLimit);
800
1579
  this.terminalOutputEmitter.emit("output", {
801
1580
  sessionId: record.sessionId,
802
1581
  terminalId,
803
1582
  delta,
804
1583
  truncated: record.output.truncated,
805
- output: record.output.truncated ? record.output.output : void 0,
1584
+ output: record.output.truncated ? record.output.output : undefined,
806
1585
  exitStatus: record.output.exitStatus
807
1586
  });
808
1587
  };
@@ -863,11 +1642,11 @@ var AcpConnection = class {
863
1642
  return;
864
1643
  }
865
1644
  this.updateStatus("stopped");
866
- this.sessionId = void 0;
867
- this.agentInfo = void 0;
1645
+ this.sessionId = undefined;
1646
+ this.agentInfo = undefined;
868
1647
  await this.stopProcess();
869
1648
  await this.closedPromise;
870
- this.connection = void 0;
1649
+ this.connection = undefined;
871
1650
  }
872
1651
  async ensureReady() {
873
1652
  if (this.state !== "ready" || !this.connection) {
@@ -899,46 +1678,42 @@ var AcpConnection = class {
899
1678
  if (!child) {
900
1679
  return;
901
1680
  }
902
- this.process = void 0;
1681
+ this.process = undefined;
903
1682
  if (child.exitCode === null && !child.killed) {
904
1683
  child.kill("SIGTERM");
905
1684
  }
906
1685
  }
907
- };
1686
+ }
908
1687
 
909
1688
  // src/acp/session-manager.ts
910
1689
  var buildPermissionKey = (sessionId, requestId) => `${sessionId}:${requestId}`;
911
1690
  var resolveModelState = (models) => {
912
1691
  if (!models) {
913
1692
  return {
914
- modelId: void 0,
915
- modelName: void 0,
916
- availableModels: void 0
1693
+ modelId: undefined,
1694
+ modelName: undefined,
1695
+ availableModels: undefined
917
1696
  };
918
1697
  }
919
1698
  const availableModels = models.availableModels?.map((model) => ({
920
1699
  id: model.modelId,
921
1700
  name: model.name,
922
- description: model.description ?? void 0
1701
+ description: model.description ?? undefined
923
1702
  }));
924
- const modelId = models.currentModelId ?? void 0;
925
- const modelName = availableModels?.find(
926
- (model) => model.id === modelId
927
- )?.name;
1703
+ const modelId = models.currentModelId ?? undefined;
1704
+ const modelName = availableModels?.find((model) => model.id === modelId)?.name;
928
1705
  return { modelId, modelName, availableModels };
929
1706
  };
930
1707
  var resolveModeState = (modes) => {
931
1708
  if (!modes) {
932
1709
  return {
933
- modeId: void 0,
934
- modeName: void 0,
935
- availableModes: void 0
1710
+ modeId: undefined,
1711
+ modeName: undefined,
1712
+ availableModes: undefined
936
1713
  };
937
1714
  }
938
- const modeId = modes.currentModeId ?? void 0;
939
- const modeName = modes.availableModes?.find(
940
- (mode) => mode.id === modeId
941
- )?.name;
1715
+ const modeId = modes.currentModeId ?? undefined;
1716
+ const modeName = modes.availableModes?.find((mode) => mode.id === modeId)?.name;
942
1717
  return {
943
1718
  modeId,
944
1719
  modeName,
@@ -948,63 +1723,87 @@ var resolveModeState = (modes) => {
948
1723
  }))
949
1724
  };
950
1725
  };
951
- var createCapabilityNotSupportedError = (message) => new AppError(
952
- createErrorDetail({
953
- code: "CAPABILITY_NOT_SUPPORTED",
954
- message,
955
- retryable: false,
956
- scope: "session"
957
- }),
958
- 409
959
- );
1726
+ var createCapabilityNotSupportedError = (message) => new AppError(createErrorDetail2({
1727
+ code: "CAPABILITY_NOT_SUPPORTED",
1728
+ message,
1729
+ retryable: false,
1730
+ scope: "session"
1731
+ }), 409);
960
1732
  var isValidWorkspacePath = async (cwd) => {
961
1733
  try {
962
- const stats = await fs3.stat(cwd);
1734
+ const stats = await fs4.stat(cwd);
963
1735
  return stats.isDirectory();
964
1736
  } catch {
965
1737
  return false;
966
1738
  }
967
1739
  };
968
- var SessionManager = class {
969
- constructor(config) {
1740
+
1741
+ class SessionManager {
1742
+ config;
1743
+ sessions = new Map;
1744
+ discoveredSessions = new Map;
1745
+ backendById;
1746
+ permissionRequests = new Map;
1747
+ permissionRequestEmitter = new EventEmitter2;
1748
+ permissionResultEmitter = new EventEmitter2;
1749
+ sessionsChangedEmitter = new EventEmitter2;
1750
+ sessionAttachedEmitter = new EventEmitter2;
1751
+ sessionDetachedEmitter = new EventEmitter2;
1752
+ sessionEventEmitter = new EventEmitter2;
1753
+ walStore;
1754
+ cryptoService;
1755
+ constructor(config, cryptoService) {
970
1756
  this.config = config;
971
- this.backendById = new Map(
972
- config.acpBackends.map((backend) => [backend.id, backend])
973
- );
974
- this.defaultBackendId = config.defaultAcpBackendId;
1757
+ this.backendById = new Map(config.acpBackends.map((backend) => [backend.id, backend]));
1758
+ this.walStore = new WalStore(config.walDbPath);
1759
+ this.cryptoService = cryptoService;
1760
+ }
1761
+ createConnection(backend) {
1762
+ return new AcpConnection({
1763
+ backend,
1764
+ client: {
1765
+ name: this.config.clientName,
1766
+ version: this.config.clientVersion
1767
+ }
1768
+ });
975
1769
  }
976
- sessions = /* @__PURE__ */ new Map();
977
- discoveredSessions = /* @__PURE__ */ new Map();
978
- backendById;
979
- defaultBackendId;
980
- permissionRequests = /* @__PURE__ */ new Map();
981
- sessionUpdateEmitter = new EventEmitter2();
982
- sessionErrorEmitter = new EventEmitter2();
983
- permissionRequestEmitter = new EventEmitter2();
984
- permissionResultEmitter = new EventEmitter2();
985
- terminalOutputEmitter = new EventEmitter2();
986
- sessionsChangedEmitter = new EventEmitter2();
987
- sessionAttachedEmitter = new EventEmitter2();
988
- sessionDetachedEmitter = new EventEmitter2();
989
1770
  listSessions() {
990
- return Array.from(this.sessions.values()).map(
991
- (record) => this.buildSummary(record)
992
- );
1771
+ return Array.from(this.sessions.values()).map((record) => this.buildSummary(record));
1772
+ }
1773
+ listAllSessions() {
1774
+ const active = this.listSessions();
1775
+ const merged = new Map(active.map((s) => [s.sessionId, s]));
1776
+ for (const s of this.walStore.getDiscoveredSessions()) {
1777
+ if (s.cwd === undefined)
1778
+ continue;
1779
+ const existing = merged.get(s.sessionId);
1780
+ if (existing) {
1781
+ const discoveredUpdatedAt = s.agentUpdatedAt ?? s.discoveredAt;
1782
+ if (discoveredUpdatedAt > existing.updatedAt) {
1783
+ merged.set(s.sessionId, {
1784
+ ...existing,
1785
+ updatedAt: discoveredUpdatedAt
1786
+ });
1787
+ }
1788
+ } else {
1789
+ merged.set(s.sessionId, {
1790
+ sessionId: s.sessionId,
1791
+ title: s.title ?? `Session ${s.sessionId.slice(0, 8)}`,
1792
+ backendId: s.backendId,
1793
+ backendLabel: s.backendId,
1794
+ cwd: s.cwd,
1795
+ createdAt: s.discoveredAt,
1796
+ updatedAt: s.agentUpdatedAt ?? s.discoveredAt
1797
+ });
1798
+ }
1799
+ }
1800
+ return Array.from(merged.values());
993
1801
  }
994
1802
  getSession(sessionId) {
995
1803
  return this.sessions.get(sessionId);
996
1804
  }
997
- onSessionUpdate(listener) {
998
- this.sessionUpdateEmitter.on("update", listener);
999
- return () => {
1000
- this.sessionUpdateEmitter.off("update", listener);
1001
- };
1002
- }
1003
- onSessionError(listener) {
1004
- this.sessionErrorEmitter.on("error", listener);
1005
- return () => {
1006
- this.sessionErrorEmitter.off("error", listener);
1007
- };
1805
+ getSessionRevision(sessionId) {
1806
+ return this.sessions.get(sessionId)?.revision;
1008
1807
  }
1009
1808
  onPermissionRequest(listener) {
1010
1809
  this.permissionRequestEmitter.on("request", listener);
@@ -1018,12 +1817,6 @@ var SessionManager = class {
1018
1817
  this.permissionResultEmitter.off("result", listener);
1019
1818
  };
1020
1819
  }
1021
- onTerminalOutput(listener) {
1022
- this.terminalOutputEmitter.on("output", listener);
1023
- return () => {
1024
- this.terminalOutputEmitter.off("output", listener);
1025
- };
1026
- }
1027
1820
  onSessionsChanged(listener) {
1028
1821
  this.sessionsChangedEmitter.on("changed", listener);
1029
1822
  return () => {
@@ -1042,6 +1835,117 @@ var SessionManager = class {
1042
1835
  this.sessionDetachedEmitter.off("detached", listener);
1043
1836
  };
1044
1837
  }
1838
+ onSessionEvent(listener) {
1839
+ this.sessionEventEmitter.on("event", listener);
1840
+ return () => {
1841
+ this.sessionEventEmitter.off("event", listener);
1842
+ };
1843
+ }
1844
+ getSessionEvents(params) {
1845
+ const record = this.sessions.get(params.sessionId);
1846
+ let actualRevision;
1847
+ if (record) {
1848
+ actualRevision = record.revision;
1849
+ } else {
1850
+ const walSession = this.walStore.getSession(params.sessionId);
1851
+ actualRevision = walSession?.currentRevision ?? params.revision;
1852
+ }
1853
+ if (!record && !this.walStore.getSession(params.sessionId)) {
1854
+ return {
1855
+ sessionId: params.sessionId,
1856
+ machineId: this.config.machineId,
1857
+ revision: actualRevision,
1858
+ events: [],
1859
+ hasMore: false
1860
+ };
1861
+ }
1862
+ if (params.revision !== actualRevision) {
1863
+ return {
1864
+ sessionId: params.sessionId,
1865
+ machineId: this.config.machineId,
1866
+ revision: actualRevision,
1867
+ events: [],
1868
+ hasMore: false
1869
+ };
1870
+ }
1871
+ const limit = params.limit ?? 100;
1872
+ const events = this.walStore.queryEvents({
1873
+ sessionId: params.sessionId,
1874
+ revision: actualRevision,
1875
+ afterSeq: params.afterSeq,
1876
+ limit: limit + 1
1877
+ });
1878
+ const hasMore = events.length > limit;
1879
+ const resultEvents = hasMore ? events.slice(0, limit) : events;
1880
+ return {
1881
+ sessionId: params.sessionId,
1882
+ machineId: this.config.machineId,
1883
+ revision: actualRevision,
1884
+ events: resultEvents.map((e) => ({
1885
+ sessionId: e.sessionId,
1886
+ machineId: this.config.machineId,
1887
+ revision: e.revision,
1888
+ seq: e.seq,
1889
+ kind: e.kind,
1890
+ createdAt: e.createdAt,
1891
+ payload: e.payload
1892
+ })),
1893
+ nextAfterSeq: resultEvents.length > 0 ? resultEvents[resultEvents.length - 1].seq : undefined,
1894
+ hasMore
1895
+ };
1896
+ }
1897
+ getUnackedEvents(sessionId, revision) {
1898
+ const events = this.walStore.getUnackedEvents(sessionId, revision);
1899
+ return events.map((e) => ({
1900
+ sessionId: e.sessionId,
1901
+ machineId: this.config.machineId,
1902
+ revision: e.revision,
1903
+ seq: e.seq,
1904
+ kind: e.kind,
1905
+ createdAt: e.createdAt,
1906
+ payload: e.payload
1907
+ }));
1908
+ }
1909
+ ackEvents(sessionId, revision, upToSeq) {
1910
+ this.walStore.ackEvents(sessionId, revision, upToSeq);
1911
+ }
1912
+ recordTurnEnd(sessionId, stopReason) {
1913
+ const record = this.sessions.get(sessionId);
1914
+ if (!record) {
1915
+ return;
1916
+ }
1917
+ record.updatedAt = new Date;
1918
+ this.writeAndEmitEvent(sessionId, record.revision, "turn_end", {
1919
+ stopReason
1920
+ });
1921
+ }
1922
+ writeAndEmitEvent(sessionId, revision, kind, payload) {
1923
+ logger.debug({ sessionId, revision, kind }, "session_write_and_emit_event_start");
1924
+ const walEvent = this.walStore.appendEvent({
1925
+ sessionId,
1926
+ revision,
1927
+ kind,
1928
+ payload
1929
+ });
1930
+ const event = {
1931
+ sessionId: walEvent.sessionId,
1932
+ machineId: this.config.machineId,
1933
+ revision: walEvent.revision,
1934
+ seq: walEvent.seq,
1935
+ kind: walEvent.kind,
1936
+ createdAt: walEvent.createdAt,
1937
+ payload: walEvent.payload
1938
+ };
1939
+ logger.info({
1940
+ sessionId: event.sessionId,
1941
+ revision: event.revision,
1942
+ seq: event.seq,
1943
+ kind: event.kind
1944
+ }, "session_event_emitting");
1945
+ this.sessionEventEmitter.emit("event", event);
1946
+ logger.debug({ sessionId: event.sessionId, seq: event.seq }, "session_event_emitted");
1947
+ return event;
1948
+ }
1045
1949
  emitSessionsChanged(payload) {
1046
1950
  this.sessionsChangedEmitter.emit("changed", payload);
1047
1951
  }
@@ -1053,13 +1957,14 @@ var SessionManager = class {
1053
1957
  if (record.isAttached && !force) {
1054
1958
  return;
1055
1959
  }
1056
- const attachedAt = /* @__PURE__ */ new Date();
1960
+ const attachedAt = new Date;
1057
1961
  record.isAttached = true;
1058
1962
  record.attachedAt = attachedAt;
1059
1963
  this.sessionAttachedEmitter.emit("attached", {
1060
1964
  sessionId,
1061
1965
  machineId: this.config.machineId,
1062
- attachedAt: attachedAt.toISOString()
1966
+ attachedAt: attachedAt.toISOString(),
1967
+ revision: record.revision
1063
1968
  });
1064
1969
  }
1065
1970
  emitSessionDetached(sessionId, reason) {
@@ -1074,7 +1979,7 @@ var SessionManager = class {
1074
1979
  this.sessionDetachedEmitter.emit("detached", {
1075
1980
  sessionId,
1076
1981
  machineId: this.config.machineId,
1077
- detachedAt: (/* @__PURE__ */ new Date()).toISOString(),
1982
+ detachedAt: new Date().toISOString(),
1078
1983
  reason
1079
1984
  });
1080
1985
  }
@@ -1083,69 +1988,72 @@ var SessionManager = class {
1083
1988
  }
1084
1989
  resolvePermissionRequest(sessionId, requestId, outcome) {
1085
1990
  const key = buildPermissionKey(sessionId, requestId);
1086
- const record = this.permissionRequests.get(key);
1087
- if (!record) {
1088
- throw new AppError(
1089
- createErrorDetail({
1090
- code: "REQUEST_VALIDATION_FAILED",
1091
- message: "Permission request not found",
1092
- retryable: false,
1093
- scope: "request"
1094
- }),
1095
- 404
1096
- );
1991
+ const permRecord = this.permissionRequests.get(key);
1992
+ if (!permRecord) {
1993
+ throw new AppError(createErrorDetail2({
1994
+ code: "REQUEST_VALIDATION_FAILED",
1995
+ message: "Permission request not found",
1996
+ retryable: false,
1997
+ scope: "request"
1998
+ }), 404);
1097
1999
  }
1098
2000
  const response = { outcome };
1099
- record.resolve(response);
2001
+ permRecord.resolve(response);
1100
2002
  this.permissionRequests.delete(key);
1101
2003
  const payload = {
1102
2004
  sessionId,
1103
2005
  requestId,
1104
2006
  outcome
1105
2007
  };
2008
+ const sessionRecord = this.sessions.get(sessionId);
2009
+ if (sessionRecord) {
2010
+ this.writeAndEmitEvent(sessionId, sessionRecord.revision, "permission_result", payload);
2011
+ }
1106
2012
  this.permissionResultEmitter.emit("result", payload);
1107
2013
  return payload;
1108
2014
  }
1109
2015
  resolveBackend(backendId) {
1110
- const normalized = backendId?.trim();
1111
- const resolvedId = normalized && normalized.length > 0 ? normalized : this.defaultBackendId;
1112
- const backend = this.backendById.get(resolvedId);
2016
+ const normalized = backendId.trim();
2017
+ if (!normalized) {
2018
+ throw new AppError(createErrorDetail2({
2019
+ code: "REQUEST_VALIDATION_FAILED",
2020
+ message: "backendId is required",
2021
+ retryable: false,
2022
+ scope: "request"
2023
+ }), 400);
2024
+ }
2025
+ const backend = this.backendById.get(normalized);
1113
2026
  if (!backend) {
1114
- throw new AppError(
1115
- createErrorDetail({
1116
- code: "REQUEST_VALIDATION_FAILED",
1117
- message: "Invalid backend ID",
1118
- retryable: false,
1119
- scope: "request"
1120
- }),
1121
- 400
1122
- );
2027
+ throw new AppError(createErrorDetail2({
2028
+ code: "REQUEST_VALIDATION_FAILED",
2029
+ message: "Invalid backend ID",
2030
+ retryable: false,
2031
+ scope: "request"
2032
+ }), 400);
1123
2033
  }
1124
2034
  return backend;
1125
2035
  }
1126
2036
  async createSession(options) {
1127
- const backend = this.resolveBackend(options?.backendId);
1128
- const connection = new AcpConnection({
1129
- backend,
1130
- client: {
1131
- name: this.config.clientName,
1132
- version: this.config.clientVersion
1133
- }
1134
- });
2037
+ const backend = this.resolveBackend(options.backendId);
2038
+ const connection = this.createConnection(backend);
1135
2039
  try {
1136
2040
  await connection.connect();
1137
2041
  const session = await connection.createSession({ cwd: options?.cwd });
1138
- connection.setPermissionHandler(
1139
- (params) => this.handlePermissionRequest(session.sessionId, params)
1140
- );
1141
- const now = /* @__PURE__ */ new Date();
2042
+ connection.setPermissionHandler((params) => this.handlePermissionRequest(session.sessionId, params));
2043
+ const now = new Date;
1142
2044
  const agentInfo = connection.getAgentInfo();
1143
- const { modelId, modelName, availableModels } = resolveModelState(
1144
- session.models
1145
- );
1146
- const { modeId, modeName, availableModes } = resolveModeState(
1147
- session.modes
1148
- );
2045
+ const { modelId, modelName, availableModels } = resolveModelState(session.models);
2046
+ const { modeId, modeName, availableModes } = resolveModeState(session.modes);
2047
+ const { revision } = this.walStore.ensureSession({
2048
+ sessionId: session.sessionId,
2049
+ machineId: this.config.machineId,
2050
+ backendId: backend.id,
2051
+ cwd: options?.cwd,
2052
+ title: options?.title ?? `Session ${this.sessions.size + 1}`
2053
+ });
2054
+ if (this.cryptoService) {
2055
+ this.cryptoService.initSessionDek(session.sessionId);
2056
+ }
1149
2057
  const record = {
1150
2058
  sessionId: session.sessionId,
1151
2059
  title: options?.title ?? `Session ${this.sessions.size + 1}`,
@@ -1162,24 +2070,25 @@ var SessionManager = class {
1162
2070
  modeName,
1163
2071
  availableModes,
1164
2072
  availableModels,
1165
- availableCommands: void 0
2073
+ availableCommands: undefined,
2074
+ revision
1166
2075
  };
1167
- record.unsubscribe = connection.onSessionUpdate(
1168
- (notification) => {
1169
- record.updatedAt = /* @__PURE__ */ new Date();
1170
- this.sessionUpdateEmitter.emit("update", notification);
1171
- this.applySessionUpdateToRecord(record, notification);
1172
- }
1173
- );
2076
+ record.unsubscribe = connection.onSessionUpdate((notification) => {
2077
+ logger.debug({
2078
+ sessionId: session.sessionId,
2079
+ updateType: notification.update.sessionUpdate
2080
+ }, "acp_session_update_received");
2081
+ record.updatedAt = new Date;
2082
+ this.writeSessionUpdateToWal(record, notification);
2083
+ this.applySessionUpdateToRecord(record, notification);
2084
+ });
1174
2085
  record.unsubscribeTerminal = connection.onTerminalOutput((event) => {
1175
- this.terminalOutputEmitter.emit("output", event);
2086
+ logger.debug({ sessionId: record.sessionId }, "acp_terminal_output_received");
2087
+ this.writeAndEmitEvent(record.sessionId, record.revision, "terminal_output", event);
1176
2088
  });
1177
2089
  connection.onStatusChange((status) => {
1178
2090
  if (status.error) {
1179
- this.sessionErrorEmitter.emit("error", {
1180
- sessionId: session.sessionId,
1181
- error: status.error
1182
- });
2091
+ this.writeAndEmitEvent(record.sessionId, record.revision, "session_error", { error: status.error });
1183
2092
  this.emitSessionDetached(session.sessionId, "agent_exit");
1184
2093
  }
1185
2094
  });
@@ -1208,7 +2117,6 @@ var SessionManager = class {
1208
2117
  requestId: record.requestId,
1209
2118
  options: record.params.options.map((option) => ({
1210
2119
  optionId: option.optionId,
1211
- // SDK uses 'name', our shared type uses 'label'
1212
2120
  label: option.name,
1213
2121
  description: option._meta?.description ?? null
1214
2122
  })),
@@ -1228,23 +2136,24 @@ var SessionManager = class {
1228
2136
  if (existing) {
1229
2137
  return existing.promise;
1230
2138
  }
1231
- let resolver = () => {
1232
- };
2139
+ let resolver = () => {};
1233
2140
  const promise = new Promise((resolve) => {
1234
2141
  resolver = resolve;
1235
2142
  });
1236
- const record = {
2143
+ const permRecord = {
1237
2144
  sessionId,
1238
2145
  requestId,
1239
2146
  params,
1240
2147
  promise,
1241
2148
  resolve: resolver
1242
2149
  };
1243
- this.permissionRequests.set(key, record);
1244
- this.permissionRequestEmitter.emit(
1245
- "request",
1246
- this.buildPermissionRequestPayload(record)
1247
- );
2150
+ this.permissionRequests.set(key, permRecord);
2151
+ const payload = this.buildPermissionRequestPayload(permRecord);
2152
+ const sessionRecord = this.sessions.get(sessionId);
2153
+ if (sessionRecord) {
2154
+ this.writeAndEmitEvent(sessionId, sessionRecord.revision, "permission_request", payload);
2155
+ }
2156
+ this.permissionRequestEmitter.emit("request", payload);
1248
2157
  return promise;
1249
2158
  }
1250
2159
  cancelPermissionRequests(sessionId) {
@@ -1267,18 +2176,15 @@ var SessionManager = class {
1267
2176
  updateTitle(sessionId, title) {
1268
2177
  const record = this.sessions.get(sessionId);
1269
2178
  if (!record) {
1270
- throw new AppError(
1271
- createErrorDetail({
1272
- code: "SESSION_NOT_FOUND",
1273
- message: "Session not found",
1274
- retryable: false,
1275
- scope: "session"
1276
- }),
1277
- 404
1278
- );
2179
+ throw new AppError(createErrorDetail2({
2180
+ code: "SESSION_NOT_FOUND",
2181
+ message: "Session not found",
2182
+ retryable: false,
2183
+ scope: "session"
2184
+ }), 404);
1279
2185
  }
1280
2186
  record.title = title;
1281
- record.updatedAt = /* @__PURE__ */ new Date();
2187
+ record.updatedAt = new Date;
1282
2188
  const summary = this.buildSummary(record);
1283
2189
  this.emitSessionsChanged({
1284
2190
  added: [],
@@ -1292,42 +2198,34 @@ var SessionManager = class {
1292
2198
  if (!record) {
1293
2199
  return;
1294
2200
  }
1295
- record.updatedAt = /* @__PURE__ */ new Date();
2201
+ record.updatedAt = new Date;
1296
2202
  }
1297
2203
  async setSessionMode(sessionId, modeId) {
1298
2204
  const record = this.sessions.get(sessionId);
1299
2205
  if (!record) {
1300
- throw new AppError(
1301
- createErrorDetail({
1302
- code: "SESSION_NOT_FOUND",
1303
- message: "Session not found",
1304
- retryable: false,
1305
- scope: "session"
1306
- }),
1307
- 404
1308
- );
2206
+ throw new AppError(createErrorDetail2({
2207
+ code: "SESSION_NOT_FOUND",
2208
+ message: "Session not found",
2209
+ retryable: false,
2210
+ scope: "session"
2211
+ }), 404);
1309
2212
  }
1310
2213
  if (!record.availableModes || record.availableModes.length === 0) {
1311
- throw createCapabilityNotSupportedError(
1312
- "Current agent does not support mode switching"
1313
- );
2214
+ throw createCapabilityNotSupportedError("Current agent does not support mode switching");
1314
2215
  }
1315
2216
  const selected = record.availableModes.find((mode) => mode.id === modeId);
1316
2217
  if (!selected) {
1317
- throw new AppError(
1318
- createErrorDetail({
1319
- code: "REQUEST_VALIDATION_FAILED",
1320
- message: "Invalid mode ID",
1321
- retryable: false,
1322
- scope: "request"
1323
- }),
1324
- 400
1325
- );
2218
+ throw new AppError(createErrorDetail2({
2219
+ code: "REQUEST_VALIDATION_FAILED",
2220
+ message: "Invalid mode ID",
2221
+ retryable: false,
2222
+ scope: "request"
2223
+ }), 400);
1326
2224
  }
1327
2225
  await record.connection.setSessionMode(sessionId, modeId);
1328
2226
  record.modeId = selected.id;
1329
2227
  record.modeName = selected.name;
1330
- record.updatedAt = /* @__PURE__ */ new Date();
2228
+ record.updatedAt = new Date;
1331
2229
  const summary = this.buildSummary(record);
1332
2230
  this.emitSessionsChanged({
1333
2231
  added: [],
@@ -1339,39 +2237,29 @@ var SessionManager = class {
1339
2237
  async setSessionModel(sessionId, modelId) {
1340
2238
  const record = this.sessions.get(sessionId);
1341
2239
  if (!record) {
1342
- throw new AppError(
1343
- createErrorDetail({
1344
- code: "SESSION_NOT_FOUND",
1345
- message: "Session not found",
1346
- retryable: false,
1347
- scope: "session"
1348
- }),
1349
- 404
1350
- );
2240
+ throw new AppError(createErrorDetail2({
2241
+ code: "SESSION_NOT_FOUND",
2242
+ message: "Session not found",
2243
+ retryable: false,
2244
+ scope: "session"
2245
+ }), 404);
1351
2246
  }
1352
2247
  if (!record.availableModels || record.availableModels.length === 0) {
1353
- throw createCapabilityNotSupportedError(
1354
- "Current agent does not support model switching"
1355
- );
2248
+ throw createCapabilityNotSupportedError("Current agent does not support model switching");
1356
2249
  }
1357
- const selected = record.availableModels.find(
1358
- (model) => model.id === modelId
1359
- );
2250
+ const selected = record.availableModels.find((model) => model.id === modelId);
1360
2251
  if (!selected) {
1361
- throw new AppError(
1362
- createErrorDetail({
1363
- code: "REQUEST_VALIDATION_FAILED",
1364
- message: "Invalid model ID",
1365
- retryable: false,
1366
- scope: "request"
1367
- }),
1368
- 400
1369
- );
2252
+ throw new AppError(createErrorDetail2({
2253
+ code: "REQUEST_VALIDATION_FAILED",
2254
+ message: "Invalid model ID",
2255
+ retryable: false,
2256
+ scope: "request"
2257
+ }), 400);
1370
2258
  }
1371
2259
  await record.connection.setSessionModel(sessionId, modelId);
1372
2260
  record.modelId = selected.id;
1373
2261
  record.modelName = selected.name;
1374
- record.updatedAt = /* @__PURE__ */ new Date();
2262
+ record.updatedAt = new Date;
1375
2263
  const summary = this.buildSummary(record);
1376
2264
  this.emitSessionsChanged({
1377
2265
  added: [],
@@ -1418,25 +2306,38 @@ var SessionManager = class {
1418
2306
  }
1419
2307
  async closeAll() {
1420
2308
  const sessionIds = Array.from(this.sessions.keys());
1421
- await Promise.all(
1422
- sessionIds.map((sessionId) => this.closeSession(sessionId))
1423
- );
1424
- }
1425
- /**
1426
- * Discover sessions persisted by the ACP agent.
1427
- * Creates a temporary connection to query sessions.
1428
- * @param options Optional parameters for discovery
1429
- * @returns List of discovered sessions and agent capabilities
1430
- */
2309
+ await Promise.all(sessionIds.map((sessionId) => this.closeSession(sessionId)));
2310
+ }
2311
+ async archiveSession(sessionId) {
2312
+ if (this.sessions.has(sessionId)) {
2313
+ await this.closeSession(sessionId);
2314
+ }
2315
+ this.walStore.archiveSession(sessionId);
2316
+ this.discoveredSessions.delete(sessionId);
2317
+ }
2318
+ async bulkArchiveSessions(sessionIds) {
2319
+ await Promise.allSettled(sessionIds.filter((id) => this.sessions.has(id)).map((id) => this.closeSession(id)));
2320
+ const archivedCount = this.walStore.bulkArchiveSessions(sessionIds);
2321
+ for (const id of sessionIds) {
2322
+ this.discoveredSessions.delete(id);
2323
+ }
2324
+ return { archivedCount };
2325
+ }
2326
+ async shutdown() {
2327
+ await this.closeAll();
2328
+ this.walStore.close();
2329
+ }
2330
+ getPersistedDiscoveredSessions(backendId) {
2331
+ return this.walStore.getDiscoveredSessions(backendId).filter((s) => !this.sessions.has(s.sessionId) && s.cwd !== undefined).map((s) => ({
2332
+ sessionId: s.sessionId,
2333
+ cwd: s.cwd,
2334
+ title: s.title,
2335
+ updatedAt: s.agentUpdatedAt
2336
+ }));
2337
+ }
1431
2338
  async discoverSessions(options) {
1432
- const backend = this.resolveBackend(options?.backendId);
1433
- const connection = new AcpConnection({
1434
- backend,
1435
- client: {
1436
- name: this.config.clientName,
1437
- version: this.config.clientVersion
1438
- }
1439
- });
2339
+ const backend = this.resolveBackend(options.backendId);
2340
+ const connection = this.createConnection(backend);
1440
2341
  try {
1441
2342
  await connection.connect();
1442
2343
  const capabilities = connection.getSessionCapabilities();
@@ -1448,98 +2349,123 @@ var SessionManager = class {
1448
2349
  cursor: options?.cursor
1449
2350
  });
1450
2351
  nextCursor = response.nextCursor;
1451
- const validity = await Promise.all(
1452
- response.sessions.map(async (session) => ({
1453
- session,
1454
- isValid: session.cwd ? await isValidWorkspacePath(session.cwd) : false
1455
- }))
1456
- );
2352
+ const archivedIds = new Set(this.walStore.getArchivedSessionIds());
2353
+ const validity = await Promise.all(response.sessions.map(async (session) => ({
2354
+ session,
2355
+ isValid: session.cwd ? await isValidWorkspacePath(session.cwd) : false
2356
+ })));
2357
+ const now = new Date().toISOString();
2358
+ const discoveredRecords = [];
1457
2359
  for (const { session, isValid } of validity) {
1458
2360
  if (!isValid) {
1459
2361
  this.discoveredSessions.delete(session.sessionId);
2362
+ this.walStore.markDiscoveredSessionStale(session.sessionId);
2363
+ continue;
2364
+ }
2365
+ if (archivedIds.has(session.sessionId)) {
1460
2366
  continue;
1461
2367
  }
1462
2368
  this.discoveredSessions.set(session.sessionId, {
1463
2369
  sessionId: session.sessionId,
1464
2370
  cwd: session.cwd,
1465
- title: session.title ?? void 0,
1466
- updatedAt: session.updatedAt ?? void 0
2371
+ title: session.title ?? undefined,
2372
+ updatedAt: session.updatedAt ?? undefined
1467
2373
  });
1468
2374
  sessions.push({
1469
2375
  sessionId: session.sessionId,
1470
2376
  cwd: session.cwd,
1471
- title: session.title ?? void 0,
1472
- updatedAt: session.updatedAt ?? void 0
2377
+ title: session.title ?? undefined,
2378
+ updatedAt: session.updatedAt ?? undefined
2379
+ });
2380
+ discoveredRecords.push({
2381
+ sessionId: session.sessionId,
2382
+ backendId: backend.id,
2383
+ cwd: session.cwd,
2384
+ title: session.title ?? undefined,
2385
+ agentUpdatedAt: session.updatedAt ?? undefined,
2386
+ discoveredAt: now,
2387
+ isStale: false
1473
2388
  });
1474
2389
  }
2390
+ if (discoveredRecords.length > 0) {
2391
+ this.walStore.saveDiscoveredSessions(discoveredRecords);
2392
+ }
1475
2393
  }
1476
- logger.info(
1477
- {
1478
- backendId: backend.id,
1479
- sessionCount: sessions.length,
1480
- capabilities
1481
- },
1482
- "sessions_discovered"
1483
- );
2394
+ logger.info({
2395
+ backendId: backend.id,
2396
+ sessionCount: sessions.length,
2397
+ capabilities
2398
+ }, "sessions_discovered");
1484
2399
  return { sessions, capabilities, nextCursor };
1485
2400
  } finally {
1486
2401
  await connection.disconnect();
1487
2402
  }
1488
2403
  }
1489
- /**
1490
- * Load a historical session from the ACP agent.
1491
- * This will replay the session's message history.
1492
- * @param sessionId The session ID to load
1493
- * @param cwd The working directory
1494
- * @param backendId Optional backend ID
1495
- * @returns The loaded session summary
1496
- */
1497
2404
  async loadSession(sessionId, cwd, backendId) {
2405
+ logger.info({ sessionId, cwd, backendId }, "load_session_start");
1498
2406
  const existing = this.sessions.get(sessionId);
1499
2407
  if (existing) {
2408
+ logger.debug({ sessionId }, "load_session_already_loaded");
1500
2409
  this.emitSessionAttached(sessionId, true);
1501
2410
  return this.buildSummary(existing);
1502
2411
  }
1503
2412
  const backend = this.resolveBackend(backendId);
1504
- const connection = new AcpConnection({
1505
- backend,
1506
- client: {
1507
- name: this.config.clientName,
1508
- version: this.config.clientVersion
1509
- }
1510
- });
2413
+ const connection = this.createConnection(backend);
1511
2414
  try {
1512
2415
  await connection.connect();
1513
2416
  if (!connection.supportsSessionLoad()) {
1514
- throw createCapabilityNotSupportedError(
1515
- "Agent does not support session loading"
1516
- );
2417
+ throw createCapabilityNotSupportedError("Agent does not support session loading");
2418
+ }
2419
+ const existingWalSession = this.walStore.getSession(sessionId);
2420
+ const hasExistingHistory = existingWalSession !== null && this.walStore.queryEvents({
2421
+ sessionId,
2422
+ revision: existingWalSession.currentRevision,
2423
+ afterSeq: 0,
2424
+ limit: 1
2425
+ }).length > 0;
2426
+ let revision;
2427
+ if (hasExistingHistory) {
2428
+ revision = this.walStore.incrementRevision(sessionId);
2429
+ logger.debug({ sessionId, revision }, "load_session_bump_revision");
2430
+ } else {
2431
+ const result = this.walStore.ensureSession({
2432
+ sessionId,
2433
+ machineId: this.config.machineId,
2434
+ backendId: backend.id,
2435
+ cwd
2436
+ });
2437
+ revision = result.revision;
1517
2438
  }
1518
2439
  const bufferedUpdates = [];
1519
2440
  let recordRef;
1520
- const unsubscribe = connection.onSessionUpdate(
1521
- (notification) => {
1522
- this.sessionUpdateEmitter.emit("update", notification);
1523
- if (recordRef) {
1524
- recordRef.updatedAt = /* @__PURE__ */ new Date();
1525
- this.applySessionUpdateToRecord(recordRef, notification);
1526
- } else {
1527
- bufferedUpdates.push(notification);
1528
- }
2441
+ const unsubscribe = connection.onSessionUpdate((notification) => {
2442
+ logger.debug({
2443
+ sessionId,
2444
+ updateType: notification.update.sessionUpdate,
2445
+ hasRecordRef: !!recordRef
2446
+ }, "load_session_update_received");
2447
+ if (recordRef) {
2448
+ this.writeSessionUpdateToWal(recordRef, notification);
2449
+ recordRef.updatedAt = new Date;
2450
+ this.applySessionUpdateToRecord(recordRef, notification);
2451
+ } else {
2452
+ bufferedUpdates.push(notification);
2453
+ logger.debug({ sessionId, bufferedCount: bufferedUpdates.length }, "load_session_buffered");
1529
2454
  }
1530
- );
2455
+ });
2456
+ logger.debug({ sessionId }, "load_session_calling_acp");
1531
2457
  const response = await connection.loadSession(sessionId, cwd);
1532
- connection.setPermissionHandler(
1533
- (params) => this.handlePermissionRequest(sessionId, params)
1534
- );
1535
- const now = /* @__PURE__ */ new Date();
2458
+ logger.debug({
2459
+ sessionId,
2460
+ bufferedCount: bufferedUpdates.length,
2461
+ hasModels: !!response.models,
2462
+ hasModes: !!response.modes
2463
+ }, "load_session_acp_returned");
2464
+ connection.setPermissionHandler((params) => this.handlePermissionRequest(sessionId, params));
2465
+ const now = new Date;
1536
2466
  const agentInfo = connection.getAgentInfo();
1537
- const { modelId, modelName, availableModels } = resolveModelState(
1538
- response.models
1539
- );
1540
- const { modeId, modeName, availableModes } = resolveModeState(
1541
- response.modes
1542
- );
2467
+ const { modelId, modelName, availableModels } = resolveModelState(response.models);
2468
+ const { modeId, modeName, availableModes } = resolveModeState(response.modes);
1543
2469
  const discovered = this.discoveredSessions.get(sessionId);
1544
2470
  const record = {
1545
2471
  sessionId,
@@ -1557,11 +2483,18 @@ var SessionManager = class {
1557
2483
  modeName,
1558
2484
  availableModes,
1559
2485
  availableModels,
1560
- availableCommands: void 0
2486
+ availableCommands: undefined,
2487
+ revision
1561
2488
  };
1562
2489
  recordRef = record;
1563
2490
  record.unsubscribe = unsubscribe;
2491
+ if (this.cryptoService) {
2492
+ this.cryptoService.initSessionDek(sessionId);
2493
+ }
2494
+ logger.debug({ sessionId, bufferedCount: bufferedUpdates.length }, "load_session_writing_buffered");
1564
2495
  for (const notification of bufferedUpdates) {
2496
+ logger.debug({ sessionId, updateType: notification.update.sessionUpdate }, "load_session_writing_buffered_event");
2497
+ this.writeSessionUpdateToWal(record, notification);
1565
2498
  this.applySessionUpdateToRecord(record, notification);
1566
2499
  }
1567
2500
  this.setupSessionSubscriptions(record, { skipSessionUpdates: true });
@@ -1573,34 +2506,26 @@ var SessionManager = class {
1573
2506
  removed: []
1574
2507
  });
1575
2508
  this.emitSessionAttached(sessionId);
1576
- logger.info({ sessionId, backendId: backend.id }, "session_loaded");
2509
+ logger.info({ sessionId, backendId: backend.id, revision: record.revision }, "load_session_complete");
1577
2510
  return summary;
1578
2511
  } catch (error) {
1579
2512
  await connection.disconnect();
1580
2513
  throw error;
1581
2514
  }
1582
2515
  }
1583
- /**
1584
- * Reload a historical session from the ACP agent.
1585
- * Replays session history even if the session is already loaded.
1586
- */
1587
2516
  async reloadSession(sessionId, cwd, backendId) {
1588
2517
  const existing = this.sessions.get(sessionId);
1589
2518
  if (!existing) {
1590
2519
  return this.loadSession(sessionId, cwd, backendId);
1591
2520
  }
1592
2521
  if (!existing.connection.supportsSessionLoad()) {
1593
- throw createCapabilityNotSupportedError(
1594
- "Agent does not support session loading"
1595
- );
2522
+ throw createCapabilityNotSupportedError("Agent does not support session loading");
1596
2523
  }
2524
+ const newRevision = this.walStore.incrementRevision(sessionId);
2525
+ existing.revision = newRevision;
1597
2526
  const response = await existing.connection.loadSession(sessionId, cwd);
1598
- const { modelId, modelName, availableModels } = resolveModelState(
1599
- response.models
1600
- );
1601
- const { modeId, modeName, availableModes } = resolveModeState(
1602
- response.modes
1603
- );
2527
+ const { modelId, modelName, availableModels } = resolveModelState(response.models);
2528
+ const { modeId, modeName, availableModes } = resolveModeState(response.modes);
1604
2529
  const agentInfo = existing.connection.getAgentInfo();
1605
2530
  existing.cwd = cwd;
1606
2531
  existing.agentName = agentInfo?.title ?? agentInfo?.name ?? existing.agentName;
@@ -1610,7 +2535,7 @@ var SessionManager = class {
1610
2535
  existing.modeId = modeId;
1611
2536
  existing.modeName = modeName;
1612
2537
  existing.availableModes = availableModes;
1613
- existing.updatedAt = /* @__PURE__ */ new Date();
2538
+ existing.updatedAt = new Date;
1614
2539
  const summary = this.buildSummary(existing);
1615
2540
  this.emitSessionsChanged({
1616
2541
  added: [],
@@ -1618,7 +2543,7 @@ var SessionManager = class {
1618
2543
  removed: []
1619
2544
  });
1620
2545
  this.emitSessionAttached(sessionId, true);
1621
- logger.info({ sessionId, backendId }, "session_reloaded");
2546
+ logger.info({ sessionId, backendId, revision: newRevision }, "session_reloaded");
1622
2547
  return summary;
1623
2548
  }
1624
2549
  applySessionUpdateToRecord(record, notification) {
@@ -1642,27 +2567,69 @@ var SessionManager = class {
1642
2567
  }
1643
2568
  }
1644
2569
  }
1645
- /**
1646
- * Set up event subscriptions for a session record.
1647
- */
2570
+ writeSessionUpdateToWal(record, notification) {
2571
+ const update = notification.update;
2572
+ let kind;
2573
+ logger.debug({
2574
+ sessionId: record.sessionId,
2575
+ revision: record.revision,
2576
+ updateType: update.sessionUpdate
2577
+ }, "write_session_update_to_wal_start");
2578
+ switch (update.sessionUpdate) {
2579
+ case "user_message_chunk":
2580
+ kind = "user_message";
2581
+ break;
2582
+ case "agent_message_chunk":
2583
+ kind = "agent_message_chunk";
2584
+ break;
2585
+ case "agent_thought_chunk":
2586
+ kind = "agent_thought_chunk";
2587
+ break;
2588
+ case "tool_call":
2589
+ kind = "tool_call";
2590
+ break;
2591
+ case "tool_call_update":
2592
+ kind = "tool_call_update";
2593
+ break;
2594
+ case "session_info_update":
2595
+ case "current_mode_update":
2596
+ case "available_commands_update":
2597
+ kind = "session_info_update";
2598
+ break;
2599
+ default:
2600
+ logger.warn({ sessionId: record.sessionId, updateType: update.sessionUpdate }, "unknown_session_update_type_skipped");
2601
+ return;
2602
+ }
2603
+ logger.info({
2604
+ sessionId: record.sessionId,
2605
+ revision: record.revision,
2606
+ updateType: update.sessionUpdate,
2607
+ kind
2608
+ }, "write_session_update_to_wal_mapped");
2609
+ this.writeAndEmitEvent(record.sessionId, record.revision, kind, notification);
2610
+ }
1648
2611
  setupSessionSubscriptions(record, options) {
1649
2612
  const { sessionId, connection } = record;
2613
+ logger.debug({ sessionId, skipSessionUpdates: options?.skipSessionUpdates }, "setup_session_subscriptions");
1650
2614
  if (!options?.skipSessionUpdates) {
1651
- record.unsubscribe = connection.onSessionUpdate(
1652
- (notification) => {
1653
- record.updatedAt = /* @__PURE__ */ new Date();
1654
- this.sessionUpdateEmitter.emit("update", notification);
1655
- this.applySessionUpdateToRecord(record, notification);
1656
- }
1657
- );
2615
+ record.unsubscribe = connection.onSessionUpdate((notification) => {
2616
+ logger.debug({
2617
+ sessionId,
2618
+ updateType: notification.update.sessionUpdate
2619
+ }, "acp_session_update_received_via_setup");
2620
+ record.updatedAt = new Date;
2621
+ this.writeSessionUpdateToWal(record, notification);
2622
+ this.applySessionUpdateToRecord(record, notification);
2623
+ });
1658
2624
  }
1659
2625
  record.unsubscribeTerminal = connection.onTerminalOutput((event) => {
1660
- this.terminalOutputEmitter.emit("output", event);
2626
+ logger.debug({ sessionId }, "acp_terminal_output_received_via_setup");
2627
+ this.writeAndEmitEvent(sessionId, record.revision, "terminal_output", event);
1661
2628
  });
1662
2629
  connection.onStatusChange((status) => {
2630
+ logger.debug({ sessionId, hasError: !!status.error }, "acp_status_change");
1663
2631
  if (status.error) {
1664
- this.sessionErrorEmitter.emit("error", {
1665
- sessionId,
2632
+ this.writeAndEmitEvent(sessionId, record.revision, "session_error", {
1666
2633
  error: status.error
1667
2634
  });
1668
2635
  this.emitSessionDetached(sessionId, "agent_exit");
@@ -1688,28 +2655,81 @@ var SessionManager = class {
1688
2655
  modeName: record.modeName,
1689
2656
  availableModes: record.availableModes,
1690
2657
  availableModels: record.availableModels,
1691
- availableCommands: record.availableCommands
2658
+ availableCommands: record.availableCommands,
2659
+ revision: record.revision,
2660
+ wrappedDek: this.cryptoService?.getWrappedDek(record.sessionId) ?? undefined
1692
2661
  };
1693
2662
  }
1694
- };
2663
+ }
2664
+
2665
+ // src/e2ee/crypto-service.ts
2666
+ import {
2667
+ deriveAuthKeyPair as deriveAuthKeyPair2,
2668
+ deriveContentKeyPair,
2669
+ encryptPayload,
2670
+ generateDEK,
2671
+ getSodium as getSodium2,
2672
+ wrapDEK
2673
+ } from "@mobvibe/shared";
2674
+
2675
+ class CliCryptoService {
2676
+ authKeyPair;
2677
+ contentKeyPair;
2678
+ sessionDeks = new Map;
2679
+ wrappedDekCache = new Map;
2680
+ constructor(masterSecret) {
2681
+ this.authKeyPair = deriveAuthKeyPair2(masterSecret);
2682
+ this.contentKeyPair = deriveContentKeyPair(masterSecret);
2683
+ }
2684
+ initSessionDek(sessionId) {
2685
+ const dek = generateDEK();
2686
+ const wrappedDek = wrapDEK(dek, this.contentKeyPair.publicKey);
2687
+ this.sessionDeks.set(sessionId, dek);
2688
+ this.wrappedDekCache.set(sessionId, wrappedDek);
2689
+ return { dek, wrappedDek };
2690
+ }
2691
+ setSessionDek(sessionId, dek) {
2692
+ this.sessionDeks.set(sessionId, dek);
2693
+ this.wrappedDekCache.set(sessionId, wrapDEK(dek, this.contentKeyPair.publicKey));
2694
+ }
2695
+ encryptEvent(event) {
2696
+ const dek = this.sessionDeks.get(event.sessionId);
2697
+ if (!dek) {
2698
+ return event;
2699
+ }
2700
+ return {
2701
+ ...event,
2702
+ payload: encryptPayload(event.payload, dek)
2703
+ };
2704
+ }
2705
+ getWrappedDek(sessionId) {
2706
+ return this.wrappedDekCache.get(sessionId) ?? null;
2707
+ }
2708
+ getAuthPublicKeyBase64() {
2709
+ const sodium = getSodium2();
2710
+ return sodium.to_base64(this.authKeyPair.publicKey, sodium.base64_variants.ORIGINAL);
2711
+ }
2712
+ }
1695
2713
 
1696
2714
  // src/daemon/socket-client.ts
1697
2715
  import { EventEmitter as EventEmitter3 } from "events";
1698
- import fs4 from "fs/promises";
2716
+ import fs5 from "fs/promises";
1699
2717
  import { homedir } from "os";
1700
- import path5 from "path";
2718
+ import path6 from "path";
2719
+ import { createSignedToken } from "@mobvibe/shared";
1701
2720
  import ignore from "ignore";
1702
2721
  import { io } from "socket.io-client";
1703
2722
 
1704
2723
  // src/lib/git-utils.ts
1705
- import { exec } from "child_process";
1706
- import path4 from "path";
2724
+ import { execFile } from "child_process";
2725
+ import { readFile } from "fs/promises";
2726
+ import path5 from "path";
1707
2727
  import { promisify } from "util";
1708
- var execAsync = promisify(exec);
2728
+ var execFileAsync = promisify(execFile);
1709
2729
  var MAX_BUFFER = 10 * 1024 * 1024;
1710
2730
  async function isGitRepo(cwd) {
1711
2731
  try {
1712
- await execAsync("git rev-parse --is-inside-work-tree", {
2732
+ await execFileAsync("git", ["rev-parse", "--is-inside-work-tree"], {
1713
2733
  cwd,
1714
2734
  maxBuffer: MAX_BUFFER
1715
2735
  });
@@ -1720,7 +2740,7 @@ async function isGitRepo(cwd) {
1720
2740
  }
1721
2741
  async function getGitBranch(cwd) {
1722
2742
  try {
1723
- const { stdout } = await execAsync("git branch --show-current", {
2743
+ const { stdout } = await execFileAsync("git", ["branch", "--show-current"], {
1724
2744
  cwd,
1725
2745
  maxBuffer: MAX_BUFFER
1726
2746
  });
@@ -1728,18 +2748,16 @@ async function getGitBranch(cwd) {
1728
2748
  if (branch) {
1729
2749
  return branch;
1730
2750
  }
1731
- const { stdout: hashOut } = await execAsync("git rev-parse --short HEAD", {
1732
- cwd,
1733
- maxBuffer: MAX_BUFFER
1734
- });
1735
- return hashOut.trim() || void 0;
2751
+ const { stdout: hashOut } = await execFileAsync("git", ["rev-parse", "--short", "HEAD"], { cwd, maxBuffer: MAX_BUFFER });
2752
+ return hashOut.trim() || undefined;
1736
2753
  } catch {
1737
- return void 0;
2754
+ return;
1738
2755
  }
1739
2756
  }
1740
2757
  function parseGitStatus(output) {
1741
2758
  const files = [];
1742
- const lines = output.split("\n").filter((line) => line.length > 0);
2759
+ const lines = output.split(`
2760
+ `).filter((line) => line.length > 0);
1743
2761
  for (const line of lines) {
1744
2762
  const indexStatus = line[0];
1745
2763
  const workTreeStatus = line[1];
@@ -1773,10 +2791,7 @@ function parseGitStatus(output) {
1773
2791
  }
1774
2792
  async function getGitStatus(cwd) {
1775
2793
  try {
1776
- const { stdout } = await execAsync("git status --porcelain=v1", {
1777
- cwd,
1778
- maxBuffer: MAX_BUFFER
1779
- });
2794
+ const { stdout } = await execFileAsync("git", ["status", "--porcelain=v1"], { cwd, maxBuffer: MAX_BUFFER });
1780
2795
  return parseGitStatus(stdout);
1781
2796
  } catch {
1782
2797
  return [];
@@ -1797,7 +2812,7 @@ function aggregateDirStatus(files) {
1797
2812
  for (const file of files) {
1798
2813
  const parts = file.path.split("/");
1799
2814
  let currentPath = "";
1800
- for (let i = 0; i < parts.length - 1; i++) {
2815
+ for (let i = 0;i < parts.length - 1; i++) {
1801
2816
  currentPath = currentPath ? `${currentPath}/${parts[i]}` : parts[i];
1802
2817
  const existing = dirStatus[currentPath];
1803
2818
  if (!existing || statusPriority[file.status] > statusPriority[existing]) {
@@ -1811,7 +2826,8 @@ function parseDiffOutput(diffOutput) {
1811
2826
  const addedLines = [];
1812
2827
  const modifiedLines = [];
1813
2828
  const deletedLines = [];
1814
- const lines = diffOutput.split("\n");
2829
+ const lines = diffOutput.split(`
2830
+ `);
1815
2831
  let currentLine = 0;
1816
2832
  let inHunk = false;
1817
2833
  let pendingDeletionLine = 0;
@@ -1847,22 +2863,15 @@ function parseDiffOutput(diffOutput) {
1847
2863
  }
1848
2864
  async function getFileDiff(cwd, filePath) {
1849
2865
  try {
1850
- const relativePath = path4.isAbsolute(filePath) ? path4.relative(cwd, filePath) : filePath;
1851
- const { stdout } = await execAsync(`git diff HEAD -- "${relativePath}"`, {
1852
- cwd,
1853
- maxBuffer: MAX_BUFFER
1854
- });
2866
+ const relativePath = path5.isAbsolute(filePath) ? path5.relative(cwd, filePath) : filePath;
2867
+ const { stdout } = await execFileAsync("git", ["diff", "HEAD", "--", relativePath], { cwd, maxBuffer: MAX_BUFFER });
1855
2868
  if (!stdout.trim()) {
1856
- const { stdout: statusOut } = await execAsync(
1857
- `git status --porcelain=v1 -- "${relativePath}"`,
1858
- { cwd, maxBuffer: MAX_BUFFER }
1859
- );
2869
+ const { stdout: statusOut } = await execFileAsync("git", ["status", "--porcelain=v1", "--", relativePath], { cwd, maxBuffer: MAX_BUFFER });
1860
2870
  if (statusOut.startsWith("?") || statusOut.startsWith("A")) {
1861
- const { stdout: wcOut } = await execAsync(`wc -l < "${relativePath}"`, {
1862
- cwd,
1863
- maxBuffer: MAX_BUFFER
1864
- });
1865
- const lineCount = Number.parseInt(wcOut.trim(), 10) || 0;
2871
+ const absPath = path5.isAbsolute(filePath) ? filePath : path5.resolve(cwd, relativePath);
2872
+ const content = await readFile(absPath, "utf-8");
2873
+ const lineCount = content.split(`
2874
+ `).filter((l) => l.length > 0).length;
1866
2875
  return {
1867
2876
  addedLines: Array.from({ length: lineCount }, (_, i) => i + 1),
1868
2877
  modifiedLines: [],
@@ -1879,7 +2888,7 @@ async function getFileDiff(cwd, filePath) {
1879
2888
 
1880
2889
  // src/daemon/socket-client.ts
1881
2890
  var SESSION_ROOT_NAME = "Working Directory";
1882
- var MAX_RESOURCE_FILES = 2e3;
2891
+ var MAX_RESOURCE_FILES = 2000;
1883
2892
  var DEFAULT_IGNORES = [
1884
2893
  "node_modules",
1885
2894
  ".git",
@@ -1897,15 +2906,14 @@ var DEFAULT_IGNORES = [
1897
2906
  var loadGitignore = async (rootPath) => {
1898
2907
  const ig = ignore().add(DEFAULT_IGNORES);
1899
2908
  try {
1900
- const gitignorePath = path5.join(rootPath, ".gitignore");
1901
- const content = await fs4.readFile(gitignorePath, "utf8");
2909
+ const gitignorePath = path6.join(rootPath, ".gitignore");
2910
+ const content = await fs5.readFile(gitignorePath, "utf8");
1902
2911
  ig.add(content);
1903
- } catch {
1904
- }
2912
+ } catch {}
1905
2913
  return ig;
1906
2914
  };
1907
2915
  var resolveImageMimeType = (filePath) => {
1908
- const extension = path5.extname(filePath).toLowerCase();
2916
+ const extension = path6.extname(filePath).toLowerCase();
1909
2917
  switch (extension) {
1910
2918
  case ".apng":
1911
2919
  return "image/apng";
@@ -1924,31 +2932,28 @@ var resolveImageMimeType = (filePath) => {
1924
2932
  case ".webp":
1925
2933
  return "image/webp";
1926
2934
  default:
1927
- return void 0;
2935
+ return;
1928
2936
  }
1929
2937
  };
1930
2938
  var readDirectoryEntries = async (dirPath) => {
1931
- const entries = await fs4.readdir(dirPath, { withFileTypes: true });
1932
- const resolvedEntries = await Promise.all(
1933
- entries.map(async (entry) => {
1934
- const entryPath = path5.join(dirPath, entry.name);
1935
- let isDirectory = entry.isDirectory();
1936
- if (!isDirectory && entry.isSymbolicLink()) {
1937
- try {
1938
- const stats = await fs4.stat(entryPath);
1939
- isDirectory = stats.isDirectory();
1940
- } catch {
1941
- }
1942
- }
1943
- const entryType = isDirectory ? "directory" : "file";
1944
- return {
1945
- name: entry.name,
1946
- path: entryPath,
1947
- type: entryType,
1948
- hidden: entry.name.startsWith(".")
1949
- };
1950
- })
1951
- );
2939
+ const entries = await fs5.readdir(dirPath, { withFileTypes: true });
2940
+ const resolvedEntries = await Promise.all(entries.map(async (entry) => {
2941
+ const entryPath = path6.join(dirPath, entry.name);
2942
+ let isDirectory = entry.isDirectory();
2943
+ if (!isDirectory && entry.isSymbolicLink()) {
2944
+ try {
2945
+ const stats = await fs5.stat(entryPath);
2946
+ isDirectory = stats.isDirectory();
2947
+ } catch {}
2948
+ }
2949
+ const entryType = isDirectory ? "directory" : "file";
2950
+ return {
2951
+ name: entry.name,
2952
+ path: entryPath,
2953
+ type: entryType,
2954
+ hidden: entry.name.startsWith(".")
2955
+ };
2956
+ }));
1952
2957
  return resolvedEntries.sort((left, right) => {
1953
2958
  if (left.type !== right.type) {
1954
2959
  return left.type === "directory" ? -1 : 1;
@@ -1957,6 +2962,16 @@ var readDirectoryEntries = async (dirPath) => {
1957
2962
  });
1958
2963
  };
1959
2964
  var filterVisibleEntries = (entries) => entries.filter((entry) => !entry.hidden);
2965
+ var resolveWithinCwd = (cwd, requestPath) => {
2966
+ if (path6.isAbsolute(requestPath)) {
2967
+ throw new Error("Absolute paths are not allowed");
2968
+ }
2969
+ const resolved = path6.resolve(cwd, requestPath);
2970
+ if (resolved !== cwd && !resolved.startsWith(`${cwd}/`)) {
2971
+ throw new Error("Path escapes working directory");
2972
+ }
2973
+ return resolved;
2974
+ };
1960
2975
  var buildHostFsRoots = async () => {
1961
2976
  const homePath = homedir();
1962
2977
  return {
@@ -1964,41 +2979,46 @@ var buildHostFsRoots = async () => {
1964
2979
  roots: [{ name: "Home", path: homePath }]
1965
2980
  };
1966
2981
  };
1967
- var SocketClient = class extends EventEmitter3 {
2982
+
2983
+ class SocketClient extends EventEmitter3 {
2984
+ options;
2985
+ socket;
2986
+ connected = false;
2987
+ reconnectAttempts = 0;
2988
+ heartbeatInterval;
1968
2989
  constructor(options) {
1969
2990
  super();
1970
2991
  this.options = options;
2992
+ const { cryptoService } = options;
1971
2993
  this.socket = io(`${options.config.gatewayUrl}/cli`, {
1972
2994
  path: "/socket.io",
1973
2995
  reconnection: true,
1974
2996
  reconnectionAttempts: Number.POSITIVE_INFINITY,
1975
- reconnectionDelay: 1e3,
1976
- reconnectionDelayMax: 3e4,
2997
+ reconnectionDelay: 1000,
2998
+ reconnectionDelayMax: 30000,
1977
2999
  transports: ["websocket"],
1978
3000
  autoConnect: false,
1979
- extraHeaders: {
1980
- "x-api-key": options.apiKey
1981
- }
3001
+ auth: (cb) => cb(createSignedToken(cryptoService.authKeyPair))
1982
3002
  });
1983
3003
  this.setupEventHandlers();
1984
3004
  this.setupRpcHandlers();
1985
3005
  this.setupSessionManagerListeners();
1986
3006
  }
1987
- socket;
1988
- connected = false;
1989
- reconnectAttempts = 0;
1990
- heartbeatInterval;
1991
3007
  setupEventHandlers() {
1992
3008
  this.socket.on("connect", () => {
1993
- logger.info(
1994
- { gatewayUrl: this.options.config.gatewayUrl },
1995
- "gateway_connected"
1996
- );
3009
+ const wasReconnect = this.reconnectAttempts > 0;
3010
+ logger.info({
3011
+ gatewayUrl: this.options.config.gatewayUrl,
3012
+ wasReconnect
3013
+ }, "gateway_connected");
1997
3014
  this.connected = true;
1998
3015
  this.reconnectAttempts = 0;
1999
3016
  logger.info("gateway_register_start");
2000
3017
  this.register();
2001
3018
  this.startHeartbeat();
3019
+ if (wasReconnect) {
3020
+ this.replayUnackedEvents();
3021
+ }
2002
3022
  this.emit("connected");
2003
3023
  });
2004
3024
  this.socket.on("disconnect", (reason) => {
@@ -2010,41 +3030,55 @@ var SocketClient = class extends EventEmitter3 {
2010
3030
  this.socket.on("connect_error", (error) => {
2011
3031
  this.reconnectAttempts++;
2012
3032
  if (this.reconnectAttempts <= 3 || this.reconnectAttempts % 10 === 0) {
2013
- logger.error(
2014
- { attempt: this.reconnectAttempts, err: error },
2015
- "gateway_connect_error"
2016
- );
3033
+ logger.error({ attempt: this.reconnectAttempts, err: error }, "gateway_connect_error");
2017
3034
  }
2018
3035
  });
2019
3036
  this.socket.on("cli:registered", async (info) => {
2020
3037
  logger.info({ machineId: info.machineId }, "gateway_registered");
2021
- try {
2022
- let cursor;
2023
- let page = 0;
2024
- do {
2025
- const { sessions, capabilities, nextCursor } = await this.options.sessionManager.discoverSessions({ cursor });
2026
- cursor = nextCursor;
2027
- if (sessions.length > 0) {
2028
- this.socket.emit("sessions:discovered", {
2029
- sessions,
2030
- capabilities,
2031
- nextCursor
3038
+ for (const backend of this.options.config.acpBackends) {
3039
+ try {
3040
+ let cursor;
3041
+ let page = 0;
3042
+ do {
3043
+ const { sessions, capabilities, nextCursor } = await this.options.sessionManager.discoverSessions({
3044
+ backendId: backend.id,
3045
+ cursor
2032
3046
  });
2033
- logger.info(
2034
- { count: sessions.length, capabilities, page },
2035
- "historical_sessions_discovered"
2036
- );
2037
- }
2038
- page += 1;
2039
- } while (cursor);
2040
- } catch (error) {
2041
- logger.warn({ err: error }, "session_discovery_failed");
3047
+ cursor = nextCursor;
3048
+ if (sessions.length > 0) {
3049
+ this.socket.emit("sessions:discovered", {
3050
+ sessions,
3051
+ capabilities,
3052
+ nextCursor,
3053
+ backendId: backend.id,
3054
+ backendLabel: backend.label
3055
+ });
3056
+ logger.info({
3057
+ count: sessions.length,
3058
+ capabilities,
3059
+ page,
3060
+ backendId: backend.id
3061
+ }, "historical_sessions_discovered");
3062
+ }
3063
+ page += 1;
3064
+ } while (cursor);
3065
+ } catch (error) {
3066
+ logger.warn({ err: error, backendId: backend.id }, "session_discovery_failed");
3067
+ }
2042
3068
  }
2043
3069
  });
2044
3070
  this.socket.on("cli:error", (error) => {
2045
3071
  logger.error({ err: error }, "gateway_auth_error");
2046
3072
  this.emit("auth_error", error);
2047
3073
  });
3074
+ this.socket.on("events:ack", (payload) => {
3075
+ logger.debug({
3076
+ sessionId: payload.sessionId,
3077
+ revision: payload.revision,
3078
+ upToSeq: payload.upToSeq
3079
+ }, "events_acked");
3080
+ this.options.sessionManager.ackEvents(payload.sessionId, payload.revision, payload.upToSeq);
3081
+ });
2048
3082
  }
2049
3083
  setupRpcHandlers() {
2050
3084
  const { sessionManager } = this.options;
@@ -2054,106 +3088,73 @@ var SocketClient = class extends EventEmitter3 {
2054
3088
  const session = await sessionManager.createSession(request.params);
2055
3089
  this.sendRpcResponse(request.requestId, session);
2056
3090
  } catch (error) {
2057
- logger.error(
2058
- { err: error, requestId: request.requestId },
2059
- "rpc_session_create_error"
2060
- );
3091
+ logger.error({ err: error, requestId: request.requestId }, "rpc_session_create_error");
2061
3092
  this.sendRpcError(request.requestId, error);
2062
3093
  }
2063
3094
  });
2064
3095
  this.socket.on("rpc:session:close", async (request) => {
2065
3096
  try {
2066
- logger.info(
2067
- { requestId: request.requestId, sessionId: request.params.sessionId },
2068
- "rpc_session_close"
2069
- );
3097
+ logger.info({ requestId: request.requestId, sessionId: request.params.sessionId }, "rpc_session_close");
2070
3098
  await sessionManager.closeSession(request.params.sessionId);
2071
3099
  this.sendRpcResponse(request.requestId, { ok: true });
2072
3100
  } catch (error) {
2073
- logger.error(
2074
- {
2075
- err: error,
2076
- requestId: request.requestId,
2077
- sessionId: request.params.sessionId
2078
- },
2079
- "rpc_session_close_error"
2080
- );
3101
+ logger.error({
3102
+ err: error,
3103
+ requestId: request.requestId,
3104
+ sessionId: request.params.sessionId
3105
+ }, "rpc_session_close_error");
2081
3106
  this.sendRpcError(request.requestId, error);
2082
3107
  }
2083
3108
  });
2084
3109
  this.socket.on("rpc:session:cancel", async (request) => {
2085
3110
  try {
2086
- logger.info(
2087
- { requestId: request.requestId, sessionId: request.params.sessionId },
2088
- "rpc_session_cancel"
2089
- );
3111
+ logger.info({ requestId: request.requestId, sessionId: request.params.sessionId }, "rpc_session_cancel");
2090
3112
  await sessionManager.cancelSession(request.params.sessionId);
2091
3113
  this.sendRpcResponse(request.requestId, { ok: true });
2092
3114
  } catch (error) {
2093
- logger.error(
2094
- {
2095
- err: error,
2096
- requestId: request.requestId,
2097
- sessionId: request.params.sessionId
2098
- },
2099
- "rpc_session_cancel_error"
2100
- );
3115
+ logger.error({
3116
+ err: error,
3117
+ requestId: request.requestId,
3118
+ sessionId: request.params.sessionId
3119
+ }, "rpc_session_cancel_error");
2101
3120
  this.sendRpcError(request.requestId, error);
2102
3121
  }
2103
3122
  });
2104
3123
  this.socket.on("rpc:session:mode", async (request) => {
2105
3124
  try {
2106
- logger.info(
2107
- {
2108
- requestId: request.requestId,
2109
- sessionId: request.params.sessionId,
2110
- modeId: request.params.modeId
2111
- },
2112
- "rpc_session_mode"
2113
- );
2114
- const session = await sessionManager.setSessionMode(
2115
- request.params.sessionId,
2116
- request.params.modeId
2117
- );
3125
+ logger.info({
3126
+ requestId: request.requestId,
3127
+ sessionId: request.params.sessionId,
3128
+ modeId: request.params.modeId
3129
+ }, "rpc_session_mode");
3130
+ const session = await sessionManager.setSessionMode(request.params.sessionId, request.params.modeId);
2118
3131
  this.sendRpcResponse(request.requestId, session);
2119
3132
  } catch (error) {
2120
- logger.error(
2121
- {
2122
- err: error,
2123
- requestId: request.requestId,
2124
- sessionId: request.params.sessionId,
2125
- modeId: request.params.modeId
2126
- },
2127
- "rpc_session_mode_error"
2128
- );
3133
+ logger.error({
3134
+ err: error,
3135
+ requestId: request.requestId,
3136
+ sessionId: request.params.sessionId,
3137
+ modeId: request.params.modeId
3138
+ }, "rpc_session_mode_error");
2129
3139
  this.sendRpcError(request.requestId, error);
2130
3140
  }
2131
3141
  });
2132
3142
  this.socket.on("rpc:session:model", async (request) => {
2133
3143
  try {
2134
- logger.info(
2135
- {
2136
- requestId: request.requestId,
2137
- sessionId: request.params.sessionId,
2138
- modelId: request.params.modelId
2139
- },
2140
- "rpc_session_model"
2141
- );
2142
- const session = await sessionManager.setSessionModel(
2143
- request.params.sessionId,
2144
- request.params.modelId
2145
- );
3144
+ logger.info({
3145
+ requestId: request.requestId,
3146
+ sessionId: request.params.sessionId,
3147
+ modelId: request.params.modelId
3148
+ }, "rpc_session_model");
3149
+ const session = await sessionManager.setSessionModel(request.params.sessionId, request.params.modelId);
2146
3150
  this.sendRpcResponse(request.requestId, session);
2147
3151
  } catch (error) {
2148
- logger.error(
2149
- {
2150
- err: error,
2151
- requestId: request.requestId,
2152
- sessionId: request.params.sessionId,
2153
- modelId: request.params.modelId
2154
- },
2155
- "rpc_session_model_error"
2156
- );
3152
+ logger.error({
3153
+ err: error,
3154
+ requestId: request.requestId,
3155
+ sessionId: request.params.sessionId,
3156
+ modelId: request.params.modelId
3157
+ }, "rpc_session_model_error");
2157
3158
  this.sendRpcError(request.requestId, error);
2158
3159
  }
2159
3160
  });
@@ -2161,97 +3162,71 @@ var SocketClient = class extends EventEmitter3 {
2161
3162
  const requestStart = process.hrtime.bigint();
2162
3163
  try {
2163
3164
  const { sessionId, prompt } = request.params;
2164
- logger.info(
2165
- {
2166
- requestId: request.requestId,
2167
- sessionId,
2168
- promptBlocks: prompt.length
2169
- },
2170
- "rpc_message_send"
2171
- );
2172
- logger.debug(
2173
- {
2174
- requestId: request.requestId,
2175
- sessionId,
2176
- promptBlocks: prompt.length
2177
- },
2178
- "rpc_message_send_start"
2179
- );
3165
+ logger.info({
3166
+ requestId: request.requestId,
3167
+ sessionId,
3168
+ promptBlocks: prompt.length
3169
+ }, "rpc_message_send");
3170
+ logger.debug({
3171
+ requestId: request.requestId,
3172
+ sessionId,
3173
+ promptBlocks: prompt.length
3174
+ }, "rpc_message_send_start");
2180
3175
  const record = sessionManager.getSession(sessionId);
2181
3176
  if (!record) {
2182
3177
  throw new Error("Session not found");
2183
3178
  }
2184
3179
  sessionManager.touchSession(sessionId);
2185
- const result = await record.connection.prompt(
2186
- sessionId,
2187
- prompt
2188
- );
3180
+ const result = await record.connection.prompt(sessionId, prompt);
2189
3181
  sessionManager.touchSession(sessionId);
3182
+ sessionManager.recordTurnEnd(sessionId, result.stopReason);
2190
3183
  this.sendRpcResponse(request.requestId, {
2191
3184
  stopReason: result.stopReason
2192
3185
  });
2193
3186
  const durationMs = Number(process.hrtime.bigint() - requestStart) / 1e6;
2194
- logger.info(
2195
- {
2196
- requestId: request.requestId,
2197
- sessionId,
2198
- stopReason: result.stopReason,
2199
- durationMs
2200
- },
2201
- "rpc_message_send_complete"
2202
- );
2203
- logger.debug(
2204
- {
2205
- requestId: request.requestId,
2206
- sessionId,
2207
- durationMs
2208
- },
2209
- "rpc_message_send_finish"
2210
- );
3187
+ logger.info({
3188
+ requestId: request.requestId,
3189
+ sessionId,
3190
+ stopReason: result.stopReason,
3191
+ durationMs
3192
+ }, "rpc_message_send_complete");
3193
+ logger.debug({
3194
+ requestId: request.requestId,
3195
+ sessionId,
3196
+ durationMs
3197
+ }, "rpc_message_send_finish");
2211
3198
  } catch (error) {
2212
3199
  const durationMs = Number(process.hrtime.bigint() - requestStart) / 1e6;
2213
- logger.error(
2214
- {
2215
- err: error,
2216
- requestId: request.requestId,
2217
- sessionId: request.params.sessionId,
2218
- promptBlocks: request.params.prompt.length,
2219
- durationMs
2220
- },
2221
- "rpc_message_send_error"
2222
- );
3200
+ logger.error({
3201
+ err: error,
3202
+ requestId: request.requestId,
3203
+ sessionId: request.params.sessionId,
3204
+ promptBlocks: request.params.prompt.length,
3205
+ durationMs
3206
+ }, "rpc_message_send_error");
2223
3207
  this.sendRpcError(request.requestId, error);
2224
3208
  }
2225
3209
  });
2226
3210
  this.socket.on("rpc:permission:decision", async (request) => {
2227
3211
  try {
2228
3212
  const { sessionId, requestId, outcome } = request.params;
2229
- logger.info(
2230
- { requestId: request.requestId, sessionId, outcome },
2231
- "rpc_permission_decision"
2232
- );
3213
+ logger.info({ requestId: request.requestId, sessionId, outcome }, "rpc_permission_decision");
2233
3214
  sessionManager.resolvePermissionRequest(sessionId, requestId, outcome);
2234
3215
  this.sendRpcResponse(request.requestId, { ok: true });
2235
3216
  } catch (error) {
2236
- logger.error(
2237
- {
2238
- err: error,
2239
- requestId: request.requestId,
2240
- sessionId: request.params.sessionId,
2241
- permissionRequestId: request.params.requestId,
2242
- outcome: request.params.outcome
2243
- },
2244
- "rpc_permission_decision_error"
2245
- );
3217
+ logger.error({
3218
+ err: error,
3219
+ requestId: request.requestId,
3220
+ sessionId: request.params.sessionId,
3221
+ permissionRequestId: request.params.requestId,
3222
+ outcome: request.params.outcome
3223
+ }, "rpc_permission_decision_error");
2246
3224
  this.sendRpcError(request.requestId, error);
2247
3225
  }
2248
3226
  });
2249
3227
  this.socket.on("rpc:fs:roots", async (request) => {
2250
3228
  try {
2251
- logger.debug(
2252
- { requestId: request.requestId, sessionId: request.params.sessionId },
2253
- "rpc_fs_roots"
2254
- );
3229
+ logger.debug({ requestId: request.requestId, sessionId: request.params.sessionId }, "rpc_fs_roots");
2255
3230
  const record = sessionManager.getSession(request.params.sessionId);
2256
3231
  if (!record || !record.cwd) {
2257
3232
  throw new Error("Session not found or no working directory");
@@ -2262,105 +3237,81 @@ var SocketClient = class extends EventEmitter3 {
2262
3237
  };
2263
3238
  this.sendRpcResponse(request.requestId, { root });
2264
3239
  } catch (error) {
2265
- logger.error(
2266
- {
2267
- err: error,
2268
- requestId: request.requestId,
2269
- sessionId: request.params.sessionId
2270
- },
2271
- "rpc_fs_roots_error"
2272
- );
3240
+ logger.error({
3241
+ err: error,
3242
+ requestId: request.requestId,
3243
+ sessionId: request.params.sessionId
3244
+ }, "rpc_fs_roots_error");
2273
3245
  this.sendRpcError(request.requestId, error);
2274
3246
  }
2275
3247
  });
2276
3248
  this.socket.on("rpc:hostfs:roots", async (request) => {
2277
3249
  try {
2278
- logger.debug(
2279
- {
2280
- requestId: request.requestId,
2281
- machineId: request.params.machineId
2282
- },
2283
- "rpc_hostfs_roots"
2284
- );
3250
+ logger.debug({
3251
+ requestId: request.requestId,
3252
+ machineId: request.params.machineId
3253
+ }, "rpc_hostfs_roots");
2285
3254
  const result = await buildHostFsRoots();
2286
3255
  this.sendRpcResponse(request.requestId, result);
2287
3256
  } catch (error) {
2288
- logger.error(
2289
- {
2290
- err: error,
2291
- requestId: request.requestId,
2292
- machineId: request.params.machineId
2293
- },
2294
- "rpc_hostfs_roots_error"
2295
- );
3257
+ logger.error({
3258
+ err: error,
3259
+ requestId: request.requestId,
3260
+ machineId: request.params.machineId
3261
+ }, "rpc_hostfs_roots_error");
2296
3262
  this.sendRpcError(request.requestId, error);
2297
3263
  }
2298
3264
  });
2299
3265
  this.socket.on("rpc:hostfs:entries", async (request) => {
2300
3266
  try {
2301
3267
  const { path: requestPath, machineId } = request.params;
2302
- logger.debug(
2303
- { requestId: request.requestId, machineId, path: requestPath },
2304
- "rpc_hostfs_entries"
2305
- );
3268
+ logger.debug({ requestId: request.requestId, machineId, path: requestPath }, "rpc_hostfs_entries");
2306
3269
  const entries = await readDirectoryEntries(requestPath);
2307
3270
  this.sendRpcResponse(request.requestId, {
2308
3271
  path: requestPath,
2309
3272
  entries: filterVisibleEntries(entries)
2310
3273
  });
2311
3274
  } catch (error) {
2312
- logger.error(
2313
- {
2314
- err: error,
2315
- requestId: request.requestId,
2316
- machineId: request.params.machineId
2317
- },
2318
- "rpc_hostfs_entries_error"
2319
- );
3275
+ logger.error({
3276
+ err: error,
3277
+ requestId: request.requestId,
3278
+ machineId: request.params.machineId
3279
+ }, "rpc_hostfs_entries_error");
2320
3280
  this.sendRpcError(request.requestId, error);
2321
3281
  }
2322
3282
  });
2323
3283
  this.socket.on("rpc:fs:entries", async (request) => {
2324
3284
  try {
2325
3285
  const { sessionId, path: requestPath } = request.params;
2326
- logger.debug(
2327
- { requestId: request.requestId, sessionId, path: requestPath },
2328
- "rpc_fs_entries"
2329
- );
3286
+ logger.debug({ requestId: request.requestId, sessionId, path: requestPath }, "rpc_fs_entries");
2330
3287
  const record = sessionManager.getSession(sessionId);
2331
3288
  if (!record || !record.cwd) {
2332
3289
  throw new Error("Session not found or no working directory");
2333
3290
  }
2334
- const resolved = requestPath ? path5.isAbsolute(requestPath) ? requestPath : path5.join(record.cwd, requestPath) : record.cwd;
3291
+ const resolved = requestPath ? resolveWithinCwd(record.cwd, requestPath) : record.cwd;
2335
3292
  const entries = await readDirectoryEntries(resolved);
2336
3293
  this.sendRpcResponse(request.requestId, { path: resolved, entries });
2337
3294
  } catch (error) {
2338
- logger.error(
2339
- {
2340
- err: error,
2341
- requestId: request.requestId,
2342
- sessionId: request.params.sessionId
2343
- },
2344
- "rpc_fs_entries_error"
2345
- );
3295
+ logger.error({
3296
+ err: error,
3297
+ requestId: request.requestId,
3298
+ sessionId: request.params.sessionId
3299
+ }, "rpc_fs_entries_error");
2346
3300
  this.sendRpcError(request.requestId, error);
2347
3301
  }
2348
3302
  });
2349
3303
  this.socket.on("rpc:fs:file", async (request) => {
2350
3304
  try {
2351
3305
  const { sessionId, path: requestPath } = request.params;
2352
- logger.debug(
2353
- { requestId: request.requestId, sessionId, path: requestPath },
2354
- "rpc_fs_file"
2355
- );
3306
+ logger.debug({ requestId: request.requestId, sessionId, path: requestPath }, "rpc_fs_file");
2356
3307
  const record = sessionManager.getSession(sessionId);
2357
3308
  if (!record || !record.cwd) {
2358
3309
  throw new Error("Session not found or no working directory");
2359
3310
  }
2360
- const resolved = path5.isAbsolute(requestPath) ? requestPath : path5.join(record.cwd, requestPath);
3311
+ const resolved = resolveWithinCwd(record.cwd, requestPath);
2361
3312
  const mimeType = resolveImageMimeType(resolved);
2362
3313
  if (mimeType) {
2363
- const buffer = await fs4.readFile(resolved);
3314
+ const buffer = await fs5.readFile(resolved);
2364
3315
  const preview2 = {
2365
3316
  path: resolved,
2366
3317
  previewType: "image",
@@ -2370,7 +3321,7 @@ var SocketClient = class extends EventEmitter3 {
2370
3321
  this.sendRpcResponse(request.requestId, preview2);
2371
3322
  return;
2372
3323
  }
2373
- const content = await fs4.readFile(resolved, "utf8");
3324
+ const content = await fs5.readFile(resolved, "utf8");
2374
3325
  const preview = {
2375
3326
  path: resolved,
2376
3327
  previewType: "code",
@@ -2378,24 +3329,18 @@ var SocketClient = class extends EventEmitter3 {
2378
3329
  };
2379
3330
  this.sendRpcResponse(request.requestId, preview);
2380
3331
  } catch (error) {
2381
- logger.error(
2382
- {
2383
- err: error,
2384
- requestId: request.requestId,
2385
- sessionId: request.params.sessionId
2386
- },
2387
- "rpc_fs_file_error"
2388
- );
3332
+ logger.error({
3333
+ err: error,
3334
+ requestId: request.requestId,
3335
+ sessionId: request.params.sessionId
3336
+ }, "rpc_fs_file_error");
2389
3337
  this.sendRpcError(request.requestId, error);
2390
3338
  }
2391
3339
  });
2392
3340
  this.socket.on("rpc:fs:resources", async (request) => {
2393
3341
  try {
2394
3342
  const { sessionId } = request.params;
2395
- logger.debug(
2396
- { requestId: request.requestId, sessionId },
2397
- "rpc_fs_resources"
2398
- );
3343
+ logger.debug({ requestId: request.requestId, sessionId }, "rpc_fs_resources");
2399
3344
  const record = sessionManager.getSession(sessionId);
2400
3345
  if (!record || !record.cwd) {
2401
3346
  throw new Error("Session not found or no working directory");
@@ -2406,24 +3351,18 @@ var SocketClient = class extends EventEmitter3 {
2406
3351
  entries
2407
3352
  });
2408
3353
  } catch (error) {
2409
- logger.error(
2410
- {
2411
- err: error,
2412
- requestId: request.requestId,
2413
- sessionId: request.params.sessionId
2414
- },
2415
- "rpc_fs_resources_error"
2416
- );
3354
+ logger.error({
3355
+ err: error,
3356
+ requestId: request.requestId,
3357
+ sessionId: request.params.sessionId
3358
+ }, "rpc_fs_resources_error");
2417
3359
  this.sendRpcError(request.requestId, error);
2418
3360
  }
2419
3361
  });
2420
3362
  this.socket.on("rpc:sessions:discover", async (request) => {
2421
3363
  try {
2422
3364
  const { cwd, backendId, cursor } = request.params;
2423
- logger.info(
2424
- { requestId: request.requestId, cwd, backendId, cursor },
2425
- "rpc_sessions_discover"
2426
- );
3365
+ logger.info({ requestId: request.requestId, cwd, backendId, cursor }, "rpc_sessions_discover");
2427
3366
  const result = await sessionManager.discoverSessions({
2428
3367
  cwd,
2429
3368
  backendId,
@@ -2431,65 +3370,47 @@ var SocketClient = class extends EventEmitter3 {
2431
3370
  });
2432
3371
  this.sendRpcResponse(request.requestId, result);
2433
3372
  } catch (error) {
2434
- logger.error(
2435
- {
2436
- err: error,
2437
- requestId: request.requestId
2438
- },
2439
- "rpc_sessions_discover_error"
2440
- );
3373
+ logger.error({
3374
+ err: error,
3375
+ requestId: request.requestId
3376
+ }, "rpc_sessions_discover_error");
2441
3377
  this.sendRpcError(request.requestId, error);
2442
3378
  }
2443
3379
  });
2444
3380
  this.socket.on("rpc:session:load", async (request) => {
2445
3381
  try {
2446
- const { sessionId, cwd } = request.params;
2447
- logger.info(
2448
- { requestId: request.requestId, sessionId, cwd },
2449
- "rpc_session_load"
2450
- );
2451
- const session = await sessionManager.loadSession(sessionId, cwd);
3382
+ const { sessionId, cwd, backendId } = request.params;
3383
+ logger.info({ requestId: request.requestId, sessionId, cwd, backendId }, "rpc_session_load");
3384
+ const session = await sessionManager.loadSession(sessionId, cwd, backendId);
2452
3385
  this.sendRpcResponse(request.requestId, session);
2453
3386
  } catch (error) {
2454
- logger.error(
2455
- {
2456
- err: error,
2457
- requestId: request.requestId,
2458
- sessionId: request.params.sessionId
2459
- },
2460
- "rpc_session_load_error"
2461
- );
3387
+ logger.error({
3388
+ err: error,
3389
+ requestId: request.requestId,
3390
+ sessionId: request.params.sessionId
3391
+ }, "rpc_session_load_error");
2462
3392
  this.sendRpcError(request.requestId, error);
2463
3393
  }
2464
3394
  });
2465
3395
  this.socket.on("rpc:session:reload", async (request) => {
2466
3396
  try {
2467
- const { sessionId, cwd } = request.params;
2468
- logger.info(
2469
- { requestId: request.requestId, sessionId, cwd },
2470
- "rpc_session_reload"
2471
- );
2472
- const session = await sessionManager.reloadSession(sessionId, cwd);
3397
+ const { sessionId, cwd, backendId } = request.params;
3398
+ logger.info({ requestId: request.requestId, sessionId, cwd, backendId }, "rpc_session_reload");
3399
+ const session = await sessionManager.reloadSession(sessionId, cwd, backendId);
2473
3400
  this.sendRpcResponse(request.requestId, session);
2474
3401
  } catch (error) {
2475
- logger.error(
2476
- {
2477
- err: error,
2478
- requestId: request.requestId,
2479
- sessionId: request.params.sessionId
2480
- },
2481
- "rpc_session_reload_error"
2482
- );
3402
+ logger.error({
3403
+ err: error,
3404
+ requestId: request.requestId,
3405
+ sessionId: request.params.sessionId
3406
+ }, "rpc_session_reload_error");
2483
3407
  this.sendRpcError(request.requestId, error);
2484
3408
  }
2485
3409
  });
2486
3410
  this.socket.on("rpc:git:status", async (request) => {
2487
3411
  try {
2488
3412
  const { sessionId } = request.params;
2489
- logger.debug(
2490
- { requestId: request.requestId, sessionId },
2491
- "rpc_git_status"
2492
- );
3413
+ logger.debug({ requestId: request.requestId, sessionId }, "rpc_git_status");
2493
3414
  const record = sessionManager.getSession(sessionId);
2494
3415
  if (!record || !record.cwd) {
2495
3416
  throw new Error("Session not found or no working directory");
@@ -2515,28 +3436,23 @@ var SocketClient = class extends EventEmitter3 {
2515
3436
  dirStatus
2516
3437
  });
2517
3438
  } catch (error) {
2518
- logger.error(
2519
- {
2520
- err: error,
2521
- requestId: request.requestId,
2522
- sessionId: request.params.sessionId
2523
- },
2524
- "rpc_git_status_error"
2525
- );
3439
+ logger.error({
3440
+ err: error,
3441
+ requestId: request.requestId,
3442
+ sessionId: request.params.sessionId
3443
+ }, "rpc_git_status_error");
2526
3444
  this.sendRpcError(request.requestId, error);
2527
3445
  }
2528
3446
  });
2529
3447
  this.socket.on("rpc:git:fileDiff", async (request) => {
2530
3448
  try {
2531
3449
  const { sessionId, path: filePath } = request.params;
2532
- logger.debug(
2533
- { requestId: request.requestId, sessionId, path: filePath },
2534
- "rpc_git_file_diff"
2535
- );
3450
+ logger.debug({ requestId: request.requestId, sessionId, path: filePath }, "rpc_git_file_diff");
2536
3451
  const record = sessionManager.getSession(sessionId);
2537
3452
  if (!record || !record.cwd) {
2538
3453
  throw new Error("Session not found or no working directory");
2539
3454
  }
3455
+ resolveWithinCwd(record.cwd, filePath);
2540
3456
  const isRepo = await isGitRepo(record.cwd);
2541
3457
  if (!isRepo) {
2542
3458
  this.sendRpcResponse(request.requestId, {
@@ -2547,10 +3463,7 @@ var SocketClient = class extends EventEmitter3 {
2547
3463
  });
2548
3464
  return;
2549
3465
  }
2550
- const { addedLines, modifiedLines } = await getFileDiff(
2551
- record.cwd,
2552
- filePath
2553
- );
3466
+ const { addedLines, modifiedLines } = await getFileDiff(record.cwd, filePath);
2554
3467
  this.sendRpcResponse(request.requestId, {
2555
3468
  isGitRepo: true,
2556
3469
  path: filePath,
@@ -2558,33 +3471,76 @@ var SocketClient = class extends EventEmitter3 {
2558
3471
  modifiedLines
2559
3472
  });
2560
3473
  } catch (error) {
2561
- logger.error(
2562
- {
2563
- err: error,
2564
- requestId: request.requestId,
2565
- sessionId: request.params.sessionId
2566
- },
2567
- "rpc_git_file_diff_error"
2568
- );
3474
+ logger.error({
3475
+ err: error,
3476
+ requestId: request.requestId,
3477
+ sessionId: request.params.sessionId
3478
+ }, "rpc_git_file_diff_error");
2569
3479
  this.sendRpcError(request.requestId, error);
2570
3480
  }
2571
3481
  });
2572
- }
2573
- setupSessionManagerListeners() {
2574
- const { sessionManager } = this.options;
2575
- sessionManager.onSessionUpdate((notification) => {
2576
- if (this.connected) {
2577
- this.socket.emit(
2578
- "session:update",
2579
- notification
2580
- );
3482
+ this.socket.on("rpc:session:archive", async (request) => {
3483
+ try {
3484
+ const { sessionId } = request.params;
3485
+ logger.info({ requestId: request.requestId, sessionId }, "rpc_session_archive");
3486
+ await sessionManager.archiveSession(sessionId);
3487
+ this.sendRpcResponse(request.requestId, { ok: true });
3488
+ } catch (error) {
3489
+ logger.error({
3490
+ err: error,
3491
+ requestId: request.requestId,
3492
+ sessionId: request.params.sessionId
3493
+ }, "rpc_session_archive_error");
3494
+ this.sendRpcError(request.requestId, error);
2581
3495
  }
2582
3496
  });
2583
- sessionManager.onSessionError((payload) => {
2584
- if (this.connected) {
2585
- this.socket.emit("session:error", payload);
3497
+ this.socket.on("rpc:session:archive-all", async (request) => {
3498
+ try {
3499
+ const { sessionIds } = request.params;
3500
+ logger.info({
3501
+ requestId: request.requestId,
3502
+ count: sessionIds.length
3503
+ }, "rpc_session_archive_all");
3504
+ const result = await sessionManager.bulkArchiveSessions(sessionIds);
3505
+ this.sendRpcResponse(request.requestId, result);
3506
+ } catch (error) {
3507
+ logger.error({
3508
+ err: error,
3509
+ requestId: request.requestId
3510
+ }, "rpc_session_archive_all_error");
3511
+ this.sendRpcError(request.requestId, error);
3512
+ }
3513
+ });
3514
+ this.socket.on("rpc:session:events", (request) => {
3515
+ try {
3516
+ const { sessionId, revision, afterSeq, limit } = request.params;
3517
+ logger.debug({
3518
+ requestId: request.requestId,
3519
+ sessionId,
3520
+ revision,
3521
+ afterSeq,
3522
+ limit
3523
+ }, "rpc_session_events");
3524
+ const result = sessionManager.getSessionEvents({
3525
+ sessionId,
3526
+ revision,
3527
+ afterSeq,
3528
+ limit
3529
+ });
3530
+ result.events = result.events.map((e) => this.options.cryptoService.encryptEvent(e));
3531
+ this.sendRpcResponse(request.requestId, result);
3532
+ } catch (error) {
3533
+ logger.error({
3534
+ err: error,
3535
+ requestId: request.requestId,
3536
+ sessionId: request.params.sessionId
3537
+ }, "rpc_session_events_error");
3538
+ this.sendRpcError(request.requestId, error);
2586
3539
  }
2587
3540
  });
3541
+ }
3542
+ setupSessionManagerListeners() {
3543
+ const { sessionManager } = this.options;
2588
3544
  sessionManager.onPermissionRequest((payload) => {
2589
3545
  if (this.connected) {
2590
3546
  this.socket.emit("permission:request", payload);
@@ -2595,21 +3551,13 @@ var SocketClient = class extends EventEmitter3 {
2595
3551
  this.socket.emit("permission:result", payload);
2596
3552
  }
2597
3553
  });
2598
- sessionManager.onTerminalOutput((event) => {
2599
- if (this.connected) {
2600
- this.socket.emit("terminal:output", event);
2601
- }
2602
- });
2603
3554
  sessionManager.onSessionsChanged((payload) => {
2604
3555
  if (this.connected) {
2605
- logger.info(
2606
- {
2607
- added: payload.added.length,
2608
- updated: payload.updated.length,
2609
- removed: payload.removed.length
2610
- },
2611
- "sessions_changed_emit"
2612
- );
3556
+ logger.info({
3557
+ added: payload.added.length,
3558
+ updated: payload.updated.length,
3559
+ removed: payload.removed.length
3560
+ }, "sessions_changed_emit");
2613
3561
  this.socket.emit("sessions:changed", payload);
2614
3562
  }
2615
3563
  });
@@ -2623,13 +3571,42 @@ var SocketClient = class extends EventEmitter3 {
2623
3571
  this.socket.emit("session:detached", payload);
2624
3572
  }
2625
3573
  });
3574
+ sessionManager.onSessionEvent((event) => {
3575
+ logger.info({
3576
+ sessionId: event.sessionId,
3577
+ revision: event.revision,
3578
+ seq: event.seq,
3579
+ kind: event.kind,
3580
+ connected: this.connected
3581
+ }, "session_event_received_from_manager");
3582
+ if (this.connected) {
3583
+ const encrypted = this.options.cryptoService.encryptEvent(event);
3584
+ logger.debug({
3585
+ sessionId: event.sessionId,
3586
+ revision: event.revision,
3587
+ seq: event.seq,
3588
+ kind: event.kind
3589
+ }, "session_event_emitting_to_gateway");
3590
+ this.socket.emit("session:event", encrypted);
3591
+ logger.debug({
3592
+ sessionId: event.sessionId,
3593
+ seq: event.seq
3594
+ }, "session_event_emitted_to_gateway");
3595
+ } else {
3596
+ logger.warn({
3597
+ sessionId: event.sessionId,
3598
+ seq: event.seq,
3599
+ kind: event.kind
3600
+ }, "session_event_dropped_not_connected");
3601
+ }
3602
+ });
2626
3603
  }
2627
3604
  async listSessionResources(rootPath) {
2628
3605
  const ig = await loadGitignore(rootPath);
2629
3606
  const allFiles = await this.listAllFiles(rootPath, ig, rootPath, []);
2630
3607
  return allFiles.map((filePath) => ({
2631
- name: path5.basename(filePath),
2632
- relativePath: path5.relative(rootPath, filePath),
3608
+ name: path6.basename(filePath),
3609
+ relativePath: path6.relative(rootPath, filePath),
2633
3610
  path: filePath
2634
3611
  }));
2635
3612
  }
@@ -2637,13 +3614,13 @@ var SocketClient = class extends EventEmitter3 {
2637
3614
  if (collected.length >= MAX_RESOURCE_FILES) {
2638
3615
  return collected;
2639
3616
  }
2640
- const entries = await fs4.readdir(rootPath, { withFileTypes: true });
3617
+ const entries = await fs5.readdir(rootPath, { withFileTypes: true });
2641
3618
  for (const entry of entries) {
2642
3619
  if (collected.length >= MAX_RESOURCE_FILES) {
2643
3620
  break;
2644
3621
  }
2645
- const entryPath = path5.join(rootPath, entry.name);
2646
- const relativePath = path5.relative(baseDir, entryPath);
3622
+ const entryPath = path6.join(rootPath, entry.name);
3623
+ const relativePath = path6.relative(baseDir, entryPath);
2647
3624
  const checkPath = entry.isDirectory() ? `${relativePath}/` : relativePath;
2648
3625
  if (ig.ignores(checkPath)) {
2649
3626
  continue;
@@ -2663,16 +3640,13 @@ var SocketClient = class extends EventEmitter3 {
2663
3640
  }
2664
3641
  sendRpcError(requestId, error) {
2665
3642
  const message = error instanceof Error ? error.message : "Unknown error";
2666
- const detail = error instanceof Error ? error.stack : void 0;
2667
- logger.error(
2668
- {
2669
- requestId,
2670
- err: error,
2671
- message,
2672
- detail
2673
- },
2674
- "rpc_response_error_sent"
2675
- );
3643
+ const detail = error instanceof Error ? error.stack : undefined;
3644
+ logger.error({
3645
+ requestId,
3646
+ err: error,
3647
+ message,
3648
+ detail
3649
+ }, "rpc_response_error_sent");
2676
3650
  const response = {
2677
3651
  requestId,
2678
3652
  error: {
@@ -2695,28 +3669,45 @@ var SocketClient = class extends EventEmitter3 {
2695
3669
  backends: config.acpBackends.map((backend) => ({
2696
3670
  backendId: backend.id,
2697
3671
  backendLabel: backend.label
2698
- })),
2699
- defaultBackendId: config.defaultAcpBackendId
3672
+ }))
2700
3673
  });
2701
3674
  logger.info({ machineId: config.machineId }, "cli_register_sessions_list");
2702
- this.socket.emit("sessions:list", sessionManager.listSessions());
3675
+ this.socket.emit("sessions:list", sessionManager.listAllSessions());
2703
3676
  }
2704
3677
  startHeartbeat() {
2705
3678
  this.stopHeartbeat();
2706
3679
  this.heartbeatInterval = setInterval(() => {
2707
3680
  if (this.connected) {
2708
3681
  this.socket.emit("cli:heartbeat");
2709
- this.socket.emit(
2710
- "sessions:list",
2711
- this.options.sessionManager.listSessions()
2712
- );
3682
+ this.socket.emit("sessions:list", this.options.sessionManager.listAllSessions());
2713
3683
  }
2714
- }, 3e4);
3684
+ }, 30000);
2715
3685
  }
2716
3686
  stopHeartbeat() {
2717
3687
  if (this.heartbeatInterval) {
2718
3688
  clearInterval(this.heartbeatInterval);
2719
- this.heartbeatInterval = void 0;
3689
+ this.heartbeatInterval = undefined;
3690
+ }
3691
+ }
3692
+ replayUnackedEvents() {
3693
+ const { sessionManager } = this.options;
3694
+ const sessions = sessionManager.listSessions();
3695
+ for (const session of sessions) {
3696
+ const revision = sessionManager.getSessionRevision(session.sessionId);
3697
+ if (revision === undefined)
3698
+ continue;
3699
+ const unackedEvents = sessionManager.getUnackedEvents(session.sessionId, revision);
3700
+ if (unackedEvents.length > 0) {
3701
+ logger.info({
3702
+ sessionId: session.sessionId,
3703
+ revision,
3704
+ count: unackedEvents.length
3705
+ }, "replaying_unacked_events");
3706
+ for (const event of unackedEvents) {
3707
+ const encrypted = this.options.cryptoService.encryptEvent(event);
3708
+ this.socket.emit("session:event", encrypted);
3709
+ }
3710
+ }
2720
3711
  }
2721
3712
  }
2722
3713
  connect() {
@@ -2729,20 +3720,21 @@ var SocketClient = class extends EventEmitter3 {
2729
3720
  isConnected() {
2730
3721
  return this.connected;
2731
3722
  }
2732
- };
3723
+ }
2733
3724
 
2734
3725
  // src/daemon/daemon.ts
2735
- var DaemonManager = class {
3726
+ class DaemonManager {
3727
+ config;
2736
3728
  constructor(config) {
2737
3729
  this.config = config;
2738
3730
  }
2739
3731
  async ensureHomeDirectory() {
2740
- await fs5.mkdir(this.config.homePath, { recursive: true });
2741
- await fs5.mkdir(this.config.logPath, { recursive: true });
3732
+ await fs6.mkdir(this.config.homePath, { recursive: true });
3733
+ await fs6.mkdir(this.config.logPath, { recursive: true });
2742
3734
  }
2743
3735
  async getPid() {
2744
3736
  try {
2745
- const content = await fs5.readFile(this.config.pidFile, "utf8");
3737
+ const content = await fs6.readFile(this.config.pidFile, "utf8");
2746
3738
  const pid = Number.parseInt(content.trim(), 10);
2747
3739
  if (Number.isNaN(pid)) {
2748
3740
  return null;
@@ -2759,13 +3751,12 @@ var DaemonManager = class {
2759
3751
  }
2760
3752
  }
2761
3753
  async writePidFile(pid) {
2762
- await fs5.writeFile(this.config.pidFile, String(pid), "utf8");
3754
+ await fs6.writeFile(this.config.pidFile, String(pid), "utf8");
2763
3755
  }
2764
3756
  async removePidFile() {
2765
3757
  try {
2766
- await fs5.unlink(this.config.pidFile);
2767
- } catch {
2768
- }
3758
+ await fs6.unlink(this.config.pidFile);
3759
+ } catch {}
2769
3760
  }
2770
3761
  async status() {
2771
3762
  const pid = await this.getPid();
@@ -2804,7 +3795,7 @@ var DaemonManager = class {
2804
3795
  logger.info({ pid }, "daemon_stop_sigterm");
2805
3796
  process.kill(pid, "SIGTERM");
2806
3797
  const startTime = Date.now();
2807
- const timeout = 5e3;
3798
+ const timeout = 5000;
2808
3799
  while (Date.now() - startTime < timeout) {
2809
3800
  await new Promise((resolve) => setTimeout(resolve, 100));
2810
3801
  try {
@@ -2830,10 +3821,7 @@ var DaemonManager = class {
2830
3821
  }
2831
3822
  }
2832
3823
  async spawnBackground() {
2833
- const logFile = path6.join(
2834
- this.config.logPath,
2835
- `${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}-daemon.log`
2836
- );
3824
+ const logFile = path7.join(this.config.logPath, `${new Date().toISOString().replace(/[:.]/g, "-")}-daemon.log`);
2837
3825
  const args = process.argv.slice(1).filter((arg) => arg !== "--foreground" && arg !== "-f");
2838
3826
  args.push("--foreground");
2839
3827
  const child = spawn2(process.argv[0], args, {
@@ -2848,22 +3836,18 @@ var DaemonManager = class {
2848
3836
  logger.error("daemon_spawn_failed");
2849
3837
  throw new Error("Failed to spawn daemon process");
2850
3838
  }
2851
- const logStream = await fs5.open(logFile, "a");
3839
+ const logStream = await fs6.open(logFile, "a");
2852
3840
  const fileHandle = logStream;
2853
3841
  child.stdout?.on("data", (data) => {
2854
- fileHandle.write(`[stdout] ${data.toString()}`).catch(() => {
2855
- });
3842
+ fileHandle.write(`[stdout] ${data.toString()}`).catch(() => {});
2856
3843
  });
2857
3844
  child.stderr?.on("data", (data) => {
2858
- fileHandle.write(`[stderr] ${data.toString()}`).catch(() => {
2859
- });
3845
+ fileHandle.write(`[stderr] ${data.toString()}`).catch(() => {});
2860
3846
  });
2861
3847
  child.on("exit", (code, signal) => {
2862
3848
  fileHandle.write(`[exit] Process exited with code ${code}, signal ${signal}
2863
- `).catch(() => {
2864
- });
2865
- fileHandle.close().catch(() => {
2866
- });
3849
+ `).catch(() => {});
3850
+ fileHandle.close().catch(() => {});
2867
3851
  });
2868
3852
  child.unref();
2869
3853
  logger.info({ pid: child.pid }, "daemon_started");
@@ -2876,22 +3860,47 @@ var DaemonManager = class {
2876
3860
  logger.info({ pid }, "daemon_starting");
2877
3861
  logger.info({ gatewayUrl: this.config.gatewayUrl }, "daemon_gateway_url");
2878
3862
  logger.info({ machineId: this.config.machineId }, "daemon_machine_id");
2879
- const apiKey = await getApiKey();
2880
- if (!apiKey) {
2881
- logger.error("daemon_api_key_missing");
2882
- console.error(
2883
- `[mobvibe-cli] No API key found. Run 'mobvibe login' to authenticate.`
2884
- );
2885
- logger.warn("daemon_exit_missing_api_key");
3863
+ await initCrypto2();
3864
+ const masterSecretBase64 = await getMasterSecret();
3865
+ if (!masterSecretBase64) {
3866
+ logger.error("daemon_master_secret_missing");
3867
+ console.error(`[mobvibe-cli] No credentials found. Run 'mobvibe login' to authenticate.`);
3868
+ logger.warn("daemon_exit_missing_master_secret");
2886
3869
  process.exit(1);
2887
3870
  }
2888
- logger.info("daemon_api_key_loaded");
2889
- const sessionManager = new SessionManager(this.config);
3871
+ const sodium = getSodium3();
3872
+ const masterSecret = sodium.from_base64(masterSecretBase64, sodium.base64_variants.ORIGINAL);
3873
+ const cryptoService = new CliCryptoService(masterSecret);
3874
+ logger.info("daemon_crypto_initialized");
3875
+ const sessionManager = new SessionManager(this.config, cryptoService);
2890
3876
  const socketClient = new SocketClient({
2891
3877
  config: this.config,
2892
3878
  sessionManager,
2893
- apiKey
3879
+ cryptoService
2894
3880
  });
3881
+ let compactor;
3882
+ let compactionInterval;
3883
+ let compactorWalStore;
3884
+ let compactorDb;
3885
+ if (this.config.compaction.enabled) {
3886
+ compactorWalStore = new WalStore(this.config.walDbPath);
3887
+ compactorDb = new Database2(this.config.walDbPath);
3888
+ compactor = new WalCompactor(compactorWalStore, this.config.compaction, compactorDb);
3889
+ if (this.config.compaction.runOnStartup) {
3890
+ logger.info("compaction_startup_start");
3891
+ compactor.compactAll().catch((error) => {
3892
+ logger.error({ err: error }, "compaction_startup_error");
3893
+ });
3894
+ }
3895
+ const intervalMs = this.config.compaction.runIntervalHours * 60 * 60 * 1000;
3896
+ compactionInterval = setInterval(() => {
3897
+ logger.info("compaction_scheduled_start");
3898
+ compactor?.compactAll().catch((error) => {
3899
+ logger.error({ err: error }, "compaction_scheduled_error");
3900
+ });
3901
+ }, intervalMs);
3902
+ logger.info({ intervalHours: this.config.compaction.runIntervalHours }, "compaction_scheduled");
3903
+ }
2895
3904
  let shuttingDown = false;
2896
3905
  const shutdown = async (signal) => {
2897
3906
  if (shuttingDown) {
@@ -2901,8 +3910,17 @@ var DaemonManager = class {
2901
3910
  shuttingDown = true;
2902
3911
  logger.info({ signal }, "daemon_shutdown_start");
2903
3912
  try {
3913
+ if (compactionInterval) {
3914
+ clearInterval(compactionInterval);
3915
+ }
3916
+ if (compactorWalStore) {
3917
+ compactorWalStore.close();
3918
+ }
3919
+ if (compactorDb) {
3920
+ compactorDb.close();
3921
+ }
2904
3922
  socketClient.disconnect();
2905
- await sessionManager.closeAll();
3923
+ await sessionManager.shutdown();
2906
3924
  await this.removePidFile();
2907
3925
  logger.info({ signal }, "daemon_shutdown_complete");
2908
3926
  } catch (error) {
@@ -2920,18 +3938,17 @@ var DaemonManager = class {
2920
3938
  });
2921
3939
  });
2922
3940
  socketClient.connect();
2923
- await new Promise(() => {
2924
- });
3941
+ await new Promise(() => {});
2925
3942
  }
2926
3943
  async logs(options) {
2927
- const files = await fs5.readdir(this.config.logPath);
3944
+ const files = await fs6.readdir(this.config.logPath);
2928
3945
  const logFiles = files.filter((f) => f.endsWith("-daemon.log")).sort().reverse();
2929
3946
  if (logFiles.length === 0) {
2930
3947
  logger.warn("daemon_logs_empty");
2931
3948
  console.log("No log files found");
2932
3949
  return;
2933
3950
  }
2934
- const latestLog = path6.join(this.config.logPath, logFiles[0]);
3951
+ const latestLog = path7.join(this.config.logPath, logFiles[0]);
2935
3952
  logger.info({ logFile: latestLog }, "daemon_logs_latest");
2936
3953
  console.log(`Log file: ${latestLog}
2937
3954
  `);
@@ -2943,16 +3960,18 @@ var DaemonManager = class {
2943
3960
  tail.on("close", () => resolve());
2944
3961
  });
2945
3962
  } else {
2946
- const content = await fs5.readFile(latestLog, "utf8");
2947
- const lines = content.split("\n");
3963
+ const content = await fs6.readFile(latestLog, "utf8");
3964
+ const lines = content.split(`
3965
+ `);
2948
3966
  const count = options?.lines ?? 50;
2949
- console.log(lines.slice(-count).join("\n"));
3967
+ console.log(lines.slice(-count).join(`
3968
+ `));
2950
3969
  }
2951
3970
  }
2952
- };
3971
+ }
2953
3972
 
2954
3973
  // src/index.ts
2955
- var program = new Command();
3974
+ var program = new Command;
2956
3975
  program.name("mobvibe").description("Mobvibe CLI - Connect local ACP backends to the gateway").version("0.0.0");
2957
3976
  program.command("start").description("Start the mobvibe daemon").option("--gateway <url>", "Gateway URL", process.env.MOBVIBE_GATEWAY_URL).option("--foreground", "Run in foreground instead of detaching").action(async (options) => {
2958
3977
  if (options.gateway) {
@@ -2974,10 +3993,10 @@ program.command("status").description("Show daemon status").action(async () => {
2974
3993
  if (status.running) {
2975
3994
  logger.info({ pid: status.pid }, "daemon_status_running");
2976
3995
  console.log(`Daemon is running (PID ${status.pid})`);
2977
- if (status.connected !== void 0) {
3996
+ if (status.connected !== undefined) {
2978
3997
  console.log(`Connected to gateway: ${status.connected ? "yes" : "no"}`);
2979
3998
  }
2980
- if (status.sessionCount !== void 0) {
3999
+ if (status.sessionCount !== undefined) {
2981
4000
  console.log(`Active sessions: ${status.sessionCount}`);
2982
4001
  }
2983
4002
  } else {
@@ -3007,6 +4026,97 @@ program.command("logout").description("Remove stored credentials").action(async
3007
4026
  program.command("auth-status").description("Show authentication status").action(async () => {
3008
4027
  await loginStatus();
3009
4028
  });
4029
+ var e2eeCmd = program.command("e2ee").description("E2EE key management");
4030
+ e2eeCmd.command("show").description("Display the master secret for pairing other devices").action(async () => {
4031
+ const credentials = await loadCredentials();
4032
+ if (!credentials) {
4033
+ console.error("Not logged in. Run 'mobvibe login' first.");
4034
+ process.exit(1);
4035
+ }
4036
+ const base64url = credentials.masterSecret.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
4037
+ const pairingUrl = `mobvibe://pair?secret=${base64url}`;
4038
+ const QRCode = await import("qrcode");
4039
+ const qrText = await QRCode.toString(pairingUrl, {
4040
+ type: "terminal",
4041
+ small: true
4042
+ });
4043
+ console.log(qrText);
4044
+ console.log("Master secret (for pairing WebUI/Tauri devices):");
4045
+ console.log(` ${credentials.masterSecret}`);
4046
+ console.log(`
4047
+ Scan the QR code with your phone, or paste the secret into WebUI Settings > E2EE > Pair.`);
4048
+ });
4049
+ e2eeCmd.command("status").description("Show E2EE key status").action(async () => {
4050
+ const { initCrypto: initCrypto3, deriveAuthKeyPair: deriveAuthKeyPair3, deriveContentKeyPair: deriveContentKeyPair2, getSodium: getSodium4 } = await import("@mobvibe/shared");
4051
+ const credentials = await loadCredentials();
4052
+ if (!credentials) {
4053
+ console.log("Status: Not logged in");
4054
+ console.log("Run 'mobvibe login' to authenticate.");
4055
+ return;
4056
+ }
4057
+ await initCrypto3();
4058
+ const sodium = getSodium4();
4059
+ const masterSecret = sodium.from_base64(credentials.masterSecret, sodium.base64_variants.ORIGINAL);
4060
+ const authKp = deriveAuthKeyPair3(masterSecret);
4061
+ const contentKp = deriveContentKeyPair2(masterSecret);
4062
+ const authPub = sodium.to_base64(authKp.publicKey, sodium.base64_variants.ORIGINAL);
4063
+ const contentPub = sodium.to_base64(contentKp.publicKey, sodium.base64_variants.ORIGINAL);
4064
+ console.log("Status: E2EE enabled");
4065
+ console.log(`Auth public key: ${authPub.slice(0, 16)}...`);
4066
+ console.log(`Content public key: ${contentPub.slice(0, 16)}...`);
4067
+ console.log(`Saved: ${new Date(credentials.createdAt).toLocaleString()}`);
4068
+ });
4069
+ program.command("compact").description("Compact the WAL database to reclaim space").option("--session <id>", "Compact a specific session only").option("--dry-run", "Show what would be deleted without actually deleting").option("-v, --verbose", "Show detailed output").action(async (options) => {
4070
+ const config = await getCliConfig();
4071
+ if (!config.compaction.enabled && !options.dryRun) {
4072
+ console.log("Compaction is disabled in configuration.");
4073
+ console.log("Set MOBVIBE_COMPACTION_ENABLED=true to enable.");
4074
+ return;
4075
+ }
4076
+ const walStore = new WalStore(config.walDbPath);
4077
+ const db = new Database3(config.walDbPath);
4078
+ const compactor = new WalCompactor(walStore, config.compaction, db);
4079
+ console.log(options.dryRun ? "Dry run - no changes will be made" : "Starting compaction...");
4080
+ try {
4081
+ if (options.session) {
4082
+ const stats = await compactor.compactSession(options.session, {
4083
+ dryRun: options.dryRun
4084
+ });
4085
+ console.log(`Session ${options.session}:`);
4086
+ console.log(` Acked events deleted: ${stats.ackedEventsDeleted}`);
4087
+ console.log(` Old revisions deleted: ${stats.oldRevisionsDeleted}`);
4088
+ console.log(` Duration: ${stats.durationMs.toFixed(2)}ms`);
4089
+ } else {
4090
+ const result = await compactor.compactAll({
4091
+ dryRun: options.dryRun
4092
+ });
4093
+ if (options.verbose) {
4094
+ for (const stats of result.stats) {
4095
+ if (stats.ackedEventsDeleted > 0 || stats.oldRevisionsDeleted > 0) {
4096
+ console.log(`
4097
+ Session ${stats.sessionId}:`);
4098
+ console.log(` Acked events deleted: ${stats.ackedEventsDeleted}`);
4099
+ console.log(` Old revisions deleted: ${stats.oldRevisionsDeleted}`);
4100
+ }
4101
+ }
4102
+ if (result.skipped.length > 0) {
4103
+ console.log(`
4104
+ Skipped (active sessions): ${result.skipped.join(", ")}`);
4105
+ }
4106
+ }
4107
+ const totalDeleted = result.stats.reduce((sum, s) => sum + s.ackedEventsDeleted + s.oldRevisionsDeleted, 0);
4108
+ console.log(`
4109
+ Summary:`);
4110
+ console.log(` Sessions processed: ${result.stats.length}`);
4111
+ console.log(` Sessions skipped: ${result.skipped.length}`);
4112
+ console.log(` Total events ${options.dryRun ? "to delete" : "deleted"}: ${totalDeleted}`);
4113
+ console.log(` Duration: ${result.totalDurationMs.toFixed(2)}ms`);
4114
+ }
4115
+ } finally {
4116
+ walStore.close();
4117
+ db.close();
4118
+ }
4119
+ });
3010
4120
  async function run() {
3011
4121
  await program.parseAsync(process.argv);
3012
4122
  }
@@ -3018,4 +4128,5 @@ run().catch((error) => {
3018
4128
  export {
3019
4129
  run
3020
4130
  };
3021
- //# sourceMappingURL=index.js.map
4131
+
4132
+ //# debugId=3BEAA052FAC1A9B764756E2164756E21