Skip to content

feat: team-memory layer#257

Merged
emal-avala merged 3 commits into
mainfrom
feat/team-memory-layer
May 1, 2026
Merged

feat: team-memory layer#257
emal-avala merged 3 commits into
mainfrom
feat/team-memory-layer

Conversation

@emal-avala
Copy link
Copy Markdown
Member

Summary

Adds Phase 8.11 from ROADMAP.md: a project-shared, version-controlled memory layer that lives alongside per-user and per-project memory.

  • Storage: <project>/.agent/team-memory/ — same file format as user memory (frontmatter + body), so the existing reader handles it without changes. Frontmatter gains optional author and created_at fields.
  • New Scope enum (User / Project / Team) tagged on every loaded MemoryFile.
  • Loader merges team-memory entries into the in-context memory with precedence project > team > user. Collisions are kept silent except for a debug-level log.
  • New /team-remember slash command — the only sanctioned write path. Prompts for confirmation since entries enter version control. Subcommands: list, remove <name>. Writes are append-only by default; collisions require --force. Each entry stamps the author from git config user.email (falls back to unknown) plus an ISO-8601 timestamp.
  • Safety invariant: is_team_memory_path predicate; background extraction now refuses to coast off any FileWrite/FileEdit that targeted a team-memory path. The extraction loop's own writer continues to target only the per-user config dir, so the model can read team memory but not silently write to it.
  • Consolidation: thin run_team_consolidation wrapper; the existing pipeline already takes a &Path, so consolidation on team-memory is a one-line call.
  • Docs: extends docs/concepts/memory.mdx and the mdBook copy at docs/src/concepts/memory.md with a Team memory section, the read-but-not-silently-write invariant, and the collision precedence rule.

Test plan

  • cargo build --workspace clean.
  • cargo test -p agent-code-lib --lib — 789 passed (was 783; six new memory tests).
  • cargo test -p agent-code (CLI unit + smoke) — green.
  • cargo fmt --check clean; cargo clippy --workspace --no-deps clean.
  • New unit tests:
    • team-memory round-trip: write entry, reload via MemoryContext::load, assert it appears with Scope::Team and the body roundtrips.
    • collision precedence: same id across User/Team/Project collapses to one file with the highest precedence kept.
    • extraction safety: ensure_memory_dir() (the only path background extraction writes to) does not satisfy is_team_memory_path.
    • team-write append-only: collision without --force returns Err and leaves the file untouched; --force overwrites.
    • is_team_memory_path recognizes .agent/team-memory/ and rejects .agent/memory/.
    • list_team_memory skips the MEMORY.md index.
    • delete_team_memory accepts a bare name without .md.
  • No new crate dependencies; uses chrono (already a workspace dep) for timestamps and shells out to git for the author lookup.
  • Manual smoke (left for reviewer): run /team-remember test entry, confirm prompt fires, file lands under <project>/.agent/team-memory/, restart session, confirm the entry shows up under # Team Memory Index in the system prompt section.

Adds a project-shared, version-controlled memory layer alongside the
existing per-user and per-project memory.

- New `Scope` enum (User / Project / Team) and `author` / `created_at`
  frontmatter fields on `MemoryMeta`.
- Loader merges team-memory entries into the in-context memory with
  precedence project > team > user; collisions log a debug note.
- New writer entry points `write_team_memory`, `list_team_memory`, and
  `delete_team_memory`. Writes are append-only by default; a collision
  without `force` returns an error.
- `is_team_memory_path` guard predicate; background extraction skips
  any tool call that targets a team-memory path so the model can read
  team memory but cannot silently write to it.
- New `/team-remember` slash command (with `list` and `remove`
  subcommands and `--force`) — the only sanctioned write path. Prompts
  for confirmation since entries enter version control.
- Consolidation pipeline gains a `run_team_consolidation` wrapper that
  targets the team-memory dir.
- Docs: extends `docs/concepts/memory.mdx` (and the mdBook copy) with
  a Team memory section, the read-but-not-silently-write invariant,
  and the precedence rule.

Tests: round-trip load of a team-memory entry; collision precedence;
that the per-user extraction dir cannot satisfy the team-memory path
guard; team-write refuses overwrites without `--force` and accepts
them with `--force`.
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: bd5a53d673

ℹ️ 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 +139 to +143
name_or_filename.to_string()
} else {
format!("{name_or_filename}.md")
};
delete_memory(team_memory_dir, &filename)
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 Reject path traversal in team-memory removals

delete_team_memory forwards unvalidated user input directly into delete_memory, which does team_memory_dir.join(filename); names like ../../README therefore resolve outside .agent/team-memory and allow /team-remember remove ... to delete arbitrary .md files in parent directories. Because this command is the new public delete path for team memory, it should reject path separators/.. segments (or canonicalize and enforce containment) before calling delete_memory.

Useful? React with 👍 / 👎.

Comment thread crates/lib/src/memory/mod.rs Outdated
}
}
}
by_id.into_values().collect()
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve deterministic order when merging memory files

Collecting merged entries via HashMap::into_values() produces non-deterministic iteration order, so the memory_files sequence (and therefore system prompt layout) can shuffle between loads even when inputs are unchanged. Since MemoryContext::load runs during prompt construction, this can cause prompt churn and unnecessary cache misses; the merged result should be made stable (e.g., preserve input order or sort by key/path before returning).

Useful? React with 👍 / 👎.

- delete_team_memory and write_team_memory now validate filenames
  (no path separators, no '..', no NUL/control chars, ASCII-printable,
  length-capped) and verify the resolved path stays inside the
  team-memory directory via canonicalize() as defense-in-depth. The
  same checks apply to the per-user write_memory and delete_memory
  paths so '/team-remember remove ../README' (and equivalents) can no
  longer delete arbitrary files.
- merge_scoped_files used HashMap::into_values, whose iteration order
  is non-deterministic; the resulting memory_files Vec (and the
  system-prompt layout) shuffled between loads. Switched to BTreeMap
  and a final sort by (scope_priority, path) with User < Team < Project
  so prompt-cache hits stay stable. Added round-trip and order tests.
@emal-avala
Copy link
Copy Markdown
Member Author

Addressed codex review findings: validated team-memory filenames (rejects path separators, '..', NUL/control chars, non-ASCII, overlong names) on both create and delete paths plus a canonicalize() defense-in-depth check; switched merge_scoped_files from HashMap to BTreeMap + (scope_priority, path) sort so the system-prompt layout is byte-stable across loads. Added tests for traversal rejection and merge determinism.

…en writer

- Block all write tools (FileWrite/FileEdit/MultiEdit/NotebookEdit) and
  Bash redirection from writing under `<project_root>/.agent/team-memory/`.
  The `/team-remember` slash command is the only sanctioned writer; it
  bypasses the tool permission check by calling `write_team_memory`
  directly. Permission checker grows an optional project root pinned at
  startup; when set, canonical-prefix containment refuses traversal.
- Route consolidation `delete` actions through `delete_memory` so the
  validator + `ensure_path_within` guards apply. A consolidation reply
  with `../../README.md` no longer unlinks files outside the memory dir.
- Validate `MEMORY.md` link targets the same way as the writer: refuse
  absolute paths, `..` segments, escapes from base_dir, and symlink
  leaves. Applies to user, team, and project memory.
- Filename validator now rejects case-insensitive collisions with
  `MEMORY.md` / `README.md`, trailing dots/whitespace, and Windows-
  reserved device names (CON, PRN, AUX, NUL, COM1-9, LPT1-9).
- `ensure_path_within` refuses a symlink leaf so `std::fs::write` can no
  longer follow a pre-planted link out of the memory dir.
- `rebuild_index` sorts headers lexicographically before emitting so the
  on-disk MEMORY.md is byte-stable across mtime drift between checkouts.
@emal-avala
Copy link
Copy Markdown
Member Author

Addressed second-round codex findings: enforce team-memory read-only invariant in the permission checker (writes via FileWrite/FileEdit/MultiEdit/NotebookEdit/Bash redirection are refused; /team-remember is the only sanctioned writer), route consolidation deletes through validated delete_memory, validate MEMORY.md link targets (no traversal, no symlink leaves) for user/team/project memory, reject case-insensitive index collisions + trailing dots/whitespace + Windows device names, refuse symlink leaves on writes, and sort rebuild_index lexicographically for byte-stable output across checkouts.

@emal-avala emal-avala merged commit 54d1ef8 into main May 1, 2026
14 checks passed
@emal-avala emal-avala deleted the feat/team-memory-layer branch May 1, 2026 20:44
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