@ai-agent-forge/plugin-memory 0.85.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. package/README.md +65 -0
  2. package/agent-forge.json +11 -0
  3. package/dist/capability.d.ts +182 -0
  4. package/dist/capability.d.ts.map +1 -0
  5. package/dist/capability.js +2565 -0
  6. package/dist/capability.js.map +1 -0
  7. package/dist/entry.d.ts +36 -0
  8. package/dist/entry.d.ts.map +1 -0
  9. package/dist/entry.js +154 -0
  10. package/dist/entry.js.map +1 -0
  11. package/dist/index.d.ts +49 -0
  12. package/dist/index.d.ts.map +1 -0
  13. package/dist/index.js +49 -0
  14. package/dist/index.js.map +1 -0
  15. package/dist/memory/assistant-card.d.ts +31 -0
  16. package/dist/memory/assistant-card.d.ts.map +1 -0
  17. package/dist/memory/assistant-card.js +108 -0
  18. package/dist/memory/assistant-card.js.map +1 -0
  19. package/dist/memory/candidates.d.ts +65 -0
  20. package/dist/memory/candidates.d.ts.map +1 -0
  21. package/dist/memory/candidates.js +100 -0
  22. package/dist/memory/candidates.js.map +1 -0
  23. package/dist/memory/code-memory.d.ts +89 -0
  24. package/dist/memory/code-memory.d.ts.map +1 -0
  25. package/dist/memory/code-memory.js +104 -0
  26. package/dist/memory/code-memory.js.map +1 -0
  27. package/dist/memory/compaction-sequencer.d.ts +63 -0
  28. package/dist/memory/compaction-sequencer.d.ts.map +1 -0
  29. package/dist/memory/compaction-sequencer.js +129 -0
  30. package/dist/memory/compaction-sequencer.js.map +1 -0
  31. package/dist/memory/continuation.d.ts +44 -0
  32. package/dist/memory/continuation.d.ts.map +1 -0
  33. package/dist/memory/continuation.js +49 -0
  34. package/dist/memory/continuation.js.map +1 -0
  35. package/dist/memory/curation.d.ts +58 -0
  36. package/dist/memory/curation.d.ts.map +1 -0
  37. package/dist/memory/curation.js +68 -0
  38. package/dist/memory/curation.js.map +1 -0
  39. package/dist/memory/egress-policy.d.ts +50 -0
  40. package/dist/memory/egress-policy.d.ts.map +1 -0
  41. package/dist/memory/egress-policy.js +71 -0
  42. package/dist/memory/egress-policy.js.map +1 -0
  43. package/dist/memory/embedding-provider.d.ts +70 -0
  44. package/dist/memory/embedding-provider.d.ts.map +1 -0
  45. package/dist/memory/embedding-provider.js +164 -0
  46. package/dist/memory/embedding-provider.js.map +1 -0
  47. package/dist/memory/embedding-reranker.d.ts +56 -0
  48. package/dist/memory/embedding-reranker.d.ts.map +1 -0
  49. package/dist/memory/embedding-reranker.js +109 -0
  50. package/dist/memory/embedding-reranker.js.map +1 -0
  51. package/dist/memory/foundation.d.ts +168 -0
  52. package/dist/memory/foundation.d.ts.map +1 -0
  53. package/dist/memory/foundation.js +487 -0
  54. package/dist/memory/foundation.js.map +1 -0
  55. package/dist/memory/host-module-import.d.ts +25 -0
  56. package/dist/memory/host-module-import.d.ts.map +1 -0
  57. package/dist/memory/host-module-import.js +41 -0
  58. package/dist/memory/host-module-import.js.map +1 -0
  59. package/dist/memory/ledger.d.ts +58 -0
  60. package/dist/memory/ledger.d.ts.map +1 -0
  61. package/dist/memory/ledger.js +315 -0
  62. package/dist/memory/ledger.js.map +1 -0
  63. package/dist/memory/lifecycle.d.ts +124 -0
  64. package/dist/memory/lifecycle.d.ts.map +1 -0
  65. package/dist/memory/lifecycle.js +201 -0
  66. package/dist/memory/lifecycle.js.map +1 -0
  67. package/dist/memory/memory-network.d.ts +55 -0
  68. package/dist/memory/memory-network.d.ts.map +1 -0
  69. package/dist/memory/memory-network.js +70 -0
  70. package/dist/memory/memory-network.js.map +1 -0
  71. package/dist/memory/model-cache-hygiene.d.ts +18 -0
  72. package/dist/memory/model-cache-hygiene.d.ts.map +1 -0
  73. package/dist/memory/model-cache-hygiene.js +38 -0
  74. package/dist/memory/model-cache-hygiene.js.map +1 -0
  75. package/dist/memory/preference-disambiguator.d.ts +43 -0
  76. package/dist/memory/preference-disambiguator.d.ts.map +1 -0
  77. package/dist/memory/preference-disambiguator.js +81 -0
  78. package/dist/memory/preference-disambiguator.js.map +1 -0
  79. package/dist/memory/preference-lifecycle.d.ts +66 -0
  80. package/dist/memory/preference-lifecycle.d.ts.map +1 -0
  81. package/dist/memory/preference-lifecycle.js +129 -0
  82. package/dist/memory/preference-lifecycle.js.map +1 -0
  83. package/dist/memory/preference-promotion.d.ts +87 -0
  84. package/dist/memory/preference-promotion.d.ts.map +1 -0
  85. package/dist/memory/preference-promotion.js +102 -0
  86. package/dist/memory/preference-promotion.js.map +1 -0
  87. package/dist/memory/preference-resolver.d.ts +44 -0
  88. package/dist/memory/preference-resolver.d.ts.map +1 -0
  89. package/dist/memory/preference-resolver.js +107 -0
  90. package/dist/memory/preference-resolver.js.map +1 -0
  91. package/dist/memory/purge-journal.d.ts +76 -0
  92. package/dist/memory/purge-journal.d.ts.map +1 -0
  93. package/dist/memory/purge-journal.js +130 -0
  94. package/dist/memory/purge-journal.js.map +1 -0
  95. package/dist/memory/purge.d.ts +90 -0
  96. package/dist/memory/purge.d.ts.map +1 -0
  97. package/dist/memory/purge.js +138 -0
  98. package/dist/memory/purge.js.map +1 -0
  99. package/dist/memory/recall-agent.d.ts +84 -0
  100. package/dist/memory/recall-agent.d.ts.map +1 -0
  101. package/dist/memory/recall-agent.js +199 -0
  102. package/dist/memory/recall-agent.js.map +1 -0
  103. package/dist/memory/recall-index.d.ts +87 -0
  104. package/dist/memory/recall-index.d.ts.map +1 -0
  105. package/dist/memory/recall-index.js +222 -0
  106. package/dist/memory/recall-index.js.map +1 -0
  107. package/dist/memory/recall-packet.d.ts +121 -0
  108. package/dist/memory/recall-packet.d.ts.map +1 -0
  109. package/dist/memory/recall-packet.js +156 -0
  110. package/dist/memory/recall-packet.js.map +1 -0
  111. package/dist/memory/scheduler-api.d.ts +99 -0
  112. package/dist/memory/scheduler-api.d.ts.map +1 -0
  113. package/dist/memory/scheduler-api.js +93 -0
  114. package/dist/memory/scheduler-api.js.map +1 -0
  115. package/dist/memory/scheduler.d.ts +55 -0
  116. package/dist/memory/scheduler.d.ts.map +1 -0
  117. package/dist/memory/scheduler.js +91 -0
  118. package/dist/memory/scheduler.js.map +1 -0
  119. package/dist/memory/store.d.ts +107 -0
  120. package/dist/memory/store.d.ts.map +1 -0
  121. package/dist/memory/store.js +208 -0
  122. package/dist/memory/store.js.map +1 -0
  123. package/dist/memory/suite-memory.d.ts +208 -0
  124. package/dist/memory/suite-memory.d.ts.map +1 -0
  125. package/dist/memory/suite-memory.js +288 -0
  126. package/dist/memory/suite-memory.js.map +1 -0
  127. package/dist/memory/transfer.d.ts +142 -0
  128. package/dist/memory/transfer.d.ts.map +1 -0
  129. package/dist/memory/transfer.js +210 -0
  130. package/dist/memory/transfer.js.map +1 -0
  131. package/dist/memory/vector-index.d.ts +39 -0
  132. package/dist/memory/vector-index.d.ts.map +1 -0
  133. package/dist/memory/vector-index.js +136 -0
  134. package/dist/memory/vector-index.js.map +1 -0
  135. package/dist/memory/write-budget.d.ts +33 -0
  136. package/dist/memory/write-budget.d.ts.map +1 -0
  137. package/dist/memory/write-budget.js +45 -0
  138. package/dist/memory/write-budget.js.map +1 -0
  139. package/dist/testing/memory-testkit.d.ts +149 -0
  140. package/dist/testing/memory-testkit.d.ts.map +1 -0
  141. package/dist/testing/memory-testkit.js +438 -0
  142. package/dist/testing/memory-testkit.js.map +1 -0
  143. package/dist/utils/sync-sleep.d.ts +2 -0
  144. package/dist/utils/sync-sleep.d.ts.map +1 -0
  145. package/dist/utils/sync-sleep.js +11 -0
  146. package/dist/utils/sync-sleep.js.map +1 -0
  147. package/package.json +56 -0
  148. package/plugin.json +10 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"suite-memory.d.ts","sourceRoot":"","sources":["../../src/memory/suite-memory.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Suite memory facade (M5, 方案系统设计 §6.1 + 方案实施计划 §10) — the library\n * surface a host uses to LIST and FORGET one suite's memories.\n *\n * The listing is the user-visible control surface for assistant-style personal\n * memory (显式管理命令:列出/忘记 — controllability is a hard requirement when\n * personal information is stored). Forgetting is the M4 suite-deletion hook:\n * the atom set circled by suiteId goes through the canonical purge gate\n * (user-immediate or profile-policy authorization) plus the purge journal, so\n * authorization is structured, partial failures stay `purge_eligible` for an\n * idempotent retry, and every step leaves an audit record. The physical\n * replica purge is injected — this facade never reaches into store internals.\n */\n\nimport type { JsonValue } from \"@agent-forge/plugin-sdk\";\nimport type {\n\tMemoryAtomV1,\n\tMemoryKindV1,\n\tMemoryPreferenceEnvelopeV1,\n\tMemoryScopeV1,\n\tMemorySuiteFilterStatsV1,\n} from \"./foundation.ts\";\nimport { LEGACY_MEMORY_DOMAIN } from \"./ledger.ts\";\nimport type { MemoryPurgeAuthorizationRefV1, MemoryPurgeGateV1, MemoryReplicaV1 } from \"./purge.ts\";\nimport type { MemoryPurgeJournalV1 } from \"./purge-journal.ts\";\nimport type { CommittedMemoryV1, MemoryStoreV1 } from \"./store.ts\";\n\nfunction assertNonEmptyString(value: unknown, label: string): asserts value is string {\n\tif (typeof value !== \"string\" || value.trim().length === 0) throw new Error(`${label} must be a non-empty string`);\n}\n\n/**\n * Structured authorization for forgetting a suite. Suite deletion is a bulk,\n * user- or policy-initiated destructive action, so the unstructured\n * `authorizedBy`-only path and `retention-expiry` mode are not accepted.\n */\nexport type SuiteForgetAuthorizationRefV1 =\n\t| {\n\t\t\treadonly mode: \"user-immediate\";\n\t\t\treadonly issuedAt: number;\n\t\t\treadonly issuedBy: string;\n\t\t\treadonly confirmationRef: string;\n\t }\n\t| {\n\t\t\treadonly mode: \"profile-policy\";\n\t\t\treadonly issuedAt: number;\n\t\t\treadonly issuedBy: string;\n\t\t\treadonly policyServiceRef: string;\n\t };\n\n/** One user-visible suite memory entry (列出语义). */\nexport interface SuiteMemoryListEntryV1 {\n\treadonly memoryId: string;\n\treadonly memoryKind: MemoryKindV1;\n\treadonly isPreference: boolean;\n\treadonly scope: MemoryScopeV1;\n\treadonly occurredAt: string;\n\treadonly payload: JsonValue;\n\t/** Present for preference atoms: the preference envelope summary. */\n\treadonly preference?: {\n\t\treadonly subject: MemoryPreferenceEnvelopeV1[\"subject\"];\n\t\treadonly key: string;\n\t\treadonly scopeLevel: MemoryPreferenceEnvelopeV1[\"scope\"][\"level\"];\n\t\t/** True when explicitly promoted (user-default + confirmedAt) — cross-suite visible. */\n\t\treadonly crossSuiteVisible: boolean;\n\t};\n}\n\nexport interface SuiteMemoryListResultV1 {\n\treadonly suiteId: string;\n\treadonly owner: string;\n\t/** Deterministic order: store revision ascending (commit order). */\n\treadonly entries: readonly SuiteMemoryListEntryV1[];\n\treadonly preferenceCount: number;\n\treadonly factCount: number;\n\t/** Legacy/foreign-suite records excluded by the suite read boundary (设计 §11). */\n\treadonly filter: MemorySuiteFilterStatsV1;\n}\n\nexport interface SuiteMemoryFacadeDepsV1 {\n\treadonly store: MemoryStoreV1;\n\treadonly purgeGate: MemoryPurgeGateV1;\n\treadonly purgeJournal: MemoryPurgeJournalV1;\n\treadonly now?: () => number;\n\t/**\n\t * index 副本 (如 memory-vector-index, 混合检索工程化) 的物理清除回调。缺省\n\t * undefined 时 index 副本分派为 no-op 且不报错——向量通道未装配 (off) 或\n\t * 不可用 (disabled) 时索引里没有本会话写入的行, 无物可清。canonical 及其余\n\t * 副本仍走 `purgeMemories` 回调 (行为零变化)。\n\t */\n\treadonly purgeIndexReplica?: (memoryIds: readonly string[]) => void;\n}\n\nexport interface ListSuiteMemoriesInputV1 {\n\treadonly owner: string;\n\treadonly suiteId: string;\n}\n\n/**\n * The suite read-boundary DOMAIN a memory operation is scoped to: a real suite\n * (方案) membership, or the \"legacy\" sentinel domain for atoms recorded\n * without a suite binding (方案系统设计 §6.1). Membership, distinct from the\n * read boundary: an atom belongs to exactly one domain — the suite stored in\n * its `suiteId`, or the legacy domain when absent. A promoted user-default\n * preference is READABLE from every suite but remains a member of its origin\n * domain and is listed/forgotten only there.\n */\nexport type SuiteMemoryDomainV1 = { readonly kind: \"suite\"; readonly suiteId: string } | { readonly kind: \"legacy\" };\n\n/** Domain membership predicate (ownership), not the read boundary. */\nfunction isDomainMember(atom: MemoryAtomV1, domain: SuiteMemoryDomainV1): boolean {\n\tif (domain.kind === \"legacy\") return atom.suiteId === undefined;\n\treturn atom.suiteId === domain.suiteId;\n}\n\n/** Visible name of a domain: the suite id, or the \"legacy\" sentinel. */\nexport function suiteMemoryDomainName(domain: SuiteMemoryDomainV1): string {\n\treturn domain.kind === \"suite\" ? domain.suiteId : LEGACY_MEMORY_DOMAIN;\n}\n\ninterface DomainMembershipPage {\n\treadonly members: readonly CommittedMemoryV1[];\n\treadonly filter: MemorySuiteFilterStatsV1;\n}\n\nfunction domainMembershipPage(store: MemoryStoreV1, owner: string, domain: SuiteMemoryDomainV1): DomainMembershipPage {\n\tconst members: CommittedMemoryV1[] = [];\n\tlet legacySkipped = 0;\n\tlet foreignSuiteSkipped = 0;\n\tfor (const record of store.list({ owner })) {\n\t\tif (isDomainMember(record.atom, domain)) {\n\t\t\tmembers.push(record);\n\t\t} else if (record.atom.suiteId === undefined) {\n\t\t\tlegacySkipped += 1;\n\t\t} else {\n\t\t\tforeignSuiteSkipped += 1;\n\t\t}\n\t}\n\treturn { members, filter: { legacySkipped, foreignSuiteSkipped } };\n}\n\n/**\n * Lists one suite's memories with preference/fact classification and\n * membership skip counters. Promoted user-default preferences stay listed\n * under their origin suite — only their READ visibility crosses suites.\n */\nexport function listSuiteMemories(\n\tdeps: Pick<SuiteMemoryFacadeDepsV1, \"store\">,\n\tinput: ListSuiteMemoriesInputV1,\n): SuiteMemoryListResultV1 {\n\tassertNonEmptyString(input.owner, \"listSuiteMemories owner\");\n\tassertNonEmptyString(input.suiteId, \"listSuiteMemories suiteId\");\n\tconst { members, filter } = domainMembershipPage(deps.store, input.owner, { kind: \"suite\", suiteId: input.suiteId });\n\tconst entries = members.map((record) => toEntry(record));\n\tconst preferenceCount = entries.filter((entry) => entry.isPreference).length;\n\treturn Object.freeze({\n\t\tsuiteId: input.suiteId,\n\t\towner: input.owner,\n\t\tentries: Object.freeze(entries),\n\t\tpreferenceCount,\n\t\tfactCount: entries.length - preferenceCount,\n\t\tfilter,\n\t});\n}\n\n/** Result of {@link listDomainMemories}: one domain's entries plus skip counters. */\nexport interface DomainMemoryListResultV1 {\n\t/** The suite id, or the \"legacy\" sentinel for the no-suite domain. */\n\treadonly domain: string;\n\treadonly owner: string;\n\t/** Deterministic order: store revision ascending (commit order). */\n\treadonly entries: readonly SuiteMemoryListEntryV1[];\n\treadonly preferenceCount: number;\n\treadonly factCount: number;\n\t/** Excluded-record counters for the other domains (设计 §11). */\n\treadonly filter: MemorySuiteFilterStatsV1;\n}\n\nexport interface ListDomainMemoriesInputV1 {\n\treadonly owner: string;\n\treadonly domain: SuiteMemoryDomainV1;\n}\n\n/**\n * Lists one read-boundary DOMAIN's memories (suite membership or the legacy\n * no-suite domain). The legacy domain lists exactly the suite-less atoms;\n * suite-bound atoms count into `foreignSuiteSkipped`.\n */\nexport function listDomainMemories(\n\tdeps: Pick<SuiteMemoryFacadeDepsV1, \"store\">,\n\tinput: ListDomainMemoriesInputV1,\n): DomainMemoryListResultV1 {\n\tassertNonEmptyString(input.owner, \"listDomainMemories owner\");\n\tif (input.domain === null || typeof input.domain !== \"object\" || ![\"suite\", \"legacy\"].includes(input.domain.kind)) {\n\t\tthrow new Error(\"listDomainMemories requires a suite or legacy domain\");\n\t}\n\tif (input.domain.kind === \"suite\") assertNonEmptyString(input.domain.suiteId, \"listDomainMemories suiteId\");\n\tconst { members, filter } = domainMembershipPage(deps.store, input.owner, input.domain);\n\tconst entries = members.map((record) => toEntry(record));\n\tconst preferenceCount = entries.filter((entry) => entry.isPreference).length;\n\treturn Object.freeze({\n\t\tdomain: suiteMemoryDomainName(input.domain),\n\t\towner: input.owner,\n\t\tentries: Object.freeze(entries),\n\t\tpreferenceCount,\n\t\tfactCount: entries.length - preferenceCount,\n\t\tfilter,\n\t});\n}\n\nfunction toEntry(record: CommittedMemoryV1): SuiteMemoryListEntryV1 {\n\tconst atom = record.atom;\n\tconst preference = atom.preference;\n\treturn Object.freeze({\n\t\tmemoryId: atom.memoryId,\n\t\tmemoryKind: atom.memoryKind,\n\t\tisPreference: atom.memoryKind === \"preference\",\n\t\tscope: atom.scope,\n\t\toccurredAt: atom.occurredAt,\n\t\tpayload: atom.payload,\n\t\t...(preference === undefined\n\t\t\t? {}\n\t\t\t: {\n\t\t\t\t\tpreference: Object.freeze({\n\t\t\t\t\t\tsubject: preference.subject,\n\t\t\t\t\t\tkey: preference.key,\n\t\t\t\t\t\tscopeLevel: preference.scope.level,\n\t\t\t\t\t\tcrossSuiteVisible: preference.scope.level === \"user-default\" && preference.confirmedAt !== undefined,\n\t\t\t\t\t}),\n\t\t\t\t}),\n\t});\n}\n\nexport interface ForgetSuiteMemoriesInputV1 {\n\treadonly owner: string;\n\treadonly suiteId: string;\n\treadonly authorizedBy: string;\n\treadonly authorizationRef: SuiteForgetAuthorizationRefV1;\n}\n\nexport type SuiteForgetResultV1 =\n\t| {\n\t\t\treadonly status: \"nothing_to_purge\";\n\t\t\treadonly suiteId: string;\n\t\t\treadonly filter: MemorySuiteFilterStatsV1;\n\t }\n\t| {\n\t\t\treadonly status: \"completed\" | \"purge_eligible\";\n\t\t\treadonly suiteId: string;\n\t\t\treadonly batchId: string;\n\t\t\treadonly memoryIds: readonly string[];\n\t\t\treadonly confirmedReplicas: readonly string[];\n\t\t\treadonly failedReplicas?: readonly { readonly replicaId: string; readonly error: string }[];\n\t };\n\n/**\n * Forgets every memory belonging to one suite (membership = the atom's\n * `suiteId`): circles the atom set, runs it through the purge gate\n * (`user-immediate` requires `confirmationRef`, `profile-policy` requires\n * `policyServiceRef`), journals authorization and per-replica confirmations,\n * and completes the journal only when the barrier fully confirmed. A partial\n * failure keeps the batch `purge_eligible` — retry with\n * {@link retrySuiteForget} using the returned batchId. `purgeMemories` is the\n * host-injected canonical-replica purge; it must be idempotent (the gate\n * re-runs it on retry). Batch ids come from the gate's `batchIdFactory`\n * (gate construction option).\n */\nexport function forgetSuiteMemories(\n\tdeps: SuiteMemoryFacadeDepsV1,\n\tinput: ForgetSuiteMemoriesInputV1,\n\tpurgeMemories: (memoryIds: readonly string[]) => void,\n): SuiteForgetResultV1 {\n\tassertNonEmptyString(input.owner, \"forgetSuiteMemories owner\");\n\tassertNonEmptyString(input.suiteId, \"forgetSuiteMemories suiteId\");\n\tassertNonEmptyString(input.authorizedBy, \"forgetSuiteMemories authorizedBy\");\n\t// Runtime guard for non-TS callers: the static type already excludes\n\t// retention-expiry, but the gate alone would accept it.\n\tif ((input.authorizationRef as MemoryPurgeAuthorizationRefV1).mode === \"retention-expiry\") {\n\t\tthrow new Error(\"forgetSuiteMemories requires user-immediate or profile-policy authorization\");\n\t}\n\tconst now = deps.now ?? (() => Date.now());\n\tconst { members, filter } = domainMembershipPage(deps.store, input.owner, { kind: \"suite\", suiteId: input.suiteId });\n\tconst memoryIds = members.map((record) => record.atom.memoryId);\n\tif (memoryIds.length === 0) {\n\t\treturn Object.freeze({ status: \"nothing_to_purge\", suiteId: input.suiteId, filter });\n\t}\n\tconst batch = deps.purgeGate.authorizeBatch({\n\t\tmemoryIds,\n\t\treason: `suite-forget:${input.suiteId}`,\n\t\tauthorizedBy: input.authorizedBy,\n\t\tauthorizationRef: input.authorizationRef,\n\t});\n\tdeps.purgeJournal.authorize({\n\t\tbatchId: batch.batchId,\n\t\tmemoryIds: batch.memoryIds,\n\t\treplicas: batch.replicas,\n\t\tauthorizedBy: batch.authorizedBy,\n\t\treason: batch.reason,\n\t\tat: now(),\n\t});\n\tconst outcome = executePurgeBatch(deps, batch.batchId, purgeMemories, now);\n\treturn Object.freeze({\n\t\tstatus: outcome.status,\n\t\tsuiteId: input.suiteId,\n\t\tbatchId: batch.batchId,\n\t\tmemoryIds: outcome.memoryIds,\n\t\tconfirmedReplicas: outcome.confirmedReplicas,\n\t\t...(outcome.failedReplicas === undefined ? {} : { failedReplicas: outcome.failedReplicas }),\n\t}) as SuiteForgetResultV1;\n}\n\n/**\n * Idempotent retry of a `purge_eligible` suite-forget batch. Unknown batch ids\n * throw; already-completed batches are a no-op that reports `completed`\n * (gate semantics).\n */\nexport function retrySuiteForget(\n\tdeps: Pick<SuiteMemoryFacadeDepsV1, \"purgeGate\" | \"purgeJournal\" | \"purgeIndexReplica\" | \"now\">,\n\tinput: { readonly batchId: string; readonly suiteId: string },\n\tpurgeMemories: (memoryIds: readonly string[]) => void,\n): SuiteForgetResultV1 {\n\tassertNonEmptyString(input.batchId, \"retrySuiteForget batchId\");\n\tassertNonEmptyString(input.suiteId, \"retrySuiteForget suiteId\");\n\tconst now = deps.now ?? (() => Date.now());\n\tconst batch = deps.purgeGate.batchState(input.batchId);\n\tif (!batch) throw new Error(`Unknown purge batch: ${input.batchId}`);\n\tconst outcome = executePurgeBatch(deps, input.batchId, purgeMemories, now);\n\treturn Object.freeze({\n\t\tstatus: outcome.status,\n\t\tsuiteId: input.suiteId,\n\t\tbatchId: input.batchId,\n\t\tmemoryIds: outcome.memoryIds,\n\t\tconfirmedReplicas: outcome.confirmedReplicas,\n\t\t...(outcome.failedReplicas === undefined ? {} : { failedReplicas: outcome.failedReplicas }),\n\t}) as SuiteForgetResultV1;\n}\n\ninterface PurgeBatchOutcome {\n\treadonly status: \"completed\" | \"purge_eligible\";\n\treadonly memoryIds: readonly string[];\n\treadonly confirmedReplicas: readonly string[];\n\treadonly failedReplicas?: readonly { readonly replicaId: string; readonly error: string }[];\n}\n\n/**\n * Shared gate+journal batch execution behind both forget facades: completed\n * batches are idempotent no-ops, replica confirmations are replay-safe, and\n * the journal completes only after the barrier fully confirmed.\n */\nfunction executePurgeBatch(\n\tdeps: Pick<SuiteMemoryFacadeDepsV1, \"purgeGate\" | \"purgeJournal\" | \"purgeIndexReplica\">,\n\tbatchId: string,\n\tpurgeMemories: (memoryIds: readonly string[]) => void,\n\tnow: () => number,\n): PurgeBatchOutcome {\n\t// Idempotent no-op: the journal already holds the completion record, so a\n\t// re-run must not re-execute the replica purge nor replay journal entries.\n\tif (deps.purgeJournal.batchState(batchId)?.state === \"completed\") {\n\t\tconst batch = deps.purgeGate.batchState(batchId);\n\t\tif (!batch) throw new Error(`Unknown purge batch: ${batchId}`);\n\t\treturn {\n\t\t\tstatus: \"completed\",\n\t\t\tmemoryIds: batch.memoryIds,\n\t\t\tconfirmedReplicas: Object.freeze([...(batch.confirmedReplicas ?? [])]),\n\t\t};\n\t}\n\t// 按副本分派 (D-035, 混合检索工程化): index 副本走向量投影清除回调; 其余\n\t// 副本维持原 purgeMemories 路径 (canonical 行为零变化)。index 回调缺省时\n\t// no-op 确认——副本契约要求 purge 幂等可重试, no-op 满足。\n\tconst outcome = deps.purgeGate.executeBatch(batchId, (replica: MemoryReplicaV1, memoryIds) => {\n\t\tif (replica.kind === \"index\") {\n\t\t\tif (deps.purgeIndexReplica === undefined) return;\n\t\t\tdeps.purgeIndexReplica(memoryIds);\n\t\t\treturn;\n\t\t}\n\t\tpurgeMemories(memoryIds);\n\t});\n\t// Journal confirmations are idempotent per (batchId, replicaId); replays\n\t// from a retry never double-count.\n\tfor (const replica of outcome.replicas) {\n\t\tif (outcome.confirmedReplicas?.includes(replica.replicaId)) {\n\t\t\tdeps.purgeJournal.confirmReplica({ batchId, replicaId: replica.replicaId, at: now() });\n\t\t}\n\t}\n\tif (outcome.state === \"completed\") {\n\t\tdeps.purgeJournal.complete({ batchId, at: now() });\n\t}\n\treturn {\n\t\tstatus: outcome.state as \"completed\" | \"purge_eligible\",\n\t\tmemoryIds: outcome.memoryIds,\n\t\tconfirmedReplicas: Object.freeze([...(outcome.confirmedReplicas ?? [])]),\n\t\t...(outcome.failedReplicas === undefined ? {} : { failedReplicas: outcome.failedReplicas }),\n\t};\n}\n\nexport interface ForgetDomainMemoryInputV1 {\n\treadonly owner: string;\n\t/** The read-boundary domain the CALLER is operating in (tool-side isolation). */\n\treadonly domain: SuiteMemoryDomainV1;\n\t/** The single memory to forget; it must be a member of the caller's domain. */\n\treadonly memoryId: string;\n\treadonly authorizedBy: string;\n\treadonly authorizationRef: SuiteForgetAuthorizationRefV1;\n}\n\nexport type DomainMemoryForgetResultV1 =\n\t/** No memory with this id under the owner. */\n\t| { readonly status: \"not_found\"; readonly memoryId: string }\n\t/** The memory exists but belongs to another domain — refused, no gate run. */\n\t| { readonly status: \"foreign_domain\"; readonly memoryId: string }\n\t| {\n\t\t\treadonly status: \"completed\" | \"purge_eligible\";\n\t\t\treadonly batchId: string;\n\t\t\treadonly memoryIds: readonly string[];\n\t\t\treadonly confirmedReplicas: readonly string[];\n\t\t\treadonly failedReplicas?: readonly { readonly replicaId: string; readonly error: string }[];\n\t };\n\n/**\n * Forgets ONE memory on behalf of a caller scoped to a read-boundary domain\n * (assistant 场景可控性硬需求, 方案系统设计 §6.1): the atom must be a member of\n * the caller's domain (its `suiteId` matches, or it is suite-less for the\n * legacy domain) — a caller can never forget another suite's atoms by id.\n * Deletion goes through the canonical purge gate (user-immediate or\n * profile-policy authorization) plus the purge journal, exactly like\n * {@link forgetSuiteMemories}; the injected `purgeMemories` callback performs\n * the physical replica purge (e.g. the durable ledger rewrite plus store\n * eviction) and must be idempotent for `purge_eligible` retries.\n */\nexport function forgetDomainMemory(\n\tdeps: SuiteMemoryFacadeDepsV1,\n\tinput: ForgetDomainMemoryInputV1,\n\tpurgeMemories: (memoryIds: readonly string[]) => void,\n): DomainMemoryForgetResultV1 {\n\tassertNonEmptyString(input.owner, \"forgetDomainMemory owner\");\n\tassertNonEmptyString(input.memoryId, \"forgetDomainMemory memoryId\");\n\tassertNonEmptyString(input.authorizedBy, \"forgetDomainMemory authorizedBy\");\n\tif (input.domain === null || typeof input.domain !== \"object\" || ![\"suite\", \"legacy\"].includes(input.domain.kind)) {\n\t\tthrow new Error(\"forgetDomainMemory requires a suite or legacy domain\");\n\t}\n\tif (input.domain.kind === \"suite\") assertNonEmptyString(input.domain.suiteId, \"forgetDomainMemory suiteId\");\n\t// Runtime guard for non-TS callers: the static type already excludes\n\t// retention-expiry, but the gate alone would accept it.\n\tif ((input.authorizationRef as MemoryPurgeAuthorizationRefV1).mode === \"retention-expiry\") {\n\t\tthrow new Error(\"forgetDomainMemory requires user-immediate or profile-policy authorization\");\n\t}\n\tconst now = deps.now ?? (() => Date.now());\n\tconst record = deps.store.get(input.memoryId, { owner: input.owner });\n\tif (!record) return Object.freeze({ status: \"not_found\", memoryId: input.memoryId });\n\tif (!isDomainMember(record.atom, input.domain)) {\n\t\treturn Object.freeze({ status: \"foreign_domain\", memoryId: input.memoryId });\n\t}\n\tconst domainName = suiteMemoryDomainName(input.domain);\n\tconst batch = deps.purgeGate.authorizeBatch({\n\t\tmemoryIds: [input.memoryId],\n\t\treason: `domain-forget:${domainName}`,\n\t\tauthorizedBy: input.authorizedBy,\n\t\tauthorizationRef: input.authorizationRef,\n\t});\n\tdeps.purgeJournal.authorize({\n\t\tbatchId: batch.batchId,\n\t\tmemoryIds: batch.memoryIds,\n\t\treplicas: batch.replicas,\n\t\tauthorizedBy: batch.authorizedBy,\n\t\treason: batch.reason,\n\t\tat: now(),\n\t});\n\tconst outcome = executePurgeBatch(deps, batch.batchId, purgeMemories, now);\n\treturn Object.freeze({\n\t\tstatus: outcome.status,\n\t\tbatchId: batch.batchId,\n\t\tmemoryIds: outcome.memoryIds,\n\t\tconfirmedReplicas: outcome.confirmedReplicas,\n\t\t...(outcome.failedReplicas === undefined ? {} : { failedReplicas: outcome.failedReplicas }),\n\t}) as DomainMemoryForgetResultV1;\n}\n"]}
@@ -0,0 +1,288 @@
1
+ /**
2
+ * Suite memory facade (M5, 方案系统设计 §6.1 + 方案实施计划 §10) — the library
3
+ * surface a host uses to LIST and FORGET one suite's memories.
4
+ *
5
+ * The listing is the user-visible control surface for assistant-style personal
6
+ * memory (显式管理命令:列出/忘记 — controllability is a hard requirement when
7
+ * personal information is stored). Forgetting is the M4 suite-deletion hook:
8
+ * the atom set circled by suiteId goes through the canonical purge gate
9
+ * (user-immediate or profile-policy authorization) plus the purge journal, so
10
+ * authorization is structured, partial failures stay `purge_eligible` for an
11
+ * idempotent retry, and every step leaves an audit record. The physical
12
+ * replica purge is injected — this facade never reaches into store internals.
13
+ */
14
+ import { LEGACY_MEMORY_DOMAIN } from "./ledger.js";
15
+ function assertNonEmptyString(value, label) {
16
+ if (typeof value !== "string" || value.trim().length === 0)
17
+ throw new Error(`${label} must be a non-empty string`);
18
+ }
19
+ /** Domain membership predicate (ownership), not the read boundary. */
20
+ function isDomainMember(atom, domain) {
21
+ if (domain.kind === "legacy")
22
+ return atom.suiteId === undefined;
23
+ return atom.suiteId === domain.suiteId;
24
+ }
25
+ /** Visible name of a domain: the suite id, or the "legacy" sentinel. */
26
+ export function suiteMemoryDomainName(domain) {
27
+ return domain.kind === "suite" ? domain.suiteId : LEGACY_MEMORY_DOMAIN;
28
+ }
29
+ function domainMembershipPage(store, owner, domain) {
30
+ const members = [];
31
+ let legacySkipped = 0;
32
+ let foreignSuiteSkipped = 0;
33
+ for (const record of store.list({ owner })) {
34
+ if (isDomainMember(record.atom, domain)) {
35
+ members.push(record);
36
+ }
37
+ else if (record.atom.suiteId === undefined) {
38
+ legacySkipped += 1;
39
+ }
40
+ else {
41
+ foreignSuiteSkipped += 1;
42
+ }
43
+ }
44
+ return { members, filter: { legacySkipped, foreignSuiteSkipped } };
45
+ }
46
+ /**
47
+ * Lists one suite's memories with preference/fact classification and
48
+ * membership skip counters. Promoted user-default preferences stay listed
49
+ * under their origin suite — only their READ visibility crosses suites.
50
+ */
51
+ export function listSuiteMemories(deps, input) {
52
+ assertNonEmptyString(input.owner, "listSuiteMemories owner");
53
+ assertNonEmptyString(input.suiteId, "listSuiteMemories suiteId");
54
+ const { members, filter } = domainMembershipPage(deps.store, input.owner, { kind: "suite", suiteId: input.suiteId });
55
+ const entries = members.map((record) => toEntry(record));
56
+ const preferenceCount = entries.filter((entry) => entry.isPreference).length;
57
+ return Object.freeze({
58
+ suiteId: input.suiteId,
59
+ owner: input.owner,
60
+ entries: Object.freeze(entries),
61
+ preferenceCount,
62
+ factCount: entries.length - preferenceCount,
63
+ filter,
64
+ });
65
+ }
66
+ /**
67
+ * Lists one read-boundary DOMAIN's memories (suite membership or the legacy
68
+ * no-suite domain). The legacy domain lists exactly the suite-less atoms;
69
+ * suite-bound atoms count into `foreignSuiteSkipped`.
70
+ */
71
+ export function listDomainMemories(deps, input) {
72
+ assertNonEmptyString(input.owner, "listDomainMemories owner");
73
+ if (input.domain === null || typeof input.domain !== "object" || !["suite", "legacy"].includes(input.domain.kind)) {
74
+ throw new Error("listDomainMemories requires a suite or legacy domain");
75
+ }
76
+ if (input.domain.kind === "suite")
77
+ assertNonEmptyString(input.domain.suiteId, "listDomainMemories suiteId");
78
+ const { members, filter } = domainMembershipPage(deps.store, input.owner, input.domain);
79
+ const entries = members.map((record) => toEntry(record));
80
+ const preferenceCount = entries.filter((entry) => entry.isPreference).length;
81
+ return Object.freeze({
82
+ domain: suiteMemoryDomainName(input.domain),
83
+ owner: input.owner,
84
+ entries: Object.freeze(entries),
85
+ preferenceCount,
86
+ factCount: entries.length - preferenceCount,
87
+ filter,
88
+ });
89
+ }
90
+ function toEntry(record) {
91
+ const atom = record.atom;
92
+ const preference = atom.preference;
93
+ return Object.freeze({
94
+ memoryId: atom.memoryId,
95
+ memoryKind: atom.memoryKind,
96
+ isPreference: atom.memoryKind === "preference",
97
+ scope: atom.scope,
98
+ occurredAt: atom.occurredAt,
99
+ payload: atom.payload,
100
+ ...(preference === undefined
101
+ ? {}
102
+ : {
103
+ preference: Object.freeze({
104
+ subject: preference.subject,
105
+ key: preference.key,
106
+ scopeLevel: preference.scope.level,
107
+ crossSuiteVisible: preference.scope.level === "user-default" && preference.confirmedAt !== undefined,
108
+ }),
109
+ }),
110
+ });
111
+ }
112
+ /**
113
+ * Forgets every memory belonging to one suite (membership = the atom's
114
+ * `suiteId`): circles the atom set, runs it through the purge gate
115
+ * (`user-immediate` requires `confirmationRef`, `profile-policy` requires
116
+ * `policyServiceRef`), journals authorization and per-replica confirmations,
117
+ * and completes the journal only when the barrier fully confirmed. A partial
118
+ * failure keeps the batch `purge_eligible` — retry with
119
+ * {@link retrySuiteForget} using the returned batchId. `purgeMemories` is the
120
+ * host-injected canonical-replica purge; it must be idempotent (the gate
121
+ * re-runs it on retry). Batch ids come from the gate's `batchIdFactory`
122
+ * (gate construction option).
123
+ */
124
+ export function forgetSuiteMemories(deps, input, purgeMemories) {
125
+ assertNonEmptyString(input.owner, "forgetSuiteMemories owner");
126
+ assertNonEmptyString(input.suiteId, "forgetSuiteMemories suiteId");
127
+ assertNonEmptyString(input.authorizedBy, "forgetSuiteMemories authorizedBy");
128
+ // Runtime guard for non-TS callers: the static type already excludes
129
+ // retention-expiry, but the gate alone would accept it.
130
+ if (input.authorizationRef.mode === "retention-expiry") {
131
+ throw new Error("forgetSuiteMemories requires user-immediate or profile-policy authorization");
132
+ }
133
+ const now = deps.now ?? (() => Date.now());
134
+ const { members, filter } = domainMembershipPage(deps.store, input.owner, { kind: "suite", suiteId: input.suiteId });
135
+ const memoryIds = members.map((record) => record.atom.memoryId);
136
+ if (memoryIds.length === 0) {
137
+ return Object.freeze({ status: "nothing_to_purge", suiteId: input.suiteId, filter });
138
+ }
139
+ const batch = deps.purgeGate.authorizeBatch({
140
+ memoryIds,
141
+ reason: `suite-forget:${input.suiteId}`,
142
+ authorizedBy: input.authorizedBy,
143
+ authorizationRef: input.authorizationRef,
144
+ });
145
+ deps.purgeJournal.authorize({
146
+ batchId: batch.batchId,
147
+ memoryIds: batch.memoryIds,
148
+ replicas: batch.replicas,
149
+ authorizedBy: batch.authorizedBy,
150
+ reason: batch.reason,
151
+ at: now(),
152
+ });
153
+ const outcome = executePurgeBatch(deps, batch.batchId, purgeMemories, now);
154
+ return Object.freeze({
155
+ status: outcome.status,
156
+ suiteId: input.suiteId,
157
+ batchId: batch.batchId,
158
+ memoryIds: outcome.memoryIds,
159
+ confirmedReplicas: outcome.confirmedReplicas,
160
+ ...(outcome.failedReplicas === undefined ? {} : { failedReplicas: outcome.failedReplicas }),
161
+ });
162
+ }
163
+ /**
164
+ * Idempotent retry of a `purge_eligible` suite-forget batch. Unknown batch ids
165
+ * throw; already-completed batches are a no-op that reports `completed`
166
+ * (gate semantics).
167
+ */
168
+ export function retrySuiteForget(deps, input, purgeMemories) {
169
+ assertNonEmptyString(input.batchId, "retrySuiteForget batchId");
170
+ assertNonEmptyString(input.suiteId, "retrySuiteForget suiteId");
171
+ const now = deps.now ?? (() => Date.now());
172
+ const batch = deps.purgeGate.batchState(input.batchId);
173
+ if (!batch)
174
+ throw new Error(`Unknown purge batch: ${input.batchId}`);
175
+ const outcome = executePurgeBatch(deps, input.batchId, purgeMemories, now);
176
+ return Object.freeze({
177
+ status: outcome.status,
178
+ suiteId: input.suiteId,
179
+ batchId: input.batchId,
180
+ memoryIds: outcome.memoryIds,
181
+ confirmedReplicas: outcome.confirmedReplicas,
182
+ ...(outcome.failedReplicas === undefined ? {} : { failedReplicas: outcome.failedReplicas }),
183
+ });
184
+ }
185
+ /**
186
+ * Shared gate+journal batch execution behind both forget facades: completed
187
+ * batches are idempotent no-ops, replica confirmations are replay-safe, and
188
+ * the journal completes only after the barrier fully confirmed.
189
+ */
190
+ function executePurgeBatch(deps, batchId, purgeMemories, now) {
191
+ // Idempotent no-op: the journal already holds the completion record, so a
192
+ // re-run must not re-execute the replica purge nor replay journal entries.
193
+ if (deps.purgeJournal.batchState(batchId)?.state === "completed") {
194
+ const batch = deps.purgeGate.batchState(batchId);
195
+ if (!batch)
196
+ throw new Error(`Unknown purge batch: ${batchId}`);
197
+ return {
198
+ status: "completed",
199
+ memoryIds: batch.memoryIds,
200
+ confirmedReplicas: Object.freeze([...(batch.confirmedReplicas ?? [])]),
201
+ };
202
+ }
203
+ // 按副本分派 (D-035, 混合检索工程化): index 副本走向量投影清除回调; 其余
204
+ // 副本维持原 purgeMemories 路径 (canonical 行为零变化)。index 回调缺省时
205
+ // no-op 确认——副本契约要求 purge 幂等可重试, no-op 满足。
206
+ const outcome = deps.purgeGate.executeBatch(batchId, (replica, memoryIds) => {
207
+ if (replica.kind === "index") {
208
+ if (deps.purgeIndexReplica === undefined)
209
+ return;
210
+ deps.purgeIndexReplica(memoryIds);
211
+ return;
212
+ }
213
+ purgeMemories(memoryIds);
214
+ });
215
+ // Journal confirmations are idempotent per (batchId, replicaId); replays
216
+ // from a retry never double-count.
217
+ for (const replica of outcome.replicas) {
218
+ if (outcome.confirmedReplicas?.includes(replica.replicaId)) {
219
+ deps.purgeJournal.confirmReplica({ batchId, replicaId: replica.replicaId, at: now() });
220
+ }
221
+ }
222
+ if (outcome.state === "completed") {
223
+ deps.purgeJournal.complete({ batchId, at: now() });
224
+ }
225
+ return {
226
+ status: outcome.state,
227
+ memoryIds: outcome.memoryIds,
228
+ confirmedReplicas: Object.freeze([...(outcome.confirmedReplicas ?? [])]),
229
+ ...(outcome.failedReplicas === undefined ? {} : { failedReplicas: outcome.failedReplicas }),
230
+ };
231
+ }
232
+ /**
233
+ * Forgets ONE memory on behalf of a caller scoped to a read-boundary domain
234
+ * (assistant 场景可控性硬需求, 方案系统设计 §6.1): the atom must be a member of
235
+ * the caller's domain (its `suiteId` matches, or it is suite-less for the
236
+ * legacy domain) — a caller can never forget another suite's atoms by id.
237
+ * Deletion goes through the canonical purge gate (user-immediate or
238
+ * profile-policy authorization) plus the purge journal, exactly like
239
+ * {@link forgetSuiteMemories}; the injected `purgeMemories` callback performs
240
+ * the physical replica purge (e.g. the durable ledger rewrite plus store
241
+ * eviction) and must be idempotent for `purge_eligible` retries.
242
+ */
243
+ export function forgetDomainMemory(deps, input, purgeMemories) {
244
+ assertNonEmptyString(input.owner, "forgetDomainMemory owner");
245
+ assertNonEmptyString(input.memoryId, "forgetDomainMemory memoryId");
246
+ assertNonEmptyString(input.authorizedBy, "forgetDomainMemory authorizedBy");
247
+ if (input.domain === null || typeof input.domain !== "object" || !["suite", "legacy"].includes(input.domain.kind)) {
248
+ throw new Error("forgetDomainMemory requires a suite or legacy domain");
249
+ }
250
+ if (input.domain.kind === "suite")
251
+ assertNonEmptyString(input.domain.suiteId, "forgetDomainMemory suiteId");
252
+ // Runtime guard for non-TS callers: the static type already excludes
253
+ // retention-expiry, but the gate alone would accept it.
254
+ if (input.authorizationRef.mode === "retention-expiry") {
255
+ throw new Error("forgetDomainMemory requires user-immediate or profile-policy authorization");
256
+ }
257
+ const now = deps.now ?? (() => Date.now());
258
+ const record = deps.store.get(input.memoryId, { owner: input.owner });
259
+ if (!record)
260
+ return Object.freeze({ status: "not_found", memoryId: input.memoryId });
261
+ if (!isDomainMember(record.atom, input.domain)) {
262
+ return Object.freeze({ status: "foreign_domain", memoryId: input.memoryId });
263
+ }
264
+ const domainName = suiteMemoryDomainName(input.domain);
265
+ const batch = deps.purgeGate.authorizeBatch({
266
+ memoryIds: [input.memoryId],
267
+ reason: `domain-forget:${domainName}`,
268
+ authorizedBy: input.authorizedBy,
269
+ authorizationRef: input.authorizationRef,
270
+ });
271
+ deps.purgeJournal.authorize({
272
+ batchId: batch.batchId,
273
+ memoryIds: batch.memoryIds,
274
+ replicas: batch.replicas,
275
+ authorizedBy: batch.authorizedBy,
276
+ reason: batch.reason,
277
+ at: now(),
278
+ });
279
+ const outcome = executePurgeBatch(deps, batch.batchId, purgeMemories, now);
280
+ return Object.freeze({
281
+ status: outcome.status,
282
+ batchId: batch.batchId,
283
+ memoryIds: outcome.memoryIds,
284
+ confirmedReplicas: outcome.confirmedReplicas,
285
+ ...(outcome.failedReplicas === undefined ? {} : { failedReplicas: outcome.failedReplicas }),
286
+ });
287
+ }
288
+ //# sourceMappingURL=suite-memory.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"suite-memory.js","sourceRoot":"","sources":["../../src/memory/suite-memory.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Suite memory facade (M5, 方案系统设计 §6.1 + 方案实施计划 §10) — the library\n * surface a host uses to LIST and FORGET one suite's memories.\n *\n * The listing is the user-visible control surface for assistant-style personal\n * memory (显式管理命令:列出/忘记 — controllability is a hard requirement when\n * personal information is stored). Forgetting is the M4 suite-deletion hook:\n * the atom set circled by suiteId goes through the canonical purge gate\n * (user-immediate or profile-policy authorization) plus the purge journal, so\n * authorization is structured, partial failures stay `purge_eligible` for an\n * idempotent retry, and every step leaves an audit record. The physical\n * replica purge is injected — this facade never reaches into store internals.\n */\n\nimport type { JsonValue } from \"@agent-forge/plugin-sdk\";\nimport type {\n\tMemoryAtomV1,\n\tMemoryKindV1,\n\tMemoryPreferenceEnvelopeV1,\n\tMemoryScopeV1,\n\tMemorySuiteFilterStatsV1,\n} from \"./foundation.ts\";\nimport { LEGACY_MEMORY_DOMAIN } from \"./ledger.ts\";\nimport type { MemoryPurgeAuthorizationRefV1, MemoryPurgeGateV1, MemoryReplicaV1 } from \"./purge.ts\";\nimport type { MemoryPurgeJournalV1 } from \"./purge-journal.ts\";\nimport type { CommittedMemoryV1, MemoryStoreV1 } from \"./store.ts\";\n\nfunction assertNonEmptyString(value: unknown, label: string): asserts value is string {\n\tif (typeof value !== \"string\" || value.trim().length === 0) throw new Error(`${label} must be a non-empty string`);\n}\n\n/**\n * Structured authorization for forgetting a suite. Suite deletion is a bulk,\n * user- or policy-initiated destructive action, so the unstructured\n * `authorizedBy`-only path and `retention-expiry` mode are not accepted.\n */\nexport type SuiteForgetAuthorizationRefV1 =\n\t| {\n\t\t\treadonly mode: \"user-immediate\";\n\t\t\treadonly issuedAt: number;\n\t\t\treadonly issuedBy: string;\n\t\t\treadonly confirmationRef: string;\n\t }\n\t| {\n\t\t\treadonly mode: \"profile-policy\";\n\t\t\treadonly issuedAt: number;\n\t\t\treadonly issuedBy: string;\n\t\t\treadonly policyServiceRef: string;\n\t };\n\n/** One user-visible suite memory entry (列出语义). */\nexport interface SuiteMemoryListEntryV1 {\n\treadonly memoryId: string;\n\treadonly memoryKind: MemoryKindV1;\n\treadonly isPreference: boolean;\n\treadonly scope: MemoryScopeV1;\n\treadonly occurredAt: string;\n\treadonly payload: JsonValue;\n\t/** Present for preference atoms: the preference envelope summary. */\n\treadonly preference?: {\n\t\treadonly subject: MemoryPreferenceEnvelopeV1[\"subject\"];\n\t\treadonly key: string;\n\t\treadonly scopeLevel: MemoryPreferenceEnvelopeV1[\"scope\"][\"level\"];\n\t\t/** True when explicitly promoted (user-default + confirmedAt) — cross-suite visible. */\n\t\treadonly crossSuiteVisible: boolean;\n\t};\n}\n\nexport interface SuiteMemoryListResultV1 {\n\treadonly suiteId: string;\n\treadonly owner: string;\n\t/** Deterministic order: store revision ascending (commit order). */\n\treadonly entries: readonly SuiteMemoryListEntryV1[];\n\treadonly preferenceCount: number;\n\treadonly factCount: number;\n\t/** Legacy/foreign-suite records excluded by the suite read boundary (设计 §11). */\n\treadonly filter: MemorySuiteFilterStatsV1;\n}\n\nexport interface SuiteMemoryFacadeDepsV1 {\n\treadonly store: MemoryStoreV1;\n\treadonly purgeGate: MemoryPurgeGateV1;\n\treadonly purgeJournal: MemoryPurgeJournalV1;\n\treadonly now?: () => number;\n\t/**\n\t * index 副本 (如 memory-vector-index, 混合检索工程化) 的物理清除回调。缺省\n\t * undefined 时 index 副本分派为 no-op 且不报错——向量通道未装配 (off) 或\n\t * 不可用 (disabled) 时索引里没有本会话写入的行, 无物可清。canonical 及其余\n\t * 副本仍走 `purgeMemories` 回调 (行为零变化)。\n\t */\n\treadonly purgeIndexReplica?: (memoryIds: readonly string[]) => void;\n}\n\nexport interface ListSuiteMemoriesInputV1 {\n\treadonly owner: string;\n\treadonly suiteId: string;\n}\n\n/**\n * The suite read-boundary DOMAIN a memory operation is scoped to: a real suite\n * (方案) membership, or the \"legacy\" sentinel domain for atoms recorded\n * without a suite binding (方案系统设计 §6.1). Membership, distinct from the\n * read boundary: an atom belongs to exactly one domain — the suite stored in\n * its `suiteId`, or the legacy domain when absent. A promoted user-default\n * preference is READABLE from every suite but remains a member of its origin\n * domain and is listed/forgotten only there.\n */\nexport type SuiteMemoryDomainV1 = { readonly kind: \"suite\"; readonly suiteId: string } | { readonly kind: \"legacy\" };\n\n/** Domain membership predicate (ownership), not the read boundary. */\nfunction isDomainMember(atom: MemoryAtomV1, domain: SuiteMemoryDomainV1): boolean {\n\tif (domain.kind === \"legacy\") return atom.suiteId === undefined;\n\treturn atom.suiteId === domain.suiteId;\n}\n\n/** Visible name of a domain: the suite id, or the \"legacy\" sentinel. */\nexport function suiteMemoryDomainName(domain: SuiteMemoryDomainV1): string {\n\treturn domain.kind === \"suite\" ? domain.suiteId : LEGACY_MEMORY_DOMAIN;\n}\n\ninterface DomainMembershipPage {\n\treadonly members: readonly CommittedMemoryV1[];\n\treadonly filter: MemorySuiteFilterStatsV1;\n}\n\nfunction domainMembershipPage(store: MemoryStoreV1, owner: string, domain: SuiteMemoryDomainV1): DomainMembershipPage {\n\tconst members: CommittedMemoryV1[] = [];\n\tlet legacySkipped = 0;\n\tlet foreignSuiteSkipped = 0;\n\tfor (const record of store.list({ owner })) {\n\t\tif (isDomainMember(record.atom, domain)) {\n\t\t\tmembers.push(record);\n\t\t} else if (record.atom.suiteId === undefined) {\n\t\t\tlegacySkipped += 1;\n\t\t} else {\n\t\t\tforeignSuiteSkipped += 1;\n\t\t}\n\t}\n\treturn { members, filter: { legacySkipped, foreignSuiteSkipped } };\n}\n\n/**\n * Lists one suite's memories with preference/fact classification and\n * membership skip counters. Promoted user-default preferences stay listed\n * under their origin suite — only their READ visibility crosses suites.\n */\nexport function listSuiteMemories(\n\tdeps: Pick<SuiteMemoryFacadeDepsV1, \"store\">,\n\tinput: ListSuiteMemoriesInputV1,\n): SuiteMemoryListResultV1 {\n\tassertNonEmptyString(input.owner, \"listSuiteMemories owner\");\n\tassertNonEmptyString(input.suiteId, \"listSuiteMemories suiteId\");\n\tconst { members, filter } = domainMembershipPage(deps.store, input.owner, { kind: \"suite\", suiteId: input.suiteId });\n\tconst entries = members.map((record) => toEntry(record));\n\tconst preferenceCount = entries.filter((entry) => entry.isPreference).length;\n\treturn Object.freeze({\n\t\tsuiteId: input.suiteId,\n\t\towner: input.owner,\n\t\tentries: Object.freeze(entries),\n\t\tpreferenceCount,\n\t\tfactCount: entries.length - preferenceCount,\n\t\tfilter,\n\t});\n}\n\n/** Result of {@link listDomainMemories}: one domain's entries plus skip counters. */\nexport interface DomainMemoryListResultV1 {\n\t/** The suite id, or the \"legacy\" sentinel for the no-suite domain. */\n\treadonly domain: string;\n\treadonly owner: string;\n\t/** Deterministic order: store revision ascending (commit order). */\n\treadonly entries: readonly SuiteMemoryListEntryV1[];\n\treadonly preferenceCount: number;\n\treadonly factCount: number;\n\t/** Excluded-record counters for the other domains (设计 §11). */\n\treadonly filter: MemorySuiteFilterStatsV1;\n}\n\nexport interface ListDomainMemoriesInputV1 {\n\treadonly owner: string;\n\treadonly domain: SuiteMemoryDomainV1;\n}\n\n/**\n * Lists one read-boundary DOMAIN's memories (suite membership or the legacy\n * no-suite domain). The legacy domain lists exactly the suite-less atoms;\n * suite-bound atoms count into `foreignSuiteSkipped`.\n */\nexport function listDomainMemories(\n\tdeps: Pick<SuiteMemoryFacadeDepsV1, \"store\">,\n\tinput: ListDomainMemoriesInputV1,\n): DomainMemoryListResultV1 {\n\tassertNonEmptyString(input.owner, \"listDomainMemories owner\");\n\tif (input.domain === null || typeof input.domain !== \"object\" || ![\"suite\", \"legacy\"].includes(input.domain.kind)) {\n\t\tthrow new Error(\"listDomainMemories requires a suite or legacy domain\");\n\t}\n\tif (input.domain.kind === \"suite\") assertNonEmptyString(input.domain.suiteId, \"listDomainMemories suiteId\");\n\tconst { members, filter } = domainMembershipPage(deps.store, input.owner, input.domain);\n\tconst entries = members.map((record) => toEntry(record));\n\tconst preferenceCount = entries.filter((entry) => entry.isPreference).length;\n\treturn Object.freeze({\n\t\tdomain: suiteMemoryDomainName(input.domain),\n\t\towner: input.owner,\n\t\tentries: Object.freeze(entries),\n\t\tpreferenceCount,\n\t\tfactCount: entries.length - preferenceCount,\n\t\tfilter,\n\t});\n}\n\nfunction toEntry(record: CommittedMemoryV1): SuiteMemoryListEntryV1 {\n\tconst atom = record.atom;\n\tconst preference = atom.preference;\n\treturn Object.freeze({\n\t\tmemoryId: atom.memoryId,\n\t\tmemoryKind: atom.memoryKind,\n\t\tisPreference: atom.memoryKind === \"preference\",\n\t\tscope: atom.scope,\n\t\toccurredAt: atom.occurredAt,\n\t\tpayload: atom.payload,\n\t\t...(preference === undefined\n\t\t\t? {}\n\t\t\t: {\n\t\t\t\t\tpreference: Object.freeze({\n\t\t\t\t\t\tsubject: preference.subject,\n\t\t\t\t\t\tkey: preference.key,\n\t\t\t\t\t\tscopeLevel: preference.scope.level,\n\t\t\t\t\t\tcrossSuiteVisible: preference.scope.level === \"user-default\" && preference.confirmedAt !== undefined,\n\t\t\t\t\t}),\n\t\t\t\t}),\n\t});\n}\n\nexport interface ForgetSuiteMemoriesInputV1 {\n\treadonly owner: string;\n\treadonly suiteId: string;\n\treadonly authorizedBy: string;\n\treadonly authorizationRef: SuiteForgetAuthorizationRefV1;\n}\n\nexport type SuiteForgetResultV1 =\n\t| {\n\t\t\treadonly status: \"nothing_to_purge\";\n\t\t\treadonly suiteId: string;\n\t\t\treadonly filter: MemorySuiteFilterStatsV1;\n\t }\n\t| {\n\t\t\treadonly status: \"completed\" | \"purge_eligible\";\n\t\t\treadonly suiteId: string;\n\t\t\treadonly batchId: string;\n\t\t\treadonly memoryIds: readonly string[];\n\t\t\treadonly confirmedReplicas: readonly string[];\n\t\t\treadonly failedReplicas?: readonly { readonly replicaId: string; readonly error: string }[];\n\t };\n\n/**\n * Forgets every memory belonging to one suite (membership = the atom's\n * `suiteId`): circles the atom set, runs it through the purge gate\n * (`user-immediate` requires `confirmationRef`, `profile-policy` requires\n * `policyServiceRef`), journals authorization and per-replica confirmations,\n * and completes the journal only when the barrier fully confirmed. A partial\n * failure keeps the batch `purge_eligible` — retry with\n * {@link retrySuiteForget} using the returned batchId. `purgeMemories` is the\n * host-injected canonical-replica purge; it must be idempotent (the gate\n * re-runs it on retry). Batch ids come from the gate's `batchIdFactory`\n * (gate construction option).\n */\nexport function forgetSuiteMemories(\n\tdeps: SuiteMemoryFacadeDepsV1,\n\tinput: ForgetSuiteMemoriesInputV1,\n\tpurgeMemories: (memoryIds: readonly string[]) => void,\n): SuiteForgetResultV1 {\n\tassertNonEmptyString(input.owner, \"forgetSuiteMemories owner\");\n\tassertNonEmptyString(input.suiteId, \"forgetSuiteMemories suiteId\");\n\tassertNonEmptyString(input.authorizedBy, \"forgetSuiteMemories authorizedBy\");\n\t// Runtime guard for non-TS callers: the static type already excludes\n\t// retention-expiry, but the gate alone would accept it.\n\tif ((input.authorizationRef as MemoryPurgeAuthorizationRefV1).mode === \"retention-expiry\") {\n\t\tthrow new Error(\"forgetSuiteMemories requires user-immediate or profile-policy authorization\");\n\t}\n\tconst now = deps.now ?? (() => Date.now());\n\tconst { members, filter } = domainMembershipPage(deps.store, input.owner, { kind: \"suite\", suiteId: input.suiteId });\n\tconst memoryIds = members.map((record) => record.atom.memoryId);\n\tif (memoryIds.length === 0) {\n\t\treturn Object.freeze({ status: \"nothing_to_purge\", suiteId: input.suiteId, filter });\n\t}\n\tconst batch = deps.purgeGate.authorizeBatch({\n\t\tmemoryIds,\n\t\treason: `suite-forget:${input.suiteId}`,\n\t\tauthorizedBy: input.authorizedBy,\n\t\tauthorizationRef: input.authorizationRef,\n\t});\n\tdeps.purgeJournal.authorize({\n\t\tbatchId: batch.batchId,\n\t\tmemoryIds: batch.memoryIds,\n\t\treplicas: batch.replicas,\n\t\tauthorizedBy: batch.authorizedBy,\n\t\treason: batch.reason,\n\t\tat: now(),\n\t});\n\tconst outcome = executePurgeBatch(deps, batch.batchId, purgeMemories, now);\n\treturn Object.freeze({\n\t\tstatus: outcome.status,\n\t\tsuiteId: input.suiteId,\n\t\tbatchId: batch.batchId,\n\t\tmemoryIds: outcome.memoryIds,\n\t\tconfirmedReplicas: outcome.confirmedReplicas,\n\t\t...(outcome.failedReplicas === undefined ? {} : { failedReplicas: outcome.failedReplicas }),\n\t}) as SuiteForgetResultV1;\n}\n\n/**\n * Idempotent retry of a `purge_eligible` suite-forget batch. Unknown batch ids\n * throw; already-completed batches are a no-op that reports `completed`\n * (gate semantics).\n */\nexport function retrySuiteForget(\n\tdeps: Pick<SuiteMemoryFacadeDepsV1, \"purgeGate\" | \"purgeJournal\" | \"purgeIndexReplica\" | \"now\">,\n\tinput: { readonly batchId: string; readonly suiteId: string },\n\tpurgeMemories: (memoryIds: readonly string[]) => void,\n): SuiteForgetResultV1 {\n\tassertNonEmptyString(input.batchId, \"retrySuiteForget batchId\");\n\tassertNonEmptyString(input.suiteId, \"retrySuiteForget suiteId\");\n\tconst now = deps.now ?? (() => Date.now());\n\tconst batch = deps.purgeGate.batchState(input.batchId);\n\tif (!batch) throw new Error(`Unknown purge batch: ${input.batchId}`);\n\tconst outcome = executePurgeBatch(deps, input.batchId, purgeMemories, now);\n\treturn Object.freeze({\n\t\tstatus: outcome.status,\n\t\tsuiteId: input.suiteId,\n\t\tbatchId: input.batchId,\n\t\tmemoryIds: outcome.memoryIds,\n\t\tconfirmedReplicas: outcome.confirmedReplicas,\n\t\t...(outcome.failedReplicas === undefined ? {} : { failedReplicas: outcome.failedReplicas }),\n\t}) as SuiteForgetResultV1;\n}\n\ninterface PurgeBatchOutcome {\n\treadonly status: \"completed\" | \"purge_eligible\";\n\treadonly memoryIds: readonly string[];\n\treadonly confirmedReplicas: readonly string[];\n\treadonly failedReplicas?: readonly { readonly replicaId: string; readonly error: string }[];\n}\n\n/**\n * Shared gate+journal batch execution behind both forget facades: completed\n * batches are idempotent no-ops, replica confirmations are replay-safe, and\n * the journal completes only after the barrier fully confirmed.\n */\nfunction executePurgeBatch(\n\tdeps: Pick<SuiteMemoryFacadeDepsV1, \"purgeGate\" | \"purgeJournal\" | \"purgeIndexReplica\">,\n\tbatchId: string,\n\tpurgeMemories: (memoryIds: readonly string[]) => void,\n\tnow: () => number,\n): PurgeBatchOutcome {\n\t// Idempotent no-op: the journal already holds the completion record, so a\n\t// re-run must not re-execute the replica purge nor replay journal entries.\n\tif (deps.purgeJournal.batchState(batchId)?.state === \"completed\") {\n\t\tconst batch = deps.purgeGate.batchState(batchId);\n\t\tif (!batch) throw new Error(`Unknown purge batch: ${batchId}`);\n\t\treturn {\n\t\t\tstatus: \"completed\",\n\t\t\tmemoryIds: batch.memoryIds,\n\t\t\tconfirmedReplicas: Object.freeze([...(batch.confirmedReplicas ?? [])]),\n\t\t};\n\t}\n\t// 按副本分派 (D-035, 混合检索工程化): index 副本走向量投影清除回调; 其余\n\t// 副本维持原 purgeMemories 路径 (canonical 行为零变化)。index 回调缺省时\n\t// no-op 确认——副本契约要求 purge 幂等可重试, no-op 满足。\n\tconst outcome = deps.purgeGate.executeBatch(batchId, (replica: MemoryReplicaV1, memoryIds) => {\n\t\tif (replica.kind === \"index\") {\n\t\t\tif (deps.purgeIndexReplica === undefined) return;\n\t\t\tdeps.purgeIndexReplica(memoryIds);\n\t\t\treturn;\n\t\t}\n\t\tpurgeMemories(memoryIds);\n\t});\n\t// Journal confirmations are idempotent per (batchId, replicaId); replays\n\t// from a retry never double-count.\n\tfor (const replica of outcome.replicas) {\n\t\tif (outcome.confirmedReplicas?.includes(replica.replicaId)) {\n\t\t\tdeps.purgeJournal.confirmReplica({ batchId, replicaId: replica.replicaId, at: now() });\n\t\t}\n\t}\n\tif (outcome.state === \"completed\") {\n\t\tdeps.purgeJournal.complete({ batchId, at: now() });\n\t}\n\treturn {\n\t\tstatus: outcome.state as \"completed\" | \"purge_eligible\",\n\t\tmemoryIds: outcome.memoryIds,\n\t\tconfirmedReplicas: Object.freeze([...(outcome.confirmedReplicas ?? [])]),\n\t\t...(outcome.failedReplicas === undefined ? {} : { failedReplicas: outcome.failedReplicas }),\n\t};\n}\n\nexport interface ForgetDomainMemoryInputV1 {\n\treadonly owner: string;\n\t/** The read-boundary domain the CALLER is operating in (tool-side isolation). */\n\treadonly domain: SuiteMemoryDomainV1;\n\t/** The single memory to forget; it must be a member of the caller's domain. */\n\treadonly memoryId: string;\n\treadonly authorizedBy: string;\n\treadonly authorizationRef: SuiteForgetAuthorizationRefV1;\n}\n\nexport type DomainMemoryForgetResultV1 =\n\t/** No memory with this id under the owner. */\n\t| { readonly status: \"not_found\"; readonly memoryId: string }\n\t/** The memory exists but belongs to another domain — refused, no gate run. */\n\t| { readonly status: \"foreign_domain\"; readonly memoryId: string }\n\t| {\n\t\t\treadonly status: \"completed\" | \"purge_eligible\";\n\t\t\treadonly batchId: string;\n\t\t\treadonly memoryIds: readonly string[];\n\t\t\treadonly confirmedReplicas: readonly string[];\n\t\t\treadonly failedReplicas?: readonly { readonly replicaId: string; readonly error: string }[];\n\t };\n\n/**\n * Forgets ONE memory on behalf of a caller scoped to a read-boundary domain\n * (assistant 场景可控性硬需求, 方案系统设计 §6.1): the atom must be a member of\n * the caller's domain (its `suiteId` matches, or it is suite-less for the\n * legacy domain) — a caller can never forget another suite's atoms by id.\n * Deletion goes through the canonical purge gate (user-immediate or\n * profile-policy authorization) plus the purge journal, exactly like\n * {@link forgetSuiteMemories}; the injected `purgeMemories` callback performs\n * the physical replica purge (e.g. the durable ledger rewrite plus store\n * eviction) and must be idempotent for `purge_eligible` retries.\n */\nexport function forgetDomainMemory(\n\tdeps: SuiteMemoryFacadeDepsV1,\n\tinput: ForgetDomainMemoryInputV1,\n\tpurgeMemories: (memoryIds: readonly string[]) => void,\n): DomainMemoryForgetResultV1 {\n\tassertNonEmptyString(input.owner, \"forgetDomainMemory owner\");\n\tassertNonEmptyString(input.memoryId, \"forgetDomainMemory memoryId\");\n\tassertNonEmptyString(input.authorizedBy, \"forgetDomainMemory authorizedBy\");\n\tif (input.domain === null || typeof input.domain !== \"object\" || ![\"suite\", \"legacy\"].includes(input.domain.kind)) {\n\t\tthrow new Error(\"forgetDomainMemory requires a suite or legacy domain\");\n\t}\n\tif (input.domain.kind === \"suite\") assertNonEmptyString(input.domain.suiteId, \"forgetDomainMemory suiteId\");\n\t// Runtime guard for non-TS callers: the static type already excludes\n\t// retention-expiry, but the gate alone would accept it.\n\tif ((input.authorizationRef as MemoryPurgeAuthorizationRefV1).mode === \"retention-expiry\") {\n\t\tthrow new Error(\"forgetDomainMemory requires user-immediate or profile-policy authorization\");\n\t}\n\tconst now = deps.now ?? (() => Date.now());\n\tconst record = deps.store.get(input.memoryId, { owner: input.owner });\n\tif (!record) return Object.freeze({ status: \"not_found\", memoryId: input.memoryId });\n\tif (!isDomainMember(record.atom, input.domain)) {\n\t\treturn Object.freeze({ status: \"foreign_domain\", memoryId: input.memoryId });\n\t}\n\tconst domainName = suiteMemoryDomainName(input.domain);\n\tconst batch = deps.purgeGate.authorizeBatch({\n\t\tmemoryIds: [input.memoryId],\n\t\treason: `domain-forget:${domainName}`,\n\t\tauthorizedBy: input.authorizedBy,\n\t\tauthorizationRef: input.authorizationRef,\n\t});\n\tdeps.purgeJournal.authorize({\n\t\tbatchId: batch.batchId,\n\t\tmemoryIds: batch.memoryIds,\n\t\treplicas: batch.replicas,\n\t\tauthorizedBy: batch.authorizedBy,\n\t\treason: batch.reason,\n\t\tat: now(),\n\t});\n\tconst outcome = executePurgeBatch(deps, batch.batchId, purgeMemories, now);\n\treturn Object.freeze({\n\t\tstatus: outcome.status,\n\t\tbatchId: batch.batchId,\n\t\tmemoryIds: outcome.memoryIds,\n\t\tconfirmedReplicas: outcome.confirmedReplicas,\n\t\t...(outcome.failedReplicas === undefined ? {} : { failedReplicas: outcome.failedReplicas }),\n\t}) as DomainMemoryForgetResultV1;\n}\n"]}
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Memory Foundation — contract compatibility, upcaster chain, and import/export
3
+ * boundaries (1C.1c).
4
+ *
5
+ * Semantics frozen here (记忆系统设计.md §4 + D-035):
6
+ * - Canonical atoms are never rewritten by an upgrade; old atoms are converted
7
+ * on read by versioned upcasters. A failed upcaster preserves the original
8
+ * data and reports the adapter as unavailable — never a silent reinterpretation.
9
+ * - A reader must declare which contract versions it supports; mixed-version
10
+ * input is only processed when every version is supported, otherwise the
11
+ * result is an explicit `unsupported`.
12
+ * - Export/import bundles carry atoms, lifecycle events, source revisions,
13
+ * owner, contractVersion, and policy version. Purged memories export as
14
+ * audit stubs without payload and can never be re-imported as recallable
15
+ * atoms.
16
+ * - D-035: the first-party V1 ships a single replica and a single contract
17
+ * version. Until the D-035 trigger fires (a second managed replica or a
18
+ * second contract version), migration/export capabilities return explicit
19
+ * `unsupported` instead of pretending the distributed protocol exists.
20
+ */
21
+ import type { MemoryAtomV1 } from "./foundation.ts";
22
+ /** The single contract version the first-party V1 Foundation reads natively. */
23
+ export declare const MEMORY_CONTRACT_VERSION_V1 = "agent-forge/memory@1";
24
+ /**
25
+ * The first-party write contract since M5 (方案系统设计 §11 契约版本随 M5
26
+ * 升级): introduces the optional atom `suiteId` dimension. Readers accept both
27
+ * @1 and @2; @1 atoms (no suiteId) read as legacy — invisible to every
28
+ * suite-scoped query and counted in the suite filter diagnostics.
29
+ */
30
+ export declare const MEMORY_CONTRACT_VERSION_V2 = "agent-forge/memory@2";
31
+ /** Every contract version the first-party Foundation reads. */
32
+ export declare const MEMORY_CONTRACT_VERSIONS: readonly string[];
33
+ export type MemoryContractCompatibilityStatus = "compatible" | "unsupported";
34
+ export interface MemoryContractCompatibilityResult {
35
+ readonly status: MemoryContractCompatibilityStatus;
36
+ readonly supportedVersions: readonly string[];
37
+ readonly unsupportedVersions: readonly string[];
38
+ readonly reason?: string;
39
+ }
40
+ /**
41
+ * Checks whether every observed contract version is covered by the reader's
42
+ * supported set. A single foreign version makes the whole input explicitly
43
+ * unsupported — mixed-version data is never silently interpreted with the
44
+ * default schema (D-035).
45
+ */
46
+ export declare function checkMemoryContractCompatibility(input: {
47
+ readonly supportedContractVersions: readonly string[];
48
+ readonly contractVersions: readonly string[];
49
+ }): MemoryContractCompatibilityResult;
50
+ /** One versioned upcaster: converts an atom from `fromContractVersion` to `toContractVersion`. */
51
+ export interface MemoryUpcaster {
52
+ readonly fromContractVersion: string;
53
+ readonly toContractVersion: string;
54
+ upcast(atom: MemoryAtomV1): MemoryAtomV1;
55
+ }
56
+ export type MemoryUpcastResult = {
57
+ readonly status: "upcast";
58
+ readonly atom: MemoryAtomV1;
59
+ readonly viaVersions: readonly string[];
60
+ } | {
61
+ readonly status: "already-current";
62
+ readonly atom: MemoryAtomV1;
63
+ } | {
64
+ readonly status: "unsupported";
65
+ readonly reason: string;
66
+ } | {
67
+ readonly status: "failed";
68
+ readonly error: Error;
69
+ readonly originalAtom: MemoryAtomV1;
70
+ };
71
+ /**
72
+ * A chain of registered upcasters. A missing path reports `unsupported`; a
73
+ * throwing upcaster reports `failed` and preserves the original atom — the
74
+ * canonical data is never partially rewritten.
75
+ */
76
+ export interface MemoryUpcasterChain {
77
+ register(upcaster: MemoryUpcaster): void;
78
+ upcast(atom: MemoryAtomV1, targetContractVersion: string): MemoryUpcastResult;
79
+ readonly registeredPaths: readonly string[];
80
+ }
81
+ export declare function createMemoryUpcasterChain(initialUpcasters?: readonly MemoryUpcaster[]): MemoryUpcasterChain;
82
+ /** Audit stub for a purged memory: identity only, never payload. */
83
+ export interface PurgedMemoryAuditStubV1 {
84
+ readonly memoryId: string;
85
+ readonly purgeGroupId: string;
86
+ readonly contractVersion: string;
87
+ }
88
+ export interface MemoryExportBundleV1 {
89
+ readonly bundleVersion: 1;
90
+ readonly owner: string;
91
+ readonly contractVersion: string;
92
+ readonly retentionPolicyVersions: readonly string[];
93
+ readonly atoms: readonly MemoryAtomV1[];
94
+ /** Versioned lifecycle event payloads; typed and populated by 1C.2. */
95
+ readonly lifecycleEvents: readonly unknown[];
96
+ /** Audit identities of purged memories — payload is intentionally absent. */
97
+ readonly purgedAudit: readonly PurgedMemoryAuditStubV1[];
98
+ readonly exportedAt: string;
99
+ }
100
+ export type MemoryExportResult = {
101
+ readonly status: "exported";
102
+ readonly bundle: MemoryExportBundleV1;
103
+ } | {
104
+ readonly status: "unsupported";
105
+ readonly reason: string;
106
+ } | {
107
+ readonly status: "rejected";
108
+ readonly reason: string;
109
+ };
110
+ export interface MemoryExportInput {
111
+ readonly owner: string;
112
+ readonly atoms: readonly MemoryAtomV1[];
113
+ readonly lifecycleEvents?: readonly unknown[];
114
+ readonly purgedAudit?: readonly PurgedMemoryAuditStubV1[];
115
+ readonly exportedAt?: string;
116
+ }
117
+ /**
118
+ * Exports one owner's memories as a self-describing bundle. Mixed contract
119
+ * versions across the input are an explicit `unsupported` (D-035); a bundle
120
+ * never contains a payload summary in place of the atom.
121
+ */
122
+ export declare function exportMemoryBundle(input: MemoryExportInput): MemoryExportResult;
123
+ export type MemoryImportResult = {
124
+ readonly status: "imported";
125
+ readonly atoms: readonly MemoryAtomV1[];
126
+ readonly skippedPurgedAudit: number;
127
+ } | {
128
+ readonly status: "unsupported";
129
+ readonly reason: string;
130
+ } | {
131
+ readonly status: "rejected";
132
+ readonly reason: string;
133
+ };
134
+ /**
135
+ * Imports a bundle after full re-validation. Purged audit identities can never
136
+ * come back as atoms: a bundle whose atoms overlap its purged audit list (or
137
+ * that carries a purged stub with payload) is rejected as tampered.
138
+ */
139
+ export declare function importMemoryBundle(bundle: MemoryExportBundleV1, options: {
140
+ readonly owner: string;
141
+ }): MemoryImportResult;
142
+ //# sourceMappingURL=transfer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transfer.d.ts","sourceRoot":"","sources":["../../src/memory/transfer.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Memory Foundation — contract compatibility, upcaster chain, and import/export\n * boundaries (1C.1c).\n *\n * Semantics frozen here (记忆系统设计.md §4 + D-035):\n * - Canonical atoms are never rewritten by an upgrade; old atoms are converted\n * on read by versioned upcasters. A failed upcaster preserves the original\n * data and reports the adapter as unavailable — never a silent reinterpretation.\n * - A reader must declare which contract versions it supports; mixed-version\n * input is only processed when every version is supported, otherwise the\n * result is an explicit `unsupported`.\n * - Export/import bundles carry atoms, lifecycle events, source revisions,\n * owner, contractVersion, and policy version. Purged memories export as\n * audit stubs without payload and can never be re-imported as recallable\n * atoms.\n * - D-035: the first-party V1 ships a single replica and a single contract\n * version. Until the D-035 trigger fires (a second managed replica or a\n * second contract version), migration/export capabilities return explicit\n * `unsupported` instead of pretending the distributed protocol exists.\n */\nimport type { MemoryAtomV1 } from \"./foundation.ts\";\nimport { validateMemoryAtomV1 } from \"./foundation.ts\";\n\n/** The single contract version the first-party V1 Foundation reads natively. */\nexport const MEMORY_CONTRACT_VERSION_V1 = \"agent-forge/memory@1\";\n\n/**\n * The first-party write contract since M5 (方案系统设计 §11 契约版本随 M5\n * 升级): introduces the optional atom `suiteId` dimension. Readers accept both\n * @1 and @2; @1 atoms (no suiteId) read as legacy — invisible to every\n * suite-scoped query and counted in the suite filter diagnostics.\n */\nexport const MEMORY_CONTRACT_VERSION_V2 = \"agent-forge/memory@2\";\n\n/** Every contract version the first-party Foundation reads. */\nexport const MEMORY_CONTRACT_VERSIONS: readonly string[] = Object.freeze([\n\tMEMORY_CONTRACT_VERSION_V1,\n\tMEMORY_CONTRACT_VERSION_V2,\n]);\n\nexport type MemoryContractCompatibilityStatus = \"compatible\" | \"unsupported\";\n\nexport interface MemoryContractCompatibilityResult {\n\treadonly status: MemoryContractCompatibilityStatus;\n\treadonly supportedVersions: readonly string[];\n\treadonly unsupportedVersions: readonly string[];\n\treadonly reason?: string;\n}\n\n/**\n * Checks whether every observed contract version is covered by the reader's\n * supported set. A single foreign version makes the whole input explicitly\n * unsupported — mixed-version data is never silently interpreted with the\n * default schema (D-035).\n */\nexport function checkMemoryContractCompatibility(input: {\n\treadonly supportedContractVersions: readonly string[];\n\treadonly contractVersions: readonly string[];\n}): MemoryContractCompatibilityResult {\n\tconst supported = Object.freeze([...input.supportedContractVersions]);\n\tconst unsupportedVersions = Object.freeze(\n\t\t[...new Set(input.contractVersions)].filter((version) => !supported.includes(version)),\n\t);\n\tif (unsupportedVersions.length > 0) {\n\t\treturn Object.freeze({\n\t\t\tstatus: \"unsupported\",\n\t\t\tsupportedVersions: supported,\n\t\t\tunsupportedVersions,\n\t\t\treason:\n\t\t\t\t\"D-035: mixed or foreign memory contract versions require the versioned migration boundary, \" +\n\t\t\t\t`which the single-replica V1 does not enable. Unsupported: ${unsupportedVersions.join(\", \")}`,\n\t\t});\n\t}\n\treturn Object.freeze({ status: \"compatible\", supportedVersions: supported, unsupportedVersions: [] });\n}\n\n/** One versioned upcaster: converts an atom from `fromContractVersion` to `toContractVersion`. */\nexport interface MemoryUpcaster {\n\treadonly fromContractVersion: string;\n\treadonly toContractVersion: string;\n\tupcast(atom: MemoryAtomV1): MemoryAtomV1;\n}\n\nexport type MemoryUpcastResult =\n\t| { readonly status: \"upcast\"; readonly atom: MemoryAtomV1; readonly viaVersions: readonly string[] }\n\t| { readonly status: \"already-current\"; readonly atom: MemoryAtomV1 }\n\t| { readonly status: \"unsupported\"; readonly reason: string }\n\t| { readonly status: \"failed\"; readonly error: Error; readonly originalAtom: MemoryAtomV1 };\n\n/**\n * A chain of registered upcasters. A missing path reports `unsupported`; a\n * throwing upcaster reports `failed` and preserves the original atom — the\n * canonical data is never partially rewritten.\n */\nexport interface MemoryUpcasterChain {\n\tregister(upcaster: MemoryUpcaster): void;\n\tupcast(atom: MemoryAtomV1, targetContractVersion: string): MemoryUpcastResult;\n\treadonly registeredPaths: readonly string[];\n}\n\nexport function createMemoryUpcasterChain(initialUpcasters: readonly MemoryUpcaster[] = []): MemoryUpcasterChain {\n\tconst edges = new Map<string, Map<string, MemoryUpcaster>>();\n\tconst register = (upcaster: MemoryUpcaster): void => {\n\t\tif (upcaster.fromContractVersion === upcaster.toContractVersion) {\n\t\t\tthrow new Error(\"Upcaster must change the contract version\");\n\t\t}\n\t\tlet targets = edges.get(upcaster.fromContractVersion);\n\t\tif (!targets) {\n\t\t\ttargets = new Map();\n\t\t\tedges.set(upcaster.fromContractVersion, targets);\n\t\t}\n\t\tif (targets.has(upcaster.toContractVersion)) {\n\t\t\tthrow new Error(\n\t\t\t\t`Upcaster already registered: ${upcaster.fromContractVersion} -> ${upcaster.toContractVersion}`,\n\t\t\t);\n\t\t}\n\t\ttargets.set(upcaster.toContractVersion, upcaster);\n\t};\n\tfor (const upcaster of initialUpcasters) register(upcaster);\n\n\treturn {\n\t\tregister,\n\t\tget registeredPaths() {\n\t\t\treturn Object.freeze(\n\t\t\t\t[...edges].flatMap(([from, targets]) => [...targets.keys()].map((to) => `${from}->${to}`)),\n\t\t\t);\n\t\t},\n\t\tupcast(atom, targetContractVersion) {\n\t\t\tif (atom.contractVersion === targetContractVersion) {\n\t\t\t\treturn { status: \"already-current\", atom };\n\t\t\t}\n\t\t\t// Walk the chain one hop at a time; every hop produces a validated,\n\t\t\t// frozen atom while the original stays untouched.\n\t\t\tlet current = atom;\n\t\t\tconst viaVersions: string[] = [atom.contractVersion];\n\t\t\tlet version = atom.contractVersion;\n\t\t\tconst visited = new Set<string>([version]);\n\t\t\twhile (version !== targetContractVersion) {\n\t\t\t\tconst targets = edges.get(version);\n\t\t\t\tconst nextEdge = targets ? [...targets.keys()][0] : undefined;\n\t\t\t\tif (!targets || nextEdge === undefined) {\n\t\t\t\t\treturn {\n\t\t\t\t\t\tstatus: \"unsupported\",\n\t\t\t\t\t\treason: `No upcaster path from ${version} to ${targetContractVersion} (D-035: V1 ships without migration adapters)`,\n\t\t\t\t\t};\n\t\t\t\t}\n\t\t\t\tif (visited.has(nextEdge)) {\n\t\t\t\t\treturn { status: \"unsupported\", reason: `Upcaster path cycle at ${nextEdge}` };\n\t\t\t\t}\n\t\t\t\tvisited.add(nextEdge);\n\t\t\t\tconst upcaster = targets.get(nextEdge)!;\n\t\t\t\tlet converted: MemoryAtomV1;\n\t\t\t\ttry {\n\t\t\t\t\tconverted = upcaster.upcast(current);\n\t\t\t\t\tconverted = validateMemoryAtomV1({\n\t\t\t\t\t\t...converted,\n\t\t\t\t\t\tcontractVersion: nextEdge,\n\t\t\t\t\t});\n\t\t\t\t} catch (error) {\n\t\t\t\t\treturn {\n\t\t\t\t\t\tstatus: \"failed\",\n\t\t\t\t\t\terror: error instanceof Error ? error : new Error(String(error)),\n\t\t\t\t\t\toriginalAtom: atom,\n\t\t\t\t\t};\n\t\t\t\t}\n\t\t\t\tcurrent = converted;\n\t\t\t\tversion = nextEdge;\n\t\t\t\tviaVersions.push(nextEdge);\n\t\t\t}\n\t\t\treturn { status: \"upcast\", atom: current, viaVersions };\n\t\t},\n\t};\n}\n\n// ---------------------------------------------------------------------------\n// Export / import boundary\n// ---------------------------------------------------------------------------\n\nconst MEMORY_BUNDLE_VERSION = 1;\n\n/** Audit stub for a purged memory: identity only, never payload. */\nexport interface PurgedMemoryAuditStubV1 {\n\treadonly memoryId: string;\n\treadonly purgeGroupId: string;\n\treadonly contractVersion: string;\n}\n\nexport interface MemoryExportBundleV1 {\n\treadonly bundleVersion: 1;\n\treadonly owner: string;\n\treadonly contractVersion: string;\n\treadonly retentionPolicyVersions: readonly string[];\n\treadonly atoms: readonly MemoryAtomV1[];\n\t/** Versioned lifecycle event payloads; typed and populated by 1C.2. */\n\treadonly lifecycleEvents: readonly unknown[];\n\t/** Audit identities of purged memories — payload is intentionally absent. */\n\treadonly purgedAudit: readonly PurgedMemoryAuditStubV1[];\n\treadonly exportedAt: string;\n}\n\nexport type MemoryExportResult =\n\t| { readonly status: \"exported\"; readonly bundle: MemoryExportBundleV1 }\n\t| { readonly status: \"unsupported\"; readonly reason: string }\n\t| { readonly status: \"rejected\"; readonly reason: string };\n\nexport interface MemoryExportInput {\n\treadonly owner: string;\n\treadonly atoms: readonly MemoryAtomV1[];\n\treadonly lifecycleEvents?: readonly unknown[];\n\treadonly purgedAudit?: readonly PurgedMemoryAuditStubV1[];\n\treadonly exportedAt?: string;\n}\n\n/**\n * Exports one owner's memories as a self-describing bundle. Mixed contract\n * versions across the input are an explicit `unsupported` (D-035); a bundle\n * never contains a payload summary in place of the atom.\n */\nexport function exportMemoryBundle(input: MemoryExportInput): MemoryExportResult {\n\tif (input.owner.trim() === \"\") throw new Error(\"Export requires a non-empty owner\");\n\tconst versions = [...new Set(input.atoms.map((atom) => atom.contractVersion))];\n\tconst compatibility = checkMemoryContractCompatibility({\n\t\tsupportedContractVersions: MEMORY_CONTRACT_VERSIONS,\n\t\tcontractVersions: versions,\n\t});\n\tif (compatibility.status === \"unsupported\") {\n\t\t// v8 ignore next -- 兼容性检查的 unsupported 结果恒带 reason\n\t\treturn { status: \"unsupported\", reason: compatibility.reason ?? \"unsupported contract versions\" };\n\t}\n\tfor (const atom of input.atoms) {\n\t\tif (atom.owner !== input.owner) {\n\t\t\treturn {\n\t\t\t\tstatus: \"rejected\",\n\t\t\t\treason: `Atom ${atom.memoryId} belongs to ${atom.owner}, not the export owner ${input.owner}`,\n\t\t\t};\n\t\t}\n\t}\n\tconst foreignPurged = (input.purgedAudit ?? []).filter(\n\t\t(stub) => !MEMORY_CONTRACT_VERSIONS.includes(stub.contractVersion),\n\t);\n\tif (foreignPurged.length > 0) {\n\t\treturn { status: \"unsupported\", reason: \"Purged audit stubs carry foreign contract versions\" };\n\t}\n\tconst retentionPolicyVersions = [...new Set(input.atoms.map((atom) => atom.retentionPolicyVersion))];\n\treturn {\n\t\tstatus: \"exported\",\n\t\tbundle: Object.freeze({\n\t\t\tbundleVersion: MEMORY_BUNDLE_VERSION,\n\t\t\towner: input.owner,\n\t\t\t// The envelope declares the exporter's current write contract; atoms\n\t\t\t// carry their own contractVersion (first-party @1 and @2 mix freely).\n\t\t\tcontractVersion: MEMORY_CONTRACT_VERSION_V2,\n\t\t\tretentionPolicyVersions: Object.freeze(retentionPolicyVersions),\n\t\t\tatoms: Object.freeze(input.atoms.map((atom) => validateMemoryAtomV1(atom))),\n\t\t\tlifecycleEvents: Object.freeze([...(input.lifecycleEvents ?? [])]),\n\t\t\tpurgedAudit: Object.freeze([...(input.purgedAudit ?? [])]),\n\t\t\texportedAt: input.exportedAt ?? new Date().toISOString(),\n\t\t}),\n\t};\n}\n\nexport type MemoryImportResult =\n\t| { readonly status: \"imported\"; readonly atoms: readonly MemoryAtomV1[]; readonly skippedPurgedAudit: number }\n\t| { readonly status: \"unsupported\"; readonly reason: string }\n\t| { readonly status: \"rejected\"; readonly reason: string };\n\n/**\n * Imports a bundle after full re-validation. Purged audit identities can never\n * come back as atoms: a bundle whose atoms overlap its purged audit list (or\n * that carries a purged stub with payload) is rejected as tampered.\n */\nexport function importMemoryBundle(\n\tbundle: MemoryExportBundleV1,\n\toptions: { readonly owner: string },\n): MemoryImportResult {\n\tif (bundle.bundleVersion !== 1) {\n\t\treturn { status: \"unsupported\", reason: `Unsupported memory bundle version: ${String(bundle.bundleVersion)}` };\n\t}\n\tif (bundle.owner !== options.owner) {\n\t\treturn {\n\t\t\tstatus: \"rejected\",\n\t\t\treason: `Bundle owner ${bundle.owner} does not match the importing owner ${options.owner}`,\n\t\t};\n\t}\n\tconst compatibility = checkMemoryContractCompatibility({\n\t\tsupportedContractVersions: MEMORY_CONTRACT_VERSIONS,\n\t\tcontractVersions: [bundle.contractVersion, ...bundle.atoms.map((atom) => atom.contractVersion)],\n\t});\n\tif (compatibility.status === \"unsupported\") {\n\t\treturn { status: \"unsupported\", reason: compatibility.reason ?? \"unsupported contract versions\" };\n\t}\n\tconst purgedIds = new Set(bundle.purgedAudit.map((stub) => stub.memoryId));\n\tconst overlapping = bundle.atoms.filter((atom) => purgedIds.has(atom.memoryId));\n\tif (overlapping.length > 0) {\n\t\treturn {\n\t\t\tstatus: \"rejected\",\n\t\t\treason: `Bundle attempts to resurrect purged memories: ${overlapping.map((atom) => atom.memoryId).join(\", \")}`,\n\t\t};\n\t}\n\tconst atoms: MemoryAtomV1[] = [];\n\tfor (const atom of bundle.atoms) {\n\t\ttry {\n\t\t\tatoms.push(validateMemoryAtomV1(atom));\n\t\t} catch (error) {\n\t\t\treturn {\n\t\t\t\tstatus: \"rejected\",\n\t\t\t\t// v8 ignore next -- 校验器只抛 Error 对象\n\t\t\t\treason: `Atom ${atom.memoryId} failed validation: ${error instanceof Error ? error.message : String(error)}`,\n\t\t\t};\n\t\t}\n\t}\n\tfor (const stub of bundle.purgedAudit) {\n\t\tif (\"payload\" in stub || \"atom\" in stub) {\n\t\t\treturn { status: \"rejected\", reason: `Purged audit entry ${stub.memoryId} carries forbidden payload` };\n\t\t}\n\t}\n\treturn {\n\t\tstatus: \"imported\",\n\t\tatoms: Object.freeze(atoms),\n\t\tskippedPurgedAudit: bundle.purgedAudit.length,\n\t};\n}\n"]}