@memorax/memorax-code 0.1.1 → 0.1.2

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 (48) hide show
  1. package/bin/memorax-code-plugin-postinstall.mjs +88 -21
  2. package/docs/configuration.md +15 -0
  3. package/docs/troubleshooting.md +26 -0
  4. package/lib/memorax-code-adapter-common/src/hooks/memory-skill-reminder-hook.mjs +1 -1
  5. package/lib/memorax-code-adapter-common/src/repo-memory/repo-memory-auto-build.mjs +38 -0
  6. package/lib/memorax-code-backend/dist/automatic-memory-writeback.js +7 -0
  7. package/lib/memorax-code-backend/dist/claude-memory-hook-runtime.js +10 -3
  8. package/lib/memorax-code-backend/dist/codex-memory-hook-runtime.js +10 -3
  9. package/lib/memorax-code-backend/dist/memorax-cli.js +2 -0
  10. package/lib/memorax-code-backend/dist/memory-cli.js +24 -2
  11. package/lib/memorax-code-backend/dist/memory-service.js +3 -1
  12. package/lib/memorax-code-backend/dist/memory-turn-coordinator.js +6 -3
  13. package/lib/memorax-code-backend/dist/memory-viewer-icon.js +1 -0
  14. package/lib/memorax-code-backend/dist/memory-viewer-user-html.js +6 -5
  15. package/lib/memorax-code-backend/dist/memory-writeback-buffer.js +29 -0
  16. package/lib/memorax-code-backend/dist/repository-memory-context.js +24 -3
  17. package/lib/memorax-code-backend/dist/repository-memory-scope.js +81 -33
  18. package/lib/memorax-code-backend/dist/server-cli.js +30 -7
  19. package/lib/memorax-code-backend/dist/server-memory-viewer.js +11 -0
  20. package/lib/memorax-code-backend/package.json +1 -1
  21. package/lib/memorax-code-claude-adapter/.claude-plugin/plugin.json +1 -1
  22. package/lib/memorax-code-claude-adapter/hooks/runtime-shell.json +1 -1
  23. package/lib/memorax-code-claude-adapter/package.json +1 -1
  24. package/lib/memorax-code-claude-adapter/runtime-hooks/memory-turn.mjs +5 -0
  25. package/lib/memorax-code-claude-adapter/skills/memorax-code/SKILL.md +12 -13
  26. package/lib/memorax-code-claude-adapter/skills/memorax-code/references/memorax-add.md +4 -0
  27. package/lib/memorax-code-claude-adapter/skills/memorax-code/references/memorax-search.md +3 -1
  28. package/lib/memorax-code-claude-adapter/src/plugin-install.mjs +1 -0
  29. package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/.claude-plugin/plugin.json +1 -1
  30. package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/hooks/runtime-shell.json +1 -1
  31. package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/memorax-code-adapter-common/src/hooks/memory-skill-reminder-hook.mjs +1 -1
  32. package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/memorax-code-adapter-common/src/repo-memory/repo-memory-auto-build.mjs +38 -0
  33. package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/package.json +1 -1
  34. package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/runtime-hooks/memory-turn.mjs +5 -0
  35. package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/skills/memorax-code/SKILL.md +12 -13
  36. package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/skills/memorax-code/references/memorax-add.md +4 -0
  37. package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/skills/memorax-code/references/memorax-search.md +3 -1
  38. package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/src/plugin-install.mjs +1 -0
  39. package/lib/memorax-code-codex-adapter/.codex-plugin/plugin.json +1 -1
  40. package/lib/memorax-code-codex-adapter/hooks/runtime-shell.json +1 -1
  41. package/lib/memorax-code-codex-adapter/package.json +1 -1
  42. package/lib/memorax-code-codex-adapter/runtime-hooks/memory-skill-reminder.mjs +6 -0
  43. package/lib/memorax-code-codex-adapter/skills/memorax-code/SKILL.md +12 -13
  44. package/lib/memorax-code-codex-adapter/skills/memorax-code/references/memorax-add.md +4 -0
  45. package/lib/memorax-code-codex-adapter/skills/memorax-code/references/memorax-search.md +3 -1
  46. package/lib/resolve-claude-command.mjs +154 -1
  47. package/lib/resolve-codex-command.mjs +56 -1
  48. package/package.json +1 -1
@@ -48,7 +48,7 @@ const codexCommand = codexRuntime.command;
48
48
  const claudeRuntime = ensureClaudeCommandEnv();
49
49
  const claudeCommand = claudeRuntime.command;
50
50
  const updatePostinstall = postinstallUpdateMode();
51
- const scriptedAnswers = (canPrompt() || canPromptForHookUpdate()) && process.stdin.isTTY !== true
51
+ const scriptedAnswers = (canPrompt() || canPromptForUpdate()) && process.stdin.isTTY !== true
52
52
  ? parseScriptedAnswers(readFileSync(0, "utf8"))
53
53
  : undefined;
54
54
  const previousClients = readPersistedClientSelection();
@@ -70,20 +70,31 @@ try {
70
70
  process.exit(0);
71
71
  }
72
72
  runCommonPreflight();
73
- const requestedClients = previousClients ?? ["codex", "claude"];
73
+ const requestedClients = ["codex", "claude"];
74
74
  const codexPreflight = requestedClients.includes("codex") && !skipCodexPluginInstall
75
- ? runCodexPreflight()
75
+ ? runCodexPreflight({
76
+ integrationSelected: !updatePostinstall
77
+ || previousClients === undefined
78
+ || previousClients.includes("codex"),
79
+ })
76
80
  : { ok: true, pluginCache: { marketplaceName: CLI_MARKETPLACE_NAME, versions: [] } };
77
81
  const claudePreflight = requestedClients.includes("claude") && !skipClaudeAdapterInstall
78
- ? runClaudePreflight()
82
+ ? runClaudePreflight({
83
+ integrationSelected: !updatePostinstall
84
+ || previousClients === undefined
85
+ || previousClients.includes("claude"),
86
+ })
79
87
  : { ok: true };
80
- const installClients = requestedClients.filter((client) => {
88
+ const detectedClients = requestedClients.filter((client) => {
81
89
  if (client === "codex") return !skipCodexPluginInstall && codexPreflight.ok;
82
90
  return !skipClaudeAdapterInstall && claudePreflight.ok;
83
91
  });
84
- const selectedClients = previousClients ?? installClients;
85
- if (previousClients) {
86
- log(clientSelectionMessage(previousClients));
92
+ const selectedClients = updatePostinstall && previousClients !== undefined
93
+ ? await chooseUpdateClients(previousClients, detectedClients, scriptedAnswers)
94
+ : detectedClients;
95
+ const installClients = detectedClients.filter((client) => selectedClients.includes(client));
96
+ if (updatePostinstall && previousClients !== undefined) {
97
+ log(clientSelectionMessage(selectedClients));
87
98
  } else {
88
99
  log(detectedClientMessage(installClients));
89
100
  }
@@ -110,7 +121,11 @@ if (memoraxConfigResult === "configured") {
110
121
  }
111
122
  const codexClientEnabled = installClients.includes("codex");
112
123
  const claudeClientEnabled = installClients.includes("claude");
113
- const codexHooksBeforeUpdate = codexClientEnabled && updatePostinstall
124
+ const codexClientNewlyEnabled = codexClientEnabled
125
+ && updatePostinstall
126
+ && previousClients !== undefined
127
+ && !previousClients.includes("codex");
128
+ const codexHooksBeforeUpdate = codexClientEnabled && updatePostinstall && !codexClientNewlyEnabled
114
129
  ? inspectCodexPluginHooksForUpdate()
115
130
  : undefined;
116
131
  const result = codexClientEnabled
@@ -122,8 +137,13 @@ if (codexClientEnabled && result.status !== 0 && process.env.npm_lifecycle_event
122
137
  }
123
138
 
124
139
  if (codexClientEnabled && result.status === 0) {
125
- if (updatePostinstall) await maybeTrustUpdatedCodexPluginHooks(scriptedAnswers, codexHooksBeforeUpdate);
126
- else await maybeActivateCodexPluginHooks(scriptedAnswers);
140
+ if (codexClientNewlyEnabled) {
141
+ await maybeActivateCodexPluginHooks(scriptedAnswers, { updatePrompt: true });
142
+ } else if (updatePostinstall) {
143
+ await maybeTrustUpdatedCodexPluginHooks(scriptedAnswers, codexHooksBeforeUpdate);
144
+ } else {
145
+ await maybeActivateCodexPluginHooks(scriptedAnswers);
146
+ }
127
147
  }
128
148
  const skipCodexAdapter = !codexClientEnabled;
129
149
  const skipClaudeAdapter = !claudeClientEnabled;
@@ -194,6 +214,45 @@ async function maybeConfigureMemoraxMemory(scriptedAnswers) {
194
214
  }
195
215
  }
196
216
 
217
+ async function chooseUpdateClients(previousClients, detectedClients, scriptedAnswers) {
218
+ const selected = new Set(previousClients);
219
+ const availableDisabledClients = detectedClients.filter((client) => !selected.has(client));
220
+ if (availableDisabledClients.length === 0) return [...previousClients];
221
+
222
+ if (!canPromptForUpdate()) {
223
+ for (const client of availableDisabledClients) {
224
+ const label = client === "codex" ? "Codex" : "Claude Code";
225
+ log(`${label} runtime is available, but its integration remains disabled because this update cannot prompt. Rerun \`memorax-code update\` from an interactive terminal to choose whether to enable it.`);
226
+ }
227
+ return [...previousClients];
228
+ }
229
+
230
+ let rl;
231
+ try {
232
+ for (const client of availableDisabledClients) {
233
+ const label = client === "codex" ? "Codex" : "Claude Code";
234
+ const question = `${label} runtime is available, but its integration is disabled in [clients]. Enable it now? [Y/n]`;
235
+ let answer;
236
+ if (scriptedAnswers) {
237
+ log(question);
238
+ answer = String(scriptedAnswers.shift() ?? "").trim();
239
+ } else {
240
+ rl ??= createInterface({ input: process.stdin, output: process.stderr });
241
+ answer = (await rl.question(`${PREFIX} ${question} `)).trim();
242
+ }
243
+ if (/^n(?:o)?$/i.test(answer)) {
244
+ log(`Keeping the ${label} integration disabled.`);
245
+ } else {
246
+ selected.add(client);
247
+ logGreen(`Enabling the ${label} integration.`);
248
+ }
249
+ }
250
+ } finally {
251
+ rl?.close();
252
+ }
253
+ return ["codex", "claude"].filter((client) => selected.has(client));
254
+ }
255
+
197
256
  async function configureMemoraxMemoryFromAnswers(answers) {
198
257
  if (answers.length === 0) {
199
258
  log(`No MemoraX connection response was received. Register at ${MEMORAX_ACCOUNT_URL}, then edit \`~/.memorax-code/config.toml\` or rerun interactively with \`--foreground-scripts\`.`);
@@ -350,7 +409,7 @@ async function maybeTrustUpdatedCodexPluginHooks(scriptedAnswers, previousHooks)
350
409
  if (report.hooks.length === 0) return "unchanged";
351
410
 
352
411
  printUpdatedCodexHooks(report.hooks, previousHooks);
353
- if (!canPromptForHookUpdate()) {
412
+ if (!canPromptForUpdate()) {
354
413
  warnUpdatedHookTrustSkipped("This update is running without an interactive terminal, so the new or changed Hooks remain untrusted.");
355
414
  return "skipped";
356
415
  }
@@ -470,8 +529,8 @@ function warnUpdatedHookTrustSkipped(message) {
470
529
  logRed("Review and authorize the current MemoraX Code Codex Hooks with `memorax-code codex-plugin trust-hooks`.");
471
530
  }
472
531
 
473
- async function maybeActivateCodexPluginHooks(scriptedAnswers) {
474
- if (!canPrompt()) {
532
+ async function maybeActivateCodexPluginHooks(scriptedAnswers, { updatePrompt = false } = {}) {
533
+ if (!(updatePrompt ? canPromptForUpdate() : canPrompt())) {
475
534
  log("Codex hook activation was not prompted. Run `memorax-code codex-plugin activate --yes` later to activate and trust MemoraX Code Codex Adapter hooks.");
476
535
  return "skipped";
477
536
  }
@@ -512,7 +571,7 @@ function canPrompt() {
512
571
  return canPromptOnStderr();
513
572
  }
514
573
 
515
- function canPromptForHookUpdate() {
574
+ function canPromptForUpdate() {
516
575
  return updatePostinstall && canPromptOnStderr();
517
576
  }
518
577
 
@@ -705,7 +764,7 @@ function tomlString(value) {
705
764
  function runCommonPreflight() {
706
765
  log("Checking local install state...");
707
766
  if (updatePostinstall) {
708
- log("Package update detected; refreshing MemoraX Code assets without interactive setup prompts.");
767
+ log("Package update detected; refreshing MemoraX Code assets and checking client availability.");
709
768
  }
710
769
  const memoraxCodeVersion = runNodeMemoraxCodeCommand(["--version"], { print: false });
711
770
  log(`MemoraX Code backend package: ${commandSummary(memoraxCodeVersion) ?? packageVersionSummary()}`);
@@ -718,7 +777,7 @@ function runCommonPreflight() {
718
777
  return {};
719
778
  }
720
779
 
721
- function runCodexPreflight() {
780
+ function runCodexPreflight({ integrationSelected = true } = {}) {
722
781
  const version = runExternalCommand(codexCommand, ["--version"], { print: false, timeout: 10_000 });
723
782
  const runtimeLabel = codexRuntime.source === "app-bundled"
724
783
  ? "Codex App runtime"
@@ -732,16 +791,24 @@ function runCodexPreflight() {
732
791
  const pluginCache = installedPluginCache();
733
792
  log(`Existing Codex plugin cache: ${pluginCache.versions.length > 0 ? `found (${pluginCache.versions.join(", ")})` : "not installed"}`);
734
793
  log(`Codex client process: ${codexClientRunning() ? "running" : "not detected"}`);
735
- log("Keeping Codex provider config unchanged and enabling the shared memory hook integration.");
794
+ log(integrationSelected
795
+ ? "Keeping Codex provider config unchanged and enabling the shared memory hook integration."
796
+ : "Keeping Codex provider config unchanged while checking whether to enable its integration.");
736
797
  return { ok: true, pluginCache };
737
798
  }
738
799
 
739
- function runClaudePreflight() {
800
+ function runClaudePreflight({ integrationSelected = true } = {}) {
740
801
  const version = runExternalCommand(claudeCommand, ["--version"], { print: false, timeout: 10_000 });
741
- const runtimeLabel = claudeRuntime.source === "vscode-bundled" ? "Claude VS Code runtime" : "Claude CLI";
802
+ const runtimeLabel = claudeRuntime.source === "app-bundled"
803
+ ? "Claude Code App runtime"
804
+ : claudeRuntime.source === "vscode-bundled"
805
+ ? "Claude VS Code runtime"
806
+ : "Claude CLI";
742
807
  log(`${runtimeLabel}: ${commandSummary(version) ?? "not runnable"}`);
743
808
  if (version.status !== 0) return { ok: false };
744
- log("Keeping Claude Code provider config unchanged and enabling the shared memory Hook integration.");
809
+ log(integrationSelected
810
+ ? "Keeping Claude Code provider config unchanged and enabling the shared memory Hook integration."
811
+ : "Keeping Claude Code provider config unchanged while checking whether to enable its integration.");
745
812
  return { ok: true };
746
813
  }
747
814
 
@@ -59,6 +59,16 @@ disabled. The command-line override is:
59
59
  --clients codex|claude|codex,claude|all|none
60
60
  ```
61
61
 
62
+ A normal npm install or reinstall refreshes `[clients]` from the runnable
63
+ clients detected at that time. Update-mode postinstall runs preserve enabled
64
+ clients and also probe each disabled client. An interactive update offers each
65
+ runnable disabled integration for activation with a default of yes. Declining
66
+ the prompt, or running non-interactively, keeps that integration disabled. A
67
+ selected client that is temporarily unavailable also remains selected in the
68
+ configuration instead of being permanently disabled. When an update newly
69
+ enables Codex, it requests initial Hook activation after the client-selection
70
+ prompt.
71
+
62
72
  Client selection controls plugin and Hook lifecycle only. It does not change
63
73
  Codex or Claude Code provider settings. `--clients none` runs the Backend
64
74
  without managing either client integration.
@@ -167,6 +177,11 @@ Supported policies are `every-commit`, `commit-count`, `daily`,
167
177
  `pull-request`, `pull-request-or-daily`, and `adaptive`. Invalid policy values
168
178
  fall back to `adaptive`.
169
179
 
180
+ The first eligible prompt starts a background build only when the Backend has
181
+ authorized a Git worktree and that worktree has no `.repo_memory/PROFILE.md`.
182
+ If the Backend or workspace authority is unavailable, the Hook skips the
183
+ initial build instead of falling back to its local `cwd`.
184
+
170
185
  ## Local traces
171
186
 
172
187
  `[trace.codex]` and `[trace.claude]` support the same fields:
@@ -147,6 +147,32 @@ MemoraX Code reads filesystem Git metadata without executing Git. Linked
147
147
  worktrees share the remote repository identity; non-Git workspaces use the
148
148
  normalized folder name. Resolution never falls back to the bare base user ID.
149
149
 
150
+ A live Codex or Claude Code session remains pinned to the repository or local
151
+ workspace resolved at the start of the session. Starting the client from a
152
+ parent workspace and then entering a nested Git repository does not rebind the
153
+ session. The only in-session scope upgrade is from a direct `.git` directory
154
+ whose internal metadata was malformed or incomplete to a verified Git
155
+ repository at the same canonical workspace root and for the same Base User ID.
156
+
157
+ During that degraded state, MemoraX Code reports
158
+ `workspaceScopeFallbackReason: git_metadata_invalid` for manual CLI operations
159
+ and continues Search and Add with the normalized local folder name. Automatic
160
+ writeback also continues under that fallback scope without interrupting the
161
+ client task. The reported `effectiveUserId` identifies the fallback namespace
162
+ and may differ from the restored Git repository namespace. Repair the
163
+ repository or restore valid `.git` metadata, and later Search, Add, and
164
+ automatic writeback in the same client session automatically use the verified
165
+ Git repository scope. Memories already accepted under the fallback namespace
166
+ are not migrated. Any unsent automatic-writeback buffer for the fallback scope
167
+ is discarded instead of being flushed into either namespace.
168
+
169
+ Invalid or unreadable Git marker files, symlinked markers, and session scope
170
+ conflicts do not use this fallback. If Search or Add reports
171
+ `workspace_scope_mismatch` or `workspace_scope_unavailable`, start a new
172
+ session from the target repository or local workspace and verify that its
173
+ `.git` metadata is readable and valid. These failures stop Search or Add before
174
+ any request is sent to MemoraX.
175
+
150
176
  Codex projectless tasks under its canonical dated-task location intentionally
151
177
  use the shared `Codex-General` memory name. Open a task in a real workspace
152
178
  when repository isolation matters.
@@ -182,7 +182,7 @@ function combinedReminderContext(options, due, cadenceReminderContext, personalM
182
182
 
183
183
  function memoryReminderContext(options) {
184
184
  const invocation = stringOption(options.memorySkillInvocation) ?? DEFAULT_MEMORY_SKILL_INVOCATION;
185
- return `MemoraX Code reminder: proactively invoke ${invocation} whenever coding memory might help, even when uncertain; when in doubt, run one focused coding-memory search for relevant prior knowledge. Also use ${invocation} for repository-scoped personal memory, and classify the authority before reading or writing.`;
185
+ return `MemoraX Code reminder: proactively invoke ${invocation} whenever coding memory might help, even when uncertain; follow the skill's router to decide whether any memory operation is needed. Also use ${invocation} for repository-scoped personal memory, and classify the authority before reading or writing.`;
186
186
  }
187
187
 
188
188
  function defaultMemoraxCodeHome() {
@@ -0,0 +1,38 @@
1
+ import { spawn } from "node:child_process";
2
+ import { existsSync } from "node:fs";
3
+ import { join } from "node:path";
4
+
5
+ export function scheduleMissingRepoMemoryBuild(repo, options = {}) {
6
+ try {
7
+ const repoPath = nonEmptyString(repo);
8
+ const pluginRoot = nonEmptyString(options.pluginRoot);
9
+ if (!repoPath || !pluginRoot) return false;
10
+ if (existsSync(join(repoPath, ".repo_memory", "PROFILE.md"))) return false;
11
+
12
+ const jobHookPath = join(pluginRoot, "hooks", "repo-memory-job.mjs");
13
+ if (!existsSync(jobHookPath)) return false;
14
+ const child = spawn(process.execPath, [jobHookPath, "maintain", "--repo", repoPath], {
15
+ cwd: repoPath,
16
+ detached: true,
17
+ env: process.env,
18
+ stdio: "ignore",
19
+ windowsHide: true,
20
+ });
21
+ child.once("error", (error) => debug(options, error));
22
+ child.unref();
23
+ return true;
24
+ } catch (error) {
25
+ debug(options, error);
26
+ return false;
27
+ }
28
+ }
29
+
30
+ function nonEmptyString(value) {
31
+ return typeof value === "string" && value.trim() ? value.trim() : undefined;
32
+ }
33
+
34
+ function debug(options, error) {
35
+ if (process.env[options.debugEnv] === "1") {
36
+ console.error(error instanceof Error ? error.message : String(error));
37
+ }
38
+ }
@@ -20,6 +20,13 @@ export function createAutomaticMemoryWritebackRuntime(options = {}) {
20
20
  enqueue(options) {
21
21
  return enqueueAutomaticMemoryWritebackForRuntime(state, options);
22
22
  },
23
+ discardForScopeUpgrade(upgrade) {
24
+ return state.writebackBuffer.discardForScopeUpgrade({
25
+ client: upgrade.client,
26
+ sessionKey: upgrade.sessionId,
27
+ currentScope: upgrade.currentScope,
28
+ });
29
+ },
23
30
  drain() {
24
31
  if (state.drainPromise)
25
32
  return state.drainPromise;
@@ -2,7 +2,7 @@ import { readClaudeInterruptedTranscriptTurn, readClaudeTranscriptTurn, } from "
2
2
  import { retrieveAutomaticMemoryContext } from "./automatic-memory-retrieval.js";
3
3
  import { createAutomaticMemoryWritebackRuntime, } from "./automatic-memory-writeback.js";
4
4
  import { createMemoryTurnCoordinator, } from "./memory-turn-coordinator.js";
5
- import { createRepositoryMemorySessionRuntime, } from "./repository-memory-context.js";
5
+ import { createRepositoryMemorySessionRuntime, resolvedRepoMemoryWorktree, } from "./repository-memory-context.js";
6
6
  import { traceContextFromClaudeHookBody } from "./trace-context.js";
7
7
  import { markCurrentClaudeTurnOutcome, readOpenClaudeTurn, recordClaudeTraceEvent, traceTurnEventId, writeCurrentClaudeTurn, } from "./trace-store.js";
8
8
  const CLAUDE_MEMORY_TURN_CLIENT = "claude-code";
@@ -23,7 +23,9 @@ export function createClaudeMemoryHookRuntime(options = {}) {
23
23
  cleanupIntervalMs: options.cleanupIntervalMs,
24
24
  });
25
25
  const ownsTurnCoordinator = options.turnCoordinator === undefined;
26
- const repositoryMemorySession = options.repositoryMemorySession ?? createRepositoryMemorySessionRuntime();
26
+ const repositoryMemorySession = options.repositoryMemorySession ?? createRepositoryMemorySessionRuntime({
27
+ onScopeUpgrade: automaticWritebackRuntime?.discardForScopeUpgrade,
28
+ });
27
29
  const ownsRepositoryMemorySession = options.repositoryMemorySession === undefined;
28
30
  const automaticRetrievalPrompts = new Set();
29
31
  const automaticRetrievalPromptLimit = positiveInteger(options.maxEntries, 256);
@@ -45,6 +47,7 @@ export function createClaudeMemoryHookRuntime(options = {}) {
45
47
  await reconcilePreviousInterruptedTurn(turnCoordinator, turn, options, now);
46
48
  turnCoordinator.pruneExpired();
47
49
  const repositoryMemory = await resolveHookRepositoryMemory(turn, options, repositoryMemorySession);
50
+ const repoMemoryWorktree = resolvedRepoMemoryWorktree(repositoryMemory);
48
51
  turnCoordinator.recordTurnStart({
49
52
  client: CLAUDE_MEMORY_TURN_CLIENT,
50
53
  sessionId: turn.sessionId,
@@ -84,7 +87,10 @@ export function createClaudeMemoryHookRuntime(options = {}) {
84
87
  now: () => new Date(now()),
85
88
  }), options.diagnosticLogger);
86
89
  if (!claimAutomaticRetrievalPrompt(automaticRetrievalPrompts, automaticRetrievalPromptLimit, turn.sessionId, turn.promptId)) {
87
- return { ok: true };
90
+ return {
91
+ ok: true,
92
+ ...(repoMemoryWorktree ? { repoMemoryWorktree } : {}),
93
+ };
88
94
  }
89
95
  const retrieval = await retrieveAutomaticMemoryContext({
90
96
  diagnosticLogger: options.diagnosticLogger,
@@ -99,6 +105,7 @@ export function createClaudeMemoryHookRuntime(options = {}) {
99
105
  });
100
106
  return {
101
107
  ok: true,
108
+ ...(repoMemoryWorktree ? { repoMemoryWorktree } : {}),
102
109
  ...(retrieval.context ? { additionalContext: retrieval.context } : {}),
103
110
  };
104
111
  },
@@ -4,7 +4,7 @@ import { retrieveAutomaticMemoryContext } from "./automatic-memory-retrieval.js"
4
4
  import { readCodexSessionTurnIndex } from "./codex-session-turn-index.js";
5
5
  import { resolveCodexWorkspaceRoot } from "./codex-workspace-links.js";
6
6
  import { createMemoryTurnCoordinator, } from "./memory-turn-coordinator.js";
7
- import { createRepositoryMemorySessionRuntime, } from "./repository-memory-context.js";
7
+ import { createRepositoryMemorySessionRuntime, resolvedRepoMemoryWorktree, } from "./repository-memory-context.js";
8
8
  import { traceContextFromHookBody } from "./trace-context.js";
9
9
  import { markCurrentCodexTurnOutcome, readCurrentCodexTurn, readOpenCodexTurn, recordCodexTraceEvent, traceTurnEventId, writeCurrentCodexTurn, } from "./trace-store.js";
10
10
  const CODEX_MEMORY_TURN_CLIENT = "codex";
@@ -23,7 +23,9 @@ export function createCodexMemoryHookRuntime(options = {}) {
23
23
  cleanupIntervalMs: options.cleanupIntervalMs,
24
24
  });
25
25
  const ownsTurnCoordinator = options.turnCoordinator === undefined;
26
- const repositoryMemorySession = options.repositoryMemorySession ?? createRepositoryMemorySessionRuntime();
26
+ const repositoryMemorySession = options.repositoryMemorySession ?? createRepositoryMemorySessionRuntime({
27
+ onScopeUpgrade: automaticWritebackRuntime?.discardForScopeUpgrade,
28
+ });
27
29
  const ownsRepositoryMemorySession = options.repositoryMemorySession === undefined;
28
30
  const automaticRetrievalTurns = new Set();
29
31
  const automaticRetrievalTurnLimit = positiveInteger(options.maxEntries, 256);
@@ -44,6 +46,7 @@ export function createCodexMemoryHookRuntime(options = {}) {
44
46
  resolveHookRepositoryMemory(turn, options, repositoryMemorySession),
45
47
  resolveSessionTurnIndex(turn, options.diagnosticLogger),
46
48
  ]);
49
+ const repoMemoryWorktree = resolvedRepoMemoryWorktree(repositoryMemory);
47
50
  if (turn.turnId) {
48
51
  turnCoordinator.recordTurnStart({
49
52
  client: CODEX_MEMORY_TURN_CLIENT,
@@ -90,7 +93,10 @@ export function createCodexMemoryHookRuntime(options = {}) {
90
93
  }), options.diagnosticLogger);
91
94
  if (!turn.turnId
92
95
  || !claimAutomaticRetrievalTurn(automaticRetrievalTurns, automaticRetrievalTurnLimit, turn.sessionId, turn.turnId)) {
93
- return { ok: true };
96
+ return {
97
+ ok: true,
98
+ ...(repoMemoryWorktree ? { repoMemoryWorktree } : {}),
99
+ };
94
100
  }
95
101
  const retrieval = await retrieveAutomaticMemoryContext({
96
102
  diagnosticLogger: options.diagnosticLogger,
@@ -105,6 +111,7 @@ export function createCodexMemoryHookRuntime(options = {}) {
105
111
  });
106
112
  return {
107
113
  ok: true,
114
+ ...(repoMemoryWorktree ? { repoMemoryWorktree } : {}),
108
115
  ...(retrieval.context ? { additionalContext: retrieval.context } : {}),
109
116
  };
110
117
  },
@@ -2,6 +2,8 @@
2
2
  import { runMemoryCli } from "./memory-cli.js";
3
3
  runMemoryCli(process.argv.slice(2)).then((result) => {
4
4
  if (!process.argv.includes("--json") && result.ok && result.action === "memory.search") {
5
+ if (result.userNotice)
6
+ console.warn(`Warning: ${result.userNotice}`);
5
7
  console.log(result.answer?.trim() || "No memory context returned.");
6
8
  }
7
9
  else {
@@ -72,7 +72,7 @@ async function memorySearch(args, options) {
72
72
  const query = queryResult.text;
73
73
  const repositoryMemory = await resolveMemoryCliRepositoryMemory(options);
74
74
  if (!repositoryMemory.ok) {
75
- return { ok: false, action: "memory.search", query, error: repositoryMemory.error };
75
+ return memoryCliRepositoryFailure("memory.search", repositoryMemory, { query });
76
76
  }
77
77
  const observability = await memoryCliObservability(options.env);
78
78
  const response = await invokeMemoraxMemoryProvider({ sessionId: memoryCliSessionId(args, options.env), prompt: query }, {
@@ -135,7 +135,7 @@ async function memoryAdd(args, options) {
135
135
  return { ok: false, action: "memory.add", error: contentOptions.error };
136
136
  const repositoryMemory = await resolveMemoryCliRepositoryMemory(options);
137
137
  if (!repositoryMemory.ok)
138
- return { ok: false, action: "memory.add", error: repositoryMemory.error };
138
+ return memoryCliRepositoryFailure("memory.add", repositoryMemory);
139
139
  const sessionId = memoryCliSessionId(args, env);
140
140
  const observability = await memoryCliObservability(env);
141
141
  const response = await invokeMemoraxMemoryProvider({ sessionId, prompt: memory }, {
@@ -226,6 +226,24 @@ async function resolveMemoryCliRepositoryMemory(options) {
226
226
  }
227
227
  return commandMemory;
228
228
  }
229
+ function memoryCliRepositoryFailure(action, failure, fields = {}) {
230
+ const userAction = failure.reason === "workspace_scope_mismatch"
231
+ ? "Start a new Codex or Claude Code session from the target repository or local workspace."
232
+ : failure.reason === "workspace_scope_unavailable"
233
+ ? "Start a new Codex or Claude Code session from the target repository or local workspace. If the problem continues, make sure its .git metadata is readable and valid."
234
+ : undefined;
235
+ return {
236
+ ok: false,
237
+ action,
238
+ ...fields,
239
+ ...(userAction ? {
240
+ workspaceScope: "unavailable",
241
+ workspaceScopeReason: failure.reason,
242
+ userAction,
243
+ } : {}),
244
+ error: failure.error,
245
+ };
246
+ }
229
247
  function memoryCliIdentityFields(memory) {
230
248
  return {
231
249
  baseUserId: memory.config.userId,
@@ -234,6 +252,10 @@ function memoryCliIdentityFields(memory) {
234
252
  scopeKind: repositoryMemoryScopeKind(memory.scope),
235
253
  effectiveUserId: memory.scope.effectiveUserId,
236
254
  workspaceScope: "bound",
255
+ ...(memory.scope.fallbackReason === "git_metadata_invalid" ? {
256
+ workspaceScopeFallbackReason: memory.scope.fallbackReason,
257
+ userNotice: `Git repository metadata is invalid or incomplete. MemoraX Code is using the local folder name "${memory.scope.repositorySlug}" for memory scope, so Search and Add use "${memory.scope.effectiveUserId}". Repair the repository or restore valid .git metadata. Later Search, Add, and automatic writeback in the same client session will automatically use the restored Git repository scope.`,
258
+ } : {}),
237
259
  } : {
238
260
  workspaceScope: "unavailable",
239
261
  }),
@@ -7,7 +7,9 @@ export function createMemoryService(options = {}) {
7
7
  const automaticWriteback = createAutomaticMemoryWritebackRuntime({
8
8
  diagnosticLogger: options.diagnosticLogger,
9
9
  });
10
- const repositoryMemorySession = createRepositoryMemorySessionRuntime();
10
+ const repositoryMemorySession = createRepositoryMemorySessionRuntime({
11
+ onScopeUpgrade: automaticWriteback.discardForScopeUpgrade,
12
+ });
11
13
  const turnCoordinator = createMemoryTurnCoordinator({
12
14
  automaticWriteback: automaticWriteback.enqueue,
13
15
  now: options.now,
@@ -1,4 +1,4 @@
1
- import { repositoryMemoryScopesMatch, } from "./repository-memory-scope.js";
1
+ import { repositoryMemoryScopeCanUpgradeFromDegradedGit, repositoryMemoryScopesMatch, } from "./repository-memory-scope.js";
2
2
  const DEFAULT_TTL_MS = 5 * 60 * 1000;
3
3
  const DEFAULT_MAX_ENTRIES = 256;
4
4
  const DEFAULT_CLEANUP_INTERVAL_MS = 60 * 1000;
@@ -72,10 +72,13 @@ export function createMemoryTurnCoordinator(options) {
72
72
  if (!currentScope) {
73
73
  return reject(input.metadata ? "workspace_scope_mismatch" : "workspace_scope_unavailable");
74
74
  }
75
- const repositoryScope = input.metadata?.repositoryScope ?? currentScope;
75
+ let repositoryScope = input.metadata?.repositoryScope ?? currentScope;
76
76
  if (input.metadata?.repositoryScope
77
77
  && !repositoryMemoryScopesMatch(input.metadata.repositoryScope, currentScope)) {
78
- return reject("workspace_scope_mismatch");
78
+ if (!repositoryMemoryScopeCanUpgradeFromDegradedGit(input.metadata.repositoryScope, currentScope)) {
79
+ return reject("workspace_scope_mismatch");
80
+ }
81
+ repositoryScope = currentScope;
79
82
  }
80
83
  const acceptance = options.automaticWriteback({
81
84
  ...input.writeback,
@@ -0,0 +1 @@
1
+ export const MEMORAX_ICON_DATA_URL = "data:image/png;base64,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";
@@ -5,6 +5,7 @@ export function memoryViewerUserHtml() {
5
5
  <meta charset="utf-8">
6
6
  <meta name="viewport" content="width=device-width,initial-scale=1">
7
7
  <meta name="theme-color" content="#f7f8fc">
8
+ <link rel="icon" type="image/png" sizes="64x64" href="/memory-viewer/favicon.png">
8
9
  <title>MemoraX Code Memory Viewer</title>
9
10
  <script>
10
11
  const THEME_STORAGE_KEY='memorax-code-memory-viewer-theme',LANGUAGE_STORAGE_KEY='memorax-code-memory-viewer-language';
@@ -27,8 +28,8 @@ export function memoryViewerUserHtml() {
27
28
  cardTurns:'会话',cardSearch:'搜索',cardAdd:'写入',cardRepo:'仓库记忆',loading:'正在加载',checking:'正在检查',repoLoading:'仓库知识状态',recentActivity:'最近活动',
28
29
  empty:'当前范围还没有会话、搜索或写入活动',error:'暂时无法加载记忆活动,请稍后重试。',selectProject:'选择仓库',selectProjectForStatus:'选择仓库后查看状态',noProjects:'暂无可识别仓库',unassignedProject:'未归属仓库',systemActivity:'系统活动',
29
30
  turnNote:'轮客户端会话',searchUnit:'次搜索',addUnit:'次写入',processingUnit:'个处理中',unknownUnit:'个数量未知',activityUnit:'条',
30
- repoStates:{ready:'可用',preparing:'准备中',not_ready:'未就绪',unknown:'未知'},
31
- reasons:{usable:'仓库知识可以使用',active_job:'正在生成或更新',bundle_missing:'尚未生成仓库知识',bundle_invalid:'仓库知识需要重新生成',project_unresolved:'无法定位仓库',validator_unavailable:'暂时无法检查'},
31
+ repoStates:{ready:'可用',preparing:'生成中',not_ready:'未就绪',unknown:'未知'},
32
+ reasons:{usable:'仓库知识可以使用',active_job:'正在生成或更新',bundle_missing:'正在生成仓库知识',bundle_invalid:'仓库知识需要重新生成',project_unresolved:'无法定位仓库',validator_unavailable:'暂时无法检查'},
32
33
  statuses:{completed:'完成',saved:'已保存',skipped:'未写入',processing:'进行中',interrupted:'已中断',failed:'失败',unknown:'数量未知'},
33
34
  sources:{client:'客户端会话',assistant:'助手主动',automatic:'系统自动'}
34
35
  },
@@ -39,8 +40,8 @@ export function memoryViewerUserHtml() {
39
40
  cardTurns:'Turns',cardSearch:'Search',cardAdd:'Add',cardRepo:'Repo Memory',loading:'Loading',checking:'Checking',repoLoading:'Repository knowledge status',recentActivity:'Recent activity',
40
41
  empty:'No turn, Search, or Add activity in the current scope',error:'Memory activity is temporarily unavailable. Please try again later.',selectProject:'Select a repository',selectProjectForStatus:'Select a repository to view its status',noProjects:'No repositories found',unassignedProject:'Unassigned repository',systemActivity:'System activity',
41
42
  turnNote:'client turns',searchUnit:'searches',addUnit:'adds',processingUnit:'processing',unknownUnit:'with unknown counts',activityUnit:'items',
42
- repoStates:{ready:'Ready',preparing:'Preparing',not_ready:'Not Ready',unknown:'Unknown'},
43
- reasons:{usable:'Repository knowledge is ready',active_job:'Generating or updating repository knowledge',bundle_missing:'Repository knowledge has not been generated',bundle_invalid:'Repository knowledge needs to be regenerated',project_unresolved:'Repository could not be resolved',validator_unavailable:'Status is temporarily unavailable'},
43
+ repoStates:{ready:'Ready',preparing:'Generating',not_ready:'Not Ready',unknown:'Unknown'},
44
+ reasons:{usable:'Repository knowledge is ready',active_job:'Generating or updating repository knowledge',bundle_missing:'Generating repository knowledge',bundle_invalid:'Repository knowledge needs to be regenerated',project_unresolved:'Repository could not be resolved',validator_unavailable:'Status is temporarily unavailable'},
44
45
  statuses:{completed:'Completed',saved:'Saved',skipped:'No Add',processing:'Processing',interrupted:'Interrupted',failed:'Failed',unknown:'Count unknown'},
45
46
  sources:{client:'Client session',assistant:'Assistant initiated',automatic:'Automatic'}
46
47
  }
@@ -54,7 +55,7 @@ export function memoryViewerUserHtml() {
54
55
  function setTheme(next,persist=true){theme=next;document.documentElement.dataset.theme=theme;document.querySelector('meta[name="theme-color"]').content=theme==='dark'?'#10121a':'#f7f8fc';if(persist)writePreference(THEME_STORAGE_KEY,theme);syncThemeButton()}
55
56
  function setLanguage(next,persist=true){language=next;if(persist)writePreference(LANGUAGE_STORAGE_KEY,language);syncStaticCopy();if(lastBody)render(lastBody);if(connectionState!==null)connection(connectionState)}
56
57
  function syncProjects(items){const current=selected;project.innerHTML='<option value="">'+esc(copy().allProjects)+'</option>'+items.map(item=>'<option value="'+esc(item.projectId)+'">'+esc(item.projectLabel)+'</option>').join('');project.value=items.some(item=>item.projectId===current)?current:'';selected=project.value}
57
- function repoState(items){const current=copy(),target=selected?items.find(item=>item.projectId===selected):items.length===1?items[0]:null,state=target?.repoMemory||{status:'unknown',reason:'project_unresolved'};E('repo-state').className='repo-state';E('repo-dot').className='state-dot '+state.status;E('repo-label').textContent=target?(current.repoStates[state.status]||current.repoStates.unknown):current.selectProject;E('repo-note').classList.remove('skeleton','loading-copy');E('repo-note').textContent=target?(target.projectLabel+' · '+(current.reasons[state.reason]||current.reasons.validator_unavailable)):items.length?current.selectProjectForStatus:current.noProjects}
58
+ function repoState(items){const current=copy(),target=selected?items.find(item=>item.projectId===selected):items.length===1?items[0]:null,state=target?.repoMemory||{status:'unknown',reason:'project_unresolved'},displayStatus=state.reason==='bundle_missing'?'preparing':state.status;E('repo-state').className='repo-state';E('repo-dot').className='state-dot '+displayStatus;E('repo-label').textContent=target?(current.repoStates[displayStatus]||current.repoStates.unknown):current.selectProject;E('repo-note').classList.remove('skeleton','loading-copy');E('repo-note').textContent=target?(target.projectLabel+' · '+(current.reasons[state.reason]||current.reasons.validator_unavailable)):items.length?current.selectProjectForStatus:current.noProjects}
58
59
  function memoryCount(value){if(language==='zh')return value+' 条记忆';return value+' '+(value===1?'memory':'memories')}
59
60
  function activityTitle(item){if(language==='zh'){if(item.kind==='turn')return item.status==='processing'?'会话进行中':item.status==='interrupted'?'会话已中断':'完成一轮会话';if(item.kind==='search')return item.status==='failed'?'搜索失败':'搜索 · '+memoryCount(item.count??0);if(item.status==='saved')return item.count===null?'写入 · 数量未知':'写入 · '+memoryCount(item.count);if(item.status==='skipped')return '写入 · 没有新记忆';if(item.status==='processing')return '写入处理中';if(item.status==='failed')return '写入失败';return '写入完成 · 数量未知'}if(item.kind==='turn')return item.status==='processing'?'Session in progress':item.status==='interrupted'?'Session interrupted':'Session turn completed';if(item.kind==='search')return item.status==='failed'?'Search failed':'Search · '+memoryCount(item.count??0);if(item.status==='saved')return item.count===null?'Add · Count unknown':'Add · '+memoryCount(item.count);if(item.status==='skipped')return 'Add · No new memory';if(item.status==='processing')return 'Add in progress';if(item.status==='failed')return 'Add failed';return 'Add completed · Count unknown'}
60
61
  function metaItem(value){return '<span class="meta-item">'+esc(value)+'</span>'}
@@ -1,5 +1,6 @@
1
1
  import { writebackMessagesContentChars } from "./memory-writeback-chunk.js";
2
2
  import { memoryWritebackBufferConfig } from "./memorax-config.js";
3
+ import { repositoryMemoryScopeCanUpgradeFromDegradedGit, } from "./repository-memory-scope.js";
3
4
  const SYSTEM_CLOCK = {
4
5
  now: () => Date.now(),
5
6
  setTimeout: (callback, delayMs) => setTimeout(callback, delayMs),
@@ -11,6 +12,9 @@ export function createMemoryWritebackBufferRuntime() {
11
12
  enqueue(decision, options, deps) {
12
13
  enqueueMemoryWritebackBufferForRuntime(writebackBuffers, decision, options, deps);
13
14
  },
15
+ discardForScopeUpgrade(upgrade) {
16
+ return discardDegradedGitBuffersForUpgrade(writebackBuffers, upgrade);
17
+ },
14
18
  flushAll(flushReason) {
15
19
  let flushed = 0;
16
20
  for (const [bufferKey, buffer] of [...writebackBuffers.entries()]) {
@@ -34,6 +38,11 @@ function enqueueMemoryWritebackBufferForRuntime(writebackBuffers, decision, opti
34
38
  const config = memoryWritebackBufferConfig(env);
35
39
  if (!options.repositoryScope)
36
40
  return;
41
+ discardDegradedGitBuffersForUpgrade(writebackBuffers, {
42
+ client: decision.client,
43
+ sessionKey: decision.sessionKey,
44
+ currentScope: options.repositoryScope,
45
+ });
37
46
  const bufferKey = memoryWritebackBufferKey(decision.client, decision.sessionKey, options.repositoryScope);
38
47
  let buffer = writebackBuffers.get(bufferKey);
39
48
  if (buffer?.turnKeys.has(decision.idempotencyKey)) {
@@ -85,6 +94,26 @@ function enqueueMemoryWritebackBufferForRuntime(writebackBuffers, decision, opti
85
94
  resetMemoryWritebackIdleTimer(writebackBuffers, buffer, config.maxAgeMs, deps);
86
95
  }
87
96
  }
97
+ function discardDegradedGitBuffersForUpgrade(writebackBuffers, upgrade) {
98
+ let discarded = 0;
99
+ for (const [bufferKey, buffer] of writebackBuffers.entries()) {
100
+ if (buffer.client !== upgrade.client
101
+ || buffer.sessionKey !== upgrade.sessionKey
102
+ || !repositoryMemoryScopeCanUpgradeFromDegradedGit(buffer.repositoryScope, upgrade.currentScope))
103
+ continue;
104
+ writebackBuffers.delete(bufferKey);
105
+ if (buffer.timer)
106
+ buffer.clock.clearTimeout(buffer.timer);
107
+ buffer.deps.debug("memory.automatic_writeback", {
108
+ scheduled: false,
109
+ skipReason: "buffer_scope_upgraded",
110
+ sessionKey: upgrade.sessionKey,
111
+ discardedTurnCount: buffer.turns.length,
112
+ });
113
+ discarded += 1;
114
+ }
115
+ return discarded;
116
+ }
88
117
  function createMemoryWritebackBuffer(bufferKey, client, sessionKey, env, options, repositoryScope, clock, deps) {
89
118
  const now = clock.now();
90
119
  const buffer = {