Unclassified

Spine and Memory Model — Canonical vs Operational, Scopes, Promotion, Triage

Date: 2026-07-30
Status: Proposed — resolves D4 from RUNTIME_PROFILE_AND_ENTRY_CHANNEL_CONTRACT.md §9
Depends on: ARCHITECTURE_LOCK.md, CODEBASE_EXCAVATION.md §S8


1. The distinction

Two stores, two different kinds of truth, two different lifecycles. They are not tiers of the same thing.

Operational SpineCanonical Spine
Answers"What is wired, and is it working?""What is true about the business?"
ContentsConnections, connectors, sync jobs, webhooks, schemas, capabilities, timeline16 root concepts + 7 operational primitives
LifecycleHigh-churn; reflects runtime stateAppend-only, versioned; reflects durable fact
WriterThe service that owns the runtime objectOne writer — the Pipeline
Read byActivation Hub, Integration Manager, ObservabilityWorkbench projections, Twin, Capabilities
StoreD1 (edge-fast, tenant-partitioned)Neon (durable, relational, versioned)
If lostRewire and resyncUnrecoverable

The clean formulation: Operational Spine is the state of the machine. Canonical Spine is the state of the business. Projections read canonical. Governance gates writes into canonical. Operational needs no governance gate because it holds no business truth.

Memory is a third thing, and §3 covers it — it is neither operational nor canonical, it is interpreted.


2. Code reality: the operational Spine is real; the canonical Spine does not run

This is the finding that resolves D4, and it is not what the documents imply.

2.1 Four implementations claiming Spine authority

ImplementationLOCStoreStatus
services/spine/src/entity-store.ts + index.ts898D1 (DB)LIVE. 7 entity types: connections, connectors, sync-jobs, webhooks, schemas, capabilities, timeline-events. This is the Operational Spine, correctly built.
services/spine/src/neon-workbench.ts558Neon (DATABASE_URL)LIVE. Tenant/actor-scoped Workbench projection. Named canonical by CLAUDE.md.
services/spine/src/canonical-store.ts995D1DEAD. Zero importers. Declares the 16 root concepts + 7 primitives and states "This is the single source of truth… The pipeline is the sole writer." Its types were copy-pasted into pipeline/src/canonical/entity-type-map.ts:16 — "Types duplicated from spine canonical-store to avoid cross-service dependency."
services/pipeline/src/spine/index.ts2,773 (with entity360-assembler, vault, crypto)SupabaseLEGACY. Self-labelled "the legacy writer (Supabase tables under 'spine'/'sales'/'cs' schemas)."

2.2 The canonical write path is bound to a placeholder

services/pipeline/src/canonical/canonical-writer.ts (296 LOC) is the intended canonical writer. It is invoked at services/pipeline/src/index.ts:1923:

// Write to canonical spine (16 root concepts + relationship graph)
if (env.CANONICAL_SPINE_DB) {
  const canonicalWriter = new CanonicalSpineWriter(env.CANONICAL_SPINE_DB);
  await canonicalWriter.write(rec, { create_relationships: true });
}

Its binding, in services/pipeline/wrangler.toml:118–120:

[[d1_databases]]
binding = "CANONICAL_SPINE_DB"
database_name = "integratewise-canonical-spine"
database_id = "YOUR_D1_DATABASE_ID_HERE"  # Create via: wrangler d1 create ...

The database ID is an unreplaced scaffold placeholder. The binding never resolves, if (env.CANONICAL_SPINE_DB) is always false, and every canonical Spine write is silently skipped. The surrounding try/catch logs to console.warn on failure — so even if it did resolve, failures would not surface.

The line immediately after is decisive:

// For writes, D1 is authoritative. Return success. (No legacy spine-v2 call for writes.)
return json({ success: true, written: entities.length, via: "d1-primary" });

The pipeline returns success for entities that were written only to the D1 entity cache (writeToEntityCache(env.CACHE_DB, …), database integratewise-spine-cache). A cache write is being reported as canonical persistence.

Conclusion: the Operational Spine is implemented and running. The Canonical Spine exists as an ontology, a dead 995-line store, and a writer bound to a non-existent database. Nothing durable is being written.

This also inverts the framing from CODEBASE_EXCAVATION.md §S8. There are not two competing canonical writers to choose between — there are zero working ones, plus a legacy Supabase writer that still runs.

2.3 Resolution of D4

The Canonical Spine is Neon. The Operational Spine is D1. services/spine owns both boundaries. The Pipeline is the only writer into canonical.

Concretely:

  1. Provision integratewise-canonical-spine, or — preferred — drop CANONICAL_SPINE_DB entirely and have the Pipeline write canonical entities through the SPINE service binding to Neon. A second D1 database is not needed; it recreates the split.
  2. Move canonical-store.ts's ontology into packages/spine-entities (which already exists and is already consumed by services/spine), and delete the copy-paste in pipeline/src/canonical/entity-type-map.ts. One definition of the 16 root concepts.
  3. Change the console.warn swallow to a hard failure. A canonical write that fails must not return success: true.
  4. Retire services/pipeline/src/spine/ once (1)–(3) land.
  5. Rename for clarity: spine/src/entity-store.ts → operational; spine/src/neon-workbench.ts → canonical projection boundary. The word "Spine" alone should stop appearing unqualified in new code.

Item (3) is the highest-value single change in this document: it converts a silent data-loss path into a loud one.


3. Memory scopes

Memory is neither operational nor canonical. It is interpreted truth — derived from canonical facts and session evidence, carrying confidence and provenance, and therefore requiring governance that neither Spine needs.

3.1 Target model — four scopes, one dimension

ScopeBoundaryGovernanceStore
Personal MemoryOne user. Hard boundary — no other principal reads, including the org's own Twin acting for someone else.User owns it. No approval needed.personal_memory
Agent MemoryOne agent/Twin instance within one tenant. Working state, learned routing, tool outcomes.Not promotable directly to org. Must pass through triage as evidence.agent_memory
Org MemoryTenant-wide. The organisation's interpreted truth.Governed. staging → approved → published.org_memory
Conversational MemoryOne session/thread. Ephemeral working context.Decays. Never promoted directly.conversational_memory

The rule that keeps this coherent: scope only ever widens through promotion, and promotion is always governed. Conversational and Agent memory are evidence, not truth. Personal memory never widens at all — it can only be cited by the user themselves.

3.2 Code reality: four competing vocabularies

DefinitionScopesStatus
services/intelligence/migrations/d1_memory.sqlpersonal_memory, conversational_memoryTables exist. personal_memory correctly documented "Hard boundary — only this user reads", 8 categories, governance_state.
services/intelligence/migrations/d1_knowledge.sqlorg_memoryTable exists, defaults to governance_state = 'staging'. Correct default.
services/gateway/src/memory-governance.tsMemoryScope = user | team | organization; MemoryType = conversation | insight | decision | pattern | workflowORPHANED — not imported by gateway/src/index.ts. Introduces a team scope with no backing table anywhere in the repo.
services/pipeline/src/spine/index.ts:1360agent_memoryOnly definition in the repo. CREATE TABLE IF NOT EXISTS agent_memory (kv_key, agent_id, session_id, type, …) — inside the retired Supabase legacy writer.

Two structural problems.

Agent Memory exists only in retired infrastructure. If this repository is to own agent memory (per the operational/canonical repo split), the only implementation is in the Supabase writer scheduled for deletion. Agent memory needs a D1 table alongside personal_memory before pipeline/src/spine/ is retired, or the capability is lost.

org_memory is a cache being written as a store. d1_knowledge.sql:12 documents it as "Mirror of memory.org_memory for edge-fast reads." Yet six services write to it directly: pipeline/src/index.ts, pipeline/src/memory-promoter.ts, intelligence/src/memory/evolutionary-memory.ts, intelligence/src/handoff/outcome.ts, intelligence/src/workflows/triage-workflow.ts, mcp-connector/src/handlers/tools.ts.

This is the Spine problem repeated one layer up: a read-through cache treated as the system of record, with no single writer and no defined reconciliation against the Postgres memory.org_memory it claims to mirror. Either promote the D1 table to the store and delete the mirror language, or enforce a single writer and make the other five read-only. It cannot stay ambiguous.

There is also a schema-ownership split: org_memory is created in services/intelligence's migrations but written primarily by services/pipeline. Table ownership should follow write authority.


4. Promotion and Triage

4.1 The intended shape

Evidence                    Interpretation              Truth
────────                    ──────────────              ─────
conversational_memory  ┐
agent_memory           ├──► TRIAGE ──► candidate ──► PROMOTION GATE ──► org_memory
session events         │    (classify)               (govern)           (staging→approved)
canonical entities     ┘
                                                          │
                                            personal_memory (never promoted —
                                             widened only by the user, explicitly)

Triage classifies. The Promotion Gate governs. They are different responsibilities and must stay separate — a classifier that can also approve is not a gate.

4.2 Code reality: four governance models, two of them contradictory

ImplementationDecision vocabularyStatus
pipeline/src/memory-promoter.ts (415 LOC)7 Promotion Signals — important, repeated, strategic, behavioral, decision, preference, exception → approved | stagingLIVE. Queue-driven: spine.written → MEMORY_PROMOTE → scoreForPromotion() → governance gate → org_memory + memory_promotion_audit + AI Search index. Honours tenant governanceDefaults.requiresApproval. This is the good one.
knowledge/src/synthesis/promotion-gate.tsauto_approved | hitl_required | rejected | deferredORPHANED. Zero importers outside synthesis/.
knowledge/src/triage-bot.tsSlack reactions ✅/❌/⏰Neon + D1 + Workers AI.
gateway/src/memory-governance.tscapture → triage → promote → decayORPHANED.

The contradiction, in one service:

  • knowledge/src/triage-bot.ts:13 — "The Triage Bot NEVER auto-approves. Every extraction requires customer review."
  • knowledge/src/synthesis/promotion-gate.ts:15 — "AUTO-APPROVE: High confidence + no governance flags + trusted domain."

Both live in services/knowledge. One of them is wrong. Given ARCHITECTURE_LOCK's "Governance is a hard gate" and the Twin's "never acts without explicit approval," the never-auto-approve position is the correct one for anything that will inform Twin reasoning or external action. memory-promoter.ts already implements the defensible middle: auto-promote to staging, which the Twin may not use until approved (its R11.3 comment) — approval automation without approval bypass.

The synthesis pipeline is built on retired infrastructure. synthesis/triage-bot.ts:4 — "Classifies raw CouchDB conversations"; synthesis/index.ts:14 — "CouchDB Session → TriageBot.classifySession() → ContinuityObjectFactory…"; synthesis/lineage-tracker.ts:279 — "Serialize lineage to a CouchDB-compatible document." CouchDB was superseded by the D1 memory tables (packages/coda-pack/template.ts:200 records the migration: "Rewrite continuity & intelligence caching from CouchDB to conversational, org, and personal memory tables"). The entire synthesis/ folder — triage-bot, promotion-gate, continuity-object-factory, lineage-tracker, knowledge-model — is a complete, unreferenced pipeline for a store that no longer exists.

Worth noting what is genuinely valuable in there and should be salvaged rather than deleted: lineage-tracker.ts and the UnderstandingObject provenance model have no equivalent in the live path. memory-promoter.ts writes an audit row but does not track lineage across promotions.

4.3 Resolution

  1. pipeline/src/memory-promoter.ts is the single Promotion Gate. Delete knowledge/src/synthesis/promotion-gate.ts.
  2. knowledge/src/triage-bot.ts is the single Triage classifier, retargeted from CouchDB sessions to conversational_memory + agent_memory + session events from services/continuity. Delete synthesis/triage-bot.ts.
  3. Salvage lineage-tracker.ts into the promoter so promotions carry provenance, then delete the rest of synthesis/.
  4. Delete gateway/src/memory-governance.ts. Its team scope has no store; if team memory is wanted, it is a new scope decision, not an orphaned type file.
  5. Never auto-approve into approved. Auto-promotion may only reach staging. Make this a test, not a convention.
  6. Add agent_memory as a D1 table in intelligence/migrations/d1_memory.sql alongside personal_memory, before pipeline/src/spine/ is retired.

5. Co-worker

This is already decided, already written down, and already orphaned.

packages/contracts/src/authority.ts (228 LOC) declares itself the "Canonical Authority Doctrine — supersedes all other architectural rules" and defines the boundary precisely:

Twin        = understands context
Co-worker   = performs preparation and bounded delegated work
Human       = owns decisions and authority

The Co-worker can say: "I found the relevant information, prepared the update, and recommend this action."
It cannot say: "I approved my recommendation and executed it."

And docs/wiki/21-adrs-glossary-invariants-and-superseded-concepts.md:66 records the decision as already taken:

"'Co-worker' as a second runtime → Twin is the contextual AI Co-worker behavior." — superseded concept

So the position is settled: Co-worker is a behavioural mode of the Twin, not a second runtime, not a service, not a memory scope. The doctrine matches the mechanisms already in place:

Co-worker behaviourExisting primitive
Remembers your preferencesPersonal Memory
Knows the org's decisionsOrg Memory (promoted)
Picks up where you left offContinuity
Learns from its own runsAgent Memory
Prepares bounded workTwin + Capability
Cannot approve its own workGovernance

5.1 The problem: the authority doctrine is in a dead package

packages/contracts has zero consumers (CODEBASE_EXCAVATION.md §3c). Nothing imports it. So the document that calls itself "supreme architectural principle… supersedes all other architectural rules" is enforced by nothing, and its neighbours share the fate:

FileDefinesEnforced by
authority.tsAuthority Doctrine, Twin/Co-worker boundary, approval lifecycle (Draft → Proposed → …), Triage Boundarynothing
surfaces/chat.ts"The Chat surface IS the Triage Hub" — nav id chat, route /app/chat, projection platform.chatnothing
surfaces/spine-access-plane.ts, platform.ts, admin.ts, iw-spine-network.tsSurface contractsnothing
marketplace/installation.tsInstallation contractnothing — services/marketplace-installation defines its own types

This matters more than the usual dead-package finding, because §4.2's contradiction has an authoritative answer sitting right here. authority.ts states the Triage Boundary explicitly:

The Triage Hub can receive, validate, classify, redact, deduplicate, link, quarantine, stage. It CANNOT independently decide that unverified material becomes organizational truth. Triage decides where a candidate should go for review, not whether it becomes final truth by itself.

That settles it: knowledge/src/synthesis/promotion-gate.ts's AUTO-APPROVE path violates the canonical doctrine. knowledge/src/triage-bot.ts's "NEVER auto-approves" is correct. The rule was written; it was just never wired to anything that could enforce it.

5.2 Resolution

  1. Move authority.ts into packages/types (99 consumers) or packages/spine-schema, so the doctrine sits in a package that is actually imported.
  2. Turn the Triage Boundary into a test, not prose. A unit test asserting that no promotion path can write governance_state = 'approved' without an approver identity is worth more than the doctrine as a string constant. Note that authority.ts currently exports the doctrine as a template literal — human-readable, machine-inert.
  3. Adopt surfaces/chat.ts's definition: the Chat surface is the Triage Hub. This is a better answer than a separate "Triage" nav entry, and it connects the sidebar's Personal → Brainstorming Sessions to the triage pipeline.
  4. Reconcile marketplace/installation.ts with the state machine's own types, or delete it (see RUNTIME_PROFILE_AND_ENTRY_CHANNEL_CONTRACT.md §4.1).
  5. In user-facing copy, "Co-worker" is good positioning language. In code it remains Twin behaviour — never a type, table, service, or route.

Correction note: an earlier scan of this repository reported zero occurrences of "co-worker". That was a false negative caused by a case- and path-scoped grep. The term is present in 10 tracked files, including the two contract modules above. The conclusion was right; the evidence was wrong, and the real evidence is stronger.


6. What to fix, in order

#ChangeWhy first
1Make canonical write failure hard-fail instead of console.warn + success: trueConverts silent data loss into a visible error. One-line risk, highest value.
2Route canonical writes through the SPINE binding to Neon; delete CANONICAL_SPINE_DBRemoves the placeholder binding and the second D1 canonical store
3Move the 16-concept ontology to packages/spine-entities; delete the copy-paste in pipeline/src/canonical/entity-type-map.tsOne definition of canonical truth
4Decide org_memory: store or cache. Enforce one writer.Six writers into a documented cache is the §2 problem repeating
5Add agent_memory to D1Must land before pipeline/src/spine/ is deleted or the capability is lost
6Delete synthesis/promotion-gate.ts + synthesis/triage-bot.ts; salvage lineage-tracker.tsRemoves the contradictory governance rule
7Retarget knowledge/src/triage-bot.ts off CouchDB onto the D1 memory tablesMakes triage real
8Delete gateway/src/memory-governance.ts and services/spine/src/canonical-store.ts~1,300 LOC of orphaned competing definitions
9Retire services/pipeline/src/spine/Now safe
10Move packages/contracts/src/authority.ts into packages/types; make the Triage Boundary a testThe canonical doctrine is currently enforced by nothing (§5.1)

Steps 1–3 unblock the operational/canonical repo split in RUNTIME_PROFILE_AND_ENTRY_CHANNEL_CONTRACT.md §7. That split cannot be drawn while the canonical store is a placeholder.


7. Open decisions

#DecisionBlocks
M1Is org_memory (D1) the store, or a cache of Postgres memory.org_memory?§3.2, step 4
M2Does a team memory scope exist? (Currently a type with no table.)§3.2, step 8
M3May Agent Memory be cited by the Twin before triage, or only after promotion?§3.1 — determines whether agent learning is immediate or governed
M4Does Personal Memory ever widen to Org, and if so by what act?§3.1 — the hard boundary is currently absolute
M5Canonical Spine on Neon only, or Neon + a D1 read projection?§2.3, step 2
M6Is the Chat surface the Triage Hub, per contracts/src/surfaces/chat.ts?§5.2, sidebar structure