cogmemory 0.0.1-beta.2

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.
@@ -0,0 +1,199 @@
1
+ /**
2
+ * Deterministic text, relevance and reconciliation primitives.
3
+ *
4
+ * The whole decision layer of a memory system, with no model, no database and
5
+ * no dependencies — so the service, an in-process runtime, and a test all make
6
+ * the same calls. The reasoning behind each threshold is written down because
7
+ * the numbers are only defensible with it:
8
+ *
9
+ * - A memory cannot be found by a model being smart, only by words overlapping.
10
+ * - Lexical overlap peaks on identical strings and bottoms out on the
11
+ * paraphrases that actually add information, so the *recall* floor is low and
12
+ * a separate step decides what to do with what came back.
13
+ * - Losing a fact is permanent; an extra duplicate costs one row. Every
14
+ * threshold below leans that way.
15
+ */
16
+
17
+ const STOP_WORDS = new Set([
18
+ "the", "and", "for", "this", "that", "with", "from", "you", "are", "was", "has", "have",
19
+ "what", "which", "when", "were", "will", "your", "our", "its", "not", "but", "all", "any",
20
+ "can", "did", "does", "how", "into", "out", "use", "used", "using", "one", "two", "get",
21
+ "new", "now", "then", "than", "them", "they", "his", "her", "she", "him", "been", "being",
22
+ "there", "here", "also", "just", "like", "make", "made", "need", "want", "about", "after",
23
+ "tell", "know", "give", "show", "please", "would", "could", "should", "shall"
24
+ ])
25
+
26
+ /** Words that appear in every restatement of a fact and so carry no identity. */
27
+ const FILLER_TOKENS = new Set([
28
+ "this", "that", "these", "those", "there", "here", "with", "from", "into", "must",
29
+ "should", "always", "never", "under", "over", "about", "after", "before", "when",
30
+ "where", "which", "what", "your", "their", "them", "they", "then", "than", "also",
31
+ "just", "only", "each", "every", "some", "such", "very", "more", "most", "same",
32
+ "file", "files", "name", "names", "project", "repository", "repo", "note"
33
+ ])
34
+
35
+ /** Content words worth matching a memory against. */
36
+ export const relevanceTokens = (value: string): Set<string> =>
37
+ new Set(
38
+ value
39
+ .toLowerCase()
40
+ .split(/[^a-z0-9]+/)
41
+ .filter((word) => word.length >= 3 && !STOP_WORDS.has(word))
42
+ )
43
+
44
+ /** Overlap of two token sets, normalised by the smaller side. */
45
+ export const overlapScore = (query: Set<string>, candidate: Set<string>): number => {
46
+ if (query.size === 0 || candidate.size === 0) return 0
47
+ let shared = 0
48
+ for (const word of query) if (candidate.has(word)) shared += 1
49
+ return shared / Math.min(query.size, candidate.size)
50
+ }
51
+
52
+ /**
53
+ * Floor for *candidate* recall, not a similarity decision.
54
+ *
55
+ * Deliberately low. This only decides who gets adjudicated; a tight gate misses
56
+ * exactly the paraphrases worth merging.
57
+ */
58
+ export const CANDIDATE_FLOOR = 0.3
59
+
60
+ /** Above this, a memory is worth promoting into the pre-staged index. */
61
+ export const PROMOTE_THRESHOLD = 0.12
62
+
63
+ /** How many existing memories to put in front of an adjudicator. */
64
+ export const MAX_CANDIDATES = 8
65
+
66
+ /** Two results this similar are the same memory, restated. */
67
+ export const COLLAPSE_THRESHOLD = 0.8
68
+
69
+ /**
70
+ * Tokens that carry a fact's identity: identifiers, numbers, codes.
71
+ *
72
+ * Function words and generic nouns are dropped because they recur in every
73
+ * restatement and hide real differences.
74
+ */
75
+ export const distinctiveTokens = (value: string): Set<string> =>
76
+ new Set(
77
+ value
78
+ .toLowerCase()
79
+ .replace(/['']/g, "")
80
+ .split(/[^a-z0-9]+/)
81
+ .filter(Boolean)
82
+ .filter((w) => /\d/.test(w) || w.length >= 4)
83
+ .filter((w) => !FILLER_TOKENS.has(w))
84
+ )
85
+
86
+ /**
87
+ * Whether rewriting `original` as `replacement` would drop information.
88
+ *
89
+ * Distinctive tokens carry a fact's identity, so a replacement missing one has
90
+ * deleted something — usually the qualifier that made the two statements differ
91
+ * at all ("never production", a build id, a port). Trailing plurals are folded
92
+ * so "deploys" merging into "deploy" is not read as a deletion.
93
+ *
94
+ * Biased towards reporting a loss: a false positive costs a duplicate entry,
95
+ * which is recoverable, while a false negative deletes a fact for good.
96
+ */
97
+ export const isLossyRewrite = (original: string, replacement: string): boolean => {
98
+ const fold = (tokens: Set<string>): Set<string> =>
99
+ new Set([...tokens].map((t) => (t.length >= 4 && t.endsWith("s") ? t.slice(0, -1) : t)))
100
+ const after = fold(distinctiveTokens(replacement))
101
+ for (const token of fold(distinctiveTokens(original))) {
102
+ if (!after.has(token)) return true
103
+ }
104
+ return false
105
+ }
106
+
107
+ /**
108
+ * Dotted names that are not prose: hostnames, package names, file names.
109
+ *
110
+ * `internal-hbr-2291.pineapple.example` is precisely the kind of value a user
111
+ * names and a memory holds, and without this the identifier patterns below missed
112
+ * it — the trigger silently never fired for a host, which is one of the cases it
113
+ * exists for. The last label must be alphabetic, which is what keeps version
114
+ * strings and abbreviations out; a short stoplist covers the rest.
115
+ */
116
+ const HOSTNAME = /\b[a-z0-9][a-z0-9-]{1,}(?:\.[a-z0-9-]+)*\.[a-z]{2,}\b/gi
117
+
118
+ /** Dotted tokens that are ordinary writing, not names. */
119
+ const NOT_A_HOSTNAME = new Set([
120
+ "eg", "ie", "etc", "vs", "approx", "inc", "ltd", "corp", "dept", "est", "fig", "no", "al"
121
+ ])
122
+
123
+ /**
124
+ * Identifiers worth matching a memory against: URLs, paths, hostnames,
125
+ * SCREAMING_SNAKE, camelCase, long kebab-case and hex-ish codes.
126
+ *
127
+ * The same idea as Aider's `mentioned_idents`: a user naming a concrete thing
128
+ * is a much stronger signal than the words around it. This is what turns a
129
+ * recall into a full-body injection without asking a model.
130
+ */
131
+ export const extractIdentifiers = (text: string): string[] => {
132
+ const found = new Set<string>()
133
+ for (const match of text.match(/\bhttps?:\/\/[^\s<>()[\]"'`]+/g) ?? []) found.add(match)
134
+ for (const match of text.match(HOSTNAME) ?? []) {
135
+ if (NOT_A_HOSTNAME.has(match.toLowerCase())) continue
136
+ found.add(match)
137
+ }
138
+ for (const match of text.match(/\b(?:\.{0,2}\/)?[\w-]+(?:\/[\w.-]+)+\/?/g) ?? []) {
139
+ if (match.length > 3) found.add(match)
140
+ }
141
+ for (const match of text.match(/\b[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)+\b/g) ?? []) found.add(match)
142
+ for (const match of text.match(/\b[A-Z]{2,}[0-9][A-Z0-9-]*\b/g) ?? []) found.add(match)
143
+ for (const match of text.match(/\b[a-z]+(?:[A-Z][a-z0-9]+){1,}\b/g) ?? []) found.add(match)
144
+ for (const match of text.match(/\b[a-z]+(?:-[a-z0-9]+){2,}\b/g) ?? []) found.add(match)
145
+ for (const match of text.match(/\b[0-9a-f]{6,}\b/gi) ?? []) found.add(match)
146
+ return [...found]
147
+ }
148
+
149
+ /**
150
+ * Instructions about *this conversation* rather than durable facts about the
151
+ * project.
152
+ *
153
+ * "Do not verify the staging build against the repository" and "just remember
154
+ * this" describe how to behave right now, so storing them produces noise that
155
+ * later looks like a project constraint.
156
+ *
157
+ * Explicit patterns rather than a "is this specific enough?" heuristic, because
158
+ * over-filtering would silently lose real memories, which is the worse failure.
159
+ */
160
+ const INTERACTION_SCOPED = [
161
+ /\b(?:do not|don'?t|never|no need to)\s+(?:verify|check|confirm|look\s?up|search|investigate|browse|resolve)\b/i,
162
+ /\bjust\s+(?:remember|note|acknowledge|retain|treat)\b/i,
163
+ /\b(?:held|noted|stored|remembered)\s+(?:in|for)\s+(?:this|the)\s+conversation\b/i,
164
+ /\bnot\s+verified\b/i,
165
+ /\bwithout\s+verifying\b/i,
166
+ /\bfor\s+this\s+(?:conversation|session|turn|reply|response)\s+only\b/i,
167
+ /^(?:ok|okay|noted|got it|sure|thanks)\b[.!]?$/i
168
+ ]
169
+
170
+ export const isInteractionScoped = (content: string): boolean =>
171
+ INTERACTION_SCOPED.some((pattern) => pattern.test(content))
172
+
173
+ /** Similarity of two statements by the tokens that carry identity. */
174
+ export const similarity = (a: string, b: string): number => {
175
+ const left = distinctiveTokens(a)
176
+ const right = distinctiveTokens(b)
177
+ if (left.size === 0 || right.size === 0) return 0
178
+ let shared = 0
179
+ for (const word of left) if (right.has(word)) shared += 1
180
+ return shared / Math.min(left.size, right.size)
181
+ }
182
+
183
+ export const normalise = (value: string): string => value.trim().toLowerCase()
184
+
185
+ /** Approximate token cost of a string. */
186
+ export const estimateTokens = (value: string): number => Math.ceil(value.length / 4)
187
+
188
+ /**
189
+ * The index line: short, scannable, no body.
190
+ *
191
+ * Falling back to the first sentence keeps an index line readable when a memory
192
+ * arrives from the API without a gist.
193
+ */
194
+ export const gistOf = (item: { readonly content: string; readonly gist?: string | undefined }): string => {
195
+ if (item.gist && item.gist.trim().length > 0) return item.gist.trim()
196
+ const first = item.content.split(/(?<=[.!?])\s/)[0] ?? item.content
197
+ const trimmed = first.trim()
198
+ return trimmed.length > 90 ? `${trimmed.slice(0, 90)}…` : trimmed
199
+ }
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Deterministic fact extraction.
3
+ *
4
+ * A model extractor is good at judgement but it is still a model: it can
5
+ * refuse, hedge, or return nothing, and a fact the user plainly stated then
6
+ * never gets learned. Everything stated unambiguously is captured here with
7
+ * plain pattern matching, and the model is layered on top for the subtler cases.
8
+ *
9
+ * Only high-confidence shapes are matched. Being wrong in this direction writes
10
+ * a useless memory; being silent loses a real one, which is the worse failure.
11
+ */
12
+
13
+ const URL = /\bhttps?:\/\/[^\s<>()[\]"'`]+/g
14
+
15
+ /**
16
+ * Positive requirements only.
17
+ *
18
+ * "never X" reads correctly as a requirement. "do not X" / "don't X" must NOT be
19
+ * captured here: naively taking the object of a negation produces nonsense like
20
+ * "The user requires: verify it against the repo" from "do not verify it against
21
+ * the repo", which inverts the meaning.
22
+ */
23
+ const REQUIREMENT =
24
+ /\b(always|never|must|should|make sure to|remember to)\b[\s:]+([^.?!\n]{4,160})/gi
25
+
26
+ /**
27
+ * "<subject> is <value>".
28
+ *
29
+ * The value must NOT stop at the first `.`: that truncates
30
+ * `internal-hbr-2291.pineapple.example` to `internal-hbr-2291` and then poisons
31
+ * recall with the broken value. Stop at a period only when it ends a sentence.
32
+ */
33
+ const ASSIGNMENT = /\b([A-Za-z][\w ()-]{2,60}?)\s+(?:is|are)\s+([^\n?!]{3,200})/g
34
+
35
+ const CODEISH = /[A-Z0-9_./:-]/
36
+
37
+ const looksLikeValue = (value: string): boolean => {
38
+ const trimmed = value.trim()
39
+ if (trimmed.length < 3 || trimmed.length > 200) return false
40
+ if (/^(not|nothing|unclear|unknown|the same|that|this|it)\b/i.test(trimmed)) return false
41
+ return CODEISH.test(trimmed)
42
+ }
43
+
44
+ /** Cut a captured clause back to its first complete sentence. */
45
+ const firstSentence = (value: string): string => {
46
+ const match = /\.(?=\s+[A-Z(])/.exec(value)
47
+ return match ? value.slice(0, match.index) : value
48
+ }
49
+
50
+ const tidy = (value: string): string =>
51
+ firstSentence(value)
52
+ .replace(/\s+/g, " ")
53
+ .replace(/^(?:that|the|a|an|it|this)\s+/i, "")
54
+ .replace(/[,;:\s.。]+$/, "")
55
+ .trim()
56
+
57
+ export interface DeterministicMemory {
58
+ readonly content: string
59
+ readonly domains: Array<string>
60
+ }
61
+
62
+ /**
63
+ * Extract statements the user made that are worth remembering, without a model.
64
+ */
65
+ export const extractDeterministic = (userMessage: string): Array<DeterministicMemory> => {
66
+ const out: Array<DeterministicMemory> = []
67
+ const push = (content: string): void => {
68
+ const cleaned = tidy(content)
69
+ if (cleaned.length < 8 || cleaned.length > 300) return
70
+ if (/^(yes|no|ok|okay|sure|thanks|thank you|got it|noted)\b/i.test(cleaned)) return
71
+ if (out.some((existing) => existing.content.toLowerCase() === cleaned.toLowerCase())) return
72
+ out.push({ content: cleaned, domains: [] })
73
+ }
74
+
75
+ // 1. URLs are unambiguous.
76
+ for (const match of userMessage.match(URL) ?? []) {
77
+ push(`User-provided URL: ${match}`)
78
+ }
79
+
80
+ // 2. Stated requirements, kept in the user's own words.
81
+ // Paraphrasing flipped meaning once: "Never force push" became
82
+ // "The user requires: force push" — the exact opposite instruction.
83
+ for (const match of userMessage.matchAll(REQUIREMENT)) {
84
+ const verb = (match[1] ?? "").toLowerCase()
85
+ const body = tidy(match[2] ?? "")
86
+ if (!body) continue
87
+ push(`User requirement (${verb}): ${body}`)
88
+ }
89
+
90
+ // 3. "X is Y" where Y is identifier-shaped.
91
+ for (const match of userMessage.matchAll(ASSIGNMENT)) {
92
+ const subject = tidy(match[1] ?? "")
93
+ const value = tidy(match[2] ?? "")
94
+ if (!subject || !looksLikeValue(value)) continue
95
+ push(`${subject} is ${value}`)
96
+ }
97
+
98
+ // The three passes overlap: "Always run the canary pipeline for staging" gets
99
+ // caught as a requirement *and* as an assignment, and the assignment copy is
100
+ // truncated mid-value. Two rows for one sentence is noise the engine then has
101
+ // to reconcile later, so the contained one is dropped here instead.
102
+ const deduped = out.filter(
103
+ (candidate, index) =>
104
+ !out.some((other, otherIndex) => {
105
+ if (otherIndex === index) return false
106
+ if (other.content.length <= candidate.content.length) return false
107
+ return isCoveredBy(candidate.content, other.content)
108
+ })
109
+ )
110
+
111
+ return deduped.slice(0, 6)
112
+ }
113
+
114
+ /**
115
+ * Whether everything worth keeping in `narrow` is already in `wide`.
116
+ *
117
+ * Compared on the tokens that carry a fact's identity, so dropping a number or a
118
+ * qualifier counts as loss even when the wording differs.
119
+ */
120
+ const isCoveredBy = (narrow: string, wide: string): boolean => {
121
+ const have = new Set(wide.toLowerCase().split(/[^a-z0-9]+/).filter((word) => word.length >= 4))
122
+ const covered = narrow
123
+ .toLowerCase()
124
+ .split(/[^a-z0-9]+/)
125
+ .filter((word) => word.length >= 4)
126
+ return covered.length > 0 && covered.every((word) => have.has(word))
127
+ }
@@ -0,0 +1,232 @@
1
+ /**
2
+ * Cognitive Memory types and contracts for not-another-harness.
3
+ */
4
+
5
+ export type MemoryTier = "L0" | "L1" | "L2" | "L3";
6
+
7
+ export type TensionStatus = "active" | "latent" | "resolved";
8
+
9
+ export type TensionImpact = "low" | "medium" | "critical";
10
+
11
+ export interface KnowledgeTension {
12
+ id: string;
13
+ status: TensionStatus;
14
+ claimA: {
15
+ source: string;
16
+ statement: string;
17
+ timestamp: number;
18
+ };
19
+ claimB: {
20
+ source: string;
21
+ statement: string;
22
+ timestamp: number;
23
+ };
24
+ impact: TensionImpact;
25
+ taskRelevance: number; // 0.0 - 1.0
26
+ actionableQuestion: string;
27
+ resolution?: {
28
+ resolvedAt: number;
29
+ resolvedBy: string;
30
+ pattern: string;
31
+ };
32
+ }
33
+
34
+ export interface DomainCapability {
35
+ reliabilityScore: number; // 0.0 - 1.0 (historical success rate)
36
+ sampleCount: number;
37
+ knownFailurePatterns: string[];
38
+ recommendedStrategies: string[];
39
+ }
40
+
41
+ export interface ProprioceptiveSelfModel {
42
+ /** Map of domain tag (e.g. "auth", "db-migration", "css") to capability stats */
43
+ domains: Record<string, DomainCapability>;
44
+ /** Overall calibration score (1.0 = well-calibrated, <0.7 = overconfident) */
45
+ calibrationFactor: number;
46
+ /** Active domains detected in current work */
47
+ activeDomains: string[];
48
+ }
49
+
50
+ export interface MemoryMetadata {
51
+ domains: string[];
52
+ isFailurePattern?: boolean;
53
+ isSuccessfulStrategy?: boolean;
54
+ isGuardrail?: boolean;
55
+ sourceSessionId?: string;
56
+ createdAt: number;
57
+ lastAccessedAt: number;
58
+ accessCount: number;
59
+ }
60
+
61
+ export interface MemoryItem {
62
+ id: string;
63
+ content: string;
64
+ /** Dense 1-2 sentence representation for fast arbiter scanning */
65
+ bookmark: string;
66
+ /**
67
+ * Short label shown in the pre-staged index (Claude Code's `MEMORY.md` pattern:
68
+ * the index is always in context, the body is fetched on demand). Falls back to
69
+ * a truncation of `content` when the extractor did not supply one.
70
+ */
71
+ gist?: string;
72
+ /** Dense embedding vector (empty array if embedding disabled) */
73
+ embedding?: number[];
74
+ tier: MemoryTier;
75
+ metadata: MemoryMetadata;
76
+ }
77
+
78
+ /** Why a memory ended up in the prompt — the thing you tune once you log it. */
79
+ /** What to do with a candidate memory once compared against what we hold. */
80
+ export type MemoryReconciliation = {
81
+ /**
82
+ * `add` keeps it as a new memory and is the safe default when unsure: an
83
+ * unmerged duplicate costs a row, while a wrong merge corrupts what we believe.
84
+ * `merge` collapses a restatement into the existing entry, keeping whichever
85
+ * carries more information. `replace` supersedes the existing entry. `reject`
86
+ * drops it as not worth storing.
87
+ */
88
+ action: "add" | "merge" | "replace" | "reject";
89
+ /** Content to store when merging or replacing; defaults to the candidate. */
90
+ content?: string;
91
+ /** Why, so a bad merge is diagnosable rather than mysterious. */
92
+ reason?: string;
93
+ };
94
+
95
+
96
+ export type MemoryInclusionReason =
97
+ /** Shown in the always-present index as a gist only. */
98
+ | "index"
99
+ /** Unseen identifier in the user's message matched this memory: body included. */
100
+ | "trigger"
101
+ /** Active knowledge tension: body included. */
102
+ | "tension"
103
+ /** Proprioceptive guardrail: body included. */
104
+ | "guardrail";
105
+
106
+ export interface MemoryInjectionEntry {
107
+ id: string;
108
+ tier: MemoryTier;
109
+ reason: MemoryInclusionReason;
110
+ /** Index line only. */
111
+ gist: string;
112
+ /** Present only when the body was worth the tokens. */
113
+ body?: string;
114
+ /** Rough token cost of what was included. */
115
+ tokens: number;
116
+ }
117
+
118
+ /** What was put in the prompt this turn, and why. */
119
+ export interface MemoryInjectionReport {
120
+ text: string;
121
+ entries: MemoryInjectionEntry[];
122
+ totalTokens: number;
123
+ /** True when the total budget forced something out. */
124
+ truncated: boolean;
125
+ }
126
+
127
+ export interface TrajectoryPrediction {
128
+ predictedDomains: string[];
129
+ predictedFiles: string[];
130
+ prefetchMemoryIds: string[];
131
+ confidence: number;
132
+ }
133
+
134
+ export interface ArbiterEvaluationResult {
135
+ promotions: Array<{
136
+ memoryId: string;
137
+ targetTier: "L1";
138
+ signalType: "anticipatory" | "tension" | "proprioceptive" | "recency";
139
+ urgency: number; // 0.0 - 1.0
140
+ }>;
141
+ demotions: Array<{
142
+ memoryId: string;
143
+ targetTier: "L2";
144
+ reason: string;
145
+ }>;
146
+ pins: Array<{
147
+ memoryId: string;
148
+ targetTier: "L0";
149
+ reason: string;
150
+ }>;
151
+ detectedTensions: Array<{
152
+ claimA: string;
153
+ claimB: string;
154
+ impact: TensionImpact;
155
+ actionableQuestion: string;
156
+ }>;
157
+ trajectoryPrediction?: TrajectoryPrediction;
158
+ selfModelUpdate?: {
159
+ domain: string;
160
+ success?: boolean;
161
+ failurePatternObserved?: string;
162
+ };
163
+ }
164
+
165
+ export type ArbiterFn = (params: {
166
+ turnText: string;
167
+ assistantReply: string;
168
+ l0Prompt: string;
169
+ l1Summaries: Array<{ id: string; bookmark: string; domains: string[] }>;
170
+ candidates: Array<{ id: string; bookmark: string; domains: string[]; hasTension?: boolean }>;
171
+ }) => Promise<ArbiterEvaluationResult>;
172
+
173
+ export interface CognitiveMemoryOptions {
174
+ /** Maximum token budget for L0 (default: 2000) */
175
+ maxL0Tokens?: number;
176
+ /** Maximum token budget for L1 hot cache (default: 8000) */
177
+ maxL1Tokens?: number;
178
+ /** Ceiling on everything injected into a single prompt (index + bodies). Default 2000. */
179
+ maxTotalTokens?: number;
180
+ /** Custom Arbiter evaluator function (e.g. powered by Jev, Gemini Flash, or local model) */
181
+ arbiter?: ArbiterFn;
182
+ /** Initial self-model */
183
+ initialSelfModel?: Partial<ProprioceptiveSelfModel>;
184
+ /** Auto-extract candidate items from completed turns (default: true) */
185
+ autoExtractMemories?: boolean;
186
+ /**
187
+ * Adjudicate a candidate memory against ones that already exist.
188
+ *
189
+ * Lexical overlap cannot make this call: it peaks on identical strings and
190
+ * bottoms out on the paraphrases that actually add information. Every mature
191
+ * memory system therefore recalls candidates cheaply and then asks a model,
192
+ * explicitly allowing "no match". `remember` is the low-bar candidate
193
+ * recall; `decide` is the precision step.
194
+ */
195
+ reconcile?: (input: {
196
+ /** Every candidate from this turn that resembles something we hold. */
197
+ items: Array<{ candidate: string; remember: string[] }>;
198
+ }) => Promise<MemoryReconciliation[]>;
199
+ /**
200
+ * Model-backed turn extractor. Preferred over the built-in regex, which only
201
+ * recognises "always/never/make sure to/remember to" and therefore never
202
+ * learned ordinary project facts. Return an empty array to learn nothing.
203
+ */
204
+ extract?: (turn: { userMessage: string; assistantResponse: string }) => Promise<{
205
+ memories: Array<{ content: string; domains?: string[] }>;
206
+ tensions?: Array<{
207
+ claimA: string;
208
+ claimB: string;
209
+ impact: "low" | "medium" | "critical";
210
+ actionableQuestion: string;
211
+ }>;
212
+ }>;
213
+ /** Persistence callback to save L2/L3 state */
214
+ onPersist?: (state: CognitiveMemoryStateSnapshot) => Promise<void> | void;
215
+ }
216
+
217
+ export interface CognitiveMemoryStateSnapshot {
218
+ l0: {
219
+ tensions: KnowledgeTension[];
220
+ selfModel: ProprioceptiveSelfModel;
221
+ activeTaskTrace: string;
222
+ };
223
+ l1: MemoryItem[];
224
+ l2: MemoryItem[];
225
+ l3: MemoryItem[];
226
+ stats: {
227
+ totalTurnsProcessed: number;
228
+ predictionsHit: number;
229
+ predictionsTotal: number;
230
+ tensionsDetected: number;
231
+ };
232
+ }
package/src/errors.ts ADDED
@@ -0,0 +1,85 @@
1
+ import type { CognitiveMemoryErrorBody } from "./types"
2
+
3
+ /**
4
+ * One error class for every failure the service can produce.
5
+ *
6
+ * The service already returns a structured envelope — a tag, a message, the
7
+ * scope that was missing, the fields that failed validation — so the SDK's job is
8
+ * to hand that over as a typed object instead of making callers dig through
9
+ * `error.response.data`.
10
+ *
11
+ * The predicate methods exist so callers ask semantic questions. `status === 403`
12
+ * scatters a magic number through every integration and means something slightly
13
+ * different at each one; `isScopeError()` means the same thing everywhere.
14
+ */
15
+ export class CognitiveMemoryError extends Error {
16
+ /**
17
+ * Set as a real value, because a minifier renames the class and that silently
18
+ * changes what monitoring groups on. `error.name` is correct either way — the
19
+ * constructor assigns it — but `SomeError.name` and `error.constructor.name`
20
+ * are how a reporter identifies a class, and a minified `"h"` breaks that.
21
+ */
22
+ static override readonly name = "CognitiveMemoryError"
23
+
24
+ /** The service's own tag: `Unauthorized`, `Forbidden`, `InvalidRequest`, … */
25
+ readonly code: string
26
+ readonly status: number
27
+ /** Present on a 403: the scope the key would have needed. */
28
+ readonly requiredScope?: string
29
+ /** Present on a 400: which field, and why. */
30
+ readonly issues?: string[]
31
+ readonly body?: CognitiveMemoryErrorBody
32
+
33
+ constructor(
34
+ message: string,
35
+ init: {
36
+ status: number
37
+ code: string
38
+ body?: CognitiveMemoryErrorBody
39
+ requiredScope?: string
40
+ issues?: string[]
41
+ }
42
+ ) {
43
+ super(message)
44
+ this.name = "CognitiveMemoryError"
45
+ this.status = init.status
46
+ this.code = init.code
47
+ this.body = init.body
48
+ this.requiredScope = init.requiredScope
49
+ this.issues = init.issues
50
+
51
+ // Keeps the constructor out of the stack, so the first frame is the caller.
52
+ if (Error.captureStackTrace) {
53
+ Error.captureStackTrace(this, CognitiveMemoryError)
54
+ }
55
+ }
56
+
57
+ /** The key is missing, malformed, expired or revoked. */
58
+ isAuthError(): boolean {
59
+ return this.status === 401
60
+ }
61
+
62
+ /** The key is valid but lacks a scope. `requiredScope` says which. */
63
+ isScopeError(): boolean {
64
+ return this.status === 403
65
+ }
66
+
67
+ /** The request body did not match the schema. `issues` says where. */
68
+ isValidationError(): boolean {
69
+ return this.status === 400
70
+ }
71
+
72
+ /** Nothing exists at that id — or it belongs to another organisation. */
73
+ isNotFoundError(): boolean {
74
+ return this.status === 404
75
+ }
76
+
77
+ isRateLimitError(): boolean {
78
+ return this.status === 429
79
+ }
80
+
81
+ /** A dependency of ours failed, not the request. Safe to retry. */
82
+ isServerError(): boolean {
83
+ return this.status >= 500
84
+ }
85
+ }