Skip to content

feat(memory): cross-session learned memory (file-based, git-backed, BYOM-aware)#112

Merged
cnjack merged 4 commits into
mainfrom
feat/agent-memory
Jul 4, 2026
Merged

feat(memory): cross-session learned memory (file-based, git-backed, BYOM-aware)#112
cnjack merged 4 commits into
mainfrom
feat/agent-memory

Conversation

@cnjack

@cnjack cnjack commented Jul 4, 2026

Copy link
Copy Markdown
Owner

What & why

jcode had only static memory (AGENTS.md) and within-session memory (compaction) — nothing that learns across sessions. This adds Project Memory: jcode distills durable facts (preferences, conventions, pitfalls, workflows) from past sessions and feeds them back into future ones. Modeled on Claude Code's auto-memory and Codex's memory pipeline, adapted for jcode's pure-Go, no-SQLite, BYOM (bring-your-own-model) reality.

Design (three layers)

Layer What Where
L0 AGENTS.md static, human-authored (unchanged) project + ~/.jcode
L1 online notes memory_note tool writes one durable fact to an inbox instantly; path-locked + secret-redacted ~/.jcode/memory/projects/<slug>/notes/
L2 distillation phase 1 extracts per-session facts (small model); phase 2 consolidates via a restricted subagent, git-diff driven, ADD/UPDATE/DELETE/NOOP protocol same scope, curated MEMORY.md + memory_summary.md
  • Read path: a size-capped memory summary is injected into the system prompt (TUI/ACP/web + plan mode). Full index/notes are grep-able on demand. Usage accounting feeds consolidation ranking.
  • No SQLite: state.json + flock; the memory root is a git repo for change detection, forgetting, and rollback.
  • BYOM cost discipline: daily token budget spanning both phases, cooldown, small-model default, kill switch, no runs during -p or remote sessions.
  • Storage is global (~/.jcode/memory/, scoped per project) — matches Claude Code/Codex; keeps personal learned data out of the repo. Shareable/committable project knowledge stays in AGENTS.md.

Surface

  • Config: config.Memory{...}, zero-config defaults (all documented).
  • CLI: jcode memory {path,status,sync,clear}. TUI: /memory, /memory sync, /memory clear.
  • New package internal/memory (+ pipeline subpkg); tool memory_note; usage middleware; injection in GetSystemPrompt/GetPlanSystemPrompt; wiring in interactive/acp/web.

Testing

  • Unit (internal/memory/..., all green): redaction (incl. JSON-quoted/URL/github_pat_), path-guard traversal + .git/ block, concurrent note writes, state flock, UTF-8-safe truncation, git no-op fast path, phase-1 selection/budget, expire+rank.
  • e2e (agent-eval, real model glm-5.1): extended the harness with multi-step runs, HOME fixtures/config, and home_* oracles; added a memory tier — 9/9 cases pass (explicit remember, cross-session recall, summary injection, redaction, prompt-injection resistance, write discipline, kill switch, phase-1 extract, phase-2 consolidate incl. no-op-on-rerun).
  • Human-perspective probes: model cites memory provenance, flags staleness, reads Chinese summaries without garbling, doesn't over-record.

Adversarial review

Reviewed across 5 dimensions (correctness/concurrency/security/cost/integration). All confirmed findings fixed, including:

  • Critical: git churn (state.json in the tree) permanently broke the zero-token no-op fast path → .gitignore in the scope root; phase-2 had no budget gate and failure didn't set cooldown → retry storm.
  • Major: broken usage-feedback loop (ranking on always-zero counters); WriteNote same-second concurrency race (O_EXCL fix); phase-1 worker missing panic recover; redaction gaps; remote task triggering the pipeline.
  • Minor: UTF-8 byte-truncation corrupting Chinese text (unified TruncateRunes); greedy JSON extraction; .git/ writable to the consolidation agent.

Reviewer notes

  • Scope: this PR is the agent-memory feature only. Left out on purpose: the pre-existing unrelated staged design docs (internal-doc/{dynamic-workflow-prd,model-research,sdk-hooks-design,usage-stats}.md) and a stray root MEMORY.md (not created here).
  • Design/research docs for context: internal-doc/agent-memory-design.md, agent-memory-e2e-plan.md, memory-research-2026-07.md.
  • Storage location was deliberated (global vs in-project) — see design doc §2.2; global mirrors Claude Code's actual layout.

Generated with Jack AI bot

Summary by CodeRabbit

  • New Features
    • Added cross-session Project Memory with /memory commands (status, path, clear, and sync) and persistent memory notes.
    • Memory is now injected into prompts and processed asynchronously in the background across sessions.
    • Agent evaluation now supports multi-step trajectories and new memory-focused test scenarios.
  • Documentation
    • Updated Project Memory, configuration, and command docs to explain storage, injection, and controls.
  • Bug Fixes
    • Strengthened local isolation, path safety, and secret redaction for stored memory artifacts.
  • Chores
    • Improved Git hook behavior for pre-commit and pre-push checks.

…YOM-aware)

Adds a three-layer agent memory system so jcode learns durable facts from past
sessions and feeds them into future ones:

- L0 AGENTS.md (unchanged) — static, human-authored instructions.
- L1 online notes — the `memory_note` tool writes one durable fact to a
  per-project inbox instantly (path-locked + secret redaction).
- L2 offline distillation — phase 1 extracts per-ended-session facts with the
  small model; phase 2 consolidates via a restricted subagent, git-diff driven
  with a zero-token no-op fast path and an ADD/UPDATE/DELETE/NOOP protocol.

The read path injects a size-capped memory summary into the system prompt
(TUI/ACP/web + plan mode); usage accounting feeds consolidation ranking. No
SQLite — state.json + flock, and the memory root is a git repo used for change
detection, forgetting, and rollback. BYOM cost discipline: a daily token budget
spanning both phases, a cooldown, small-model default, and a kill switch.

Storage mirrors Claude Code/Codex: global ~/.jcode/memory/, scoped per project.

Config: config.Memory{...} with zero-config defaults. CLI `jcode memory
{path,status,sync,clear}`; TUI `/memory`.

e2e: agent-eval is extended with multi-step runs, HOME fixtures/config, and
home_* oracles, plus a `memory` tier (9 cases, 9/9 pass on glm-5.1). Unit tests
cover redaction, the path guard, concurrency, UTF-8 truncation, and the git
no-op fast path.

Docs: site/docs/overview/learned-memory.md plus config/commands cross-links.
Design + research live in internal-doc/agent-memory-{design,e2e-plan}.md and
memory-research-2026-07.md.

Reviewed adversarially across 5 dimensions; fixes include git churn breaking
the no-op fast path, phase-2 budget/cooldown gaps, a broken usage-feedback
loop, a WriteNote concurrency race, redaction gaps, and UTF-8-safe truncation.

Generated with Jack AI bot
@coderabbitai

coderabbitai Bot commented Jul 4, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds cross-session Project Memory: persistent local storage, redaction and path safety, memory injection into prompts, an offline distillation pipeline, runtime wiring, CLI/TUI commands, documentation, and agent-eval coverage for multi-step memory flows.

Changes

Project Memory feature

Layer / File(s) Summary
Memory storage and config
internal/memory/..., internal/config/config.go
Defines the memory layout, state files, file locks, redaction, note storage, state coordination, and defaulted memory config accessors.
Guard, usage accounting, and prompt injection
internal/memory/guard.go, internal/memory/usage.go, internal/memory/inject.go, internal/prompts/prompts.go, internal/agent/agent.go
Adds path-guard and usage-tracking middleware and injects Project Memory into system and plan prompts, with the agent stack observing usage events.
Offline distillation pipeline
internal/memory/pipeline/..., internal/memory/pipeline/*_test.go, internal/command/memory_sync.go
Implements git-backed phase 1/phase 2 memory distillation, orchestration gates, and pipeline tests, plus the CLI sync entry point.
Runtime wiring
internal/command/acp.go, internal/command/interactive.go, internal/command/web.go, internal/tools/memory_note.go, internal/command/memory.go, cmd/jcode/main.go, internal/tui/input_views.go, internal/tui/update.go
Registers the memory note tool and starts background memory distillation across ACP, interactive, web, CLI, and TUI entry points.
Documentation
internal-doc/*.md, site/docs/..., .githooks/*, Makefile
Adds design/research docs and user-facing docs for Project Memory, plus hook and setup text updates.
agent-eval harness extension
agent-eval/suite/orchestrate.py, agent-eval/suite/verify.py, agent-eval/suite/testcases.json
Extends the evaluator with multi-step HOME-based trajectories, HOME oracles, and memory-specific cases.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ACP/Interactive/Web session
  participant memory_note tool
  participant mempipeline.Run
  participant memory store
  participant consolidation agent

  ACP/Interactive/Web session->>mempipeline.Run: MaybeStartBackground(cfg, pwd)
  ACP/Interactive/Web session->>memory_note tool: write durable fact
  memory_note tool->>memory store: WriteNote
  mempipeline.Run->>memory store: LoadState / TryLockPipeline
  mempipeline.Run->>consolidation agent: phase 1 extraction / phase 2 consolidation
  consolidation agent-->>memory store: summaries, state, baseline commit
Loading

Possibly related PRs

  • cnjack/jcode#69: Both PRs touch internal/command/acp.go session construction and session wiring, so they share a code-level area of impact.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding cross-session file-based, git-backed memory.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/agent-memory

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.

@cnjack cnjack left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Staff-engineer review

Reviewed the core new package (internal/memory, internal/memory/pipeline), the tool/command surface (memory_note, jcode memory {path,status,sync,clear}, TUI /memory), and all wiring points (agent.go, interactive.go, acp.go, web.go, config.go). The security posture (path guard, redaction, git-dir/lock-file blocking for the restricted consolidation subagent) is solid and clearly already adversarially reviewed, per the PR description. Found one real, unaddressed concurrency/data-integrity gap and a related weaker instance of the same class.

Finding 1 — TUI /memory clear has no pipeline lock at all

Impact: interactive.go calls mempipeline.MaybeStartBackground(cfg, pwd) on essentially every interactive session start (gated only by cooldown/budget, not user action). That goroutine may be mid-flight — reading/writing notes/, session_summaries/, state.json, or running git add/git commit inside the scope's .git — when the user types /memory clear. This is a real risk in normal usage, not a hypothetical, since the pipeline auto-starts on nearly every session.

Evidence: internal/tui/input_views.go, handleMemoryInput, case "clear":

case "clear":
    if err := os.RemoveAll(root); err != nil {

No call to memory.TryLockPipeline anywhere in this path. Contrast with the CLI equivalent (internal/command/memory.go, newMemoryClearCmd), which does take the lock first — the codebase already recognizes this hazard elsewhere, just not here. Racing a git commit mid-write can corrupt the baseline repo; racing phase 1/2 writes can silently recreate files under a directory the user just tried to wipe, defeating "clear."

Suggested fix: Acquire memory.TryLockPipeline(root) before RemoveAll in the TUI clear case and refuse with a clear message if the pipeline is running, mirroring the CLI path — but see Finding 2 for why the lock must be held through the delete, not released beforehand.

Finding 2 — CLI jcode memory clear releases the pipeline lock before deleting

Impact: The comment directly above the code says "Don't delete out from under a running pipeline: take its lock first," but the lock is released immediately after acquisition, before RemoveAll runs — reopening the exact TOCTOU window the lock exists to close. A pipeline invocation from another process (or another interactive session in the same project) can grab the now-free lock and start writing in the gap before the delete executes.

Evidence: internal/command/memory.go, newMemoryClearCmd:

release, ok, lerr := memory.TryLockPipeline(root)
if lerr == nil && !ok {
    return fmt.Errorf("memory pipeline is running for this project; try again shortly")
}
if release != nil {
    release()          // <-- lock dropped here
}
fmt.Printf("clearing project memory: %s\n", root)
return os.RemoveAll(root)   // <-- unprotected window starts here

Suggested fix: Defer the release until after RemoveAll completes:

release, ok, lerr := memory.TryLockPipeline(root)
if lerr == nil && !ok {
    return fmt.Errorf(...)
}
if release != nil {
    defer release()
}
return os.RemoveAll(root)

Other areas checked (no issues found at high confidence)

  • Path-traversal guarding (withinRoot): symlink resolution on existing prefixes, encoded-traversal rejection (%2e/%2f/%5c).
  • Restricted consolidation subagent's tool/argument allowlist — verified pathGuardMiddleware's pathKeys (file_path, path, dir, directory, root) actually covers every path-bearing argument used by the four tools (read, grep, write, edit) it's granted; .git/ and lock-file writes are separately blocked.
  • Secret redaction ordering in WriteNote — redaction runs before filename-slug derivation, so secrets can't leak into filenames.
  • O_EXCL unique-filename loop in WriteNote correctly handles same-second concurrent writers.
  • Daily token-budget/cooldown gates and the git-diff-driven no-op fast path.

Overall Risk: Medium

The memory pipeline runs automatically on nearly every session, so the two clear-command races aren't edge cases — worth fixing before merge.


Generated by Claude Code

cnjack added 2 commits July 4, 2026 17:53
- phase1: os.MkdirAll shadowed err so the summary WriteFile error was checked
  against the wrong variable and silently dropped (ineffassign) — use a
  dedicated werr.
- firstJSONObject: the `break` after an invalid balanced object broke the
  switch, not the scan loop, so an invalid-then-valid object sequence never
  found the valid one (staticcheck SA4011) — use a labeled `break scan`, plus
  a regression test.
- note: check handle.Close() (errcheck) — close explicitly after the write to
  surface flush errors, defer covers error paths.
- git: drop ensureBaseline's unused bool return (unparam); update callers.

Generated with Jack AI bot
The .githooks/pre-commit existed but was never active (core.hooksPath unset),
which is why the golangci-lint failure only surfaced in CI. Split into:

- pre-commit: fast gofmt/goimports gate on staged Go files only (instant, keeps
  commits snappy). Portable to macOS's stock bash 3.2 (no mapfile).
- pre-push: mirrors CI's Go job — go build, go vet, golangci-lint with the same
  --new-from-rev=origin/main gating (so pre-existing lint debt doesn't block),
  and go test. Runs once per push.

Enable with `make setup-hooks`. Bypass a hook with --no-verify; skip only tests
via SKIP_TESTS=1 git push.

Generated with Jack AI bot

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (4)
agent-eval/suite/orchestrate.py (1)

253-268: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

harness_rc semantics differ between step kinds.

For "cli" steps, harness_rc is set to the invoked jcode binary's own exit code (Line 267), but for "prompt" steps it's the ACP test-harness wrapper's exit code (Line 287) — not jcode's. Downstream tooling reading record["harness_rc"] can't distinguish "harness crashed" from "my cli subcommand returned non-zero" without also inspecting step_records[].kind. Consider a distinct field (e.g. cli_rc) for cli-step failures to avoid conflating the two.

Also applies to: 285-297

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@agent-eval/suite/orchestrate.py` around lines 253 - 268, The step failure
status is overloaded in orchestrate.py: the run loop in the cli-step branch
stores the jcode subcommand exit code into harness_rc, while prompt steps use
harness_rc for the wrapper’s own exit code. Update the step-record/result
handling in the cli branch (around subprocess.run, step_records.append, and the
rc != 0 failure path) to record the subcommand exit code in a separate field
such as cli_rc, and keep harness_rc reserved for harness-level failures so
downstream consumers can reliably tell them apart.
internal/memory/inject.go (3)

17-33: 📐 Maintainability & Code Quality | 🔵 Trivial

Consider adding unit tests for BuildInjection.

This function has non-trivial budget/truncation logic (per-source truncation + a global hard cap, empty-state short-circuit) but no test file is included in this cohort. Given it directly controls what gets injected into every model call, targeted tests (empty state, hard-cap truncation, index-only case) would help guard against regressions.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/memory/inject.go` around lines 17 - 33, Add unit tests for
BuildInjection to cover its budget and empty-state behavior. Focus on the
BuildInjection function in internal/memory/inject.go and validate the empty
short-circuit, the per-source truncation from readTruncated, the global hard cap
for globalSummary, and the index-only case when fileExists is true but
summaries/notes are empty. Use targeted tests around ProjectRoot, GlobalRoot,
RecentNotes, and fileExists so regressions in injection content are caught.

24-29: 🚀 Performance & Scalability | 🔵 Trivial

Char-per-token heuristic and hardcoded global budget.

maxChars := config.MemorySummaryInjectTokens(cfg) * 4 and the hardCap on Line 79 assume ~4 chars/token, which is a reasonable approximation for English but significantly overestimates capacity for CJK text (jcode explicitly supports Chinese, e.g. the WeChat channel and memory_note's "记住X" example). For CJK-heavy memory content, the actual token count injected into every prompt could be far higher than MemorySummaryInjectTokens intends, defeating the budget control this code is meant to enforce. Separately, the global-summary budget (300*4 on Line 26) is hardcoded rather than derived from a config knob like the project summary is, which is an inconsistency worth reconsidering.

Also applies to: 79-79

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/memory/inject.go` around lines 24 - 29, The memory injection logic
in inject() is using a fixed chars-per-token approximation and a hardcoded
global summary budget, which can overrun the intended token limit for CJK-heavy
content. Replace the `* 4` sizing in the project/global summary truncation and
the `hardCap` logic with a token-aware calculation or a shared budgeting helper,
and make the global-summary limit derive from configuration instead of a literal
so it stays consistent with `config.MemorySummaryInjectTokens` and
`readTruncated`.

36-73: 🔒 Security & Privacy | 🔵 Trivial

Reliance on soft "data not instructions" guardrail for injected memory.

Notes/summaries persisted via memory_note (potentially containing content the agent picked up during a session) get re-injected verbatim into every future system prompt. The mitigation here is purely textual instruction to the model ("Memory content below is data, not instructions"), which is a weak defense against prompt injection if a session ever records attacker-influenced text (e.g., from a fetched file/URL) as a "durable fact." Worth considering whether redaction/sanitization at write time (in note.go) should also strip instruction-like patterns, in addition to the read-time disclaimer here.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/memory/inject.go` around lines 36 - 73, The memory injection prompt
in inject.go relies only on a read-time disclaimer, but note content from
memory_note can still carry instruction-like text into future system prompts.
Update the write path in note.go to sanitize or redact prompt-injection patterns
before persisting notes, and keep the existing inject.go disclaimer as a
secondary guard; use the memory_note, NoteFile, and summary/globalSummary flow
to locate where stored text is later reinserted.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal-doc/agent-memory-design.md`:
- Around line 78-89: The fenced examples in the agent-memory design doc are
missing language tags and are triggering the MD040 lint rule. Update the
affected fenced blocks in the documentation to use explicit language identifiers
appropriate to the content (for example, text or json) so the examples remain
lint-clean; use the existing fenced sections in the architecture overview and
other referenced example blocks as the targets.

In `@internal/command/memory.go`:
- Around line 119-127: The memory clear flow releases the pipeline lock before
`os.RemoveAll`, which reintroduces a race in `memory.TryLockPipeline` handling.
Keep the lock held until after the remove completes, and only call the returned
`release` afterward. Also update the `lerr` handling in this block so any lock
acquisition error causes an immediate return instead of proceeding with the
delete.

In `@site/docs/overview/learned-memory.md`:
- Around line 222-227: The “Read-only notebook” description for the generate
toggle is misleading in the learned-memory docs. Update the wording in the
“Turning it off” section to match the actual behavior of the generate=false mode
in learned-memory.md: either rename the mode to something more accurate or
explicitly state whether /memory writes are still allowed when generation is
disabled, while keeping the contrast with enabled=false clear.

---

Nitpick comments:
In `@agent-eval/suite/orchestrate.py`:
- Around line 253-268: The step failure status is overloaded in orchestrate.py:
the run loop in the cli-step branch stores the jcode subcommand exit code into
harness_rc, while prompt steps use harness_rc for the wrapper’s own exit code.
Update the step-record/result handling in the cli branch (around subprocess.run,
step_records.append, and the rc != 0 failure path) to record the subcommand exit
code in a separate field such as cli_rc, and keep harness_rc reserved for
harness-level failures so downstream consumers can reliably tell them apart.

In `@internal/memory/inject.go`:
- Around line 17-33: Add unit tests for BuildInjection to cover its budget and
empty-state behavior. Focus on the BuildInjection function in
internal/memory/inject.go and validate the empty short-circuit, the per-source
truncation from readTruncated, the global hard cap for globalSummary, and the
index-only case when fileExists is true but summaries/notes are empty. Use
targeted tests around ProjectRoot, GlobalRoot, RecentNotes, and fileExists so
regressions in injection content are caught.
- Around line 24-29: The memory injection logic in inject() is using a fixed
chars-per-token approximation and a hardcoded global summary budget, which can
overrun the intended token limit for CJK-heavy content. Replace the `* 4` sizing
in the project/global summary truncation and the `hardCap` logic with a
token-aware calculation or a shared budgeting helper, and make the
global-summary limit derive from configuration instead of a literal so it stays
consistent with `config.MemorySummaryInjectTokens` and `readTruncated`.
- Around line 36-73: The memory injection prompt in inject.go relies only on a
read-time disclaimer, but note content from memory_note can still carry
instruction-like text into future system prompts. Update the write path in
note.go to sanitize or redact prompt-injection patterns before persisting notes,
and keep the existing inject.go disclaimer as a secondary guard; use the
memory_note, NoteFile, and summary/globalSummary flow to locate where stored
text is later reinserted.
🪄 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: 473c872c-18d2-4c64-b99a-25bd5dfa6584

📥 Commits

Reviewing files that changed from the base of the PR and between aa2930b and 6a404e9.

📒 Files selected for processing (42)
  • agent-eval/suite/orchestrate.py
  • agent-eval/suite/testcases.json
  • agent-eval/suite/verify.py
  • cmd/jcode/main.go
  • internal-doc/agent-memory-design.md
  • internal-doc/agent-memory-e2e-plan.md
  • internal-doc/memory-research-2026-07.md
  • internal/agent/agent.go
  • internal/command/acp.go
  • internal/command/interactive.go
  • internal/command/memory.go
  • internal/command/memory_sync.go
  • internal/command/web.go
  • internal/config/config.go
  • internal/memory/filelock_unix.go
  • internal/memory/filelock_windows.go
  • internal/memory/guard.go
  • internal/memory/inject.go
  • internal/memory/memory.go
  • internal/memory/memory_test.go
  • internal/memory/note.go
  • internal/memory/pipeline/git.go
  • internal/memory/pipeline/phase1.go
  • internal/memory/pipeline/phase2.go
  • internal/memory/pipeline/pipeline.go
  • internal/memory/pipeline/pipeline_test.go
  • internal/memory/pipeline/prompts.go
  • internal/memory/redact.go
  • internal/memory/state.go
  • internal/memory/usage.go
  • internal/prompts/prompts.go
  • internal/tools/memory_note.go
  • internal/tui/input_views.go
  • internal/tui/update.go
  • site/docs/commands.md
  • site/docs/configuration.md
  • site/docs/overview/buddy.md
  • site/docs/overview/channels.md
  • site/docs/overview/context-memory.md
  • site/docs/overview/ide-integration.md
  • site/docs/overview/learned-memory.md
  • site/docs/overview/mcp.md

Comment thread internal-doc/agent-memory-design.md Outdated
Comment thread internal/command/memory.go Outdated
Comment on lines +119 to +127
release, ok, lerr := memory.TryLockPipeline(root)
if lerr == nil && !ok {
return fmt.Errorf("memory pipeline is running for this project; try again shortly")
}
if release != nil {
release()
}
fmt.Printf("clearing project memory: %s\n", root)
return os.RemoveAll(root)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
fd -e go filelock internal/memory --exec cat -n {}
rg -nP -C3 'func TryLockPipeline|func.*[Ll]ock' internal/memory

Repository: cnjack/jcode

Length of output: 7252


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== internal/command/memory.go =="
cat -n internal/command/memory.go | sed -n '1,220p'

echo
echo "== internal/memory/state.go =="
cat -n internal/memory/state.go | sed -n '1,180p'

echo
echo "== any clear/removeall related tests/usages =="
rg -n -C 3 'RemoveAll|TryLockPipeline|clear project memory|--all|--global|state.lock' internal/command internal/memory

Repository: cnjack/jcode

Length of output: 19093


🏁 Script executed:

#!/bin/bash
python3 - <<'PY'
import os, tempfile, fcntl, shutil, time, subprocess, textwrap, pathlib, sys

base = tempfile.mkdtemp(prefix="locktest-")
root = os.path.join(base, "scope")
os.makedirs(root, exist_ok=True)
lock = os.path.join(root, ".state.lock")

f = open(lock, "w+")
fcntl.flock(f, fcntl.LOCK_EX | fcntl.LOCK_NB)
print("locked:", os.path.exists(lock), os.path.isdir(root))

try:
    shutil.rmtree(root)
    print("rmtree succeeded while locked")
except Exception as e:
    print("rmtree failed:", type(e).__name__, e)

print("after:", os.path.exists(lock), os.path.exists(root))
fcntl.flock(f, fcntl.LOCK_UN)
f.close()
shutil.rmtree(base, ignore_errors=True)
PY

Repository: cnjack/jcode

Length of output: 217


🏁 Script executed:

#!/bin/bash
set -euo pipefail

cat -n internal/command/memory.go | sed -n '70,160p'

Repository: cnjack/jcode

Length of output: 3661


🌐 Web query:

Go os.OpenFile Windows share delete open file RemoveAll locked file directory behavior

💡 Result:

In Go on Windows, the behavior of file operations like os.Remove and os.RemoveAll is constrained by Windows-specific file locking and sharing semantics, which differ significantly from Unix-like systems [1][2][3]. File Deletion and Locking On Windows, a file that is currently open by a process generally cannot be deleted by another process (or even the same process) unless it was opened with appropriate sharing flags [4][5][2]. Standard os.Open or os.OpenFile calls do not automatically include the FILE_SHARE_DELETE flag [1][4]. Consequently, if a file is held open without this share mode, attempts to call os.Remove or os.RemoveAll on that file will typically fail with a sharing violation or access denied error [4][5][3]. Modern Windows Behavior Recent versions of Windows (Windows 10 RS1 and later) support FILE_DISPOSITION_POSIX_SEMANTICS, which allows for Unix-like deletion behavior (deleting a file even if it is currently open) [6]. Go's os.Remove and os.RemoveAll have been updated to utilize these POSIX semantics when supported by the underlying Windows version and file system [6]. However, this support is not universal across all Windows versions and file systems, leading to historical differences in behavior [7][6]. os.RemoveAll and Read-Only Files os.RemoveAll is designed to recursively delete directory trees. In recent Go versions (notably Go 1.25+), there have been significant updates and some regressions regarding how os.RemoveAll handles read-only files and access permissions on Windows [8][9][10]. Historically, os.Remove would explicitly clear the read-only attribute before attempting deletion if the initial attempt failed [11]. Recent improvements have aimed to make this process more robust by handling read-only attributes more effectively, though these changes have occasionally introduced regressions related to access control lists (ACLs) and permission checks when requesting additional access rights like SYNCHRONIZE [9][7]. Workarounds If you encounter issues with file deletion on Windows due to locks: 1. Ensure all file handles are closed: This is the most reliable way to ensure deletion succeeds [1][4]. 2. Use FILE_SHARE_DELETE: If you must open a file that you intend to delete later while it remains open, you can use syscall.OpenFile (or similar low-level syscalls) to pass the FILE_SHARE_DELETE flag, provided the API allows it [4][2]. 3. Handle errors: Applications should be prepared to handle "Access is denied" or "sharing violation" errors, as these are expected Windows behaviors when resources are busy [12][8][9]. For the most up-to-date behavior, consult the Go repository issues, specifically those tracking Windows-specific regressions in the os package, as this is an area of active development and platform-specific adjustment [9][10][6].

Citations:


🌐 Web query:

Unix unlink file while flock held safe open file descriptor lock persists

💡 Result:

Yes, the flock lock persists even if the file is unlinked while the lock is held [1][2]. In Unix-like systems, when you unlink a file, you are merely removing the directory entry (the name) associated with that file's inode [3][2]. The file itself—including its data and the associated "open file description"—remains in existence as long as at least one process holds an open file descriptor referring to it [3][2]. Because flock locks are associated with the "open file description" rather than the file path or name, the lock remains active and effective for any process that currently holds an open file descriptor to that specific inode [4][5][6]. Key implications: 1. Persistence: The lock does not disappear when unlink is called [2]. Other processes that also have the file open (or that open it before it is fully deleted) will continue to respect the lock because they are still interacting with the same underlying open file description or inode [1][2]. 2. Cleanup: The file's inode and data are only fully removed from the filesystem when all file descriptors referring to that file are closed [3][2]. 3. Race Conditions: If you are using a file as a lockfile, be aware that while unlink removes the name, it does not release the lock [1][2]. If your goal is to manage a mutex, a common strategy to avoid race conditions is to verify the file's identity (e.g., using fstat to compare inode numbers) after acquiring the lock to ensure you are locking the intended file and not a new file that was created with the same name after the original was unlinked [1].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n -C 2 'GlobalRoot\(|TryLockPipeline\(|ProjectRoot\(' internal

Repository: cnjack/jcode

Length of output: 10008


🏁 Script executed:

#!/bin/bash
set -euo pipefail
cat -n go.mod | sed -n '1,80p'

Repository: cnjack/jcode

Length of output: 4237


Hold the pipeline lock through RemoveAll. Releasing it first reopens a race where another pipeline can reacquire .pipeline.lock and write into the scope before the clear finishes. Return on lerr too; proceeding on a lock error deletes without the intended guard.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/command/memory.go` around lines 119 - 127, The memory clear flow
releases the pipeline lock before `os.RemoveAll`, which reintroduces a race in
`memory.TryLockPipeline` handling. Keep the lock held until after the remove
completes, and only call the returned `release` afterward. Also update the
`lerr` handling in this block so any lock acquisition error causes an immediate
return instead of proceeding with the delete.

Source: Coding guidelines

Comment thread site/docs/overview/learned-memory.md
Review (CodeRabbit) follow-ups:
- clear race (Major): `memory clear` acquired the pipeline lock, released it,
  then RemoveAll — a TOCTOU gap where a pipeline could start mid-clear, plus a
  Windows sharing-violation on the still-open lock file. Add memory.ClearScope:
  refuse if the pipeline holds the lock, otherwise hold it across the delete and
  retry after release (Windows). Wire both the CLI and TUI clear through it;
  add TestClearScope (busy / success / missing-scope).
- docs (Minor): "read-only notebook" for generate=false was misleading (online
  notes still write). Reword to "manual notebook" and spell out that reading,
  injection, and memory_note stay on — only distillation is disabled.
- docs (Minor, MD040): fenced blocks in the design doc now carry language tags.

Also: translate the internal-doc memory docs (design, research, e2e-plan) from
Chinese to English so the whole PR is community-reviewable — structure, tables,
code blocks, [[wikilinks]], paths, and config keys preserved verbatim.

Generated with Jack AI bot
@cnjack

cnjack commented Jul 4, 2026

Copy link
Copy Markdown
Owner Author

Addressed the review in 2c1033a:

  • memory clear race (Major) — replaced the acquire-release-then-RemoveAll sequence with memory.ClearScope, which refuses when the pipeline holds the lock and otherwise holds the lock across the delete (retrying after release to dodge the Windows sharing-violation on the still-open lock file). CLI and TUI clear both route through it; added TestClearScope (busy / success / missing-scope).
  • generate=false wording (Minor) — "read-only notebook" was misleading since online notes still write. Reworded to "manual notebook" and spelled out that reading, injection, and memory_note stay on — only the distillation pipeline is off.
  • MD040 fenced blocks (Minor) — the design doc's code fences now carry language tags.

Also translated the internal design docs (agent-memory-design.md, memory-research-2026-07.md, agent-memory-e2e-plan.md) from Chinese to English so the whole PR is community-reviewable — structure, tables, code blocks, wikilinks, and config keys preserved verbatim.

@cnjack cnjack merged commit bd2c855 into main Jul 4, 2026
2 of 3 checks passed
@cnjack cnjack deleted the feat/agent-memory branch July 4, 2026 13:19

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
.githooks/pre-commit (1)

10-19: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Missing pipefail masks upstream git diff failures.

Without pipefail, the exit status of the git diff --cached | while ... pipeline is only that of the trailing while loop. If git diff --cached itself fails (corrupted index, git error), the loop simply reads no lines, files ends up empty, and the hook exits 0 on line 20 — silently skipping the formatting gate instead of failing loudly. Bash 3.2 (the portability target mentioned in the header comment) does support pipefail, so it can be enabled here without breaking the stated compatibility goal.

🛠️ Proposed fix
-set -eu
+set -eu -o pipefail
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.githooks/pre-commit around lines 10 - 19, The pre-commit hook pipeline in
the files collection block can hide failures from git diff because only the
while loop exit status is checked. Update the hook near the files assignment to
enable pipefail alongside set -eu so git diff --cached errors propagate and the
hook fails instead of silently skipping formatting checks.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In @.githooks/pre-commit:
- Around line 10-19: The pre-commit hook pipeline in the files collection block
can hide failures from git diff because only the while loop exit status is
checked. Update the hook near the files assignment to enable pipefail alongside
set -eu so git diff --cached errors propagate and the hook fails instead of
silently skipping formatting checks.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2be4b737-d78c-46b3-8126-171cb4309fcb

📥 Commits

Reviewing files that changed from the base of the PR and between 6a404e9 and 2c1033a.

📒 Files selected for processing (16)
  • .githooks/pre-commit
  • .githooks/pre-push
  • Makefile
  • internal-doc/agent-memory-design.md
  • internal-doc/agent-memory-e2e-plan.md
  • internal-doc/memory-research-2026-07.md
  • internal/command/memory.go
  • internal/memory/memory_test.go
  • internal/memory/note.go
  • internal/memory/pipeline/git.go
  • internal/memory/pipeline/phase1.go
  • internal/memory/pipeline/phase2.go
  • internal/memory/pipeline/pipeline_test.go
  • internal/memory/state.go
  • internal/tui/input_views.go
  • site/docs/overview/learned-memory.md
✅ Files skipped from review due to trivial changes (1)
  • site/docs/overview/learned-memory.md
🚧 Files skipped from review as they are similar to previous changes (8)
  • internal/memory/pipeline/git.go
  • internal/command/memory.go
  • internal/memory/state.go
  • internal/memory/note.go
  • internal/tui/input_views.go
  • internal/memory/memory_test.go
  • internal/memory/pipeline/phase2.go
  • internal/memory/pipeline/pipeline_test.go

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.

1 participant