@memorax/memorax-code 0.1.5 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/README.md +7 -18
  2. package/bin/memorax-code-npm-preinstall.mjs +28 -39
  3. package/bin/memorax-code-plugin-postinstall.mjs +23 -1537
  4. package/bin/memorax-code-setup.mjs +1615 -0
  5. package/bin/memorax-code.mjs +289 -5
  6. package/docs/configuration.md +66 -17
  7. package/docs/troubleshooting.md +123 -7
  8. package/lib/memorax-code-adapter-common/src/config-utils.d.mts +5 -1
  9. package/lib/memorax-code-adapter-common/src/config-utils.mjs +36 -3
  10. package/lib/memorax-code-adapter-common/src/credentials/linux-secret-service.mjs +110 -0
  11. package/lib/memorax-code-adapter-common/src/credentials/macos-keychain.mjs +224 -0
  12. package/lib/memorax-code-adapter-common/src/credentials/secure-command.mjs +340 -0
  13. package/lib/memorax-code-adapter-common/src/credentials/trial-credential-record.d.mts +82 -0
  14. package/lib/memorax-code-adapter-common/src/credentials/trial-credential-record.mjs +183 -0
  15. package/lib/memorax-code-adapter-common/src/credentials/trial-credential-store.d.mts +82 -0
  16. package/lib/memorax-code-adapter-common/src/credentials/trial-credential-store.mjs +271 -0
  17. package/lib/memorax-code-adapter-common/src/credentials/windows-dpapi.mjs +440 -0
  18. package/lib/memorax-code-adapter-common/src/hooks/memory-skill-reminder-hook.mjs +7 -4
  19. package/lib/memorax-code-adapter-common/src/setup-completion.d.mts +81 -0
  20. package/lib/memorax-code-adapter-common/src/setup-completion.mjs +154 -0
  21. package/lib/memorax-code-backend/dist/clients/claude/memory-hook-runtime.js +21 -2
  22. package/lib/memorax-code-backend/dist/clients/codex/memory-hook-runtime.js +23 -3
  23. package/lib/memorax-code-backend/dist/clients/opencode/memory-hook-runtime.js +19 -1
  24. package/lib/memorax-code-backend/dist/entrypoints/backend-cli.js +17 -16
  25. package/lib/memorax-code-backend/dist/lifecycle/backend/service.js +1 -0
  26. package/lib/memorax-code-backend/dist/lifecycle/orchestrator.js +81 -22
  27. package/lib/memorax-code-backend/dist/memorax-cli.js +3 -0
  28. package/lib/memorax-code-backend/dist/memory/automatic-retrieval.js +8 -2
  29. package/lib/memorax-code-backend/dist/memory/automatic-writeback.js +13 -1
  30. package/lib/memorax-code-backend/dist/memory/cli.js +10 -0
  31. package/lib/memorax-code-backend/dist/memory/quota-notice.js +208 -0
  32. package/lib/memorax-code-backend/dist/memory/service.js +11 -0
  33. package/lib/memorax-code-backend/dist/provider/memorax/adapter.js +4 -2
  34. package/lib/memorax-code-backend/dist/provider/memorax/http.js +10 -3
  35. package/lib/memorax-code-backend/dist/provider/memorax/quota.js +26 -0
  36. package/lib/memorax-code-backend/package.json +1 -1
  37. package/lib/memorax-code-claude-adapter/.claude-plugin/plugin.json +1 -1
  38. package/lib/memorax-code-claude-adapter/hooks/runtime-shell.json +1 -1
  39. package/lib/memorax-code-claude-adapter/package.json +1 -1
  40. package/lib/memorax-code-claude-adapter/runtime-hooks/memory-turn.mjs +5 -3
  41. package/lib/memorax-code-claude-adapter/skills/memorax-code/references/memorax-add.md +2 -0
  42. package/lib/memorax-code-claude-adapter/skills/memorax-code/references/memorax-search.md +4 -2
  43. package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/.claude-plugin/plugin.json +1 -1
  44. package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/hooks/runtime-shell.json +1 -1
  45. package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/memorax-code-adapter-common/src/config-utils.d.mts +5 -1
  46. package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/memorax-code-adapter-common/src/config-utils.mjs +36 -3
  47. package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/memorax-code-adapter-common/src/credentials/linux-secret-service.mjs +110 -0
  48. package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/memorax-code-adapter-common/src/credentials/macos-keychain.mjs +224 -0
  49. package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/memorax-code-adapter-common/src/credentials/secure-command.mjs +340 -0
  50. package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/memorax-code-adapter-common/src/credentials/trial-credential-record.d.mts +82 -0
  51. package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/memorax-code-adapter-common/src/credentials/trial-credential-record.mjs +183 -0
  52. package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/memorax-code-adapter-common/src/credentials/trial-credential-store.d.mts +82 -0
  53. package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/memorax-code-adapter-common/src/credentials/trial-credential-store.mjs +271 -0
  54. package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/memorax-code-adapter-common/src/credentials/windows-dpapi.mjs +440 -0
  55. package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/memorax-code-adapter-common/src/hooks/memory-skill-reminder-hook.mjs +7 -4
  56. package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/memorax-code-adapter-common/src/setup-completion.d.mts +81 -0
  57. package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/memorax-code-adapter-common/src/setup-completion.mjs +154 -0
  58. package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/package.json +1 -1
  59. package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/runtime-hooks/memory-turn.mjs +5 -3
  60. package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/skills/memorax-code/references/memorax-add.md +2 -0
  61. package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/skills/memorax-code/references/memorax-search.md +4 -2
  62. package/lib/memorax-code-codex-adapter/.codex-plugin/plugin.json +1 -1
  63. package/lib/memorax-code-codex-adapter/hooks/runtime-shell.json +1 -1
  64. package/lib/memorax-code-codex-adapter/package.json +1 -1
  65. package/lib/memorax-code-codex-adapter/runtime-hooks/memory-skill-reminder.mjs +2 -0
  66. package/lib/memorax-code-codex-adapter/skills/memorax-code/references/memorax-add.md +2 -0
  67. package/lib/memorax-code-codex-adapter/skills/memorax-code/references/memorax-search.md +4 -2
  68. package/lib/memorax-code-dsh-adapter/hooks/repo-memory-job.mjs +5 -2
  69. package/lib/memorax-code-dsh-adapter/package.json +1 -1
  70. package/lib/memorax-code-dsh-adapter/skills/memorax-code/references/memorax-add.md +2 -0
  71. package/lib/memorax-code-dsh-adapter/skills/memorax-code/references/memorax-search.md +4 -2
  72. package/lib/memorax-code-dsh-adapter/src/profile-lifecycle.mjs +158 -7
  73. package/lib/memorax-code-dsh-adapter/src/runtime-state.mjs +21 -0
  74. package/lib/memorax-code-opencode-adapter/package.json +1 -1
  75. package/lib/memorax-code-opencode-adapter/skills/memorax-code/references/memorax-add.md +2 -0
  76. package/lib/memorax-code-opencode-adapter/skills/memorax-code/references/memorax-search.md +4 -2
  77. package/lib/memorax-code-opencode-adapter/src/plugin.mjs +20 -0
  78. package/lib/package-transition.mjs +312 -0
  79. package/lib/resolve-codex-command.mjs +32 -6
  80. package/lib/setup-memory-preferences.mjs +113 -0
  81. package/lib/setup-reconcile.mjs +128 -0
  82. package/lib/trial-plugin-mark.mjs +171 -0
  83. package/lib/trial-provision-client.mjs +307 -0
  84. package/lib/trial-provision-flow.mjs +215 -0
  85. package/lib/trial-setup.mjs +53 -0
  86. package/package.json +1 -1
@@ -5,6 +5,7 @@ import { dirname, join, resolve } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import { BackendConnectionAuthorityError } from "../../../memorax-code-adapter-common/src/backend-connection.mjs";
7
7
  import { RuntimeRecordError } from "../../../memorax-code-adapter-common/src/runtime-record.mjs";
8
+ import { withSetupCompletionLock } from "../../../memorax-code-adapter-common/src/setup-completion.mjs";
8
9
  import { runBackendStatus } from "./backend/status.js";
9
10
  import { readBackendServiceState, preflightBackendServiceStart, startBackendService, stopBackendService, } from "./backend/service.js";
10
11
  import { backendServiceHome, BackendLifecycleLockError, withBackendLifecycleLock, } from "./lock.js";
@@ -17,6 +18,7 @@ import { claudeAdapterLifecycle } from "../clients/claude/lifecycle.js";
17
18
  import { collectDshAdapterLifecycleStatus, withDshAdapterLifecycleLock, } from "../clients/dsh/lifecycle.js";
18
19
  import { openCodeAdapterLifecycle } from "../clients/opencode/lifecycle.js";
19
20
  const PACKAGE_REPLACEMENT_ENV = "MEMORAX_CODE_PACKAGE_REPLACEMENT";
21
+ const DSH_OPTIONAL_ENV = "MEMORAX_CODE_DSH_ADAPTER_OPTIONAL";
20
22
  const DSH_RECOVERY_ENV = "MEMORAX_CODE_DSH_ADAPTER_RECOVERY";
21
23
  const DSH_RECOVERY_REVISION_ENV = "MEMORAX_CODE_DSH_ADAPTER_EXPECTED_REVISION";
22
24
  export async function collectMemoraxCodeStatus(backendUrl, backendToken, serviceOptions, argv) {
@@ -50,9 +52,9 @@ export async function collectMemoraxCodeStatus(backendUrl, backendToken, service
50
52
  const claudeAdapter = clients.claude
51
53
  ? await claudeAdapterLifecycle.status({ argv, serviceOptions, backendUrl })
52
54
  : undefined;
53
- const dshAdapter = clients.dsh
55
+ const dshAdapter = markOptionalDshAdapter(clients.dsh
54
56
  ? await collectDshAdapterLifecycleStatus({ argv, serviceOptions, backendUrl })
55
- : undefined;
57
+ : undefined, clients.dsh && isOptionalDshSelection(argv));
56
58
  const opencodeAdapter = clients.opencode
57
59
  ? await openCodeAdapterLifecycle.status({ argv, serviceOptions, backendUrl })
58
60
  : undefined;
@@ -60,13 +62,15 @@ export async function collectMemoraxCodeStatus(backendUrl, backendToken, service
60
62
  const claudeReady = claudeAdapter ? isAdapterReady(claudeAdapter) : true;
61
63
  const dshReady = dshAdapter ? isAdapterReady(dshAdapter) : true;
62
64
  const opencodeReady = opencodeAdapter ? isAdapterReady(opencodeAdapter) : true;
65
+ const optionalDshUnavailable = isOptionalUnavailableDshAdapter(dshAdapter);
63
66
  return {
64
67
  ok: backend.ok
65
68
  && codexReady
66
69
  && opencodeReady
67
70
  && (isOptionalUnconfiguredClaudeAdapter(claudeAdapter, codexAdapter) || claudeReady)
68
- && (isOptionalUnavailableDshAdapter(dshAdapter) || dshReady),
71
+ && (optionalDshUnavailable || dshReady),
69
72
  action: "status",
73
+ ...(optionalDshUnavailable ? { degraded: true } : {}),
70
74
  backend,
71
75
  ...(codexAdapter ? { codexAdapter } : {}),
72
76
  ...(claudeAdapter ? { claudeAdapter } : {}),
@@ -104,16 +108,22 @@ async function startMemoraxCodeServiceLocked(serviceOptions, argv) {
104
108
  if (serviceStateFailure) {
105
109
  return { ok: false, action: "start", backend: serviceStateFailure };
106
110
  }
107
- const config = loadManagedClientsConfig(memoraxCodeHome);
108
- const requestedClients = resolveManagedClients(argv, config);
111
+ const packageReplacement = isPackageReplacement();
112
+ // Active client intent must not bypass strict lifecycle config validation.
113
+ if (packageReplacement)
114
+ loadManagedClientsConfig(memoraxCodeHome);
115
+ const requestedClients = managedClientsFor(argv, serviceOptions, {
116
+ preferActive: packageReplacement,
117
+ });
109
118
  const clients = isDshAdapterRecovery()
110
119
  ? { ...requestedClients, dsh: true }
111
120
  : requestedClients;
112
121
  const previousClients = readActiveManagedClients(memoraxCodeHome);
122
+ const optionalDsh = clients.dsh && isOptionalDshSelection(argv);
113
123
  const context = { argv, serviceOptions };
114
124
  if (clients.dsh || previousClients?.dsh || isDshAdapterRecovery()) {
115
125
  try {
116
- return await withDshAdapterLifecycleLock(context, (dshLifecycle) => (executeMemoraxCodeStart(serviceOptions, argv, backendUrl, memoraxCodeHome, clients, previousClients, dshLifecycle)));
126
+ return await withDshAdapterLifecycleLock(context, (dshLifecycle) => (executeMemoraxCodeStart(serviceOptions, argv, backendUrl, memoraxCodeHome, clients, previousClients, dshLifecycle, optionalDsh)));
117
127
  }
118
128
  catch (error) {
119
129
  return dshLifecycleFailure("start", error);
@@ -121,7 +131,7 @@ async function startMemoraxCodeServiceLocked(serviceOptions, argv) {
121
131
  }
122
132
  return executeMemoraxCodeStart(serviceOptions, argv, backendUrl, memoraxCodeHome, clients, previousClients);
123
133
  }
124
- async function executeMemoraxCodeStart(serviceOptions, argv, backendUrl, memoraxCodeHome, clients, previousClients, dshLifecycle) {
134
+ async function executeMemoraxCodeStart(serviceOptions, argv, backendUrl, memoraxCodeHome, clients, previousClients, dshLifecycle, optionalDsh = false) {
125
135
  const lifecycleContext = { argv, serviceOptions };
126
136
  const lifecycleBackendContext = { ...lifecycleContext, backendUrl };
127
137
  if (isDshAdapterRecovery() && dshLifecycle) {
@@ -148,7 +158,7 @@ async function executeMemoraxCodeStart(serviceOptions, argv, backendUrl, memorax
148
158
  const quiescedDsh = dshLifecycle && (clients.dsh || previousClients?.dsh)
149
159
  ? await dshLifecycle.quiesce?.(lifecycleContext)
150
160
  : undefined;
151
- if (quiescedDsh?.ok === false) {
161
+ if (quiescedDsh?.ok === false && !optionalDsh) {
152
162
  return {
153
163
  ok: false,
154
164
  action: "start",
@@ -249,10 +259,12 @@ async function executeMemoraxCodeStart(serviceOptions, argv, backendUrl, memorax
249
259
  opencodeAdapter,
250
260
  };
251
261
  }
252
- const preparedDshAdapter = clients.dsh && dshLifecycle
253
- ? await dshLifecycle.prepareEnable(lifecycleBackendContext)
254
- : undefined;
255
- if (preparedDshAdapter?.ok === false) {
262
+ const preparedDshAdapter = markOptionalDshAdapter(quiescedDsh?.ok === false
263
+ ? quiescedDsh
264
+ : clients.dsh && dshLifecycle
265
+ ? await dshLifecycle.prepareEnable(lifecycleBackendContext)
266
+ : undefined, optionalDsh);
267
+ if (preparedDshAdapter?.ok === false && !optionalDsh) {
256
268
  const recovery = await recoverPreparationFailure("dsh_adapter_enable_failed");
257
269
  return {
258
270
  ok: false,
@@ -286,12 +298,15 @@ async function executeMemoraxCodeStart(serviceOptions, argv, backendUrl, memorax
286
298
  ...(disabledOpenCode ? { opencodeAdapter: disabledOpenCode } : {}),
287
299
  };
288
300
  }
289
- const dshAdapter = preparedDshAdapter?.installed === true
301
+ const dshAdapter = markOptionalDshAdapter(preparedDshAdapter?.installed === true
290
302
  ? await dshLifecycle?.activate?.(lifecycleBackendContext)
291
- : preparedDshAdapter;
303
+ : preparedDshAdapter, optionalDsh);
304
+ const optionalDshUnavailable = isOptionalUnavailableDshAdapter(dshAdapter);
292
305
  return {
293
- ok: claudeAdapter?.ok !== false && dshAdapter?.ok !== false,
306
+ ok: claudeAdapter?.ok !== false
307
+ && (!dshAdapter || isAdapterReady(dshAdapter) || optionalDshUnavailable),
294
308
  action: "start",
309
+ ...(optionalDshUnavailable ? { degraded: true } : {}),
295
310
  backend,
296
311
  ...(codexAdapter ? { codexAdapter } : {}),
297
312
  ...(claudeAdapter ? { claudeAdapter } : {}),
@@ -426,7 +441,9 @@ async function executeMemoraxCodeStop(serviceOptions, argv, context, dshLifecycl
426
441
  ...(dshAdapter ? { dshAdapter } : {}),
427
442
  ...(opencodeAdapter ? { opencodeAdapter } : {}),
428
443
  },
429
- remainingClients: remaining && hasRemainingClients ? remaining : undefined,
444
+ remainingClients: packageReplacement
445
+ ? activeClients
446
+ : remaining && hasRemainingClients ? remaining : undefined,
430
447
  };
431
448
  }
432
449
  export async function restartMemoraxCodeService(serviceOptions, argv, commitReadyState) {
@@ -457,7 +474,39 @@ async function restartMemoraxCodeServiceLocked(serviceOptions, argv) {
457
474
  return { ...started, action: "restart" };
458
475
  }
459
476
  export async function uninstallMemoraxCodeService(serviceOptions, argv) {
460
- return withMemoraxCodeLifecycleLock("uninstall", serviceOptions, () => uninstallMemoraxCodeServiceLocked(serviceOptions, argv));
477
+ const memoraxCodeHome = memoraxCodeHomeForService(serviceOptions);
478
+ let report;
479
+ let lifecycleEntered = false;
480
+ try {
481
+ return await withSetupCompletionLock(memoraxCodeHome, async (_completion, mutation) => {
482
+ lifecycleEntered = true;
483
+ report = await withMemoraxCodeLifecycleLock("uninstall", serviceOptions, () => uninstallMemoraxCodeServiceLocked(serviceOptions, argv));
484
+ if (isCompleteSuccessfulUninstall(report))
485
+ mutation.clear();
486
+ return report;
487
+ });
488
+ }
489
+ catch (error) {
490
+ if (lifecycleEntered && !report)
491
+ throw error;
492
+ if (!report) {
493
+ return {
494
+ ok: false,
495
+ action: "uninstall",
496
+ reason: "setup_completion_lock_failed",
497
+ message: error instanceof Error ? error.message : String(error),
498
+ npmPackageRemoval: skippedNpmPackageRemoval("setup_completion_lock_failed"),
499
+ removesPlugin: false,
500
+ removesUserState: false,
501
+ };
502
+ }
503
+ return {
504
+ ...report,
505
+ ok: false,
506
+ reason: "setup_completion_clear_failed",
507
+ message: error instanceof Error ? error.message : String(error),
508
+ };
509
+ }
461
510
  }
462
511
  async function uninstallMemoraxCodeServiceLocked(serviceOptions, argv) {
463
512
  const context = memoraxCodeStopContext(serviceOptions, argv);
@@ -537,6 +586,11 @@ async function executeMemoraxCodeUninstall(serviceOptions, argv, context, dshLif
537
586
  function skippedNpmPackageRemoval(reason) {
538
587
  return { ok: true, action: "npm-package-remove", skipped: true, reason };
539
588
  }
589
+ function isCompleteSuccessfulUninstall(report) {
590
+ return report.ok
591
+ && report.npmPackageRemoval !== undefined
592
+ && report.npmPackageRemoval.reason !== "partial_client_uninstall";
593
+ }
540
594
  async function removeNpmPackageIfInstalled(argv) {
541
595
  if (argv.includes("--no-npm-uninstall")) {
542
596
  return { ok: true, action: "npm-package-remove", skipped: true, reason: "disabled_by_flag" };
@@ -638,6 +692,10 @@ function commitActiveManagedClients(memoraxCodeHome, clients) {
638
692
  function hasExplicitClientSelection(argv) {
639
693
  return argv.includes("--clients");
640
694
  }
695
+ function isOptionalDshSelection(argv) {
696
+ return !isDshAdapterRecovery()
697
+ && (!hasExplicitClientSelection(argv) || truthyEnv(process.env[DSH_OPTIONAL_ENV]));
698
+ }
641
699
  function argValue(argv, name) {
642
700
  const index = argv.indexOf(name);
643
701
  return index >= 0 ? argv[index + 1] : undefined;
@@ -777,11 +835,12 @@ export function isAdapterReady(report) {
777
835
  && report.opencodeSkills?.ok !== false;
778
836
  }
779
837
  export function isOptionalUnavailableDshAdapter(report) {
780
- return Boolean(report
781
- && report.ok !== false
782
- && report.skipped === true
783
- && report.managed !== true
784
- && report.reason === "no_existing_profiles");
838
+ return Boolean(report?.optional === true && !isAdapterReady(report));
839
+ }
840
+ function markOptionalDshAdapter(report, optional) {
841
+ return report && optional && !isAdapterReady(report)
842
+ ? { ...report, optional: true }
843
+ : report;
785
844
  }
786
845
  export function isOptionalUnconfiguredClaudeAdapter(report, codexAdapter) {
787
846
  return Boolean(report
@@ -1,6 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  import { runMemoryCli } from "./memory/cli.js";
3
3
  runMemoryCli(process.argv.slice(2)).then((result) => {
4
+ if (!process.argv.includes("--json") && result.ok && result.quotaNotice) {
5
+ console.warn(result.quotaNotice);
6
+ }
4
7
  if (!process.argv.includes("--json") && result.ok && result.action === "memory.search") {
5
8
  if (result.userNotice)
6
9
  console.warn(`Warning: ${result.userNotice}`);
@@ -48,6 +48,7 @@ export async function retrieveAutomaticMemoryContext(options) {
48
48
  }, {
49
49
  config,
50
50
  diagnosticLogger: options.diagnosticLogger,
51
+ env,
51
52
  fetchImpl: options.fetchImpl,
52
53
  observability: options.memoryObservability,
53
54
  observabilitySource: options.memoryObservabilitySource ?? "automatic_retrieval",
@@ -57,6 +58,9 @@ export async function retrieveAutomaticMemoryContext(options) {
57
58
  if (!response.ok)
58
59
  return finish(skipped("retrieve_failed", response.error));
59
60
  const result = response.result ?? {};
61
+ const userNotice = result.quota && options.claimQuotaNotice
62
+ ? await options.claimQuotaNotice(config, result.quota, { diagnosticLogger: options.diagnosticLogger, env })
63
+ : undefined;
60
64
  const payload = isRecord(result.tool_result_payload) ? result.tool_result_payload : {};
61
65
  const memoryItems = Array.isArray(payload.items) ? payload.items : [];
62
66
  const contextBlocks = Array.isArray(payload.contextBlocks) ? payload.contextBlocks : [];
@@ -69,7 +73,7 @@ export async function retrieveAutomaticMemoryContext(options) {
69
73
  itemCount: memoryItems.length,
70
74
  contextBlockCount: contextBlocks.length,
71
75
  blockChars,
72
- }));
76
+ }, userNotice));
73
77
  }
74
78
  return finish({
75
79
  context,
@@ -77,6 +81,7 @@ export async function retrieveAutomaticMemoryContext(options) {
77
81
  itemCount: memoryItems.length,
78
82
  contextBlockCount: contextBlocks.length,
79
83
  blockChars,
84
+ ...(userNotice ? { userNotice } : {}),
80
85
  });
81
86
  }
82
87
  function isAutomaticMemoryControlCommand(text) {
@@ -85,7 +90,7 @@ function isAutomaticMemoryControlCommand(text) {
85
90
  return true;
86
91
  return /^(?:memorax-code|memorax-cli)(?:\s|$)/i.test(trimmed);
87
92
  }
88
- function skipped(skipReason, error, fields = {}) {
93
+ function skipped(skipReason, error, fields = {}, userNotice) {
89
94
  return {
90
95
  retrieved: false,
91
96
  skipReason,
@@ -93,6 +98,7 @@ function skipped(skipReason, error, fields = {}) {
93
98
  contextBlockCount: fields.contextBlockCount ?? 0,
94
99
  blockChars: fields.blockChars ?? 0,
95
100
  ...(error ? { error } : {}),
101
+ ...(userNotice ? { userNotice } : {}),
96
102
  };
97
103
  }
98
104
  function automaticMemoryContextText(result) {
@@ -12,6 +12,7 @@ const AUTOMATIC_MEMORY_WRITEBACK_MAX_RETRY_DELAY_MS = 5_000;
12
12
  export function createAutomaticMemoryWritebackRuntime(options = {}) {
13
13
  const state = {
14
14
  diagnosticLogger: options.diagnosticLogger ?? (() => { }),
15
+ queueQuotaNotice: options.queueQuotaNotice,
15
16
  pendingWritebacks: new Map(),
16
17
  writebackBuffer: createMemoryWritebackBufferRuntime(),
17
18
  accepting: true,
@@ -243,8 +244,11 @@ async function enqueueAutomaticMemoryWritebackAsync(state, decision, options) {
243
244
  errorKind: response.ok ? undefined : response.errorKind,
244
245
  httpStatus: response.ok ? undefined : response.httpStatus,
245
246
  });
246
- if (response.ok)
247
+ if (response.ok) {
248
+ if (response.result.quota)
249
+ queueQuotaNoticeBestEffort(state, response.result.quota);
247
250
  break;
251
+ }
248
252
  if (!retrying) {
249
253
  releasePendingWritebacks(state, [decision.idempotencyKey, ...(decision.dedupeKeys ?? [])]);
250
254
  return;
@@ -283,6 +287,14 @@ function trackAutomaticMemoryWriteback(state, writeback) {
283
287
  state.inFlight.add(writeback);
284
288
  void writeback.then(() => state.inFlight.delete(writeback), () => state.inFlight.delete(writeback));
285
289
  }
290
+ function queueQuotaNoticeBestEffort(state, quota) {
291
+ try {
292
+ state.queueQuotaNotice?.(quota);
293
+ }
294
+ catch {
295
+ state.diagnosticLogger("memory.automatic_writeback.quota_notice_queue_failed", {});
296
+ }
297
+ }
286
298
  function hasPendingWriteback(state, idempotencyKey) {
287
299
  prunePendingWritebacks(state);
288
300
  return state.pendingWritebacks.has(idempotencyKey);
@@ -7,6 +7,7 @@ import { resolveConfiguredRepositoryMemory, } from "./repository-session.js";
7
7
  import { repositoryMemoryScopeContainsWorkspace, repositoryMemoryScopeKind, repositoryMemoryScopesMatch, } from "../repository/scope.js";
8
8
  import { isTraceClient } from "../trace/context.js";
9
9
  import { readCurrentTraceTurn, recordTraceEvent } from "../trace/store.js";
10
+ import { claimQuotaNotice, } from "./quota-notice.js";
10
11
  export async function runMemoryCli(args, options = {}) {
11
12
  const command = args[0] || "status";
12
13
  const env = options.env ?? process.env;
@@ -88,6 +89,7 @@ async function memorySearch(args, options) {
88
89
  }, {
89
90
  config: repositoryMemory.memory.config,
90
91
  diagnosticLogger: backendDebug,
92
+ env: options.env,
91
93
  fetchImpl: options.fetchImpl,
92
94
  observability: observability.hook,
93
95
  observabilitySource: "memory_cli",
@@ -96,6 +98,9 @@ async function memorySearch(args, options) {
96
98
  await observability.flush();
97
99
  if (!response.ok)
98
100
  return { ok: false, action: "memory.search", query, error: response.error };
101
+ const quotaNotice = response.result.quota
102
+ ? await (options.claimQuotaNotice ?? claimQuotaNotice)(repositoryMemory.memory.config, response.result.quota, { diagnosticLogger: backendDebug, env: options.env })
103
+ : undefined;
99
104
  const payload = isRecord(response.result.tool_result_payload) ? response.result.tool_result_payload : {};
100
105
  return {
101
106
  ok: true,
@@ -106,6 +111,7 @@ async function memorySearch(args, options) {
106
111
  answer: typeof payload.answer === "string" ? payload.answer : "",
107
112
  items: Array.isArray(payload.items) ? payload.items : [],
108
113
  receipt: response.result.dispatch_receipt ?? null,
114
+ ...(quotaNotice ? { quotaNotice } : {}),
109
115
  };
110
116
  }
111
117
  async function memoryAdd(args, options) {
@@ -167,12 +173,16 @@ async function memoryAdd(args, options) {
167
173
  await observability.flush();
168
174
  if (!response.ok)
169
175
  return { ok: false, action: "memory.add", error: response.error };
176
+ const quotaNotice = response.result.quota
177
+ ? await (options.claimQuotaNotice ?? claimQuotaNotice)(repositoryMemory.memory.config, response.result.quota, { diagnosticLogger: backendDebug, env })
178
+ : undefined;
170
179
  return {
171
180
  ok: true,
172
181
  action: "memory.add",
173
182
  provider: "memory.memorax",
174
183
  ...memoryCliIdentityFields(repositoryMemory.memory),
175
184
  receipt: response.result.dispatch_receipt ?? null,
185
+ ...(quotaNotice ? { quotaNotice } : {}),
176
186
  };
177
187
  }
178
188
  async function resolveMemoryCliRepositoryMemory(options) {
@@ -0,0 +1,208 @@
1
+ import { createHash } from "node:crypto";
2
+ import { join } from "node:path";
3
+ import { withJsonFileLockAsync, } from "../../../memorax-code-adapter-common/src/config-utils.mjs";
4
+ import { readJsonRuntimeRecord, writePrivateJsonRecord, } from "../../../memorax-code-adapter-common/src/runtime-record.mjs";
5
+ import { MEMORAX_ACCOUNT_URL, } from "../../../memorax-code-adapter-common/src/memorax-defaults.mjs";
6
+ import { defaultMemoraxCodeHome } from "../config/memorax-code.js";
7
+ const QUOTA_NOTICE_STATE_VERSION = 1;
8
+ const DEFAULT_NOTICE_TIMEOUT_MS = 6_000;
9
+ const LOCK_TIMEOUT_MS = 500;
10
+ const ANONYMOUS_QUOTA_LIMIT = 100;
11
+ const NOTICE_LEVELS = [10, 0];
12
+ const QUOTA_NOTICE_STATE_KEYS = new Set([
13
+ "version",
14
+ "connection_fingerprint",
15
+ "last_notified_write_level",
16
+ "last_notified_search_level",
17
+ ]);
18
+ const SHA256_PATTERN = /^[0-9a-f]{64}$/;
19
+ export const claimQuotaNotice = async (config, quota, options = {}) => {
20
+ const env = options.env ?? process.env;
21
+ const transitionState = options.transitionState ?? transitionQuotaNoticeState;
22
+ const timeoutMs = positiveTimeout(options.timeoutMs);
23
+ const controller = new AbortController();
24
+ const level = quotaNoticeLevel(quota);
25
+ const initial = initialQuotaNoticeState(config);
26
+ const noticeField = quota.featureCode === "memory_write"
27
+ ? "last_notified_write_level"
28
+ : "last_notified_search_level";
29
+ let timeout;
30
+ let claimed = false;
31
+ try {
32
+ await Promise.race([
33
+ transitionState(initial, (current) => {
34
+ const lastNotifiedLevel = current[noticeField];
35
+ if (level === undefined) {
36
+ return lastNotifiedLevel === null
37
+ ? undefined
38
+ : { ...current, [noticeField]: null };
39
+ }
40
+ if (lastNotifiedLevel !== null && level >= lastNotifiedLevel) {
41
+ return undefined;
42
+ }
43
+ claimed = true;
44
+ return { ...current, [noticeField]: level };
45
+ }, {
46
+ memoraxCodeHome: defaultMemoraxCodeHome(env),
47
+ signal: controller.signal,
48
+ timeoutMs: LOCK_TIMEOUT_MS,
49
+ }),
50
+ new Promise((_resolve, reject) => {
51
+ timeout = setTimeout(() => {
52
+ controller.abort();
53
+ reject(new Error("Quota notice state update timed out"));
54
+ }, timeoutMs);
55
+ timeout.unref?.();
56
+ }),
57
+ ]);
58
+ }
59
+ catch {
60
+ options.diagnosticLogger?.("memorax_quota_notice.update_failed", {});
61
+ return undefined;
62
+ }
63
+ finally {
64
+ if (timeout)
65
+ clearTimeout(timeout);
66
+ }
67
+ return claimed && level !== undefined
68
+ ? quotaNotice(quota, level, config.memoryOutputLanguage)
69
+ : undefined;
70
+ };
71
+ export function createPendingQuotaNoticeRuntime(options = {}) {
72
+ const { claimQuotaNotice: claim = claimQuotaNotice, ...noticeOptions } = options;
73
+ let pendingQuota;
74
+ let closed = false;
75
+ return {
76
+ queue(quota) {
77
+ if (closed || quota.featureCode !== "memory_write")
78
+ return;
79
+ if (!pendingQuota || quotaRemainingRatio(quota) < quotaRemainingRatio(pendingQuota)) {
80
+ pendingQuota = { ...quota };
81
+ }
82
+ },
83
+ async claim(config) {
84
+ if (closed || !pendingQuota)
85
+ return undefined;
86
+ const quota = pendingQuota;
87
+ pendingQuota = undefined;
88
+ try {
89
+ return await claim(config, quota, noticeOptions);
90
+ }
91
+ catch {
92
+ options.diagnosticLogger?.("memorax_quota_notice.claim_failed", {});
93
+ return undefined;
94
+ }
95
+ },
96
+ close() {
97
+ closed = true;
98
+ pendingQuota = undefined;
99
+ },
100
+ };
101
+ }
102
+ async function transitionQuotaNoticeState(initial, operation, options) {
103
+ const path = join(options.memoraxCodeHome, "runtime", "memory", "quota-notices.json");
104
+ return await withJsonFileLockAsync(path, async () => {
105
+ const stored = readQuotaNoticeState(path);
106
+ const current = stored?.connection_fingerprint === initial.connection_fingerprint
107
+ ? stored
108
+ : initial;
109
+ const next = operation(current);
110
+ if (!next)
111
+ return current;
112
+ writePrivateJsonRecord(path, next, { durableBoundary: options.memoraxCodeHome });
113
+ return next;
114
+ }, {
115
+ signal: options.signal,
116
+ timeoutMs: options.timeoutMs,
117
+ });
118
+ }
119
+ function readQuotaNoticeState(path) {
120
+ const state = readJsonRuntimeRecord(path);
121
+ if (state.status === "absent")
122
+ return undefined;
123
+ if (state.status !== "present")
124
+ throw new Error("Quota notice state is unreadable");
125
+ const value = state.value;
126
+ if (value.version !== QUOTA_NOTICE_STATE_VERSION
127
+ || Object.keys(value).some((key) => !QUOTA_NOTICE_STATE_KEYS.has(key))
128
+ || Object.keys(value).length !== QUOTA_NOTICE_STATE_KEYS.size
129
+ || typeof value.connection_fingerprint !== "string"
130
+ || !SHA256_PATTERN.test(value.connection_fingerprint)
131
+ || !validNoticeLevel(value.last_notified_write_level)
132
+ || !validNoticeLevel(value.last_notified_search_level)) {
133
+ throw new Error("Quota notice state is invalid");
134
+ }
135
+ return {
136
+ version: QUOTA_NOTICE_STATE_VERSION,
137
+ connection_fingerprint: value.connection_fingerprint,
138
+ last_notified_write_level: value.last_notified_write_level,
139
+ last_notified_search_level: value.last_notified_search_level,
140
+ };
141
+ }
142
+ function initialQuotaNoticeState(config) {
143
+ return {
144
+ version: QUOTA_NOTICE_STATE_VERSION,
145
+ connection_fingerprint: createHash("sha256")
146
+ .update(config.baseUrl)
147
+ .update("\0")
148
+ .update(config.apiKey)
149
+ .digest("hex"),
150
+ last_notified_write_level: null,
151
+ last_notified_search_level: null,
152
+ };
153
+ }
154
+ function positiveTimeout(value) {
155
+ return typeof value === "number" && Number.isSafeInteger(value) && value > 0
156
+ ? value
157
+ : DEFAULT_NOTICE_TIMEOUT_MS;
158
+ }
159
+ function quotaNoticeLevel(quota) {
160
+ if (quota.remaining === 0)
161
+ return 0;
162
+ if (quota.limit <= 0)
163
+ return undefined;
164
+ const percentage = quotaRemainingRatio(quota) * 100;
165
+ if (percentage <= 10)
166
+ return 10;
167
+ return undefined;
168
+ }
169
+ function quotaRemainingRatio(quota) {
170
+ return quota.limit === 0 ? 0 : quota.remaining / quota.limit;
171
+ }
172
+ function validNoticeLevel(value) {
173
+ return value === null || NOTICE_LEVELS.some((level) => value === level);
174
+ }
175
+ function quotaNotice(quota, level, language) {
176
+ if (language === "zh") {
177
+ const quotaName = quota.featureCode === "memory_write" ? "记忆写入" : "记忆搜索";
178
+ const quotaStatus = level === 0 ? "已用完" : `剩余不超过 ${level}%`;
179
+ if (quota.limit !== ANONYMOUS_QUOTA_LIMIT) {
180
+ return [
181
+ `额度提醒:您的 MemoraX Code ${quotaName}额度${quotaStatus}。`,
182
+ `请访问 ${MEMORAX_ACCOUNT_URL} 查看额度或管理账户。`,
183
+ ].join(" ");
184
+ }
185
+ return [
186
+ `额度提醒:您的 MemoraX Code ${quotaName}额度${quotaStatus}。`,
187
+ `请访问 ${MEMORAX_ACCOUNT_URL} 查看额度、注册或管理账户。`,
188
+ "若本机使用的是尚未注册的匿名身份,可以在本机终端运行 `memorax-code account --show-mark-id` 获取用于认领当前身份的 Mark ID。",
189
+ "请妥善保管,不要在聊天中分享。",
190
+ ].join(" ");
191
+ }
192
+ const quotaName = quota.featureCode === "memory_write" ? "memory write" : "memory search";
193
+ const quotaStatus = level === 0
194
+ ? "has been used up"
195
+ : `has ${level}% or less remaining`;
196
+ if (quota.limit !== ANONYMOUS_QUOTA_LIMIT) {
197
+ return [
198
+ `Quota reminder: Your MemoraX Code ${quotaName} quota ${quotaStatus}.`,
199
+ `Visit ${MEMORAX_ACCOUNT_URL} to view your quota or manage your account.`,
200
+ ].join(" ");
201
+ }
202
+ return [
203
+ `Quota reminder: Your MemoraX Code ${quotaName} quota ${quotaStatus}.`,
204
+ `Visit ${MEMORAX_ACCOUNT_URL} to view your quota, register, or manage your account.`,
205
+ "If this device uses an unregistered anonymous identity, run `memorax-code account --show-mark-id` in your local terminal to retrieve the Mark ID used to claim it.",
206
+ "Keep it private and do not share it in chat.",
207
+ ].join(" ");
208
+ }
@@ -5,9 +5,16 @@ import { createDshMemoryHookRuntime, } from "../clients/dsh/memory-hook-runtime.
5
5
  import { createOpenCodeMemoryHookRuntime, } from "../clients/opencode/memory-hook-runtime.js";
6
6
  import { createMemoryTurnCoordinator } from "./turn-coordinator.js";
7
7
  import { createRepositoryMemorySessionRuntime, } from "./repository-session.js";
8
+ import { createPendingQuotaNoticeRuntime } from "./quota-notice.js";
8
9
  export function createMemoryService(options = {}) {
10
+ const pendingQuotaNotice = createPendingQuotaNoticeRuntime({
11
+ claimQuotaNotice: options.claimQuotaNotice,
12
+ diagnosticLogger: options.diagnosticLogger,
13
+ env: options.env,
14
+ });
9
15
  const automaticWriteback = createAutomaticMemoryWritebackRuntime({
10
16
  diagnosticLogger: options.diagnosticLogger,
17
+ queueQuotaNotice: pendingQuotaNotice.queue,
11
18
  });
12
19
  const repositoryMemorySession = createRepositoryMemorySessionRuntime({
13
20
  onScopeUpgrade: automaticWriteback.discardForScopeUpgrade,
@@ -21,16 +28,19 @@ export function createMemoryService(options = {}) {
21
28
  });
22
29
  const codexHook = createCodexMemoryHookRuntime({
23
30
  ...options,
31
+ pendingQuotaNotice,
24
32
  repositoryMemorySession,
25
33
  turnCoordinator,
26
34
  });
27
35
  const claudeHook = createClaudeMemoryHookRuntime({
28
36
  ...options,
37
+ pendingQuotaNotice,
29
38
  repositoryMemorySession,
30
39
  turnCoordinator,
31
40
  });
32
41
  const openCodeHook = createOpenCodeMemoryHookRuntime({
33
42
  ...options,
43
+ pendingQuotaNotice,
34
44
  repositoryMemorySession,
35
45
  turnCoordinator,
36
46
  });
@@ -81,6 +91,7 @@ export function createMemoryService(options = {}) {
81
91
  turnCoordinator.close();
82
92
  repositoryMemorySession.close();
83
93
  automaticWriteback.close();
94
+ pendingQuotaNotice.close();
84
95
  },
85
96
  };
86
97
  }
@@ -40,7 +40,7 @@ export async function invokeMemoraxMemoryProvider(run, request, options = {}) {
40
40
  return repositoryScope;
41
41
  const payload = buildMemoraxSearchPayload(config, query, context, repositoryScope.scope);
42
42
  try {
43
- const raw = await callMemoSearch(config, payload, options.fetchImpl);
43
+ const { body: raw, quota } = await callMemoSearch(config, payload, options.fetchImpl);
44
44
  const items = extractMemoraxSearchItems(raw);
45
45
  const contextBlocks = renderMemoraxContextBlocks(items, config);
46
46
  const promptFragments = contextBlocks.map((block) => ({
@@ -72,6 +72,7 @@ export async function invokeMemoraxMemoryProvider(run, request, options = {}) {
72
72
  slot: request.slot || "state_context",
73
73
  operation,
74
74
  prompt_fragments: promptFragments,
75
+ ...(quota ? { quota } : {}),
75
76
  tool_result_payload: {
76
77
  answer: contextBlocks.map((block) => block.content).join("\n\n"),
77
78
  items,
@@ -195,7 +196,7 @@ async function invokeMemoraxWriteback(run, request, config, repositoryScope, opt
195
196
  return { ok: false, error: addOptions.error };
196
197
  const payload = buildMemoraxAddPayload(config, run, messages, context, idempotencyKey, repositoryScope, addOptions.options);
197
198
  try {
198
- const raw = await callMemoAdd(config, payload, options.fetchImpl);
199
+ const { body: raw, quota } = await callMemoAdd(config, payload, options.fetchImpl);
199
200
  recordMemoryObservabilityEvent(options, {
200
201
  operation: "writeback",
201
202
  ok: true,
@@ -217,6 +218,7 @@ async function invokeMemoraxWriteback(run, request, config, repositoryScope, opt
217
218
  slot: request.slot || "state_context",
218
219
  operation: "writeback",
219
220
  prompt_fragments: [],
221
+ ...(quota ? { quota } : {}),
220
222
  tool_result_payload: {
221
223
  accepted: true,
222
224
  receiptId: memoraxReceiptId(raw),