Skip to content

Promotion and sharing

Two different things in aizk are called promotion. One moves a note from fast working memory into the real graph, and one copies a document from one scope set into another. They share a verb and a few column names and nothing else. This page assumes you know what a scope set is and roughly how intake turns text into a document with chunks.

session_item is the fast tier. A write that should be available immediately but does not deserve a full extraction pass lands there first. The table is small.

Diagram

The row carries its own embedding, so an item is retrievable the moment it is written. The vector lane reads it directly and filters on promoted_at IS NULL, so an item is served from working memory until the graph has a better version of it and never from both at once.

SessionItem.due_for_promotion() decides what is ready, and it decides in SQL with two window functions over the still-working items of one exact scope set. An item is due when it is older than session_promote_age_minutes, which is 60, or when it sits among the oldest items past session_promote_threshold, which is 20. The second condition is the overflow rule, so a burst of fifty notes in one minute does not sit around waiting for an hour, and results come back oldest first either way.

promote_sessions() in src/aizk/graph/session_tier.py runs every 15 minutes under SessionPromoteJob. It takes the due items, rebuilds each one’s CaptureContext from its stored provenance, and hands them all to ingest_texts, which is the same intake path a normal write uses. Then it stamps promoted_at so the items stop being offered and stop appearing in the session lane, and calls enqueue_pending() so the new chunks get projected into the graph. The item row itself stays, since it is the record of what was captured.

promote() in src/aizk/graph/promote.py is the implementation behind the share MCP tool and behind aizk data promote on the CLI. Memory.share resolves the destination through user.write_scope(scopes) first, so a caller can only ever name a scope set they may write to, and then the whole pass runs inside the caller’s own session rather than as the system user. Row security therefore enforces both halves, since an invisible source document simply is not there to read and an unauthorized destination fails on insert.

source document (private scope)
already shared into target? --- yes --> skip (promoted_from is unique per scope)
| no
v
read live facts sourced from this document's chunks
-> copy chunks into the target scope (text + embedding included)
-> Artifact.share: new scoped metadata over the same blob
-> new document, promoted_from = source.id
-> claim every subject and object entity in the target
-> claim each fact content in the target (source_chunk_id = the copy)

Each step is a copy, not a reference. The new document row repeats the title, subject type, source URI, observed and expiry timestamps and content hash, and records promoted_from pointing at the original. Every chunk is duplicated into the destination scope with its text, lexical vector, token count, provenance and embedding intact, and the copies are kept keyed by their source chunk ID so the facts can be rewired to them.

Facts and entities do not get duplicated, because content is already global and scope-free. Only a new claim is written in the destination for each one, carrying the original’s valid range, attributes and perspective key, pointing its source_chunk_id at the copied chunk, and recording promoted_from as the source claim. That is the whole provenance link, and it is what lets the retrieval side give shared documents a small promoted_bonus of 0.01 when fusing chunk ranks.

Sharing twice is a no-op. Before doing anything the pass looks for a document already carrying promoted_from = source.id in the target scopes and skips when it finds one, and the artifact tables carry a matching uq_artifact_promotion_scope constraint.

Why an artifact share reuses the original bytes

Section titled “Why an artifact share reuses the original bytes”

Artifact.share is the one place that deliberately does not copy. A blob is content-addressed and immutable, so duplicating a whole PDF just to change who may read it would be pure waste. Instead the destination gets its own artifact and artifact_content rows, scoped to the target, whose blob_id points at the very same stored object. Visibility lives entirely on the metadata rows, so two scopes can hold independent permissions over one physical file.

The method takes a transaction-scoped advisory lock keyed by the destination identity, then looks for an existing target artifact by promoted_from or by matching source_uri, reuses an existing revision when one already references the same blob, and otherwise appends at max(revision) + 1. The lock is what makes the lookup, the blob dedup and the revision number safe against a concurrent share of the same document. It also fetches the artifact and its content joined together, so a forged pair whose artifact_content_id belongs to some other artifact cannot smuggle foreign provenance into the destination.

That is a deliberate trade. A live link would mean a private correction silently rewriting what an organization already read, and the destination’s visibility depending on a row it cannot see. A snapshot keeps both scopes independently readable and correctable, and the promoted_from columns are still there when you need to know where something came from. Re-sharing does not refresh either, since the duplicate guard skips it.