memgraph-context-graph 0.12.0__tar.gz → 0.12.2__tar.gz

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 (101) hide show
  1. {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/PKG-INFO +1 -1
  2. {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/docs/memory.md +53 -19
  3. {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/docs/research/2026-10-codex-memory.md +2 -0
  4. {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/pyproject.toml +1 -1
  5. {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/adapters/_spec.py +33 -5
  6. {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/adapters/antigravity_cli.py +3 -1
  7. {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/adapters/claude_code_memory.py +113 -39
  8. memgraph_context_graph-0.12.2/src/memgraph_context_graph/adapters/codex_memory.py +396 -0
  9. {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/connectors.py +1 -1
  10. {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/hooks/runner.py +83 -7
  11. {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/memory_backend.py +37 -7
  12. {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/setup/engine.py +19 -8
  13. {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/setup/tool.py +2 -2
  14. {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/tests/test_agent_memory_wiring.py +7 -1
  15. {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/tests/test_antigravity_cli_adapter.py +16 -0
  16. {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/tests/test_capture_log.py +43 -0
  17. memgraph_context_graph-0.12.2/tests/test_claude_code_memory.py +31 -0
  18. {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/tests/test_codex_adapter.py +13 -1
  19. memgraph_context_graph-0.12.2/tests/test_codex_memory.py +69 -0
  20. {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/tests/test_setup.py +1 -1
  21. memgraph_context_graph-0.12.0/src/memgraph_context_graph/adapters/codex_memory.py +0 -241
  22. {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/.gitignore +0 -0
  23. {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/CONTEXT.md +0 -0
  24. {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/LICENSE +0 -0
  25. {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/README.md +0 -0
  26. {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/docs/adr/0001-runtime-adapter-terminology.md +0 -0
  27. {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/docs/adr/0002-config-file-only-hook-resolution.md +0 -0
  28. {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/docs/adr/0003-config-path-override.md +0 -0
  29. {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/docs/adr/0004-user-id-in-session-start-event.md +0 -0
  30. {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/docs/adr/0005-config-file-covers-llm-key-and-subprocess-env.md +0 -0
  31. {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/docs/adr/0006-memory-tool-writes-explicit-memory.md +0 -0
  32. {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/docs/adr/0007-spawned-processes-read-the-config-file-themselves.md +0 -0
  33. {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/docs/command-hooks.md +0 -0
  34. {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/docs/configuration.md +0 -0
  35. {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/docs/extending.md +0 -0
  36. {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/docs/runtime-setup.md +0 -0
  37. {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/docs/sdk-memory.md +0 -0
  38. {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/examples/sdk_claude_agent.py +0 -0
  39. {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/examples/sdk_openai_agents.py +0 -0
  40. {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/__init__.py +0 -0
  41. {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/adapters/__init__.py +0 -0
  42. {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/adapters/_identity.py +0 -0
  43. {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/adapters/_opencode_plugin.js +0 -0
  44. {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/adapters/agent_mcp.py +0 -0
  45. {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/adapters/agent_memory.py +0 -0
  46. {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/adapters/claude_code.py +0 -0
  47. {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/adapters/codex.py +0 -0
  48. {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/adapters/copilot_cli.py +0 -0
  49. {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/adapters/cursor.py +0 -0
  50. {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/adapters/grok.py +0 -0
  51. {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/adapters/opencode.py +0 -0
  52. {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/capture_log.py +0 -0
  53. {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/capture_policy.py +0 -0
  54. {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/cli.py +0 -0
  55. {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/config_schema.py +0 -0
  56. {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/events.py +0 -0
  57. {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/forget.py +0 -0
  58. {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/hooks/__init__.py +0 -0
  59. {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/hooks/cli.py +0 -0
  60. {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/hooks/runtime_plugin.py +0 -0
  61. {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/link.py +0 -0
  62. {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/mcp_server.py +0 -0
  63. {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/protocols.py +0 -0
  64. {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/sdk/__init__.py +0 -0
  65. {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/sdk/claude.py +0 -0
  66. {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/sdk/openai.py +0 -0
  67. {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/session.py +0 -0
  68. {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/setup/__init__.py +0 -0
  69. {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/setup/agents.py +0 -0
  70. {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/setup/cli.py +0 -0
  71. {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/setup/memgraph.py +0 -0
  72. {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/setup/prompts.py +0 -0
  73. {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/setup/uninstall.py +0 -0
  74. {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/setup/update.py +0 -0
  75. {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/spaces.py +0 -0
  76. {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/terminal.py +0 -0
  77. {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/tools.py +0 -0
  78. {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/tests/__init__.py +0 -0
  79. {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/tests/conftest.py +0 -0
  80. {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/tests/fake_agent_cli.py +0 -0
  81. {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/tests/test_capture_policy.py +0 -0
  82. {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/tests/test_claude_code_adapter.py +0 -0
  83. {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/tests/test_config_cli.py +0 -0
  84. {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/tests/test_connectors.py +0 -0
  85. {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/tests/test_context_before_tool.py +0 -0
  86. {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/tests/test_copilot_cli_adapter.py +0 -0
  87. {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/tests/test_cursor_adapter.py +0 -0
  88. {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/tests/test_dev_scripts.py +0 -0
  89. {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/tests/test_events.py +0 -0
  90. {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/tests/test_grok_adapter.py +0 -0
  91. {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/tests/test_hook_cli.py +0 -0
  92. {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/tests/test_identity.py +0 -0
  93. {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/tests/test_link.py +0 -0
  94. {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/tests/test_opencode_adapter.py +0 -0
  95. {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/tests/test_plugin_hooks.py +0 -0
  96. {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/tests/test_runtime_plugin.py +0 -0
  97. {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/tests/test_session.py +0 -0
  98. {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/tests/test_setup_docker.py +0 -0
  99. {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/tests/test_spaces.py +0 -0
  100. {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/tests/test_terminal.py +0 -0
  101. {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/tests/test_tools.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: memgraph-context-graph
3
- Version: 0.12.0
3
+ Version: 0.12.2
4
4
  Summary: Connect agent SDKs to context-graph components (actions-graph, skills-graph, etc.)
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -89,7 +89,9 @@ back on, since without the plugin it would have none.)
89
89
 
90
90
  Opting in does three things, in this order:
91
91
  1. Imports the existing auto-memory files from every project once:
92
- - `user`/`feedback` memories go to `/memories/<type>/`;
92
+ - `user`/`feedback` memories go to `/memories/<type>/`. When another
93
+ project already put a different memory of the same name there, this one
94
+ goes to its own project's folder instead, so neither is lost;
93
95
  - `project`/`reference` memories go to that project's folder;
94
96
  - `MEMORY.md` is skipped, and anything already in the graph is never overwritten.
95
97
  2. Sets `autoMemoryEnabled: false` in `~/.claude/settings.json`, the user-level
@@ -100,12 +102,20 @@ Opting in does three things, in this order:
100
102
  For Codex, `setup codex --memory-backend context-graph` does the same against
101
103
  its global store, with three differences:
102
104
  - **Import.** `memory_summary.md`, the profile Codex injected each session,
103
- becomes `/memories/user/codex-memory-summary.md`, and `MEMORY.md` becomes
104
- `/memories/codex/memory.md`.
105
+ becomes `/memories/codex/memory-summary.md`, and `MEMORY.md` becomes
106
+ `/memories/codex/memory.md`. Both stay under `/memories/codex/` rather than
107
+ the user-wide root, since Codex wrote them across every project.
105
108
  - **Switch.** It turns off `features.memories`, `memories.use_memories` and
106
109
  `memories.generate_memories` in `~/.codex/config.toml` (or under
107
- `$CODEX_HOME`), keeping the user's comments and layout. Handing memory back
108
- restores whatever those keys were before. If the user's own
110
+ `$CODEX_HOME`), keeping the user's comments, layout and line endings. The
111
+ file is first copied to `config.toml.context-graph-memory.bak`, apart from
112
+ the backup setup's own writes leave. Handing memory back restores those keys
113
+ from it, drops a `[memories]` table the switch created, and deletes the
114
+ backup. Repeating the hand-back preserves the restored settings. If the
115
+ switch backup is missing, including on older installs, current settings stay
116
+ unchanged. The general `config.toml.context-graph.bak` may predate the switch
117
+ and is never used to guess the prior settings; the output explains how to
118
+ turn Codex memories on explicitly when needed. If the user's own
109
119
  `[mcp_servers.context-graph]` narrows tools with `enabled_tools`, `memory` is
110
120
  added there.
111
121
  - **Compaction.** The Codex plugin's `SessionStart` hook also fires after
@@ -123,20 +133,44 @@ Nothing writes to both memories: while the backend is `context-graph`, the
123
133
  graph is the only one. Handing memory back (`--memory-backend native`, or
124
134
  `uninstall`) first copies the graph's memories into Claude Code's auto memory,
125
135
  the reverse of the import, so it picks up where the graph left off:
126
- - a project's files go to every Claude Code project folder with that project
127
- key, `user`/`feedback` ones to every folder with auto memory, each with its
128
- `<type>_` prefix back;
129
- - the graph's copy wins over the older one there, and each folder's
130
- `MEMORY.md` is rebuilt;
131
- - files Claude Code has no folder for (a project it never opened, another
132
- agent's files) stay in the graph only, and the output lists them.
133
-
134
- Then it removes the setting. The copy runs only while the backend is
135
- `context-graph`, so handing back twice never overwrites newer files. If
136
- Memgraph is down, `setup` switches nothing; `uninstall` turns the memory back
137
- on anyway, with what it held before, and warns. Codex, Copilot CLI and Grok
138
- Build get nothing copied back: their memory is generated or hosted by the
139
- agent itself.
136
+ - Claude Code keeps one memory folder per git repository, shared by its
137
+ worktrees and subdirectories, so a project's files go to that repository's
138
+ folder only, each with its `<type>_` prefix back;
139
+ - `user`/`feedback` files go back to the folders they were imported from. One
140
+ first written while the graph was the backend goes to the repositories of
141
+ the sessions that wrote it. Claude Code has no memory that applies
142
+ everywhere, so these files are not copied into every project;
143
+ - a file already there is replaced only when it is an earlier text of the
144
+ graph's file. Any other file, changed outside the graph, is kept, and the
145
+ output lists it. Each folder that changed gets its `MEMORY.md` rebuilt;
146
+ - files with no folder to go to (a project Claude Code never opened, a
147
+ user-wide file no recorded session wrote, another agent's files) stay in
148
+ the graph only, and the output lists them.
149
+
150
+ Then it removes the setting. If Memgraph is down, `setup` switches nothing;
151
+ `uninstall` turns the memory back on anyway, with what it held before, and
152
+ warns. Copilot CLI and Grok Build get nothing copied back: their memory is
153
+ hosted by the agent, or kept in folders only it can name.
154
+
155
+ Codex rewrites its own memory files at each consolidation, so handing back
156
+ leaves each graph memory as an ad-hoc note in
157
+ `~/.codex/memories/extensions/ad_hoc/notes/` (`memories_v2/` under
158
+ `[memories] version = "v2"`), the one place Codex takes outside writes. Its
159
+ next consolidation folds them into `memory_summary.md` and `MEMORY.md`, in its
160
+ own words, so they aren't copied exactly. That runs only while
161
+ `features.memories` and `memories.generate_memories` are on; the output says
162
+ when they weren't before the switch. A note names its project when the memory
163
+ had one, since Codex keeps one store for all projects. Files the import took
164
+ from Codex and the graph never changed aren't noted, and a note already left
165
+ isn't left again.
166
+
167
+ The backend is one setting for every agent, and the copy runs only while it
168
+ is `context-graph`. So `setup codex claude-code --memory-backend native` hands
169
+ each named agent back first and changes the setting last. Setup stops handing
170
+ back agents at the first failure and keeps the graph backend, so retrying the
171
+ command can still export their memories. An agent left out
172
+ of that command gets nothing copied back once the setting is `native`, so name
173
+ every agent that uses the graph's memory.
140
174
 
141
175
  `doctor` fails when the backend is `context-graph` but Claude Code's own memory
142
176
  is still on, including when the project's `.claude/settings.json` or
@@ -80,6 +80,8 @@ The `[memories]` table (`MemoriesToml`, `config/src/types.rs:322`):
80
80
  - `rollout_summaries/<timestamp>-<shorthash>[-<slug>].md`: one recap per selected rollout. Each recap points at the original session `rollout_path`, which is a jsonl file under `~/.codex/sessions/`.
81
81
  - `skills/<name>/SKILL.md` (with optional `scripts/`, `examples/`, `templates/`): skills the consolidator promotes.
82
82
  - `extensions/ad_hoc/notes/<timestamp>-<slug>.md`: the only place the model may write. It does so only when the user explicitly asks to update memory, and Phase 2 folds the note in later.
83
+ - The name must be `YYYY-MM-DDTHH-MM-SS-<slug>.md`, with a slug of 1 to 80 lowercase letters, digits and hyphens, and 128 bytes at most. A note is created, never overwritten (`ext/memories/src/local/ad_hoc_note.rs`). Phase 2 is told to treat notes as information, never instructions, to never delete one, and to tag what it derives from them `[ad-hoc note]` (`memories/write/templates/extensions/ad_hoc/instructions.md`). [src]
84
+ - **Live check, codex-cli 0.162.0, 2026-10-27.** Two notes were written into a throwaway `CODEX_HOME` with memories on. One session's Phase 2 folded both into `memory_summary.md` and `MEMORY.md`, tagged `[ad-hoc note]`. That included a note the first baseline had already committed. The consolidation agent runs in the background of the session that claims the job, and it is shut down with that session: a `codex exec` that ends in seconds kills it (`failed_agent`), and the job then backs off for an hour. [local]
83
85
  - `phase2_workspace_diff.md` (transient) and `.git/`: the git baseline that Phase 2 diffs against.
84
86
 
85
87
  **Injection** [src `ext/memories/src/prompts.rs`, `extension.rs`, `core/src/session/mod.rs`]:
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "memgraph-context-graph"
3
- version = "0.12.0"
3
+ version = "0.12.2"
4
4
  description = "Connect agent SDKs to context-graph components (actions-graph, skills-graph, etc.)"
5
5
  readme = "README.md"
6
6
  license = { text = "MIT" }
@@ -9,8 +9,10 @@ only declares what differs about it.
9
9
  from __future__ import annotations
10
10
 
11
11
  import json
12
+ import os
12
13
  import shlex
13
14
  import shutil
15
+ import subprocess
14
16
  import sys
15
17
  from collections.abc import Callable, Mapping
16
18
  from dataclasses import dataclass, field
@@ -69,6 +71,9 @@ class FieldMap:
69
71
  reply: tuple[str, ...] = ("last_assistant_message",)
70
72
 
71
73
 
74
+ _ON_WINDOWS = os.name == "nt"
75
+
76
+
72
77
  @dataclass(frozen=True)
73
78
  class HookConfig:
74
79
  """Describe how a runtime represents and stores command hooks.
@@ -78,6 +83,9 @@ class HookConfig:
78
83
  ``flat_hooks`` names events written with the flat layout even when
79
84
  ``layout`` is nested, for runtimes that only accept matcher groups on some
80
85
  events.
86
+ ``windows_shell`` is the shell the runtime hands hook commands to on
87
+ Windows. ``cmd.exe`` doesn't read POSIX single quotes, so a ``"cmd"``
88
+ runtime's commands are quoted for it instead.
81
89
  """
82
90
 
83
91
  layout: Literal["nested", "flat"]
@@ -89,6 +97,7 @@ class HookConfig:
89
97
  inject_event_name: bool = False
90
98
  matchers: Mapping[str, str] = field(default_factory=dict)
91
99
  flat_hooks: frozenset[str] = frozenset()
100
+ windows_shell: Literal["posix", "cmd"] = "posix"
92
101
 
93
102
 
94
103
  @dataclass(frozen=True)
@@ -300,7 +309,7 @@ def build_hooks_config(spec: RuntimeSpec, command: str, *, timeout: int = 30) ->
300
309
  for hook_name in spec.hooks:
301
310
  hook_command = command
302
311
  if spec.config.inject_event_name:
303
- hook_command = f"{command} --event-name {shlex.quote(hook_name)}"
312
+ hook_command = f"{command} --event-name {_quote(hook_name, spec.config.windows_shell)}"
304
313
  command_entry: dict[str, Any] = {"command": hook_command, "type": "command"}
305
314
  if spec.config.timeout_key is not None:
306
315
  command_entry[spec.config.timeout_key] = timeout
@@ -343,7 +352,11 @@ def write_hook_config(
343
352
  if config_path.exists() and not spec.config.merge and not force:
344
353
  raise FileExistsError(f"Refusing to overwrite existing {spec.name} config: {config_path} (pass force=True)")
345
354
 
346
- command = hook_command or hook_command_for(spec.name, connectors)
355
+ command = hook_command or hook_command_for(spec.name, connectors, windows_shell=spec.config.windows_shell)
356
+ if hook_command is None and timeout != 30:
357
+ from memgraph_context_graph.hooks.runner import deadline_for
358
+
359
+ command = f"{command} --deadline {deadline_for(timeout):g}"
347
360
  hooks = build_hooks_config(spec, command, timeout=timeout)
348
361
  document: dict[str, Any]
349
362
  if config_path.exists() and spec.config.merge:
@@ -444,9 +457,24 @@ def hook_command_argv(runtime: str, connectors: list[str]) -> list[str]:
444
457
  return argv
445
458
 
446
459
 
447
- def hook_command_for(runtime: str, connectors: list[str]) -> str:
448
- """Build the installed hook command for *runtime* and *connectors* as a shell string."""
449
- return shlex.join(hook_command_argv(runtime, connectors))
460
+ def hook_command_for(runtime: str, connectors: list[str], *, windows_shell: Literal["posix", "cmd"] = "posix") -> str:
461
+ """Build the installed hook command for *runtime* and *connectors* as a shell string.
462
+
463
+ On Windows a ``"cmd"`` *windows_shell* gets ``cmd.exe`` quoting, which
464
+ leaves a path without spaces bare; elsewhere the command is POSIX-quoted.
465
+ """
466
+ argv = hook_command_argv(runtime, connectors)
467
+ if _uses_cmd(windows_shell):
468
+ return subprocess.list2cmdline(argv)
469
+ return shlex.join(argv)
470
+
471
+
472
+ def _uses_cmd(windows_shell: Literal["posix", "cmd"]) -> bool:
473
+ return windows_shell == "cmd" and _ON_WINDOWS
474
+
475
+
476
+ def _quote(word: str, windows_shell: Literal["posix", "cmd"]) -> str:
477
+ return subprocess.list2cmdline([word]) if _uses_cmd(windows_shell) else shlex.quote(word)
450
478
 
451
479
 
452
480
  def _metadata_from_payload(payload: dict[str, Any], keys: tuple[str, ...]) -> dict[str, Any]:
@@ -183,7 +183,8 @@ SPEC = RuntimeSpec(
183
183
  ),
184
184
  # hooks.json is keyed by named hook groups; this runtime owns one group.
185
185
  # Only tool events take matcher groups: agy rejects the whole file if a
186
- # lifecycle event is nested, so those are written as flat handlers.
186
+ # lifecycle event is nested, so those are written as flat handlers. On
187
+ # Windows agy runs each command through cmd.exe, inside its own quotes.
187
188
  config=HookConfig(
188
189
  path=".agents/hooks.json",
189
190
  layout="nested",
@@ -191,6 +192,7 @@ SPEC = RuntimeSpec(
191
192
  merge=True,
192
193
  inject_event_name=True,
193
194
  flat_hooks=frozenset({"SessionStart", "Stop"}),
195
+ windows_shell="cmd",
194
196
  ),
195
197
  fields=FieldMap(session_id=("conversationId",)),
196
198
  # PostToolUse requires an object; an empty PreToolUse answer leaves agy's
@@ -1,8 +1,11 @@
1
1
  """Claude Code's auto memory, as Context Graph switches away from and imports it.
2
2
 
3
3
  Auto memory is Markdown files in ``~/.claude/projects/<project>/memory/``, one
4
- folder per project. ``<project>`` is the project's absolute path with every
5
- character other than a letter or digit replaced by ``-``. ``MEMORY.md`` there
4
+ folder per git repository, shared by its worktrees and subdirectories (outside
5
+ a repository, per project folder). ``<project>`` is that folder's absolute path
6
+ with every character other than a letter or digit replaced by ``-``; the other
7
+ ``~/.claude/projects/`` folders, one per directory a session started in, hold
8
+ transcripts and are never read for memory. ``MEMORY.md`` there
6
9
  is the index Claude Code loads at session start; the other files are
7
10
  memories, each with frontmatter naming its ``type``. The ``autoMemoryEnabled``
8
11
  setting turns it off; a plugin can't set it, so this edits the user's own
@@ -16,6 +19,7 @@ from __future__ import annotations
16
19
  import json
17
20
  import re
18
21
  import shutil
22
+ import subprocess
19
23
  from dataclasses import dataclass
20
24
  from pathlib import Path
21
25
  from typing import TYPE_CHECKING, Any
@@ -23,7 +27,7 @@ from typing import TYPE_CHECKING, Any
23
27
  from memgraph_context_graph.memory_backend import ExportReport, ImportReport, MemoryBackendError
24
28
 
25
29
  if TYPE_CHECKING:
26
- from memgraph_context_graph.memory_backend import MemoryFiles, MemorySource, StoredMemory
30
+ from memgraph_context_graph.memory_backend import MemoryFiles, MemorySource
27
31
 
28
32
  _SETTING = "autoMemoryEnabled"
29
33
  _INDEX_FILE = "MEMORY.md"
@@ -83,8 +87,11 @@ class ClaudeCodeMemory:
83
87
  ``user``/``feedback`` memories go under ``/memories/<type>/``, since they
84
88
  apply everywhere; ``project``/``reference`` (and untyped) ones under
85
89
  that project's ``/memories/projects/<key>/``, keyed as the session-start
86
- index keys the checkout. ``MEMORY.md`` is skipped: the index is
87
- generated now. The original files are left as they are.
90
+ index keys the checkout. A ``user``/``feedback`` memory whose name the
91
+ graph already holds with other text, so another project's own version,
92
+ goes under its project's folder under its own filename instead, where
93
+ handing memory back finds it again. ``MEMORY.md`` is skipped: the index
94
+ is generated now. The original files are left as they are.
88
95
  """
89
96
  from sessions_graph.memory_index import project_key_for
90
97
 
@@ -99,7 +106,11 @@ class ClaudeCodeMemory:
99
106
  if not text.strip():
100
107
  continue
101
108
  target = _target_path(source.name, text, project)
102
- if _exists(files, target):
109
+ lineage = _lineage(files, target)
110
+ if lineage and text not in lineage and not target.startswith("/memories/projects/"):
111
+ target = f"/memories/projects/{project}/{source.name}"
112
+ lineage = _lineage(files, target)
113
+ if lineage:
103
114
  report.skipped.append(target)
104
115
  continue
105
116
  try:
@@ -113,48 +124,87 @@ class ClaudeCodeMemory:
113
124
  def export_from(self, source: MemorySource, home: Path) -> ExportReport:
114
125
  """Write the graph's memories back into auto memory, the reverse of :meth:`import_into`.
115
126
 
116
- A project's files go to every Claude Code project folder whose checkout has that key, and
117
- ``user``/``feedback`` ones, which apply everywhere, to every folder that ends up with auto memory.
118
- The graph's copy wins over an older one here, and each folder's ``MEMORY.md`` is rebuilt. Files
119
- Claude Code has no folder for (a project it never opened, nested paths, other agents' files) stay
120
- in the graph only.
127
+ A project's files go to its repository's memory folder. A ``user``/``feedback`` file goes back
128
+ to the folders it was imported from, which still hold it; one first written in the graph goes
129
+ to the repositories of the sessions that wrote it. A file there is overwritten only when it is
130
+ an earlier text of the graph's file, so a newer or unrelated native file is kept and reported.
131
+ Each folder written to gets its ``MEMORY.md`` rebuilt. Files with no folder to go to (a project
132
+ Claude Code never opened, nested paths, other agents' files) stay in the graph only.
121
133
  """
122
134
  folders = _memory_folders(home)
135
+ every_folder = sorted({folder for found in folders.values() for folder in found})
123
136
  report = ExportReport()
124
- user_wide: list[tuple[str, StoredMemory]] = []
137
+ written: set[Path] = set()
125
138
  for memory in source.files():
126
139
  parts = memory.path.removeprefix("/memories/").split("/")
127
140
  if len(parts) == 2 and parts[0] in _USER_WIDE_TYPES:
128
- user_wide.append((_native_name(parts[1], memory.content, parts[0]), memory))
129
- elif len(parts) == 3 and parts[0] == "projects" and parts[1] in folders:
130
- for folder in folders[parts[1]]:
131
- _write_memory(folder, _native_name(parts[2], memory.content, "project"), memory.content)
132
- report.exported.append(memory.path)
141
+ name = _native_name(parts[1], memory.content, parts[0])
142
+ targets = [folder for folder in every_folder if (folder / name).is_file()] or [
143
+ folder for key in source.written_in(memory.path) for folder in folders.get(key, [])
144
+ ]
145
+ elif len(parts) == 3 and parts[0] == "projects":
146
+ name = _native_name(parts[2], memory.content, "project")
147
+ targets = folders.get(parts[1], [])
133
148
  else:
149
+ targets = []
150
+ if not targets:
134
151
  report.skipped.append(memory.path)
135
- targets = sorted({folder for found in folders.values() for folder in found if folder.is_dir()})
136
- for name, memory in user_wide:
152
+ continue
153
+ lineage = {memory.content} | {version.content for version in source.versions(memory.path)}
154
+ kept = []
137
155
  for folder in targets:
138
- _write_memory(folder, name, memory.content)
139
- (report.exported if targets else report.skipped).append(memory.path)
140
- for folder in targets:
156
+ if _write_memory(folder, name, memory.content, lineage):
157
+ written.add(folder)
158
+ elif (folder / name).read_text(encoding="utf-8") != memory.content:
159
+ kept.append(str(folder / name))
160
+ report.kept += kept
161
+ if len(kept) < len(targets):
162
+ report.exported.append(memory.path)
163
+ for folder in sorted(written):
141
164
  _write_index(folder)
142
165
  return report
143
166
 
144
167
 
145
168
  def _memory_folders(home: Path) -> dict[str, list[Path]]:
146
- """Each project key's auto-memory folders: every project Claude Code has opened, keyed as the import keys it."""
169
+ """Each project key's auto-memory folders, keyed as the import keys them: one per repository Claude Code opened.
170
+
171
+ Every ``~/.claude/projects/`` folder whose checkout is in a repository leads to the one folder named after
172
+ that repository's main checkout, the only one Claude Code reads memory from; a folder whose checkout is
173
+ gone counts only when it already holds memory.
174
+ """
147
175
  from sessions_graph.memory_index import project_key_for
148
176
 
149
177
  projects = home / ".claude" / "projects"
150
- folders: dict[str, list[Path]] = {}
178
+ folders: dict[str, set[Path]] = {}
151
179
  for project in sorted(projects.iterdir() if projects.is_dir() else []):
152
180
  if not project.is_dir():
153
181
  continue
154
182
  checkout = _checkout_for(project.name)
155
- key = (project_key_for(checkout) if checkout else None) or _fallback_key(project.name)
156
- folders.setdefault(key, []).append(project / "memory")
157
- return folders
183
+ if checkout is None:
184
+ if (project / "memory").is_dir():
185
+ folders.setdefault(_fallback_key(project.name), set()).add(project / "memory")
186
+ continue
187
+ root = _repository_root(checkout) or checkout
188
+ folder = projects / _SLUG_RE.sub("-", str(root)) / "memory"
189
+ key = project_key_for(checkout) or _fallback_key(folder.parent.name)
190
+ folders.setdefault(key, set()).add(folder)
191
+ return {key: sorted(found) for key, found in folders.items()}
192
+
193
+
194
+ def _repository_root(checkout: Path) -> Path | None:
195
+ """The main checkout of the repository *checkout* is in, which its worktrees share; None outside git."""
196
+ try:
197
+ result = subprocess.run(
198
+ ["git", "-C", str(checkout), "rev-parse", "--path-format=absolute", "--git-common-dir"],
199
+ capture_output=True,
200
+ text=True,
201
+ timeout=5,
202
+ check=False,
203
+ )
204
+ except (OSError, subprocess.SubprocessError):
205
+ return None
206
+ common = Path(result.stdout.strip())
207
+ return common.parent if result.returncode == 0 and common.name == ".git" else None
158
208
 
159
209
 
160
210
  def _native_name(name: str, text: str, default_type: str) -> str:
@@ -164,12 +214,20 @@ def _native_name(name: str, text: str, default_type: str) -> str:
164
214
  return name if _PREFIX_RE.match(name) else f"{memory_type}_{name}"
165
215
 
166
216
 
167
- def _write_memory(folder: Path, name: str, text: str) -> None:
217
+ def _write_memory(folder: Path, name: str, text: str, lineage: set[str]) -> bool:
218
+ """Write *text* to *folder*/*name* unless a file there holds text outside *lineage*; returns whether it wrote.
219
+
220
+ *lineage* is the graph file's current and earlier texts. A native file among them is one the graph's copy
221
+ descends from, so writing over it loses nothing; any other was written beside the graph and is kept.
222
+ """
168
223
  path = folder / name
169
- if path.is_file() and path.read_text(encoding="utf-8") == text:
170
- return
224
+ if path.is_file():
225
+ current = path.read_text(encoding="utf-8")
226
+ if current == text or current not in lineage:
227
+ return False
171
228
  folder.mkdir(parents=True, exist_ok=True)
172
229
  path.write_text(text, encoding="utf-8")
230
+ return True
173
231
 
174
232
 
175
233
  def _write_index(folder: Path) -> None:
@@ -198,23 +256,39 @@ def _target_path(filename: str, text: str, project: str) -> str:
198
256
  return f"/memories/projects/{project}/{name}"
199
257
 
200
258
 
201
- def _exists(files: MemoryFiles, path: str) -> bool:
202
- try:
203
- files.view(path)
204
- except Exception:
205
- return False
206
- return True
259
+ def _lineage(files: MemoryFiles, path: str) -> set[str]:
260
+ """The graph file at *path*'s current and earlier texts; empty when there is no such file."""
261
+ current = files.read(path)
262
+ if current is None:
263
+ return set()
264
+ return {current} | {version.content for version in files.versions(path)}
207
265
 
208
266
 
209
267
  def _checkout_for(slug: str) -> Path | None:
210
- """The directory Claude Code named *slug*, found by walking the filesystem from ``/``.
268
+ """The directory Claude Code named *slug*, found by walking the filesystem from its root.
211
269
 
212
270
  The slug is lossy: ``/repos/ai-toolkit`` and ``/repos/ai/toolkit`` give
213
271
  the same one. So each step matches the directory's real entries by their
214
272
  own slugs, preferring the longest, and backtracks when a path dead-ends.
215
273
  """
216
- tokens = slug.lstrip("-").split("-")
217
- return _walk(Path("/"), tokens) if tokens and tokens[0] else None
274
+ start = _slug_root(slug)
275
+ return _walk(*start) if start else None
276
+
277
+
278
+ def _slug_root(slug: str) -> tuple[Path, list[str]] | None:
279
+ """The filesystem root *slug* starts from, and the tokens left to walk below it.
280
+
281
+ A POSIX slug starts with the ``-`` of ``/``. A Windows one starts with the
282
+ drive letter, and the drive's ``:\\`` makes a double dash: ``C:\\source\\x`` is
283
+ ``C--source-x``.
284
+ """
285
+ if slug.startswith("-"):
286
+ tokens = slug[1:].split("-")
287
+ return (Path("/"), tokens) if tokens[0] else None
288
+ drive, double_dash, rest = slug.partition("--")
289
+ if len(drive) == 1 and drive.isalpha() and double_dash and rest:
290
+ return Path(f"{drive}:/"), rest.split("-")
291
+ return None
218
292
 
219
293
 
220
294
  def _walk(directory: Path, tokens: list[str]) -> Path | None: