Skip to content

feat(copilot): add Nowledge Mem plugin for Copilot CLI#201

Merged
wey-gu merged 5 commits intonowledge-co:mainfrom
HamsteRider-m:copilot/copilot-cli-plugin
Apr 21, 2026
Merged

feat(copilot): add Nowledge Mem plugin for Copilot CLI#201
wey-gu merged 5 commits intonowledge-co:mainfrom
HamsteRider-m:copilot/copilot-cli-plugin

Conversation

@HamsteRider-m
Copy link
Copy Markdown

@HamsteRider-m HamsteRider-m commented Apr 16, 2026

Summary

  • add a dedicated nowledge-mem-copilot-cli-plugin package with Copilot-native hooks, skills, installer, docs, and tests
  • capture Copilot CLI sessions incrementally into Nowledge Mem with secret filtering, lock protection, incomplete-turn detection, and auto-distill guardrails
  • register the new integration in integrations.json so it can ship through the Nowledge community marketplace

Why this exists

GitHub Copilot CLI can load Claude Code-style plugins, but using the Claude Code integration directly is brittle in practice because some lifecycle assumptions, command surfaces, and config paths are Claude-specific.

This PR therefore treats the Copilot plugin as a real adaptation, not a straight copy:

  • behavior and product intent were inherited from the Claude Code integration
  • Copilot-facing packaging, hook wiring, installer flow, and transcript capture were rewritten for Copilot CLI
  • the result preserves the Nowledge Mem workflow on Copilot without relying on Claude-only runtime assumptions

Copilot surface note

This plugin is now intentionally skill-only on the user-facing surface.

Earlier iterations shipped both command docs and skills, which could make Copilot expose overlapping entries for the same underlying action. That was especially confusing for explicit save flows.

The current shape is:

  • lifecycle hooks for automatic Working Memory load, per-turn guidance, and async session capture
  • skills for explicit model-mediated actions such as read-working-memory, search-memory, distill-memory, and save-thread
  • no separate Copilot command-doc surface for save/search/sum/status

For direct terminal troubleshooting outside the skill surface, users should run the nmem CLI directly, for example nmem status.

Implementation approach

The recorded build session shows an orchestrator-worker approach rather than a role-play pipeline:

  • parallel research across the existing Claude Code, Hermes, Codex, Droid, OpenCode, and registry patterns
  • a rubber-duck critique pass before implementation
  • phased implementation across registry, hooks/skills/commands, Python capture runtime, docs, and tests
  • a final independent external review pass requested from GPT-5.4

The captured planning artifacts also show three design corrections made before/during implementation:

  1. switch thread identity from per-turn IDs to stable per-session IDs: copilot-{session_id}
  2. make the installer idempotent instead of assuming a one-shot setup flow
  3. keep Python as the primary runtime and add platform-aware locking behavior instead of depending on a Unix-only shell path

What the reviewer should look at first

If you want to review from the source of the design rather than only from the final diff, start here:

  • nowledge-mem-copilot-cli-plugin/scripts/copilot-stop-save.py — core Copilot transcript capture and incremental thread save logic
  • nowledge-mem-copilot-cli-plugin/hooks/hooks.json — lifecycle wiring for SessionStart, UserPromptSubmit, and async Stop
  • nowledge-mem-copilot-cli-plugin/README.md and AGENTS.md — user-facing and agent-facing behavior model, now documented as skill-only
  • integrations.json — registry entry for the new copilot-cli integration

Session evidence and validation notes

The Copilot session artifacts I reviewed show:

  • the main implementation session running on claude-opus-4.6
  • parallel exploration work recorded with claude-haiku-4.5
  • rubber-duck / external-review activity recorded against gpt-5.4

I did not find reliable evidence for a specific reasoning-effort tier such as high, so I am intentionally not claiming that here.

The session artifacts clearly preserve the planning and review flow. They are less consistent on which validation steps were fully completed versus merely planned, so I would treat the recorded validation narrative as implementation context rather than as the authoritative release checklist.

Test plan

  • local install flow exercised via scripts/install-hooks.sh
  • plugin assets and registry entry are wired for Copilot CLI
  • please run your normal maintainer validation pass on the current branch before merge

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Nowledge Mem plugin for Copilot CLI with integrated knowledge management, working-memory briefing at session start, automatic session capture, and four skills (Read Working Memory, Search Memory, Distill Memory, Save Thread)
    • Lifecycle hooks and installer to enable automatic capture and import workflows
  • Documentation

    • Comprehensive README, AGENTS guide, CHANGELOG, and release instructions
  • Tests

    • New test suite validating transcript parsing, redaction, state handling, and command invocation behaviors

Full-featured plugin with auto-capture, working memory, search, and distill.

- Plugin manifest (.claude-plugin/plugin.json) v0.1.0
- 4 lifecycle hooks (SessionStart×2, UserPromptSubmit, Stop)
- 4 skills (read-working-memory, search-memory, distill-memory, save-thread)
- 4 commands (/save, /search, /sum, /status)
- Session capture script (copilot-stop-save.py) with:
  - Incremental append via nmem t import + nmem t append fallback
  - Secret filtering (6 patterns) and sensitive content detection
  - Auto-distill with cooldown, content hash dedup, min thresholds
  - Cross-platform locking (fcntl/msvcrt)
  - Incomplete turn detection (background tasks, ask_user, questions)
- Idempotent installer (install-hooks.sh)
- Registry entry in integrations.json (plugin-capture transport)
- Full test suite (14 tests covering all capture script logic)
- Documentation: README, CHANGELOG, RELEASING, AGENTS.md

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 16, 2026

Warning

Rate limit exceeded

@wey-gu has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 14 minutes and 48 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 14 minutes and 48 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 10f0e33d-dd16-46fb-ad5b-d5482e68427f

📥 Commits

Reviewing files that changed from the base of the PR and between d097ce3 and 1f6ad21.

📒 Files selected for processing (2)
  • integrations.json
  • nowledge-mem-copilot-cli-plugin/README.md
📝 Walkthrough

Walkthrough

Adds a Copilot CLI integration and a Nowledge Mem Copilot plugin that loads working memory, nudges per-turn memory actions, captures sessions on stop via hooks, imports threads into nmem, and provides skills, install scripts, docs, and tests.

Changes

Cohort / File(s) Summary
Integration Registry
integrations.json
Adds copilot-cli plugin entry (id: copilot-cli) with transport: "cli", plugin-capture thread-save method, autonomy policy, toolNaming, skills list, install/update commands and detection hints.
Plugin Manifest
nowledge-mem-copilot-cli-plugin/.claude-plugin/plugin.json
New plugin manifest: metadata, version 0.1.0, author, repo, license, keywords.
Hook Configuration
nowledge-mem-copilot-cli-plugin/hooks/hooks.json
New lifecycle hooks: SessionStart (startup
Capture Implementation
nowledge-mem-copilot-cli-plugin/scripts/copilot-stop-save.py, .../copilot-stop-save.sh
New stop-hook Python processor with state/locking, transcript parsing, incomplete-turn detection, secret redaction, nmem import/append, guarded auto-distill, and JSONL logging; bash launcher wrapper.
Installer
nowledge-mem-copilot-cli-plugin/scripts/install-hooks.sh
Idempotent installer that copies hooks/scripts to ~/.copilot/nowledge-mem-hooks/, sets exec bits, and verifies python3/nmem availability.
Skills docs
nowledge-mem-copilot-cli-plugin/skills/.../SKILL.md
Adds skill docs for read-working-memory, search-memory, distill-memory, save-thread describing triggers, CLI commands, and guidance.
Docs & Release
nowledge-mem-copilot-cli-plugin/README.md, AGENTS.md, CHANGELOG.md, RELEASING.md
Comprehensive README, agent behavior doc, changelog (0.1.0 unreleased), and releasing instructions covering hooks, capture behavior, and testing checklist.
Tests
nowledge-mem-copilot-cli-plugin/tests/test_copilot_plugin.py
Pytest suite exercising transcript parsing, redaction, incomplete-turn heuristics, state persistence, content hashing, and platform CLI command construction.

Sequence Diagram

sequenceDiagram
    participant Copilot as Copilot CLI
    participant Hook as Hook System
    participant PyScript as copilot-stop-save.py
    participant StateFile as Session State (lock/file)
    participant Transcript as Transcript JSON(.l)
    participant NmemCLI as nmem CLI

    Copilot->>Hook: Trigger Stop hook (stdin JSON)
    Hook->>PyScript: Invoke script with payload
    PyScript->>PyScript: Parse payload (sessionId, transcriptPath, stopReason)
    PyScript->>StateFile: Acquire per-session lock
    StateFile-->>PyScript: Lock acquired
    PyScript->>Transcript: Read/parse transcript events
    PyScript->>PyScript: Select user→assistant turn window
    PyScript->>PyScript: Redact secrets & detect incomplete/background work
    alt Turn invalid or sensitive
        PyScript->>PyScript: Log skip and persist state
    else Valid turn
        PyScript->>NmemCLI: `nmem t import` (temp file, thread id copilot-{session})
        NmemCLI-->>PyScript: Import result (or exists)
        PyScript->>NmemCLI: `nmem t append` fallback or `nmem t triage`/`t distill` as guarded
        PyScript->>PyScript: Update state (last_saved_turn_end_id, last_distill_ts, hash)
    end
    PyScript->>StateFile: Write JSONL log entry and release lock
    StateFile-->>PyScript: Lock released
    PyScript-->>Hook: Exit (0 / non-zero)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰
I hop through sessions, quiet and sly,
Hooks gather threads as conversations fly,
Secrets tucked safely, insights condensed tight,
Nowledge stored neat through day and night,
— The Memory Rabbit 🥕✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 5.80% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and accurately summarizes the main addition: a new Nowledge Mem plugin for GitHub Copilot CLI, which is the core objective of this PR.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: eee5555b54

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

return [json.loads(line) for line in fh]


def find_index(events: list[dict], event_id: str | None) -> int | None:
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep capture script compatible with advertised Python 3

This script uses 3.10-only type syntax (str | None / -> int | None), but the installer only checks for python3 and does not require 3.10+, so users on Python 3.8/3.9 will hit a SyntaxError before the Stop hook can run. In those environments, session capture silently never works (the hook command ends with || true). Either enforce a minimum Python version during install or replace these annotations with older-compatible typing syntax.

Useful? React with 👍 / 👎.

Comment on lines +574 to +577
triage = run_json(
build_nmem_command(
nmem_bin, "--json", "t", "triage", thread_id
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Persist state even when triage/distill fails

A failure in nmem t triage (for example, older nmem versions without this subcommand or transient backend errors) raises out of main() before last_saved_turn_end_id is updated, because state is saved only later. After that, each Stop hook reprocesses the same turn and keeps trying to import/append it again, causing repeated duplicate appends and noisy errors. Triage/distill should be best-effort and must not block state advancement once thread import/append succeeded.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (10)
nowledge-mem-copilot-cli-plugin/skills/save-thread/SKILL.md (1)

10-12: Include /save in explicit trigger examples.

Please add /save to the explicit “when to save” trigger list so the skill aligns with command-driven usage.

As per coding guidelines: Use slash commands (/save, /search <query>, /sum, /status) to manage Nowledge Mem integration.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@nowledge-mem-copilot-cli-plugin/skills/save-thread/SKILL.md` around lines 10
- 12, Update the explicit trigger examples in SKILL.md's "Only when user
explicitly says:" list to include the slash command `/save` alongside the
existing phrases ("Save this session", "Checkpoint this", "Record conversation")
so the skill supports command-driven usage; edit the trigger list in SKILL.md to
add `/save` and ensure consistency with other command examples (`/search
<query>`, `/sum`, `/status`) mentioned in the guidelines.
nowledge-mem-copilot-cli-plugin/commands/status.md (1)

9-13: Prefer /status as the command shown to users.

Please make /status the top-level invocation in this page, and keep nmem status as implementation/troubleshooting detail.

As per coding guidelines: Use slash commands (/save, /search <query>, /sum, /status) to manage Nowledge Mem integration.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@nowledge-mem-copilot-cli-plugin/commands/status.md` around lines 9 - 13,
Update the example in status.md to present "/status" as the primary invocation
and move "nmem status" into a secondary implementation/troubleshooting note;
specifically replace the top-level code block showing "nmem status" with
"/status" and add a small following paragraph or code block that notes the
equivalent CLI command "nmem status" for debugging or local runs so both usages
appear but "/status" is shown as the canonical user-facing command.
nowledge-mem-copilot-cli-plugin/commands/search.md (1)

6-14: Add /search <query> as the primary command example.

The current section is technically correct, but this command page should lead with the slash command and then optionally show the underlying nmem --json ... call.

As per coding guidelines: Use slash commands (/save, /search <query>, /sum, /status) to manage Nowledge Mem integration.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@nowledge-mem-copilot-cli-plugin/commands/search.md` around lines 6 - 14, The
documentation should lead with the slash command example; update the Search
Memory section to present "/search <query>" as the primary command example
(e.g., show a code block with "/search <query>") and then optionally include the
underlying CLI invocation "nmem --json m search \"$ARGUMENTS\"" afterwards;
ensure you keep the existing heading "Search Memory" and the explanatory
sentence, and follow the guideline to use slash commands (`/save`, `/search
<query>`, `/sum`, `/status`) as the primary examples.
nowledge-mem-copilot-cli-plugin/commands/sum.md (1)

7-30: Make /sum the primary invocation in this command page.

This page currently teaches direct nmem usage first; for consistency with plugin UX, show /sum as the primary action and keep raw CLI as implementation detail/reference.

As per coding guidelines: Use slash commands (/save, /search <query>, /sum, /status) to manage Nowledge Mem integration.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@nowledge-mem-copilot-cli-plugin/commands/sum.md` around lines 7 - 30, The
page should present the slash command /sum as the primary invocation and move
the raw nmem CLI example (nmem m add ...) to a secondary "Implementation/CLI
reference" section; update the introduction and the top-level example to show
"/sum" (and related slash commands /save, /search <query>, /status) as the
recommended workflow, then retain the existing nmem CLI snippet only as an
optional reference and label it accordingly; modify headings and order in sum.md
and adjust any examples or usage steps that currently teach direct nmem usage
first so they now demonstrate slash-command usage first.
nowledge-mem-copilot-cli-plugin/commands/save.md (1)

9-17: Show /save first in the Usage section.

Since this is the /save command doc, lead with slash-command usage and keep nmem t create ... as the underlying execution detail.

As per coding guidelines: Use slash commands (/save, /search <query>, /sum, /status) to manage Nowledge Mem integration.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@nowledge-mem-copilot-cli-plugin/commands/save.md` around lines 9 - 17,
Reorder the Usage section to lead with the slash command `/save` (showing the
exact slash-command usage first) and then present the `nmem t create "..."
--title "..." -s copilot-cli` example as the underlying execution detail; update
the text to state that `/save` triggers an immediate save and that `nmem t
create` is the equivalent lower-level invocation, and ensure the slash-command
conforms to the established style used elsewhere (`/save`, `/search <query>`,
`/sum`, `/status`) so readers see the slash command first and the `nmem t
create` form as supplemental.
nowledge-mem-copilot-cli-plugin/tests/test_copilot_plugin.py (3)

324-345: Test isolation: mutating module-level STATE_DIR can cause flaky tests.

Directly assigning copilot_stop_save.STATE_DIR = Path(tmpdir) mutates global state that persists across tests. If test ordering changes or tests run in parallel, this can cause failures. Also, the unpacked path and path2 variables are unused (per static analysis).

♻️ Proposed fix using `patch` and underscore prefix
 class TestStateManagement:
     def test_load_state_new_session(self):
         with tempfile.TemporaryDirectory() as tmpdir:
-            copilot_stop_save.STATE_DIR = Path(tmpdir)
-            path, state = copilot_stop_save.load_state("new-session")
+            with patch.object(copilot_stop_save, "STATE_DIR", Path(tmpdir)):
+                _path, state = copilot_stop_save.load_state("new-session")
             assert state["active_start_event_id"] is None
             assert state["last_saved_turn_end_id"] is None
             assert state["last_distill_ts"] == 0
             assert state["last_content_hash"] is None

     def test_save_and_load_state(self):
         with tempfile.TemporaryDirectory() as tmpdir:
-            copilot_stop_save.STATE_DIR = Path(tmpdir)
-            path, state = copilot_stop_save.load_state("test-session")
-            state["last_saved_turn_end_id"] = "e4"
-            state["last_distill_ts"] = 1000
-            state["last_content_hash"] = "abc123"
-            copilot_stop_save.save_state(path, state)
-
-            path2, state2 = copilot_stop_save.load_state("test-session")
-            assert state2["last_saved_turn_end_id"] == "e4"
-            assert state2["last_distill_ts"] == 1000
-            assert state2["last_content_hash"] == "abc123"
+            with patch.object(copilot_stop_save, "STATE_DIR", Path(tmpdir)):
+                path, state = copilot_stop_save.load_state("test-session")
+                state["last_saved_turn_end_id"] = "e4"
+                state["last_distill_ts"] = 1000
+                state["last_content_hash"] = "abc123"
+                copilot_stop_save.save_state(path, state)
+
+                _path2, state2 = copilot_stop_save.load_state("test-session")
+                assert state2["last_saved_turn_end_id"] == "e4"
+                assert state2["last_distill_ts"] == 1000
+                assert state2["last_content_hash"] == "abc123"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@nowledge-mem-copilot-cli-plugin/tests/test_copilot_plugin.py` around lines
324 - 345, The tests mutate the module-level copilot_stop_save.STATE_DIR
directly which can leak between tests; update both test_load_state_new_session
and test_save_and_load_state to patch STATE_DIR only for the test scope (use
unittest.mock.patch or pytest monkeypatch to set copilot_stop_save.STATE_DIR =
Path(tmpdir) inside the context) and avoid assigning unused variables by
renaming path and path2 to _path/_path2 or not capturing them when calling
load_state; keep calls to load_state and save_state unchanged.

383-392: Test leaves temporary file behind on assertion failure.

If an assertion fails before os.unlink(f.name), the temp file remains. Using a context manager with delete_on_close=False (Python 3.12+) or a try/finally block ensures cleanup:

♻️ Proposed fix for reliable cleanup
     def test_load_events_from_jsonl(self):
         events = basic_transcript()
-        with tempfile.NamedTemporaryFile("w", suffix=".jsonl", delete=False) as f:
-            for event in events:
-                f.write(json.dumps(event) + "\n")
-            f.flush()
-            loaded = copilot_stop_save.load_events(f.name)
-            assert len(loaded) == len(events)
-            assert loaded[0]["type"] == "user.message"
-            os.unlink(f.name)
+        with tempfile.TemporaryDirectory() as tmpdir:
+            fpath = Path(tmpdir) / "transcript.jsonl"
+            with fpath.open("w") as f:
+                for event in events:
+                    f.write(json.dumps(event) + "\n")
+            loaded = copilot_stop_save.load_events(str(fpath))
+            assert len(loaded) == len(events)
+            assert loaded[0]["type"] == "user.message"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@nowledge-mem-copilot-cli-plugin/tests/test_copilot_plugin.py` around lines
383 - 392, The test test_load_events_from_jsonl leaves the temp file behind if
an assertion fails; wrap the body that writes, calls
copilot_stop_save.load_events and asserts in a try/finally so os.unlink(f.name)
is always called (or use a context manager that ensures deletion), locating the
tempfile usage in test_load_events_from_jsonl and the call to
copilot_stop_save.load_events to add the try/finally cleanup around those
operations.

151-174: Secret redaction tests use weak assertions.

The assertions like assert "ghp_" not in result or "[REDACTED]" in result pass even if the token is partially present. A stricter check ensures the secret is fully redacted:

♻️ Proposed stricter assertions
     def test_redacts_github_token(self):
         text = "Use token ghp_1234567890abcdefghijklmn"
         result = copilot_stop_save.redact(text)
-        assert "ghp_" not in result or "[REDACTED]" in result
+        assert "ghp_1234567890abcdefghijklmn" not in result
+        assert "[REDACTED]" in result

     def test_redacts_github_pat(self):
         text = "Token: github_pat_1234567890abcdefghijklmn"
         result = copilot_stop_save.redact(text)
-        assert "github_pat_" not in result or "[REDACTED]" in result
+        assert "github_pat_1234567890abcdefghijklmn" not in result
+        assert "[REDACTED]" in result

     def test_redacts_openai_key(self):
         text = "API key is sk-1234567890abcdefgh"
         result = copilot_stop_save.redact(text)
-        assert "sk-" not in result or "[REDACTED]" in result
+        assert "sk-1234567890abcdefgh" not in result
+        assert "[REDACTED]" in result
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@nowledge-mem-copilot-cli-plugin/tests/test_copilot_plugin.py` around lines
151 - 174, The tests (test_redacts_github_token, test_redacts_github_pat,
test_redacts_openai_key, test_redacts_bearer_token) use weak OR assertions that
allow partial secrets to remain; update each to assert the secret prefix is
absent AND that "[REDACTED]" appears (e.g. replace `assert "ghp_" not in result
or "[REDACTED]" in result` with `assert "ghp_" not in result and "[REDACTED]" in
result`), and do the same for "github_pat_", "sk-" and the bearer case when
calling copilot_stop_save.redact to ensure full redaction while keeping
test_preserves_normal_text unchanged.
nowledge-mem-copilot-cli-plugin/README.md (1)

32-37: WSL shim may break on arguments containing spaces or special characters.

The quoting approach q="$q \"$a\"" doesn't handle arguments with embedded quotes, backslashes, or other special characters. Consider using printf '%q ' for proper shell escaping:

♻️ Proposed fix for robust argument handling
 mkdir -p ~/.local/bin && cat > ~/.local/bin/nmem << 'SHIMEOF'
 #!/bin/bash
-q=""; for a in "$@"; do q="$q \"$a\""; done
-cmd.exe /s /c "\"nmem.cmd\"$q"
+args=()
+for a in "$@"; do args+=("$(printf '%q' "$a")"); done
+cmd.exe /s /c "nmem.cmd ${args[*]}"
 SHIMEOF
 chmod +x ~/.local/bin/nmem
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@nowledge-mem-copilot-cli-plugin/README.md` around lines 32 - 37, The WSL shim
in the nmem script builds q with q="$q \"$a\"" which fails on embedded
quotes/backslashes; update the argument-quoting loop in the nmem shim to use a
robust shell-escaping approach (e.g., build q with printf '%q' for each "$a" or
otherwise use an array and proper quoting) so arguments containing
spaces/special chars are escaped correctly; look for the q variable and the for
a in "$@" loop in the nmem shim and replace the naive concatenation with printf
'%q' "$a" (or an equivalent safe-escaping strategy) before invoking cmd.exe.
nowledge-mem-copilot-cli-plugin/scripts/copilot-stop-save.py (1)

314-321: File locking on Windows locks only 1 byte of an empty file.

msvcrt.locking(lock_fh.fileno(), msvcrt.LK_LOCK, 1) locks 1 byte, but the file is empty (just opened with "w"). While this works as a mutex (any concurrent lock attempt will block), the lock is never released explicitly—relying on file handle close. Also, if the file grows beyond 1 byte from another process writing, the lock scope is ambiguous.

Consider writing a sentinel byte before locking, or document this is intentional mutex-style locking:

♻️ Proposed improvement
     with lock_path.open("w", encoding="utf-8") as lock_fh:
+        lock_fh.write("L")  # Sentinel byte for msvcrt locking
+        lock_fh.flush()
         if fcntl:
             fcntl.flock(lock_fh, fcntl.LOCK_EX)
         else:
             import msvcrt
             msvcrt.locking(lock_fh.fileno(), msvcrt.LK_LOCK, 1)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@nowledge-mem-copilot-cli-plugin/scripts/copilot-stop-save.py` around lines
314 - 321, The Windows branch currently calls msvcrt.locking(lock_fh.fileno(),
msvcrt.LK_LOCK, 1) on an empty file which leads to ambiguous locking; change the
Windows path (around lock_path, lock_fh, session_id, and the fcntl/ mscrt
branch) to first write a sentinel byte (e.g., b"\0") to the opened file, flush()
and seek(0) so the file actually contains that byte, then call
msvcrt.locking(..., msvcrt.LK_LOCK, 1) to lock that byte; keep the existing
behavior for fcntl, and rely on closing lock_fh to release the lock as before.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@nowledge-mem-copilot-cli-plugin/CHANGELOG.md`:
- Line 8: The release heading "## [0.1.0] - 2026-07-14" uses a future date;
update that heading in CHANGELOG.md (the "## [0.1.0] - 2026-07-14" line) to
either the actual release date (e.g., "2026-04-16") or replace the date with
"Unreleased" until the real release date is known, ensuring the header stays
exactly "## [0.1.0] - <date or Unreleased>".

In `@nowledge-mem-copilot-cli-plugin/RELEASING.md`:
- Around line 5-7: Reorder the three release steps so the `integrations.json`
update happens before the `.claude-plugin/plugin.json` update: first bump the
`copilot-cli` version in integrations.json (the canonical source), then update
the `version` field in `.claude-plugin/plugin.json`, and finally add the new
section to `CHANGELOG.md`; ensure the RELEASING.md steps reflect that exact
sequence and phrasing.

In `@nowledge-mem-copilot-cli-plugin/scripts/copilot-stop-save.py`:
- Around line 240-254: The load_state function (and any code that builds
lock_path or other filesystem paths from session_id) currently embeds raw
session_id into STATE_DIR / f"{session_id}.json" which allows path traversal;
add a sanitization/normalization step (e.g., a sanitize_session_id(session_id)
helper called from main() and before any use in load_state or when creating
lock_path) that either (a) validates allowed characters and rejects or
canonicalizes input (strip path separators, dots, and directory components), or
(b) replaces the session_id with a safe deterministic token (e.g., hex of
sha256(session_id) or a UUID derived from it) and use that token for filenames;
update load_state, any lock_path construction, and callers in main() to use the
sanitized token instead of the raw session_id.
- Around line 125-134: The redact function currently discards group 1 prefixes
by replacing the entire match when pattern.groups == 1; update the logic in
redact (which loops over SECRET_PATTERNS and checks pattern.groups) so that for
pattern.groups == 1 you preserve the first capture and append “[REDACTED]”
(i.e., perform the same substitution used for 2+ groups such as using the
"\1[REDACTED]" replacement), keep 0-group behavior unchanged, and ensure
SECRET_PATTERNS and pattern.groups are referenced as-is to locate the change.

In `@nowledge-mem-copilot-cli-plugin/skills/read-working-memory/SKILL.md`:
- Around line 34-36: Replace the plain-shell example command "nmem wm read" with
the JSON-mode command "nmem --json wm read" in SKILL.md (update the code block
that contains the command), and scan the same MD file for any other occurrences
of "nmem wm read" to change them to the "--json" variant so recall examples
follow the structured parsing convention.

In `@nowledge-mem-copilot-cli-plugin/skills/save-thread/SKILL.md`:
- Around line 33-37: Update the fenced code block that contains the "✓ Thread
saved" message so it declares a language identifier (e.g., change the opening
``` to ```text) to satisfy markdown linting; locate the fenced block that wraps
"✓ Thread saved / Summary: {summary} / Thread ID: {thread_id from nmem output}"
in SKILL.md and add the language token immediately after the opening backticks.

---

Nitpick comments:
In `@nowledge-mem-copilot-cli-plugin/commands/save.md`:
- Around line 9-17: Reorder the Usage section to lead with the slash command
`/save` (showing the exact slash-command usage first) and then present the `nmem
t create "..." --title "..." -s copilot-cli` example as the underlying execution
detail; update the text to state that `/save` triggers an immediate save and
that `nmem t create` is the equivalent lower-level invocation, and ensure the
slash-command conforms to the established style used elsewhere (`/save`,
`/search <query>`, `/sum`, `/status`) so readers see the slash command first and
the `nmem t create` form as supplemental.

In `@nowledge-mem-copilot-cli-plugin/commands/search.md`:
- Around line 6-14: The documentation should lead with the slash command
example; update the Search Memory section to present "/search <query>" as the
primary command example (e.g., show a code block with "/search <query>") and
then optionally include the underlying CLI invocation "nmem --json m search
\"$ARGUMENTS\"" afterwards; ensure you keep the existing heading "Search Memory"
and the explanatory sentence, and follow the guideline to use slash commands
(`/save`, `/search <query>`, `/sum`, `/status`) as the primary examples.

In `@nowledge-mem-copilot-cli-plugin/commands/status.md`:
- Around line 9-13: Update the example in status.md to present "/status" as the
primary invocation and move "nmem status" into a secondary
implementation/troubleshooting note; specifically replace the top-level code
block showing "nmem status" with "/status" and add a small following paragraph
or code block that notes the equivalent CLI command "nmem status" for debugging
or local runs so both usages appear but "/status" is shown as the canonical
user-facing command.

In `@nowledge-mem-copilot-cli-plugin/commands/sum.md`:
- Around line 7-30: The page should present the slash command /sum as the
primary invocation and move the raw nmem CLI example (nmem m add ...) to a
secondary "Implementation/CLI reference" section; update the introduction and
the top-level example to show "/sum" (and related slash commands /save, /search
<query>, /status) as the recommended workflow, then retain the existing nmem CLI
snippet only as an optional reference and label it accordingly; modify headings
and order in sum.md and adjust any examples or usage steps that currently teach
direct nmem usage first so they now demonstrate slash-command usage first.

In `@nowledge-mem-copilot-cli-plugin/README.md`:
- Around line 32-37: The WSL shim in the nmem script builds q with q="$q \"$a\""
which fails on embedded quotes/backslashes; update the argument-quoting loop in
the nmem shim to use a robust shell-escaping approach (e.g., build q with printf
'%q' for each "$a" or otherwise use an array and proper quoting) so arguments
containing spaces/special chars are escaped correctly; look for the q variable
and the for a in "$@" loop in the nmem shim and replace the naive concatenation
with printf '%q' "$a" (or an equivalent safe-escaping strategy) before invoking
cmd.exe.

In `@nowledge-mem-copilot-cli-plugin/scripts/copilot-stop-save.py`:
- Around line 314-321: The Windows branch currently calls
msvcrt.locking(lock_fh.fileno(), msvcrt.LK_LOCK, 1) on an empty file which leads
to ambiguous locking; change the Windows path (around lock_path, lock_fh,
session_id, and the fcntl/ mscrt branch) to first write a sentinel byte (e.g.,
b"\0") to the opened file, flush() and seek(0) so the file actually contains
that byte, then call msvcrt.locking(..., msvcrt.LK_LOCK, 1) to lock that byte;
keep the existing behavior for fcntl, and rely on closing lock_fh to release the
lock as before.

In `@nowledge-mem-copilot-cli-plugin/skills/save-thread/SKILL.md`:
- Around line 10-12: Update the explicit trigger examples in SKILL.md's "Only
when user explicitly says:" list to include the slash command `/save` alongside
the existing phrases ("Save this session", "Checkpoint this", "Record
conversation") so the skill supports command-driven usage; edit the trigger list
in SKILL.md to add `/save` and ensure consistency with other command examples
(`/search <query>`, `/sum`, `/status`) mentioned in the guidelines.

In `@nowledge-mem-copilot-cli-plugin/tests/test_copilot_plugin.py`:
- Around line 324-345: The tests mutate the module-level
copilot_stop_save.STATE_DIR directly which can leak between tests; update both
test_load_state_new_session and test_save_and_load_state to patch STATE_DIR only
for the test scope (use unittest.mock.patch or pytest monkeypatch to set
copilot_stop_save.STATE_DIR = Path(tmpdir) inside the context) and avoid
assigning unused variables by renaming path and path2 to _path/_path2 or not
capturing them when calling load_state; keep calls to load_state and save_state
unchanged.
- Around line 383-392: The test test_load_events_from_jsonl leaves the temp file
behind if an assertion fails; wrap the body that writes, calls
copilot_stop_save.load_events and asserts in a try/finally so os.unlink(f.name)
is always called (or use a context manager that ensures deletion), locating the
tempfile usage in test_load_events_from_jsonl and the call to
copilot_stop_save.load_events to add the try/finally cleanup around those
operations.
- Around line 151-174: The tests (test_redacts_github_token,
test_redacts_github_pat, test_redacts_openai_key, test_redacts_bearer_token) use
weak OR assertions that allow partial secrets to remain; update each to assert
the secret prefix is absent AND that "[REDACTED]" appears (e.g. replace `assert
"ghp_" not in result or "[REDACTED]" in result` with `assert "ghp_" not in
result and "[REDACTED]" in result`), and do the same for "github_pat_", "sk-"
and the bearer case when calling copilot_stop_save.redact to ensure full
redaction while keeping test_preserves_normal_text unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: afb203e7-f806-40ad-87b3-00520c2d7682

📥 Commits

Reviewing files that changed from the base of the PR and between 0052bf1 and eee5555.

⛔ Files ignored due to path filters (1)
  • nowledge-mem-copilot-cli-plugin/icon.png is excluded by !**/*.png
📒 Files selected for processing (19)
  • integrations.json
  • nowledge-mem-copilot-cli-plugin/.claude-plugin/plugin.json
  • nowledge-mem-copilot-cli-plugin/AGENTS.md
  • nowledge-mem-copilot-cli-plugin/CHANGELOG.md
  • nowledge-mem-copilot-cli-plugin/README.md
  • nowledge-mem-copilot-cli-plugin/RELEASING.md
  • nowledge-mem-copilot-cli-plugin/commands/save.md
  • nowledge-mem-copilot-cli-plugin/commands/search.md
  • nowledge-mem-copilot-cli-plugin/commands/status.md
  • nowledge-mem-copilot-cli-plugin/commands/sum.md
  • nowledge-mem-copilot-cli-plugin/hooks/hooks.json
  • nowledge-mem-copilot-cli-plugin/scripts/copilot-stop-save.py
  • nowledge-mem-copilot-cli-plugin/scripts/copilot-stop-save.sh
  • nowledge-mem-copilot-cli-plugin/scripts/install-hooks.sh
  • nowledge-mem-copilot-cli-plugin/skills/distill-memory/SKILL.md
  • nowledge-mem-copilot-cli-plugin/skills/read-working-memory/SKILL.md
  • nowledge-mem-copilot-cli-plugin/skills/save-thread/SKILL.md
  • nowledge-mem-copilot-cli-plugin/skills/search-memory/SKILL.md
  • nowledge-mem-copilot-cli-plugin/tests/test_copilot_plugin.py

Comment thread nowledge-mem-copilot-cli-plugin/CHANGELOG.md Outdated
Comment thread nowledge-mem-copilot-cli-plugin/RELEASING.md Outdated
Comment on lines +125 to +134
def redact(text: str) -> str:
redacted = text or ""
for pattern in SECRET_PATTERNS:
if pattern.groups == 0:
redacted = pattern.sub("[REDACTED]", redacted)
elif pattern.groups == 1:
redacted = pattern.sub("[REDACTED]", redacted)
else:
redacted = pattern.sub(r"\1[REDACTED]", redacted)
return redacted
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Redaction logic discards intended prefixes for single-group patterns.

When pattern.groups == 1, the current code replaces the entire match with [REDACTED], losing the prefix captured in group 1. Patterns like the Bearer token pattern (line 56) capture "Bearer " in group 1 specifically to preserve it. The logic should be:

  • 0 groups → replace whole match
  • 1 group → preserve group 1 as prefix
  • 2+ groups → already handled with \1[REDACTED]
🐛 Proposed fix
 def redact(text: str) -> str:
     redacted = text or ""
     for pattern in SECRET_PATTERNS:
         if pattern.groups == 0:
             redacted = pattern.sub("[REDACTED]", redacted)
         elif pattern.groups == 1:
-            redacted = pattern.sub("[REDACTED]", redacted)
+            redacted = pattern.sub(r"\1[REDACTED]", redacted)
         else:
             redacted = pattern.sub(r"\1[REDACTED]", redacted)
     return redacted

Or simplify since groups >= 1 now have the same behavior:

 def redact(text: str) -> str:
     redacted = text or ""
     for pattern in SECRET_PATTERNS:
         if pattern.groups == 0:
             redacted = pattern.sub("[REDACTED]", redacted)
         else:
             redacted = pattern.sub(r"\1[REDACTED]", redacted)
     return redacted
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def redact(text: str) -> str:
redacted = text or ""
for pattern in SECRET_PATTERNS:
if pattern.groups == 0:
redacted = pattern.sub("[REDACTED]", redacted)
elif pattern.groups == 1:
redacted = pattern.sub("[REDACTED]", redacted)
else:
redacted = pattern.sub(r"\1[REDACTED]", redacted)
return redacted
def redact(text: str) -> str:
redacted = text or ""
for pattern in SECRET_PATTERNS:
if pattern.groups == 0:
redacted = pattern.sub("[REDACTED]", redacted)
elif pattern.groups == 1:
redacted = pattern.sub(r"\1[REDACTED]", redacted)
else:
redacted = pattern.sub(r"\1[REDACTED]", redacted)
return redacted
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@nowledge-mem-copilot-cli-plugin/scripts/copilot-stop-save.py` around lines
125 - 134, The redact function currently discards group 1 prefixes by replacing
the entire match when pattern.groups == 1; update the logic in redact (which
loops over SECRET_PATTERNS and checks pattern.groups) so that for pattern.groups
== 1 you preserve the first capture and append “[REDACTED]” (i.e., perform the
same substitution used for 2+ groups such as using the "\1[REDACTED]"
replacement), keep 0-group behavior unchanged, and ensure SECRET_PATTERNS and
pattern.groups are referenced as-is to locate the change.

Comment thread nowledge-mem-copilot-cli-plugin/scripts/copilot-stop-save.py
Comment thread nowledge-mem-copilot-cli-plugin/skills/read-working-memory/SKILL.md
Comment thread nowledge-mem-copilot-cli-plugin/skills/save-thread/SKILL.md Outdated
Hanson Mei and others added 2 commits April 17, 2026 00:52
Clarify that Copilot CLI currently ships bundled command docs without exposing them as interactive slash commands, and update the install path examples accordingly.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Keep the Copilot plugin user-facing surface skill-only so command docs no longer create overlapping save/search/status entries alongside skills.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (2)
nowledge-mem-copilot-cli-plugin/CHANGELOG.md (1)

8-8: ⚠️ Potential issue | 🟡 Minor

Update release date or mark as Unreleased.

The release date 2026-07-14 is approximately three months in the future (today is April 17, 2026). Use the actual release date or change to Unreleased until the package ships.

📝 Suggested fix
-## [0.1.0] - 2026-07-14
+## [0.1.0] - Unreleased

Or use today's date if releasing now:

-## [0.1.0] - 2026-07-14
+## [0.1.0] - 2026-04-17
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@nowledge-mem-copilot-cli-plugin/CHANGELOG.md` at line 8, The changelog entry
header "## [0.1.0] - 2026-07-14" uses a future release date; update the date to
the actual release date (today's date) or replace the date with "Unreleased" for
the 0.1.0 entry; edit the heading text in CHANGELOG.md (the "## [0.1.0] -
2026-07-14" line) to either "## [0.1.0] - Unreleased" or "## [0.1.0] -
YYYY-MM-DD" with the correct release date.
nowledge-mem-copilot-cli-plugin/skills/save-thread/SKILL.md (1)

33-37: ⚠️ Potential issue | 🟡 Minor

Add language identifier to fenced code block.

The fenced block should specify a language (e.g., text) to satisfy markdown linting.

📝 Suggested fix
-```
+```text
 ✓ Thread saved
 Summary: {summary}
 Thread ID: {thread_id from nmem output}
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

Verify each finding against the current code and only fix it if needed.

In @nowledge-mem-copilot-cli-plugin/skills/save-thread/SKILL.md around lines 33

  • 37, Update the fenced code block in SKILL.md (the block that currently shows
    "✓ Thread saved\nSummary: {summary}\nThread ID: {thread_id from nmem output}")
    to include a language identifier (for example "text") after the opening triple
    backticks so Markdown linting passes; ensure the opening fence is changed from
    totext and do not alter the block contents.

</details>

</blockquote></details>

</blockquote></details>

<details>
<summary>🤖 Prompt for all review comments with AI agents</summary>

Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In @nowledge-mem-copilot-cli-plugin/CHANGELOG.md:

  • Line 8: The changelog entry header "## [0.1.0] - 2026-07-14" uses a future
    release date; update the date to the actual release date (today's date) or
    replace the date with "Unreleased" for the 0.1.0 entry; edit the heading text in
    CHANGELOG.md (the "## [0.1.0] - 2026-07-14" line) to either "## [0.1.0] -
    Unreleased" or "## [0.1.0] - YYYY-MM-DD" with the correct release date.

In @nowledge-mem-copilot-cli-plugin/skills/save-thread/SKILL.md:

  • Around line 33-37: Update the fenced code block in SKILL.md (the block that
    currently shows "✓ Thread saved\nSummary: {summary}\nThread ID: {thread_id from
    nmem output}") to include a language identifier (for example "text") after the
    opening triple backticks so Markdown linting passes; ensure the opening fence is
    changed from totext and do not alter the block contents.

</details>

---

<details>
<summary>ℹ️ Review info</summary>

<details>
<summary>⚙️ Run configuration</summary>

**Configuration used**: defaults

**Review profile**: CHILL

**Plan**: Pro

**Run ID**: `53f25813-660f-43f6-996f-7b40a66f9d6c`

</details>

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between eee5555b54b28499bcf40d2387d43479d8515ea2 and 02c3b71ee1697a8a8b7b2ae58cf1575348f2d593.

</details>

<details>
<summary>📒 Files selected for processing (8)</summary>

* `integrations.json`
* `nowledge-mem-copilot-cli-plugin/AGENTS.md`
* `nowledge-mem-copilot-cli-plugin/CHANGELOG.md`
* `nowledge-mem-copilot-cli-plugin/README.md`
* `nowledge-mem-copilot-cli-plugin/RELEASING.md`
* `nowledge-mem-copilot-cli-plugin/skills/distill-memory/SKILL.md`
* `nowledge-mem-copilot-cli-plugin/skills/save-thread/SKILL.md`
* `nowledge-mem-copilot-cli-plugin/skills/search-memory/SKILL.md`

</details>

<details>
<summary>✅ Files skipped from review due to trivial changes (3)</summary>

* nowledge-mem-copilot-cli-plugin/skills/distill-memory/SKILL.md
* nowledge-mem-copilot-cli-plugin/RELEASING.md
* nowledge-mem-copilot-cli-plugin/skills/search-memory/SKILL.md

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (1)</summary>

* integrations.json

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->

@wey-gu
Copy link
Copy Markdown
Member

wey-gu commented Apr 21, 2026

bugbot run

@wey-gu
Copy link
Copy Markdown
Member

wey-gu commented Apr 21, 2026

@codex review, please

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 02c3b71ee1

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +299 to +301
session_id = hook_input.get("sessionId")
transcript_path = hook_input.get("transcriptPath")
stop_reason = hook_input.get("stopReason")
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Accept snake_case Stop payload fields

The Stop hook is configured with the PascalCase event name ("Stop" in nowledge-mem-copilot-cli-plugin/hooks/hooks.json), but this parser only reads camelCase keys (sessionId, transcriptPath, stopReason). In Copilot CLI, PascalCase hook events provide VS Code-compatible snake_case fields (session_id, transcript_path, stop_reason) per the official hook payload docs, so this code will treat valid Stop payloads as missing required fields and return early without saving any thread data. See GitHub’s payload format reference: https://docs.github.com/en/copilot/reference/copilot-cli-reference/cli-command-reference#hook-event-input-payloads.

Useful? React with 👍 / 👎.

@wey-gu
Copy link
Copy Markdown
Member

wey-gu commented Apr 21, 2026

@codex review

@wey-gu
Copy link
Copy Markdown
Member

wey-gu commented Apr 21, 2026

bugbot run

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

♻️ Duplicate comments (1)
nowledge-mem-copilot-cli-plugin/scripts/copilot-stop-save.py (1)

125-133: ⚠️ Potential issue | 🟡 Minor

Preserve prefixes for captured-secret patterns.

Line 130 still drops the captured prefix for one-group patterns like Bearer and assignment prefixes, reducing captured context. Treat all grouped patterns as prefix-preserving.

🛡️ Proposed fix
 def redact(text: str) -> str:
     redacted = text or ""
     for pattern in SECRET_PATTERNS:
         if pattern.groups == 0:
             redacted = pattern.sub("[REDACTED]", redacted)
-        elif pattern.groups == 1:
-            redacted = pattern.sub("[REDACTED]", redacted)
         else:
             redacted = pattern.sub(r"\1[REDACTED]", redacted)
     return redacted
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@nowledge-mem-copilot-cli-plugin/scripts/copilot-stop-save.py` around lines
125 - 133, The redact function currently drops captured prefixes when
pattern.groups == 1 (e.g., "Bearer ") by replacing the whole match with
"[REDACTED]"; update the logic in redact to treat any pattern with one or more
capture groups as prefix-preserving by using the first-group backreference
(e.g., pattern.sub(r"\1[REDACTED]", redacted)) for pattern.groups >= 1 so
SECRET_PATTERNS that capture prefixes keep the prefix while the secret portion
is redacted; change the conditional handling in redact (the branches referencing
pattern.groups) accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@nowledge-mem-copilot-cli-plugin/README.md`:
- Around line 106-112: The README shows NMEM_SPACE assigned but not applied to
the Copilot launch; update the snippet so the environment variable is actually
exported or used when invoking the Copilot binary (e.g., either export
NMEM_SPACE="Research Agent" before running or run the command with the variable
in-line) so sessions are launched into the specified NMEM_SPACE; reference the
NMEM_SPACE variable and the copilot command in the updated example.

In `@nowledge-mem-copilot-cli-plugin/RELEASING.md`:
- Around line 13-15: Update the three release validation commands in
RELEASING.md so they are cwd-safe by referencing the plugin directory explicitly
(use the plugin path prefix with integrations.json, hooks/hooks.json and
tests/), e.g. run the json.tool check against
nowledge-mem-copilot-cli-plugin/integrations.json and
nowledge-mem-copilot-cli-plugin/hooks/hooks.json and run pytest against
nowledge-mem-copilot-cli-plugin/tests/ so the checks succeed regardless of the
current working directory.

In `@nowledge-mem-copilot-cli-plugin/scripts/copilot-stop-save.py`:
- Around line 230-237: The run_json function currently calls subprocess.run
without a timeout which can hang the worker; modify run_json (the subprocess.run
invocation) to pass a reasonable timeout (e.g., a few seconds) and catch
subprocess.TimeoutExpired to raise a RuntimeError with a clear message including
the command and that it timed out; ensure normal error handling still applies
(use proc.stderr/proc.stdout if available) and that an expired timeout does not
leave resources open.
- Around line 579-599: The triage/distill block (using should_try_distill,
run_json and build_nmem_command) can raise and prevent saving state after the
thread append; wrap the calls to run_json for the "t triage" and "t distill"
steps in try/except so any exception is caught, logged, and does not propagate —
still set distill_attempted only when triage returns should_distill or when
distill actually ran successfully, and always allow execution to continue to the
state update (state["last_distill_ts"]) and subsequent save; reference the
triage variable, distill_attempted flag, run_json, build_nmem_command and
state["last_distill_ts"] when adding the error handling.
- Around line 183-240: Add the future annotations import at the top of the
script to enable PEP 563-style postponed evaluation so union types like those
used in find_index(event_id: str | None) and load_state(session_id: str) ->
tuple[Path, dict] (and other annotations using |) work on Python 3.9 and
earlier; specifically insert from __future__ import annotations as the first
non-shebang/import line in
nowledge-mem-copilot-cli-plugin/scripts/copilot-stop-save.py so the existing
type hints require no other syntax changes.
- Around line 304-348: The hook compares hook_timestamp (raw JSON) to
event_timestamp_ms() (int ms) causing type errors when hook_timestamp is an ISO
string; normalize hook_timestamp to an integer millisecond timestamp before the
comparison in the block that locates current_turn_end. Add a small helper (e.g.,
normalize_hook_timestamp) used where hook_timestamp is read (variable
hook_timestamp) to convert ISO strings or numeric seconds/ms to an int ms value
(or None if absent/invalid), then use that normalized value in the comparison
with event_timestamp_ms(event) inside the current_turn_end selection; keep
existing functions load_state, load_events and state_token unchanged.

---

Duplicate comments:
In `@nowledge-mem-copilot-cli-plugin/scripts/copilot-stop-save.py`:
- Around line 125-133: The redact function currently drops captured prefixes
when pattern.groups == 1 (e.g., "Bearer ") by replacing the whole match with
"[REDACTED]"; update the logic in redact to treat any pattern with one or more
capture groups as prefix-preserving by using the first-group backreference
(e.g., pattern.sub(r"\1[REDACTED]", redacted)) for pattern.groups >= 1 so
SECRET_PATTERNS that capture prefixes keep the prefix while the secret portion
is redacted; change the conditional handling in redact (the branches referencing
pattern.groups) accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7ad46be1-f6a5-4a08-aa54-3ad9e3b6bcec

📥 Commits

Reviewing files that changed from the base of the PR and between 02c3b71 and d097ce3.

📒 Files selected for processing (8)
  • nowledge-mem-copilot-cli-plugin/CHANGELOG.md
  • nowledge-mem-copilot-cli-plugin/README.md
  • nowledge-mem-copilot-cli-plugin/RELEASING.md
  • nowledge-mem-copilot-cli-plugin/hooks/hooks.json
  • nowledge-mem-copilot-cli-plugin/scripts/copilot-stop-save.py
  • nowledge-mem-copilot-cli-plugin/skills/read-working-memory/SKILL.md
  • nowledge-mem-copilot-cli-plugin/skills/save-thread/SKILL.md
  • nowledge-mem-copilot-cli-plugin/tests/test_copilot_plugin.py
✅ Files skipped from review due to trivial changes (2)
  • nowledge-mem-copilot-cli-plugin/skills/save-thread/SKILL.md
  • nowledge-mem-copilot-cli-plugin/skills/read-working-memory/SKILL.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • nowledge-mem-copilot-cli-plugin/hooks/hooks.json

Comment on lines +106 to +112
Spaces are optional. If one Copilot CLI process naturally belongs to one project or agent lane, launch Copilot CLI with:

```bash
NMEM_SPACE="Research Agent"
```

The session-start Working Memory read, per-turn guidance, skills, and background capture will then stay in that lane automatically.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Show NMEM_SPACE attached to the Copilot launch.

The snippet currently sets an environment variable but does not launch Copilot CLI or export it, so copying it as-is will not put a session into that space.

🛠️ Proposed docs fix
-NMEM_SPACE="Research Agent"
+NMEM_SPACE="Research Agent" copilot

Or, for a shell session:

export NMEM_SPACE="Research Agent"
copilot
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@nowledge-mem-copilot-cli-plugin/README.md` around lines 106 - 112, The README
shows NMEM_SPACE assigned but not applied to the Copilot launch; update the
snippet so the environment variable is actually exported or used when invoking
the Copilot binary (e.g., either export NMEM_SPACE="Research Agent" before
running or run the command with the variable in-line) so sessions are launched
into the specified NMEM_SPACE; reference the NMEM_SPACE variable and the copilot
command in the updated example.

Comment on lines +13 to +15
1. Verify `integrations.json` is valid JSON: `python3 -m json.tool integrations.json > /dev/null`
2. Verify `hooks/hooks.json` is valid JSON: `python3 -m json.tool hooks/hooks.json > /dev/null`
3. Run fixture tests: `uv run --with pytest pytest tests/ -v`
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Make the release validation commands cwd-safe.

Line 13 is repo-root relative, while Lines 14-15 are plugin-directory relative. As written, maintainers will fail either integrations.json or hooks/hooks.json/tests/ depending on where they run the checklist.

🛠️ Proposed docs fix
-1. Verify `integrations.json` is valid JSON: `python3 -m json.tool integrations.json > /dev/null`
-2. Verify `hooks/hooks.json` is valid JSON: `python3 -m json.tool hooks/hooks.json > /dev/null`
-3. Run fixture tests: `uv run --with pytest pytest tests/ -v`
+1. From the repository root, verify `integrations.json` is valid JSON: `python3 -m json.tool integrations.json > /dev/null`
+2. Verify `hooks/hooks.json` is valid JSON: `python3 -m json.tool nowledge-mem-copilot-cli-plugin/hooks/hooks.json > /dev/null`
+3. Run fixture tests: `uv run --with pytest pytest nowledge-mem-copilot-cli-plugin/tests/ -v`
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@nowledge-mem-copilot-cli-plugin/RELEASING.md` around lines 13 - 15, Update
the three release validation commands in RELEASING.md so they are cwd-safe by
referencing the plugin directory explicitly (use the plugin path prefix with
integrations.json, hooks/hooks.json and tests/), e.g. run the json.tool check
against nowledge-mem-copilot-cli-plugin/integrations.json and
nowledge-mem-copilot-cli-plugin/hooks/hooks.json and run pytest against
nowledge-mem-copilot-cli-plugin/tests/ so the checks succeed regardless of the
current working directory.

Comment on lines +183 to +240
def build_nmem_command(nmem_bin: str, *args: str) -> list[str]:
if nmem_bin.lower().endswith(".cmd"):
return [
"cmd.exe",
"/s",
"/c",
subprocess.list2cmdline([nmem_bin, *args]),
]
return [nmem_bin, *args]


def load_events(transcript_path: str) -> list[dict]:
with open(transcript_path, encoding="utf-8") as fh:
return [json.loads(line) for line in fh]


def find_index(events: list[dict], event_id: str | None) -> int | None:
if not event_id:
return None
for idx, event in enumerate(events):
if event.get("id") == event_id:
return idx
return None


def collect_messages(events: list[dict]) -> list[dict]:
messages: list[dict] = []
for event in events:
etype = event.get("type")
if etype == "user.message":
text = clean_user_text(event)
role = "user"
elif etype == "assistant.message":
text = clean_assistant_text(event)
role = "assistant"
else:
continue

if not text:
continue
if messages and messages[-1]["role"] == role:
messages[-1]["content"] += "\n\n" + text
else:
messages.append({"role": role, "content": text})
return messages


def run_json(args: list[str]) -> dict:
proc = subprocess.run(args, capture_output=True, text=True)
if proc.returncode != 0:
raise RuntimeError(
proc.stderr.strip() or proc.stdout.strip() or "command failed"
)
output = proc.stdout.strip()
return json.loads(output) if output else {}


def load_state(session_id: str) -> tuple[Path, dict]:
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify whether the repo declares/enforces a Python 3.10+ requirement for this hook.
rg -n "Python 3\\.10|Python >=3\\.10|requires-python|python_requires|from __future__ import annotations|str \\| None|int \\| None" nowledge-mem-copilot-cli-plugin README.md pyproject.toml setup.cfg setup.py 2>/dev/null || true

Repository: nowledge-co/community

Length of output: 317


🏁 Script executed:

#!/bin/bash
# Get the full copilot-stop-save.py file to examine the syntax usage
wc -l nowledge-mem-copilot-cli-plugin/scripts/copilot-stop-save.py

Repository: nowledge-co/community

Length of output: 129


🏁 Script executed:

#!/bin/bash
# Search for all instances of union type syntax and future imports in the file
rg -n "from __future__|str \| |int \| |list \| |dict \| " nowledge-mem-copilot-cli-plugin/scripts/copilot-stop-save.py

Repository: nowledge-co/community

Length of output: 195


🏁 Script executed:

#!/bin/bash
# Check the shebang and opening lines to understand the Python environment requirement
head -20 nowledge-mem-copilot-cli-plugin/scripts/copilot-stop-save.py

Repository: nowledge-co/community

Length of output: 620


🏁 Script executed:

#!/bin/bash
# Check if there's any Python version specification in the installer or README
cat nowledge-mem-copilot-cli-plugin/README.md 2>/dev/null | head -50

Repository: nowledge-co/community

Length of output: 1391


🏁 Script executed:

#!/bin/bash
# Check setup.py or pyproject.toml for Python version constraints
find nowledge-mem-copilot-cli-plugin -name "setup.py" -o -name "pyproject.toml" -o -name "setup.cfg" | xargs cat 2>/dev/null | grep -i "python" | head -20

Repository: nowledge-co/community

Length of output: 47


Add from __future__ import annotations to support Python 3.9 and earlier.

The hook is invoked as plain python3 without a version constraint, but the script uses union type syntax (str | None, int | None) that requires Python 3.10+ or postponed annotation evaluation. The repository has no documented Python 3.10+ requirement. Add the future import to make the script compatible with Python 3.9 and earlier:

Proposed fix
 #!/usr/bin/env python3
 """Copilot CLI session capture for Nowledge Mem.
@@ -9,6 +9,8 @@ thread ID: ``copilot-{session_id}`` (stable per-session, enables incremental
 append). State management with file locking for concurrent session safety.
 """
 
+from __future__ import annotations
+
 import hashlib
 import json
 import os

Affects lines 199 and 273.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def build_nmem_command(nmem_bin: str, *args: str) -> list[str]:
if nmem_bin.lower().endswith(".cmd"):
return [
"cmd.exe",
"/s",
"/c",
subprocess.list2cmdline([nmem_bin, *args]),
]
return [nmem_bin, *args]
def load_events(transcript_path: str) -> list[dict]:
with open(transcript_path, encoding="utf-8") as fh:
return [json.loads(line) for line in fh]
def find_index(events: list[dict], event_id: str | None) -> int | None:
if not event_id:
return None
for idx, event in enumerate(events):
if event.get("id") == event_id:
return idx
return None
def collect_messages(events: list[dict]) -> list[dict]:
messages: list[dict] = []
for event in events:
etype = event.get("type")
if etype == "user.message":
text = clean_user_text(event)
role = "user"
elif etype == "assistant.message":
text = clean_assistant_text(event)
role = "assistant"
else:
continue
if not text:
continue
if messages and messages[-1]["role"] == role:
messages[-1]["content"] += "\n\n" + text
else:
messages.append({"role": role, "content": text})
return messages
def run_json(args: list[str]) -> dict:
proc = subprocess.run(args, capture_output=True, text=True)
if proc.returncode != 0:
raise RuntimeError(
proc.stderr.strip() or proc.stdout.strip() or "command failed"
)
output = proc.stdout.strip()
return json.loads(output) if output else {}
def load_state(session_id: str) -> tuple[Path, dict]:
#!/usr/bin/env python3
"""Copilot CLI session capture for Nowledge Mem.
This module is invoked by the Copilot CLI stop hook to capture and persist
session state (messages, metadata, transcript) for incremental thread updates via
Nowledge Mem. Session state is persisted to `.copilot-cli/session/{session_id}.json`;
thread ID: ``copilot-{session_id}`` (stable per-session, enables incremental
append). State management with file locking for concurrent session safety.
"""
from __future__ import annotations
import hashlib
import json
import os
🧰 Tools
🪛 Ruff (0.15.10)

[error] 231-231: subprocess call: check for execution of untrusted input

(S603)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@nowledge-mem-copilot-cli-plugin/scripts/copilot-stop-save.py` around lines
183 - 240, Add the future annotations import at the top of the script to enable
PEP 563-style postponed evaluation so union types like those used in
find_index(event_id: str | None) and load_state(session_id: str) -> tuple[Path,
dict] (and other annotations using |) work on Python 3.9 and earlier;
specifically insert from __future__ import annotations as the first
non-shebang/import line in
nowledge-mem-copilot-cli-plugin/scripts/copilot-stop-save.py so the existing
type hints require no other syntax changes.

Comment on lines +230 to +237
def run_json(args: list[str]) -> dict:
proc = subprocess.run(args, capture_output=True, text=True)
if proc.returncode != 0:
raise RuntimeError(
proc.stderr.strip() or proc.stdout.strip() or "command failed"
)
output = proc.stdout.strip()
return json.loads(output) if output else {}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Put a timeout on nmem subprocess calls.

The Stop hook runs after each response; a hung nmem command can keep the worker and session lock alive indefinitely, blocking later captures for that session.

⏱️ Proposed fix
 DISTILL_COOLDOWN_SECS = 120
+NMEM_COMMAND_TIMEOUT_SECS = 30
@@
-def run_json(args: list[str]) -> dict:
-    proc = subprocess.run(args, capture_output=True, text=True)
+def run_json(args: list[str]) -> dict:
+    try:
+        proc = subprocess.run(
+            args,
+            capture_output=True,
+            text=True,
+            timeout=NMEM_COMMAND_TIMEOUT_SECS,
+        )
+    except subprocess.TimeoutExpired as exc:
+        raise RuntimeError(
+            f"nmem command timed out after {NMEM_COMMAND_TIMEOUT_SECS}s"
+        ) from exc
     if proc.returncode != 0:
         raise RuntimeError(
             proc.stderr.strip() or proc.stdout.strip() or "command failed"
         )
🧰 Tools
🪛 Ruff (0.15.10)

[error] 231-231: subprocess call: check for execution of untrusted input

(S603)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@nowledge-mem-copilot-cli-plugin/scripts/copilot-stop-save.py` around lines
230 - 237, The run_json function currently calls subprocess.run without a
timeout which can hang the worker; modify run_json (the subprocess.run
invocation) to pass a reasonable timeout (e.g., a few seconds) and catch
subprocess.TimeoutExpired to raise a RuntimeError with a clear message including
the command and that it timed out; ensure normal error handling still applies
(use proc.stderr/proc.stdout if available) and that an expired timeout does not
leave resources open.

Comment on lines +304 to +348
session_id = hook_input.get("sessionId")
transcript_path = hook_input.get("transcriptPath")
stop_reason = hook_input.get("stopReason")
hook_timestamp = hook_input.get("timestamp")

if not session_id or not transcript_path or stop_reason != "end_turn":
return 0
if not Path(transcript_path).exists():
return 0

nmem_bin = shutil.which("nmem") or shutil.which("nmem.cmd")
if not nmem_bin:
log({"session_id": session_id, "action": "skip", "reason": "nmem_missing"})
return 0

lock_path = STATE_DIR / f"{state_token(session_id)}.lock"
lock_path.parent.mkdir(parents=True, exist_ok=True)
with lock_path.open("w+", encoding="utf-8") as lock_fh:
if fcntl:
fcntl.flock(lock_fh, fcntl.LOCK_EX)
else:
import msvcrt
lock_fh.write("\0")
lock_fh.flush()
lock_fh.seek(0)
msvcrt.locking(lock_fh.fileno(), msvcrt.LK_LOCK, 1)

state_path, state = load_state(session_id)
events = load_events(transcript_path)

# --- Locate current turn end ---
turn_end_events = [
e for e in events if e.get("type") == "assistant.turn_end"
]
if not turn_end_events:
return 0
current_turn_end = next(
(
event
for event in reversed(turn_end_events)
if hook_timestamp is None
or (
event_timestamp_ms(event) is not None
and event_timestamp_ms(event) <= hook_timestamp
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -name "copilot-stop-save.py" -type f

Repository: nowledge-co/community

Length of output: 127


🏁 Script executed:

wc -l ./nowledge-mem-copilot-cli-plugin/scripts/copilot-stop-save.py

Repository: nowledge-co/community

Length of output: 131


🏁 Script executed:

sed -n '304,348p' ./nowledge-mem-copilot-cli-plugin/scripts/copilot-stop-save.py

Repository: nowledge-co/community

Length of output: 1671


🏁 Script executed:

grep -n "def event_timestamp_ms" ./nowledge-mem-copilot-cli-plugin/scripts/copilot-stop-save.py

Repository: nowledge-co/community

Length of output: 119


🏁 Script executed:

sed -n '273,290p' ./nowledge-mem-copilot-cli-plugin/scripts/copilot-stop-save.py

Repository: nowledge-co/community

Length of output: 644


🏁 Script executed:

grep -n "hook_input" ./nowledge-mem-copilot-cli-plugin/scripts/copilot-stop-save.py | head -20

Repository: nowledge-co/community

Length of output: 320


🏁 Script executed:

grep -n "def extract_input" ./nowledge-mem-copilot-cli-plugin/scripts/copilot-stop-save.py

Repository: nowledge-co/community

Length of output: 110


🏁 Script executed:

sed -n '106,130p' ./nowledge-mem-copilot-cli-plugin/scripts/copilot-stop-save.py

Repository: nowledge-co/community

Length of output: 861


🏁 Script executed:

grep -n "hook_timestamp" ./nowledge-mem-copilot-cli-plugin/scripts/copilot-stop-save.py

Repository: nowledge-co/community

Length of output: 235


🏁 Script executed:

sed -n '340,360p' ./nowledge-mem-copilot-cli-plugin/scripts/copilot-stop-save.py

Repository: nowledge-co/community

Length of output: 783


🏁 Script executed:

grep -n "event_timestamp_ms" ./nowledge-mem-copilot-cli-plugin/scripts/copilot-stop-save.py

Repository: nowledge-co/community

Length of output: 253


🏁 Script executed:

head -50 ./nowledge-mem-copilot-cli-plugin/scripts/copilot-stop-save.py

Repository: nowledge-co/community

Length of output: 1502


Normalize the hook timestamp before comparing it to event milliseconds.

event_timestamp_ms() returns an integer (milliseconds), but hook_timestamp at line 307 is extracted raw from JSON. If the Stop payload supplies an ISO timestamp string, the comparison at line 347 raises a type error and capture is skipped by the hook wrapper.

Extract the proposed helper function and apply normalization:

🐛 Proposed fix
 def event_timestamp_ms(event: dict) -> int | None:
-    raw = event.get("timestamp")
+    return timestamp_ms(event.get("timestamp"))
+
+
+def timestamp_ms(raw) -> int | None:
     if not raw:
         return None
+    if isinstance(raw, (int, float)):
+        value = int(raw)
+        return value if value > 10_000_000_000 else value * 1000
+    if not isinstance(raw, str):
+        return None
     return int(
         datetime.fromisoformat(raw.replace("Z", "+00:00")).timestamp() * 1000
     )
@@
-    hook_timestamp = hook_input.get("timestamp")
+    hook_timestamp = timestamp_ms(hook_input.get("timestamp"))
@@
                 if hook_timestamp is None
                 or (
                     event_timestamp_ms(event) is not None
                     and event_timestamp_ms(event) <= hook_timestamp
                 )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@nowledge-mem-copilot-cli-plugin/scripts/copilot-stop-save.py` around lines
304 - 348, The hook compares hook_timestamp (raw JSON) to event_timestamp_ms()
(int ms) causing type errors when hook_timestamp is an ISO string; normalize
hook_timestamp to an integer millisecond timestamp before the comparison in the
block that locates current_turn_end. Add a small helper (e.g.,
normalize_hook_timestamp) used where hook_timestamp is read (variable
hook_timestamp) to convert ISO strings or numeric seconds/ms to an int ms value
(or None if absent/invalid), then use that normalized value in the comparison
with event_timestamp_ms(event) inside the current_turn_end selection; keep
existing functions load_state, load_events and state_token unchanged.

Comment on lines +579 to +599
triage = None
distill_attempted = False
if should_try_distill:
triage = run_json(
build_nmem_command(
nmem_bin, "--json", "t", "triage", thread_id
)
)
if triage.get("should_distill"):
distill_attempted = True
run_json(
build_nmem_command(
nmem_bin,
"--json",
"t",
"distill",
thread_id,
"--triage",
)
)
state["last_distill_ts"] = now_ts
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Do not let auto-distill failures roll back capture state.

The thread import/append has already succeeded before Line 581. If t triage or t distill fails, the exception prevents Lines 601-604 from saving state, so the next Stop hook can append the same slice again.

🐛 Proposed fix
         triage = None
         distill_attempted = False
         if should_try_distill:
-            triage = run_json(
-                build_nmem_command(
-                    nmem_bin, "--json", "t", "triage", thread_id
+            try:
+                triage = run_json(
+                    build_nmem_command(
+                        nmem_bin, "--json", "t", "triage", thread_id
+                    )
                 )
-            )
-            if triage.get("should_distill"):
-                distill_attempted = True
-                run_json(
-                    build_nmem_command(
-                        nmem_bin,
-                        "--json",
-                        "t",
-                        "distill",
-                        thread_id,
-                        "--triage",
+                if triage.get("should_distill"):
+                    distill_attempted = True
+                    run_json(
+                        build_nmem_command(
+                            nmem_bin,
+                            "--json",
+                            "t",
+                            "distill",
+                            thread_id,
+                            "--triage",
+                        )
                     )
+                    state["last_distill_ts"] = now_ts
+            except Exception as exc:
+                log(
+                    {
+                        "session_id": session_id,
+                        "action": "distill_skip",
+                        "reason": str(exc),
+                    }
                 )
-                state["last_distill_ts"] = now_ts
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@nowledge-mem-copilot-cli-plugin/scripts/copilot-stop-save.py` around lines
579 - 599, The triage/distill block (using should_try_distill, run_json and
build_nmem_command) can raise and prevent saving state after the thread append;
wrap the calls to run_json for the "t triage" and "t distill" steps in
try/except so any exception is caught, logged, and does not propagate — still
set distill_attempted only when triage returns should_distill or when distill
actually ran successfully, and always allow execution to continue to the state
update (state["last_distill_ts"]) and subsequent save; reference the triage
variable, distill_attempted flag, run_json, build_nmem_command and
state["last_distill_ts"] when adding the error handling.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d097ce3023

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +304 to +306
session_id = hook_input.get("sessionId")
transcript_path = hook_input.get("transcriptPath")
stop_reason = hook_input.get("stopReason")
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Parse snake_case Stop payload fields from Copilot hooks

With this plugin configured to use PascalCase hook names (Stop, UserPromptSubmit, SessionStart), Copilot CLI can send VS Code-compatible payloads (session_id, transcript_path, stop_reason, ISO timestamp) rather than camelCase. The script only reads camelCase keys here, so session_id/transcript_path resolve to None and the early guard exits without saving any thread data. GitHub’s CLI reference documents both payload shapes for agentStop / Stop and notes snake_case for PascalCase events, so this causes silent loss of session capture in that mode.

Useful? React with 👍 / 👎.

@wey-gu
Copy link
Copy Markdown
Member

wey-gu commented Apr 21, 2026

bugbot run

@wey-gu wey-gu merged commit d40981f into nowledge-co:main Apr 21, 2026
1 check passed
@wey-gu
Copy link
Copy Markdown
Member

wey-gu commented Apr 21, 2026

@HamsteRider-m thanks a lot for your amazing work!!!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants