Skip to content

feat(memory): add team-level memory attached to the team object#3972

Open
jbecke wants to merge 5 commits into
mainfrom
claude/team-level-memory-pefwhh
Open

feat(memory): add team-level memory attached to the team object#3972
jbecke wants to merge 5 commits into
mainfrom
claude/team-level-memory-pefwhh

Conversation

@jbecke

@jbecke jbecke commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Summary

Implements team-level memory generation and retrieval, allowing AI agents to access shared context about a team's work, members, and processes. This complements the existing per-user memory system by providing team-wide background knowledge to all team members.

Key Changes

  • Team Memory Service (team_service.rs): New service that generates and retrieves team memories using an AI agent loop. Includes:

    • get_or_generate_team_memory(): Fetches latest team memory or triggers background generation if stale (>24h) or missing
    • generate_team_memory(): Uses AI agent with tool access to research team workspace (documents, projects, emails, channels) and generate ~1000-3000 word team summary
    • Cross-instance generation locking via PostgreSQL advisory locks to prevent concurrent regenerations
    • Quality judgment via a second AI pass to reject low-quality or speculative memories
  • Team Memory Repository (pg_team_memory_repo.rs): PostgreSQL-backed repository implementing TeamMemoryRepo trait with:

    • save_team_memory(): Upserts team memory (one per team)
    • get_latest_team_memory(): Retrieves current team memory with timestamp
    • get_team_memory_by_id(): Fetches specific memory version
    • get_user_team_id(): Resolves user's team membership
    • get_team_overview(): Fetches team name and member list for grounding generation
  • HTTP API (axum_router.rs): New /memory/team endpoint that:

    • Returns current team memory (200) or triggers background generation if stale/missing
    • Returns 404 if user has no team or no memory exists yet
    • Follows same pattern as user memory endpoint
  • Database Schema (20260610212946_create_team_memory_table.sql): New team_memory table with:

    • One row per team (unique constraint on team_id)
    • Cascading delete when team is deleted
    • Timestamps for freshness tracking
  • Integration Points:

    • Scheduled agent tasks now fetch and include team memory in system prompts
    • Chat message streaming fetches team memory alongside user memory
    • Main application and test contexts initialize team memory service
  • Comprehensive Tests: Unit tests for repository operations, system prompt building, and edge cases (team isolation, missing teams, stale data)

Notable Implementation Details

  • Team memory generation is non-blocking: stale/missing memory triggers background task while returning current state
  • Generation is serialized cross-instance via Postgres advisory locks to handle multiple team members triggering refresh simultaneously
  • System prompt includes previous team memory to preserve durable facts while updating with fresh research
  • Quality judgment uses strict criteria: rejects speculation, insufficient data, or single-member profiles
  • Team memory is scoped and isolated per team with proper authorization checks

https://claude.ai/code/session_01CyfX47q3gqFp49pLrcco8b

Mirrors the personal memory system for teams:
- team_memory table (one row per team, FK to team, upsert on team_id)
- TeamMemoryRepo / TeamMemoryService ports + PgTeamMemoryRepo
- TeamMemoryServiceImpl with team-focused generation and judge prompts,
  24h freshness window, and a Postgres advisory lock so concurrent
  members don't trigger duplicate generations
- GET /memory/team endpoint on the document cognition service
- <team_memory> injected into chat and scheduled-agent system prompts
  alongside <user_memory>

https://claude.ai/code/session_01CyfX47q3gqFp49pLrcco8b
@jbecke jbecke requested a review from a team as a code owner June 10, 2026 21:35
@coderabbitai

coderabbitai Bot commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds team memory support to the memory service system, enabling agents to maintain and retrieve contextual memory at both the user and team level. The implementation extends the database schema to store team-scoped memory records, introduces domain types to represent dual-memory results, implements a team memory generation pipeline with concurrent retrieval and cross-instance locking, updates the HTTP API to expose both memories, and integrates team memory into chat, document cognition, and scheduled agent endpoints.

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Description check ✅ Passed The description is comprehensive and directly related to the changeset, detailing all major features, implementation details, and integration points for team-level memory functionality.
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.
Title check ✅ Passed The PR title follows conventional commits format with 'feat:' prefix, is under 72 characters (63 chars), and accurately describes the main feature: adding team-level memory functionality.

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


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.

claude added 2 commits June 10, 2026 21:45
Restores the .sqlx entries accidentally deleted in the previous commit
(cargo sqlx prepare clears the directory while it runs) and adds the
new team_memory query metadata.

https://claude.ai/code/session_01CyfX47q3gqFp49pLrcco8b
@ehayes2000

Copy link
Copy Markdown
Contributor

Looks super close.

Can we create one memory service that we can call get_or_generate_memory on to trigger both generations and fetch the memories that exist? I don't think there's a place where we want only team or only user memory.

Also looks like this is last write wins. Probably not a problem

…ervice

Review feedback from #3972:
- team memory is now a team_id-scoped row in the existing memory table
  (user_id/team_id are mutually exclusive, partial unique indexes keep
  one-row-per-owner upsert semantics) instead of a separate table
- one MemoryService::get_or_generate_memory call now fetches both the
  personal and team memory and triggers both background generations;
  the separate TeamMemoryService and GET /memory/team endpoint are gone
- GET /memory returns { memory?, team_memory? }; regenerated OpenAPI
  spec and TypeScript client

https://claude.ai/code/session_01CyfX47q3gqFp49pLrcco8b

jbecke commented Jun 10, 2026

Copy link
Copy Markdown
Contributor Author

Both addressed in 0407542:

  • Single table (from your Slack note): team memory is now a team_id-scoped row in the existing memory table — user_id/team_id are mutually exclusive (CHECK num_nonnulls(...) = 1) with partial unique indexes so each scope keeps one-row-per-owner upsert semantics. ON DELETE CASCADE ties the row's lifetime to the team.
  • One service: MemoryService::get_or_generate_memory(user) now returns Memories { user, team } and triggers both background generations (joined concurrently). The separate TeamMemoryService and GET /memory/team endpoint are gone; GET /memory returns { memory?, team_memory? }.

On last-write-wins: agreed it's fine for personal memory, but since any member of a team can trigger a refresh, concurrent team generations are serialized with a pg_try_advisory_lock — losers skip instead of redundantly regenerating and overwriting.


Generated by Claude Code

@github-actions

github-actions Bot commented Jun 10, 2026

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (3)
rust/cloud-storage/memory/src/domain/ports.rs (1)

78-103: ⚡ Quick win

Add documentation to the TeamMemoryRepo trait.

The TeamMemoryRepo trait is missing a doc comment. If this crate has #![deny(missing_docs)] enabled (per Rust guidelines for new crates), this will cause a compilation error.

📝 Suggested doc comment
+/// Repository for team-scoped memory persistence.
+///
+/// Team memory is stored in the same table as user memory but scoped to
+/// `team_id` rather than `user_id`.
 pub trait TeamMemoryRepo: Send + Sync + 'static {
🤖 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 `@rust/cloud-storage/memory/src/domain/ports.rs` around lines 78 - 103, Add a
doc comment for the TeamMemoryRepo trait (place a /// comment block immediately
above pub trait TeamMemoryRepo) describing its purpose as the in-memory
persistence interface for team-related memories and lookups, and briefly
document each provided method (save_team_memory, get_latest_team_memory,
get_team_memory_by_id, get_user_team_id, get_team_overview) so users know what
each does and expected return semantics; ensure the comments are descriptive
enough to satisfy `#![deny(missing_docs)]` and mention any important behaviors
(e.g., Option returns, errors wrapped in Result, async Future returns).

Source: Coding guidelines

rust/cloud-storage/memory/src/outbound/pg_team_memory_repo.rs (1)

12-20: ⚡ Quick win

Add documentation to public items.

The PgTeamMemoryRepo struct and its new constructor lack doc comments. If this crate has #![deny(missing_docs)] enabled, this will cause a compilation error.

📝 Suggested doc comments
+/// Postgres-backed repository for team memory storage.
 pub struct PgTeamMemoryRepo {
     inner: PgPool,
 }

 impl PgTeamMemoryRepo {
+    /// Create a new team memory repository with the given database pool.
     pub fn new(inner: PgPool) -> Self {
🤖 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 `@rust/cloud-storage/memory/src/outbound/pg_team_memory_repo.rs` around lines
12 - 20, Add missing documentation comments for the public struct
PgTeamMemoryRepo and its constructor method new: add a brief /// doc comment
above the PgTeamMemoryRepo declaration describing its purpose (e.g.,
in-memory/outbound team repository backed by a PgPool) and a /// doc comment
above impl PgTeamMemoryRepo::new that explains the parameter and what the
constructor returns. Ensure the comments follow crate style (short summary
sentence, optional longer description) so the crate compiles with
#![deny(missing_docs)].

Source: Coding guidelines

rust/cloud-storage/macro_db_client/migrations/20260610212946_add_team_id_to_memory_table.sql (1)

3-11: ⚖️ Poor tradeoff

Consider adding idempotency guards to the migration.

The migration uses ALTER TABLE statements that will fail if re-run. While SQLx tracks applied migrations and won't re-execute them, the SQL review guidelines recommend making migrations idempotent where possible.

You could wrap the alterations in a DO block that checks for column existence, though this adds complexity for minimal practical benefit given SQLx's migration tracking.

🤖 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
`@rust/cloud-storage/macro_db_client/migrations/20260610212946_add_team_id_to_memory_table.sql`
around lines 3 - 11, Make the migration idempotent by guarding each schema
change for the memory table: check that the user_id column exists and currently
has a NOT NULL constraint before running ALTER COLUMN to DROP NOT NULL; add
team_id using ADD COLUMN IF NOT EXISTS (with the REFERENCES team(id) ON DELETE
CASCADE) or a conditional check for the column in pg_catalog/information_schema;
add the memory_user_or_team CHECK only if it doesn't already exist (check
pg_constraint by conname); use DROP CONSTRAINT IF EXISTS memory_user_id_unique
before attempting to drop it; and create the two partial unique indexes
(memory_user_id_unique and memory_team_id_unique) using CREATE UNIQUE INDEX IF
NOT EXISTS (or wrap creation in a conditional that checks pg_index/pg_class) so
re-running the migration is safe.

Source: Coding guidelines

🤖 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 `@rust/cloud-storage/memory/src/domain/service.rs`:
- Around line 107-119: In get_or_generate_memory, avoid treating the joined
results from tokio::join! as all-or-nothing: instead of immediately using the
`?` on both results, handle each Result returned by get_or_generate_user_memory
and get_or_generate_team_memory individually so one Err does not discard the
other's Ok; call both functions with tokio::join!, map each result to either a
successful memory or None / a fallback value, log or propagate errors
selectively if needed, and construct Memories { user: ..., team: ... } using the
available values so the endpoint can return whichever scope(s) exist.

In `@rust/cloud-storage/memory/src/domain/service/team.rs`:
- Around line 128-130: The code calls svc.generate_team_memory(team_id,
user.clone(), previous_memory) using the triggering user's permissions, which
risks leaking artifacts private to that user into a shared team memory; update
the call and implementation to use a team-scoped visibility check instead of a
single user's view by (a) changing generate_team_memory to accept the team's
membership list or a team-permission context (e.g., team_id plus Vec<member_ids>
or a TeamPermission object) and (b) inside generate_team_memory only include
artifacts visible to every teammate (compute intersection of visible artifact
IDs) or otherwise restrict to artifacts explicitly marked as team-visible;
update every call site (the current svc.generate_team_memory invocations and
analogous calls at the other noted locations) to pass the team
membership/permission context so the persisted team memory contains only data
safe for all members.
- Around line 99-140: The spawned task captures needs_generation and
previous_memory before acquiring the advisory lock, allowing a stale generation
to proceed; after acquiring the lock via try_acquire_generation_lock(&pool,
team_id).await but before calling svc.generate_team_memory(...), re-fetch the
current row/state (the same source used to compute
needs_generation/previous_memory) and re-check whether generation is still
required and whether previous_memory changed, and only call
MemoryServiceImpl::generate_team_memory(team_id, user.clone(), previous_memory)
when the re-check indicates generation is still needed; if not needed, log and
return. Ensure this re-check uses the same repo/db pool and the same semantics
as the original needs_generation computation so the advisory lock enforces
deduplication rather than just serialization.

In
`@rust/cloud-storage/scheduled_action/src/outbound/inprocess_executor/agent_task.rs`:
- Around line 125-131: The None branch for building system_prompt drops
SCHEDULED_AGENT_PROMPT so scheduled runs lose guardrails; update the None branch
that currently returns tools.prompt.to_string() to include
SCHEDULED_AGENT_PROMPT (e.g., concatenate tools.prompt and
SCHEDULED_AGENT_PROMPT with a newline) so system_prompt always contains both
tools.prompt and SCHEDULED_AGENT_PROMPT even when memories.user is None; modify
the code around system_prompt, memories.user, SCHEDULED_AGENT_PROMPT and
tools.prompt accordingly.

---

Nitpick comments:
In
`@rust/cloud-storage/macro_db_client/migrations/20260610212946_add_team_id_to_memory_table.sql`:
- Around line 3-11: Make the migration idempotent by guarding each schema change
for the memory table: check that the user_id column exists and currently has a
NOT NULL constraint before running ALTER COLUMN to DROP NOT NULL; add team_id
using ADD COLUMN IF NOT EXISTS (with the REFERENCES team(id) ON DELETE CASCADE)
or a conditional check for the column in pg_catalog/information_schema; add the
memory_user_or_team CHECK only if it doesn't already exist (check pg_constraint
by conname); use DROP CONSTRAINT IF EXISTS memory_user_id_unique before
attempting to drop it; and create the two partial unique indexes
(memory_user_id_unique and memory_team_id_unique) using CREATE UNIQUE INDEX IF
NOT EXISTS (or wrap creation in a conditional that checks pg_index/pg_class) so
re-running the migration is safe.

In `@rust/cloud-storage/memory/src/domain/ports.rs`:
- Around line 78-103: Add a doc comment for the TeamMemoryRepo trait (place a
/// comment block immediately above pub trait TeamMemoryRepo) describing its
purpose as the in-memory persistence interface for team-related memories and
lookups, and briefly document each provided method (save_team_memory,
get_latest_team_memory, get_team_memory_by_id, get_user_team_id,
get_team_overview) so users know what each does and expected return semantics;
ensure the comments are descriptive enough to satisfy `#![deny(missing_docs)]`
and mention any important behaviors (e.g., Option returns, errors wrapped in
Result, async Future returns).

In `@rust/cloud-storage/memory/src/outbound/pg_team_memory_repo.rs`:
- Around line 12-20: Add missing documentation comments for the public struct
PgTeamMemoryRepo and its constructor method new: add a brief /// doc comment
above the PgTeamMemoryRepo declaration describing its purpose (e.g.,
in-memory/outbound team repository backed by a PgPool) and a /// doc comment
above impl PgTeamMemoryRepo::new that explains the parameter and what the
constructor returns. Ensure the comments follow crate style (short summary
sentence, optional longer description) so the crate compiles with
#![deny(missing_docs)].
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: a828049f-bba2-4aca-ba25-acabfc5e766e

📥 Commits

Reviewing files that changed from the base of the PR and between 05e1890 and 0407542.

⛔ Files ignored due to path filters (11)
  • js/app/packages/service-clients/service-cognition/generated/client.ts is excluded by !**/generated/**
  • js/app/packages/service-clients/service-cognition/generated/schemas/index.ts is excluded by !**/generated/**
  • js/app/packages/service-clients/service-cognition/generated/schemas/memoryResponse.ts is excluded by !**/generated/**
  • js/app/packages/service-clients/service-cognition/generated/schemas/memoryResponseMemory.ts is excluded by !**/generated/**
  • js/app/packages/service-clients/service-cognition/generated/schemas/memoryResponseTeamMemory.ts is excluded by !**/generated/**
  • rust/cloud-storage/.sqlx/query-2bacd87def214ac03a40e3b5858a67bc5e7f96ca1d145e65f0c6e3f2787cf227.json is excluded by !**/.sqlx/**
  • rust/cloud-storage/.sqlx/query-5a427312ded69610ede4679d54cefaf14c6da39d8e7a2ade3b370343002be82c.json is excluded by !**/.sqlx/**
  • rust/cloud-storage/.sqlx/query-91bfe5ec5b5f8eadaa5a149987c59081daf7cd5fb66aa4d8299f39c96e402d80.json is excluded by !**/.sqlx/**
  • rust/cloud-storage/.sqlx/query-bdbe32af4a2c3138e6853ec1d541abbce728b93591e34fb09d9ce8bfa19729c1.json is excluded by !**/.sqlx/**
  • rust/cloud-storage/.sqlx/query-d932a832244139cad77a3dcfb0b11ff406ea348014419a970a7a4ea5b7a455b0.json is excluded by !**/.sqlx/**
  • rust/cloud-storage/.sqlx/query-ea3b56b003d40d52eb5fe2e247fa38e7cfbcff3def9db49e5d9bad83207a3a48.json is excluded by !**/.sqlx/**
📒 Files selected for processing (18)
  • js/app/packages/service-clients/service-cognition/openapi.json
  • rust/cloud-storage/document_cognition_service/src/api/context/mod.rs
  • rust/cloud-storage/document_cognition_service/src/api/context/test.rs
  • rust/cloud-storage/document_cognition_service/src/api/stream/chat_message/mod.rs
  • rust/cloud-storage/document_cognition_service/src/main.rs
  • rust/cloud-storage/macro_db_client/migrations/20260610212946_add_team_id_to_memory_table.sql
  • rust/cloud-storage/memory/src/domain/mod.rs
  • rust/cloud-storage/memory/src/domain/ports.rs
  • rust/cloud-storage/memory/src/domain/service.rs
  • rust/cloud-storage/memory/src/domain/service/team.rs
  • rust/cloud-storage/memory/src/domain/service/team/test.rs
  • rust/cloud-storage/memory/src/inbound/axum_router.rs
  • rust/cloud-storage/memory/src/main.rs
  • rust/cloud-storage/memory/src/outbound.rs
  • rust/cloud-storage/memory/src/outbound/pg_memory_repo.rs
  • rust/cloud-storage/memory/src/outbound/pg_team_memory_repo.rs
  • rust/cloud-storage/memory/src/outbound/pg_team_memory_repo/test.rs
  • rust/cloud-storage/scheduled_action/src/outbound/inprocess_executor/agent_task.rs

Comment on lines +107 to +119
async fn get_or_generate_memory(
&self,
user: macro_user_id::user_id::MacroUserIdStr<'static>,
) -> super::Result<Memories> {
let (user_memory, team_memory) = tokio::join!(
self.get_or_generate_user_memory(user.clone()),
self.get_or_generate_team_memory(user)
);

Ok(Memories {
user: user_memory?,
team: team_memory?,
})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don't make combined memory retrieval all-or-nothing.

tokio::join! is fine here, but the immediate ? on both results means a transient failure in either branch drops the other successful memory on the floor. That turns /memory into a 500 even when one scope was available, which defeats the new "return whichever memories currently exist" contract.

Suggested direction
-        let (user_memory, team_memory) = tokio::join!(
+        let (user_memory, team_memory) = tokio::join!(
             self.get_or_generate_user_memory(user.clone()),
             self.get_or_generate_team_memory(user)
         );
 
-        Ok(Memories {
-            user: user_memory?,
-            team: team_memory?,
-        })
+        match (user_memory, team_memory) {
+            (Ok(user), Ok(team)) => Ok(Memories { user, team }),
+            (Ok(user), Err(e)) => {
+                tracing::error!(error = ?e, "failed to load team memory");
+                Ok(Memories { user, team: None })
+            }
+            (Err(e), Ok(team)) => {
+                tracing::error!(error = ?e, "failed to load user memory");
+                Ok(Memories { user: None, team })
+            }
+            (Err(user_err), Err(team_err)) => {
+                tracing::error!(error = ?user_err, "failed to load user memory");
+                tracing::error!(error = ?team_err, "failed to load team memory");
+                Err(user_err)
+            }
+        }
🤖 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 `@rust/cloud-storage/memory/src/domain/service.rs` around lines 107 - 119, In
get_or_generate_memory, avoid treating the joined results from tokio::join! as
all-or-nothing: instead of immediately using the `?` on both results, handle
each Result returned by get_or_generate_user_memory and
get_or_generate_team_memory individually so one Err does not discard the other's
Ok; call both functions with tokio::join!, map each result to either a
successful memory or None / a fallback value, log or propagate errors
selectively if needed, and construct Memories { user: ..., team: ... } using the
available values so the endpoint can return whichever scope(s) exist.

Comment on lines +99 to +140
let env = Environment::new_or_prod();
if needs_generation && !matches!(env, Environment::Local) {
let previous_memory = record.as_ref().map(|r| r.memory.clone());
let pool = self.db.clone();
let tool_context = self.tool_context.clone();
let toolset = self.tools.toolset.clone();
let prompt: Box<dyn std::fmt::Display + Send + Sync> =
Box::new(self.tools.prompt.to_string());
tokio::spawn(async move {
// Unlike personal memory, every member of a (possibly large)
// team can trigger a regeneration while one is already in
// flight, so generations are serialized cross-instance via a
// Postgres advisory lock.
let _lock = match try_acquire_generation_lock(&pool, team_id).await {
Ok(Some(lock)) => lock,
Ok(None) => {
tracing::debug!(%team_id, "team memory generation already in progress");
return;
}
Err(e) => {
tracing::error!(error = ?e, %team_id, "failed to acquire team memory generation lock");
return;
}
};
let repo = crate::outbound::pg_memory_repo::PgMemoryRepo::new(pool.clone());
let team_repo =
crate::outbound::pg_team_memory_repo::PgTeamMemoryRepo::new(pool.clone());
let tools = ToolSetWithPrompt { toolset, prompt };
let svc = MemoryServiceImpl::new(pool, repo, team_repo, tool_context, tools);
match svc
.generate_team_memory(team_id, user.clone(), previous_memory)
.await
{
Ok(_) => tracing::info!(%team_id, "team memory generated"),
Err(MemoryError::Rejected(reason)) => {
tracing::warn!(%team_id, %reason, "team memory rejected by judge")
}
Err(e) => {
tracing::error!(%team_id, error = ?e, "team memory generation failed")
}
}
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Re-check staleness after taking the advisory lock.

needs_generation and previous_memory are captured before the spawned task acquires the lock. If another request refreshes the row first, a delayed task can still grab the lock afterward and run a second full generation based on stale state. That turns the lock into serialization only, not deduplication, and can overwrite a fresh summary with a redundant pass.

Suggested direction
             tokio::spawn(async move {
                 // Unlike personal memory, every member of a (possibly large)
                 // team can trigger a regeneration while one is already in
                 // flight, so generations are serialized cross-instance via a
                 // Postgres advisory lock.
                 let _lock = match try_acquire_generation_lock(&pool, team_id).await {
                     Ok(Some(lock)) => lock,
                     Ok(None) => {
                         tracing::debug!(%team_id, "team memory generation already in progress");
                         return;
                     }
                     Err(e) => {
                         tracing::error!(error = ?e, %team_id, "failed to acquire team memory generation lock");
                         return;
                     }
                 };
                 let repo = crate::outbound::pg_memory_repo::PgMemoryRepo::new(pool.clone());
                 let team_repo =
                     crate::outbound::pg_team_memory_repo::PgTeamMemoryRepo::new(pool.clone());
+                let latest = match team_repo.get_latest_team_memory(team_id).await {
+                    Ok(latest) => latest,
+                    Err(e) => {
+                        tracing::error!(error = ?e, %team_id, "failed to reload latest team memory");
+                        return;
+                    }
+                };
+                let still_needs_generation = match &latest {
+                    Some(r) => {
+                        let age = Utc::now() - r.updated_at;
+                        age > chrono::Duration::from_std(MAX_AGE).unwrap_or(chrono::TimeDelta::MAX)
+                    }
+                    None => true,
+                };
+                if !still_needs_generation {
+                    tracing::debug!(%team_id, "team memory already refreshed");
+                    return;
+                }
+                let previous_memory = latest.as_ref().map(|r| r.memory.clone());
                 let tools = ToolSetWithPrompt { toolset, prompt };
                 let svc = MemoryServiceImpl::new(pool, repo, team_repo, tool_context, tools);
🤖 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 `@rust/cloud-storage/memory/src/domain/service/team.rs` around lines 99 - 140,
The spawned task captures needs_generation and previous_memory before acquiring
the advisory lock, allowing a stale generation to proceed; after acquiring the
lock via try_acquire_generation_lock(&pool, team_id).await but before calling
svc.generate_team_memory(...), re-fetch the current row/state (the same source
used to compute needs_generation/previous_memory) and re-check whether
generation is still required and whether previous_memory changed, and only call
MemoryServiceImpl::generate_team_memory(team_id, user.clone(), previous_memory)
when the re-check indicates generation is still needed; if not needed, log and
return. Ensure this re-check uses the same repo/db pool and the same semantics
as the original needs_generation computation so the advisory lock enforces
deduplication rather than just serialization.

Comment on lines +128 to +130
match svc
.generate_team_memory(team_id, user.clone(), previous_memory)
.await

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Shared team memory is generated with one member's permissions.

This flow researches workspace artifacts using the triggering user, then persists the result under team_id and serves that same summary to every teammate. If one member can see private docs/channels/emails that another teammate cannot, the generated team memory becomes a cross-user data leak.

Also applies to: 146-153, 209-210

🤖 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 `@rust/cloud-storage/memory/src/domain/service/team.rs` around lines 128 - 130,
The code calls svc.generate_team_memory(team_id, user.clone(), previous_memory)
using the triggering user's permissions, which risks leaking artifacts private
to that user into a shared team memory; update the call and implementation to
use a team-scoped visibility check instead of a single user's view by (a)
changing generate_team_memory to accept the team's membership list or a
team-permission context (e.g., team_id plus Vec<member_ids> or a TeamPermission
object) and (b) inside generate_team_memory only include artifacts visible to
every teammate (compute intersection of visible artifact IDs) or otherwise
restrict to artifacts explicitly marked as team-visible; update every call site
(the current svc.generate_team_memory invocations and analogous calls at the
other noted locations) to pass the team membership/permission context so the
persisted team memory contains only data safe for all members.

Comment on lines 125 to 131
let mut system_prompt = match memories.user {
Some(memory) => format!(
"{}\n{}\n<user_memory>\n{}\n</user_memory>\n{}",
tools.prompt, SCHEDULED_AGENT_PROMPT, memory, agent_task.prompt
"{}\n{}\n<user_memory>\n{}\n</user_memory>",
tools.prompt, SCHEDULED_AGENT_PROMPT, memory
),
None => format!("{}\n{}", tools.prompt, agent_task.prompt),
None => tools.prompt.to_string(),
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Preserve SCHEDULED_AGENT_PROMPT when user memory is missing.

Line 130 drops SCHEDULED_AGENT_PROMPT in the None branch, so scheduled runs without personal memory lose required guardrail instructions.

Suggested fix
-    let mut system_prompt = match memories.user {
-        Some(memory) => format!(
-            "{}\n{}\n<user_memory>\n{}\n</user_memory>",
-            tools.prompt, SCHEDULED_AGENT_PROMPT, memory
-        ),
-        None => tools.prompt.to_string(),
-    };
+    let mut system_prompt = format!("{}\n{}", tools.prompt, SCHEDULED_AGENT_PROMPT);
+    if let Some(memory) = memories.user {
+        system_prompt.push_str(&format!("\n<user_memory>\n{memory}\n</user_memory>"));
+    }
🤖 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
`@rust/cloud-storage/scheduled_action/src/outbound/inprocess_executor/agent_task.rs`
around lines 125 - 131, The None branch for building system_prompt drops
SCHEDULED_AGENT_PROMPT so scheduled runs lose guardrails; update the None branch
that currently returns tools.prompt.to_string() to include
SCHEDULED_AGENT_PROMPT (e.g., concatenate tools.prompt and
SCHEDULED_AGENT_PROMPT with a newline) so system_prompt always contains both
tools.prompt and SCHEDULED_AGENT_PROMPT even when memories.user is None; modify
the code around system_prompt, memories.user, SCHEDULED_AGENT_PROMPT and
tools.prompt accordingly.

- get_or_generate_memory returns whichever scope resolved instead of
  failing both when one errors
- re-check team memory staleness after acquiring the advisory lock so
  it deduplicates rather than merely serializes generations
- scheduled agent runs keep SCHEDULED_AGENT_PROMPT when no user memory
  exists
- document the team memory repo trait and Postgres impl

https://claude.ai/code/session_01CyfX47q3gqFp49pLrcco8b

jbecke commented Jun 10, 2026

Copy link
Copy Markdown
Contributor Author

Status of the review findings as of 73e1af5:

  • Fixed: all-or-nothing memory retrieval, staleness re-check under the advisory lock, missing SCHEDULED_AGENT_PROMPT guardrail, missing doc comments.
  • Skipped: migration idempotency guards — sqlx tracks applied migrations, and the repo's existing migrations follow the same plain-DDL style.
  • Open: team memory generated with the triggering member's permissions. Valid finding; @jbecke raised the same concern and the permission model (restrict research to team-visible artifacts vs. accept member-view generation) is being decided before this merges.

Generated by Claude Code

@jbecke jbecke changed the title Add team-level memory generation and retrieval feat(memory): add team-level memory attached to the team object Jun 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants