Skip to content

docs(rfc): cross-channel conversation memory + platform identity - #100

Merged
Weegy merged 5 commits into
mainfrom
docs/cross-channel-memory-rfc
May 18, 2026
Merged

docs(rfc): cross-channel conversation memory + platform identity#100
Weegy merged 5 commits into
mainfrom
docs/cross-channel-memory-rfc

Conversation

@iret77

@iret77 iret77 commented May 18, 2026

Copy link
Copy Markdown
Contributor

Summary

  • New RFC at docs/cross-channel-memory.md proposing two omadia core capabilities — platformIdentity@1 and crossChannelConversationMemory@1 — together with four provider plugins (Neon + in-memory siblings per capability), modelled on harness-knowledge-graph-neon / harness-knowledge-graph-inmemory.
  • Driver is the omadia-ui Tier-2 orchestrator, which declares a hard requires: crossChannelConversationMemory@1 to deliver the "Telegram commute → desktop office" continuity scenario.
  • Additive against harness-channel-sdk: today's ConversationHistoryStore contract stays unchanged. A new DurableConversationHistoryStore adapter bridges to the capability and degrades to the existing InMemoryConversationHistoryStore when the capability is not installed, so channel plugins migrate one-by-one without breaking changes.
  • docs/middleware-agent-handoff.md §13 gains a "Phase 13 — Cross-Channel Conversation Memory" roadmap entry pointing at the RFC; CHANGELOG records the proposal under [Unreleased] → Added.

Key design decisions, opinionated

  • Two capabilities, not one. platformIdentity@1 is intentionally separate from crossChannelConversationMemory@1 so Slice 2.5 can replace the identity implementation without touching CCM consumers. Four packages by default; pragmatic fallback to two packages (one per backend) is called out as v1.0-acceptable.
  • No pgvector, no score-decay, no HOT/WARM/COLD tiering for v1. Conversation log is chronological; value fades with time. Single ccm-gc cron job with TTL (90 days default) + per-user cap (10000 turns default).
  • Persist raw, redact on egress. Existing egressWalker pipeline stays as-is. Async metadata job populates redaction_metadata; reads default to redacted projection. Tenant-scoped ccm_redact_on_persist flag available for strict-compliance setups. includeRaw reads are admin-only and audit-logged.
  • Hard tenant isolation. tenant_id NOT NULL on every table, every query binds WHERE tenant_id = $1, fail-closed if missing. No admin override. Same operational model as the KG plugin, no RLS.

PR sequence (this PR is #1, docs-only)

# What
1 This RFC + handoff Roadmap entry + CHANGELOG
2 harness-platform-identity-* packages (Neon + in-memory) provide platformIdentity@1
3 harness-cross-channel-conversation-memory-* packages (Neon + in-memory) provide crossChannelConversationMemory@1, ccm-gc job
4 DurableConversationHistoryStore adapter in harness-channel-sdk
5–8 Per-channel opt-in PRs: teams, slack, telegram, web-chat (one PR each)
9 omadia-ui orchestrator consumes the capability

Each PR is independently mergeable. Full details in §14 of the RFC.

Test plan (PR 1 is docs-only)

  • docs/cross-channel-memory.md renders cleanly on github.com (table, fenced code, schema blocks).
  • docs/middleware-agent-handoff.md Phase 13 entry inlined before the closing ---; existing phases unchanged.
  • docs/CHANGELOG.md entry under [Unreleased] → Added in Keep-a-Changelog format, preceding the engineering-standards entry.
  • Pre-push hook passes; branch protection allows merge after review.
  • Codex review on the RFC contract — ~3 rounds expected, per the omadia-ui CONCEPT.md cadence.
  • End-to-end capability verification deferred to PR 3 (provider tests) and PR 9 (omadia-ui smoke).

@iret77

iret77 commented May 18, 2026

Copy link
Copy Markdown
Contributor Author

Round-1 Codex-Review surfaced structural findings (mismatched ConversationTurn shapes, service-registry key form, missing ctx.tenantId on TurnContextValue, ctx.notifications used as ops/audit primitive, schema integrity gaps). Closing while iterating on the branch — will reopen once the review verdict reaches 'merge as-is' / 'merge after small revisions'. No premature merge candidates in the repo.

@iret77 iret77 closed this May 18, 2026
@iret77

iret77 commented May 18, 2026

Copy link
Copy Markdown
Contributor Author

Reopening after four review rounds. Codex final verdict: merge as-is (no remaining structural or cosmetic findings).

Round summary:

  • Round 1 → 'needs another round' (11 findings, 3 structural: ConversationTurn shape conflict, service-registry key form, ctx.tenantId on TurnContextValue, ctx.notifications misuse)
  • Round 2 → 'needs another round' (3 blockers: invented permissions.routes.allowed manifest key, outbox idempotency / failure taxonomy underspecified, audit-table retention / PII minimization missing)
  • Round 3 → 'merge after small revisions' (3 cosmetic: missing nullifiedAuditRows in audit allowed-keys, missing 'audit' in gc-counter label set, missing CcmAppendError initialiser note)
  • Round 4 → merge as-is

All findings were verified against the actual code in middleware/packages/plugin-api/, middleware/packages/harness-channel-sdk/, middleware/packages/harness-knowledge-graph-neon/, and middleware/src/plugins/manifestLoader.ts before applying fixes. No code changes in this PR — the contract is locked and ready for implementation PR 2 (platform-identity provider) to start.

@iret77 iret77 reopened this May 18, 2026
cwendler added 5 commits May 18, 2026 18:22
Driven by omadia-ui Tier-2 orchestrator's hard dependency on
crossChannelConversationMemory@1 and the "Telegram commute → desktop
office" continuity scenario. Per-channel ephemeral history today
(InMemoryConversationHistoryStore, 10 turns / 2h / 500 LRU) cannot
carry state across channels or restarts.

The RFC proposes two capabilities — platformIdentity@1 and
crossChannelConversationMemory@1 — with four provider plugins
(Neon + in-memory siblings per capability), modelled on the
harness-knowledge-graph-neon / -inmemory pattern. A new
DurableConversationHistoryStore adapter in harness-channel-sdk
bridges the contract; the existing ConversationHistoryStore interface
stays unchanged, channels opt in per PR, missing capability falls
back to today's in-memory behaviour.

Adds docs/cross-channel-memory.md, a Phase 13 entry in §13 of
middleware-agent-handoff.md, and a CHANGELOG entry under [Unreleased].
Round 1 left structural contract bugs that Codex flagged. Round 2
addresses them against the actual code in middleware/packages/:

- Service-registry keys are bare names, not capability refs. The RFC
  now declares constants (PLATFORM_IDENTITY_SERVICE = 'platformIdentity',
  CROSS_CHANNEL_CONVERSATION_MEMORY_SERVICE =
  'crossChannelConversationMemory') and reserves the '@1' suffix for
  manifest provides/requires only. Matches pluginContext.ts:226-229
  and the existing KG plugin's KNOWLEDGE_GRAPH_SERVICE = 'knowledgeGraph'
  pattern (harness-knowledge-graph-neon/src/plugin.ts:61).
- TurnContextValue does not carry tenantId or user identity today
  (turnContext.ts:34). The RFC now spells out the three additive
  optional fields (tenantId?, originatorUserRef?, originatorUserId?)
  that PR 4 lands together with the adapter, and notes that this
  absorbs the Phase-12 tenantId work.
- The two ConversationTurn shapes in the SDK (stores.ts:15 with
  timestampMs? vs. inMemoryConversationHistory.ts:23 with required at)
  are now called out as pre-existing tech-debt; the adapter implements
  the canonical stores.ts contract and bridges to the legacy internal
  type at delegation time.
- ctx.notifications is the wrong surface for ops/audit (it broadcasts
  cross-channel user notifications; pluginContext.ts:98-102, 520-526).
  Failure logging now goes through ctx.log; raw-access audit is a
  durable ccm_audit_events table written in-transaction with the
  audited operation.
- platformIdentity@1 auto-merge on email is opt-in per tenant
  (pi_auto_merge_on_email, default false) and only triggers on
  email_verified=true. UNIQUE partial index +
  INSERT ... ON CONFLICT makes the resolver race-safe. Shared mailbox,
  email rename, and recycled email are documented edges.
- cross_channel_messages gains materialised byte counters and a
  redaction_state column ('pending' | 'clean' | 'redacted'). Default
  reads exclude 'pending' rows to bound the post-insert/pre-redaction
  exposure window. GC gains a third pass for ccm_user_byte_cap.
- Outbox is now specified: durable ccm_outbox table in the destination
  DB, with a process-local bounded fallback queue when the destination
  is unreachable, drained by a dedicated ccm-outbox job with
  exponential backoff and a 5-attempt DLQ.
- PR sequence rephrased to source-mergeable with explicit
  deploy-prerequisite ordering, matching the kernel's boot-time
  `requires` check (pluginContext.ts:224-225). Each implementation PR
  carries the AGENTS-mandated .env.example / migration-ID /
  security-architecture / middleware-agent-handoff doc updates.
- New §13 (Observability) lists the counter names, performance targets
  (appendTurn p99 <100ms, getRecentByUser p99 <50ms at limit<=20,
  gc <5min/1M turns), and the per-PR eval/test matrix. Plugin-internal
  registry exposed via /ccm/metrics; migrates when ctx.metrics lands.
- §3.1 spells out the inmemory sibling's operational limits
  (single-process, lost-on-restart, not multi-pod). §12.3 documents
  that assistant quotes of forgotten content survive unless
  ccm_redact_on_persist is enabled.

Status changed from 'Proposed' to 'Proposed (Round 2)'.
Round 2 left three blocker findings the Codex reviewer flagged.
Round 3 closes them against the real plugin-api / manifestLoader code:

- §13/§14.1 no longer invents `permissions.routes.allowed`. The
  manifest loader (middleware/src/plugins/manifestLoader.ts:465-487)
  extracts permissions for memory, graph, network, subAgents and llm
  — there is no `permissions.routes` key today, and `RoutesAccessor`
  (pluginContext.ts:464-472) registers routers with no permission
  gate. /ccm/metrics is registered via the normal
  `ctx.routes.register('/ccm', router)` path, with auth handled by
  the plugin itself (static `CCM_METRICS_TOKEN` or operator-managed
  reverse-proxy allow-list). The manifest fragment no longer carries
  the fictional key.

- Outbox correctness is now spec'd end-to-end:
  - `cross_channel_messages` gains a `client_message_id` UNIQUE
    column. `appendTurn` is INSERT ... ON CONFLICT DO NOTHING; retries
    from the outbox reuse the same ULID and are idempotent at the
    storage layer.
  - §6 introduces a structured `CcmAppendError` with
    code = 'committed' | 'rejected' | 'timeout' | 'transport' and
    optional `messageId`. The adapter branches on `code`:
    'committed' is treated as success (no retry), 'rejected' is
    permanent failure (no retry), 'timeout' / 'transport' go to the
    outbox.
  - §5.6 names the process-local fallback buffer honestly: bounded
    in-memory ring, crash-lossy, overflow drops oldest. Counter
    `ccm_outbox_local_dropped_total` lets operators alert. The
    durable outbox in the destination DB is named as THE durability
    boundary; the local buffer is only a brief-outage recovery aid.

- Audit-event retention and PII minimization (§8.4):
  - `ccm_audit_events.detail` is bounded by an enumerated allow-list
    of keys per `op`. Free-form turn content is never stored. The
    capability impl validates `detail` shape on insert.
  - `ccm_audit_retention_days` config (default 365) drives a new
    pass-4 in `ccm-gc` that deletes expired audit rows.
  - `forgetByUser` interaction with audit: 'read_raw' rows for the
    forgotten user are deleted in the same transaction; 'forget_user'
    and 'merge_identities' rows have their PII identifiers nullified
    but the event itself is retained for compliance. New return field
    `deletedAuditRows` on `forgetByUser`.

Partial findings also addressed in this round:

- §10.1 TurnContextValue extension grows a 4th optional field
  `canvasSessionId?` so §10.4's read call is self-consistent. All
  four new fields documented as optional with explicit
  "tools / sub-agents / routines see undefined → degrade gracefully"
  guidance.
- §10.3 spells out the resolveUserId cache: per-turn AsyncLocalStorage
  Map<string, string> keyed on (tenantId, platformId), dropped on
  turn exit. Group-chat multi-sender turns get one entry per ref.
- §3.1 gains a parity matrix listing exactly which durable behaviors
  the in-memory sibling mirrors and which it skips (outbox doesn't
  apply because the destination IS the process; audit is a ring
  buffer with no retention guarantee; multi-pod unsupported).
- §11 tenant-assertion language trimmed — defense-in-depth catches
  caller bugs, structural binding prevents cross-tenant reads. New
  startup warning if `pi_tenant_id` and `ccm_tenant_id` disagree.
- §5.3, §5.5, §8.4 schema columns gain CHECK constraints for enums
  and non-negative counters.
- §15.1 migration-ID column now cites concrete filenames
  (`pi/0001_init.sql`, `ccm/0001_init.sql`) with a note that
  implementers update the table when actual migrations land.

CHANGELOG entry updated to reflect the multi-round review and the
specific fixes that came out of it, satisfying AGENTS' rule that
significant doc changes carry a changelog entry in the same step.

Status header changed from 'Proposed (Round 2)' to 'Proposed (Round 3)'.
Round-3 verdict was "merge after small revisions". Round 4 closes
the three remaining nits:

- §8.4 allowed-key enum for op='forget_user' adds 'nullifiedAuditRows'
  (referenced in §8.4.1 but missing from the enum, would have made the
  capability impl's validator reject its own audit insert).
- §13.1 ccm_gc_deletes_total{pass} label set adds 'audit' (the
  pass-4 audit-retention GC added in Round 3 was not covered by the
  counter label, so the GC sweep would have lacked observability for
  that pass).
- §6 CcmAppendError gains a doc-comment requiring full constructor
  initialisation so implementers don't ship partially-initialised
  exception instances.

Status: 'Proposed (Round 4 — merge-ready pending final Codex pass)'.
@iret77
iret77 force-pushed the docs/cross-channel-memory-rfc branch from 8c1dbd3 to 9968dde Compare May 18, 2026 16:35
@Weegy
Weegy merged commit 32a89ca into main May 18, 2026
5 checks passed
@Weegy
Weegy deleted the docs/cross-channel-memory-rfc branch May 18, 2026 19:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants