@hviana/sema 0.2.0 → 0.2.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.
package/src/mind/mind.ts CHANGED
@@ -14,8 +14,6 @@ import { bindSeat, fold, Sema, Space } from "../sema.js";
14
14
  import { Alphabet } from "../alphabet.js";
15
15
  import {
16
16
  bytesToTree,
17
- bytesToTreePyramid,
18
- type FoldPyramid,
19
17
  Grid,
20
18
  gridToTree,
21
19
  hilbertBytes,
@@ -109,7 +107,7 @@ export interface Conversation {
109
107
  * foldTree returns their ids immediately — O(suffix) instead of
110
108
  * O(context) for every tree walk. */
111
109
  interface ConversationData {
112
- pyramid: FoldPyramid;
110
+ tree: Sema;
113
111
  bytes: Uint8Array;
114
112
  boundaries: number[];
115
113
  perceiveMemo: Map<string, Sema>;
@@ -244,9 +242,10 @@ export class Mind implements MindContext {
244
242
  // bounded: a pyramid costs ~KB per content byte (one D-float gist per
245
243
  // interior node), and only the few live conversation chains need to stay
246
244
  // warm, so 8 entries is the honest budget.
247
- _depositTrees = new BoundedMap<string, import("../geometry.js").FoldPyramid>(
248
- 8,
249
- );
245
+ _depositTrees = new BoundedMap<
246
+ string,
247
+ import("./types.js").DepositCacheEntry
248
+ >(8);
250
249
  _depositLens = new Set<number>();
251
250
  _internIds = new WeakMap<import("../sema.js").Sema, number>();
252
251
 
@@ -513,18 +512,20 @@ export class Mind implements MindContext {
513
512
  * where one turn ends and the next begins. */
514
513
  beginConversation(state?: ConversationState): Conversation {
515
514
  const id = this._nextConvId++;
516
- // Build the initial pyramid: from scratch for a new conversation,
517
- // or from the saved context bytes when restoring.
518
515
  const initBytes = state?.context ?? new Uint8Array(0);
519
- const { pyramid } = bytesToTreePyramid(
516
+ const initBoundaries = state?.boundaries ? [...state.boundaries] : [];
517
+ const tree = bytesToTree(
520
518
  this.space,
521
519
  this.alphabet,
522
520
  initBytes,
521
+ undefined,
522
+ undefined,
523
+ initBoundaries.length > 0 ? initBoundaries : undefined,
523
524
  );
524
525
  this._conversations.set(id, {
525
- pyramid,
526
+ tree,
526
527
  bytes: initBytes,
527
- boundaries: state?.boundaries ? [...state.boundaries] : [],
528
+ boundaries: initBoundaries,
528
529
  perceiveMemo: new Map(),
529
530
  recogniseMemo: new Map(),
530
531
  climbMemo: new Map(),
@@ -572,25 +573,23 @@ export class Mind implements MindContext {
572
573
  * place a context grows ({@link addTurn} and {@link respondTurn} both
573
574
  * come through here), so the append semantics cannot drift. */
574
575
  private _growContext(data: ConversationData, turnBytes: Uint8Array): Sema {
575
- const prevLen = data.pyramid.bytes;
576
+ const prevLen = data.bytes.length;
576
577
  // An empty turn neither grows the context nor marks a boundary —
577
578
  // boundaries are documented strictly increasing, and a zero-length
578
- // "turn" is no turn. The pyramid's zero-growth shortcut makes the
579
- // refold below O(1), so the existing tree is returned unchanged.
579
+ // "turn" is no turn. Nothing changed, so the existing tree stands.
580
580
  const grow = turnBytes.length > 0;
581
- const grown = !grow
582
- ? data.bytes
583
- : prevLen > 0
584
- ? concat2(data.bytes, turnBytes)
585
- : turnBytes;
586
- if (grow && prevLen > 0) data.boundaries.push(prevLen);
587
- const { tree, pyramid } = bytesToTreePyramid(
581
+ if (!grow) return data.tree;
582
+ const grown = prevLen > 0 ? concat2(data.bytes, turnBytes) : turnBytes;
583
+ if (prevLen > 0) data.boundaries.push(prevLen);
584
+ const tree = bytesToTree(
588
585
  this.space,
589
586
  this.alphabet,
590
587
  grown,
591
- data.pyramid,
588
+ undefined,
589
+ undefined,
590
+ data.boundaries.length > 0 ? data.boundaries : undefined,
592
591
  );
593
- data.pyramid = pyramid;
592
+ data.tree = tree;
594
593
  data.bytes = grown;
595
594
  data.perceiveMemo.set(latin1Key(grown), tree);
596
595
  return tree;
@@ -644,6 +643,15 @@ export class Mind implements MindContext {
644
643
  this.canonMemo = this.canon ? new Map() : null;
645
644
 
646
645
  try {
646
+ // No recognise-memo pre-seeding here: that used to be necessary
647
+ // because the flat/positional fold lost visibility into an earlier
648
+ // turn's own structure once later bytes shifted its position
649
+ // (foldTree no longer visited the turn's root node). The
650
+ // STABLE-PREFIX fold (see {@link ConversationData}) makes every
651
+ // turn's subtree independent of what follows it by construction, so
652
+ // recognise() finds it correctly on its own, first-touch, exactly
653
+ // once per turn — no workaround needed, and none pre-empting
654
+ // recognise()'s own memo with a partial result.
647
655
  const top = this.trace?.enter("respondTurn", [
648
656
  rItem(newContext, "query"),
649
657
  ]);
@@ -8,16 +8,16 @@ import { Sema } from "../sema.js";
8
8
  import {
9
9
  bytesToTree,
10
10
  bytesToTreePyramid,
11
- type FoldPyramid,
12
11
  Grid,
13
12
  gridToTree,
14
13
  hilbertBytes,
14
+ stablePrefixFoldIncremental,
15
15
  stackGrids,
16
16
  } from "../geometry.js";
17
17
  import { canonHash } from "../canon.js";
18
18
  import { bytesEqual } from "../bytes.js";
19
19
  import { ALL } from "./types.js";
20
- import type { Input, MindContext } from "./types.js";
20
+ import type { DepositCacheEntry, Input, MindContext } from "./types.js";
21
21
 
22
22
  // ── Address: bytes → node ──────────────────────────────────────────────
23
23
 
@@ -93,34 +93,76 @@ export function perceive(
93
93
  return gridToTree(ctx.space, ctx.alphabet, input as Grid);
94
94
  }
95
95
 
96
- /** The DEPOSIT-shaped perceive: the PLAIN fold (bit-identical to inference
97
- * perception that structural train/inference agreement is load-bearing
98
- * for exact recall), computed INCREMENTALLY via the fold's level pyramid
99
- * ({@link bytesToTreePyramid}). An accumulated context (a conversation)
100
- * grows by suffixes: the previous context's pyramid is cached by CONTENT
101
- * (ctx._depositTrees), and this deposit refolds only the right edge of
102
- * each level O(turn) instead of O(context) per turn. Purely a cache:
103
- * the produced tree never depends on cache state. */
104
- export function perceiveDeposit(ctx: MindContext, bytes: Uint8Array): Sema {
105
- let prev: FoldPyramid | undefined;
106
- // Longest cached PROPER prefix first.
107
- const lens = [...ctx._depositLens]
108
- .filter((L) => L >= 2 && L < bytes.length)
109
- .sort((a, b) => b - a);
110
- for (const L of lens) {
111
- const hit = ctx._depositTrees.get(latin1Key(bytes.subarray(0, L)));
112
- if (hit !== undefined) {
113
- prev = hit;
114
- break;
96
+ /** The DEPOSIT-shaped perceive. A FIRST-SEEN input takes the PLAIN fold
97
+ * (bit-identical to inference perception of a standalone query that
98
+ * structural train/inference agreement is load-bearing for exact recall),
99
+ * computed incrementally via the fold's level pyramid
100
+ * ({@link bytesToTreePyramid}). An input that EXTENDS a previously
101
+ * deposited one is a conversation context grown by one turn — the cached
102
+ * prefix length IS the turn boundary (derived from the deposit sequence
103
+ * itself, never from content conventions) and takes the STABLE-PREFIX
104
+ * fold over the accumulated boundaries, bit-identical to the boundary
105
+ * fold query-time conversation perception uses, so the trained context
106
+ * node and the query's context subtree are the SAME node. Segment folds
107
+ * reuse across deposits ({@link stablePrefixFoldIncremental}) — O(turn)
108
+ * instead of O(context) per turn. The fold state is purely a cache; the
109
+ * boundary accumulation is what an evicted chain loses (falling back to
110
+ * the plain fold, the pre-boundary shape — a warm replay restores it). */
111
+ export function perceiveDeposit(
112
+ ctx: MindContext,
113
+ bytes: Uint8Array,
114
+ conversational = false,
115
+ ): Sema {
116
+ let prev: DepositCacheEntry | undefined;
117
+ let prefixLen = 0;
118
+ // Cache consult (both boundary lookup and stable-prefix reuse) is scoped
119
+ // to conversational deposits only — a bare, unrelated fact whose bytes
120
+ // happen to extend an earlier deposit is NOT a conversation turn, and
121
+ // must keep the plain fold so it shares structure with ITS OWN prior
122
+ // deposits, not fragment against a coincidental byte-prefix.
123
+ if (conversational) {
124
+ // Longest cached PROPER prefix first.
125
+ const lens = [...ctx._depositLens]
126
+ .filter((L) => L >= 2 && L < bytes.length)
127
+ .sort((a, b) => b - a);
128
+ for (const L of lens) {
129
+ const hit = ctx._depositTrees.get(latin1Key(bytes.subarray(0, L)));
130
+ // The suffix must bytes-equal the hit's OWN recorded continuation —
131
+ // proof this deposit is that turn's actual next turn, not a fact
132
+ // that coincidentally shares its byte prefix.
133
+ if (
134
+ hit !== undefined && hit.nextBytes !== undefined &&
135
+ bytesEqual(hit.nextBytes, bytes.subarray(L))
136
+ ) {
137
+ prev = hit;
138
+ prefixLen = L;
139
+ break;
140
+ }
115
141
  }
116
142
  }
117
- const { tree, pyramid } = bytesToTreePyramid(
118
- ctx.space,
119
- ctx.alphabet,
120
- bytes,
121
- prev,
122
- );
123
- if (bytes.length >= 2) {
143
+ let tree: Sema;
144
+ let entry: DepositCacheEntry;
145
+ if (prev !== undefined) {
146
+ const boundaries = [...prev.boundaries, prefixLen];
147
+ const folded = stablePrefixFoldIncremental(
148
+ ctx.space,
149
+ ctx.alphabet,
150
+ bytes,
151
+ boundaries,
152
+ prev.stable,
153
+ );
154
+ tree = folded.tree;
155
+ entry = { boundaries, stable: folded.fold };
156
+ } else {
157
+ const plain = bytesToTreePyramid(ctx.space, ctx.alphabet, bytes);
158
+ tree = plain.tree;
159
+ entry = { boundaries: [], pyramid: plain.pyramid };
160
+ }
161
+ // Only a conversational deposit writes the cache too — otherwise a bare
162
+ // fact's plain fold could later be misread as a conversation's turn-zero
163
+ // boundary by an unrelated conversational deposit that happens to extend
164
+ // its bytes.
165
+ if (conversational && bytes.length >= 2) {
124
166
  // The lengths set drifts as the map evicts; past the probe budget the
125
167
  // drift itself becomes the cost (each stale length is an O(len) key
126
168
  // build), so both reset together — losing only warm-up on live chains.
@@ -128,7 +170,7 @@ export function perceiveDeposit(ctx: MindContext, bytes: Uint8Array): Sema {
128
170
  ctx._depositLens.clear();
129
171
  ctx._depositTrees.clear();
130
172
  }
131
- ctx._depositTrees.set(latin1Key(bytes), pyramid);
173
+ ctx._depositTrees.set(latin1Key(bytes), entry);
132
174
  ctx._depositLens.add(bytes.length);
133
175
  }
134
176
  return tree;
package/src/mind/types.ts CHANGED
@@ -19,7 +19,26 @@ import type {
19
19
  Site,
20
20
  } from "./graph-search.js";
21
21
  import type { Rationale } from "./rationale.js";
22
- import type { FoldPyramid, Grid } from "../geometry.js";
22
+ import type { FoldPyramid, Grid, StableFold } from "../geometry.js";
23
+
24
+ /** One {@link MindContext._depositTrees} entry — see that field's doc. */
25
+ export interface DepositCacheEntry {
26
+ /** Turn boundaries accumulated over this content's deposit chain —
27
+ * strictly increasing proper offsets, each a previously-deposited
28
+ * whole-context length. Empty for a first-seen (single-turn) input. */
29
+ boundaries: number[];
30
+ /** Plain-fold pyramid (first-seen inputs only). */
31
+ pyramid?: FoldPyramid;
32
+ /** Stable-prefix segment folds (grown-context inputs only). */
33
+ stable?: StableFold;
34
+ /** The continuation bytes this ctxInput was paired with in ingestPair, if
35
+ * any — the ONLY thing that makes a later, longer ctxInput a genuine next
36
+ * TURN of the same conversation rather than an unrelated fact that
37
+ * happens to share this one's byte prefix (e.g. "2+2" vs. "2+2=5"). A
38
+ * later deposit only takes this entry as its stable-prefix `prev` when
39
+ * its own suffix bytes-equal this exactly. */
40
+ nextBytes?: Uint8Array;
41
+ }
23
42
  import { concatBytes } from "../bytes.js";
24
43
  import { dominates } from "../geometry.js";
25
44
 
@@ -189,15 +208,19 @@ export interface MindContext extends GraphSearchHost {
189
208
  * never invalidated. Bounded LRU (byte-sized); a miss only re-perceives,
190
209
  * never a correctness risk. */
191
210
  _gistCache: BoundedMap<number, Vec>;
192
- /** DEPOSIT-path stable-prefix tree cache: content key (latin1) of a
193
- * deposited input → its plain-fold level PYRAMID, so the NEXT deposit
194
- * whose prefix it is (a conversation's accumulated context) reuses every
195
- * full aligned block and refolds only the right edge of each level
196
- * O(turn) per deposit instead of O(context), with the tree BIT-IDENTICAL
197
- * to a from-scratch fold (see FoldPyramid). Purely a performance cache.
198
- * Entry-count bounded (a pyramid is ~KB/byte of content; only the few
199
- * live conversation chains matter). */
200
- _depositTrees: BoundedMap<string, FoldPyramid>;
211
+ /** DEPOSIT-path perception cache: content key (latin1) of a deposited
212
+ * input → its accumulated turn BOUNDARIES plus reusable fold state. A
213
+ * deposit whose content extends a cached entry IS a conversation context
214
+ * grown by one turn the cached length is the new boundary so it
215
+ * folds with the SAME stable-prefix fold query-time perception uses
216
+ * (structural train/inference agreement, load-bearing for recall),
217
+ * reusing every already-folded segment via `stable` (see StableFold)
218
+ * O(turn) per deposit instead of O(context). A first-seen input keeps
219
+ * the plain fold and caches its `pyramid` (see FoldPyramid). Purely a
220
+ * performance cache for the FOLD STATE; the boundaries are semantic but
221
+ * derived only from the deposit sequence itself (an evicted chain falls
222
+ * back to plain-fold behavior, exactly the pre-boundary shape). */
223
+ _depositTrees: BoundedMap<string, DepositCacheEntry>;
201
224
  /** The byte lengths present in {@link _depositTrees} — the candidate
202
225
  * prefix lengths probed (longest first). Drifts on eviction (a stale
203
226
  * length only costs a miss); cleared with the map when it outgrows the