Skip to main content
By default a memory is scoped to the user_id that ingested it. Groups let you share memory across users: tag a memory to a group at ingest, and anyone who searches that group can see it. The motivating case is collaborative agents — e.g. a travel-planning assistant where each trip is a group. Every traveler’s AI tags trip-relevant facts to the trip’s group, so the whole party shares one evolving picture (hotels, restaurants, dates) while each person’s unrelated personal memories stay private.

The model

  • A group is a registry entry with a name and an optional prompt. The prompt determines the group’s mode:
    • Prompted — the group has a prompt describing what belongs in it. The ingest classifier reads it and tags a memory only when the prompt clearly applies.
    • Catch-all — the group has no prompt. It receives every extracted memory judged shareable, with no per-group matching.
  • The personal gate applies to every group. Before any tagging, the classifier judges each extracted memory personal vs. shareable; personal memories are never group-tagged, whatever the group’s mode. See The personal gate.
  • Group ids are server-generated, unguessable handles (grp_…). Knowing the id is the access boundary — your client decides which users belong to which groups and which ids to send; the memory service doesn’t track membership.
  • Tagging is additive: tagging a memory to a group never removes it from the author’s own scope.

1. Register a group

Prompted — pass a prompt describing what belongs:
Write the prompt the way you’d brief the classifier: concrete about what to include. A sharp prompt (“facts about the Tokyo trip”) tags precisely; a vague one over- or under-tags. Catch-all — omit the prompt entirely (or send null):
Only omitting prompt (or sending null) creates a catch-all. An empty string is rejected with 422 — a blank field from a buggy client can’t silently turn a group into a catch-all.

2. Tag memories at ingest

Pass the group ids in group_ids. At extraction time the classifier tags each extracted memory:
  • Each shareable extracted memory is tagged with the prompted groups its content matches plus every requested catch-all group — several, one, or none. (“Stays near Shibuya” → the Tokyo trip and the workspace; an off-topic but shareable aside → the workspace only.)
  • Personal memories are never tagged to any group — see the personal gate.
  • Unknown or archived ids are soft-skipped (never fail the ingest) and returned in result.ignored_group_ids.
  • Up to 20 group ids per ingest; more returns 422.

The personal gate

Every extracted memory is first judged personal vs. shareable, and personal memories are never group-tagged — not by prompted groups, not by catch-alls. This is the privacy backstop that makes catch-alls safe: passing a group id doesn’t mean everything in the conversation crosses to the group.
  • Personal means private or sensitive circumstances: health, family matters, private feelings, finances, credentials.
  • Merely being about the user does not make a memory personal. “Alice is vegetarian” on a shared-trip ingest is shareable — the group needs it to plan dinner. “Alice’s father is in the hospital” is personal, even if it came up while discussing trip dates.
  • Personal memories are not dropped. They’re stored in the authoring user’s personal scope like any untagged memory and remain fully retrievable there (search({ user_id }), recall). The gate only controls sharing.
  • The gate fails closed: if the personal-vs-shareable verdict is unavailable, the memory is left untagged rather than shared.

3. Read shared memories back

The whole group, across every member — omit user_id, pass group_ids:
A user’s own memories plus the group’s shared memories, in one ready-to-inject prompt — use recall:
recall sections the prompt by Personal + the group’s name and attributes each shared line to its author (you: for the caller, <user_id>: for fellow members). See Searching memories for the full picture, including how AND-scoping makes { user_id, group_ids } on a plain search an intersection rather than a union.
Group membership matching is any-of: pass group_ids: [tripA, tripB] to search across both trips at once (a user can belong to many groups).

Managing groups

Prompt edits are a one-way door:
  • Adding a prompt to a catch-all group is allowed — it becomes a prompted group from then on, and new ingests match against the prompt.
  • Removing a prompt is not: null on update means “keep the existing prompt”, and an empty string is a 422. Once a group is prompted, it stays prompted.
  • Prompt changes never retroactively re-tag existing memories — they only steer future ingests.
Archiving is soft (status becomes "archived"): the group stays readable, but it’s dropped from future ingest tagging — a stale id passed on ingest just lands in ignored_group_ids. Re-activate by update(id, { status: 'active' }).

Best practices

Pick the mode that matches the ingest. A catch-all fits a shared workspace or trip bucket where the conversations you ingest are already about the shared context — everything shareable from them belongs to the group, so per-memory matching would only lose facts. A prompted group fits a curated topical slice — the conversations mix concerns and only some of it belongs (e.g. a team channel’s ingests feeding a narrow “on-call runbook facts” group). Expect lower search precision on catch-alls — by design. A catch-all trades precision for recall: it accumulates everything shareable from its ingests, so a group-scoped search over it surfaces more loosely related neighbors than a search over a sharply prompted group. If group-search quality matters more than capture-everything coverage, use a prompted group. Model a group around a real shared boundary. A group should map to something multiple users genuinely share — a trip, a project, a workspace channel — not a single user (that’s what user_id is for) and not a throwaway topic. If only one person will ever read it, it doesn’t need to be a group. For prompted groups, write the prompt like a brief to the classifier. It’s the only signal used to decide what gets tagged. Name the subject so unrelated facts don’t leak in: Send only the groups the user is actually in. Your app owns membership; pass the group_ids relevant to this conversation, not every group in the org. This matters doubly for catch-alls: every shareable memory lands in every catch-all you pass, so a stray catch-all id pollutes that group with an unrelated conversation. (Max 20 per ingest.) Don’t lean on the personal gate for topical scoping. The gate keeps private and sensitive content (health, family, finances, credentials) out of all groups server-side — but it’s a privacy backstop, not a relevance filter. An off-topic but shareable fact still lands in a catch-all. Scope which ids you send — and prompted groups’ prompts — to control topic; let the gate handle privacy. Pick the right read for the job:
  • An agent serving one user inside a grouprecall({ pools: [{ user_id }, { group_ids }] }) — their own context plus the group’s, deduped and attributed.
  • A whole-group overview (“what does the trip know?”) → search({ group_ids }), omit user_id.
  • Don’t expect search({ user_id, group_ids }) to union — it’s an intersection (see Searching memories).
Treat tagging as best-effort. Whether a fact lands in a group is an LLM relevance call, not a guarantee — and the personal gate fails closed, leaving a memory untagged when its verdict is unavailable. Don’t build logic that requires a specific fact to be tagged. Always read result.ignored_group_ids to catch unknown or archived ids. Group ids are the access boundary. They’re unguessable (grp_…) and the service doesn’t track membership — your app decides who can read a group. Don’t expose a group id to anyone who shouldn’t see the group, and don’t tag anything you wouldn’t want every member to read — especially a catch-all, where everything shareable crosses over. Archive groups when they’re done. A finished trip shouldn’t keep collecting tags; archive it to stop new tagging while keeping its history readable.

See also