@memnexus-ai/cli 1.7.218 → 1.7.220

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.
@@ -36,7 +36,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
36
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
- exports.StdinTooLargeError = exports.BATCH_CREATE_ALLOWED_KEYS = exports.BATCH_CREATE_MAX_LINE_BYTES = exports.BATCH_CREATE_MAX_STDIN_BYTES = exports.BATCH_CREATE_MAX_RETRIES = exports.BATCH_CREATE_CONCURRENCY_MAX = exports.BATCH_CREATE_CONCURRENCY_MIN = exports.BATCH_CREATE_DEFAULT_CONCURRENCY = exports.BULK_DELETE_LIST_PAGE_SIZE = exports.BULK_DELETE_MAX_RETRIES = exports.BULK_DELETE_PAGE_PAUSE_MS = exports.BULK_DELETE_MAX_IDS = exports.BULK_DELETE_GUARD_WINDOW_MS = exports.BULK_DELETE_GUARD_THRESHOLD = void 0;
39
+ exports.StdinTooLargeError = exports.BATCH_CREATE_ALLOWED_KEYS = exports.BATCH_CREATE_MAX_LINE_BYTES = exports.BATCH_CREATE_MAX_STDIN_BYTES = exports.BATCH_CREATE_MAX_RETRIES = exports.BATCH_CREATE_CONCURRENCY_MAX = exports.BATCH_CREATE_CONCURRENCY_MIN = exports.BATCH_CREATE_DEFAULT_CONCURRENCY = exports.sleep = exports.getRetryAfterMs = exports.isQuotaError = exports.isServiceUnavailableError = exports.isRateLimitError = exports.BULK_DELETE_LIST_PAGE_SIZE = exports.BULK_DELETE_MAX_RETRIES = exports.BULK_DELETE_PAGE_PAUSE_MS = exports.BULK_DELETE_MAX_IDS = exports.BULK_DELETE_GUARD_WINDOW_MS = exports.BULK_DELETE_GUARD_THRESHOLD = void 0;
40
40
  exports.getBulkDeleteGuardPath = getBulkDeleteGuardPath;
41
41
  exports.readDeleteGuardTimestamps = readDeleteGuardTimestamps;
42
42
  exports.recordDeletion = recordDeletion;
@@ -45,15 +45,10 @@ exports.parseCodeContextFlag = parseCodeContextFlag;
45
45
  exports.parseBatchIds = parseBatchIds;
46
46
  exports.isValidIsoDate = isValidIsoDate;
47
47
  exports.chunkIds = chunkIds;
48
- exports.isRateLimitError = isRateLimitError;
49
- exports.getRetryAfterMs = getRetryAfterMs;
50
- exports.sleep = sleep;
51
48
  exports.bulkDeleteWithRetry = bulkDeleteWithRetry;
52
49
  exports.runBulkDeletePaged = runBulkDeletePaged;
53
50
  exports.collectAllMemoryIds = collectAllMemoryIds;
54
- exports.isServiceUnavailableError = isServiceUnavailableError;
55
51
  exports.isRetryableCreateError = isRetryableCreateError;
56
- exports.isQuotaError = isQuotaError;
57
52
  exports.parseJsonlCreateLine = parseJsonlCreateLine;
58
53
  exports.resolveCreateConcurrency = resolveCreateConcurrency;
59
54
  exports.batchCreateWithRetry = batchCreateWithRetry;
@@ -74,6 +69,12 @@ const chalk_1 = __importDefault(require("chalk"));
74
69
  const api_client_1 = require("../lib/api-client");
75
70
  const config_1 = require("../lib/config");
76
71
  const errors_1 = require("../lib/errors");
72
+ const retry_1 = require("../lib/retry");
73
+ Object.defineProperty(exports, "isRateLimitError", { enumerable: true, get: function () { return retry_1.isRateLimitError; } });
74
+ Object.defineProperty(exports, "isServiceUnavailableError", { enumerable: true, get: function () { return retry_1.isServiceUnavailableError; } });
75
+ Object.defineProperty(exports, "isQuotaError", { enumerable: true, get: function () { return retry_1.isQuotaError; } });
76
+ Object.defineProperty(exports, "getRetryAfterMs", { enumerable: true, get: function () { return retry_1.getRetryAfterMs; } });
77
+ Object.defineProperty(exports, "sleep", { enumerable: true, get: function () { return retry_1.sleep; } });
77
78
  const formatters_1 = require("../lib/formatters");
78
79
  const js_yaml_1 = __importDefault(require("js-yaml"));
79
80
  const spinner_1 = require("../lib/spinner");
@@ -270,11 +271,11 @@ async function resolveShortIds(ids, opts = {}) {
270
271
  }
271
272
  else {
272
273
  // Resolve short ID
273
- const listResponse = await (0, api_client_1.getSdk)().memories.listMemories({
274
+ const listResponse = await retryRead(() => (0, api_client_1.getSdk)().memories.listMemories({
274
275
  limit: 10,
275
276
  offset: 0,
276
277
  idPrefix: id,
277
- });
278
+ }));
278
279
  const matches = listResponse.data?.data || [];
279
280
  if (matches.length === 0) {
280
281
  warn(`Warning: No memory found with prefix "${id}"`);
@@ -392,10 +393,10 @@ async function handleBatchMemoryGet(ids, options) {
392
393
  }
393
394
  spinner.text = `Fetching ${resolvedIds.length} memories...`;
394
395
  const batchDetail = options.detail || 'minimal';
395
- const response = await (0, api_client_1.getSdk)().memories.getMemoriesBatch({
396
+ const response = await retryRead(() => (0, api_client_1.getSdk)().memories.getMemoriesBatch({
396
397
  ids: resolvedIds,
397
398
  detail: batchDetail,
398
- });
399
+ }));
399
400
  const result = response.data;
400
401
  if (result && result.meta && result.meta.missing && result.meta.missing.length > 0) {
401
402
  spinner.warn(`Found ${result.meta.found} of ${result.meta.requested} memories`);
@@ -506,42 +507,46 @@ function chunkIds(ids, size = exports.BULK_DELETE_MAX_IDS) {
506
507
  }
507
508
  return chunks;
508
509
  }
509
- /** True when an error is an HTTP 429 (rate limited). */
510
- function isRateLimitError(error) {
511
- const status = error?.status ?? error?.statusCode ?? error?.response?.status;
512
- return status === 429;
510
+ // ── Single-shot retry wiring (PO CLI-latency directive, item 3) ──────────────
511
+ // These thin wrappers apply the shared withSingleShotRetry helper at the plain
512
+ // single-request SDK call sites. The idempotency flag is passed PER CALL SITE —
513
+ // that is the crux of the retry matrix, and it cannot be inferred centrally
514
+ // because the same SDK method (createMemory) serves BOTH single create (retry)
515
+ // and the batch --stdin-jsonl path (already retries via batchCreateWithRetry).
516
+ // Wrapping per single-shot site is therefore the correct, non-double-wrapping
517
+ // chokepoint; the batch helpers are left untouched.
518
+ /** STDERR-only retry notice. Never touches stdout, so `--format json` stays pure. */
519
+ function emitRetryNotice(info) {
520
+ const reason = info.status === 429 ? 'rate limited' : `transient error ${info.status ?? ''}`.trim();
521
+ console.error(chalk_1.default.dim(`⏳ ${reason}; retry ${info.attempt}/${info.maxRetries} in ${(info.waitMs / 1000).toFixed(1)}s`));
513
522
  }
514
523
  /**
515
- * Extract the server's Retry-After hint in milliseconds, or null if absent.
516
- * The API surfaces it as a body field (`retryAfter`, seconds) on 429s
517
- * (see lib/errors.ts) and may also send a Retry-After header; we honor both.
524
+ * Wrap an idempotent single-shot read (GET search/get/list/recall) or a
525
+ * naturally idempotent write (DELETE — deleting an already-deleted id is a
526
+ * no-op). Retries 429 and transient 5xx (502/503/504/408). Do NOT use this for
527
+ * named-memory PUT: it is a version bump that is only idempotent when
528
+ * expectedHeadId is supplied, and the CLI/SDK never supply it — use retryWrite.
518
529
  */
519
- function getRetryAfterMs(error) {
520
- const data = error?.data ?? error?.response?.data ?? {};
521
- const secs = data?.retryAfter ?? data?.retry_after;
522
- if (typeof secs === 'number' && Number.isFinite(secs) && secs >= 0) {
523
- return Math.round(secs * 1000);
524
- }
525
- const headers = error?.headers;
526
- let headerVal;
527
- if (headers) {
528
- if (typeof headers.get === 'function') {
529
- headerVal = headers.get('retry-after') ?? headers.get('Retry-After');
530
- }
531
- else {
532
- headerVal = headers['retry-after'] ?? headers['Retry-After'];
533
- }
534
- }
535
- if (headerVal) {
536
- const n = parseInt(String(headerVal), 10);
537
- if (Number.isFinite(n) && n >= 0)
538
- return n * 1000;
539
- }
540
- return null;
530
+ function retryRead(fn) {
531
+ return (0, retry_1.withSingleShotRetry)(fn, {
532
+ idempotent: true,
533
+ sleep: retry_1.sleep,
534
+ maxRetries: retry_1.SINGLE_SHOT_MAX_RETRIES,
535
+ onRetry: emitRetryNotice,
536
+ });
541
537
  }
542
- /** Promise-based sleep. Extracted so tests can inject a no-op/spy. */
543
- function sleep(ms) {
544
- return new Promise((resolve) => setTimeout(resolve, ms));
538
+ /**
539
+ * Wrap a non-idempotent single-shot write (plain POST create, non-CAS update).
540
+ * Retries ONLY 429 (server rejected before processing — no duplicate risk);
541
+ * a transient 5xx is NOT auto-retried because the write may already have landed.
542
+ */
543
+ function retryWrite(fn) {
544
+ return (0, retry_1.withSingleShotRetry)(fn, {
545
+ idempotent: false,
546
+ sleep: retry_1.sleep,
547
+ maxRetries: retry_1.SINGLE_SHOT_MAX_RETRIES,
548
+ onRetry: emitRetryNotice,
549
+ });
545
550
  }
546
551
  /**
547
552
  * Delete a single page, retrying on 429 up to `maxRetries` times. Honors the
@@ -556,8 +561,8 @@ async function bulkDeleteWithRetry(ids, deps) {
556
561
  return await deps.deleteFn(ids);
557
562
  }
558
563
  catch (error) {
559
- if (isRateLimitError(error) && attempt < maxRetries) {
560
- const waitMs = getRetryAfterMs(error) ?? defaultRetryMs * (attempt + 1);
564
+ if ((0, retry_1.isRateLimitError)(error) && attempt < maxRetries) {
565
+ const waitMs = (0, retry_1.getRetryAfterMs)(error) ?? defaultRetryMs * (attempt + 1);
561
566
  deps.onRateLimited?.(waitMs);
562
567
  await deps.sleep(waitMs);
563
568
  attempt++;
@@ -737,7 +742,7 @@ async function handleBatchDelete(rawIds, options, mode) {
737
742
  try {
738
743
  const result = await runBulkDeletePaged(resolvedIds, {
739
744
  deleteFn: bulkDeleteViaSdk,
740
- sleep,
745
+ sleep: retry_1.sleep,
741
746
  onPage: (i, total) => {
742
747
  if (total > 1 && !jsonMode) {
743
748
  spinner.text = `Deleting page ${i + 1} of ${total}...`;
@@ -841,19 +846,9 @@ exports.BATCH_CREATE_ALLOWED_KEYS = new Set([
841
846
  'outcome',
842
847
  'distill',
843
848
  ]);
844
- /** True when an error is an HTTP 503 (service unavailable — transient). */
845
- function isServiceUnavailableError(error) {
846
- const status = error?.status ?? error?.statusCode ?? error?.response?.status;
847
- return status === 503;
848
- }
849
849
  /** True when a create should be retried: 429 (rate limited) or 503 (transient). */
850
850
  function isRetryableCreateError(error) {
851
- return isRateLimitError(error) || isServiceUnavailableError(error);
852
- }
853
- /** True when an error is an HTTP 402 (quota / payment required). */
854
- function isQuotaError(error) {
855
- const status = error?.status ?? error?.statusCode ?? error?.response?.status;
856
- return status === 402;
851
+ return (0, retry_1.isRateLimitError)(error) || (0, retry_1.isServiceUnavailableError)(error);
857
852
  }
858
853
  /**
859
854
  * Parse and validate one JSONL line into a createMemory body.
@@ -990,10 +985,10 @@ async function batchCreateWithRetry(data, deps) {
990
985
  return await deps.createFn(data);
991
986
  }
992
987
  catch (error) {
993
- if (isQuotaError(error))
988
+ if ((0, retry_1.isQuotaError)(error))
994
989
  throw error;
995
990
  if (isRetryableCreateError(error) && attempt < maxRetries) {
996
- const waitMs = getRetryAfterMs(error) ?? defaultRetryMs * (attempt + 1);
991
+ const waitMs = (0, retry_1.getRetryAfterMs)(error) ?? defaultRetryMs * (attempt + 1);
997
992
  deps.onRateLimited?.(waitMs);
998
993
  await deps.sleep(waitMs);
999
994
  attempt++;
@@ -1087,7 +1082,7 @@ async function runBatchCreate(items, deps) {
1087
1082
  }
1088
1083
  catch (error) {
1089
1084
  summary.failed++;
1090
- if (isQuotaError(error)) {
1085
+ if ((0, retry_1.isQuotaError)(error)) {
1091
1086
  // Quota exhausted — pointless to keep hammering. Stop pulling new
1092
1087
  // lines; in-flight workers finish, remaining lines are not-attempted.
1093
1088
  stopped = true;
@@ -1253,7 +1248,7 @@ async function handleStreamingCreate(maxStdinBytes = exports.BATCH_CREATE_MAX_ST
1253
1248
  const claudeSessionId = claudeSession?.sessionId || undefined;
1254
1249
  const summary = await runBatchCreate(items, {
1255
1250
  createFn: (data) => createOneViaSdk(claudeSessionId ? { ...data, claudeSessionId } : data),
1256
- sleep,
1251
+ sleep: retry_1.sleep,
1257
1252
  concurrency,
1258
1253
  onResult: (r) => {
1259
1254
  // Pure JSONL on stdout — the ONLY stdout write on this path. A broken pipe
@@ -1319,7 +1314,7 @@ function registerMemoriesCommands(program) {
1319
1314
  // Named memories filter — fetch and filter client-side
1320
1315
  if (options.named) {
1321
1316
  const spinner = (0, spinner_1.createSpinner)('Fetching named memories...', options.format).start();
1322
- const namedResponse = await (0, api_client_1.getSdk)().memories.listMemories({ limit: 100, offset: 0 });
1317
+ const namedResponse = await retryRead(() => (0, api_client_1.getSdk)().memories.listMemories({ limit: 100, offset: 0 }));
1323
1318
  const namedMemories = (namedResponse.data?.data || []).filter((m) => m.name);
1324
1319
  spinner.succeed(`Found ${namedMemories.length} named memories`);
1325
1320
  if (options.format === 'table' || !options.format) {
@@ -1420,7 +1415,7 @@ function registerMemoriesCommands(program) {
1420
1415
  .filter((t) => t.length > 0)
1421
1416
  .join(',');
1422
1417
  }
1423
- const response = await (0, api_client_1.getSdk)().memories.listMemories(listParams);
1418
+ const response = await retryRead(() => (0, api_client_1.getSdk)().memories.listMemories(listParams));
1424
1419
  const result = response.data;
1425
1420
  spinner.succeed(`Found ${result?.data?.length || 0} memories`);
1426
1421
  // Format for table display
@@ -1479,9 +1474,9 @@ function registerMemoriesCommands(program) {
1479
1474
  // Named memory lookup — short-circuits the entire ID-based flow
1480
1475
  if (options.name) {
1481
1476
  const spinner = (0, spinner_1.createSpinner)('Fetching named memory...', options.format).start();
1482
- const response = await (0, api_client_1.getSdk)().memories.getMemoryByName(options.name, {
1477
+ const response = await retryRead(() => (0, api_client_1.getSdk)().memories.getMemoryByName(options.name, {
1483
1478
  detail: options.detail || 'standard',
1484
- });
1479
+ }));
1485
1480
  const memory = response.data?.data;
1486
1481
  if (!memory) {
1487
1482
  spinner.fail(`No memory found with name '${options.name}'`);
@@ -1521,9 +1516,9 @@ function registerMemoriesCommands(program) {
1521
1516
  if (isNamedKey) {
1522
1517
  const spinner = (0, spinner_1.createSpinner)('Fetching named memory...', options.format).start();
1523
1518
  try {
1524
- const response = await (0, api_client_1.getSdk)().memories.getMemoryByName(singleId, {
1519
+ const response = await retryRead(() => (0, api_client_1.getSdk)().memories.getMemoryByName(singleId, {
1525
1520
  detail: options.detail || 'standard',
1526
- });
1521
+ }));
1527
1522
  const memory = response.data?.data;
1528
1523
  if (!memory) {
1529
1524
  spinner.fail(`No memory found with name '${singleId}'`);
@@ -1552,11 +1547,11 @@ function registerMemoriesCommands(program) {
1552
1547
  let resolvedMemory = null;
1553
1548
  if (isShortId) {
1554
1549
  const spinner = (0, spinner_1.createSpinner)('Searching for matching memory...', options.format).start();
1555
- const listResponse = await (0, api_client_1.getSdk)().memories.listMemories({
1550
+ const listResponse = await retryRead(() => (0, api_client_1.getSdk)().memories.listMemories({
1556
1551
  limit: 10,
1557
1552
  offset: 0,
1558
1553
  idPrefix: singleId,
1559
- });
1554
+ }));
1560
1555
  const matches = listResponse.data?.data || [];
1561
1556
  if (matches.length === 0) {
1562
1557
  spinner.fail(`No memory found with ID prefix: ${singleId}`);
@@ -1597,9 +1592,9 @@ function registerMemoriesCommands(program) {
1597
1592
  });
1598
1593
  }
1599
1594
  else {
1600
- const response = await (0, api_client_1.getSdk)().memories.getMemoryById(memoryId, {
1595
+ const response = await retryRead(() => (0, api_client_1.getSdk)().memories.getMemoryById(memoryId, {
1601
1596
  detail,
1602
- });
1597
+ }));
1603
1598
  const memory = response.data?.data;
1604
1599
  printMemoryWithDetail(memory, options.format, {
1605
1600
  includeVectors: !!options.includeVectors,
@@ -1789,7 +1784,12 @@ function registerMemoriesCommands(program) {
1789
1784
  let response;
1790
1785
  let upserted = false;
1791
1786
  try {
1792
- response = await (0, api_client_1.getSdk)().memories.createMemory(memoryData);
1787
+ // Single create is non-idempotent → retryWrite (429-only: a rejected
1788
+ // 429 never reached processing, so a retry cannot duplicate the create;
1789
+ // a transient 5xx is NOT retried here to avoid a duplicate). A 409
1790
+ // (named already exists) is not retryable and falls through to the
1791
+ // auto-upsert branch below unchanged.
1792
+ response = await retryWrite(() => (0, api_client_1.getSdk)().memories.createMemory(memoryData));
1793
1793
  }
1794
1794
  catch (createError) {
1795
1795
  // 409 = named memory already exists → auto-upsert via updateNamedMemory
@@ -1811,7 +1811,15 @@ function registerMemoriesCommands(program) {
1811
1811
  // intentionally NOT forwarded here: it is applied uniformly for both the
1812
1812
  // create and upsert paths by the createMemoryRelationship call below, so
1813
1813
  // forwarding it in the body too would create a duplicate SUPERSEDES edge.
1814
- response = await (0, api_client_1.getSdk)().memories.updateNamedMemory(memoryData.name, {
1814
+ // Named update is NOT idempotent on this path: the CLI/SDK never
1815
+ // supply expectedHeadId, so PUT /named/:name does an UNCONDITIONAL
1816
+ // version bump (buildNamedVersionBumpCypher). retryWrite → 429-only:
1817
+ // the named-write gate acquires the permit BEFORE this.write, so a
1818
+ // 429 is pre-write and safe to retry; a 5xx is ambiguous (the bump
1819
+ // may already have committed — e.g. Kong 504 after Neo4j commits)
1820
+ // and MUST NOT be retried, or it silently creates a second identical
1821
+ // version (memory-history corruption).
1822
+ response = await retryWrite(() => (0, api_client_1.getSdk)().memories.updateNamedMemory(memoryData.name, {
1815
1823
  content: memoryData.content,
1816
1824
  ...(options.memoryType && { memoryType: options.memoryType }),
1817
1825
  ...(memoryData.topics && { topics: memoryData.topics }),
@@ -1825,7 +1833,7 @@ function registerMemoriesCommands(program) {
1825
1833
  options.conversationId.toUpperCase() !== 'NEW' && {
1826
1834
  conversationId: options.conversationId,
1827
1835
  }),
1828
- });
1836
+ }));
1829
1837
  upserted = true;
1830
1838
  }
1831
1839
  else {
@@ -1839,7 +1847,9 @@ function registerMemoriesCommands(program) {
1839
1847
  const version = meta?.version || 'new';
1840
1848
  spinner.succeed(`Named memory '${memoryData.name}' updated (v${version}) via auto-upsert. ID: ${memory?.id}`);
1841
1849
  if (meta?.previousVersionId) {
1842
- console.log(chalk_1.default.gray(`Previous version: ${meta.previousVersionId}`));
1850
+ if (options.format !== 'json' && options.format !== 'yaml') {
1851
+ console.log(chalk_1.default.gray(`Previous version: ${meta.previousVersionId}`));
1852
+ }
1843
1853
  }
1844
1854
  }
1845
1855
  else {
@@ -1848,7 +1858,9 @@ function registerMemoriesCommands(program) {
1848
1858
  : 'Memory created with';
1849
1859
  if (isNewConversation && meta?.conversationCreated) {
1850
1860
  spinner.succeed(`New conversation created: ${meta.conversationId}\n${nameLabel} ID: ${memory?.id}`);
1851
- console.log(chalk_1.default.gray(`\nSession ID: ${meta.sessionId}`));
1861
+ if (options.format !== 'json' && options.format !== 'yaml') {
1862
+ console.log(chalk_1.default.gray(`\nSession ID: ${meta.sessionId}`));
1863
+ }
1852
1864
  }
1853
1865
  else {
1854
1866
  spinner.succeed(`${nameLabel} ID: ${memory?.id}`);
@@ -1858,18 +1870,20 @@ function registerMemoriesCommands(program) {
1858
1870
  if (options.supersedes && memory?.id) {
1859
1871
  const relSpinner = (0, spinner_1.createSpinner)('Creating SUPERSEDES relationship...', options.format).start();
1860
1872
  try {
1861
- await (0, api_client_1.getSdk)().memories.createMemoryRelationship(memory.id, {
1873
+ await retryWrite(() => (0, api_client_1.getSdk)().memories.createMemoryRelationship(memory.id, {
1862
1874
  targetMemoryId: options.supersedes,
1863
1875
  type: 'SUPERSEDES',
1864
1876
  confidence: 1.0,
1865
1877
  reason: 'Created via --supersedes flag',
1866
- });
1878
+ }));
1867
1879
  relSpinner.succeed(`Relationship created: ${memory.id.substring(0, 8)}... SUPERSEDES ${options.supersedes.substring(0, 8)}...`);
1868
1880
  }
1869
1881
  catch (relError) {
1870
1882
  relSpinner.fail(`Failed to create relationship: ${relError.message || 'Unknown error'}`);
1871
- console.log(chalk_1.default.yellow('Memory was created successfully. You can manually create the relationship with:'));
1872
- console.log(chalk_1.default.cyan(` mx memories relationships create ${memory.id} --target ${options.supersedes} --type SUPERSEDES`));
1883
+ if (options.format !== 'json' && options.format !== 'yaml') {
1884
+ console.log(chalk_1.default.yellow('Memory was created successfully. You can manually create the relationship with:'));
1885
+ console.log(chalk_1.default.cyan(` mx memories relationships create ${memory.id} --target ${options.supersedes} --type SUPERSEDES`));
1886
+ }
1873
1887
  }
1874
1888
  }
1875
1889
  (0, formatters_1.printOutput)(memory, options.format);
@@ -1928,7 +1942,7 @@ function registerMemoriesCommands(program) {
1928
1942
  }
1929
1943
  updates.name = options.assignName;
1930
1944
  const spinner = (0, spinner_1.createSpinner)('Assigning name...', options.format).start();
1931
- const response = await (0, api_client_1.getSdk)().memories.updateMemory(id, updates);
1945
+ const response = await retryWrite(() => (0, api_client_1.getSdk)().memories.updateMemory(id, updates));
1932
1946
  const memory = response.data?.data;
1933
1947
  spinner.succeed(`Memory ${id} named '${options.assignName}'`);
1934
1948
  (0, formatters_1.printOutput)(memory, options.format);
@@ -1939,15 +1953,15 @@ function registerMemoriesCommands(program) {
1939
1953
  if (options.name) {
1940
1954
  // Look up current memory by name, then update its name
1941
1955
  const spinner = (0, spinner_1.createSpinner)('Renaming...', options.format).start();
1942
- const getResponse = await (0, api_client_1.getSdk)().memories.getMemoryByName(options.name);
1956
+ const getResponse = await retryRead(() => (0, api_client_1.getSdk)().memories.getMemoryByName(options.name));
1943
1957
  const currentMemory = getResponse.data?.data;
1944
1958
  if (!currentMemory?.id) {
1945
1959
  spinner.fail(`Named memory '${options.name}' not found`);
1946
1960
  return;
1947
1961
  }
1948
- const response = await (0, api_client_1.getSdk)().memories.updateMemory(currentMemory.id, {
1962
+ const response = await retryWrite(() => (0, api_client_1.getSdk)().memories.updateMemory(currentMemory.id, {
1949
1963
  name: options.rename,
1950
- });
1964
+ }));
1951
1965
  const memory = response.data?.data;
1952
1966
  spinner.succeed(`Named memory '${options.name}' renamed to '${options.rename}'`);
1953
1967
  (0, formatters_1.printOutput)(memory, options.format);
@@ -1956,7 +1970,7 @@ function registerMemoriesCommands(program) {
1956
1970
  if (id) {
1957
1971
  updates.name = options.rename;
1958
1972
  const spinner = (0, spinner_1.createSpinner)('Renaming...', options.format).start();
1959
- const response = await (0, api_client_1.getSdk)().memories.updateMemory(id, updates);
1973
+ const response = await retryWrite(() => (0, api_client_1.getSdk)().memories.updateMemory(id, updates));
1960
1974
  const memory = response.data?.data;
1961
1975
  spinner.succeed(`Memory ${id} renamed to '${options.rename}'`);
1962
1976
  (0, formatters_1.printOutput)(memory, options.format);
@@ -1972,18 +1986,26 @@ function registerMemoriesCommands(program) {
1972
1986
  // Named memory update — creates new version
1973
1987
  if (options.name) {
1974
1988
  const spinner = (0, spinner_1.createSpinner)('Creating new version...', options.format).start();
1975
- const response = await (0, api_client_1.getSdk)().memories.updateNamedMemory(options.name, updates);
1989
+ // Named update is NOT idempotent without expectedHeadId (which the
1990
+ // CLI/SDK never supply): PUT /named/:name unconditionally bumps the
1991
+ // version. retryWrite → 429-only: the gate takes the permit before the
1992
+ // write, so a 429 is pre-write and safe to retry; a 5xx is ambiguous
1993
+ // (the bump may already have committed) and MUST NOT be retried, or it
1994
+ // creates a second identical version (silent history corruption).
1995
+ const response = await retryWrite(() => (0, api_client_1.getSdk)().memories.updateNamedMemory(options.name, updates));
1976
1996
  const memory = response.data?.data;
1977
1997
  const meta = response.data?.meta;
1978
1998
  spinner.succeed(`Named memory '${options.name}' updated to v${meta?.version}`);
1979
1999
  if (meta?.previousVersionId) {
1980
- console.log(chalk_1.default.gray(`Previous version: ${meta.previousVersionId}`));
2000
+ if (options.format !== 'json' && options.format !== 'yaml') {
2001
+ console.log(chalk_1.default.gray(`Previous version: ${meta.previousVersionId}`));
2002
+ }
1981
2003
  }
1982
2004
  (0, formatters_1.printOutput)(memory, options.format);
1983
2005
  return;
1984
2006
  }
1985
2007
  const spinner = (0, spinner_1.createSpinner)('Updating memory...', options.format).start();
1986
- const response = await (0, api_client_1.getSdk)().memories.updateMemory(id, updates);
2008
+ const response = await retryWrite(() => (0, api_client_1.getSdk)().memories.updateMemory(id, updates));
1987
2009
  const memory = response.data?.data;
1988
2010
  spinner.succeed(`Memory ${id} updated successfully`);
1989
2011
  (0, formatters_1.printOutput)(memory, options.format);
@@ -2004,7 +2026,7 @@ function registerMemoriesCommands(program) {
2004
2026
  .action(async (name, options) => {
2005
2027
  try {
2006
2028
  const spinner = (0, spinner_1.createSpinner)('Fetching version history...', options.format).start();
2007
- const response = await (0, api_client_1.getSdk)().memories.getNamedMemoryHistory(name);
2029
+ const response = await retryRead(() => (0, api_client_1.getSdk)().memories.getNamedMemoryHistory(name));
2008
2030
  const data = response.data;
2009
2031
  if (!data?.versions?.length) {
2010
2032
  spinner.fail(`No memory found with name '${name}'`);
@@ -2205,7 +2227,9 @@ function registerMemoriesCommands(program) {
2205
2227
  }
2206
2228
  }
2207
2229
  const spinner = (0, spinner_1.createSpinner)('Deleting memory...', 'table').start();
2208
- await (0, api_client_1.getSdk)().memories.deleteMemory(id);
2230
+ // DELETE is idempotent (deleting an already-deleted id is a no-op), so a
2231
+ // transient 5xx is retry-safe as well as a 429.
2232
+ await retryRead(() => (0, api_client_1.getSdk)().memories.deleteMemory(id));
2209
2233
  // Record this deletion in the cross-process guard file so the next
2210
2234
  // invocation's threshold check reflects it.
2211
2235
  if (isNonInteractive) {
@@ -2426,7 +2450,7 @@ function registerMemoriesCommands(program) {
2426
2450
  searchRequest.order = options.order;
2427
2451
  }
2428
2452
  // Use GET endpoint for better performance (Issue #79)
2429
- const response = await (0, api_client_1.getSdk)().memories.searchMemoriesGet(searchRequest);
2453
+ const response = await retryRead(() => (0, api_client_1.getSdk)().memories.searchMemoriesGet(searchRequest));
2430
2454
  const searchData = response.data;
2431
2455
  // Normalize: API may return flat objects or {memory, score} wrappers.
2432
2456
  // Ensure consistent {memory, score, matchedVia} shape for display code.
@@ -2844,19 +2868,19 @@ async function handleMemoryDiscovery(memoryId, options) {
2844
2868
  const sdk = (0, api_client_1.getSdk)();
2845
2869
  if (options.similar) {
2846
2870
  discoveryType = 'similar';
2847
- const response = await sdk.memories.getSimilarMemories(memoryId, { limit: options.limit });
2871
+ const response = await retryRead(() => sdk.memories.getSimilarMemories(memoryId, { limit: options.limit }));
2848
2872
  result = response.data;
2849
2873
  }
2850
2874
  else if (options.conversation) {
2851
2875
  discoveryType = 'conversation';
2852
- const response = await sdk.memories.getConversationMemories(memoryId, {
2876
+ const response = await retryRead(() => sdk.memories.getConversationMemories(memoryId, {
2853
2877
  limit: options.limit,
2854
- });
2878
+ }));
2855
2879
  result = response.data;
2856
2880
  }
2857
2881
  else if (options.related) {
2858
2882
  discoveryType = 'related';
2859
- const response = await sdk.memories.getRelatedMemories(memoryId, { limit: options.limit });
2883
+ const response = await retryRead(() => sdk.memories.getRelatedMemories(memoryId, { limit: options.limit }));
2860
2884
  result = response.data;
2861
2885
  }
2862
2886
  else {
@@ -2957,9 +2981,9 @@ function registerMemoryRelationshipCommands(memories) {
2957
2981
  return;
2958
2982
  }
2959
2983
  const spinner = (0, spinner_1.createSpinner)('Fetching relationships...', options.format).start();
2960
- const response = await (0, api_client_1.getSdk)().memories.getMemoryRelationships(id, {
2984
+ const response = await retryRead(() => (0, api_client_1.getSdk)().memories.getMemoryRelationships(id, {
2961
2985
  direction: options.direction,
2962
- });
2986
+ }));
2963
2987
  const result = response.data;
2964
2988
  spinner.succeed(`Found ${result?.data?.length || 0} relationships`);
2965
2989
  if (options.format === 'table' || !options.format) {
@@ -3065,7 +3089,7 @@ function registerMemoryRelationshipCommands(memories) {
3065
3089
  };
3066
3090
  }
3067
3091
  const spinner = (0, spinner_1.createSpinner)('Creating relationship...', options.format).start();
3068
- const response = await (0, api_client_1.getSdk)().memories.createMemoryRelationship(id, relationshipData);
3092
+ const response = await retryWrite(() => (0, api_client_1.getSdk)().memories.createMemoryRelationship(id, relationshipData));
3069
3093
  const relationship = response.data?.data;
3070
3094
  spinner.succeed(`Relationship created: ${id} ${relationship?.type} ${relationshipData.targetMemoryId}`);
3071
3095
  (0, formatters_1.printOutput)(relationship, options.format);
@@ -3108,7 +3132,7 @@ function registerMemoryRelationshipCommands(memories) {
3108
3132
  }
3109
3133
  // Non-interactive without --force: auto-confirm single delete
3110
3134
  const spinner = (0, spinner_1.createSpinner)('Deleting relationship...', 'table').start();
3111
- await (0, api_client_1.getSdk)().memories.deleteMemoryRelationship(id, relationshipId);
3135
+ await retryRead(() => (0, api_client_1.getSdk)().memories.deleteMemoryRelationship(id, relationshipId));
3112
3136
  spinner.succeed(`Relationship ${relationshipId} deleted successfully`);
3113
3137
  }
3114
3138
  catch (error) {
@@ -3136,7 +3160,7 @@ function registerMemoryTimelineCommand(memories) {
3136
3160
  const before = parseInt(options.before, 10);
3137
3161
  const after = parseInt(options.after, 10);
3138
3162
  const spinner = (0, spinner_1.createSpinner)('Fetching timeline context...', options.format).start();
3139
- const response = await (0, api_client_1.getSdk)().memories.getMemoryTimeline(id, { before, after });
3163
+ const response = await retryRead(() => (0, api_client_1.getSdk)().memories.getMemoryTimeline(id, { before, after }));
3140
3164
  const result = response.data;
3141
3165
  spinner.succeed('Timeline context retrieved');
3142
3166
  if (options.format === 'table' || !options.format) {
@@ -3199,12 +3223,14 @@ function registerDetectRelationshipsCommand(memories) {
3199
3223
  const spinner = (0, spinner_1.createSpinner)(options.autoCreate
3200
3224
  ? 'Detecting and creating relationships...'
3201
3225
  : 'Detecting relationships...', options.format).start();
3202
- const response = await (0, api_client_1.getSdk)().memories.detectMemoryRelationships(id, {
3226
+ // detect with --auto-create writes relationship edges, so it is
3227
+ // non-idempotent → retryWrite (429-only) to avoid duplicate edges on 5xx.
3228
+ const response = await retryWrite(() => (0, api_client_1.getSdk)().memories.detectMemoryRelationships(id, {
3203
3229
  body: {
3204
3230
  autoCreate: options.autoCreate || false,
3205
3231
  minConfidence,
3206
3232
  },
3207
- });
3233
+ }));
3208
3234
  const result = response.data;
3209
3235
  const suggestions = result?.data || [];
3210
3236
  spinner.succeed(`Found ${suggestions.length} potential relationships`);
@@ -3344,7 +3370,7 @@ function registerDigestCommand(memories) {
3344
3370
  sdk.config.timeoutMs = RECALL_MAX_MS;
3345
3371
  let response;
3346
3372
  try {
3347
- response = await sdk.memories.generateMemoryDigest(body);
3373
+ response = await retryRead(() => sdk.memories.generateMemoryDigest(body));
3348
3374
  }
3349
3375
  finally {
3350
3376
  if (sdk.config)
@@ -3433,7 +3459,7 @@ function registerStatusCommand(memories) {
3433
3459
  sdk.config.timeoutMs = 180000;
3434
3460
  let response;
3435
3461
  try {
3436
- response = await sdk.memories.generateMemoryDigest(body);
3462
+ response = await retryRead(() => sdk.memories.generateMemoryDigest(body));
3437
3463
  }
3438
3464
  finally {
3439
3465
  if (sdk.config)
@@ -3579,7 +3605,7 @@ function registerRecapCommand(memories) {
3579
3605
  };
3580
3606
  if (excludeTopicsParam)
3581
3607
  searchRequest.excludeTopics = excludeTopicsParam;
3582
- const response = await (0, api_client_1.getSdk)().memories.searchMemoriesGet(searchRequest);
3608
+ const response = await retryRead(() => (0, api_client_1.getSdk)().memories.searchMemoriesGet(searchRequest));
3583
3609
  // Normalize: API may return flat objects or {memory, score} wrappers.
3584
3610
  results = (response.data?.data || []).map((r) => r.memory ? r : { memory: r, score: r.score, matchedVia: r.matchedVia });
3585
3611
  }
@@ -3595,7 +3621,7 @@ function registerRecapCommand(memories) {
3595
3621
  };
3596
3622
  if (excludeTopicsParam)
3597
3623
  listRequest.excludeTopics = excludeTopicsParam;
3598
- const response = await (0, api_client_1.getSdk)().memories.listMemories(listRequest);
3624
+ const response = await retryRead(() => (0, api_client_1.getSdk)().memories.listMemories(listRequest));
3599
3625
  // Wrap in search-result shape expected by groupByConversation / displayRecap
3600
3626
  results = (response.data?.data || []).map((m) => ({ memory: m }));
3601
3627
  }
@@ -3833,7 +3859,7 @@ function registerBuildContextCommand(memories) {
3833
3859
  body.files = options.files;
3834
3860
  }
3835
3861
  const sdk = (0, api_client_1.getSdk)();
3836
- const response = await sdk.memories.buildContext(body);
3862
+ const response = await retryRead(() => sdk.memories.buildContext(body));
3837
3863
  const data = response?.data || response;
3838
3864
  if (!data) {
3839
3865
  spinner.fail('No response data received');