@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.
- package/README.md +65 -0
- package/agent-forge.json +11 -0
- package/dist/capability.d.ts +182 -0
- package/dist/capability.d.ts.map +1 -0
- package/dist/capability.js +2565 -0
- package/dist/capability.js.map +1 -0
- package/dist/entry.d.ts +36 -0
- package/dist/entry.d.ts.map +1 -0
- package/dist/entry.js +154 -0
- package/dist/entry.js.map +1 -0
- package/dist/index.d.ts +49 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +49 -0
- package/dist/index.js.map +1 -0
- package/dist/memory/assistant-card.d.ts +31 -0
- package/dist/memory/assistant-card.d.ts.map +1 -0
- package/dist/memory/assistant-card.js +108 -0
- package/dist/memory/assistant-card.js.map +1 -0
- package/dist/memory/candidates.d.ts +65 -0
- package/dist/memory/candidates.d.ts.map +1 -0
- package/dist/memory/candidates.js +100 -0
- package/dist/memory/candidates.js.map +1 -0
- package/dist/memory/code-memory.d.ts +89 -0
- package/dist/memory/code-memory.d.ts.map +1 -0
- package/dist/memory/code-memory.js +104 -0
- package/dist/memory/code-memory.js.map +1 -0
- package/dist/memory/compaction-sequencer.d.ts +63 -0
- package/dist/memory/compaction-sequencer.d.ts.map +1 -0
- package/dist/memory/compaction-sequencer.js +129 -0
- package/dist/memory/compaction-sequencer.js.map +1 -0
- package/dist/memory/continuation.d.ts +44 -0
- package/dist/memory/continuation.d.ts.map +1 -0
- package/dist/memory/continuation.js +49 -0
- package/dist/memory/continuation.js.map +1 -0
- package/dist/memory/curation.d.ts +58 -0
- package/dist/memory/curation.d.ts.map +1 -0
- package/dist/memory/curation.js +68 -0
- package/dist/memory/curation.js.map +1 -0
- package/dist/memory/egress-policy.d.ts +50 -0
- package/dist/memory/egress-policy.d.ts.map +1 -0
- package/dist/memory/egress-policy.js +71 -0
- package/dist/memory/egress-policy.js.map +1 -0
- package/dist/memory/embedding-provider.d.ts +70 -0
- package/dist/memory/embedding-provider.d.ts.map +1 -0
- package/dist/memory/embedding-provider.js +164 -0
- package/dist/memory/embedding-provider.js.map +1 -0
- package/dist/memory/embedding-reranker.d.ts +56 -0
- package/dist/memory/embedding-reranker.d.ts.map +1 -0
- package/dist/memory/embedding-reranker.js +109 -0
- package/dist/memory/embedding-reranker.js.map +1 -0
- package/dist/memory/foundation.d.ts +168 -0
- package/dist/memory/foundation.d.ts.map +1 -0
- package/dist/memory/foundation.js +487 -0
- package/dist/memory/foundation.js.map +1 -0
- package/dist/memory/host-module-import.d.ts +25 -0
- package/dist/memory/host-module-import.d.ts.map +1 -0
- package/dist/memory/host-module-import.js +41 -0
- package/dist/memory/host-module-import.js.map +1 -0
- package/dist/memory/ledger.d.ts +58 -0
- package/dist/memory/ledger.d.ts.map +1 -0
- package/dist/memory/ledger.js +315 -0
- package/dist/memory/ledger.js.map +1 -0
- package/dist/memory/lifecycle.d.ts +124 -0
- package/dist/memory/lifecycle.d.ts.map +1 -0
- package/dist/memory/lifecycle.js +201 -0
- package/dist/memory/lifecycle.js.map +1 -0
- package/dist/memory/memory-network.d.ts +55 -0
- package/dist/memory/memory-network.d.ts.map +1 -0
- package/dist/memory/memory-network.js +70 -0
- package/dist/memory/memory-network.js.map +1 -0
- package/dist/memory/model-cache-hygiene.d.ts +18 -0
- package/dist/memory/model-cache-hygiene.d.ts.map +1 -0
- package/dist/memory/model-cache-hygiene.js +38 -0
- package/dist/memory/model-cache-hygiene.js.map +1 -0
- package/dist/memory/preference-disambiguator.d.ts +43 -0
- package/dist/memory/preference-disambiguator.d.ts.map +1 -0
- package/dist/memory/preference-disambiguator.js +81 -0
- package/dist/memory/preference-disambiguator.js.map +1 -0
- package/dist/memory/preference-lifecycle.d.ts +66 -0
- package/dist/memory/preference-lifecycle.d.ts.map +1 -0
- package/dist/memory/preference-lifecycle.js +129 -0
- package/dist/memory/preference-lifecycle.js.map +1 -0
- package/dist/memory/preference-promotion.d.ts +87 -0
- package/dist/memory/preference-promotion.d.ts.map +1 -0
- package/dist/memory/preference-promotion.js +102 -0
- package/dist/memory/preference-promotion.js.map +1 -0
- package/dist/memory/preference-resolver.d.ts +44 -0
- package/dist/memory/preference-resolver.d.ts.map +1 -0
- package/dist/memory/preference-resolver.js +107 -0
- package/dist/memory/preference-resolver.js.map +1 -0
- package/dist/memory/purge-journal.d.ts +76 -0
- package/dist/memory/purge-journal.d.ts.map +1 -0
- package/dist/memory/purge-journal.js +130 -0
- package/dist/memory/purge-journal.js.map +1 -0
- package/dist/memory/purge.d.ts +90 -0
- package/dist/memory/purge.d.ts.map +1 -0
- package/dist/memory/purge.js +138 -0
- package/dist/memory/purge.js.map +1 -0
- package/dist/memory/recall-agent.d.ts +84 -0
- package/dist/memory/recall-agent.d.ts.map +1 -0
- package/dist/memory/recall-agent.js +199 -0
- package/dist/memory/recall-agent.js.map +1 -0
- package/dist/memory/recall-index.d.ts +87 -0
- package/dist/memory/recall-index.d.ts.map +1 -0
- package/dist/memory/recall-index.js +222 -0
- package/dist/memory/recall-index.js.map +1 -0
- package/dist/memory/recall-packet.d.ts +121 -0
- package/dist/memory/recall-packet.d.ts.map +1 -0
- package/dist/memory/recall-packet.js +156 -0
- package/dist/memory/recall-packet.js.map +1 -0
- package/dist/memory/scheduler-api.d.ts +99 -0
- package/dist/memory/scheduler-api.d.ts.map +1 -0
- package/dist/memory/scheduler-api.js +93 -0
- package/dist/memory/scheduler-api.js.map +1 -0
- package/dist/memory/scheduler.d.ts +55 -0
- package/dist/memory/scheduler.d.ts.map +1 -0
- package/dist/memory/scheduler.js +91 -0
- package/dist/memory/scheduler.js.map +1 -0
- package/dist/memory/store.d.ts +107 -0
- package/dist/memory/store.d.ts.map +1 -0
- package/dist/memory/store.js +208 -0
- package/dist/memory/store.js.map +1 -0
- package/dist/memory/suite-memory.d.ts +208 -0
- package/dist/memory/suite-memory.d.ts.map +1 -0
- package/dist/memory/suite-memory.js +288 -0
- package/dist/memory/suite-memory.js.map +1 -0
- package/dist/memory/transfer.d.ts +142 -0
- package/dist/memory/transfer.d.ts.map +1 -0
- package/dist/memory/transfer.js +210 -0
- package/dist/memory/transfer.js.map +1 -0
- package/dist/memory/vector-index.d.ts +39 -0
- package/dist/memory/vector-index.d.ts.map +1 -0
- package/dist/memory/vector-index.js +136 -0
- package/dist/memory/vector-index.js.map +1 -0
- package/dist/memory/write-budget.d.ts +33 -0
- package/dist/memory/write-budget.d.ts.map +1 -0
- package/dist/memory/write-budget.js +45 -0
- package/dist/memory/write-budget.js.map +1 -0
- package/dist/testing/memory-testkit.d.ts +149 -0
- package/dist/testing/memory-testkit.d.ts.map +1 -0
- package/dist/testing/memory-testkit.js +438 -0
- package/dist/testing/memory-testkit.js.map +1 -0
- package/dist/utils/sync-sleep.d.ts +2 -0
- package/dist/utils/sync-sleep.d.ts.map +1 -0
- package/dist/utils/sync-sleep.js +11 -0
- package/dist/utils/sync-sleep.js.map +1 -0
- package/package.json +56 -0
- package/plugin.json +10 -0
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { validateMemoryAtomV1 } from "./foundation.js";
|
|
2
|
+
export function createPreferencePromotionEngine(options) {
|
|
3
|
+
const policy = options.policy;
|
|
4
|
+
if (policy.promotedConfidence <= 0 || policy.promotedConfidence > 1) {
|
|
5
|
+
throw new Error("Promotion promotedConfidence must be within (0..1]");
|
|
6
|
+
}
|
|
7
|
+
if (policy.repeatedBehaviorThreshold < 1 || policy.inferredThreshold < 1) {
|
|
8
|
+
throw new Error("Promotion thresholds must be at least 1 observation");
|
|
9
|
+
}
|
|
10
|
+
return {
|
|
11
|
+
policyVersion: policy.policyVersion,
|
|
12
|
+
evaluate(input) {
|
|
13
|
+
if (!policy.enabled) {
|
|
14
|
+
return { decision: "disabled", policyVersion: policy.policyVersion };
|
|
15
|
+
}
|
|
16
|
+
// Independent observations only: dedupe by observationId deterministically.
|
|
17
|
+
const seen = new Map();
|
|
18
|
+
for (const observation of input.observations) {
|
|
19
|
+
if (observation.evidenceClass !== "repeated_behavior" && observation.evidenceClass !== "inferred") {
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
if (!seen.has(observation.observationId))
|
|
23
|
+
seen.set(observation.observationId, observation);
|
|
24
|
+
}
|
|
25
|
+
const supporting = [...seen.values()].filter((observation) => (observation.confidence ?? 1) >= 0.5);
|
|
26
|
+
const repeatedCount = supporting.filter((observation) => observation.evidenceClass === "repeated_behavior").length;
|
|
27
|
+
const inferredCount = supporting.filter((observation) => observation.evidenceClass === "inferred").length;
|
|
28
|
+
const required = repeatedCount >= inferredCount ? policy.repeatedBehaviorThreshold : policy.inferredThreshold;
|
|
29
|
+
const eligibleCount = Math.max(repeatedCount, inferredCount);
|
|
30
|
+
if (repeatedCount >= policy.repeatedBehaviorThreshold || inferredCount >= policy.inferredThreshold) {
|
|
31
|
+
return {
|
|
32
|
+
decision: "promoted",
|
|
33
|
+
policyVersion: policy.policyVersion,
|
|
34
|
+
fromConfidence: input.currentConfidence,
|
|
35
|
+
toConfidence: Math.max(policy.promotedConfidence, input.currentConfidence),
|
|
36
|
+
supportingObservations: eligibleCount,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
decision: "not_promoted",
|
|
41
|
+
policyVersion: policy.policyVersion,
|
|
42
|
+
supportingObservations: eligibleCount,
|
|
43
|
+
requiredObservations: required,
|
|
44
|
+
};
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/** Serializes a policy for migration/replay ledgers. */
|
|
49
|
+
export function serializePromotionPolicy(policy) {
|
|
50
|
+
return {
|
|
51
|
+
enabled: policy.enabled,
|
|
52
|
+
policyVersion: policy.policyVersion,
|
|
53
|
+
repeatedBehaviorThreshold: policy.repeatedBehaviorThreshold,
|
|
54
|
+
inferredThreshold: policy.inferredThreshold,
|
|
55
|
+
promotedConfidence: policy.promotedConfidence,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Explicitly promotes a preference atom to the user-default scope (方案系统
|
|
60
|
+
* 设计 §6.1 跨方案规则). Produces a NEW canonical atom — the original is never
|
|
61
|
+
* rewritten — whose envelope carries `scope.level "user-default"` and
|
|
62
|
+
* `confirmedAt`, the canonical explicit-confirmation marker the suite read
|
|
63
|
+
* boundary ({@link memorySuiteVisibility} in foundation.ts) requires for
|
|
64
|
+
* cross-suite visibility. Scope anchors are intentionally dropped: a
|
|
65
|
+
* user-default preference applies without narrowing. The automatic
|
|
66
|
+
* applicabilityConfidence policy above stays orthogonal — it never touches
|
|
67
|
+
* user-default and never sets `confirmedAt`.
|
|
68
|
+
*
|
|
69
|
+
* Rejects non-preference atoms, atoms already at user-default, and missing
|
|
70
|
+
* `authorizedBy`/`confirmedAt`.
|
|
71
|
+
*/
|
|
72
|
+
export function promotePreferenceToUserDefault(input) {
|
|
73
|
+
if (typeof input.authorizedBy !== "string" || input.authorizedBy.trim().length === 0) {
|
|
74
|
+
throw new Error("User-default preference promotion requires a non-empty authorizedBy");
|
|
75
|
+
}
|
|
76
|
+
if (typeof input.confirmedAt !== "string" || input.confirmedAt.trim().length === 0) {
|
|
77
|
+
throw new Error("User-default preference promotion requires a non-empty confirmedAt");
|
|
78
|
+
}
|
|
79
|
+
if (Number.isNaN(Date.parse(input.confirmedAt))) {
|
|
80
|
+
throw new Error("User-default preference promotion confirmedAt must be an ISO-8601 timestamp");
|
|
81
|
+
}
|
|
82
|
+
const atom = input.atom;
|
|
83
|
+
if (atom.memoryKind !== "preference" || atom.preference === undefined) {
|
|
84
|
+
throw new Error("User-default preference promotion requires a preference atom with a preference envelope");
|
|
85
|
+
}
|
|
86
|
+
if (atom.preference.scope.level === "user-default") {
|
|
87
|
+
throw new Error(`Preference ${atom.memoryId} is already in the user-default scope`);
|
|
88
|
+
}
|
|
89
|
+
const envelope = {
|
|
90
|
+
...atom.preference,
|
|
91
|
+
scope: { level: "user-default" },
|
|
92
|
+
confirmedAt: input.confirmedAt,
|
|
93
|
+
};
|
|
94
|
+
return validateMemoryAtomV1({
|
|
95
|
+
...atom,
|
|
96
|
+
memoryId: `${atom.memoryId}-user-default`,
|
|
97
|
+
observationId: `${atom.observationId}-user-default`,
|
|
98
|
+
preference: envelope,
|
|
99
|
+
writeReason: `promote-preference-to-user-default by ${input.authorizedBy}`,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
//# sourceMappingURL=preference-promotion.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"preference-promotion.js","sourceRoot":"","sources":["../../src/memory/preference-promotion.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Automatic preference promotion (2.4d1) — the explicit-confirmation-free\n * promotion policy over preference observations.\n *\n * Promotion NEVER fakes a user confirmation (no `confirmedAt`, no\n * `explicit` evidence class): it deterministically raises the winning\n * preference's `applicabilityConfidence` once independent repeated-behavior\n * or inferred observations reach the policy thresholds. Policies carry a\n * version and an explicit `enabled` switch — a disabled policy returns\n * `disabled` for every input and never promotes silently. The same\n * observation sequence under the same policy version resolves identically\n * on any engine instance, so decisions are migratable/replayable.\n * Complex disambiguation (2.4d2), full lifecycle promotion/decay (2.4d3),\n * and networks (2.4d4) are out of scope.\n */\nimport type { JsonValue } from \"@agent-forge/plugin-sdk\";\nimport type { MemoryAtomV1, MemoryPreferenceEnvelopeV1 } from \"./foundation.ts\";\nimport { validateMemoryAtomV1 } from \"./foundation.ts\";\n\nexport interface PreferencePromotionPolicyV1 {\n\t/** Master switch; disabled engines never promote. */\n\treadonly enabled: boolean;\n\treadonly policyVersion: string;\n\t/** Independent repeated-behavior observations required for promotion. */\n\treadonly repeatedBehaviorThreshold: number;\n\t/** Independent inferred observations required for promotion. */\n\treadonly inferredThreshold: number;\n\t/** The confidence assigned by a promotion decision (0..1]. */\n\treadonly promotedConfidence: number;\n}\n\nexport type PreferenceEvidenceClassV1 = MemoryPreferenceEnvelopeV1[\"evidence\"][\"class\"];\n\nexport interface PreferenceObservationV1 {\n\treadonly evidenceClass: PreferenceEvidenceClassV1;\n\t/** Distinct observation identity; duplicates within a sequence are ignored. */\n\treadonly observationId: string;\n\treadonly confidence?: number;\n}\n\nexport type PreferencePromotionDecisionV1 =\n\t| {\n\t\t\treadonly decision: \"promoted\";\n\t\t\treadonly policyVersion: string;\n\t\t\treadonly fromConfidence: number;\n\t\t\treadonly toConfidence: number;\n\t\t\treadonly supportingObservations: number;\n\t }\n\t| {\n\t\t\treadonly decision: \"not_promoted\";\n\t\t\treadonly policyVersion: string;\n\t\t\treadonly supportingObservations: number;\n\t\t\treadonly requiredObservations: number;\n\t }\n\t| { readonly decision: \"disabled\"; readonly policyVersion: string };\n\nexport interface PreferencePromotionEngineV1 {\n\t/** Evaluates one preference candidate (same subject+key+value) deterministically. */\n\tevaluate(input: {\n\t\treadonly currentConfidence: number;\n\t\treadonly observations: readonly PreferenceObservationV1[];\n\t}): PreferencePromotionDecisionV1;\n\treadonly policyVersion: string;\n}\n\nexport function createPreferencePromotionEngine(options: {\n\treadonly policy: PreferencePromotionPolicyV1;\n}): PreferencePromotionEngineV1 {\n\tconst policy = options.policy;\n\tif (policy.promotedConfidence <= 0 || policy.promotedConfidence > 1) {\n\t\tthrow new Error(\"Promotion promotedConfidence must be within (0..1]\");\n\t}\n\tif (policy.repeatedBehaviorThreshold < 1 || policy.inferredThreshold < 1) {\n\t\tthrow new Error(\"Promotion thresholds must be at least 1 observation\");\n\t}\n\n\treturn {\n\t\tpolicyVersion: policy.policyVersion,\n\t\tevaluate(input) {\n\t\t\tif (!policy.enabled) {\n\t\t\t\treturn { decision: \"disabled\", policyVersion: policy.policyVersion };\n\t\t\t}\n\t\t\t// Independent observations only: dedupe by observationId deterministically.\n\t\t\tconst seen = new Map<string, PreferenceObservationV1>();\n\t\t\tfor (const observation of input.observations) {\n\t\t\t\tif (observation.evidenceClass !== \"repeated_behavior\" && observation.evidenceClass !== \"inferred\") {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tif (!seen.has(observation.observationId)) seen.set(observation.observationId, observation);\n\t\t\t}\n\t\t\tconst supporting = [...seen.values()].filter((observation) => (observation.confidence ?? 1) >= 0.5);\n\n\t\t\tconst repeatedCount = supporting.filter(\n\t\t\t\t(observation) => observation.evidenceClass === \"repeated_behavior\",\n\t\t\t).length;\n\t\t\tconst inferredCount = supporting.filter((observation) => observation.evidenceClass === \"inferred\").length;\n\t\t\tconst required = repeatedCount >= inferredCount ? policy.repeatedBehaviorThreshold : policy.inferredThreshold;\n\t\t\tconst eligibleCount = Math.max(repeatedCount, inferredCount);\n\n\t\t\tif (repeatedCount >= policy.repeatedBehaviorThreshold || inferredCount >= policy.inferredThreshold) {\n\t\t\t\treturn {\n\t\t\t\t\tdecision: \"promoted\",\n\t\t\t\t\tpolicyVersion: policy.policyVersion,\n\t\t\t\t\tfromConfidence: input.currentConfidence,\n\t\t\t\t\ttoConfidence: Math.max(policy.promotedConfidence, input.currentConfidence),\n\t\t\t\t\tsupportingObservations: eligibleCount,\n\t\t\t\t};\n\t\t\t}\n\t\t\treturn {\n\t\t\t\tdecision: \"not_promoted\",\n\t\t\t\tpolicyVersion: policy.policyVersion,\n\t\t\t\tsupportingObservations: eligibleCount,\n\t\t\t\trequiredObservations: required,\n\t\t\t};\n\t\t},\n\t};\n}\n\n/** Serializes a policy for migration/replay ledgers. */\nexport function serializePromotionPolicy(policy: PreferencePromotionPolicyV1): JsonValue {\n\treturn {\n\t\tenabled: policy.enabled,\n\t\tpolicyVersion: policy.policyVersion,\n\t\trepeatedBehaviorThreshold: policy.repeatedBehaviorThreshold,\n\t\tinferredThreshold: policy.inferredThreshold,\n\t\tpromotedConfidence: policy.promotedConfidence,\n\t};\n}\n\n// ---------------------------------------------------------------------------\n// Explicit user-confirmed promotion to user-default (方案系统设计 §6.1, M5)\n// ---------------------------------------------------------------------------\n\nexport interface PromotePreferenceToUserDefaultInputV1 {\n\t/** The source preference atom; it is preserved untouched. */\n\treadonly atom: MemoryAtomV1;\n\t/** Who authorized the promotion (explicit confirmation provenance); required. */\n\treadonly authorizedBy: string;\n\t/** ISO-8601 confirmation timestamp stamped into the promoted envelope's `confirmedAt`. */\n\treadonly confirmedAt: string;\n}\n\n/**\n * Explicitly promotes a preference atom to the user-default scope (方案系统\n * 设计 §6.1 跨方案规则). Produces a NEW canonical atom — the original is never\n * rewritten — whose envelope carries `scope.level \"user-default\"` and\n * `confirmedAt`, the canonical explicit-confirmation marker the suite read\n * boundary ({@link memorySuiteVisibility} in foundation.ts) requires for\n * cross-suite visibility. Scope anchors are intentionally dropped: a\n * user-default preference applies without narrowing. The automatic\n * applicabilityConfidence policy above stays orthogonal — it never touches\n * user-default and never sets `confirmedAt`.\n *\n * Rejects non-preference atoms, atoms already at user-default, and missing\n * `authorizedBy`/`confirmedAt`.\n */\nexport function promotePreferenceToUserDefault(input: PromotePreferenceToUserDefaultInputV1): MemoryAtomV1 {\n\tif (typeof input.authorizedBy !== \"string\" || input.authorizedBy.trim().length === 0) {\n\t\tthrow new Error(\"User-default preference promotion requires a non-empty authorizedBy\");\n\t}\n\tif (typeof input.confirmedAt !== \"string\" || input.confirmedAt.trim().length === 0) {\n\t\tthrow new Error(\"User-default preference promotion requires a non-empty confirmedAt\");\n\t}\n\tif (Number.isNaN(Date.parse(input.confirmedAt))) {\n\t\tthrow new Error(\"User-default preference promotion confirmedAt must be an ISO-8601 timestamp\");\n\t}\n\tconst atom = input.atom;\n\tif (atom.memoryKind !== \"preference\" || atom.preference === undefined) {\n\t\tthrow new Error(\"User-default preference promotion requires a preference atom with a preference envelope\");\n\t}\n\tif (atom.preference.scope.level === \"user-default\") {\n\t\tthrow new Error(`Preference ${atom.memoryId} is already in the user-default scope`);\n\t}\n\tconst envelope: MemoryPreferenceEnvelopeV1 = {\n\t\t...atom.preference,\n\t\tscope: { level: \"user-default\" },\n\t\tconfirmedAt: input.confirmedAt,\n\t};\n\treturn validateMemoryAtomV1({\n\t\t...atom,\n\t\tmemoryId: `${atom.memoryId}-user-default`,\n\t\tobservationId: `${atom.observationId}-user-default`,\n\t\tpreference: envelope,\n\t\twriteReason: `promote-preference-to-user-default by ${input.authorizedBy}`,\n\t});\n}\n"]}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Preference resolver (2.4c) — scoped application and conflict clarification
|
|
3
|
+
* over recalled preference atoms (1C.6a envelope schema, unchanged).
|
|
4
|
+
*
|
|
5
|
+
* Resolution is deterministic and replayable:
|
|
6
|
+
* 1. Scope filter — a preference only applies when the current context
|
|
7
|
+
* satisfies its scope (scenes/projects/tasks) and hits no exception.
|
|
8
|
+
* 2. Instruction override — a current-instruction value for the same key
|
|
9
|
+
* always wins over stored preferences (当前指令优先).
|
|
10
|
+
* 3. Deterministic ranking — confirmed explicit preferences outrank
|
|
11
|
+
* repeated_behavior, then inferred; ties break by applicabilityConfidence,
|
|
12
|
+
* then committedAt (later wins), then memoryId.
|
|
13
|
+
* 4. No silent side-taking — when competing values remain and the winner is
|
|
14
|
+
* not explicitly confirmed, the outcome is `conflict_unconfirmed`, which
|
|
15
|
+
* callers must surface for explicit user confirmation.
|
|
16
|
+
* Automatic behavior learning and cross-Profile sharing are later slices.
|
|
17
|
+
*/
|
|
18
|
+
import type { JsonValue } from "@agent-forge/plugin-sdk";
|
|
19
|
+
import type { MemoryAtomV1, MemoryPreferenceEnvelopeV1 } from "./foundation.ts";
|
|
20
|
+
export interface PreferenceContextV1 {
|
|
21
|
+
readonly scenes?: readonly string[];
|
|
22
|
+
readonly projects?: readonly string[];
|
|
23
|
+
readonly tasks?: readonly string[];
|
|
24
|
+
/** The user's current instruction value for a key (当前指令优先). */
|
|
25
|
+
readonly currentInstructions?: Readonly<Record<string, JsonValue>>;
|
|
26
|
+
}
|
|
27
|
+
export interface PreferenceDecisionV1 {
|
|
28
|
+
readonly subject: MemoryPreferenceEnvelopeV1["subject"];
|
|
29
|
+
readonly key: string;
|
|
30
|
+
readonly status: "applied" | "instruction_override" | "conflict_unconfirmed";
|
|
31
|
+
/** The winning value (undefined only when unconfirmed conflict is surfaced). */
|
|
32
|
+
readonly value?: JsonValue;
|
|
33
|
+
readonly winnerMemoryId?: string;
|
|
34
|
+
readonly instructionValue?: JsonValue;
|
|
35
|
+
readonly superseded: readonly {
|
|
36
|
+
readonly memoryId: string;
|
|
37
|
+
readonly value: JsonValue;
|
|
38
|
+
}[];
|
|
39
|
+
}
|
|
40
|
+
export interface PreferenceResolverV1 {
|
|
41
|
+
resolve(preferences: readonly MemoryAtomV1[], context: PreferenceContextV1): readonly PreferenceDecisionV1[];
|
|
42
|
+
}
|
|
43
|
+
export declare function createPreferenceResolver(): PreferenceResolverV1;
|
|
44
|
+
//# sourceMappingURL=preference-resolver.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"preference-resolver.d.ts","sourceRoot":"","sources":["../../src/memory/preference-resolver.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AACzD,OAAO,KAAK,EAAE,YAAY,EAAE,0BAA0B,EAAE,MAAM,iBAAiB,CAAC;AAEhF,MAAM,WAAW,mBAAmB;IACnC,QAAQ,CAAC,MAAM,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC,QAAQ,CAAC,QAAQ,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACtC,QAAQ,CAAC,KAAK,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,2EAA+D;IAC/D,QAAQ,CAAC,mBAAmB,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC,CAAC;CACnE;AAED,MAAM,WAAW,oBAAoB;IACpC,QAAQ,CAAC,OAAO,EAAE,0BAA0B,CAAC,SAAS,CAAC,CAAC;IACxD,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,MAAM,EAAE,SAAS,GAAG,sBAAsB,GAAG,sBAAsB,CAAC;IAC7E,gFAAgF;IAChF,QAAQ,CAAC,KAAK,CAAC,EAAE,SAAS,CAAC;IAC3B,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,gBAAgB,CAAC,EAAE,SAAS,CAAC;IACtC,QAAQ,CAAC,UAAU,EAAE,SAAS;QAAE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAA;KAAE,EAAE,CAAC;CACzF;AAED,MAAM,WAAW,oBAAoB;IACpC,OAAO,CAAC,WAAW,EAAE,SAAS,YAAY,EAAE,EAAE,OAAO,EAAE,mBAAmB,GAAG,SAAS,oBAAoB,EAAE,CAAC;CAC7G;AAED,wBAAgB,wBAAwB,IAAI,oBAAoB,CAqG/D","sourcesContent":["/**\n * Preference resolver (2.4c) — scoped application and conflict clarification\n * over recalled preference atoms (1C.6a envelope schema, unchanged).\n *\n * Resolution is deterministic and replayable:\n * 1. Scope filter — a preference only applies when the current context\n * satisfies its scope (scenes/projects/tasks) and hits no exception.\n * 2. Instruction override — a current-instruction value for the same key\n * always wins over stored preferences (当前指令优先).\n * 3. Deterministic ranking — confirmed explicit preferences outrank\n * repeated_behavior, then inferred; ties break by applicabilityConfidence,\n * then committedAt (later wins), then memoryId.\n * 4. No silent side-taking — when competing values remain and the winner is\n * not explicitly confirmed, the outcome is `conflict_unconfirmed`, which\n * callers must surface for explicit user confirmation.\n * Automatic behavior learning and cross-Profile sharing are later slices.\n */\nimport type { JsonValue } from \"@agent-forge/plugin-sdk\";\nimport type { MemoryAtomV1, MemoryPreferenceEnvelopeV1 } from \"./foundation.ts\";\n\nexport interface PreferenceContextV1 {\n\treadonly scenes?: readonly string[];\n\treadonly projects?: readonly string[];\n\treadonly tasks?: readonly string[];\n\t/** The user's current instruction value for a key (当前指令优先). */\n\treadonly currentInstructions?: Readonly<Record<string, JsonValue>>;\n}\n\nexport interface PreferenceDecisionV1 {\n\treadonly subject: MemoryPreferenceEnvelopeV1[\"subject\"];\n\treadonly key: string;\n\treadonly status: \"applied\" | \"instruction_override\" | \"conflict_unconfirmed\";\n\t/** The winning value (undefined only when unconfirmed conflict is surfaced). */\n\treadonly value?: JsonValue;\n\treadonly winnerMemoryId?: string;\n\treadonly instructionValue?: JsonValue;\n\treadonly superseded: readonly { readonly memoryId: string; readonly value: JsonValue }[];\n}\n\nexport interface PreferenceResolverV1 {\n\tresolve(preferences: readonly MemoryAtomV1[], context: PreferenceContextV1): readonly PreferenceDecisionV1[];\n}\n\nexport function createPreferenceResolver(): PreferenceResolverV1 {\n\tconst inScope = (preference: MemoryPreferenceEnvelopeV1, context: PreferenceContextV1): boolean => {\n\t\tconst scope = preference.scope;\n\t\tconst matches = (values: readonly string[] | undefined, current: readonly string[] | undefined): boolean => {\n\t\t\tif (values === undefined || values.length === 0) return true;\n\t\t\tif (current === undefined) return false;\n\t\t\treturn values.some((value) => current.includes(value));\n\t\t};\n\t\tif (!matches(scope.scenes, context.scenes)) return false;\n\t\tif (!matches(scope.projects, context.projects)) return false;\n\t\tif (!matches(scope.tasks, context.tasks)) return false;\n\t\tif (scope.exceptions !== undefined && context.scenes !== undefined) {\n\t\t\tif (scope.exceptions.some((exception) => context.scenes?.includes(exception))) return false;\n\t\t}\n\t\treturn true;\n\t};\n\n\tconst rankClass = (preference: MemoryPreferenceEnvelopeV1): number => {\n\t\tif (preference.confirmedAt !== undefined) return 3;\n\t\tif (preference.evidence.class === \"explicit\") return 2;\n\t\tif (preference.evidence.class === \"repeated_behavior\") return 1;\n\t\treturn 0;\n\t};\n\n\tconst compareAtoms = (a: MemoryAtomV1, b: MemoryAtomV1): number => {\n\t\tconst classDiff = rankClass(b.preference!) - rankClass(a.preference!);\n\t\tif (classDiff !== 0) return classDiff;\n\t\tconst confidenceDiff = b.preference!.applicabilityConfidence - a.preference!.applicabilityConfidence;\n\t\tif (confidenceDiff !== 0) return confidenceDiff;\n\t\tconst timeDiff = Date.parse(b.occurredAt) - Date.parse(a.occurredAt);\n\t\tif (timeDiff !== 0) return timeDiff;\n\t\treturn a.memoryId.localeCompare(b.memoryId);\n\t};\n\n\treturn {\n\t\tresolve(preferences, context) {\n\t\t\tconst byKey = new Map<string, MemoryAtomV1[]>();\n\t\t\tfor (const atom of preferences) {\n\t\t\t\tif (atom.memoryKind !== \"preference\" || !atom.preference) continue;\n\t\t\t\tif (!inScope(atom.preference, context)) continue;\n\t\t\t\tconst identity = `${atom.preference.subject}\\u0000${atom.preference.key}`;\n\t\t\t\tconst list = byKey.get(identity) ?? [];\n\t\t\t\tlist.push(atom);\n\t\t\t\tbyKey.set(identity, list);\n\t\t\t}\n\n\t\t\tconst decisions: PreferenceDecisionV1[] = [];\n\t\t\tfor (const [identity, atoms] of byKey) {\n\t\t\t\tconst separator = identity.indexOf(\"\\u0000\");\n\t\t\t\tconst key = identity.slice(separator + 1);\n\t\t\t\tconst instructionValue = context.currentInstructions?.[key];\n\t\t\t\tconst supersededOf = (winner: MemoryAtomV1) =>\n\t\t\t\t\tatoms\n\t\t\t\t\t\t.filter((atom) => atom !== winner)\n\t\t\t\t\t\t.map((atom) => ({ memoryId: atom.memoryId, value: atom.preference!.preferredValue }));\n\n\t\t\t\tif (instructionValue !== undefined) {\n\t\t\t\t\tdecisions.push({\n\t\t\t\t\t\tsubject: atoms[0]!.preference!.subject,\n\t\t\t\t\t\tkey,\n\t\t\t\t\t\tstatus: \"instruction_override\",\n\t\t\t\t\t\tvalue: instructionValue,\n\t\t\t\t\t\tinstructionValue,\n\t\t\t\t\t\tsuperseded: atoms.map((atom) => ({\n\t\t\t\t\t\t\tmemoryId: atom.memoryId,\n\t\t\t\t\t\t\tvalue: atom.preference!.preferredValue,\n\t\t\t\t\t\t})),\n\t\t\t\t\t});\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tconst sorted = [...atoms].sort(compareAtoms);\n\t\t\t\tconst winner = sorted[0]!;\n\t\t\t\tconst winnerPreference = winner.preference!;\n\t\t\t\tconst distinctValues = new Set(atoms.map((atom) => JSON.stringify(atom.preference!.preferredValue)));\n\n\t\t\t\tif (distinctValues.size > 1 && rankClass(winnerPreference) < 3) {\n\t\t\t\t\t// Competing values without an explicit confirmation: surface the\n\t\t\t\t\t// conflict, never silently pick a side.\n\t\t\t\t\tdecisions.push({\n\t\t\t\t\t\tsubject: winnerPreference.subject,\n\t\t\t\t\t\tkey,\n\t\t\t\t\t\tstatus: \"conflict_unconfirmed\",\n\t\t\t\t\t\tsuperseded: supersededOf(winner),\n\t\t\t\t\t});\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tdecisions.push({\n\t\t\t\t\tsubject: winnerPreference.subject,\n\t\t\t\t\tkey,\n\t\t\t\t\tstatus: \"applied\",\n\t\t\t\t\tvalue: winnerPreference.preferredValue,\n\t\t\t\t\twinnerMemoryId: winner.memoryId,\n\t\t\t\t\tsuperseded: supersededOf(winner),\n\t\t\t\t});\n\t\t\t}\n\n\t\t\treturn decisions.sort((a, b) => a.subject.localeCompare(b.subject) || a.key.localeCompare(b.key));\n\t\t},\n\t};\n}\n"]}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
export function createPreferenceResolver() {
|
|
2
|
+
const inScope = (preference, context) => {
|
|
3
|
+
const scope = preference.scope;
|
|
4
|
+
const matches = (values, current) => {
|
|
5
|
+
if (values === undefined || values.length === 0)
|
|
6
|
+
return true;
|
|
7
|
+
if (current === undefined)
|
|
8
|
+
return false;
|
|
9
|
+
return values.some((value) => current.includes(value));
|
|
10
|
+
};
|
|
11
|
+
if (!matches(scope.scenes, context.scenes))
|
|
12
|
+
return false;
|
|
13
|
+
if (!matches(scope.projects, context.projects))
|
|
14
|
+
return false;
|
|
15
|
+
if (!matches(scope.tasks, context.tasks))
|
|
16
|
+
return false;
|
|
17
|
+
if (scope.exceptions !== undefined && context.scenes !== undefined) {
|
|
18
|
+
if (scope.exceptions.some((exception) => context.scenes?.includes(exception)))
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
return true;
|
|
22
|
+
};
|
|
23
|
+
const rankClass = (preference) => {
|
|
24
|
+
if (preference.confirmedAt !== undefined)
|
|
25
|
+
return 3;
|
|
26
|
+
if (preference.evidence.class === "explicit")
|
|
27
|
+
return 2;
|
|
28
|
+
if (preference.evidence.class === "repeated_behavior")
|
|
29
|
+
return 1;
|
|
30
|
+
return 0;
|
|
31
|
+
};
|
|
32
|
+
const compareAtoms = (a, b) => {
|
|
33
|
+
const classDiff = rankClass(b.preference) - rankClass(a.preference);
|
|
34
|
+
if (classDiff !== 0)
|
|
35
|
+
return classDiff;
|
|
36
|
+
const confidenceDiff = b.preference.applicabilityConfidence - a.preference.applicabilityConfidence;
|
|
37
|
+
if (confidenceDiff !== 0)
|
|
38
|
+
return confidenceDiff;
|
|
39
|
+
const timeDiff = Date.parse(b.occurredAt) - Date.parse(a.occurredAt);
|
|
40
|
+
if (timeDiff !== 0)
|
|
41
|
+
return timeDiff;
|
|
42
|
+
return a.memoryId.localeCompare(b.memoryId);
|
|
43
|
+
};
|
|
44
|
+
return {
|
|
45
|
+
resolve(preferences, context) {
|
|
46
|
+
const byKey = new Map();
|
|
47
|
+
for (const atom of preferences) {
|
|
48
|
+
if (atom.memoryKind !== "preference" || !atom.preference)
|
|
49
|
+
continue;
|
|
50
|
+
if (!inScope(atom.preference, context))
|
|
51
|
+
continue;
|
|
52
|
+
const identity = `${atom.preference.subject}\u0000${atom.preference.key}`;
|
|
53
|
+
const list = byKey.get(identity) ?? [];
|
|
54
|
+
list.push(atom);
|
|
55
|
+
byKey.set(identity, list);
|
|
56
|
+
}
|
|
57
|
+
const decisions = [];
|
|
58
|
+
for (const [identity, atoms] of byKey) {
|
|
59
|
+
const separator = identity.indexOf("\u0000");
|
|
60
|
+
const key = identity.slice(separator + 1);
|
|
61
|
+
const instructionValue = context.currentInstructions?.[key];
|
|
62
|
+
const supersededOf = (winner) => atoms
|
|
63
|
+
.filter((atom) => atom !== winner)
|
|
64
|
+
.map((atom) => ({ memoryId: atom.memoryId, value: atom.preference.preferredValue }));
|
|
65
|
+
if (instructionValue !== undefined) {
|
|
66
|
+
decisions.push({
|
|
67
|
+
subject: atoms[0].preference.subject,
|
|
68
|
+
key,
|
|
69
|
+
status: "instruction_override",
|
|
70
|
+
value: instructionValue,
|
|
71
|
+
instructionValue,
|
|
72
|
+
superseded: atoms.map((atom) => ({
|
|
73
|
+
memoryId: atom.memoryId,
|
|
74
|
+
value: atom.preference.preferredValue,
|
|
75
|
+
})),
|
|
76
|
+
});
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
const sorted = [...atoms].sort(compareAtoms);
|
|
80
|
+
const winner = sorted[0];
|
|
81
|
+
const winnerPreference = winner.preference;
|
|
82
|
+
const distinctValues = new Set(atoms.map((atom) => JSON.stringify(atom.preference.preferredValue)));
|
|
83
|
+
if (distinctValues.size > 1 && rankClass(winnerPreference) < 3) {
|
|
84
|
+
// Competing values without an explicit confirmation: surface the
|
|
85
|
+
// conflict, never silently pick a side.
|
|
86
|
+
decisions.push({
|
|
87
|
+
subject: winnerPreference.subject,
|
|
88
|
+
key,
|
|
89
|
+
status: "conflict_unconfirmed",
|
|
90
|
+
superseded: supersededOf(winner),
|
|
91
|
+
});
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
decisions.push({
|
|
95
|
+
subject: winnerPreference.subject,
|
|
96
|
+
key,
|
|
97
|
+
status: "applied",
|
|
98
|
+
value: winnerPreference.preferredValue,
|
|
99
|
+
winnerMemoryId: winner.memoryId,
|
|
100
|
+
superseded: supersededOf(winner),
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
return decisions.sort((a, b) => a.subject.localeCompare(b.subject) || a.key.localeCompare(b.key));
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
//# sourceMappingURL=preference-resolver.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"preference-resolver.js","sourceRoot":"","sources":["../../src/memory/preference-resolver.ts"],"names":[],"mappings":"AA2CA,MAAM,UAAU,wBAAwB,GAAyB;IAChE,MAAM,OAAO,GAAG,CAAC,UAAsC,EAAE,OAA4B,EAAW,EAAE,CAAC;QAClG,MAAM,KAAK,GAAG,UAAU,CAAC,KAAK,CAAC;QAC/B,MAAM,OAAO,GAAG,CAAC,MAAqC,EAAE,OAAsC,EAAW,EAAE,CAAC;YAC3G,IAAI,MAAM,KAAK,SAAS,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;gBAAE,OAAO,IAAI,CAAC;YAC7D,IAAI,OAAO,KAAK,SAAS;gBAAE,OAAO,KAAK,CAAC;YACxC,OAAO,MAAM,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC;QAAA,CACvD,CAAC;QACF,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,MAAM,EAAE,OAAO,CAAC,MAAM,CAAC;YAAE,OAAO,KAAK,CAAC;QACzD,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,QAAQ,EAAE,OAAO,CAAC,QAAQ,CAAC;YAAE,OAAO,KAAK,CAAC;QAC7D,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,KAAK,EAAE,OAAO,CAAC,KAAK,CAAC;YAAE,OAAO,KAAK,CAAC;QACvD,IAAI,KAAK,CAAC,UAAU,KAAK,SAAS,IAAI,OAAO,CAAC,MAAM,KAAK,SAAS,EAAE,CAAC;YACpE,IAAI,KAAK,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,OAAO,CAAC,MAAM,EAAE,QAAQ,CAAC,SAAS,CAAC,CAAC;gBAAE,OAAO,KAAK,CAAC;QAC7F,CAAC;QACD,OAAO,IAAI,CAAC;IAAA,CACZ,CAAC;IAEF,MAAM,SAAS,GAAG,CAAC,UAAsC,EAAU,EAAE,CAAC;QACrE,IAAI,UAAU,CAAC,WAAW,KAAK,SAAS;YAAE,OAAO,CAAC,CAAC;QACnD,IAAI,UAAU,CAAC,QAAQ,CAAC,KAAK,KAAK,UAAU;YAAE,OAAO,CAAC,CAAC;QACvD,IAAI,UAAU,CAAC,QAAQ,CAAC,KAAK,KAAK,mBAAmB;YAAE,OAAO,CAAC,CAAC;QAChE,OAAO,CAAC,CAAC;IAAA,CACT,CAAC;IAEF,MAAM,YAAY,GAAG,CAAC,CAAe,EAAE,CAAe,EAAU,EAAE,CAAC;QAClE,MAAM,SAAS,GAAG,SAAS,CAAC,CAAC,CAAC,UAAW,CAAC,GAAG,SAAS,CAAC,CAAC,CAAC,UAAW,CAAC,CAAC;QACtE,IAAI,SAAS,KAAK,CAAC;YAAE,OAAO,SAAS,CAAC;QACtC,MAAM,cAAc,GAAG,CAAC,CAAC,UAAW,CAAC,uBAAuB,GAAG,CAAC,CAAC,UAAW,CAAC,uBAAuB,CAAC;QACrG,IAAI,cAAc,KAAK,CAAC;YAAE,OAAO,cAAc,CAAC;QAChD,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,UAAU,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC;QACrE,IAAI,QAAQ,KAAK,CAAC;YAAE,OAAO,QAAQ,CAAC;QACpC,OAAO,CAAC,CAAC,QAAQ,CAAC,aAAa,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC;IAAA,CAC5C,CAAC;IAEF,OAAO;QACN,OAAO,CAAC,WAAW,EAAE,OAAO,EAAE;YAC7B,MAAM,KAAK,GAAG,IAAI,GAAG,EAA0B,CAAC;YAChD,KAAK,MAAM,IAAI,IAAI,WAAW,EAAE,CAAC;gBAChC,IAAI,IAAI,CAAC,UAAU,KAAK,YAAY,IAAI,CAAC,IAAI,CAAC,UAAU;oBAAE,SAAS;gBACnE,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,UAAU,EAAE,OAAO,CAAC;oBAAE,SAAS;gBACjD,MAAM,QAAQ,GAAG,GAAG,IAAI,CAAC,UAAU,CAAC,OAAO,SAAS,IAAI,CAAC,UAAU,CAAC,GAAG,EAAE,CAAC;gBAC1E,MAAM,IAAI,GAAG,KAAK,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC;gBACvC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;gBAChB,KAAK,CAAC,GAAG,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;YAC3B,CAAC;YAED,MAAM,SAAS,GAA2B,EAAE,CAAC;YAC7C,KAAK,MAAM,CAAC,QAAQ,EAAE,KAAK,CAAC,IAAI,KAAK,EAAE,CAAC;gBACvC,MAAM,SAAS,GAAG,QAAQ,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;gBAC7C,MAAM,GAAG,GAAG,QAAQ,CAAC,KAAK,CAAC,SAAS,GAAG,CAAC,CAAC,CAAC;gBAC1C,MAAM,gBAAgB,GAAG,OAAO,CAAC,mBAAmB,EAAE,CAAC,GAAG,CAAC,CAAC;gBAC5D,MAAM,YAAY,GAAG,CAAC,MAAoB,EAAE,EAAE,CAC7C,KAAK;qBACH,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,KAAK,MAAM,CAAC;qBACjC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,EAAE,QAAQ,EAAE,IAAI,CAAC,QAAQ,EAAE,KAAK,EAAE,IAAI,CAAC,UAAW,CAAC,cAAc,EAAE,CAAC,CAAC,CAAC;gBAExF,IAAI,gBAAgB,KAAK,SAAS,EAAE,CAAC;oBACpC,SAAS,CAAC,IAAI,CAAC;wBACd,OAAO,EAAE,KAAK,CAAC,CAAC,CAAE,CAAC,UAAW,CAAC,OAAO;wBACtC,GAAG;wBACH,MAAM,EAAE,sBAAsB;wBAC9B,KAAK,EAAE,gBAAgB;wBACvB,gBAAgB;wBAChB,UAAU,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;4BAChC,QAAQ,EAAE,IAAI,CAAC,QAAQ;4BACvB,KAAK,EAAE,IAAI,CAAC,UAAW,CAAC,cAAc;yBACtC,CAAC,CAAC;qBACH,CAAC,CAAC;oBACH,SAAS;gBACV,CAAC;gBAED,MAAM,MAAM,GAAG,CAAC,GAAG,KAAK,CAAC,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;gBAC7C,MAAM,MAAM,GAAG,MAAM,CAAC,CAAC,CAAE,CAAC;gBAC1B,MAAM,gBAAgB,GAAG,MAAM,CAAC,UAAW,CAAC;gBAC5C,MAAM,cAAc,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,UAAW,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC;gBAErG,IAAI,cAAc,CAAC,IAAI,GAAG,CAAC,IAAI,SAAS,CAAC,gBAAgB,CAAC,GAAG,CAAC,EAAE,CAAC;oBAChE,iEAAiE;oBACjE,wCAAwC;oBACxC,SAAS,CAAC,IAAI,CAAC;wBACd,OAAO,EAAE,gBAAgB,CAAC,OAAO;wBACjC,GAAG;wBACH,MAAM,EAAE,sBAAsB;wBAC9B,UAAU,EAAE,YAAY,CAAC,MAAM,CAAC;qBAChC,CAAC,CAAC;oBACH,SAAS;gBACV,CAAC;gBAED,SAAS,CAAC,IAAI,CAAC;oBACd,OAAO,EAAE,gBAAgB,CAAC,OAAO;oBACjC,GAAG;oBACH,MAAM,EAAE,SAAS;oBACjB,KAAK,EAAE,gBAAgB,CAAC,cAAc;oBACtC,cAAc,EAAE,MAAM,CAAC,QAAQ;oBAC/B,UAAU,EAAE,YAAY,CAAC,MAAM,CAAC;iBAChC,CAAC,CAAC;YACJ,CAAC;YAED,OAAO,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,aAAa,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;QAAA,CAClG;KACD,CAAC;AAAA,CACF","sourcesContent":["/**\n * Preference resolver (2.4c) — scoped application and conflict clarification\n * over recalled preference atoms (1C.6a envelope schema, unchanged).\n *\n * Resolution is deterministic and replayable:\n * 1. Scope filter — a preference only applies when the current context\n * satisfies its scope (scenes/projects/tasks) and hits no exception.\n * 2. Instruction override — a current-instruction value for the same key\n * always wins over stored preferences (当前指令优先).\n * 3. Deterministic ranking — confirmed explicit preferences outrank\n * repeated_behavior, then inferred; ties break by applicabilityConfidence,\n * then committedAt (later wins), then memoryId.\n * 4. No silent side-taking — when competing values remain and the winner is\n * not explicitly confirmed, the outcome is `conflict_unconfirmed`, which\n * callers must surface for explicit user confirmation.\n * Automatic behavior learning and cross-Profile sharing are later slices.\n */\nimport type { JsonValue } from \"@agent-forge/plugin-sdk\";\nimport type { MemoryAtomV1, MemoryPreferenceEnvelopeV1 } from \"./foundation.ts\";\n\nexport interface PreferenceContextV1 {\n\treadonly scenes?: readonly string[];\n\treadonly projects?: readonly string[];\n\treadonly tasks?: readonly string[];\n\t/** The user's current instruction value for a key (当前指令优先). */\n\treadonly currentInstructions?: Readonly<Record<string, JsonValue>>;\n}\n\nexport interface PreferenceDecisionV1 {\n\treadonly subject: MemoryPreferenceEnvelopeV1[\"subject\"];\n\treadonly key: string;\n\treadonly status: \"applied\" | \"instruction_override\" | \"conflict_unconfirmed\";\n\t/** The winning value (undefined only when unconfirmed conflict is surfaced). */\n\treadonly value?: JsonValue;\n\treadonly winnerMemoryId?: string;\n\treadonly instructionValue?: JsonValue;\n\treadonly superseded: readonly { readonly memoryId: string; readonly value: JsonValue }[];\n}\n\nexport interface PreferenceResolverV1 {\n\tresolve(preferences: readonly MemoryAtomV1[], context: PreferenceContextV1): readonly PreferenceDecisionV1[];\n}\n\nexport function createPreferenceResolver(): PreferenceResolverV1 {\n\tconst inScope = (preference: MemoryPreferenceEnvelopeV1, context: PreferenceContextV1): boolean => {\n\t\tconst scope = preference.scope;\n\t\tconst matches = (values: readonly string[] | undefined, current: readonly string[] | undefined): boolean => {\n\t\t\tif (values === undefined || values.length === 0) return true;\n\t\t\tif (current === undefined) return false;\n\t\t\treturn values.some((value) => current.includes(value));\n\t\t};\n\t\tif (!matches(scope.scenes, context.scenes)) return false;\n\t\tif (!matches(scope.projects, context.projects)) return false;\n\t\tif (!matches(scope.tasks, context.tasks)) return false;\n\t\tif (scope.exceptions !== undefined && context.scenes !== undefined) {\n\t\t\tif (scope.exceptions.some((exception) => context.scenes?.includes(exception))) return false;\n\t\t}\n\t\treturn true;\n\t};\n\n\tconst rankClass = (preference: MemoryPreferenceEnvelopeV1): number => {\n\t\tif (preference.confirmedAt !== undefined) return 3;\n\t\tif (preference.evidence.class === \"explicit\") return 2;\n\t\tif (preference.evidence.class === \"repeated_behavior\") return 1;\n\t\treturn 0;\n\t};\n\n\tconst compareAtoms = (a: MemoryAtomV1, b: MemoryAtomV1): number => {\n\t\tconst classDiff = rankClass(b.preference!) - rankClass(a.preference!);\n\t\tif (classDiff !== 0) return classDiff;\n\t\tconst confidenceDiff = b.preference!.applicabilityConfidence - a.preference!.applicabilityConfidence;\n\t\tif (confidenceDiff !== 0) return confidenceDiff;\n\t\tconst timeDiff = Date.parse(b.occurredAt) - Date.parse(a.occurredAt);\n\t\tif (timeDiff !== 0) return timeDiff;\n\t\treturn a.memoryId.localeCompare(b.memoryId);\n\t};\n\n\treturn {\n\t\tresolve(preferences, context) {\n\t\t\tconst byKey = new Map<string, MemoryAtomV1[]>();\n\t\t\tfor (const atom of preferences) {\n\t\t\t\tif (atom.memoryKind !== \"preference\" || !atom.preference) continue;\n\t\t\t\tif (!inScope(atom.preference, context)) continue;\n\t\t\t\tconst identity = `${atom.preference.subject}\\u0000${atom.preference.key}`;\n\t\t\t\tconst list = byKey.get(identity) ?? [];\n\t\t\t\tlist.push(atom);\n\t\t\t\tbyKey.set(identity, list);\n\t\t\t}\n\n\t\t\tconst decisions: PreferenceDecisionV1[] = [];\n\t\t\tfor (const [identity, atoms] of byKey) {\n\t\t\t\tconst separator = identity.indexOf(\"\\u0000\");\n\t\t\t\tconst key = identity.slice(separator + 1);\n\t\t\t\tconst instructionValue = context.currentInstructions?.[key];\n\t\t\t\tconst supersededOf = (winner: MemoryAtomV1) =>\n\t\t\t\t\tatoms\n\t\t\t\t\t\t.filter((atom) => atom !== winner)\n\t\t\t\t\t\t.map((atom) => ({ memoryId: atom.memoryId, value: atom.preference!.preferredValue }));\n\n\t\t\t\tif (instructionValue !== undefined) {\n\t\t\t\t\tdecisions.push({\n\t\t\t\t\t\tsubject: atoms[0]!.preference!.subject,\n\t\t\t\t\t\tkey,\n\t\t\t\t\t\tstatus: \"instruction_override\",\n\t\t\t\t\t\tvalue: instructionValue,\n\t\t\t\t\t\tinstructionValue,\n\t\t\t\t\t\tsuperseded: atoms.map((atom) => ({\n\t\t\t\t\t\t\tmemoryId: atom.memoryId,\n\t\t\t\t\t\t\tvalue: atom.preference!.preferredValue,\n\t\t\t\t\t\t})),\n\t\t\t\t\t});\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tconst sorted = [...atoms].sort(compareAtoms);\n\t\t\t\tconst winner = sorted[0]!;\n\t\t\t\tconst winnerPreference = winner.preference!;\n\t\t\t\tconst distinctValues = new Set(atoms.map((atom) => JSON.stringify(atom.preference!.preferredValue)));\n\n\t\t\t\tif (distinctValues.size > 1 && rankClass(winnerPreference) < 3) {\n\t\t\t\t\t// Competing values without an explicit confirmation: surface the\n\t\t\t\t\t// conflict, never silently pick a side.\n\t\t\t\t\tdecisions.push({\n\t\t\t\t\t\tsubject: winnerPreference.subject,\n\t\t\t\t\t\tkey,\n\t\t\t\t\t\tstatus: \"conflict_unconfirmed\",\n\t\t\t\t\t\tsuperseded: supersededOf(winner),\n\t\t\t\t\t});\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tdecisions.push({\n\t\t\t\t\tsubject: winnerPreference.subject,\n\t\t\t\t\tkey,\n\t\t\t\t\tstatus: \"applied\",\n\t\t\t\t\tvalue: winnerPreference.preferredValue,\n\t\t\t\t\twinnerMemoryId: winner.memoryId,\n\t\t\t\t\tsuperseded: supersededOf(winner),\n\t\t\t\t});\n\t\t\t}\n\n\t\t\treturn decisions.sort((a, b) => a.subject.localeCompare(b.subject) || a.key.localeCompare(b.key));\n\t\t},\n\t};\n}\n"]}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Memory Foundation — purge journal, recovery, and idempotency fault matrix
|
|
3
|
+
* (1C.6d).
|
|
4
|
+
*
|
|
5
|
+
* The journal is the durable record of every purge batch: authorization,
|
|
6
|
+
* per-replica confirmations, and completion. Recovery rules (frozen per
|
|
7
|
+
* 记忆系统设计.md §5.2/§3.3):
|
|
8
|
+
* - A batch without a `completed` record is NOT complete — after a crash it
|
|
9
|
+
* stays resumable and its memory ids are not treated as purged.
|
|
10
|
+
* - Replica confirmations are idempotent per (batchId, replicaId): replays and
|
|
11
|
+
* retries never double-count.
|
|
12
|
+
* - `purgedMemoryIds()` only ever lists ids from completed batches, so an
|
|
13
|
+
* index rebuild can never resurrect purged data or report un-purged data as
|
|
14
|
+
* purged.
|
|
15
|
+
*/
|
|
16
|
+
import type { MemoryReplicaV1 } from "./purge.ts";
|
|
17
|
+
export type MemoryPurgeJournalRecordV1 = {
|
|
18
|
+
readonly kind: "authorized";
|
|
19
|
+
readonly batchId: string;
|
|
20
|
+
readonly memoryIds: readonly string[];
|
|
21
|
+
readonly replicas: readonly MemoryReplicaV1[];
|
|
22
|
+
readonly authorizedBy: string;
|
|
23
|
+
readonly reason: string;
|
|
24
|
+
readonly at: number;
|
|
25
|
+
} | {
|
|
26
|
+
readonly kind: "replica_confirmed";
|
|
27
|
+
readonly batchId: string;
|
|
28
|
+
readonly replicaId: string;
|
|
29
|
+
readonly at: number;
|
|
30
|
+
} | {
|
|
31
|
+
readonly kind: "completed";
|
|
32
|
+
readonly batchId: string;
|
|
33
|
+
readonly at: number;
|
|
34
|
+
};
|
|
35
|
+
export interface MemoryPurgeJournalBatchStateV1 {
|
|
36
|
+
readonly batchId: string;
|
|
37
|
+
readonly state: "authorized" | "completed";
|
|
38
|
+
readonly memoryIds: readonly string[];
|
|
39
|
+
readonly replicas: readonly MemoryReplicaV1[];
|
|
40
|
+
readonly confirmedReplicas: readonly string[];
|
|
41
|
+
readonly authorizedBy: string;
|
|
42
|
+
readonly reason: string;
|
|
43
|
+
}
|
|
44
|
+
export interface MemoryPurgeJournalV1 {
|
|
45
|
+
authorize(record: {
|
|
46
|
+
readonly batchId: string;
|
|
47
|
+
readonly memoryIds: readonly string[];
|
|
48
|
+
readonly replicas: readonly MemoryReplicaV1[];
|
|
49
|
+
readonly authorizedBy: string;
|
|
50
|
+
readonly reason: string;
|
|
51
|
+
readonly at: number;
|
|
52
|
+
}): {
|
|
53
|
+
readonly deduped: boolean;
|
|
54
|
+
};
|
|
55
|
+
confirmReplica(input: {
|
|
56
|
+
readonly batchId: string;
|
|
57
|
+
readonly replicaId: string;
|
|
58
|
+
readonly at: number;
|
|
59
|
+
}): {
|
|
60
|
+
readonly deduped: boolean;
|
|
61
|
+
};
|
|
62
|
+
complete(input: {
|
|
63
|
+
readonly batchId: string;
|
|
64
|
+
readonly at: number;
|
|
65
|
+
}): {
|
|
66
|
+
readonly deduped: boolean;
|
|
67
|
+
};
|
|
68
|
+
batchState(batchId: string): MemoryPurgeJournalBatchStateV1 | undefined;
|
|
69
|
+
/** Only ids of COMPLETED batches — the safe purge set for index rebuilds. */
|
|
70
|
+
purgedMemoryIds(): readonly string[];
|
|
71
|
+
records(): readonly MemoryPurgeJournalRecordV1[];
|
|
72
|
+
serialize(): string;
|
|
73
|
+
load(serialized: string): void;
|
|
74
|
+
}
|
|
75
|
+
export declare function createMemoryPurgeJournal(): MemoryPurgeJournalV1;
|
|
76
|
+
//# sourceMappingURL=purge-journal.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"purge-journal.d.ts","sourceRoot":"","sources":["../../src/memory/purge-journal.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAElD,MAAM,MAAM,0BAA0B,GACnC;IACA,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAC;IAC5B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,SAAS,EAAE,SAAS,MAAM,EAAE,CAAC;IACtC,QAAQ,CAAC,QAAQ,EAAE,SAAS,eAAe,EAAE,CAAC;IAC9C,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;CACnB,GACD;IACA,QAAQ,CAAC,IAAI,EAAE,mBAAmB,CAAC;IACnC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;CACnB,GACD;IACA,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC;IAC3B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;CACnB,CAAC;AAEL,MAAM,WAAW,8BAA8B;IAC9C,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,KAAK,EAAE,YAAY,GAAG,WAAW,CAAC;IAC3C,QAAQ,CAAC,SAAS,EAAE,SAAS,MAAM,EAAE,CAAC;IACtC,QAAQ,CAAC,QAAQ,EAAE,SAAS,eAAe,EAAE,CAAC;IAC9C,QAAQ,CAAC,iBAAiB,EAAE,SAAS,MAAM,EAAE,CAAC;IAC9C,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,oBAAoB;IACpC,SAAS,CAAC,MAAM,EAAE;QACjB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;QACzB,QAAQ,CAAC,SAAS,EAAE,SAAS,MAAM,EAAE,CAAC;QACtC,QAAQ,CAAC,QAAQ,EAAE,SAAS,eAAe,EAAE,CAAC;QAC9C,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;QAC9B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;QACxB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;KACpB,GAAG;QAAE,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC;IAClC,cAAc,CAAC,KAAK,EAAE;QAAE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;KAAE,GAAG;QACrG,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;KAC1B,CAAC;IACF,QAAQ,CAAC,KAAK,EAAE;QAAE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;KAAE,GAAG;QAAE,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC;IAClG,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,8BAA8B,GAAG,SAAS,CAAC;IACxE,+EAA6E;IAC7E,eAAe,IAAI,SAAS,MAAM,EAAE,CAAC;IACrC,OAAO,IAAI,SAAS,0BAA0B,EAAE,CAAC;IACjD,SAAS,IAAI,MAAM,CAAC;IACpB,IAAI,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CAC/B;AA6BD,wBAAgB,wBAAwB,IAAI,oBAAoB,CA4G/D","sourcesContent":["/**\n * Memory Foundation — purge journal, recovery, and idempotency fault matrix\n * (1C.6d).\n *\n * The journal is the durable record of every purge batch: authorization,\n * per-replica confirmations, and completion. Recovery rules (frozen per\n * 记忆系统设计.md §5.2/§3.3):\n * - A batch without a `completed` record is NOT complete — after a crash it\n * stays resumable and its memory ids are not treated as purged.\n * - Replica confirmations are idempotent per (batchId, replicaId): replays and\n * retries never double-count.\n * - `purgedMemoryIds()` only ever lists ids from completed batches, so an\n * index rebuild can never resurrect purged data or report un-purged data as\n * purged.\n */\nimport type { MemoryReplicaV1 } from \"./purge.ts\";\n\nexport type MemoryPurgeJournalRecordV1 =\n\t| {\n\t\t\treadonly kind: \"authorized\";\n\t\t\treadonly batchId: string;\n\t\t\treadonly memoryIds: readonly string[];\n\t\t\treadonly replicas: readonly MemoryReplicaV1[];\n\t\t\treadonly authorizedBy: string;\n\t\t\treadonly reason: string;\n\t\t\treadonly at: number;\n\t }\n\t| {\n\t\t\treadonly kind: \"replica_confirmed\";\n\t\t\treadonly batchId: string;\n\t\t\treadonly replicaId: string;\n\t\t\treadonly at: number;\n\t }\n\t| {\n\t\t\treadonly kind: \"completed\";\n\t\t\treadonly batchId: string;\n\t\t\treadonly at: number;\n\t };\n\nexport interface MemoryPurgeJournalBatchStateV1 {\n\treadonly batchId: string;\n\treadonly state: \"authorized\" | \"completed\";\n\treadonly memoryIds: readonly string[];\n\treadonly replicas: readonly MemoryReplicaV1[];\n\treadonly confirmedReplicas: readonly string[];\n\treadonly authorizedBy: string;\n\treadonly reason: string;\n}\n\nexport interface MemoryPurgeJournalV1 {\n\tauthorize(record: {\n\t\treadonly batchId: string;\n\t\treadonly memoryIds: readonly string[];\n\t\treadonly replicas: readonly MemoryReplicaV1[];\n\t\treadonly authorizedBy: string;\n\t\treadonly reason: string;\n\t\treadonly at: number;\n\t}): { readonly deduped: boolean };\n\tconfirmReplica(input: { readonly batchId: string; readonly replicaId: string; readonly at: number }): {\n\t\treadonly deduped: boolean;\n\t};\n\tcomplete(input: { readonly batchId: string; readonly at: number }): { readonly deduped: boolean };\n\tbatchState(batchId: string): MemoryPurgeJournalBatchStateV1 | undefined;\n\t/** Only ids of COMPLETED batches — the safe purge set for index rebuilds. */\n\tpurgedMemoryIds(): readonly string[];\n\trecords(): readonly MemoryPurgeJournalRecordV1[];\n\tserialize(): string;\n\tload(serialized: string): void;\n}\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\nfunction parseRecords(serialized: string): MemoryPurgeJournalRecordV1[] {\n\tconst records: MemoryPurgeJournalRecordV1[] = [];\n\tfor (const line of serialized.split(\"\\n\")) {\n\t\tif (line.trim() === \"\") continue;\n\t\ttry {\n\t\t\trecords.push(JSON.parse(line) as MemoryPurgeJournalRecordV1);\n\t\t} catch {\n\t\t\t// A crash can truncate the final line mid-write; the trailing partial\n\t\t\t// record is ignored while anything malformed in the middle fails.\n\t\t\tif (\n\t\t\t\tline ===\n\t\t\t\tserialized\n\t\t\t\t\t.split(\"\\n\")\n\t\t\t\t\t.filter((l) => l.trim() !== \"\")\n\t\t\t\t\t.at(-1)\n\t\t\t)\n\t\t\t\tbreak;\n\t\t\tthrow new Error(\"Invalid purge journal record\");\n\t\t}\n\t}\n\treturn records;\n}\n\nexport function createMemoryPurgeJournal(): MemoryPurgeJournalV1 {\n\tconst log: MemoryPurgeJournalRecordV1[] = [];\n\tconst batches = new Map<\n\t\tstring,\n\t\t{\n\t\t\tmemoryIds: string[];\n\t\t\treplicas: MemoryReplicaV1[];\n\t\t\tauthorizedBy: string;\n\t\t\treason: string;\n\t\t\tconfirmedReplicas: string[];\n\t\t\tconfirmations: Set<string>;\n\t\t\tcompletedAt?: number;\n\t\t}\n\t>();\n\n\tconst applyRecord = (record: MemoryPurgeJournalRecordV1): { deduped: boolean } => {\n\t\tif (record.kind === \"authorized\") {\n\t\t\tif (batches.has(record.batchId)) return { deduped: true };\n\t\t\tassertNonEmptyString(record.batchId, \"Purge journal batchId\");\n\t\t\tassertNonEmptyString(record.authorizedBy, \"Purge journal authorizedBy\");\n\t\t\tassertNonEmptyString(record.reason, \"Purge journal reason\");\n\t\t\tif (!Array.isArray(record.memoryIds) || record.memoryIds.length === 0) {\n\t\t\t\tthrow new Error(\"Purge journal authorize requires at least one memoryId\");\n\t\t\t}\n\t\t\tbatches.set(record.batchId, {\n\t\t\t\tmemoryIds: [...record.memoryIds],\n\t\t\t\treplicas: [...record.replicas],\n\t\t\t\tauthorizedBy: record.authorizedBy,\n\t\t\t\treason: record.reason,\n\t\t\t\tconfirmedReplicas: [],\n\t\t\t\tconfirmations: new Set(),\n\t\t\t});\n\t\t\treturn { deduped: false };\n\t\t}\n\t\tif (record.kind === \"replica_confirmed\") {\n\t\t\tconst batch = batches.get(record.batchId);\n\t\t\tif (!batch) throw new Error(`Unknown purge batch: ${record.batchId}`);\n\t\t\tconst confirmationKey = `${record.replicaId}`;\n\t\t\tif (batch.confirmations.has(confirmationKey)) return { deduped: true };\n\t\t\tbatch.confirmations.add(confirmationKey);\n\t\t\tbatch.confirmedReplicas.push(record.replicaId);\n\t\t\treturn { deduped: false };\n\t\t}\n\t\tif (record.kind === \"completed\") {\n\t\t\tconst batch = batches.get(record.batchId);\n\t\t\tif (!batch) throw new Error(`Unknown purge batch: ${record.batchId}`);\n\t\t\tif (batch.completedAt !== undefined) return { deduped: true };\n\t\t\tbatch.completedAt = record.at;\n\t\t\treturn { deduped: false };\n\t\t}\n\t\tthrow new Error(\"Invalid purge journal record\");\n\t};\n\n\treturn {\n\t\tauthorize(record) {\n\t\t\tconst result = applyRecord({ ...record, kind: \"authorized\" });\n\t\t\tlog.push({ ...record, kind: \"authorized\" });\n\t\t\treturn result;\n\t\t},\n\t\tconfirmReplica(input) {\n\t\t\tconst result = applyRecord({ ...input, kind: \"replica_confirmed\" });\n\t\t\tlog.push({ ...input, kind: \"replica_confirmed\" });\n\t\t\treturn result;\n\t\t},\n\t\tcomplete(input) {\n\t\t\tconst result = applyRecord({ ...input, kind: \"completed\" });\n\t\t\tlog.push({ ...input, kind: \"completed\" });\n\t\t\treturn result;\n\t\t},\n\t\tbatchState(batchId) {\n\t\t\tconst batch = batches.get(batchId);\n\t\t\tif (!batch) return undefined;\n\t\t\treturn {\n\t\t\t\tbatchId,\n\t\t\t\tstate: batch.completedAt !== undefined ? \"completed\" : \"authorized\",\n\t\t\t\tmemoryIds: [...batch.memoryIds],\n\t\t\t\treplicas: [...batch.replicas],\n\t\t\t\tconfirmedReplicas: [...batch.confirmedReplicas],\n\t\t\t\tauthorizedBy: batch.authorizedBy,\n\t\t\t\treason: batch.reason,\n\t\t\t};\n\t\t},\n\t\tpurgedMemoryIds() {\n\t\t\tconst purged: string[] = [];\n\t\t\tfor (const [, batch] of batches) {\n\t\t\t\t// Only completed batches contribute ids; an unfinished batch must\n\t\t\t\t// never be reported as purged.\n\t\t\t\tif (batch.completedAt === undefined) continue;\n\t\t\t\tpurged.push(...batch.memoryIds);\n\t\t\t}\n\t\t\treturn Object.freeze([...new Set(purged)]);\n\t\t},\n\t\trecords() {\n\t\t\treturn Object.freeze([...log]);\n\t\t},\n\t\tserialize() {\n\t\t\treturn log.map((record) => JSON.stringify(record)).join(\"\\n\");\n\t\t},\n\t\tload(serialized) {\n\t\t\tlog.length = 0;\n\t\t\tbatches.clear();\n\t\t\tconst records = parseRecords(serialized);\n\t\t\tfor (const record of records) {\n\t\t\t\tapplyRecord(record);\n\t\t\t\tlog.push(record);\n\t\t\t}\n\t\t},\n\t};\n}\n"]}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
function assertNonEmptyString(value, label) {
|
|
2
|
+
if (typeof value !== "string" || value.trim().length === 0)
|
|
3
|
+
throw new Error(`${label} must be a non-empty string`);
|
|
4
|
+
}
|
|
5
|
+
function parseRecords(serialized) {
|
|
6
|
+
const records = [];
|
|
7
|
+
for (const line of serialized.split("\n")) {
|
|
8
|
+
if (line.trim() === "")
|
|
9
|
+
continue;
|
|
10
|
+
try {
|
|
11
|
+
records.push(JSON.parse(line));
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
// A crash can truncate the final line mid-write; the trailing partial
|
|
15
|
+
// record is ignored while anything malformed in the middle fails.
|
|
16
|
+
if (line ===
|
|
17
|
+
serialized
|
|
18
|
+
.split("\n")
|
|
19
|
+
.filter((l) => l.trim() !== "")
|
|
20
|
+
.at(-1))
|
|
21
|
+
break;
|
|
22
|
+
throw new Error("Invalid purge journal record");
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return records;
|
|
26
|
+
}
|
|
27
|
+
export function createMemoryPurgeJournal() {
|
|
28
|
+
const log = [];
|
|
29
|
+
const batches = new Map();
|
|
30
|
+
const applyRecord = (record) => {
|
|
31
|
+
if (record.kind === "authorized") {
|
|
32
|
+
if (batches.has(record.batchId))
|
|
33
|
+
return { deduped: true };
|
|
34
|
+
assertNonEmptyString(record.batchId, "Purge journal batchId");
|
|
35
|
+
assertNonEmptyString(record.authorizedBy, "Purge journal authorizedBy");
|
|
36
|
+
assertNonEmptyString(record.reason, "Purge journal reason");
|
|
37
|
+
if (!Array.isArray(record.memoryIds) || record.memoryIds.length === 0) {
|
|
38
|
+
throw new Error("Purge journal authorize requires at least one memoryId");
|
|
39
|
+
}
|
|
40
|
+
batches.set(record.batchId, {
|
|
41
|
+
memoryIds: [...record.memoryIds],
|
|
42
|
+
replicas: [...record.replicas],
|
|
43
|
+
authorizedBy: record.authorizedBy,
|
|
44
|
+
reason: record.reason,
|
|
45
|
+
confirmedReplicas: [],
|
|
46
|
+
confirmations: new Set(),
|
|
47
|
+
});
|
|
48
|
+
return { deduped: false };
|
|
49
|
+
}
|
|
50
|
+
if (record.kind === "replica_confirmed") {
|
|
51
|
+
const batch = batches.get(record.batchId);
|
|
52
|
+
if (!batch)
|
|
53
|
+
throw new Error(`Unknown purge batch: ${record.batchId}`);
|
|
54
|
+
const confirmationKey = `${record.replicaId}`;
|
|
55
|
+
if (batch.confirmations.has(confirmationKey))
|
|
56
|
+
return { deduped: true };
|
|
57
|
+
batch.confirmations.add(confirmationKey);
|
|
58
|
+
batch.confirmedReplicas.push(record.replicaId);
|
|
59
|
+
return { deduped: false };
|
|
60
|
+
}
|
|
61
|
+
if (record.kind === "completed") {
|
|
62
|
+
const batch = batches.get(record.batchId);
|
|
63
|
+
if (!batch)
|
|
64
|
+
throw new Error(`Unknown purge batch: ${record.batchId}`);
|
|
65
|
+
if (batch.completedAt !== undefined)
|
|
66
|
+
return { deduped: true };
|
|
67
|
+
batch.completedAt = record.at;
|
|
68
|
+
return { deduped: false };
|
|
69
|
+
}
|
|
70
|
+
throw new Error("Invalid purge journal record");
|
|
71
|
+
};
|
|
72
|
+
return {
|
|
73
|
+
authorize(record) {
|
|
74
|
+
const result = applyRecord({ ...record, kind: "authorized" });
|
|
75
|
+
log.push({ ...record, kind: "authorized" });
|
|
76
|
+
return result;
|
|
77
|
+
},
|
|
78
|
+
confirmReplica(input) {
|
|
79
|
+
const result = applyRecord({ ...input, kind: "replica_confirmed" });
|
|
80
|
+
log.push({ ...input, kind: "replica_confirmed" });
|
|
81
|
+
return result;
|
|
82
|
+
},
|
|
83
|
+
complete(input) {
|
|
84
|
+
const result = applyRecord({ ...input, kind: "completed" });
|
|
85
|
+
log.push({ ...input, kind: "completed" });
|
|
86
|
+
return result;
|
|
87
|
+
},
|
|
88
|
+
batchState(batchId) {
|
|
89
|
+
const batch = batches.get(batchId);
|
|
90
|
+
if (!batch)
|
|
91
|
+
return undefined;
|
|
92
|
+
return {
|
|
93
|
+
batchId,
|
|
94
|
+
state: batch.completedAt !== undefined ? "completed" : "authorized",
|
|
95
|
+
memoryIds: [...batch.memoryIds],
|
|
96
|
+
replicas: [...batch.replicas],
|
|
97
|
+
confirmedReplicas: [...batch.confirmedReplicas],
|
|
98
|
+
authorizedBy: batch.authorizedBy,
|
|
99
|
+
reason: batch.reason,
|
|
100
|
+
};
|
|
101
|
+
},
|
|
102
|
+
purgedMemoryIds() {
|
|
103
|
+
const purged = [];
|
|
104
|
+
for (const [, batch] of batches) {
|
|
105
|
+
// Only completed batches contribute ids; an unfinished batch must
|
|
106
|
+
// never be reported as purged.
|
|
107
|
+
if (batch.completedAt === undefined)
|
|
108
|
+
continue;
|
|
109
|
+
purged.push(...batch.memoryIds);
|
|
110
|
+
}
|
|
111
|
+
return Object.freeze([...new Set(purged)]);
|
|
112
|
+
},
|
|
113
|
+
records() {
|
|
114
|
+
return Object.freeze([...log]);
|
|
115
|
+
},
|
|
116
|
+
serialize() {
|
|
117
|
+
return log.map((record) => JSON.stringify(record)).join("\n");
|
|
118
|
+
},
|
|
119
|
+
load(serialized) {
|
|
120
|
+
log.length = 0;
|
|
121
|
+
batches.clear();
|
|
122
|
+
const records = parseRecords(serialized);
|
|
123
|
+
for (const record of records) {
|
|
124
|
+
applyRecord(record);
|
|
125
|
+
log.push(record);
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
//# sourceMappingURL=purge-journal.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"purge-journal.js","sourceRoot":"","sources":["../../src/memory/purge-journal.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Memory Foundation — purge journal, recovery, and idempotency fault matrix\n * (1C.6d).\n *\n * The journal is the durable record of every purge batch: authorization,\n * per-replica confirmations, and completion. Recovery rules (frozen per\n * 记忆系统设计.md §5.2/§3.3):\n * - A batch without a `completed` record is NOT complete — after a crash it\n * stays resumable and its memory ids are not treated as purged.\n * - Replica confirmations are idempotent per (batchId, replicaId): replays and\n * retries never double-count.\n * - `purgedMemoryIds()` only ever lists ids from completed batches, so an\n * index rebuild can never resurrect purged data or report un-purged data as\n * purged.\n */\nimport type { MemoryReplicaV1 } from \"./purge.ts\";\n\nexport type MemoryPurgeJournalRecordV1 =\n\t| {\n\t\t\treadonly kind: \"authorized\";\n\t\t\treadonly batchId: string;\n\t\t\treadonly memoryIds: readonly string[];\n\t\t\treadonly replicas: readonly MemoryReplicaV1[];\n\t\t\treadonly authorizedBy: string;\n\t\t\treadonly reason: string;\n\t\t\treadonly at: number;\n\t }\n\t| {\n\t\t\treadonly kind: \"replica_confirmed\";\n\t\t\treadonly batchId: string;\n\t\t\treadonly replicaId: string;\n\t\t\treadonly at: number;\n\t }\n\t| {\n\t\t\treadonly kind: \"completed\";\n\t\t\treadonly batchId: string;\n\t\t\treadonly at: number;\n\t };\n\nexport interface MemoryPurgeJournalBatchStateV1 {\n\treadonly batchId: string;\n\treadonly state: \"authorized\" | \"completed\";\n\treadonly memoryIds: readonly string[];\n\treadonly replicas: readonly MemoryReplicaV1[];\n\treadonly confirmedReplicas: readonly string[];\n\treadonly authorizedBy: string;\n\treadonly reason: string;\n}\n\nexport interface MemoryPurgeJournalV1 {\n\tauthorize(record: {\n\t\treadonly batchId: string;\n\t\treadonly memoryIds: readonly string[];\n\t\treadonly replicas: readonly MemoryReplicaV1[];\n\t\treadonly authorizedBy: string;\n\t\treadonly reason: string;\n\t\treadonly at: number;\n\t}): { readonly deduped: boolean };\n\tconfirmReplica(input: { readonly batchId: string; readonly replicaId: string; readonly at: number }): {\n\t\treadonly deduped: boolean;\n\t};\n\tcomplete(input: { readonly batchId: string; readonly at: number }): { readonly deduped: boolean };\n\tbatchState(batchId: string): MemoryPurgeJournalBatchStateV1 | undefined;\n\t/** Only ids of COMPLETED batches — the safe purge set for index rebuilds. */\n\tpurgedMemoryIds(): readonly string[];\n\trecords(): readonly MemoryPurgeJournalRecordV1[];\n\tserialize(): string;\n\tload(serialized: string): void;\n}\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\nfunction parseRecords(serialized: string): MemoryPurgeJournalRecordV1[] {\n\tconst records: MemoryPurgeJournalRecordV1[] = [];\n\tfor (const line of serialized.split(\"\\n\")) {\n\t\tif (line.trim() === \"\") continue;\n\t\ttry {\n\t\t\trecords.push(JSON.parse(line) as MemoryPurgeJournalRecordV1);\n\t\t} catch {\n\t\t\t// A crash can truncate the final line mid-write; the trailing partial\n\t\t\t// record is ignored while anything malformed in the middle fails.\n\t\t\tif (\n\t\t\t\tline ===\n\t\t\t\tserialized\n\t\t\t\t\t.split(\"\\n\")\n\t\t\t\t\t.filter((l) => l.trim() !== \"\")\n\t\t\t\t\t.at(-1)\n\t\t\t)\n\t\t\t\tbreak;\n\t\t\tthrow new Error(\"Invalid purge journal record\");\n\t\t}\n\t}\n\treturn records;\n}\n\nexport function createMemoryPurgeJournal(): MemoryPurgeJournalV1 {\n\tconst log: MemoryPurgeJournalRecordV1[] = [];\n\tconst batches = new Map<\n\t\tstring,\n\t\t{\n\t\t\tmemoryIds: string[];\n\t\t\treplicas: MemoryReplicaV1[];\n\t\t\tauthorizedBy: string;\n\t\t\treason: string;\n\t\t\tconfirmedReplicas: string[];\n\t\t\tconfirmations: Set<string>;\n\t\t\tcompletedAt?: number;\n\t\t}\n\t>();\n\n\tconst applyRecord = (record: MemoryPurgeJournalRecordV1): { deduped: boolean } => {\n\t\tif (record.kind === \"authorized\") {\n\t\t\tif (batches.has(record.batchId)) return { deduped: true };\n\t\t\tassertNonEmptyString(record.batchId, \"Purge journal batchId\");\n\t\t\tassertNonEmptyString(record.authorizedBy, \"Purge journal authorizedBy\");\n\t\t\tassertNonEmptyString(record.reason, \"Purge journal reason\");\n\t\t\tif (!Array.isArray(record.memoryIds) || record.memoryIds.length === 0) {\n\t\t\t\tthrow new Error(\"Purge journal authorize requires at least one memoryId\");\n\t\t\t}\n\t\t\tbatches.set(record.batchId, {\n\t\t\t\tmemoryIds: [...record.memoryIds],\n\t\t\t\treplicas: [...record.replicas],\n\t\t\t\tauthorizedBy: record.authorizedBy,\n\t\t\t\treason: record.reason,\n\t\t\t\tconfirmedReplicas: [],\n\t\t\t\tconfirmations: new Set(),\n\t\t\t});\n\t\t\treturn { deduped: false };\n\t\t}\n\t\tif (record.kind === \"replica_confirmed\") {\n\t\t\tconst batch = batches.get(record.batchId);\n\t\t\tif (!batch) throw new Error(`Unknown purge batch: ${record.batchId}`);\n\t\t\tconst confirmationKey = `${record.replicaId}`;\n\t\t\tif (batch.confirmations.has(confirmationKey)) return { deduped: true };\n\t\t\tbatch.confirmations.add(confirmationKey);\n\t\t\tbatch.confirmedReplicas.push(record.replicaId);\n\t\t\treturn { deduped: false };\n\t\t}\n\t\tif (record.kind === \"completed\") {\n\t\t\tconst batch = batches.get(record.batchId);\n\t\t\tif (!batch) throw new Error(`Unknown purge batch: ${record.batchId}`);\n\t\t\tif (batch.completedAt !== undefined) return { deduped: true };\n\t\t\tbatch.completedAt = record.at;\n\t\t\treturn { deduped: false };\n\t\t}\n\t\tthrow new Error(\"Invalid purge journal record\");\n\t};\n\n\treturn {\n\t\tauthorize(record) {\n\t\t\tconst result = applyRecord({ ...record, kind: \"authorized\" });\n\t\t\tlog.push({ ...record, kind: \"authorized\" });\n\t\t\treturn result;\n\t\t},\n\t\tconfirmReplica(input) {\n\t\t\tconst result = applyRecord({ ...input, kind: \"replica_confirmed\" });\n\t\t\tlog.push({ ...input, kind: \"replica_confirmed\" });\n\t\t\treturn result;\n\t\t},\n\t\tcomplete(input) {\n\t\t\tconst result = applyRecord({ ...input, kind: \"completed\" });\n\t\t\tlog.push({ ...input, kind: \"completed\" });\n\t\t\treturn result;\n\t\t},\n\t\tbatchState(batchId) {\n\t\t\tconst batch = batches.get(batchId);\n\t\t\tif (!batch) return undefined;\n\t\t\treturn {\n\t\t\t\tbatchId,\n\t\t\t\tstate: batch.completedAt !== undefined ? \"completed\" : \"authorized\",\n\t\t\t\tmemoryIds: [...batch.memoryIds],\n\t\t\t\treplicas: [...batch.replicas],\n\t\t\t\tconfirmedReplicas: [...batch.confirmedReplicas],\n\t\t\t\tauthorizedBy: batch.authorizedBy,\n\t\t\t\treason: batch.reason,\n\t\t\t};\n\t\t},\n\t\tpurgedMemoryIds() {\n\t\t\tconst purged: string[] = [];\n\t\t\tfor (const [, batch] of batches) {\n\t\t\t\t// Only completed batches contribute ids; an unfinished batch must\n\t\t\t\t// never be reported as purged.\n\t\t\t\tif (batch.completedAt === undefined) continue;\n\t\t\t\tpurged.push(...batch.memoryIds);\n\t\t\t}\n\t\t\treturn Object.freeze([...new Set(purged)]);\n\t\t},\n\t\trecords() {\n\t\t\treturn Object.freeze([...log]);\n\t\t},\n\t\tserialize() {\n\t\t\treturn log.map((record) => JSON.stringify(record)).join(\"\\n\");\n\t\t},\n\t\tload(serialized) {\n\t\t\tlog.length = 0;\n\t\t\tbatches.clear();\n\t\t\tconst records = parseRecords(serialized);\n\t\t\tfor (const record of records) {\n\t\t\t\tapplyRecord(record);\n\t\t\t\tlog.push(record);\n\t\t\t}\n\t\t},\n\t};\n}\n"]}
|