Jet

Memory Layer

Context is what's active in an agent session. Memory is what persists. Jet's memory layer has two shapes: an append-only log of raw events, and a curated store of summarized entries. A daily compactor folds old events into summaries via the LLM so memory stays rich and lean indefinitely.

The two tables

memory_events — the raw log

Every meaningful moment gets an event: a call summary, a strategic decision, a campaign change, a sentiment shift. Automations and webhooks append to this table as things happen. Events are immutable and scoped.

  • scopeinstitutional | entity | project | team
  • scope_ref — entity_id / project_id / user_id depending on scope
  • kindcall_summary, decision, campaign_change, ...
  • body — full raw content (Markdown)
  • source + source_ref — where it came from (inbox_message_id, automation_run_id, ...)
  • occurred_at — when it happened
  • compressed_at — set by the compactor when the event is folded into a summary

memory_entries — curated summaries

This is what the context loader can reference when loading memory into a session. Each entry is a discrete, searchable, scored memory item.

  • scope + scope_ref — same shape as events
  • title — one-line identifier
  • summary — Markdown content
  • tags — JSON array, used for filtering
  • importance — 1 to 5; retention priority when compressing
  • sourcechat | call | email | manual | compactor

The daily compactor

At 06:17 UTC every day, services/memoryCompactor.ts fires across every active tenant. For each (scope, scope_ref) bucket with three or more uncompressed events older than 90 days, the compactor:

  1. Pulls all uncompressed events in that bucket
  2. Sends them to the LLM with a compression prompt: preserve every strategic decision, relationship dynamic, and recurring pattern; drop the meeting-by-meeting prose
  3. Inserts the summary as a new memory_entries row with source='compactor' and importance=4
  4. Marks the source events as compressed_at so they won't be re-processed

Events are not deleted when compressed. They stay in the log for forensics and re-summarization. The context loader just ignores compressed events — it pulls from memory_entries instead.

API reference

  • GET /memory?scope=entity&scopeRef=ent_acme&tag=decision&search=ROAS — search curated entries
  • POST /memory — create a curated entry (usually from skills)
  • GET / PUT / DELETE /memory/:id — CRUD
  • POST /memory/events — append a raw event (called by automations, webhooks, call-summary skills)
  • GET /memory/events?scope=&scopeRef= — list raw events

How skills should use memory

The canonical pattern: a skill that processes an external event (a call transcript, an inbound email, a campaign change) should:

  1. Do its primary job (summarize, analyze, draft a response)
  2. POST /memory/events with the structured output as the event body
  3. Optionally POST /memory directly if the output is immediately useful as a curated entry

The Call Summary + Action Items skill in the marketing-agency pack does exactly this: it writes a call_summary event scoped to the client entity, giving every future session access to what was said.

Privacy + isolation

Memory lives in the tenant's isolated D1. It never crosses tenant boundaries. The compactor uses the tenant's configured gateway + model routing rules, so the summarization runs under the same DLP and budget policies as any other inference.

Next steps