stratagate-dsh 0.2.61 → 0.2.64

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 @@
1
+ #!/usr/bin/env node
@@ -0,0 +1,102 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/repair-profile.ts
4
+ import { existsSync, lstatSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
5
+ import { randomUUID } from "node:crypto";
6
+ import { dirname, join } from "node:path";
7
+ import { fileURLToPath } from "node:url";
8
+ var PROFILE_PACKAGES = [
9
+ "@deepseek-ai/cordis",
10
+ "@deepseek-ai/cosmokit",
11
+ "@deepseek-ai/dsh-agent",
12
+ "@deepseek-ai/dsh-agent-default-model",
13
+ "@deepseek-ai/dsh-brand",
14
+ "@deepseek-ai/dsh-client-ui-conversation",
15
+ "@deepseek-ai/dsh-code-runtime",
16
+ "@deepseek-ai/dsh-invariants",
17
+ "@deepseek-ai/dsh-llm",
18
+ "@deepseek-ai/dsh-scope",
19
+ "@deepseek-ai/dsh-session",
20
+ "@deepseek-ai/dsh-session-format",
21
+ "@deepseek-ai/dsh-session-format-catalog",
22
+ "@deepseek-ai/dsh-session-format-v0-to-v1",
23
+ "@deepseek-ai/dsh-session-projection",
24
+ "@deepseek-ai/dsh-settings",
25
+ "@deepseek-ai/dsh-system-prompt",
26
+ "@deepseek-ai/dsh-timeout",
27
+ "@deepseek-ai/dsh-tools",
28
+ "@deepseek-ai/dsh-typert-protocol",
29
+ "@deepseek-ai/dsh-user-approval",
30
+ "@deepseek-ai/dsh-util-crypto",
31
+ "@deepseek-ai/dsh-util-values",
32
+ "@deepseek-ai/schemastery"
33
+ ];
34
+ function profileDirectory(start) {
35
+ let directory = start;
36
+ for (; ; ) {
37
+ const manifestPath = join(directory, "package.json");
38
+ if (existsSync(manifestPath)) {
39
+ try {
40
+ const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
41
+ if (manifest.dsh?.profile && typeof manifest.dsh.profile === "object") return directory;
42
+ } catch {
43
+ }
44
+ }
45
+ const parent = dirname(directory);
46
+ if (parent === directory) {
47
+ throw new Error("stratagate-dsh-repair must run inside an installed DSH profile");
48
+ }
49
+ directory = parent;
50
+ }
51
+ }
52
+ function main() {
53
+ const profile = profileDirectory(dirname(fileURLToPath(import.meta.url)));
54
+ const modules = join(profile, "node_modules");
55
+ const candidates = PROFILE_PACKAGES.map((name) => ({ name, source: join(modules, ...name.split("/")) })).filter(({ source }) => {
56
+ try {
57
+ lstatSync(source);
58
+ return true;
59
+ } catch (error) {
60
+ if (error.code === "ENOENT") return false;
61
+ throw error;
62
+ }
63
+ });
64
+ if (candidates.length === 0) {
65
+ process.stdout.write("StrataGate profile runtime is already isolated; no DSH peer residue found.\n");
66
+ return;
67
+ }
68
+ const backup = join(profile, ".stratagate-runtime-backups", `${Date.now()}-${process.pid}-${randomUUID()}`);
69
+ const moved = [];
70
+ try {
71
+ for (const candidate of candidates) {
72
+ const target = join(backup, "node_modules", ...candidate.name.split("/"));
73
+ mkdirSync(dirname(target), { recursive: true });
74
+ renameSync(candidate.source, target);
75
+ moved.push({ ...candidate, target });
76
+ }
77
+ const receipt = {
78
+ schema: "stratagate-dsh-profile-runtime-repair/v1",
79
+ profile,
80
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
81
+ packages: moved.map(({ name, source, target }) => ({ name, source, target })),
82
+ recovery: `While DSH is stopped, move the listed targets back to their source paths if this repair must be reversed.`
83
+ };
84
+ writeFileSync(join(backup, "receipt.json"), `${JSON.stringify(receipt, null, 2)}
85
+ `, { flag: "wx", mode: 384 });
86
+ } catch (error) {
87
+ for (const item of moved.reverse()) {
88
+ if (existsSync(item.target) && !existsSync(item.source)) renameSync(item.target, item.source);
89
+ }
90
+ throw error;
91
+ }
92
+ process.stdout.write(`Quarantined ${moved.length} stale DSH profile package(s) to ${backup}
93
+ `);
94
+ }
95
+ try {
96
+ main();
97
+ } catch (error) {
98
+ process.stderr.write(`${error instanceof Error ? error.stack ?? error.message : String(error)}
99
+ `);
100
+ process.exitCode = 1;
101
+ }
102
+ //# sourceMappingURL=repair-profile.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/repair-profile.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { existsSync, lstatSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs'\nimport { randomUUID } from 'node:crypto'\nimport { basename, dirname, join } from 'node:path'\nimport { fileURLToPath } from 'node:url'\n\nconst PROFILE_PACKAGES = [\n '@deepseek-ai/cordis',\n '@deepseek-ai/cosmokit',\n '@deepseek-ai/dsh-agent',\n '@deepseek-ai/dsh-agent-default-model',\n '@deepseek-ai/dsh-brand',\n '@deepseek-ai/dsh-client-ui-conversation',\n '@deepseek-ai/dsh-code-runtime',\n '@deepseek-ai/dsh-invariants',\n '@deepseek-ai/dsh-llm',\n '@deepseek-ai/dsh-scope',\n '@deepseek-ai/dsh-session',\n '@deepseek-ai/dsh-session-format',\n '@deepseek-ai/dsh-session-format-catalog',\n '@deepseek-ai/dsh-session-format-v0-to-v1',\n '@deepseek-ai/dsh-session-projection',\n '@deepseek-ai/dsh-settings',\n '@deepseek-ai/dsh-system-prompt',\n '@deepseek-ai/dsh-timeout',\n '@deepseek-ai/dsh-tools',\n '@deepseek-ai/dsh-typert-protocol',\n '@deepseek-ai/dsh-user-approval',\n '@deepseek-ai/dsh-util-crypto',\n '@deepseek-ai/dsh-util-values',\n '@deepseek-ai/schemastery',\n] as const\n\nfunction profileDirectory(start: string): string {\n let directory = start\n for (;;) {\n const manifestPath = join(directory, 'package.json')\n if (existsSync(manifestPath)) {\n try {\n const manifest = JSON.parse(readFileSync(manifestPath, 'utf8')) as { dsh?: { profile?: unknown } }\n if (manifest.dsh?.profile && typeof manifest.dsh.profile === 'object') return directory\n } catch {}\n }\n const parent = dirname(directory)\n if (parent === directory) {\n throw new Error('stratagate-dsh-repair must run inside an installed DSH profile')\n }\n directory = parent\n }\n}\n\nfunction main(): void {\n const profile = profileDirectory(dirname(fileURLToPath(import.meta.url)))\n const modules = join(profile, 'node_modules')\n const candidates = PROFILE_PACKAGES\n .map((name) => ({ name, source: join(modules, ...name.split('/')) }))\n .filter(({ source }) => {\n try {\n lstatSync(source)\n return true\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === 'ENOENT') return false\n throw error\n }\n })\n if (candidates.length === 0) {\n process.stdout.write('StrataGate profile runtime is already isolated; no DSH peer residue found.\\n')\n return\n }\n\n const backup = join(profile, '.stratagate-runtime-backups', `${Date.now()}-${process.pid}-${randomUUID()}`)\n const moved: Array<{ name: string; source: string; target: string }> = []\n try {\n for (const candidate of candidates) {\n const target = join(backup, 'node_modules', ...candidate.name.split('/'))\n mkdirSync(dirname(target), { recursive: true })\n renameSync(candidate.source, target)\n moved.push({ ...candidate, target })\n }\n const receipt = {\n schema: 'stratagate-dsh-profile-runtime-repair/v1',\n profile,\n createdAt: new Date().toISOString(),\n packages: moved.map(({ name, source, target }) => ({ name, source, target })),\n recovery: `While DSH is stopped, move the listed targets back to their source paths if this repair must be reversed.`,\n }\n writeFileSync(join(backup, 'receipt.json'), `${JSON.stringify(receipt, null, 2)}\\n`, { flag: 'wx', mode: 0o600 })\n } catch (error) {\n for (const item of moved.reverse()) {\n if (existsSync(item.target) && !existsSync(item.source)) renameSync(item.target, item.source)\n }\n throw error\n }\n process.stdout.write(`Quarantined ${moved.length} stale DSH profile package(s) to ${backup}\\n`)\n}\n\ntry {\n main()\n} catch (error) {\n process.stderr.write(`${error instanceof Error ? error.stack ?? error.message : String(error)}\\n`)\n process.exitCode = 1\n}\n"],"mappings":"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","names":[]}
@@ -1,244 +1,244 @@
1
- # StrataGate architecture
2
-
3
- StrataGate separates source preservation, derived memory, retrieval control, and reinforcement. Combining these responsibilities makes it easy for a summary mistake or a ranking feedback loop to become an apparently certain answer.
4
-
5
- ## System boundaries
6
-
7
- ```mermaid
8
- flowchart TB
9
- subgraph Source["Source layer"]
10
- T["Open conversation tail"]
11
- B["Permanent 12-turn blocks"]
12
- L["L0-L5 views"]
13
- T --> B --> L
14
- end
15
-
16
- subgraph Derived["Derived memory"]
17
- E["Temporal event cards"]
18
- P["Retryable element projection"]
19
- C["Current element cards"]
20
- W["Adoption-based weight state"]
21
- E --> P --> C
22
- E --> W
23
- C --> W
24
- end
25
-
26
- subgraph Retrieval["Retrieval control"]
27
- S["Search"]
28
- A["Five-field assessment"]
29
- X["Expand or change strategy"]
30
- S --> A
31
- A -->|"partial / wrong"| X
32
- X --> A
33
- end
34
-
35
- B --> E
36
- L --> Retrieval
37
- E --> Retrieval
38
- C --> Retrieval
39
- A -->|"sufficient"| U["Answer and usage receipt"]
40
- U --> W
41
- ```
42
-
43
- The data flow from blocks to event cards and from events to element cards is one-way. Derived cards never rewrite their source block or source event.
44
-
45
- ## Conversation blocks
46
-
47
- A completed user/assistant pair is one turn. The default block boundary is 12 completed turns. Messages that have not reached the boundary remain in the open tail and are not condensed or extracted.
48
-
49
- Hosts may attach a `threadId` to each turn. Open tails, Block boundaries, neighboring extraction context, turn ranges, and decay pointers are then isolated by thread. Persisted Blocks remain available as provenance for long-term cards, while host integrations must inject only the active thread's short-term Block context.
50
-
51
- When the boundary is reached:
52
-
53
- 1. One atomic sealing transaction moves the source messages into permanent L5 and writes deterministic L4 and L3.
54
- 2. The sealed Block is marked model-pending. It is provenance, but it is excluded from decay and cannot replace native conversation history.
55
- 3. A background summarizer produces and validates L0-L2 plus a conservative `shouldExtract` decision.
56
- 4. Event extraction completes with either validated Events or an explicit valid empty result.
57
- 5. Only then is the Block marked ready: its pointer starts at L5, it may replace native history, and it decays toward L0 as newer ready Blocks enter the same thread.
58
-
59
- The block weight is:
60
-
61
- ```text
62
- w(age) = exp(-lambda_block * age)
63
-
64
- age = latest ready Block position - pointer anchor Block position
65
- lambda_block = 0.30 by default
66
- ```
67
-
68
- Open-tail turns do not change Block age. The weight selects how many levels to drop from the pointer anchor. Expanding a block to L3 anchors the pointer at L3 and at the latest sealed Block position; it does not silently jump to L5. Hosts may configure `lambda_block`; smaller values decay more slowly, and values above `0.4` are not recommended.
69
-
70
- ## Deterministic L3 policy
71
-
72
- L3 may remove only:
73
-
74
- 1. standalone greetings or acknowledgements;
75
- 2. standalone pure confirmations;
76
- 3. raw tool-call argument payloads, while retaining tool name and a bounded result summary;
77
- 4. exact repeated long pasted text or code after the first occurrence.
78
-
79
- Short repeated natural-language messages are retained. L3 never performs semantic paraphrasing.
80
-
81
- ## Event extraction
82
-
83
- After L0-L2 validates, a candidate Block is extracted independently; a later Block is not required. The extractor receives:
84
-
85
- - target block `N`, including its L5 source and legal evidence IDs;
86
- - previous block `N-1` L2 keypoints for context, if it exists;
87
- - the nearest available later ready Block's L2 keypoints for context, if one exists;
88
- - a compact timeline of existing event IDs, titles, and temporal fields.
89
-
90
- The target is the only legal source of new facts and quotations. Neighbor blocks are context-only and must not contribute events or source references. Source message IDs are checked against the target block. The reference implementation falls back to all target messages when an extractor returns no valid source ID; stricter adapters may reject the card instead.
91
-
92
- The core callback retains full `MemoryBlock` objects for compatibility. Bundled model adapters project that callback into the target-first payload above, exposing only L2 keypoints for neighboring blocks.
93
-
94
- ## Event-card contract
95
-
96
- An event card stores content, provenance, time, governance, and weight separately.
97
-
98
- ```ts
99
- interface EventCard {
100
- id: string;
101
- title: string;
102
- summary: string;
103
- narrative: string;
104
- tags: string[];
105
- quotes: string[];
106
-
107
- sourceBlockId: string;
108
- sourceMessageIds: string[];
109
-
110
- temporal: {
111
- mentionedAt?: string;
112
- happenedStart?: string;
113
- happenedEnd?: string;
114
- originalText?: string;
115
- precision?: 'instant' | 'day' | 'month' | 'year' | 'range' | 'unknown';
116
- basis?: 'explicit' | 'relative' | 'inferred' | 'unknown';
117
- status?: 'occurred' | 'planned' | 'cancelled' | 'ongoing' | 'unknown';
118
- participants?: string[];
119
- eventType?: string;
120
- supersedesEventIds?: string[];
121
- conflictsWithEventIds?: string[];
122
- };
123
-
124
- status: 'active' | 'superseded' | 'forgotten' | 'archived';
125
- weight: MemoryWeight;
126
- }
127
- ```
128
-
129
- `mentionedAt` answers when the conversation referred to the event. `happenedStart` and `happenedEnd` answer when the event itself occurred. Keeping these axes separate avoids treating the message timestamp as the event date.
130
-
131
- ## Element-card projection
132
-
133
- Event cards are immutable history. Element cards are rebuildable materialized views across events for people, projects, organizations, tools, and places. Each element fact has one of three modes:
134
-
135
- - `state`: a new fact with the same key supersedes the previous active state;
136
- - `set`: new unique values are appended without replacing existing values;
137
- - `relation`: a new relation with the same key supersedes the previous active relation.
138
-
139
- Facts carry `validFrom`, optional `validTo`, confidence, and `sourceEventIds`. Replacing a state closes the previous fact's validity interval instead of deleting it. `expandElement(id, at)` can therefore reconstruct the view at an earlier time.
140
-
141
- Projection is a separate persisted job from event extraction. The runtime commits a `pending` job only after its events exist. It then claims the job, calls the application-provided projector outside the transaction, and atomically applies the result or records failure. Every proposed fact is ignored unless all of its source event IDs belong to the claimed batch. An interrupted `running` job becomes `failed` on restart and can be retried without re-extracting events.
142
-
143
- Applications that manage their own model loop may use `claimNextElementProjection()`, `completeElementProjection()`, and `failElementProjection()` directly. Supplying `elementProjector` lets `appendTurn()` and `resumePendingWork()` drive the same state machine automatically.
144
-
145
- ## Hybrid retrieval
146
-
147
- Event and fact-level element search use two inspectable ranking sources:
148
-
149
- 1. BM25 over field-weighted lexical tokens, including overlapping Han bigrams;
150
- 2. structured rankings from fields such as participant, event type, time range, element name, and element type.
151
-
152
- Reciprocal-rank fusion combines the available rankings. A non-empty query with no lexical or structured match returns an empty result rather than all candidates. Element search returns the matched fact plus its element ID, validity interval, and event provenance; callers expand the full element card only when needed. The reference path does not use embeddings or vector similarity.
153
-
154
- Integration tool responses intentionally expose compact search cards. They retain stable IDs, summaries,
155
- timestamps, and evidence references while leaving narrative/quotes/source-message lists and full graph
156
- facts/edges to the expand tools. `rankScore` is a BM25/RRF ordering metric only, not confidence or
157
- factual accuracy. Graph relation-only hits are filtered as likely adjacency noise; name, alias, tag,
158
- state, fact, type, and other descriptive matches remain eligible across all supported node types.
159
-
160
- ## Event weight and adoption
161
-
162
- Event decay uses:
163
-
164
- ```text
165
- w(t, n) = max(floor, exp(-lambda(n) * t))
166
- lambda(n) = 0.15 / (1 + 1.5 * ln(n))
167
- ```
168
-
169
- `n` is the number of recorded adoptions, not retrieval hits. Search updates `lastRetrievedAt` for observability, while `recordMemoryUse()` increments the adoption count and moves the decay anchor.
170
-
171
- Criticality floors in the reference implementation are:
172
-
173
- | Criticality | Floor |
174
- | --- | ---: |
175
- | routine | 0.0 |
176
- | preference | 0.3 |
177
- | identity | 0.9 |
178
- | safety | 1.0 |
179
-
180
- A pinned event has effective weight 1. A superseded event is capped at 0.1. Forgotten and archived events have effective weight 0.
181
-
182
- ## Retrieval assessment contract
183
-
184
- The assessment contract is deliberately small:
185
-
186
- ```ts
187
- interface RetrievalAssessment {
188
- verdict: 'sufficient' | 'partial' | 'wrong';
189
- evidenceRefs: string[];
190
- rejectedEvidenceRefs: Array<{
191
- inputIndex: number;
192
- ref: string;
193
- reason: 'invalid_ref' | 'duplicate' | 'not_in_batch' | 'limit_exceeded';
194
- detail: string;
195
- }>;
196
- fit: string;
197
- missing: string;
198
- nextStrategy:
199
- | 'answer'
200
- | 'search_events'
201
- | 'expand_event'
202
- | 'search_elements'
203
- | 'expand_element'
204
- | 'search_raw_memory'
205
- | 'expand_block';
206
- }
207
- ```
208
-
209
- Normalization enforces three conditions before `sufficient` is accepted:
210
-
211
- 1. at least one evidence ID belongs to the selected retrieval batch;
212
- 2. the chosen next strategy is `answer`;
213
- 3. the assessment uses the bounded schema rather than carrying a growing private scratchpad.
214
-
215
- If the retrieval budget ends without sufficient evidence, the caller should pass the full retrieval transcript to the answer model and require explicit uncertainty. The core exposes the gate; applications own the tool loop and final model call.
216
-
217
- ## Storage adapters
218
-
219
- `StrataGate.open({ database, namespace })` is the normal public entrypoint and always hydrates the state machine from transactional SQLite storage. `StrataGate.inMemory()` is an explicit test and ephemeral-use mode. Advanced integrations may supply another durable `StorageAdapter` through `StrataGate.openWithStorage()`. The bundled `SqliteStorage` adapter persists normalized rows for memory spaces, messages, blocks, events, elements, facts, provenance links, extraction/projection jobs, usage receipts, and idempotent external-turn ingestion receipts.
220
-
221
- Every namespace has a monotonically increasing revision. A write supplies the revision it loaded; SQLite commits the new revision and all related rows in one immediate transaction. A stale process receives `StorageConflictError` rather than overwriting newer state.
222
-
223
- External model calls are never made inside a database transaction:
224
-
225
- 1. a completed raw turn is committed immediately;
226
- 2. every complete Block is sealed atomically with real L3-L5 before any model call;
227
- 3. summarization first claims a persisted job, runs outside the transaction, and commits validated L0-L2 or a failed job with bounded retry metadata;
228
- 4. extraction first commits a running job, calls the extractor, then atomically commits either the event cards, a valid empty result, or a failed job state;
229
- 5. element projection follows the same claim/call/complete boundary after its source events are durable;
230
- 6. failed model jobs retry at most three total attempts with exponential backoff; completed empty extraction is terminal and is not retried.
231
-
232
- The adapter preserves these invariants:
233
-
234
- - blocks and L5 messages are append-only, including when every derived task fails;
235
- - model-pending Blocks are excluded from decay and native-history replacement;
236
- - card provenance references an existing source block and message set;
237
- - search hits do not increment adoption state;
238
- - supersession retains the old event;
239
- - element state replacement retains the old fact and its validity interval;
240
- - every element and fact source references an existing immutable event;
241
- - forget is reversible unless an application explicitly implements irreversible deletion;
242
- - usage receipts are idempotent for one answer turn through a unique `receiptId`.
243
-
244
- SQLite schema v10 includes durable external-memory import jobs and per-candidate progress, in addition to the normalized Block processing state, summary/extraction retry jobs, graph, element, provenance, receipt, decay-anchor, and lift-source data introduced earlier. Opening a schema-v1 through v9 database migrates it in one transaction and preserves existing namespaces, Blocks, Events, jobs, and receipts. Existing pre-v9 Blocks are treated as ready because their persisted L0-L5 layers were already accepted by the older engine. Schema-v5 turn anchors are converted to per-thread Block positions; schema-v6 lift timestamps retain an unknown legacy source. Pre-v5 Blocks retain no inferred thread ownership, so they remain archival provenance without being attached to a new session. SQLite uses WAL, foreign keys, and per-namespace optimistic concurrency. It does not provide encryption at rest. Search still uses the reference in-memory ranking after hydration, so enabling persistence does not silently change retrieval semantics. Database-native lexical/vector indexes and a Postgres implementation remain separate future work.
1
+ # StrataGate architecture
2
+
3
+ StrataGate separates source preservation, derived memory, retrieval control, and reinforcement. Combining these responsibilities makes it easy for a summary mistake or a ranking feedback loop to become an apparently certain answer.
4
+
5
+ ## System boundaries
6
+
7
+ ```mermaid
8
+ flowchart TB
9
+ subgraph Source["Source layer"]
10
+ T["Open conversation tail"]
11
+ B["Permanent 12-turn blocks"]
12
+ L["L0-L5 views"]
13
+ T --> B --> L
14
+ end
15
+
16
+ subgraph Derived["Derived memory"]
17
+ E["Temporal event cards"]
18
+ P["Retryable element projection"]
19
+ C["Current element cards"]
20
+ W["Adoption-based weight state"]
21
+ E --> P --> C
22
+ E --> W
23
+ C --> W
24
+ end
25
+
26
+ subgraph Retrieval["Retrieval control"]
27
+ S["Search"]
28
+ A["Five-field assessment"]
29
+ X["Expand or change strategy"]
30
+ S --> A
31
+ A -->|"partial / wrong"| X
32
+ X --> A
33
+ end
34
+
35
+ B --> E
36
+ L --> Retrieval
37
+ E --> Retrieval
38
+ C --> Retrieval
39
+ A -->|"sufficient"| U["Answer and usage receipt"]
40
+ U --> W
41
+ ```
42
+
43
+ The data flow from blocks to event cards and from events to element cards is one-way. Derived cards never rewrite their source block or source event.
44
+
45
+ ## Conversation blocks
46
+
47
+ A completed user/assistant pair is one turn. The default block boundary is 12 completed turns. Messages that have not reached the boundary remain in the open tail and are not condensed or extracted.
48
+
49
+ Hosts may attach a `threadId` to each turn. Open tails, Block boundaries, neighboring extraction context, turn ranges, and decay pointers are then isolated by thread. Persisted Blocks remain available as provenance for long-term cards, while host integrations must inject only the active thread's short-term Block context.
50
+
51
+ When the boundary is reached:
52
+
53
+ 1. One atomic sealing transaction moves the source messages into permanent L5 and writes deterministic L4 and L3.
54
+ 2. The sealed Block is marked model-pending. It is provenance, but it is excluded from decay and cannot replace native conversation history.
55
+ 3. A background summarizer produces and validates L0-L2 plus a conservative `shouldExtract` decision.
56
+ 4. Event extraction completes with either validated Events or an explicit valid empty result.
57
+ 5. Only then is the Block marked ready: its pointer starts at L5, it may replace native history, and it decays toward L0 as newer ready Blocks enter the same thread.
58
+
59
+ The block weight is:
60
+
61
+ ```text
62
+ w(age) = exp(-lambda_block * age)
63
+
64
+ age = latest ready Block position - pointer anchor Block position
65
+ lambda_block = 0.30 by default
66
+ ```
67
+
68
+ Open-tail turns do not change Block age. The weight selects how many levels to drop from the pointer anchor. Expanding a block to L3 anchors the pointer at L3 and at the latest sealed Block position; it does not silently jump to L5. Hosts may configure `lambda_block`; smaller values decay more slowly, and values above `0.4` are not recommended.
69
+
70
+ ## Deterministic L3 policy
71
+
72
+ L3 may remove only:
73
+
74
+ 1. standalone greetings or acknowledgements;
75
+ 2. standalone pure confirmations;
76
+ 3. raw tool-call argument payloads, while retaining tool name and a bounded result summary;
77
+ 4. exact repeated long pasted text or code after the first occurrence.
78
+
79
+ Short repeated natural-language messages are retained. L3 never performs semantic paraphrasing.
80
+
81
+ ## Event extraction
82
+
83
+ After L0-L2 validates, a candidate Block is extracted independently; a later Block is not required. The extractor receives:
84
+
85
+ - target block `N`, including its L5 source and legal evidence IDs;
86
+ - previous block `N-1` L2 keypoints for context, if it exists;
87
+ - the nearest available later ready Block's L2 keypoints for context, if one exists;
88
+ - a compact timeline of existing event IDs, titles, and temporal fields.
89
+
90
+ The target is the only legal source of new facts and quotations. Neighbor blocks are context-only and must not contribute events or source references. Source message IDs are checked against the target block. The reference implementation falls back to all target messages when an extractor returns no valid source ID; stricter adapters may reject the card instead.
91
+
92
+ The core callback retains full `MemoryBlock` objects for compatibility. Bundled model adapters project that callback into the target-first payload above, exposing only L2 keypoints for neighboring blocks.
93
+
94
+ ## Event-card contract
95
+
96
+ An event card stores content, provenance, time, governance, and weight separately.
97
+
98
+ ```ts
99
+ interface EventCard {
100
+ id: string;
101
+ title: string;
102
+ summary: string;
103
+ narrative: string;
104
+ tags: string[];
105
+ quotes: string[];
106
+
107
+ sourceBlockId: string;
108
+ sourceMessageIds: string[];
109
+
110
+ temporal: {
111
+ mentionedAt?: string;
112
+ happenedStart?: string;
113
+ happenedEnd?: string;
114
+ originalText?: string;
115
+ precision?: 'instant' | 'day' | 'month' | 'year' | 'range' | 'unknown';
116
+ basis?: 'explicit' | 'relative' | 'inferred' | 'unknown';
117
+ status?: 'occurred' | 'planned' | 'cancelled' | 'ongoing' | 'unknown';
118
+ participants?: string[];
119
+ eventType?: string;
120
+ supersedesEventIds?: string[];
121
+ conflictsWithEventIds?: string[];
122
+ };
123
+
124
+ status: 'active' | 'superseded' | 'forgotten' | 'archived';
125
+ weight: MemoryWeight;
126
+ }
127
+ ```
128
+
129
+ `mentionedAt` answers when the conversation referred to the event. `happenedStart` and `happenedEnd` answer when the event itself occurred. Keeping these axes separate avoids treating the message timestamp as the event date.
130
+
131
+ ## Element-card projection
132
+
133
+ Event cards are immutable history. Element cards are rebuildable materialized views across events for people, projects, organizations, tools, and places. Each element fact has one of three modes:
134
+
135
+ - `state`: a new fact with the same key supersedes the previous active state;
136
+ - `set`: new unique values are appended without replacing existing values;
137
+ - `relation`: a new relation with the same key supersedes the previous active relation.
138
+
139
+ Facts carry `validFrom`, optional `validTo`, confidence, and `sourceEventIds`. Replacing a state closes the previous fact's validity interval instead of deleting it. `expandElement(id, at)` can therefore reconstruct the view at an earlier time.
140
+
141
+ Projection is a separate persisted job from event extraction. The runtime commits a `pending` job only after its events exist. It then claims the job, calls the application-provided projector outside the transaction, and atomically applies the result or records failure. Every proposed fact is ignored unless all of its source event IDs belong to the claimed batch. An interrupted `running` job becomes `failed` on restart and can be retried without re-extracting events.
142
+
143
+ Applications that manage their own model loop may use `claimNextElementProjection()`, `completeElementProjection()`, and `failElementProjection()` directly. Supplying `elementProjector` lets `appendTurn()` and `resumePendingWork()` drive the same state machine automatically.
144
+
145
+ ## Hybrid retrieval
146
+
147
+ Event and fact-level element search use two inspectable ranking sources:
148
+
149
+ 1. BM25 over field-weighted lexical tokens, including overlapping Han bigrams;
150
+ 2. structured rankings from fields such as participant, event type, time range, element name, and element type.
151
+
152
+ Reciprocal-rank fusion combines the available rankings. A non-empty query with no lexical or structured match returns an empty result rather than all candidates. Element search returns the matched fact plus its element ID, validity interval, and event provenance; callers expand the full element card only when needed. The reference path does not use embeddings or vector similarity.
153
+
154
+ Integration tool responses intentionally expose compact search cards. They retain stable IDs, summaries,
155
+ timestamps, and evidence references while leaving narrative/quotes/source-message lists and full graph
156
+ facts/edges to the expand tools. `rankScore` is a BM25/RRF ordering metric only, not confidence or
157
+ factual accuracy. Graph relation-only hits are filtered as likely adjacency noise; name, alias, tag,
158
+ state, fact, type, and other descriptive matches remain eligible across all supported node types.
159
+
160
+ ## Event weight and adoption
161
+
162
+ Event decay uses:
163
+
164
+ ```text
165
+ w(t, n) = max(floor, exp(-lambda(n) * t))
166
+ lambda(n) = 0.15 / (1 + 1.5 * ln(n))
167
+ ```
168
+
169
+ `n` is the number of recorded adoptions, not retrieval hits. Search updates `lastRetrievedAt` for observability, while `recordMemoryUse()` increments the adoption count and moves the decay anchor.
170
+
171
+ Criticality floors in the reference implementation are:
172
+
173
+ | Criticality | Floor |
174
+ | --- | ---: |
175
+ | routine | 0.0 |
176
+ | preference | 0.3 |
177
+ | identity | 0.9 |
178
+ | safety | 1.0 |
179
+
180
+ A pinned event has effective weight 1. A superseded event is capped at 0.1. Forgotten and archived events have effective weight 0.
181
+
182
+ ## Retrieval assessment contract
183
+
184
+ The assessment contract is deliberately small:
185
+
186
+ ```ts
187
+ interface RetrievalAssessment {
188
+ verdict: 'sufficient' | 'partial' | 'wrong';
189
+ evidenceRefs: string[];
190
+ rejectedEvidenceRefs: Array<{
191
+ inputIndex: number;
192
+ ref: string;
193
+ reason: 'invalid_ref' | 'duplicate' | 'not_in_batch' | 'limit_exceeded';
194
+ detail: string;
195
+ }>;
196
+ fit: string;
197
+ missing: string;
198
+ nextStrategy:
199
+ | 'answer'
200
+ | 'search_events'
201
+ | 'expand_event'
202
+ | 'search_elements'
203
+ | 'expand_element'
204
+ | 'search_raw_memory'
205
+ | 'expand_block';
206
+ }
207
+ ```
208
+
209
+ Normalization enforces three conditions before `sufficient` is accepted:
210
+
211
+ 1. at least one evidence ID belongs to the selected retrieval batch;
212
+ 2. the chosen next strategy is `answer`;
213
+ 3. the assessment uses the bounded schema rather than carrying a growing private scratchpad.
214
+
215
+ If the retrieval budget ends without sufficient evidence, the caller should pass the full retrieval transcript to the answer model and require explicit uncertainty. The core exposes the gate; applications own the tool loop and final model call.
216
+
217
+ ## Storage adapters
218
+
219
+ `StrataGate.open({ database, namespace })` is the normal public entrypoint and always hydrates the state machine from transactional SQLite storage. `StrataGate.inMemory()` is an explicit test and ephemeral-use mode. Advanced integrations may supply another durable `StorageAdapter` through `StrataGate.openWithStorage()`. The bundled `SqliteStorage` adapter persists normalized rows for memory spaces, messages, blocks, events, elements, facts, provenance links, extraction/projection jobs, usage receipts, and idempotent external-turn ingestion receipts.
220
+
221
+ Every namespace has a monotonically increasing revision. A write supplies the revision it loaded; SQLite commits the new revision and all related rows in one immediate transaction. A stale process receives `StorageConflictError` rather than overwriting newer state.
222
+
223
+ External model calls are never made inside a database transaction:
224
+
225
+ 1. a completed raw turn is committed immediately;
226
+ 2. every complete Block is sealed atomically with real L3-L5 before any model call;
227
+ 3. summarization first claims a persisted job, runs outside the transaction, and commits validated L0-L2 or a failed job with bounded retry metadata;
228
+ 4. extraction first commits a running job, calls the extractor, then atomically commits either the event cards, a valid empty result, or a failed job state;
229
+ 5. element projection follows the same claim/call/complete boundary after its source events are durable;
230
+ 6. failed model jobs retry at most three total attempts with exponential backoff; completed empty extraction is terminal and is not retried.
231
+
232
+ The adapter preserves these invariants:
233
+
234
+ - blocks and L5 messages are append-only, including when every derived task fails;
235
+ - model-pending Blocks are excluded from decay and native-history replacement;
236
+ - card provenance references an existing source block and message set;
237
+ - search hits do not increment adoption state;
238
+ - supersession retains the old event;
239
+ - element state replacement retains the old fact and its validity interval;
240
+ - every element and fact source references an existing immutable event;
241
+ - forget is reversible unless an application explicitly implements irreversible deletion;
242
+ - usage receipts are idempotent for one answer turn through a unique `receiptId`.
243
+
244
+ SQLite schema v10 includes durable external-memory import jobs and per-candidate progress, in addition to the normalized Block processing state, summary/extraction retry jobs, graph, element, provenance, receipt, decay-anchor, and lift-source data introduced earlier. Opening a schema-v1 through v9 database migrates it in one transaction and preserves existing namespaces, Blocks, Events, jobs, and receipts. Existing pre-v9 Blocks are treated as ready because their persisted L0-L5 layers were already accepted by the older engine. Schema-v5 turn anchors are converted to per-thread Block positions; schema-v6 lift timestamps retain an unknown legacy source. Pre-v5 Blocks retain no inferred thread ownership, so they remain archival provenance without being attached to a new session. SQLite uses WAL, foreign keys, and per-namespace optimistic concurrency. It does not provide encryption at rest. Search still uses the reference in-memory ranking after hydration, so enabling persistence does not silently change retrieval semantics. Database-native lexical/vector indexes and a Postgres implementation remain separate future work.