memax-cli 0.1.0-alpha.35 → 0.1.0-alpha.37

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.
@@ -1,3 +1,4 @@
1
+ import type { AgentSession, SessionSyncPlanAction } from "memax-sdk";
1
2
  interface AgentSessionLocation {
2
3
  agent: string;
3
4
  path: string;
@@ -17,6 +18,13 @@ interface SessionProjectContext {
17
18
  projectRoot?: string;
18
19
  }
19
20
  export declare function hashPortableSessionContent(agent: string, content: Buffer, projectRoot?: string): string;
21
+ export declare function computeSessionSyncHash(agent: string, scope: string, content: Buffer, currentProjectRootPath: string): string;
22
+ export declare function isLegacyGlobalSessionShadowed(action: SessionSyncPlanAction, projectScopedKeys: Set<string>): boolean;
23
+ interface ShadowedGlobalSessionPair {
24
+ global: AgentSession;
25
+ project: AgentSession;
26
+ }
27
+ export declare function findShadowedGlobalSessions(sessions: AgentSession[]): ShadowedGlobalSessionPair[];
20
28
  export declare function computeSessionProjectContext(agent: string, path: string, fallbackProjectRoot?: string | null): SessionProjectContext;
21
29
  export declare function materializeAgentSessionContent(agent: string, content: Buffer, options: {
22
30
  scope: string;
@@ -28,7 +36,12 @@ export declare function syncAgentSessionsCommand(options?: {
28
36
  pull?: boolean;
29
37
  }): Promise<void>;
30
38
  export declare function listAgentSessionsCommand(): Promise<void>;
39
+ export declare function listDeletedAgentSessionsCommand(): Promise<void>;
40
+ export declare function restoreDeletedAgentSessionsCommand(): Promise<void>;
31
41
  export declare function deleteAgentSessionsCommand(): Promise<void>;
42
+ export declare function cleanupAgentSessionsCommand(options?: {
43
+ yes?: boolean;
44
+ }): Promise<void>;
32
45
  export declare function doctorAgentSessionsCommand(): Promise<void>;
33
46
  export declare function resolveAgentSessionWritePath(agent: string, filePath: string, scope: string, options?: {
34
47
  cwd?: string;
@@ -1 +1 @@
1
- {"version":3,"file":"agent-sessions.d.ts","sourceRoot":"","sources":["../../src/commands/agent-sessions.ts"],"names":[],"mappings":"AA8BA,UAAU,oBAAoB;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,UAAU,qBAAqB;IAC7B,IAAI,EAAE,SAAS,GAAG,YAAY,GAAG,mBAAmB,GAAG,YAAY,CAAC;IACpE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAID,UAAU,qBAAqB;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AA6GD,wBAAgB,0BAA0B,CACxC,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,MAAM,EACf,WAAW,CAAC,EAAE,MAAM,GACnB,MAAM,CAGR;AAoDD,wBAAgB,4BAA4B,CAC1C,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,MAAM,EACZ,mBAAmB,CAAC,EAAE,MAAM,GAAG,IAAI,GAClC,qBAAqB,CAYvB;AAmBD,wBAAgB,8BAA8B,CAC5C,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,MAAM,EACf,OAAO,EAAE;IACP,KAAK,EAAE,MAAM,CAAC;IACd,sBAAsB,EAAE,MAAM,CAAC;IAC/B,SAAS,EAAE,MAAM,CAAC;CACnB,GACA,MAAM,CAyBR;AAED,wBAAsB,wBAAwB,CAC5C,OAAO,GAAE;IACP,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,IAAI,CAAC,EAAE,OAAO,CAAC;CACX,GACL,OAAO,CAAC,IAAI,CAAC,CA8Zf;AAED,wBAAsB,wBAAwB,IAAI,OAAO,CAAC,IAAI,CAAC,CA0B9D;AAED,wBAAsB,0BAA0B,IAAI,OAAO,CAAC,IAAI,CAAC,CAmHhE;AAED,wBAAsB,0BAA0B,IAAI,OAAO,CAAC,IAAI,CAAC,CA8EhE;AAsID,wBAAgB,4BAA4B,CAC1C,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,EACb,OAAO,GAAE;IACP,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,sBAAsB,CAAC,EAAE,MAAM,CAAC;CAC5B,GACL,MAAM,GAAG,IAAI,CAiEf;AAED,wBAAgB,6BAA6B,CAC3C,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,EACb,OAAO,GAAE;IACP,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,UAAU,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,oBAAoB,CAAC,CAAC;CAC3C,GACL,qBAAqB,CAuBvB"}
1
+ {"version":3,"file":"agent-sessions.d.ts","sourceRoot":"","sources":["../../src/commands/agent-sessions.ts"],"names":[],"mappings":"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"}
@@ -1,13 +1,15 @@
1
1
  import chalk from "chalk";
2
2
  import { createHash } from "node:crypto";
3
- import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync, } from "node:fs";
3
+ import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync, } from "node:fs";
4
4
  import { basename, dirname, join, relative } from "node:path";
5
5
  import { homedir } from "node:os";
6
6
  import { getClient } from "../lib/client.js";
7
7
  import { getProjectScope, resolveClaudeProjectPath, resolveClaudeProjectFolder, resolveProjectRootPath, resolveProjectScope, normalizeFilePath, } from "../lib/project-context.js";
8
8
  import { getOrCreateDeviceID, loadConfig } from "../lib/config.js";
9
9
  import { ask, confirmDefault } from "../lib/prompt.js";
10
+ import { moveFileToTrash } from "../lib/trash.js";
10
11
  const PROJECT_ROOT_PLACEHOLDER = "__MEMAX_PROJECT_ROOT__";
12
+ const PORTABLE_SESSION_AGENTS = new Set(["claude-code", "codex", "gemini"]);
11
13
  function isWithinRoot(candidate, root) {
12
14
  return candidate === root || candidate.startsWith(`${root}/`);
13
15
  }
@@ -93,6 +95,61 @@ export function hashPortableSessionContent(agent, content, projectRoot) {
93
95
  const canonical = canonicalizeSessionContent(agent, content, projectRoot);
94
96
  return createHash("sha256").update(canonical).digest("hex");
95
97
  }
98
+ export function computeSessionSyncHash(agent, scope, content, currentProjectRootPath) {
99
+ return hashPortableSessionContent(agent, content, scope.startsWith("project:") ? currentProjectRootPath : undefined);
100
+ }
101
+ export function isLegacyGlobalSessionShadowed(action, projectScopedKeys) {
102
+ if (action.scope !== "global")
103
+ return false;
104
+ if (action.reason !== "cloud_only" &&
105
+ action.reason !== "deleted_everywhere") {
106
+ return false;
107
+ }
108
+ if (!PORTABLE_SESSION_AGENTS.has(action.agent))
109
+ return false;
110
+ if (action.agent === "codex" &&
111
+ normalizeFilePath(action.file_path) === "history.jsonl") {
112
+ return false;
113
+ }
114
+ return projectScopedKeys.has(`${action.agent}|${normalizeFilePath(action.file_path)}`);
115
+ }
116
+ export function findShadowedGlobalSessions(sessions) {
117
+ const projectScopedByKey = new Map();
118
+ for (const session of sessions) {
119
+ if (!PORTABLE_SESSION_AGENTS.has(session.agent))
120
+ continue;
121
+ if (!session.scope.startsWith("project:"))
122
+ continue;
123
+ const normalized = normalizeFilePath(session.file_path);
124
+ if (session.agent === "codex" && normalized === "history.jsonl")
125
+ continue;
126
+ const key = `${session.agent}|${normalized}`;
127
+ const group = projectScopedByKey.get(key) ?? [];
128
+ group.push(session);
129
+ projectScopedByKey.set(key, group);
130
+ }
131
+ const pairs = [];
132
+ for (const session of sessions) {
133
+ if (!PORTABLE_SESSION_AGENTS.has(session.agent))
134
+ continue;
135
+ if (session.scope !== "global")
136
+ continue;
137
+ const normalized = normalizeFilePath(session.file_path);
138
+ if (session.agent === "codex" && normalized === "history.jsonl")
139
+ continue;
140
+ const siblings = projectScopedByKey.get(`${session.agent}|${normalized}`);
141
+ if (!siblings)
142
+ continue;
143
+ for (const sibling of siblings) {
144
+ pairs.push({ global: session, project: sibling });
145
+ }
146
+ }
147
+ return pairs;
148
+ }
149
+ function computeDownloadedPortableHash(agent, content) {
150
+ const projectRoot = readStructuredSessionRootFromContent(agent, content);
151
+ return hashPortableSessionContent(agent, content, projectRoot ?? undefined);
152
+ }
96
153
  function readStructuredSessionRootFromContent(agent, content) {
97
154
  try {
98
155
  const raw = content.toString("utf-8");
@@ -232,6 +289,10 @@ export async function syncAgentSessionsCommand(options = {}) {
232
289
  return false;
233
290
  return true;
234
291
  });
292
+ const projectScopedKeys = new Set(actions
293
+ .filter((action) => action.scope.startsWith("project:"))
294
+ .map((action) => `${action.agent}|${normalizeFilePath(action.file_path)}`));
295
+ actions = actions.filter((action) => !isLegacyGlobalSessionShadowed(action, projectScopedKeys));
235
296
  const localByKey = new Map();
236
297
  for (const session of localSessions) {
237
298
  localByKey.set(`${session.loc.agent}|${session.loc.filePath}|${session.loc.scope}`, session);
@@ -254,6 +315,7 @@ export async function syncAgentSessionsCommand(options = {}) {
254
315
  let pushed = 0;
255
316
  let pulled = 0;
256
317
  let deletedLocal = 0;
318
+ let reconciled = 0;
257
319
  let unchanged = 0;
258
320
  let skipped = 0;
259
321
  let errors = 0;
@@ -343,17 +405,18 @@ export async function syncAgentSessionsCommand(options = {}) {
343
405
  const session = await getClient().agentSessions.get(action.session_id);
344
406
  const bytes = await downloadAgentSession(action.session_id);
345
407
  mkdirSync(dirname(writePath), { recursive: true });
346
- writeFileSync(writePath, materializeAgentSessionContent(action.agent, bytes, {
408
+ const materialized = materializeAgentSessionContent(action.agent, bytes, {
347
409
  scope: action.scope,
348
410
  currentProjectRootPath,
349
411
  writePath,
350
- }));
412
+ });
413
+ writeFileSync(writePath, materialized);
351
414
  if (action.version) {
352
415
  ackSessions.push({
353
416
  agent: action.agent,
354
417
  file_path: action.file_path,
355
418
  scope: action.scope,
356
- content_hash: session.content_hash,
419
+ content_hash: computeSessionSyncHash(action.agent, action.scope, materialized, currentProjectRootPath),
357
420
  version: action.version,
358
421
  local_path: writePath,
359
422
  });
@@ -368,10 +431,65 @@ export async function syncAgentSessionsCommand(options = {}) {
368
431
  continue;
369
432
  }
370
433
  if (action.action === "delete_local") {
434
+ if (isLegacyGlobalSessionShadowed(action, projectScopedKeys)) {
435
+ if (action.version) {
436
+ ackSessions.push({
437
+ agent: action.agent,
438
+ file_path: action.file_path,
439
+ scope: action.scope,
440
+ version: action.version,
441
+ deleted: true,
442
+ });
443
+ }
444
+ console.log(chalk.gray(` = ${action.file_path}`), chalk.gray("legacy global duplicate removed"));
445
+ reconciled++;
446
+ continue;
447
+ }
448
+ if (!options.pull) {
449
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
450
+ console.log(chalk.yellow(` - ${action.file_path}`), chalk.gray("cloud deleted this session artifact — skipped in non-interactive mode"));
451
+ skipped++;
452
+ continue;
453
+ }
454
+ const resolution = await promptSessionCloudDeletion(action.file_path);
455
+ if (resolution === "skip") {
456
+ console.log(chalk.gray(` - ${action.file_path}`), chalk.gray("skipped"));
457
+ skipped++;
458
+ continue;
459
+ }
460
+ if (resolution === "local") {
461
+ const local = localByKey.get(key);
462
+ if (!local) {
463
+ console.log(chalk.yellow(` - ${action.file_path}`), chalk.gray("local file missing — skipped"));
464
+ skipped++;
465
+ continue;
466
+ }
467
+ try {
468
+ const fileRef = await uploadLocalFile(local.loc.path, local.content);
469
+ await getClient().agentSessions.upsert({
470
+ agent: action.agent,
471
+ file_path: action.file_path,
472
+ scope: action.scope,
473
+ session_type: local.loc.sessionType,
474
+ content_hash: local.hash,
475
+ device_id: deviceID,
476
+ local_path: local.loc.path,
477
+ file_ref: fileRef,
478
+ });
479
+ console.log(chalk.green(` ↑ ${action.file_path}`), chalk.gray("kept local and restored to cloud"));
480
+ pushed++;
481
+ }
482
+ catch (err) {
483
+ console.log(chalk.red(` ✗ ${action.file_path}`), chalk.gray(err.message));
484
+ errors++;
485
+ }
486
+ continue;
487
+ }
488
+ }
371
489
  try {
372
490
  const writePath = resolveWritePath(action.agent, action.file_path, action.scope);
373
491
  if (writePath && existsSync(writePath)) {
374
- rmSync(writePath);
492
+ moveFileToTrash(writePath, "agent-sessions");
375
493
  }
376
494
  if (action.version) {
377
495
  ackSessions.push({
@@ -383,7 +501,7 @@ export async function syncAgentSessionsCommand(options = {}) {
383
501
  deleted: true,
384
502
  });
385
503
  }
386
- console.log(chalk.yellow(` - ${action.file_path}`), chalk.gray("deleted locally (cloud removed everywhere)"));
504
+ console.log(chalk.yellow(` - ${action.file_path}`), chalk.gray("deleted locally (moved to Memax trash)"));
387
505
  deletedLocal++;
388
506
  }
389
507
  catch (err) {
@@ -392,6 +510,11 @@ export async function syncAgentSessionsCommand(options = {}) {
392
510
  }
393
511
  continue;
394
512
  }
513
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
514
+ console.log(chalk.yellow(` - ${action.file_path}`), chalk.gray("conflict skipped in non-interactive mode"));
515
+ skipped++;
516
+ continue;
517
+ }
395
518
  const resolution = await promptSessionConflict(action.file_path);
396
519
  if (resolution === "local") {
397
520
  const local = localByKey.get(key);
@@ -429,17 +552,18 @@ export async function syncAgentSessionsCommand(options = {}) {
429
552
  const session = await getClient().agentSessions.get(action.session_id);
430
553
  const bytes = await downloadAgentSession(action.session_id);
431
554
  mkdirSync(dirname(writePath), { recursive: true });
432
- writeFileSync(writePath, materializeAgentSessionContent(action.agent, bytes, {
555
+ const materialized = materializeAgentSessionContent(action.agent, bytes, {
433
556
  scope: action.scope,
434
557
  currentProjectRootPath,
435
558
  writePath,
436
- }));
559
+ });
560
+ writeFileSync(writePath, materialized);
437
561
  if (action.version) {
438
562
  ackSessions.push({
439
563
  agent: action.agent,
440
564
  file_path: action.file_path,
441
565
  scope: action.scope,
442
- content_hash: session.content_hash,
566
+ content_hash: computeSessionSyncHash(action.agent, action.scope, materialized, currentProjectRootPath),
443
567
  version: action.version,
444
568
  local_path: writePath,
445
569
  });
@@ -476,6 +600,8 @@ export async function syncAgentSessionsCommand(options = {}) {
476
600
  parts.push(`${pulled} restored`);
477
601
  if (deletedLocal > 0)
478
602
  parts.push(`${deletedLocal} deleted locally`);
603
+ if (reconciled > 0)
604
+ parts.push(`${reconciled} reconciled`);
479
605
  if (unchanged > 0)
480
606
  parts.push(`${unchanged} unchanged`);
481
607
  if (skipped > 0)
@@ -510,6 +636,120 @@ export async function listAgentSessionsCommand() {
510
636
  console.error(chalk.red(` Failed to fetch session artifacts: ${err.message}\n`));
511
637
  }
512
638
  }
639
+ export async function listDeletedAgentSessionsCommand() {
640
+ try {
641
+ const result = await getClient().agentSessions.listDeleted();
642
+ const sessions = result.sessions;
643
+ if (sessions.length === 0) {
644
+ console.log(chalk.gray(" No recoverable deleted session artifacts.\n"));
645
+ return;
646
+ }
647
+ console.log(chalk.bold("\n Recoverable Deleted Session Artifacts\n"));
648
+ for (const [index, session] of sessions.entries()) {
649
+ const scopeTag = session.scope === "global"
650
+ ? chalk.dim("global")
651
+ : chalk.dim(session.scope.replace(/^project:/, ""));
652
+ console.log(` ${chalk.bold(String(index + 1).padStart(2, " "))}. ${chalk.cyan(formatAgentName(session.agent))} ${session.file_path} ${scopeTag}`);
653
+ console.log(chalk.gray(` deleted ${formatAge(session.deleted_at)} · recoverable until ${session.content_expires_at ? new Date(session.content_expires_at).toLocaleString() : "expired"}`));
654
+ }
655
+ console.log();
656
+ }
657
+ catch (err) {
658
+ console.error(chalk.red(` Failed to fetch deleted session artifacts: ${err.message}\n`));
659
+ }
660
+ }
661
+ export async function restoreDeletedAgentSessionsCommand() {
662
+ let deleted;
663
+ try {
664
+ deleted = await getClient().agentSessions.listDeleted();
665
+ }
666
+ catch (err) {
667
+ console.error(chalk.red(` Failed to fetch deleted session artifacts: ${err.message}\n`));
668
+ return;
669
+ }
670
+ if (deleted.sessions.length === 0) {
671
+ console.log(chalk.gray(" No recoverable deleted session artifacts.\n"));
672
+ return;
673
+ }
674
+ console.log(chalk.bold("\n Recover Deleted Session Artifacts\n"));
675
+ deleted.sessions.forEach((session, index) => {
676
+ const scopeTag = session.scope === "global"
677
+ ? chalk.dim("global")
678
+ : chalk.dim(session.scope.replace(/^project:/, ""));
679
+ console.log(` ${chalk.dim(`${index + 1}.`)} ${chalk.cyan(formatAgentName(session.agent))} ${session.file_path} ${scopeTag}`);
680
+ });
681
+ console.log();
682
+ const answer = await ask(" Select session artifacts to restore (comma-separated numbers, or 'q' to quit): ");
683
+ if (!answer || answer.trim().toLowerCase() === "q") {
684
+ console.log(chalk.gray(" Cancelled.\n"));
685
+ return;
686
+ }
687
+ const indexes = answer
688
+ .split(",")
689
+ .map((part) => Number.parseInt(part.trim(), 10))
690
+ .filter((idx) => Number.isInteger(idx) && idx >= 1 && idx <= deleted.sessions.length);
691
+ if (indexes.length === 0) {
692
+ console.log(chalk.gray(" No valid selection.\n"));
693
+ return;
694
+ }
695
+ const cwd = process.cwd();
696
+ const deviceID = getOrCreateDeviceID();
697
+ const currentProjectScope = getProjectScope(cwd);
698
+ const currentProjectRootPath = resolveProjectRootPath(cwd) ?? cwd;
699
+ let restored = 0;
700
+ for (const index of indexes) {
701
+ const sessionInfo = deleted.sessions[index - 1];
702
+ try {
703
+ const writePath = resolveAgentSessionWritePath(sessionInfo.agent, sessionInfo.file_path, sessionInfo.scope, {
704
+ cwd,
705
+ home: homedir(),
706
+ currentProjectScope,
707
+ });
708
+ const session = await getClient().agentSessions.restore({
709
+ agent: sessionInfo.agent,
710
+ file_path: sessionInfo.file_path,
711
+ scope: sessionInfo.scope,
712
+ device_id: deviceID,
713
+ local_path: writePath ?? undefined,
714
+ });
715
+ if (writePath && !existsSync(writePath)) {
716
+ const bytes = await downloadAgentSession(session.id);
717
+ mkdirSync(dirname(writePath), { recursive: true });
718
+ const materialized = materializeAgentSessionContent(session.agent, bytes, {
719
+ scope: session.scope,
720
+ currentProjectRootPath,
721
+ writePath,
722
+ });
723
+ writeFileSync(writePath, materialized);
724
+ await getClient().agentSessions.ack({
725
+ device_id: deviceID,
726
+ sessions: [
727
+ {
728
+ agent: session.agent,
729
+ file_path: session.file_path,
730
+ scope: session.scope,
731
+ content_hash: computeSessionSyncHash(session.agent, session.scope, materialized, currentProjectRootPath),
732
+ version: session.version,
733
+ local_path: writePath,
734
+ },
735
+ ],
736
+ });
737
+ console.log(chalk.green(` ✓ ${session.file_path}`), chalk.gray("restored to cloud and local machine"));
738
+ }
739
+ else if (writePath && existsSync(writePath)) {
740
+ console.log(chalk.yellow(` - ${session.file_path}`), chalk.gray("restored to cloud; local file already exists"));
741
+ }
742
+ else {
743
+ console.log(chalk.yellow(` - ${session.file_path}`), chalk.gray("restored to cloud; no safe local path on this machine"));
744
+ }
745
+ restored++;
746
+ }
747
+ catch (err) {
748
+ console.log(chalk.red(` ✗ ${sessionInfo.file_path}`), chalk.gray(err.message));
749
+ }
750
+ }
751
+ console.log(chalk.gray(`\n ${restored} session artifact${restored === 1 ? "" : "s"} restored.\n`));
752
+ }
513
753
  export async function deleteAgentSessionsCommand() {
514
754
  let sessions;
515
755
  try {
@@ -566,13 +806,13 @@ export async function deleteAgentSessionsCommand() {
566
806
  local_path: localPath ?? undefined,
567
807
  });
568
808
  if (localPath && existsSync(localPath)) {
569
- rmSync(localPath);
809
+ moveFileToTrash(localPath, "agent-sessions");
570
810
  }
571
811
  }
572
812
  else {
573
813
  await getClient().agentSessions.delete(session.id);
574
814
  if (localPath && existsSync(localPath)) {
575
- rmSync(localPath);
815
+ moveFileToTrash(localPath, "agent-sessions");
576
816
  }
577
817
  await getClient().agentSessions.ack({
578
818
  device_id: deviceID,
@@ -596,6 +836,94 @@ export async function deleteAgentSessionsCommand() {
596
836
  }
597
837
  console.log();
598
838
  }
839
+ export async function cleanupAgentSessionsCommand(options) {
840
+ let sessions;
841
+ try {
842
+ sessions = (await getClient().agentSessions.list()).sessions;
843
+ }
844
+ catch (err) {
845
+ console.error(chalk.red(` Failed to fetch session artifacts: ${err.message}\n`));
846
+ return;
847
+ }
848
+ const pairs = findShadowedGlobalSessions(sessions);
849
+ if (pairs.length === 0) {
850
+ console.log(chalk.gray(" No legacy global session duplicates found.\n"));
851
+ return;
852
+ }
853
+ const safePairs = [];
854
+ const divergedPairs = [];
855
+ for (const pair of pairs) {
856
+ if (pair.global.content_hash === pair.project.content_hash) {
857
+ safePairs.push(pair);
858
+ continue;
859
+ }
860
+ try {
861
+ const [globalBytes, projectBytes] = await Promise.all([
862
+ downloadAgentSession(pair.global.id),
863
+ downloadAgentSession(pair.project.id),
864
+ ]);
865
+ if (computeDownloadedPortableHash(pair.global.agent, globalBytes) ===
866
+ computeDownloadedPortableHash(pair.project.agent, projectBytes)) {
867
+ safePairs.push(pair);
868
+ }
869
+ else {
870
+ divergedPairs.push(pair);
871
+ }
872
+ }
873
+ catch {
874
+ divergedPairs.push(pair);
875
+ }
876
+ }
877
+ console.log(chalk.bold("\n Session Duplicate Cleanup\n"));
878
+ if (safePairs.length > 0) {
879
+ console.log(chalk.white(" Safe To Remove"));
880
+ for (const pair of safePairs) {
881
+ console.log(` ${chalk.cyan(formatAgentName(pair.global.agent))} ${pair.global.file_path}`);
882
+ console.log(` ${chalk.gray("delete legacy global copy; project-scoped copy remains")}`);
883
+ }
884
+ console.log();
885
+ }
886
+ if (divergedPairs.length > 0) {
887
+ console.log(chalk.yellow(" Needs Manual Review"));
888
+ for (const pair of divergedPairs) {
889
+ console.log(` ${chalk.yellow(formatAgentName(pair.global.agent))} ${pair.global.file_path}`);
890
+ console.log(` ${chalk.gray(`global hash ${pair.global.content_hash.slice(0, 8)}… differs from project ${pair.project.scope.replace(/^project:/, "")} hash ${pair.project.content_hash.slice(0, 8)}…`)}`);
891
+ }
892
+ console.log();
893
+ }
894
+ if (safePairs.length === 0) {
895
+ console.log(chalk.gray(" No identical legacy global duplicates can be removed safely.\n"));
896
+ return;
897
+ }
898
+ if (!options?.yes) {
899
+ const proceed = await confirmDefault(` Delete ${safePairs.length} safe global duplicate${safePairs.length === 1 ? "" : "s"} from cloud? [Y/n] `);
900
+ if (!proceed) {
901
+ console.log(chalk.gray(" Cancelled.\n"));
902
+ return;
903
+ }
904
+ }
905
+ let deleted = 0;
906
+ let errors = 0;
907
+ for (const pair of safePairs) {
908
+ try {
909
+ await getClient().agentSessions.delete(pair.global.id);
910
+ console.log(chalk.green(` ✓ ${pair.global.file_path}`), chalk.gray("deleted legacy global copy"));
911
+ deleted++;
912
+ }
913
+ catch (err) {
914
+ console.log(chalk.red(` ✗ ${pair.global.file_path}`), chalk.gray(err.message));
915
+ errors++;
916
+ }
917
+ }
918
+ const summary = [];
919
+ if (deleted > 0)
920
+ summary.push(`${deleted} deleted`);
921
+ if (divergedPairs.length > 0)
922
+ summary.push(`${divergedPairs.length} need review`);
923
+ if (errors > 0)
924
+ summary.push(`${errors} errors`);
925
+ console.log(chalk.bold(`\n Done: ${summary.join(", ")}\n`));
926
+ }
599
927
  export async function doctorAgentSessionsCommand() {
600
928
  const cwd = process.cwd();
601
929
  const project = resolveProjectScope(cwd);
@@ -854,6 +1182,15 @@ async function promptSessionConflict(filePath) {
854
1182
  return "cloud";
855
1183
  return "skip";
856
1184
  }
1185
+ async function promptSessionCloudDeletion(filePath) {
1186
+ const answer = await ask(` ${filePath} was deleted in cloud. [d]elete local, [k]eep local and restore cloud, or [s]kip? `);
1187
+ const normalized = answer.trim().toLowerCase();
1188
+ if (normalized === "d")
1189
+ return "delete";
1190
+ if (normalized === "k")
1191
+ return "local";
1192
+ return "skip";
1193
+ }
857
1194
  function readProjectRootMarker(path) {
858
1195
  if (!existsSync(path))
859
1196
  return null;
@@ -933,6 +1270,17 @@ function formatBytes(size) {
933
1270
  return `${(size / 1024).toFixed(1)} KB`;
934
1271
  return `${(size / (1024 * 1024)).toFixed(1)} MB`;
935
1272
  }
1273
+ function formatAge(dateStr) {
1274
+ const ms = Date.now() - new Date(dateStr).getTime();
1275
+ const mins = Math.floor(ms / 60000);
1276
+ if (mins < 60)
1277
+ return `${mins}m ago`;
1278
+ const hours = Math.floor(mins / 60);
1279
+ if (hours < 24)
1280
+ return `${hours}h ago`;
1281
+ const days = Math.floor(hours / 24);
1282
+ return `${days}d ago`;
1283
+ }
936
1284
  function basenameSafe(path) {
937
1285
  return normalizeFilePath(path).split("/").pop() ?? "artifact";
938
1286
  }