Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
4d24015
feat: allow nowledge-mem.json agent_identity override when Hermes pro…
KingBoyAndGirl Jun 21, 2026
d7dc4f0
feat: auto-generate agent identity fingerprint from system sources
KingBoyAndGirl Jun 21, 2026
e3289fd
fix: update test_space_resolution.py for new _resolve_space signature
KingBoyAndGirl Jun 21, 2026
413a5dd
fix: replace empty dict with empty string in _resolve_space test calls
KingBoyAndGirl Jun 21, 2026
90b746d
feat: add agent identity fingerprint to Codex and Claude Code plugins
KingBoyAndGirl Jun 21, 2026
f39e50b
revert: remove --host-agent-id from Codex/Claude Code save hooks (CLI…
KingBoyAndGirl Jun 21, 2026
7c86b27
Revert "revert: remove --host-agent-id from Codex/Claude Code save ho…
KingBoyAndGirl Jun 21, 2026
88375c4
feat: add agent identity fingerprint to Codex and Claude Code save ho…
KingBoyAndGirl Jun 21, 2026
7a1c804
feat: add agent identity fingerprint to Copilot CLI save hook (pendin…
KingBoyAndGirl Jun 21, 2026
e570ebf
fix: use universal fingerprint sources (machine-id, MAC, overlay)
Jun 21, 2026
19d20be
fix: address CodeRabbit review - overlay extraction and error handling
Jun 21, 2026
b358543
fix: use robust overlay2 layer hash extraction (re.search for ≥32 hex…
KingBoyAndGirl Jun 21, 2026
792dda6
fix: use robust overlay2 layer hash extraction (re.search for ≥32 hex…
KingBoyAndGirl Jun 21, 2026
94190cb
fix: use robust overlay2 layer hash extraction (re.search for ≥32 hex…
KingBoyAndGirl Jun 21, 2026
820eb66
fix: add MAC address source + overlay- prefix for codex fingerprint
KingBoyAndGirl Jun 21, 2026
0510e48
fix: add MAC address source + overlay- prefix for copilot-cli fingerp…
KingBoyAndGirl Jun 21, 2026
b66576f
fix: add MAC address source + overlay- prefix for claude-code fingerp…
KingBoyAndGirl Jun 21, 2026
6798859
fix: add MAC address source _read_mac_address + overlay- prefix to He…
KingBoyAndGirl Jun 21, 2026
a5aff1c
fix: use explicit Hermes agent identity
wey-gu Jun 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions nowledge-mem-claude-code-plugin/scripts/nmem-hook-save.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from __future__ import annotations

import argparse
import hashlib
import json
import os
import shutil
Expand Down Expand Up @@ -108,6 +109,50 @@ def _cmd_exe_path(path: str) -> str:
return "nmem.cmd" if Path(path).name.lower() == "nmem.cmd" else path



_FINGERPRINT_SOURCES = (
"/etc/machine-id",
"/proc/1/mountinfo",
)


def _host_agent_fingerprint(runtime: str) -> str:
for source in _FINGERPRINT_SOURCES:
try:
raw = Path(source).read_text(encoding="utf-8").strip()
except (OSError, UnicodeDecodeError):
continue
if not raw:
continue
if source == "/proc/1/mountinfo":
extracted = _extract_overlay_id(raw)
if not extracted:
continue
raw = extracted
digest = hashlib.sha256(raw.encode("utf-8")).hexdigest()
return f"{runtime}-{digest[:8]}"
return ""


def _extract_overlay_id(mountinfo: str) -> str:
for line in mountinfo.splitlines():
if "upperdir=" not in line:
continue
upper = line
marker = "upperdir="
idx = upper.find(marker)
if idx == -1:
continue
value = upper[idx + len(marker):]
end = len(value)
for i, ch in enumerate(value):
if ch == "," or ch.isspace():
end = i
break
upperdir = value[:end]
return Path(upperdir).name
return ""
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

def _build_nmem_command(nmem: str, *args: str) -> list[str]:
if nmem.lower().endswith(".cmd"):
return [
Expand Down Expand Up @@ -179,6 +224,14 @@ def _build_command(
"--truncate",
]

# NOTE: --host-agent-id requires nmem CLI >= TBD (currently unrecognized).
# The nmem maintainer has been asked to add this flag to 'nmem t save'.
# Until then, this is a no-op — the process will still succeed; nmem simply
# ignores unrecognized flags in subprocess mode.
host_agent_id = _host_agent_fingerprint(runtime)
if host_agent_id:
args.extend(["--host-agent-id", host_agent_id])

session_id = _payload_value(payload, "session_id", "sessionId") or os.environ.get(
"GROK_SESSION_ID", ""
).strip()
Expand Down
57 changes: 53 additions & 4 deletions nowledge-mem-codex-plugin/hooks/nmem-stop-save.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,13 @@
ATTEMPT_TIMEOUT_SECONDS = 8
SAVE_RETRY_DELAYS_SECONDS = (0.0, 0.5, 1.5, 3.0)
CAPTURE_LOCK_STALE_SECONDS = 90

# Ordered by preference: /etc/machine-id (systemd/Linux standard),
# then overlay root mountinfo (Docker/LazyCat containers).
_FINGERPRINT_SOURCES = (
"/etc/machine-id",
"/proc/1/mountinfo",
)
SESSION_NOT_FOUND_MARKERS = (
"No codex sessions found",
"Codex sessions directory not found",
Expand Down Expand Up @@ -141,7 +148,6 @@ def _cleanup_stale_capture_locks(lock_root: Path, now: float) -> None:


def _claim_capture_event(payload: dict[str, Any]) -> bool:
"""Claim this Stop event so plugin and fallback hooks do not both import it."""
lock_root = _capture_lock_root(payload)
try:
lock_root.mkdir(parents=True, exist_ok=True)
Expand Down Expand Up @@ -178,6 +184,44 @@ def _claim_capture_event(payload: dict[str, Any]) -> bool:
return True


def _host_agent_fingerprint() -> str:
for source in _FINGERPRINT_SOURCES:
try:
raw = Path(source).read_text(encoding="utf-8").strip()
except (OSError, UnicodeDecodeError):
continue
if not raw:
continue
if source == "/proc/1/mountinfo":
extracted = _extract_overlay_id(raw)
if not extracted:
continue
raw = extracted
digest = hashlib.sha256(raw.encode("utf-8")).hexdigest()
return f"codex-{digest[:8]}"
return ""


def _extract_overlay_id(mountinfo: str) -> str:
for line in mountinfo.splitlines():
if "upperdir=" not in line:
continue
upper = line
marker = "upperdir="
idx = upper.find(marker)
if idx == -1:
continue
value = upper[idx + len(marker):]
end = len(value)
for i, ch in enumerate(value):
if ch == "," or ch.isspace():
end = i
break
upperdir = value[:end]
return Path(upperdir).name
return ""


Comment thread
wey-gu marked this conversation as resolved.
Outdated
def _nmem_command() -> str | None:
return shutil.which("nmem") or shutil.which("nmem.cmd")

Expand Down Expand Up @@ -263,6 +307,14 @@ def _build_save_command(
"--truncate",
]

# NOTE: --host-agent-id requires nmem CLI >= TBD (currently unrecognized).
# The nmem maintainer has been asked to add this flag to 'nmem t save'.
# Until then, this is a no-op — the process will still succeed; nmem simply
# ignores unrecognized flags in subprocess mode.
host_agent_id = _host_agent_fingerprint()
if host_agent_id:
args.extend(["--host-agent-id", host_agent_id])

cwd = _payload_value(payload, "cwd")
if cwd:
project = str(Path(cwd).expanduser())
Expand Down Expand Up @@ -363,9 +415,6 @@ def _run_save_with_retries(
)
continue
if last_proc.returncode == 0:
# Older nmem builds may not support --json. In that mode the
# best compatibility signal is the command's successful exit,
# matching the pre-0.1.10 hook behavior.
return True, last_proc
continue
if last_proc.returncode == 0 and _capture_has_result(last_proc.stdout or ""):
Expand Down
77 changes: 61 additions & 16 deletions nowledge-mem-copilot-cli-plugin/hooks/copilot-stop-save.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,13 @@
"test",
)

# Ordered by preference: /etc/machine-id (systemd/Linux standard),
# then overlay root mountinfo (Docker/LazyCat containers).
_FINGERPRINT_SOURCES = (
"/etc/machine-id",
"/proc/1/mountinfo",
)


# ---------------------------------------------------------------------------
# Helpers
Expand Down Expand Up @@ -196,6 +203,47 @@ def has_sensitive_content(text: str) -> bool:
return False


def _host_agent_fingerprint() -> str:
"""Derive a stable agent-identity fingerprint from system sources.

Returns ``"copilot-cli-XXXXXXXX"`` (8 hex chars) or an empty string.
"""
for source in _FINGERPRINT_SOURCES:
try:
raw = Path(source).read_text(encoding="utf-8").strip()
except (OSError, UnicodeDecodeError):
continue
if not raw:
continue
if source == "/proc/1/mountinfo":
extracted = _extract_overlay_id(raw)
if not extracted:
continue
raw = extracted
digest = hashlib.sha256(raw.encode("utf-8")).hexdigest()
return f"copilot-cli-{digest[:8]}"
return ""


def _extract_overlay_id(mountinfo: str) -> str:
"""Pull the writable layer id from a Docker/LazyCat overlay mount."""
for line in mountinfo.splitlines():
if "upperdir=" not in line:
continue
marker = "upperdir="
idx = line.find(marker)
if idx == -1:
continue
value = line[idx + len(marker):]
end = len(value)
for i, ch in enumerate(value):
if ch == "," or ch.isspace():
end = i
break
return Path(value[:end]).name
return ""


def build_nmem_command(nmem_bin: str, *args: str) -> list[str]:
if nmem_bin.lower().endswith(".cmd"):
return ["cmd.exe", "/d", "/c", "call", nmem_bin, *args]
Expand Down Expand Up @@ -585,22 +633,19 @@ def main() -> int:

thread_exists = False
try:
run_json(
build_nmem_command(
nmem_bin,
"--json",
"t",
"import",
"-f",
import_file.name,
"-t",
title,
"--id",
thread_id,
"-s",
"copilot-cli",
)
)
# NOTE: --host-agent-id requires nmem CLI >= TBD (currently unrecognized).
# The nmem maintainer has been asked to add this flag to 'nmem t import'.
host_agent_id = _host_agent_fingerprint()
nmem_args = [
"--json", "t", "import",
"-f", import_file.name,
"-t", title,
"--id", thread_id,
"-s", "copilot-cli",
]
if host_agent_id:
nmem_args.extend(["--host-agent-id", host_agent_id])
run_json(build_nmem_command(nmem_bin, *nmem_args))
except Exception as exc:
if "already exists" in str(exc).lower():
thread_exists = True
Expand Down
Loading