A retrieval system needs to know who will read the answer
Adding a reader to a conversation changes more than the next reply's destination. In our Commander implementation, it changes the audience fingerprint and the model-session identity. An existing grant is refused when its recomputed audience no longer matches.
That is the boundary I want a retrieval system to preserve. A requester may be allowed to read a private document while the people receiving the answer are not. Finding relevant material under the requester's identity answers only the first permission question.
I separate three decisions: what the requesting person can read, what may enter the context for this audience, and what may be delivered to the current destination. Their inputs can change while an agent is working. A long-lived conversation summary or reused model session can carry material forward after the original access decision has become inappropriate.
The code gives us concrete mechanisms for audience-safe conversation scope, history floors and session partitioning. The wider information-flow design adds source ownership and disclosure rules around summaries, tools and shared writes. Keeping those layers distinct makes both the implementation and the remaining work easier to assess.
Start with the destination, not just the search principal
Imagine two people working in the same organization. Each can read a shared project thread. One can also read a private planning conversation that contains information the other person should not receive.
If the first person asks an assistant a question in their own private thread, retrieval may legitimately use that private source under the applicable policy. Asking the same question in a shared conversation changes the disclosure decision. The requester's broad access does not automatically become the audience's access.
This is an illustrative example, but it explains why I want a retrieval request to carry a destination contract: organization, requesting person, destination conversation, current human audience and relevant history boundaries. The model should receive a context packet selected for that contract.
The alternative is to retrieve broadly and ask the model to omit restricted details. That puts the model in possession of material the answer audience cannot receive and relies on it to maintain a boundary through reasoning, paraphrase and tool use. Filtering before model input removes an unnecessary disclosure path.
The destination also matters after retrieval. A private draft copied into a team conversation is a new disclosure action, even if the text was generated during an authorized private turn.
Compute what the current audience can read
Our turnScope implementation starts from live conversation access and current member rows. The requester must be present in the audience. For an ordinary scoped turn, the code obtains each human's readable conversation set and intersects those sets.
The useful set operation is simple. If one person can read private-A and shared-AB, and another can read private-B and shared-AB, their common source set contains shared-AB. A union would preserve everything either person can read; that is the wrong operation for a shared answer.
The surrounding branches matter. A project-bound thread narrows eligible conversations to its project. A private general context has separate conditions around ownership and sharing. A guest path performs access checks against candidate conversations and skips recognized access denials; unexpected errors are propagated rather than quietly treated as ordinary invisibility.
The current conversation is included after its access conditions are evaluated. The result is sorted before it becomes the conversation scope. Project scope is retained alongside it. That is a concrete implementation of audience-aware conversation selection, not a claim that every retrieval backend already consumes the complete policy.
I would preserve the same contract when adding another source type. A file search, vector index, memory store or tool response needs an authorization-aware source selection path appropriate to its data model. The conversation intersection cannot silently certify those other paths.
History access is part of the audience
Membership does not necessarily grant access to all earlier messages.
The history-floor implementation represents an inclusive first readable sequence. A newly added member may see messages only from a later point. For a shared assistant context, the relevant boundary is the highest applicable floor, so the context does not expose history unavailable to one of the current readers.
The SQL helper is intended to apply that predicate inside the query before LIMIT, ranking, aggregation or serialization. Ordering matters. Filtering after a top-k result has already been assembled can change the effective result set, counts and summaries, and it can place restricted material in an intermediate packet.
The code also distinguishes a Commander turn from an ordinary personal viewer. During a turn, supplying a recipient ID does not select that recipient's broader personal floor in place of the current turn audience. A transport or delivery parameter should not become an accidental way to widen the context.
The missing-membership sentinel and current audience calculation are implementation details that need actual database tests. The accompanying pure helper exercise does not execute these SQL predicates. What it does execute is the fingerprint that carries each member's history boundary into the session relationship.
Fingerprint membership and floors together
The audience fingerprint is small enough to show completely:
export function audienceFingerprint(
audience: readonly { userId: string; floor: number | string }[],
) {
return createHash("sha256")
.update(
JSON.stringify(
[...audience]
.sort((a, b) => a.userId.localeCompare(b.userId))
.map((u) => [u.userId, String(u.floor)]),
),
)
.digest("hex");
}
The copied array keeps the helper from mutating the caller's order. Sorting makes ordinary membership order irrelevant. Converting floors to strings keeps a canonical number and its matching string representation aligned across transports. A changed member or changed floor changes the serialized input.
This helper assumes a validated audience representation. It does not validate a grant, authorize a reader or turn an empty audience into a permitted destination. It does not numerically normalize every possible string: a noncanonical floor string can differ from its numeric equivalent. The membership query and surrounding admission contract remain responsible for producing the intended input.
That distinction prevents a useful correlation value from acquiring authority it does not have. The digest identifies a particular audience representation. The system still needs to establish who those members are, which access they hold and when the representation was computed.
The exercise includes member addition, removal and replacement, floor changes, input ordering, large integer strings and serialization boundaries. Those cases test the actual helper, rather than a separate hand-written description of what a fingerprint should do.
Partition the model session at the same boundary
An authorized source query is insufficient if the next model request reuses a session containing broader private context.
The bridge's actual session helper includes the audience when a requester-bound lease supplies it:
export function commanderTurnSessionId(orgId, threadKey, salt, epoch, lease) {
const audience = commanderSessionAudience(lease);
return stableUuid(orgId, threadKey, salt, `epoch:${epoch}`, ...(audience ? [audience] : []));
}
Organization, thread key, salt and epoch remain part of the namespace. The audience component partitions the session when the granted audience changes. Legacy user-custody sessions retain their pre-grant identifier where the migration path permits them.
The underlying stableUuid is a deterministic, UUID-shaped identifier derived from a hash. It truncates and fixes some bits; it is not the full SHA-256 digest, a secret or an authorization credential. Its delimiter also assumes suitable input domains. The exercise demonstrates that NUL-bearing arbitrary parts can alias under the delimiter scheme, so I do not present it as a universal canonical serialization function.
Session partitioning reduces accidental reuse of incompatible context. It does not prove a provider deleted the previous session, purged every cache or prevented an operator from moving a summary into a new conversation. Those are separate lifecycle and disclosure requirements.
I want every application-level context cache to name the authorization-relevant inputs it depends on. If a component omits an audience, history floor or policy revision that can change readability, the cache needs its own invalidation or reauthorization contract.
Refusal must not become a legacy fallback
The migration path deserves as much attention as the new session key.
The actual requestTurnGrant helper first resolves the persona identity and custody. Service custody requires a requester grant. Supported turn kinds proceed to the grant endpoint, and a returned packet must include the expected token shape, turn identity, lease owner and audience hash shape.
For an eligible legacy client, the helper permits a narrow fallback when the old server's grant endpoint is absent. An explicit denial is different. A 403, 409 or server failure must not be reinterpreted as permission to continue through the older credential path. Service custody does not get the legacy missing-endpoint escape.
The exercise checks those distinctions with injected API responses. It also checks malformed token prefixes, lengths and characters; missing turn fields; and invalid audience hash representations. No real credential is used. The fake client records which request was attempted, so a case can establish that refusal happened before another mint operation.
These checks are packet admission, not a complete live authorization decision. A well-shaped token can still be expired, revoked or unrelated to the current work. The server must evaluate its current meaning when the tool request arrives.
Recheck during the turn and at delivery
A grant admitted for one audience should not survive an audience change merely because the model is already thinking.
The live grant implementation checks the token and lease, requester/grant relationship, work state, expiry and organization execution guard. It evaluates the designated service identity and stop conditions, then recomputes the turn scope. A mismatch with the recorded audience hash refuses the grant.
That makes joining, leaving or changing relevant history access consequential to the current work. It also preserves a useful operating outcome: the work may need to stop, reconcile or obtain a newly admitted context rather than silently continuing under old assumptions.
The final delivery deserves the same current-state discipline. If a conversation becomes shared after a private answer was prepared, the output cannot be treated as already cleared for its new readers. An approval, result packet or queued message needs to remain bound to the payload and destination that were actually evaluated.
The pure helper tests do not establish all delivery routes. I would qualify each maintained delivery interface with an audience-change case at the relevant boundary, including a valid unchanged-audience positive. The model's statement that a reply is safe is not the delivery verdict.
Group mentions have a deliberately smaller scope
The implementation has a narrow path for a mention in a group conversation without an AI thread.
That mention was admitted for the current conversation. Its conversation scope is restricted to that conversation rather than expanding into everything the mentioner or other members could individually read. The source skips broader per-member visibility queries in this path because their results would be replaced by the single admitted conversation.
The audience fingerprint remains. A membership change can still invalidate the grant while the turn runs. Narrowing retrieval and maintaining current membership are complementary decisions.
This is a useful example of optimizing around the actual permission contract. The code avoids work that cannot affect the permitted result, while preserving the boundary that still matters. I would measure the effect before claiming a latency improvement; the implementation alone establishes the query-path choice.
For a product team, the rule is easier to explain than a global permission promise: a mention in this group can work from this conversation. Access to another project or private thread requires its own admitted route.
Carry source meaning through summaries and tools
A conversation intersection does not solve every way private information can move.
A retrieved source can become a model summary, a memory entry, a tool argument, an issue description or an outbound draft. If the original ownership and disclosure boundary disappear during that transformation, the next component may treat derived text as ordinary unrestricted context.
The wider information-flow proposal introduces source envelopes and server-side trust and audience rules. A useful envelope would preserve source identity and revision, origin, ownership, sensitivity, permitted audience, history boundary and the authorization decision under which it was included. A summary should retain the restrictive meaning relevant to its sources.
That proposal needs implementation at the actual read, transform and write interfaces. Adding a label to the prompt is insufficient if a shared-write endpoint ignores it. Likewise, a cache that stores only fluent summary text can lose the conditions needed to authorize reuse.
I would start with one complete chain rather than a promise that every artifact is taint-tracked. Select a source, produce a derived packet, attempt one shared disclosure and inspect the maintained server decision. Then expand the coverage with source-linked evidence for each additional route.
Give context reuse a recovery decision
When an audience check changes, the product needs a useful next state. A held answer should explain that its context must be rebuilt for the current readers. It should not silently reopen an older broader session because the narrow one lacks history.
I would give that transition an owner and an explicit recovery action:discard the prepared output, retrieve the permitted sources again, construct the current context and produce a new answer. If an external effect already happened, rebuilding context does not undo it; that effect needs reconciliation under its own identity.
This is a proposed operating contract around the implemented checks. It keeps a refusal understandable to the user and prevents convenience behavior from restoring the access that the current decision just removed.
Give the product an audience-change acceptance case
The acceptance fixture needs at least two readers with deliberately different access, a shared source, private sources and distinct history floors.
First prove the positive journeys. Each person can read what their role permits, and the shared assistant can answer from the material common to its current audience. A refusal-only fixture could conceal a service that never retrieves anything useful.
Then change one condition at a time: add a participant, remove a participant, advance a history floor, revoke source access, edit a source or move the answer to another destination. Inspect source selection, session identity and final delivery separately. The appropriate negative control should make the detector observe the forbidden reuse or disclosure.
The implementation already gives us useful places to attach those tests: live scope calculation, audience fingerprint, model session construction, grant validation and delivery. The remaining source types and derived-content flows need their own coverage instead of borrowing confidence from those mechanisms.
Take one question your assistant answers today. Name the requester, every intended reader and the sources that influenced the response. Then ask which current decision establishes that those readers may receive that answer. That is the contract a retrieval system should be able to explain.
How this was verified
Five complete helper declarations are retained verbatim from bugtracker18e32f8a: audienceFingerprint, requestTurnGrant, commanderSessionAudience, stableUuid and commanderTurnSessionId. The accompanying 59-case exercise uses synthetic API responses and context values, with three author fault controls and a clean restored run. The original session and grant Node suites pass 23 tests in a pinned tools/shared source copy; a deliberate removal of the real bridge audience component fails those tests and restoration passes. Test dependencies use the original five direct package versions with a separately committed npm transitive lock, not the complete original pnpm environment. Live SQL scope/history enforcement, all delivery routes, provider cache deletion, source-flow label coverage and production activation are not established by those helper tests. The older turn-grant document describes an earlier slice; the retained current implementation supplies the later history-floor context. Broader source-envelope rules remain a proposal.
An independent reviewer designed 27mutations from the source alone before reading the cases (set 1316914). The initial 55-case exercise detected 24 of 27 with control exit zero (receipt 01c9a63). Four additional cases distinguish missing custody, team-turn handling and an absent audience hash. The 59-case same-set follow-up detected 27 of 27 with control exit zero (receipt 51f41a5). The source hashes stayed identical. The original result remains 24/27; the follow-up is not a fresh independent score.
