memgraph-context-graph 0.12.1__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.
- {memgraph_context_graph-0.12.1 → memgraph_context_graph-0.12.2}/PKG-INFO +1 -1
- {memgraph_context_graph-0.12.1 → memgraph_context_graph-0.12.2}/docs/memory.md +10 -3
- {memgraph_context_graph-0.12.1 → memgraph_context_graph-0.12.2}/pyproject.toml +1 -1
- {memgraph_context_graph-0.12.1 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/adapters/_spec.py +33 -5
- {memgraph_context_graph-0.12.1 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/adapters/antigravity_cli.py +3 -1
- {memgraph_context_graph-0.12.1 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/adapters/claude_code_memory.py +19 -3
- {memgraph_context_graph-0.12.1 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/adapters/codex_memory.py +13 -3
- {memgraph_context_graph-0.12.1 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/connectors.py +1 -1
- {memgraph_context_graph-0.12.1 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/hooks/runner.py +83 -7
- {memgraph_context_graph-0.12.1 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/setup/engine.py +4 -1
- {memgraph_context_graph-0.12.1 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/setup/tool.py +2 -2
- {memgraph_context_graph-0.12.1 → memgraph_context_graph-0.12.2}/tests/test_antigravity_cli_adapter.py +16 -0
- {memgraph_context_graph-0.12.1 → memgraph_context_graph-0.12.2}/tests/test_capture_log.py +43 -0
- memgraph_context_graph-0.12.2/tests/test_claude_code_memory.py +31 -0
- {memgraph_context_graph-0.12.1 → memgraph_context_graph-0.12.2}/tests/test_codex_adapter.py +13 -1
- memgraph_context_graph-0.12.2/tests/test_codex_memory.py +69 -0
- {memgraph_context_graph-0.12.1 → memgraph_context_graph-0.12.2}/tests/test_setup.py +1 -1
- {memgraph_context_graph-0.12.1 → memgraph_context_graph-0.12.2}/.gitignore +0 -0
- {memgraph_context_graph-0.12.1 → memgraph_context_graph-0.12.2}/CONTEXT.md +0 -0
- {memgraph_context_graph-0.12.1 → memgraph_context_graph-0.12.2}/LICENSE +0 -0
- {memgraph_context_graph-0.12.1 → memgraph_context_graph-0.12.2}/README.md +0 -0
- {memgraph_context_graph-0.12.1 → memgraph_context_graph-0.12.2}/docs/adr/0001-runtime-adapter-terminology.md +0 -0
- {memgraph_context_graph-0.12.1 → memgraph_context_graph-0.12.2}/docs/adr/0002-config-file-only-hook-resolution.md +0 -0
- {memgraph_context_graph-0.12.1 → memgraph_context_graph-0.12.2}/docs/adr/0003-config-path-override.md +0 -0
- {memgraph_context_graph-0.12.1 → memgraph_context_graph-0.12.2}/docs/adr/0004-user-id-in-session-start-event.md +0 -0
- {memgraph_context_graph-0.12.1 → memgraph_context_graph-0.12.2}/docs/adr/0005-config-file-covers-llm-key-and-subprocess-env.md +0 -0
- {memgraph_context_graph-0.12.1 → memgraph_context_graph-0.12.2}/docs/adr/0006-memory-tool-writes-explicit-memory.md +0 -0
- {memgraph_context_graph-0.12.1 → memgraph_context_graph-0.12.2}/docs/adr/0007-spawned-processes-read-the-config-file-themselves.md +0 -0
- {memgraph_context_graph-0.12.1 → memgraph_context_graph-0.12.2}/docs/command-hooks.md +0 -0
- {memgraph_context_graph-0.12.1 → memgraph_context_graph-0.12.2}/docs/configuration.md +0 -0
- {memgraph_context_graph-0.12.1 → memgraph_context_graph-0.12.2}/docs/extending.md +0 -0
- {memgraph_context_graph-0.12.1 → memgraph_context_graph-0.12.2}/docs/research/2026-10-codex-memory.md +0 -0
- {memgraph_context_graph-0.12.1 → memgraph_context_graph-0.12.2}/docs/runtime-setup.md +0 -0
- {memgraph_context_graph-0.12.1 → memgraph_context_graph-0.12.2}/docs/sdk-memory.md +0 -0
- {memgraph_context_graph-0.12.1 → memgraph_context_graph-0.12.2}/examples/sdk_claude_agent.py +0 -0
- {memgraph_context_graph-0.12.1 → memgraph_context_graph-0.12.2}/examples/sdk_openai_agents.py +0 -0
- {memgraph_context_graph-0.12.1 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/__init__.py +0 -0
- {memgraph_context_graph-0.12.1 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/adapters/__init__.py +0 -0
- {memgraph_context_graph-0.12.1 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/adapters/_identity.py +0 -0
- {memgraph_context_graph-0.12.1 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/adapters/_opencode_plugin.js +0 -0
- {memgraph_context_graph-0.12.1 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/adapters/agent_mcp.py +0 -0
- {memgraph_context_graph-0.12.1 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/adapters/agent_memory.py +0 -0
- {memgraph_context_graph-0.12.1 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/adapters/claude_code.py +0 -0
- {memgraph_context_graph-0.12.1 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/adapters/codex.py +0 -0
- {memgraph_context_graph-0.12.1 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/adapters/copilot_cli.py +0 -0
- {memgraph_context_graph-0.12.1 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/adapters/cursor.py +0 -0
- {memgraph_context_graph-0.12.1 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/adapters/grok.py +0 -0
- {memgraph_context_graph-0.12.1 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/adapters/opencode.py +0 -0
- {memgraph_context_graph-0.12.1 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/capture_log.py +0 -0
- {memgraph_context_graph-0.12.1 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/capture_policy.py +0 -0
- {memgraph_context_graph-0.12.1 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/cli.py +0 -0
- {memgraph_context_graph-0.12.1 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/config_schema.py +0 -0
- {memgraph_context_graph-0.12.1 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/events.py +0 -0
- {memgraph_context_graph-0.12.1 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/forget.py +0 -0
- {memgraph_context_graph-0.12.1 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/hooks/__init__.py +0 -0
- {memgraph_context_graph-0.12.1 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/hooks/cli.py +0 -0
- {memgraph_context_graph-0.12.1 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/hooks/runtime_plugin.py +0 -0
- {memgraph_context_graph-0.12.1 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/link.py +0 -0
- {memgraph_context_graph-0.12.1 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/mcp_server.py +0 -0
- {memgraph_context_graph-0.12.1 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/memory_backend.py +0 -0
- {memgraph_context_graph-0.12.1 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/protocols.py +0 -0
- {memgraph_context_graph-0.12.1 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/sdk/__init__.py +0 -0
- {memgraph_context_graph-0.12.1 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/sdk/claude.py +0 -0
- {memgraph_context_graph-0.12.1 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/sdk/openai.py +0 -0
- {memgraph_context_graph-0.12.1 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/session.py +0 -0
- {memgraph_context_graph-0.12.1 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/setup/__init__.py +0 -0
- {memgraph_context_graph-0.12.1 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/setup/agents.py +0 -0
- {memgraph_context_graph-0.12.1 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/setup/cli.py +0 -0
- {memgraph_context_graph-0.12.1 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/setup/memgraph.py +0 -0
- {memgraph_context_graph-0.12.1 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/setup/prompts.py +0 -0
- {memgraph_context_graph-0.12.1 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/setup/uninstall.py +0 -0
- {memgraph_context_graph-0.12.1 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/setup/update.py +0 -0
- {memgraph_context_graph-0.12.1 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/spaces.py +0 -0
- {memgraph_context_graph-0.12.1 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/terminal.py +0 -0
- {memgraph_context_graph-0.12.1 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/tools.py +0 -0
- {memgraph_context_graph-0.12.1 → memgraph_context_graph-0.12.2}/tests/__init__.py +0 -0
- {memgraph_context_graph-0.12.1 → memgraph_context_graph-0.12.2}/tests/conftest.py +0 -0
- {memgraph_context_graph-0.12.1 → memgraph_context_graph-0.12.2}/tests/fake_agent_cli.py +0 -0
- {memgraph_context_graph-0.12.1 → memgraph_context_graph-0.12.2}/tests/test_agent_memory_wiring.py +0 -0
- {memgraph_context_graph-0.12.1 → memgraph_context_graph-0.12.2}/tests/test_capture_policy.py +0 -0
- {memgraph_context_graph-0.12.1 → memgraph_context_graph-0.12.2}/tests/test_claude_code_adapter.py +0 -0
- {memgraph_context_graph-0.12.1 → memgraph_context_graph-0.12.2}/tests/test_config_cli.py +0 -0
- {memgraph_context_graph-0.12.1 → memgraph_context_graph-0.12.2}/tests/test_connectors.py +0 -0
- {memgraph_context_graph-0.12.1 → memgraph_context_graph-0.12.2}/tests/test_context_before_tool.py +0 -0
- {memgraph_context_graph-0.12.1 → memgraph_context_graph-0.12.2}/tests/test_copilot_cli_adapter.py +0 -0
- {memgraph_context_graph-0.12.1 → memgraph_context_graph-0.12.2}/tests/test_cursor_adapter.py +0 -0
- {memgraph_context_graph-0.12.1 → memgraph_context_graph-0.12.2}/tests/test_dev_scripts.py +0 -0
- {memgraph_context_graph-0.12.1 → memgraph_context_graph-0.12.2}/tests/test_events.py +0 -0
- {memgraph_context_graph-0.12.1 → memgraph_context_graph-0.12.2}/tests/test_grok_adapter.py +0 -0
- {memgraph_context_graph-0.12.1 → memgraph_context_graph-0.12.2}/tests/test_hook_cli.py +0 -0
- {memgraph_context_graph-0.12.1 → memgraph_context_graph-0.12.2}/tests/test_identity.py +0 -0
- {memgraph_context_graph-0.12.1 → memgraph_context_graph-0.12.2}/tests/test_link.py +0 -0
- {memgraph_context_graph-0.12.1 → memgraph_context_graph-0.12.2}/tests/test_opencode_adapter.py +0 -0
- {memgraph_context_graph-0.12.1 → memgraph_context_graph-0.12.2}/tests/test_plugin_hooks.py +0 -0
- {memgraph_context_graph-0.12.1 → memgraph_context_graph-0.12.2}/tests/test_runtime_plugin.py +0 -0
- {memgraph_context_graph-0.12.1 → memgraph_context_graph-0.12.2}/tests/test_session.py +0 -0
- {memgraph_context_graph-0.12.1 → memgraph_context_graph-0.12.2}/tests/test_setup_docker.py +0 -0
- {memgraph_context_graph-0.12.1 → memgraph_context_graph-0.12.2}/tests/test_spaces.py +0 -0
- {memgraph_context_graph-0.12.1 → memgraph_context_graph-0.12.2}/tests/test_terminal.py +0 -0
- {memgraph_context_graph-0.12.1 → memgraph_context_graph-0.12.2}/tests/test_tools.py +0 -0
|
@@ -111,8 +111,13 @@ its global store, with three differences:
|
|
|
111
111
|
file is first copied to `config.toml.context-graph-memory.bak`, apart from
|
|
112
112
|
the backup setup's own writes leave. Handing memory back restores those keys
|
|
113
113
|
from it, drops a `[memories]` table the switch created, and deletes the
|
|
114
|
-
backup.
|
|
115
|
-
|
|
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
|
|
119
|
+
`[mcp_servers.context-graph]` narrows tools with `enabled_tools`, `memory` is
|
|
120
|
+
added there.
|
|
116
121
|
- **Compaction.** The Codex plugin's `SessionStart` hook also fires after
|
|
117
122
|
compaction (`compact`), so the memory index comes back once the context is
|
|
118
123
|
summarized.
|
|
@@ -161,7 +166,9 @@ isn't left again.
|
|
|
161
166
|
|
|
162
167
|
The backend is one setting for every agent, and the copy runs only while it
|
|
163
168
|
is `context-graph`. So `setup codex claude-code --memory-backend native` hands
|
|
164
|
-
each named agent back first and changes the setting last.
|
|
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
|
|
165
172
|
of that command gets nothing copied back once the setting is `native`, so name
|
|
166
173
|
every agent that uses the graph's memory.
|
|
167
174
|
|
|
@@ -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 {
|
|
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
|
-
|
|
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
|
|
@@ -265,14 +265,30 @@ def _lineage(files: MemoryFiles, path: str) -> set[str]:
|
|
|
265
265
|
|
|
266
266
|
|
|
267
267
|
def _checkout_for(slug: str) -> Path | None:
|
|
268
|
-
"""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.
|
|
269
269
|
|
|
270
270
|
The slug is lossy: ``/repos/ai-toolkit`` and ``/repos/ai/toolkit`` give
|
|
271
271
|
the same one. So each step matches the directory's real entries by their
|
|
272
272
|
own slugs, preferring the longest, and backtracks when a path dead-ends.
|
|
273
273
|
"""
|
|
274
|
-
|
|
275
|
-
return _walk(
|
|
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
|
|
276
292
|
|
|
277
293
|
|
|
278
294
|
def _walk(directory: Path, tokens: list[str]) -> Path | None:
|
|
@@ -104,15 +104,25 @@ class CodexMemory:
|
|
|
104
104
|
|
|
105
105
|
Keys that didn't exist then are removed, which leaves Codex's own
|
|
106
106
|
defaults (``features.memories`` is off by default), and so is a table
|
|
107
|
-
the switch created and left empty.
|
|
107
|
+
the switch created and left empty. Without a switch backup, settings
|
|
108
|
+
are preserved; the output explains how to enable memories if needed.
|
|
108
109
|
|
|
109
110
|
Raises:
|
|
110
111
|
MemoryBackendError: the config file or its backup isn't valid TOML.
|
|
111
112
|
"""
|
|
112
113
|
path = _config_path(home)
|
|
113
114
|
switch_backup = _switch_backup(path)
|
|
114
|
-
before = _read(switch_backup)
|
|
115
115
|
config = _read(path)
|
|
116
|
+
if not switch_backup.exists():
|
|
117
|
+
lines = [f"Unchanged {path}: no memory-switch backup; kept the current memory settings."]
|
|
118
|
+
if any(_get(config, keys, None) is False for keys, _off in _SWITCHES):
|
|
119
|
+
lines.append(
|
|
120
|
+
f"To turn Codex memories on, review {path} and set features.memories, memories.use_memories"
|
|
121
|
+
" and memories.generate_memories to true. An older config.toml.context-graph.bak may predate"
|
|
122
|
+
" the memory switch, so it wasn't restored automatically."
|
|
123
|
+
)
|
|
124
|
+
return lines
|
|
125
|
+
before = _read(switch_backup)
|
|
116
126
|
for keys, _off in _SWITCHES:
|
|
117
127
|
previous = _get(before, keys, None)
|
|
118
128
|
if previous is None:
|
|
@@ -169,7 +179,7 @@ class CodexMemory:
|
|
|
169
179
|
Codex still has them.
|
|
170
180
|
"""
|
|
171
181
|
notes = _memory_root(home).joinpath(*_NOTES_DIR)
|
|
172
|
-
left = {path
|
|
182
|
+
left = {_read_text(path) for path in notes.glob(f"*-{_NOTE_PREFIX}*.md")}
|
|
173
183
|
stamp = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H-%M-%S")
|
|
174
184
|
report = ExportReport()
|
|
175
185
|
for memory in source.files():
|
|
@@ -60,7 +60,7 @@ class ConnectorSpec:
|
|
|
60
60
|
INSTALLABLE: dict[str, str] = {
|
|
61
61
|
"skills-graph": "skills-graph[memgraph-context-graph]>=0.4.0",
|
|
62
62
|
"actions-graph": "actions-graph[memgraph-context-graph]>=0.7.1",
|
|
63
|
-
"sessions-graph": "sessions-graph[memgraph-context-graph]>=0.12.
|
|
63
|
+
"sessions-graph": "sessions-graph[memgraph-context-graph]>=0.12.2",
|
|
64
64
|
"resources-graph": "resources-graph[memgraph-context-graph]>=0.3.0",
|
|
65
65
|
}
|
|
66
66
|
|
|
@@ -6,6 +6,7 @@ import argparse
|
|
|
6
6
|
import json
|
|
7
7
|
import os
|
|
8
8
|
import sys
|
|
9
|
+
import threading
|
|
9
10
|
import time
|
|
10
11
|
from typing import TYPE_CHECKING, Any, NoReturn, TypedDict
|
|
11
12
|
|
|
@@ -36,6 +37,16 @@ if TYPE_CHECKING:
|
|
|
36
37
|
#: so the runtime killed it before the failure could be logged (#378).
|
|
37
38
|
HOOK_CONNECT_TIMEOUT = 5.0
|
|
38
39
|
|
|
40
|
+
#: Seconds a hook runs before it logs where it was stuck, answers the runtime, and exits, for the 30 s hook
|
|
41
|
+
#: timeout our plugins declare. A query waiting on Memgraph's storage access never raises, so without a deadline
|
|
42
|
+
#: of its own the hook is killed at the runtime's timeout and nothing is logged.
|
|
43
|
+
HOOK_DEADLINE = 25.0
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def deadline_for(timeout: float) -> float:
|
|
47
|
+
"""The ``--deadline`` for a hook the runtime kills after *timeout* seconds: enough margin to log and answer."""
|
|
48
|
+
return max(timeout - 5.0, timeout / 2)
|
|
49
|
+
|
|
39
50
|
|
|
40
51
|
def load_payload(stream: Any | None = None) -> dict[str, Any]:
|
|
41
52
|
"""Read one JSON hook payload from a text stream (stdin by default)."""
|
|
@@ -160,6 +171,13 @@ def _hook_parser(plugin: RuntimeCLIPlugin) -> argparse.ArgumentParser:
|
|
|
160
171
|
parser.add_argument("--memgraph-user", default=None, help="Memgraph username. Overrides config file value.")
|
|
161
172
|
parser.add_argument("--memgraph-password", default=None, help="Memgraph password. Overrides config file value.")
|
|
162
173
|
parser.add_argument("--memgraph-database", default=None, help="Memgraph database. Overrides config file value.")
|
|
174
|
+
parser.add_argument(
|
|
175
|
+
"--deadline",
|
|
176
|
+
type=float,
|
|
177
|
+
default=HOOK_DEADLINE,
|
|
178
|
+
help=f"Seconds before the hook gives up, logs why, and answers the runtime (default: {HOOK_DEADLINE:g}). "
|
|
179
|
+
"Keep it under the runtime's hook timeout.",
|
|
180
|
+
)
|
|
163
181
|
parser.add_argument(
|
|
164
182
|
"--strict",
|
|
165
183
|
action="store_true",
|
|
@@ -198,6 +216,54 @@ class _FailureReport:
|
|
|
198
216
|
self.record("emit", exc, connector=name, event_type=event.event_type.value, session_id=event.session_id)
|
|
199
217
|
|
|
200
218
|
|
|
219
|
+
class _Deadline:
|
|
220
|
+
"""Answers the runtime and ends the process once a hook has run too long, logging the stage it was stuck in.
|
|
221
|
+
|
|
222
|
+
Every response goes through :meth:`respond`, so the runtime reads one
|
|
223
|
+
answer whichever of the hook and the deadline gets there first.
|
|
224
|
+
"""
|
|
225
|
+
|
|
226
|
+
def __init__(self, plugin: RuntimeCLIPlugin, report: _FailureReport) -> None:
|
|
227
|
+
self._plugin = plugin
|
|
228
|
+
self._report = report
|
|
229
|
+
self._lock = threading.Lock()
|
|
230
|
+
self._responded = False
|
|
231
|
+
self._timer: threading.Timer | None = None
|
|
232
|
+
|
|
233
|
+
def start(self, seconds: float) -> None:
|
|
234
|
+
self._timer = threading.Timer(seconds, self._expire, args=(seconds,))
|
|
235
|
+
self._timer.daemon = True
|
|
236
|
+
self._timer.start()
|
|
237
|
+
|
|
238
|
+
def cancel(self) -> None:
|
|
239
|
+
if self._timer is not None:
|
|
240
|
+
self._timer.cancel()
|
|
241
|
+
|
|
242
|
+
def respond(self, response: dict[str, Any]) -> None:
|
|
243
|
+
with self._lock:
|
|
244
|
+
if self._responded:
|
|
245
|
+
return
|
|
246
|
+
self._responded = True
|
|
247
|
+
if response:
|
|
248
|
+
print(json.dumps(response))
|
|
249
|
+
|
|
250
|
+
def _expire(self, seconds: float) -> None:
|
|
251
|
+
# Never released once answering here: the hook's own thread may still be blocked and must not answer too.
|
|
252
|
+
self._lock.acquire()
|
|
253
|
+
if self._responded:
|
|
254
|
+
self._lock.release()
|
|
255
|
+
return
|
|
256
|
+
self._report.record(
|
|
257
|
+
self._report.stage, error_type="HookDeadline", error=f"hook still running after {seconds:g} s"
|
|
258
|
+
)
|
|
259
|
+
# The plain response: the session-start text would query the graph that is holding the hook up.
|
|
260
|
+
response = self._plugin.response_for_payload(self._report.payload) or {}
|
|
261
|
+
if response:
|
|
262
|
+
print(json.dumps(response))
|
|
263
|
+
sys.stdout.flush()
|
|
264
|
+
os._exit(0)
|
|
265
|
+
|
|
266
|
+
|
|
201
267
|
def _text(value: Any) -> str | None:
|
|
202
268
|
return value if isinstance(value, str) else None
|
|
203
269
|
|
|
@@ -216,14 +282,21 @@ def run_hook(plugin: RuntimeCLIPlugin, argv: Sequence[str] | None = None) -> int
|
|
|
216
282
|
normal response, and the exit status is 0. ``--strict`` (or the
|
|
217
283
|
``_STRICT`` env flag) re-raises after logging, for developers; a connector
|
|
218
284
|
failure then raises :class:`ConnectorFailuresError` once every connector has run.
|
|
285
|
+
|
|
286
|
+
A hook that blocks, on a query waiting for Memgraph's storage access, never
|
|
287
|
+
raises: at ``--deadline`` it logs a ``HookDeadline`` failure at the stage it
|
|
288
|
+
was stuck in, prints the runtime's plain response, and exits 0 itself,
|
|
289
|
+
before the runtime's own timeout would kill it unlogged.
|
|
219
290
|
"""
|
|
220
291
|
report = _FailureReport(plugin.name)
|
|
292
|
+
deadline = _Deadline(plugin, report)
|
|
221
293
|
strict = os.environ.get(f"{_env_prefix(plugin.name)}_STRICT") == "1"
|
|
222
294
|
payload: dict[str, Any] = {}
|
|
223
295
|
connector_names: list[str] = []
|
|
224
296
|
try:
|
|
225
297
|
args = _hook_parser(plugin).parse_args(argv)
|
|
226
298
|
strict = strict or args.strict
|
|
299
|
+
deadline.start(args.deadline)
|
|
227
300
|
|
|
228
301
|
from memgraph_context_graph.adapters._identity import load_config, resolve_memgraph_env
|
|
229
302
|
|
|
@@ -245,7 +318,7 @@ def run_hook(plugin: RuntimeCLIPlugin, argv: Sequence[str] | None = None) -> int
|
|
|
245
318
|
if problems := [*load_config().problems, *policy_problems]:
|
|
246
319
|
report.record("config", level="warning", error_type="MalformedConfig", error="; ".join(problems))
|
|
247
320
|
if policy.excludes_project(_with_cwd(payload)["cwd"]):
|
|
248
|
-
|
|
321
|
+
deadline.respond(_response(plugin, payload, connector_names))
|
|
249
322
|
return 0
|
|
250
323
|
|
|
251
324
|
report.stage = "setup"
|
|
@@ -258,7 +331,9 @@ def run_hook(plugin: RuntimeCLIPlugin, argv: Sequence[str] | None = None) -> int
|
|
|
258
331
|
adapter = plugin.adapter_class(link, session_id=args.session_id)
|
|
259
332
|
adapter.handle_payload(payload)
|
|
260
333
|
report.stage = "respond"
|
|
261
|
-
|
|
334
|
+
deadline.respond(
|
|
335
|
+
_response(plugin, payload, connector_names, before_tool=_context_before_tool(link, tool_starts.events))
|
|
336
|
+
)
|
|
262
337
|
except Exception as exc:
|
|
263
338
|
if report.stage in {"args", "config"}:
|
|
264
339
|
# Failed before stdin was read: read it now, so the runtime still gets the response its event expects
|
|
@@ -269,9 +344,11 @@ def run_hook(plugin: RuntimeCLIPlugin, argv: Sequence[str] | None = None) -> int
|
|
|
269
344
|
report.record(report.stage, exc, error_type="HookArgumentError" if report.stage == "args" else None)
|
|
270
345
|
if strict:
|
|
271
346
|
raise
|
|
272
|
-
|
|
347
|
+
deadline.respond(_response(plugin, payload, connector_names))
|
|
273
348
|
_debug_log(plugin.name, f"memgraph-context-graph {plugin.name} hook skipped: {exc}")
|
|
274
349
|
return 0
|
|
350
|
+
finally:
|
|
351
|
+
deadline.cancel()
|
|
275
352
|
if strict and report.connector_failures:
|
|
276
353
|
raise ConnectorFailuresError(report.connector_failures)
|
|
277
354
|
return 0
|
|
@@ -293,9 +370,9 @@ def _payload_session_id(payload: dict[str, Any]) -> str | None:
|
|
|
293
370
|
return None
|
|
294
371
|
|
|
295
372
|
|
|
296
|
-
def
|
|
373
|
+
def _response(
|
|
297
374
|
plugin: RuntimeCLIPlugin, payload: dict[str, Any], connector_names: list[str], *, before_tool: Sequence[str] = ()
|
|
298
|
-
) ->
|
|
375
|
+
) -> dict[str, Any]:
|
|
299
376
|
response = plugin.response_for_payload(payload) or {}
|
|
300
377
|
render_session = getattr(plugin, "session_context_response", None)
|
|
301
378
|
if render_session is not None:
|
|
@@ -303,8 +380,7 @@ def _print_response(
|
|
|
303
380
|
render = getattr(plugin, "context_before_tool_response", None)
|
|
304
381
|
if before_tool and render is not None:
|
|
305
382
|
response.update(render("\n".join(before_tool)) or {})
|
|
306
|
-
|
|
307
|
-
print(json.dumps(response))
|
|
383
|
+
return response
|
|
308
384
|
|
|
309
385
|
|
|
310
386
|
class _ToolStarts(GraphConnector):
|
|
@@ -203,7 +203,10 @@ def run(
|
|
|
203
203
|
if choices.memory_backend:
|
|
204
204
|
switchable = [runtime for runtime in choices.runtimes if has_native_memory(runtime)]
|
|
205
205
|
for runtime in switchable:
|
|
206
|
-
done(_memory(runtime, choices.memory_backend, last=runtime == switchable[-1]))
|
|
206
|
+
outcome = done(_memory(runtime, choices.memory_backend, last=runtime == switchable[-1]))
|
|
207
|
+
# Later hand-backs must still see the graph as the backend when this one is retried.
|
|
208
|
+
if outcome.status == "fail" and choices.memory_backend == "native":
|
|
209
|
+
break
|
|
207
210
|
for outcome in verify(choices.runtimes):
|
|
208
211
|
done(outcome)
|
|
209
212
|
return outcomes
|
|
@@ -17,11 +17,11 @@ if TYPE_CHECKING:
|
|
|
17
17
|
from collections.abc import Callable
|
|
18
18
|
|
|
19
19
|
#: Kept in step with the plugins' bootstrap scripts and install.sh, which install the default connectors first.
|
|
20
|
-
BASE = "memgraph-context-graph[mcp]>=0.12.
|
|
20
|
+
BASE = "memgraph-context-graph[mcp]>=0.12.2"
|
|
21
21
|
VIEWER = ("context-graph-viewer", "context-graph-viewer>=0.1.1")
|
|
22
22
|
SPACES = ("context-graph-spaces", "context-graph-spaces>=0.1.2")
|
|
23
23
|
#: sessions-graph's local extraction, which setup doesn't install itself; ``unstructured2graph`` tells it is there.
|
|
24
|
-
RECONCILIATION = ("unstructured2graph", "sessions-graph[memgraph-context-graph,reconciliation]>=0.12.
|
|
24
|
+
RECONCILIATION = ("unstructured2graph", "sessions-graph[memgraph-context-graph,reconciliation]>=0.12.2")
|
|
25
25
|
#: Connectors whose own command people run: ``sessions-graph reconcile``, ``resources-graph sweep``.
|
|
26
26
|
EXECUTABLES = ("sessions-graph", "resources-graph")
|
|
27
27
|
|
|
@@ -6,6 +6,7 @@ Payload shapes are trimmed from a live agy 1.2.13 session.
|
|
|
6
6
|
import json
|
|
7
7
|
|
|
8
8
|
from memgraph_context_graph import AgentLink
|
|
9
|
+
from memgraph_context_graph.adapters import _spec
|
|
9
10
|
from memgraph_context_graph.adapters.antigravity_cli import PLUGIN, AntigravityCLIHooksAdapter, init
|
|
10
11
|
from memgraph_context_graph.events import (
|
|
11
12
|
ErrorOccurredEvent,
|
|
@@ -162,3 +163,18 @@ def test_init_writes_a_named_group_with_flat_lifecycle_hooks(tmp_path):
|
|
|
162
163
|
# agy rejects a nested entry on a lifecycle event, and with it the whole file.
|
|
163
164
|
assert group["Stop"] == [{"command": "capture --event-name Stop", "type": "command", "timeout": 12}]
|
|
164
165
|
assert group["SessionStart"] == [{"command": "capture --event-name SessionStart", "type": "command", "timeout": 12}]
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def test_init_on_windows_writes_commands_cmd_exe_can_run(tmp_path, monkeypatch):
|
|
169
|
+
# agy hands each command to cmd.exe inside its own quotes: a POSIX-quoted
|
|
170
|
+
# 'C:\...' path is not a filename there, and a double-quoted one is quoted twice.
|
|
171
|
+
exe = r"C:\Users\dev\.local\bin\memgraph-context-graph.EXE"
|
|
172
|
+
monkeypatch.setattr(_spec, "_ON_WINDOWS", True)
|
|
173
|
+
monkeypatch.setattr(_spec.shutil, "which", lambda name: exe)
|
|
174
|
+
|
|
175
|
+
init(tmp_path, ["actions-graph"])
|
|
176
|
+
|
|
177
|
+
group = json.loads((tmp_path / ".agents" / "hooks.json").read_text())["memgraph-context-graph"]
|
|
178
|
+
assert group["Stop"][0]["command"] == (
|
|
179
|
+
rf"{exe} hook run antigravity-cli --connector actions-graph --event-name Stop"
|
|
180
|
+
)
|
|
@@ -284,6 +284,49 @@ def test_a_blackholed_memgraph_fails_inside_the_hook_timeout(monkeypatch, tmp_pa
|
|
|
284
284
|
assert [line["stage"] for line in _lines(_capture_log_in_tmp)] == ["connect"]
|
|
285
285
|
|
|
286
286
|
|
|
287
|
+
def test_a_hook_blocked_past_its_deadline_answers_and_logs_where(tmp_path):
|
|
288
|
+
"""A query waiting on Memgraph's storage never raises, so the runtime killed the hook at its timeout, unlogged.
|
|
289
|
+
|
|
290
|
+
Run as its own process: the deadline ends the process, as it must to beat a hook thread that is still blocked.
|
|
291
|
+
"""
|
|
292
|
+
log = tmp_path / "capture-failures.jsonl"
|
|
293
|
+
hook = textwrap.dedent(
|
|
294
|
+
f"""
|
|
295
|
+
import dataclasses, sys, threading
|
|
296
|
+
from pathlib import Path
|
|
297
|
+
from memgraph_context_graph import AgentLink, capture_log
|
|
298
|
+
from memgraph_context_graph.adapters.claude_code import PLUGIN
|
|
299
|
+
from memgraph_context_graph.hooks import runner
|
|
300
|
+
|
|
301
|
+
capture_log._path_override = Path({str(log)!r})
|
|
302
|
+
runner.create_link = lambda *args, **kwargs: AgentLink()
|
|
303
|
+
|
|
304
|
+
class BlockedAdapter:
|
|
305
|
+
def __init__(self, link, session_id=None):
|
|
306
|
+
pass
|
|
307
|
+
|
|
308
|
+
def handle_payload(self, payload):
|
|
309
|
+
threading.Event().wait()
|
|
310
|
+
|
|
311
|
+
sys.exit(runner.run_hook(dataclasses.replace(PLUGIN, adapter_class=BlockedAdapter), ["--deadline", "1"]))
|
|
312
|
+
"""
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
start = time.monotonic()
|
|
316
|
+
done = subprocess.run(
|
|
317
|
+
[sys.executable, "-c", hook],
|
|
318
|
+
input='{"hook_event_name":"Stop","session_id":"s1"}',
|
|
319
|
+
capture_output=True,
|
|
320
|
+
text=True,
|
|
321
|
+
timeout=60,
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
assert time.monotonic() - start < 30
|
|
325
|
+
assert (done.returncode, done.stdout.strip()) == (0, '{"continue": true}')
|
|
326
|
+
(line,) = _lines(log)
|
|
327
|
+
assert (line["stage"], line["error_type"], line["session_id"]) == ("translate", "HookDeadline", "s1")
|
|
328
|
+
|
|
329
|
+
|
|
287
330
|
@pytest.mark.skipif(os.name == "nt", reason="POSIX permissions")
|
|
288
331
|
def test_writer_cost_is_small(_capture_log_in_tmp):
|
|
289
332
|
start = time.perf_counter()
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Tests for reading Claude Code's project-folder slugs back into checkouts.
|
|
2
|
+
|
|
3
|
+
Walking a slug against a real checkout is covered end to end in sessions-graph's
|
|
4
|
+
memory backend tests; a Windows drive can't be walked off Windows, so its root is
|
|
5
|
+
tested here.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
import pytest
|
|
11
|
+
|
|
12
|
+
from memgraph_context_graph.adapters.claude_code_memory import _slug_root
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@pytest.mark.parametrize(
|
|
16
|
+
("slug", "root"),
|
|
17
|
+
[
|
|
18
|
+
("-Users-dev-repos-ai-toolkit", (Path("/"), ["Users", "dev", "repos", "ai", "toolkit"])),
|
|
19
|
+
("C--source-x", (Path("C:/"), ["source", "x"])),
|
|
20
|
+
("d--work-my-repo", (Path("d:/"), ["work", "my", "repo"])),
|
|
21
|
+
# A dot-folder after the drive makes a third dash: C:\.config\x.
|
|
22
|
+
("C---config-x", (Path("C:/"), ["", "config", "x"])),
|
|
23
|
+
],
|
|
24
|
+
)
|
|
25
|
+
def test_a_slug_walks_from_the_root_its_path_started_at(slug, root):
|
|
26
|
+
assert _slug_root(slug) == root
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@pytest.mark.parametrize("slug", ["-", "", "C--", "source-x", "CD--x"])
|
|
30
|
+
def test_a_slug_with_no_root_has_no_checkout(slug):
|
|
31
|
+
assert _slug_root(slug) is None
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
"""Tests for the OpenAI Codex hooks adapter."""
|
|
2
2
|
|
|
3
3
|
import io
|
|
4
|
+
import json
|
|
4
5
|
|
|
5
6
|
from memgraph_context_graph import AgentLink
|
|
6
|
-
from memgraph_context_graph.adapters.codex import PLUGIN, CodexHooksAdapter
|
|
7
|
+
from memgraph_context_graph.adapters.codex import PLUGIN, CodexHooksAdapter, init
|
|
7
8
|
from memgraph_context_graph.events import (
|
|
8
9
|
Event,
|
|
9
10
|
EventType,
|
|
@@ -212,3 +213,14 @@ def test_load_payload_requires_json_object():
|
|
|
212
213
|
payload = load_payload(io.StringIO('{"hook_event_name": "Stop"}'))
|
|
213
214
|
|
|
214
215
|
assert payload == {"hook_event_name": "Stop"}
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def test_init_with_a_shorter_timeout_gives_the_hook_a_deadline_inside_it(tmp_path):
|
|
219
|
+
init(tmp_path, ["actions-graph"], timeout=10)
|
|
220
|
+
init(tmp_path / "default", ["actions-graph"])
|
|
221
|
+
|
|
222
|
+
def stop_command(project):
|
|
223
|
+
return json.loads((project / ".codex" / "hooks.json").read_text())["hooks"]["Stop"][0]["hooks"][0]["command"]
|
|
224
|
+
|
|
225
|
+
assert stop_command(tmp_path).endswith("--connector actions-graph --deadline 5")
|
|
226
|
+
assert stop_command(tmp_path / "default").endswith("--connector actions-graph")
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Codex memory switches preserve user settings in a throwaway home."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from memgraph_context_graph.adapters.codex_memory import CodexMemory
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@pytest.fixture
|
|
11
|
+
def codex_config(tmp_path, monkeypatch):
|
|
12
|
+
monkeypatch.delenv("CODEX_HOME", raising=False)
|
|
13
|
+
path = tmp_path / ".codex" / "config.toml"
|
|
14
|
+
path.parent.mkdir()
|
|
15
|
+
return tmp_path, path
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@pytest.mark.parametrize(
|
|
19
|
+
"original",
|
|
20
|
+
[
|
|
21
|
+
"[features]\nmemories = true\n",
|
|
22
|
+
"[features]\nmemories = false\n[memories]\nuse_memories = false\ngenerate_memories = false\n",
|
|
23
|
+
],
|
|
24
|
+
)
|
|
25
|
+
def test_repeating_hand_back_preserves_restored_settings(codex_config, original):
|
|
26
|
+
home, config = codex_config
|
|
27
|
+
config.write_text(original)
|
|
28
|
+
# Setup's older backup must not become the restoration source on a later retry.
|
|
29
|
+
config.with_name(config.name + ".context-graph.bak").write_text("[features]\nmemories = true\n")
|
|
30
|
+
native = CodexMemory()
|
|
31
|
+
native.disable(home)
|
|
32
|
+
native.enable(home)
|
|
33
|
+
restored = config.read_bytes()
|
|
34
|
+
|
|
35
|
+
native.enable(home)
|
|
36
|
+
|
|
37
|
+
assert config.read_bytes() == restored
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_hand_back_without_a_switch_preserves_native_settings(codex_config):
|
|
41
|
+
home, config = codex_config
|
|
42
|
+
original = b"[features]\nmemories = true\n[memories]\ngenerate_memories = false\n"
|
|
43
|
+
config.write_bytes(original)
|
|
44
|
+
|
|
45
|
+
CodexMemory().enable(home)
|
|
46
|
+
|
|
47
|
+
assert config.read_bytes() == original
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_hand_back_without_a_config_does_not_create_one(codex_config):
|
|
51
|
+
home, config = codex_config
|
|
52
|
+
|
|
53
|
+
CodexMemory().enable(home)
|
|
54
|
+
|
|
55
|
+
assert not config.exists()
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_a_legacy_backup_is_not_restored_as_the_missing_switch_backup(codex_config):
|
|
59
|
+
home, config = codex_config
|
|
60
|
+
original = b"[features]\nmemories = false\n[memories]\nuse_memories = false\ngenerate_memories = false\n"
|
|
61
|
+
config.write_bytes(original)
|
|
62
|
+
backup = config.with_name(config.name + ".context-graph.bak")
|
|
63
|
+
backup.write_text("[features]\nmemories = true\n")
|
|
64
|
+
|
|
65
|
+
lines = CodexMemory().enable(home)
|
|
66
|
+
|
|
67
|
+
assert config.read_bytes() == original
|
|
68
|
+
assert backup.read_text() == "[features]\nmemories = true\n"
|
|
69
|
+
assert any("To turn Codex memories on" in line and "wasn't restored automatically" in line for line in lines)
|
|
@@ -93,7 +93,7 @@ def test_wiring_refuses_the_home_directory(setup_home):
|
|
|
93
93
|
|
|
94
94
|
|
|
95
95
|
@pytest.mark.parametrize("runtime, binary", [("claude-code", "claude"), ("codex", "codex")])
|
|
96
|
-
def test_plugin_wiring_registers_installs_and_comes_off_again(fake_agents, runtime, binary):
|
|
96
|
+
def test_plugin_wiring_registers_installs_and_comes_off_again(fake_agents, setup_home, runtime, binary):
|
|
97
97
|
agent = BY_RUNTIME[runtime]
|
|
98
98
|
lines, log = _logged()
|
|
99
99
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{memgraph_context_graph-0.12.1 → memgraph_context_graph-0.12.2}/examples/sdk_claude_agent.py
RENAMED
|
File without changes
|
{memgraph_context_graph-0.12.1 → memgraph_context_graph-0.12.2}/examples/sdk_openai_agents.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{memgraph_context_graph-0.12.1 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/cli.py
RENAMED
|
File without changes
|
|
File without changes
|
{memgraph_context_graph-0.12.1 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/events.py
RENAMED
|
File without changes
|
{memgraph_context_graph-0.12.1 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/forget.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{memgraph_context_graph-0.12.1 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/link.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{memgraph_context_graph-0.12.1 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/spaces.py
RENAMED
|
File without changes
|
|
File without changes
|
{memgraph_context_graph-0.12.1 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/tools.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{memgraph_context_graph-0.12.1 → memgraph_context_graph-0.12.2}/tests/test_agent_memory_wiring.py
RENAMED
|
File without changes
|
{memgraph_context_graph-0.12.1 → memgraph_context_graph-0.12.2}/tests/test_capture_policy.py
RENAMED
|
File without changes
|
{memgraph_context_graph-0.12.1 → memgraph_context_graph-0.12.2}/tests/test_claude_code_adapter.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{memgraph_context_graph-0.12.1 → memgraph_context_graph-0.12.2}/tests/test_context_before_tool.py
RENAMED
|
File without changes
|
{memgraph_context_graph-0.12.1 → memgraph_context_graph-0.12.2}/tests/test_copilot_cli_adapter.py
RENAMED
|
File without changes
|
{memgraph_context_graph-0.12.1 → memgraph_context_graph-0.12.2}/tests/test_cursor_adapter.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{memgraph_context_graph-0.12.1 → memgraph_context_graph-0.12.2}/tests/test_opencode_adapter.py
RENAMED
|
File without changes
|
|
File without changes
|
{memgraph_context_graph-0.12.1 → memgraph_context_graph-0.12.2}/tests/test_runtime_plugin.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|