Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
d4588b0
feat(channel-sdk): memoryContextKey — injective context-key derivation
Weegy Aug 26, 2026
6a08dcf
feat: ContextMemoryNamespacer — per-context privateRoot + reserved ~t…
Weegy Aug 26, 2026
1311792
feat: memoryPromote service — audited copy/move between an agent's me…
Weegy Aug 26, 2026
5d80428
feat: Danger-Zone purge reaches the chat-context memory trees (W5, #860)
Weegy Aug 26, 2026
fc6f9a8
feat: ScopedMemoryStore context scope grammar (team/channel/user + ro:)
Weegy Aug 26, 2026
24140b9
feat(channel-sdk): TurnOrigin contract + memoryAxesForOrigin (W5 memo…
Weegy Aug 26, 2026
aff6eb1
feat(orchestrator): effectiveMemoryScope — static ∩ dynamic scope, fa…
Weegy Aug 26, 2026
99eb45a
chore: merge W5 promote-service unit — the route factory's dependency
Weegy Aug 26, 2026
c4fda97
feat: operator memory-promotion router behind the danger-zone auth gate
Weegy Aug 26, 2026
2b471a1
chore: merge W5 TurnOrigin contract — the producer cannot compile wit…
Weegy Aug 26, 2026
fd63c9c
feat(channel-sdk): export the TurnOrigin contract from the package root
Weegy Aug 26, 2026
25ad36f
feat: MemoryBinder — LRU-cached per-context memory stack with team/ch…
Weegy Aug 26, 2026
a8566be
feat: web-ui operator surface for the chat-context memory ACL
Weegy Aug 26, 2026
7f56a1f
Merge branch 'feat/w5-memory-acl-w5-memory-acl-turn-origin-contract' …
Weegy Aug 27, 2026
840a0d6
Merge branch 'feat/w5-memory-acl-w5-memory-acl-teams-origin-producer'…
Weegy Aug 27, 2026
ba66eef
Merge branch 'feat/w5-memory-acl-w5-memory-acl-scope-grammar-contexts…
Weegy Aug 27, 2026
7a27e47
Merge branch 'feat/w5-memory-acl-w5-memory-acl-effective-scope-fail-c…
Weegy Aug 27, 2026
84afb68
Merge branch 'feat/w5-memory-acl-w5-memory-acl-context-namespacer' in…
Weegy Aug 27, 2026
106d839
Merge branch 'feat/w5-memory-acl-w5-memory-acl-memory-binder' into fe…
Weegy Aug 27, 2026
6dbf65f
Merge branch 'feat/w5-memory-acl-w5-memory-acl-purge-context-axes' in…
Weegy Aug 27, 2026
0bfeba0
Merge branch 'feat/w5-memory-acl-w5-memory-acl-promote-route' into fe…
Weegy Aug 27, 2026
7ee4d70
Merge branch 'feat/w5-memory-acl-w5-memory-acl-web-ui-context-browser…
Weegy Aug 27, 2026
6c44efe
refactor(w5): reconcile the merged units onto one contract
Weegy Aug 27, 2026
3e634ff
feat(w5): the three missing units — turn threading, HTTP fail-closed,…
Weegy Aug 27, 2026
95e294d
fix(w5): close the review findings from the unit branches
Weegy Aug 27, 2026
92c11f2
docs(w5): handoff section + CHANGELOG entry for the chat-context memo…
Weegy Aug 27, 2026
4a75dfa
fix(w5): make purgeMemory's selector optional, matching previewMemory…
Weegy Aug 27, 2026
7afc389
docs(w5): correct the effectiveMemoryScope JSDoc scope table
Weegy Aug 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,52 @@ entry. See `CONTRIBUTING.md` § Releases & changelog.

## [Unreleased]

### Added — chat-context memory ACL: per-team/channel/user agent memory (#860 W5, design #870)

2026-08-27 — Agent memory was isolated per AGENT but not per CHAT CONTEXT. What an agent
learned in Teams team A landed in one agent-global tree and was quotable in team B on the
next turn. This wave partitions that tree by chat context, fail-closed.

- **Scope grammar.** `ScopedMemoryStore` gains `team:<ctxKey>:*`, `channel:<ctxKey>:*` and
`user:<ctxKey>:*`, plus an `ro:<pattern>` access modifier. The context trees live under a
NEW top-level segment `/memories/contexts/`, deliberately not under
`/memories/orchestrators/<slug>/`: `orchestrator:<slug>:*` matches only the agent tree, so
no legacy scope reaches a context tree and no context scope reaches the agent tree. `ro:`
is a veto rather than a weak grant — an overlapping pattern cannot silently re-grant write
to a path it protects.
- **Context key.** `memoryContextKey(channelType, nativeId)` is the single sanitiser behind
every `<ctxKey>`, every physical path, every purge selector and the promote route. A
lossless id passes through byte-identically; anything else keeps a readable stem plus a
64-bit digest of the RAW input, and the two output spaces are kept disjoint so an id
spelled like a digest cannot pre-image another context's tree. The axes derivation keys on
an injective tuple of the scope's structural parts, not on its wire form — `group:x` as a
group ref and as a conversation id are two contexts, not one.
- **Per-turn binding.** `MemoryBinder.forOrigin()` resolves one stack per chat context
(LRU-cached) at the start of each turn, and the orchestrator threads it to `dispatchTool`
as an explicit parameter — never through AsyncLocalStorage, where a generator resumed in
its caller's context would lose it silently and widen the scope rather than fail.
- **What a context turn may do.** Write its own tier; read the agent tier (`ro:`); read but
NOT write the shared trees (`core`, `sessions`, `chat-sessions`, `_*`), which are the one
model-facing surface two contexts address by the same path. New knowledge leaves a context
only through the operator promote action.
- **Operator surfaces.** The Danger-Zone purge axes `user`/`team`/`channel` get a scratch
footprint for the first time and delete the named context tree across every agent; a
selector without a `<channelType>~` half is now refused with `invalid_selector` instead of
silently matching nothing. New `POST|GET /api/v1/admin/memory/promotions/:slug` copies or
moves knowledge between an agent's tiers, on the same auth gate as purge, audited three
ways (JSONL log, provenance frontmatter, `[security-audit]` line). The memory browser gains
a context dimension, a promote dialog and an audit tab.
- **No flag day.** Per-agent `agents.context_memory` (`off` | `enforce` | `enforce-strict`,
migration 0050) defaults to `off`, and `ChatTurnInput.origin` is optional. Every
combination of old/new middleware and old/new channel plugin behaves exactly as it does
today until an operator switches an agent over; unknown and NULL flag values read as `off`.

Two properties are worth remembering because they cost a rewrite each. `formatSessionScope`
is injective only over the strings `parseSessionScope` emits, while the design has channel
adapters build scopes directly — so it cannot be used to key a security boundary. And a
sanitise-or-hash key function is not injective unless the two branches have disjoint output
spaces; without that, the hash branch is pre-imageable by anyone who can name their own id.

### Fixed — plugin ingest rejected the Teams app-package template (#860 W1a)

2026-08-26 — A store update to `@omadia/channel-teams` 0.21.0 failed on the live
Expand Down
165 changes: 165 additions & 0 deletions docs/middleware-agent-handoff.md
Original file line number Diff line number Diff line change
Expand Up @@ -2407,3 +2407,168 @@ Teams-Identity-Routen aus `operatorAgents.ts` (Datei > 800 Zeilen) bei nächster
(22), `test/operatorAgentsRouter.test.ts` (42), `test/agentTeamsIdentityStore.pg.test.ts`
(9, gegen echtes Postgres, wendet Migration 0049 doppelt an), `coreMigrations.pg.test.ts`
(Double-Apply aller 49 Files).

## Chat-Kontext-Memory-ACL (W5, #860 / #870, 2026-08-27)

**Das Problem.** Agent-Memory war pro AGENT isoliert (`ScopedMemoryStore` über
`['core', 'orchestrator:<slug>:*']`), nicht pro CHAT-KONTEXT. Was ein Agent in Teams-Team A
lernte, landete im agent-globalen Baum und war im nächsten Turn in Team B zitierbar. W5
partitioniert diesen Baum nach Chat-Kontext.

### Scope-Grammatik

`ScopedMemoryStore` versteht drei neue Tokens plus einen Modifier. Physische Wurzeln kommen
ausschliesslich aus `contextTierRoot(agentSlug, axis, ctxKey)` — eine zweite Schreibweise
wäre eine Partition, die der kompilierte Scope nicht gewährt:

| Token | matcht |
|---|---|
| `team:<ctxKey>:*` | `/memories/contexts/<slug>/team/<ctxKey>/…` |
| `channel:<ctxKey>:*` | `/memories/contexts/<slug>/channel/<ctxKey>/…` |
| `user:<ctxKey>:*` | `/memories/contexts/<slug>/user/<ctxKey>/…` |
| `ro:<pattern>` | Access-Modifier: read/list/exists ja, write/delete/rename → `MemoryScopeViolation` |

`/memories/contexts/` ist ein **neues Top-Level-Segment**, nicht ein Unterbaum von
`/memories/orchestrators/`. Das ist das strukturelle Kollisionsfreiheits-Argument:
`orchestrator:<slug>:*` matcht ausschliesslich den Agent-Baum, also erreicht kein Alt-Scope
einen Kontextbaum und kein Kontext-Scope den Agent-Baum. **Nicht "aufräumen".**

`ro:` ist ein **Veto**, kein schwaches Grant: matcht ein `ro:`-Pattern den Pfad, wird der
Write abgelehnt, auch wenn ein zweites Pattern ihn gewähren würde. Sonst re-öffnet jedes
überlappende Pattern still das Tier, das `ro:` quarantänisieren soll.

`/memories/core/audit/` ist für **jeden** Agent unbeschreibbar (Deny-Prefix vor jeder
positiven Prüfung). Dort liegt das Promote-Audit-Log; `core` ist ein Read/Write-Grant, das
jeder Agent hält, also könnte ein Agent ohne diesen Ausschnitt das Protokoll dessen
überschreiben, was ein Operator mit seinem Memory gemacht hat.

### Kontext-Key

`memoryContextKey(channelType, nativeId)` (`harness-channel-sdk/src/scopeId.ts`) ist der
**einzige** Sanitizer: `${channelType}~${safeKey(nativeId)}`. Jeder `<ctxKey>` in der
Grammatik, jeder physische Pfad, jeder Purge-Selector und die Promote-Route gehen da durch.
Ein Ad-hoc-`replace(/[^a-z0-9]/g,'-')` irgendwo anders reisst das Loch wieder auf, das
`scopeGraphKey` geschlossen hat: eine Teams-Conversation-Id ist `19:abc@thread.tacv2`, und
plain sanitisiert kollidiert sie mit dem Literal `19-abc-thread-tacv2`.

Zwei Eigenschaften sind sicherheitstragend:

- **Injektiv.** Ein bereits verlustfreier Id (`/^[a-z0-9_-]{1,64}$/`) geht byte-identisch
durch, alles andere bekommt Stem + 64-Bit-sha256-Digest des ROHEN Strings. Die beiden
Ausgaberäume sind **disjunkt** — ein Id, der wie ein Digest aussieht (`…-<16 hex>`), wird
selbst gehasht. Ohne das könnte jemand, der seine eigene Conversation-Id benennen kann,
den Key eines gehashten Kontexts vorbilden und in dessen Baum landen.
- **`~` liegt ausserhalb des Safe-Alphabets** → die Zerlegung ist eindeutig und ein Key kann
nie ein `:` tragen, das das `team:<key>:*`-Format bräche.

`memoryAxesForOrigin` keyt **nicht** auf `formatSessionScope(scope)`, sondern auf eine
injektive JSON-Tupel-Kodierung der strukturellen Scope-Teile. Die Wire-Form ist nur über der
Teilmenge injektiv, die `parseSessionScope` emittiert — und Adapter bauen Scopes direkt.
Sonst teilen sich `{kind:'group',groupRef:'x'}` und
`{kind:'conversation',conversationId:'group:x'}` ein Tier.

### Effective Scope (statisch ∩ dynamisch)

```
scope = axes.isContextFree
? ['core', `orchestrator:${slug}:*`] // exakt heute
: ['ro:core', `ro:orchestrator:${slug}:*`, …axes.patterns] // enforce
: ['ro:core', …axes.patterns] // enforce-strict
```

- **Fail-closed.** Fehlender `origin`, `unscoped`, `system`, unbekannter `channelType`,
unbrauchbare Patterns → Zeile 1 der Tabelle, byte-identisch zu heute, kein Kontextbaum
erreichbar. `axes.patterns` ist eine **Allowlist**: alles ausserhalb der drei Tier-Tokens
wird verworfen und geloggt, denn diese Liste kommt über eine Paketgrenze aus einem
unabhängig versionierten Channel-Plugin.
- **Agent-Tier ist read-only.** Sonst wäre "notiere das global" ein permanenter Leak-Kanal
von Team A nach Team B.
- **`ro:core`, nicht `core`.** Die Shared-Bäume (`core`, `sessions`, `chat-sessions`,
Top-Level `_*`) reicht der Namespacer unverändert durch — sie sind die EINE modellseitige
Fläche, die zwei Kontexte unter demselben Pfad ansprechen. Schreibbar wäre
`/memories/core/notes.md` ein Einzeiler-Bypass der ganzen ACL.
- **Nie ein Throw auf dem Message-Pfad.** Kaputte Axes degradieren auf den Agent-Privat-Scope
und loggen laut (`[security-audit]`) — in BEIDEN Modi, weil ein Plugin-Bug sonst unsichtbar
bleibt.

### Turn-Bindung

`MemoryBinder.forOrigin(origin)` liefert synchron und LRU-gecacht (Cap 256) den Stack
`DurableRulesMemoryStore?( ContextMemoryNamespacer( ScopedMemoryStore(scope, rootStore) ) )`.
Der Orchestrator ruft das **einmal am Turn-Anfang** und reicht das Ergebnis als **expliziten
Parameter** bis `dispatchToolInner` durch — ausdrücklich **nicht** über `turnContext`
(AsyncLocalStorage). Ein Generator wird im Async-Kontext seines Aufrufers fortgesetzt; genau
so hat `turnContext.enter` vor W3-A auf jedem Streaming-Turn den Kontext still verloren. Eine
so verlorene Bindung würde nicht fehlschlagen, sie würde leise den Scope weiten.

Modellseitig (nur im Kontext-Modus, sonst byte-identischer Prompt):

```
/memories/… → engstes Tier des Turns (Kanal bzw. User)
/memories/~team/… → Team-Tier (rw, nur wenn eine Team-Achse existiert)
/memories/~agent/… → Agent-Baum (ro; Enforcement macht der Store, nicht der Mapper)
```

`~` ist kollisionsfrei, weil der bestehende Namespacer nie `~`-Segmente nach aussen emittiert.

### Rollout

`agents.context_memory` (Migration `0050_agent_context_memory_flag.sql`), `off` | `enforce` |
`enforce-strict`, **Default `off`**. `off` plus optionales `origin` ⇒ jede Kombination aus
alter/neuer Middleware und altem/neuem Channel-Plugin verhält sich wie heute, bis ein
Operator umschaltet. Kein Flag-Day. Unbekannte/NULL-Werte lesen sich als `off`
(deny-default), damit ein Rollback das Memory-Routing nicht ändert.

`buildOrchestrator` baut den Binder **unbedingt** und gated per Modus — `off` und der heutige
Stack sind ein Codepfad, damit der Schalter nicht von dem wegdriftet, was er schaltet.
`ChatSessionStore`/`SessionLogger` bleiben auf dem statischen `scopedStore`: Session-
Transkripte bleiben geteilt unter `core/sessions` (Entscheidung A3a).

HTTP/API-Turns emittieren **kein** `origin` (Koordinator-Entscheidung 1). Deren
Scope-Strings (`http-<scope>`, client-gewählte `sessionId`, das geteilte `'http-default'`)
sind vom Caller gelieferte Transkript-Labels — daraus eine Memory-Partition abzuleiten hiesse,
jedem API-Client das Tier eines anderen benennbar zu machen.

### Purge & Promote

**Purge** (`/api/v1/admin/memory/purge`): `axis:'team'|'channel'|'user'` hat erstmals einen
Scratch-Footprint und löscht den Kontextbaum über ALLE Agenten (Enumeration via
`store.list('/memories/contexts')`, nur list+delete, also backend-agnostisch). `axis:'agent'`
nimmt `/memories/contexts/<slug>` mit, `axis:'all'` erfasst `contexts` gratis (nicht in
`PROTECTED_SEED_ENTRIES`). Selector-Semantik: **immer** `<channelType>~<id>`; ohne `~` →
400 `invalid_selector`, denn eine Danger-Zone-Geste, die nichts löscht und Erfolg meldet, ist
schlimmer als ein Fehler. Beide Lesarten (verbatim Key / roher Native-Id) werden aufgelöst
und die Vereinigung der real existierenden Bäume gelöscht — `memoryContextKey` ist auf seiner
eigenen Digest-Form bewusst nicht idempotent. Das server-seitige Type-to-confirm prüft
weiterhin gegen den **getippten** Selector, nie gegen den abgeleiteten `ctxKey`.

**Promote** (`POST|GET /api/v1/admin/memory/promotions/:slug`, gleiches `requireAuth`-Gate
und gleicher Prefix wie Purge): kopiert/verschiebt Files und Subtrees zwischen den Tiers
EINES Agenten. Das ist der einzige Weg, auf dem Wissen eine Kontextgrenze überschreitet.
Audit dreifach: JSONL-Zeile in `/memories/core/audit/memory-promotions.jsonl`,
Provenance-Frontmatter (`promoted-from`/`-by`/`-at`) im Ziel-File, `[security-audit]`-Logzeile.
Läuft auf dem ROOT-Store (undekoriert), Präzedenz `memoryPurge`.

Zwei Fallen, die real waren: `move` löscht nur die Files, die es auch geschrieben hat — der
rekursive `delete(sourceRoot)` hätte Dotfiles vernichtet, die `store.list()` gar nicht
aufzählt (der Walk überspringt `.`-Namen, in-memory wie Postgres). Und ein Ziel, das im
Quellbaum liegt (oder umgekehrt), wird abgelehnt: `move` hätte das frisch geschriebene Ziel
mit der Quelle zusammen gelöscht und Erfolg gemeldet.

### Tests (Store-Level, kein LLM-Output; Per-Test-Fixtures)

`test/memoryContextKey.test.ts` (Injektivität, Pre-Image-Schutz),
`test/memoryAxesForOrigin.test.ts` (§2-Tabelle als Cases + Cross-Kind-Kollisionen),
`test/scopedMemoryStore.contexts.test.ts` (Token-Matrix × read/write, `ro:`,
Kollisionsfreiheit), `test/effectiveMemoryScope.test.ts` (fail-closed + Golden gegen
`orchestratorMemoryScope`), `test/contextMemoryNamespacer.test.ts` (Bijektion),
`test/memoryContextIsolation.test.ts` (**der Abnahmetest**: Team A ↮ Team B, Kanal ↮ Kanal,
User ↮ User, Shared-Namespace als Seitenkanal, Audit-Log, `off`-Golden, `enforce-strict`),
`test/memoryBinder.cache.test.ts` (LRU + Key-Kollisionsfreiheit),
`test/memoryPurge*.test.ts`, `test/memoryPromote*.test.ts`.

**Offene Follow-ups:** Der Memory-Browser im web-ui liest die Kontext-Bäume noch über den
dev-only `GET /bot-api/dev/memory/list` — in Produktion nicht gemountet, also dort inert. Eine
operator-authentifizierte Listing-Route (gleiches Gate wie Purge) ist der nächste Schritt.
Die Channel-Plugins (`omadia-channel-teams`, `omadia-channel-telegram`) bauen den `TurnOrigin`
in ihren EIGENEN Repos; die können erst nach Release des SDK mit `TurnOrigin` gebaut werden.
42 changes: 42 additions & 0 deletions middleware/migrations/0050_agent_context_memory_flag.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
-- Epic #860 / W5 — per-Agent rollout switch for chat-context-scoped memory.
--
-- Agent memory is isolated per AGENT today: what an Agent learns in Teams team
-- A lands in one agent-global tree and is quotable in team B on the next turn.
-- W5 partitions that tree by chat context. This column is the switch that turns
-- the partitioning on, per Agent.
--
-- 'off' — DEFAULT. Byte-identical to today: every turn gets the
-- agent-private memory stack, whether or not its channel
-- plugin sends a TurnOrigin.
-- 'enforce' — a context turn writes into its own tier and reads the
-- agent tier READ-ONLY, so existing knowledge stays
-- quotable but "note this globally" stops being a leak
-- channel from team A into team B.
-- 'enforce-strict' — full quarantine: a context turn cannot even read the
-- agent tier.
--
-- Default 'off' is the no-flag-day guarantee: every existing row reports 'off'
-- the moment this lands, so no deployment changes behaviour until an operator
-- flips an Agent deliberately. NOT NULL + DEFAULT rather than a nullable
-- column, so a NULL can never be read as "some other mode".
--
-- The CHECK constraint is created separately and guarded, because
-- `ADD CONSTRAINT` has no IF NOT EXISTS in PostgreSQL and this migration must
-- be applicable twice (schema CI gate).
ALTER TABLE agents
ADD COLUMN IF NOT EXISTS context_memory TEXT NOT NULL DEFAULT 'off';

DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_constraint WHERE conname = 'agents_context_memory_check'
) THEN
ALTER TABLE agents
ADD CONSTRAINT agents_context_memory_check
CHECK (context_memory IN ('off', 'enforce', 'enforce-strict'));
END IF;
END
$$;

COMMENT ON COLUMN agents.context_memory IS
'W5 memory-ACL rollout switch: off | enforce | enforce-strict. Default off = today''s agent-global memory.';
17 changes: 17 additions & 0 deletions middleware/packages/harness-channel-api/src/chatRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,23 @@ export function createApiChatRouter(deps: ApiChatRouterDeps): Router {
};

try {
// W5 memory-ACL (#860), coordinator decision 1 — this router emits NO
// `metadata.origin`, so an API turn resolves context-free and gets the
// agent-private memory stack, byte-identical to today. Deliberate:
//
// - An API key is its own identity, not a delegate for a human in a
// team or a channel (issue #438, see below), so there is no team or
// channel this turn could honestly be said to belong to.
// - `conversationId` is caller-supplied and only becomes safe after
// the `internalConversationId` hash below. Deriving a memory
// partition from the pre-hash value would let one caller name
// another's tier; deriving it from the post-hash value would create
// a per-key tier that no operator surface can list or purge by any
// name a human knows.
//
// Giving API callers context memory means resolving a real tenant from
// the key and emitting an explicit `origin` — a deliberate change, not
// something to inherit by accident.
const turn: IncomingTurn = {
channelId: deps.channelId,
// Namespaced by key identity: CoreApi derives its scope as
Expand Down
15 changes: 15 additions & 0 deletions middleware/packages/harness-channel-sdk/src/chatAgent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type {
} from './outgoing.js';
import type { SurfaceStreamEvent, PendingCanvasSurface } from './surface.js';
import type { EnvelopeProvenance } from './provenance.js';
import type { TurnOrigin } from './turnOrigin.js';

/**
* Orchestrator surface contract — the duck-typed interface every chat-handling
Expand Down Expand Up @@ -407,6 +408,20 @@ export interface ChatTurnInput {
* Set only by the canvas channel; absent → the skeleton path is unchanged.
*/
canvasState?: { basedOnRevision: string; currentTree: unknown };
/**
* Kontext-Herkunft des Turns. Fehlt → kontextfreier Memory-Scope (fail-closed).
*
* W5 memory-ACL (design #870 §4/§5): the one contract extension the
* chat-context memory ACL needs. `ChatAgent.chat()` is the only surface every
* channel adapter shares, so this is where "which team / channel / user is
* this turn from" can be stated once instead of per connector. The
* orchestrator resolves it to memory axes at the start of the turn
* (`memoryAxesForOrigin`) and never lets the model see it.
*
* Optional on purpose — an older channel plugin sends nothing, resolves to
* the context-free axes and behaves exactly as it does today.
*/
origin?: TurnOrigin;
}

/**
Expand Down
16 changes: 16 additions & 0 deletions middleware/packages/harness-channel-sdk/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,7 @@ export {
SYSTEM_SCOPE_ORIGINS,
formatSessionScope,
isAddressableScope,
memoryContextKey,
parseSessionScope,
scopeGraphKey,
unsharedConversationScope,
Expand All @@ -282,6 +283,21 @@ export {
type UnscopedReason,
} from './scopeId.js';

// W5 memory-ACL — where a turn came from, and the memory axes that follow.
// Re-exported from the package root because the PRODUCERS live outside this
// repository: `omadia-channel-teams` and `omadia-channel-telegram` resolve
// `@omadia/channel-sdk` to this package's built `dist/index.d.ts`, so a type
// that is not named here cannot be named by a channel plugin at all.
export {
CONTEXT_FREE_MEMORY_AXES,
CONTEXT_MEMORY_CHANNEL_TYPES,
memoryAxesForOrigin,
teamAxisKey,
type MemoryAxes,
type MemoryAxis,
type TurnOrigin,
} from './turnOrigin.js';

// #333 Phase 1 — the typed principal. Same home as `ScopeId` and for the same
// reason: Conductor (`middleware/src`), the orchestrator and the kernel all
// depend on this package and it depends on none of them. `Principal` says WHO;
Expand Down
Loading
Loading