Skip to main content
Requires maximem-synap 0.5.1 or newer (Python) and @maximem/synap-js-sdk 0.5.1 or newer (JS). Earlier versions do not have this method, and 0.5.0 loses events sent immediately after listen().
Every event your agent reports carries an id, and the platform acknowledges the ids it has taken responsibility for. That pair is what lets the SDK retry safely: without an id there is nothing to recognise a repeat by, and without an acknowledgement there is nothing to tell a write that landed from one that died with the connection. You do not have to do anything to get this. The SDK mints the ids, holds what has not been acknowledged, and replays it.

What an acknowledgement means

An acknowledgement says the platform has the event: recorded so a repeat is recognised, queued for anticipation, and for conversation turns handed to the writer that persists them.
It does not mean a memory exists. Extraction is asynchronous and happens later, after compaction. An acknowledgement is about delivery, nothing else.

When the stream drops

Two things can be in flight when a connection goes:
  • Events you sent while there was no stream. They are buffered and sent when one comes back.
  • Events that went out but were never acknowledged. The write succeeded and the stream broke before the platform confirmed it. These are replayed first, because they happened first.
Both carry the id they were first sent with. The platform drops a second sighting of an id before it does anything with it, so a replay of something it already processed costs nothing and cannot record the same turn twice. The buffer is bounded: 100 events, or five minutes, whichever comes first. Past that the oldest is dropped with a warning in your logs, because a buffer that grows without limit during an outage is its own failure.

Across a restart

The buffer is also written to disk, so a process that dies with events still in it does not lose them. Both SDKs append each change to a journal file, ~/.synap/outbox/<instance_id>.jsonl by default. The next time that instance calls listen(), the SDK reads the file back and sends what was left (events that were never acknowledged first, then the ones that never went out) before anything new. Each keeps its original id, so an event the platform had already processed is recognised and dropped.
  • Where the file lives. Set SYNAP_STORAGE_PATH to move it; the file is then <that path>/outbox/<instance_id>.jsonl. Python also takes a storage_path in its config, which wins over the variable. The JavaScript SDK reads the variable only.
  • What it holds. The event payloads, which include the text of conversation turns. Treat the directory as you would any place your process writes conversation text.
  • When it cannot be written. If the disk is read-only or full, the SDK logs one warning and keeps buffering in memory only, so those events do not survive a restart.
  • It is local to the machine. There is one file per instance id, in the home directory (or storage path) of the user running your process. A replacement process on another machine, or under another user, does not see it.
send_message() and the record_* methods do not raise while the stream is reconnecting. They buffer. They raise only if you never started a stream, or you stopped it.

Closing cleanly

stop_listening() flushes what is still buffered before the stream goes away, with a short deadline, and closes any session it opened. If your process exits abruptly instead, whatever was still buffered is in the journal file and goes out on the next listen() from the same machine and home directory. Call stop_listening() on your shutdown path anyway, so events are delivered now rather than on the next start.

Processes that cannot hold a stream

The gRPC stream needs a connection held open for the length of a conversation. A serverless function, a short-lived worker, or anything that suspends between requests cannot do that, so none of the delivery behaviour above applies to it: there is no stream to buffer for. Such a process reports conversation turns with conversation.record_message instead, one request per turn, which stores the turn before it returns. The cost is that pushed anticipation bundles need the stream, so a process without one fetches context the ordinary way, with context.fetch. See gRPC vs REST for the full comparison.

See also