@hviana/sema 0.2.1 → 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/dist/src/geometry.d.ts +28 -0
- package/dist/src/geometry.js +35 -0
- package/dist/src/mind/attention.js +29 -0
- package/dist/src/mind/learning.d.ts +1 -1
- package/dist/src/mind/learning.js +20 -5
- package/dist/src/mind/mechanisms/cover.js +32 -0
- package/dist/src/mind/mechanisms/recall.js +26 -2
- package/dist/src/mind/mind.d.ts +2 -2
- package/dist/src/mind/mind.js +22 -53
- package/dist/src/mind/primitives.d.ts +16 -9
- package/dist/src/mind/primitives.js +58 -22
- package/dist/src/mind/types.d.ts +32 -10
- package/package.json +1 -1
- package/src/geometry.ts +61 -1
- package/src/mind/attention.ts +38 -0
- package/src/mind/learning.ts +20 -3
- package/src/mind/mechanisms/cover.ts +38 -0
- package/src/mind/mechanisms/recall.ts +30 -2
- package/src/mind/mind.ts +31 -60
- package/src/mind/primitives.ts +71 -29
- package/src/mind/types.ts +33 -10
package/dist/src/geometry.d.ts
CHANGED
|
@@ -90,6 +90,13 @@ export declare function consensusFloor(N: number): number;
|
|
|
90
90
|
* replaces runtime coverage gating with a batch pass that removes
|
|
91
91
|
* structurally-isolated entries. Derived, never tuned. */
|
|
92
92
|
export declare function coverageBar(_maxGroup: number, D: number): number;
|
|
93
|
+
export interface Folded {
|
|
94
|
+
tree: Sema;
|
|
95
|
+
/** Byte length of the subtree — carried incrementally so the stable-prefix
|
|
96
|
+
* boundary scan never re-walks subtrees (the old per-level walk was
|
|
97
|
+
* O(n log n) over the whole input). */
|
|
98
|
+
len: number;
|
|
99
|
+
}
|
|
93
100
|
export interface Grid {
|
|
94
101
|
width: number;
|
|
95
102
|
height: number;
|
|
@@ -116,6 +123,27 @@ export declare function knownPrefixLength(bytes: Uint8Array, leafAt: (i: number)
|
|
|
116
123
|
* turn extend perception instead of refolding it: identical prefixes
|
|
117
124
|
* produce identical subtrees regardless of what follows them. */
|
|
118
125
|
export declare function bytesToTree(space: Space, alphabet: Alphabet, bytes: Uint8Array, leafAt?: (i: number) => number | null, lookup?: (leafIds: number[]) => number | null, boundaries?: readonly number[]): Sema;
|
|
126
|
+
/** A stable-prefix fold's reusable state: the segment edge offsets and each
|
|
127
|
+
* segment's independently-folded root ({@link riverFoldRaw} output). A
|
|
128
|
+
* grown stream whose boundary set EXTENDS a previous fold's reuses every
|
|
129
|
+
* matching segment's Folded unchanged (segments fold independently by
|
|
130
|
+
* construction, so reuse is bit-identical to refolding) and folds only the
|
|
131
|
+
* new right-edge segment — O(turn) per extension, the stable-prefix
|
|
132
|
+
* counterpart of {@link FoldPyramid}. Purely a cache: the produced tree
|
|
133
|
+
* never depends on cache state. */
|
|
134
|
+
export interface StableFold {
|
|
135
|
+
edges: number[];
|
|
136
|
+
segs: Folded[];
|
|
137
|
+
}
|
|
138
|
+
/** {@link stablePrefixFold} with incremental segment reuse — same cuts, same
|
|
139
|
+
* segment folds, same left-nested join, same single root normalize; `prev`
|
|
140
|
+
* only elides recomputing segments whose [start,end) offsets it already
|
|
141
|
+
* folded over a byte-identical prefix (the caller keys the cache by
|
|
142
|
+
* content). Requires a non-empty effective boundary set. */
|
|
143
|
+
export declare function stablePrefixFoldIncremental(space: Space, alphabet: Alphabet, bytes: Uint8Array, boundaries: readonly number[], prev?: StableFold): {
|
|
144
|
+
tree: Sema;
|
|
145
|
+
fold: StableFold;
|
|
146
|
+
};
|
|
119
147
|
/** The PLAIN fold's full level pyramid — every level's item list, bottom
|
|
120
148
|
* (leaves) to top (root). Left-grouped folding is RADIX-ALIGNED: the item
|
|
121
149
|
* at level L, index i, covers exactly bytes [i·mg^L, (i+1)·mg^L) whenever
|
package/dist/src/geometry.js
CHANGED
|
@@ -321,6 +321,41 @@ function stablePrefixFold(space, alphabet, bytes, boundaries) {
|
|
|
321
321
|
normalize(cur.tree.v);
|
|
322
322
|
return cur.tree;
|
|
323
323
|
}
|
|
324
|
+
/** {@link stablePrefixFold} with incremental segment reuse — same cuts, same
|
|
325
|
+
* segment folds, same left-nested join, same single root normalize; `prev`
|
|
326
|
+
* only elides recomputing segments whose [start,end) offsets it already
|
|
327
|
+
* folded over a byte-identical prefix (the caller keys the cache by
|
|
328
|
+
* content). Requires a non-empty effective boundary set. */
|
|
329
|
+
export function stablePrefixFoldIncremental(space, alphabet, bytes, boundaries, prev) {
|
|
330
|
+
const cuts = [];
|
|
331
|
+
let prevB = 0;
|
|
332
|
+
for (const b of boundaries) {
|
|
333
|
+
if (b > prevB && b < bytes.length) {
|
|
334
|
+
cuts.push(b);
|
|
335
|
+
prevB = b;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
const edges = [0, ...cuts, bytes.length];
|
|
339
|
+
const segs = [];
|
|
340
|
+
for (let i = 0; i + 1 < edges.length; i++) {
|
|
341
|
+
const hit = prev !== undefined && prev.edges[i] === edges[i] &&
|
|
342
|
+
prev.edges[i + 1] === edges[i + 1]
|
|
343
|
+
? prev.segs[i]
|
|
344
|
+
: undefined;
|
|
345
|
+
segs.push(hit ??
|
|
346
|
+
riverFoldRaw(space, bytesToLeaves(alphabet, bytes.subarray(edges[i], edges[i + 1]))));
|
|
347
|
+
}
|
|
348
|
+
if (segs.length === 1) {
|
|
349
|
+
// Degenerate boundary set — the plain fold, as stablePrefixFold does.
|
|
350
|
+
const tree = riverFold(space, bytesToLeaves(alphabet, bytes), bytes.length).tree;
|
|
351
|
+
return { tree, fold: { edges, segs } };
|
|
352
|
+
}
|
|
353
|
+
let cur = segs[0];
|
|
354
|
+
for (let i = 1; i < segs.length; i++)
|
|
355
|
+
cur = fold2(space, cur, segs[i]);
|
|
356
|
+
normalize(cur.tree.v);
|
|
357
|
+
return { tree: cur.tree, fold: { edges, segs } };
|
|
358
|
+
}
|
|
324
359
|
/** Join two folded items as one 2-kid branch — the top-level join of the
|
|
325
360
|
* stable-prefix fold, identical FP ops to foldSlice's seat-bound
|
|
326
361
|
* accumulation over a group of two. Unnormalized (interior). */
|
|
@@ -736,6 +736,27 @@ async function crossRegionVotes(ctx, query, regions, rvs, k, N, reachMemo) {
|
|
|
736
736
|
continue;
|
|
737
737
|
if (ra.end >= rb.start)
|
|
738
738
|
continue; // overlap or adjacent — nothing between
|
|
739
|
+
// Candidates strictly BETWEEN ra and rb (cand is sorted by start, so
|
|
740
|
+
// that is exactly cand[a+1 .. b-1]) that already cast their OWN vote —
|
|
741
|
+
// genuine, individually-corroborated evidence about what fills the gap
|
|
742
|
+
// — gate the container search below: a joint container is binding
|
|
743
|
+
// evidence only when it is CONSISTENT with that evidence, i.e. its own
|
|
744
|
+
// bytes actually contain what the between-region says. This is the
|
|
745
|
+
// n-ary composition's normal shape (a between-attribute's bytes DO
|
|
746
|
+
// recur inside the joint container, credited as an "extra" below) as
|
|
747
|
+
// opposed to a container that silently substitutes something else for
|
|
748
|
+
// it (e.g. bridging past "Italy" to a container whose interior is
|
|
749
|
+
// "Japan" — a different, contradicting learnt whole).
|
|
750
|
+
// Only a KNOWN (content-addressed, exact) between-region qualifies —
|
|
751
|
+
// an approximate region's resonance climbing "somewhere" is ordinary
|
|
752
|
+
// noise (any ANN query returns SOME nearest neighbour), not evidence
|
|
753
|
+
// this specific gap already means something specific.
|
|
754
|
+
const between = [];
|
|
755
|
+
for (let m = a + 1; m < b; m++) {
|
|
756
|
+
if (strong.has(cand[m]) && !consumed.has(cand[m]) &&
|
|
757
|
+
regions[cand[m]].known)
|
|
758
|
+
between.push(cand[m]);
|
|
759
|
+
}
|
|
739
760
|
// A single KNOWN region covering both: the whole form is already a
|
|
740
761
|
// stored identity that votes directly; its pieces add nothing.
|
|
741
762
|
if (regions.some((r) => r.known && r.start <= ra.start && rb.end <= r.end))
|
|
@@ -781,6 +802,14 @@ async function crossRegionVotes(ctx, query, regions, rvs, k, N, reachMemo) {
|
|
|
781
802
|
if (indexOf(query, joined, 0) >= 0)
|
|
782
803
|
continue; // query says it itself
|
|
783
804
|
}
|
|
805
|
+
// CONTRADICTION GUARD: a between-region already carrying its own
|
|
806
|
+
// vote must actually recur in this container's bytes — otherwise
|
|
807
|
+
// the container is a different learnt whole that happens to share
|
|
808
|
+
// ra/rb, and letting it stand in for the gap would silently
|
|
809
|
+
// override evidence the query itself already resolved there.
|
|
810
|
+
if (between.some((bi) => indexOf(bytes, query.subarray(regions[bi].start, regions[bi].end), 0) < 0)) {
|
|
811
|
+
continue;
|
|
812
|
+
}
|
|
784
813
|
let cov = left.length + right.length;
|
|
785
814
|
const extras = [];
|
|
786
815
|
for (const ei of cand) {
|
|
@@ -18,7 +18,7 @@ export declare function internTreeIds(ctx: MindContext, node: Sema, ids: Map<Sem
|
|
|
18
18
|
export declare function indexSubSpans(ctx: MindContext, tree: Sema, ids: Map<Sema, number>): Promise<boolean>;
|
|
19
19
|
/** Perceive, intern, and index a single input. Returns the perceived tree,
|
|
20
20
|
* root id, id map, and the changed (new) subtrees for halo reinforcement. */
|
|
21
|
-
export declare function deposit(ctx: MindContext, input: Input, track: boolean): Promise<{
|
|
21
|
+
export declare function deposit(ctx: MindContext, input: Input, track: boolean, conversational?: boolean): Promise<{
|
|
22
22
|
tree: Sema;
|
|
23
23
|
rootId: number;
|
|
24
24
|
ids: Map<Sema, number>;
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// node. A fact is an EDGE between node ids; recall traverses edges.
|
|
5
5
|
import { bindSeat, companySignature, isChunk } from "../sema.js";
|
|
6
6
|
import { changedNodes } from "./types.js";
|
|
7
|
-
import { inputBytes, perceiveDeposit, resolve, } from "./primitives.js";
|
|
7
|
+
import { inputBytes, latin1Key, perceiveDeposit, resolve, } from "./primitives.js";
|
|
8
8
|
import { canonicalWindows, leafIdPrefix } from "./canonical.js";
|
|
9
9
|
import { fold as foldVecs } from "../sema.js";
|
|
10
10
|
/** Intern a perceived tree into node ids, bottom-up, sharing equal subtrees.
|
|
@@ -103,7 +103,7 @@ export async function indexSubSpans(ctx, tree, ids) {
|
|
|
103
103
|
}
|
|
104
104
|
/** Perceive, intern, and index a single input. Returns the perceived tree,
|
|
105
105
|
* root id, id map, and the changed (new) subtrees for halo reinforcement. */
|
|
106
|
-
export async function deposit(ctx, input, track) {
|
|
106
|
+
export async function deposit(ctx, input, track, conversational = false) {
|
|
107
107
|
const bytes = inputBytes(ctx, input);
|
|
108
108
|
// Deposit-shaped perception: stable-prefix tree SEEDING (see
|
|
109
109
|
// perceiveDeposit) — an accumulated context re-folds only its new suffix,
|
|
@@ -111,8 +111,13 @@ export async function deposit(ctx, input, track) {
|
|
|
111
111
|
// (no store-probe fallback): a knownPrefixLength scan on every novel fact
|
|
112
112
|
// would cost O(n²) hashing, while conversation replays are always warm —
|
|
113
113
|
// re-deposition replays from the first turn, rebuilding the cache as it
|
|
114
|
-
// goes.
|
|
115
|
-
|
|
114
|
+
// goes. `conversational` scopes the STABLE-PREFIX variant (turn-boundary
|
|
115
|
+
// folding, matching query-time perception) to ingestPair's own growing
|
|
116
|
+
// context argument — a bare ingestOne deposit whose bytes merely happen
|
|
117
|
+
// to extend an earlier UNRELATED deposit (no conversational relationship)
|
|
118
|
+
// must keep the plain fold, or two coincidentally-prefix-sharing facts
|
|
119
|
+
// would stop sharing structure with each other.
|
|
120
|
+
const tree = perceiveDeposit(ctx, bytes, conversational);
|
|
116
121
|
const ids = new Map();
|
|
117
122
|
const rootId = await internTreeIds(ctx, tree, ids);
|
|
118
123
|
const indexed = await indexSubSpans(ctx, tree, ids);
|
|
@@ -185,9 +190,19 @@ async function propagateSuffixes(ctx, src, dst) {
|
|
|
185
190
|
}
|
|
186
191
|
/** Ingest a pair (context, continuation) — learn an edge and pour halos. */
|
|
187
192
|
export async function ingestPair(ctx, ctxInput, cont) {
|
|
188
|
-
const c = await deposit(ctx, ctxInput, true);
|
|
193
|
+
const c = await deposit(ctx, ctxInput, true, true);
|
|
189
194
|
const cont_ = await deposit(ctx, cont, false);
|
|
190
195
|
const ctxId = c.rootId, contId = cont_.rootId;
|
|
196
|
+
// Stamp this turn's continuation onto its own cache entry — the proof a
|
|
197
|
+
// FUTURE, longer ctxInput needs (see perceiveDeposit) to recognise itself
|
|
198
|
+
// as this conversation's genuine next turn rather than an unrelated fact
|
|
199
|
+
// that happens to share this ctxInput's byte prefix.
|
|
200
|
+
{
|
|
201
|
+
const ctxBytes = inputBytes(ctx, ctxInput);
|
|
202
|
+
const entry = ctx._depositTrees.get(latin1Key(ctxBytes));
|
|
203
|
+
if (entry !== undefined)
|
|
204
|
+
entry.nextBytes = inputBytes(ctx, cont);
|
|
205
|
+
}
|
|
191
206
|
await ctx.store.link(ctxId, contId);
|
|
192
207
|
await propagateSuffixes(ctx, ctxId, contId);
|
|
193
208
|
// Halos pour company SIGNATURES (identity), not gists (content) — see
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
// cover runs FIRST in defaultMechanisms: a computed-backed cover becomes a
|
|
9
9
|
// near-zero-cost incumbent that prunes the other mechanisms through the
|
|
10
10
|
// ordinary admissible-floor check, with no extension special-case anywhere.
|
|
11
|
+
import { bytesEqual, indexOf } from "../../bytes.js";
|
|
11
12
|
import { read, resolve } from "../primitives.js";
|
|
12
13
|
import { guidedFirst } from "../traverse.js";
|
|
13
14
|
import { conceptHop } from "../match.js";
|
|
@@ -158,6 +159,37 @@ export const coverMechanism = {
|
|
|
158
159
|
: "lightest derivation: the chosen spans, left to right");
|
|
159
160
|
if (segs === null)
|
|
160
161
|
return [];
|
|
162
|
+
// A chosen span's SUBSTITUTED bytes (an edge followed from a recognised
|
|
163
|
+
// site, not the site's own literal text read back) that equal a byte
|
|
164
|
+
// span the query ALREADY CONTAINS elsewhere restates part of the
|
|
165
|
+
// question — never an answer (the same principle recall.ts's tiers
|
|
166
|
+
// apply to a whole-query projection: "a projection that is a proper
|
|
167
|
+
// byte-subspan of the query restates part of the question"). A
|
|
168
|
+
// recognised site that is itself an entire PRIOR TURN of a multi-turn
|
|
169
|
+
// query is exactly this shape: it carries a genuine learnt
|
|
170
|
+
// continuation, but that continuation is something the asker already
|
|
171
|
+
// said moments later in the SAME query, not a new answer — the cover
|
|
172
|
+
// search has no notion of "turn" to gate this itself, so the check
|
|
173
|
+
// belongs here, over the derivation it already chose.
|
|
174
|
+
const W = ctx.space.maxGroup;
|
|
175
|
+
for (const s of segs) {
|
|
176
|
+
if (!s.rec)
|
|
177
|
+
continue;
|
|
178
|
+
const literal = s.j - s.i === s.bytes.length &&
|
|
179
|
+
bytesEqual(s.bytes, query.subarray(s.i, s.j));
|
|
180
|
+
if (literal)
|
|
181
|
+
continue;
|
|
182
|
+
// A span shorter than one river window is exactly the "ice"/"c"
|
|
183
|
+
// case: a chain hop's short terminal coincidentally recurring as a
|
|
184
|
+
// substring of an unrelated longer word. Below the quantum, byte
|
|
185
|
+
// overlap is chance, not evidence — the same floor identityBar and
|
|
186
|
+
// reachThreshold hold every other structural-overlap claim to.
|
|
187
|
+
if (s.bytes.length >= W && s.bytes.length < query.length &&
|
|
188
|
+
indexOf(query, s.bytes, 0) >= 0) {
|
|
189
|
+
ctx.trace?.step("restatedSpan", [rItem(s.bytes, "substituted", s.node, [s.i, s.j])], [], "the chosen span's substitution already occurs elsewhere in the query — restates it, not an answer");
|
|
190
|
+
return [];
|
|
191
|
+
}
|
|
192
|
+
}
|
|
161
193
|
const composed = liftAnswer(segs, query.length);
|
|
162
194
|
if (composed === null)
|
|
163
195
|
return [];
|
|
@@ -74,10 +74,34 @@ export async function recallByResonance(ctx, query, pre) {
|
|
|
74
74
|
maximal.push(s);
|
|
75
75
|
maxEnd = s.end;
|
|
76
76
|
}
|
|
77
|
-
|
|
77
|
+
// The wrapper must actually BE scaffolding: RC8's own premise is "the
|
|
78
|
+
// wrapper is scaffolding; the argument is the span that leads
|
|
79
|
+
// somewhere" ("How do you say 'thank you' in French?" — everything
|
|
80
|
+
// outside the argument is a small fixed template). When the query
|
|
81
|
+
// instead has ANOTHER substantial recognised form (≥ W2, the same
|
|
82
|
+
// constituent bar the argument itself must clear) sitting OUTSIDE the
|
|
83
|
+
// chosen argument, the query is not one argument in a wrapper — it is
|
|
84
|
+
// several complete, independently-meaningful pieces (a multi-turn
|
|
85
|
+
// conversation's own accumulated turns are exactly this shape), and
|
|
86
|
+
// binding to the argument's continuation would answer past content
|
|
87
|
+
// the query itself already carries forward. Derived from the same W2
|
|
88
|
+
// bar the argument itself is held to, never a separate tuned number.
|
|
89
|
+
const hasSubstantialOutside = maximal.length === 1 &&
|
|
90
|
+
pre.rec.sites.some((s) => s.end - s.start >= W2 &&
|
|
91
|
+
(s.end <= maximal[0].start || s.start >= maximal[0].end));
|
|
92
|
+
if (maximal.length === 1 && !hasSubstantialOutside) {
|
|
78
93
|
const arg = maximal[0];
|
|
79
94
|
const g = await follow(ctx, arg.payload, queryGist);
|
|
80
|
-
|
|
95
|
+
// The same "no restated fragment" guard tier 2 applies below (§ "the
|
|
96
|
+
// anchor cleared the consensus floor..."): a followed continuation
|
|
97
|
+
// that is itself a proper byte-subspan of the QUERY restates part of
|
|
98
|
+
// the question — never an answer. A multi-turn query's own later
|
|
99
|
+
// turns are exact, content-addressed matches for exactly this reason
|
|
100
|
+
// (each turn is its own previously-learnt form), so without this
|
|
101
|
+
// guard the argument's OWN later restatement in the same
|
|
102
|
+
// conversation reads as if it were the next thing to say.
|
|
103
|
+
if (g !== null && g.length > 0 &&
|
|
104
|
+
!(g.length < query.length && indexOf(query, g, 0) >= 0)) {
|
|
81
105
|
return ground(g, "argument binding — the query's sole edge-source constituent, continuation followed", [[arg.start, arg.end]], STEP);
|
|
82
106
|
}
|
|
83
107
|
}
|
package/dist/src/mind/mind.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Vec } from "../vec.js";
|
|
2
2
|
import { Sema, Space } from "../sema.js";
|
|
3
3
|
import { Alphabet } from "../alphabet.js";
|
|
4
|
-
import {
|
|
4
|
+
import { Grid } from "../geometry.js";
|
|
5
5
|
import { BoundedMap, type Store } from "../store.js";
|
|
6
6
|
import { type MindConfig } from "../config.js";
|
|
7
7
|
import { type Canon } from "../canon.js";
|
|
@@ -111,7 +111,7 @@ export declare class Mind implements MindContext {
|
|
|
111
111
|
* {@link MindContext._gistCache}. 32 MB ≈ 8K gists at D=1024; hub
|
|
112
112
|
* candidate sets (√N at most) fit comfortably and recur across queries. */
|
|
113
113
|
_gistCache: BoundedMap<number, Vec>;
|
|
114
|
-
_depositTrees: BoundedMap<string,
|
|
114
|
+
_depositTrees: BoundedMap<string, import("./types.js").DepositCacheEntry>;
|
|
115
115
|
_depositLens: Set<number>;
|
|
116
116
|
_internIds: WeakMap<Sema, number>;
|
|
117
117
|
private _nextConvId;
|
package/dist/src/mind/mind.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// Implementation split across src/mind/*.ts — this file assembles the Mind class.
|
|
11
11
|
import { makeKeyring, rng, setVecConfig } from "../vec.js";
|
|
12
12
|
import { Alphabet } from "../alphabet.js";
|
|
13
|
-
import {
|
|
13
|
+
import { bytesToTree, reachThreshold, } from "../geometry.js";
|
|
14
14
|
import { BoundedMap } from "../store.js";
|
|
15
15
|
import { SQliteStore } from "../store-sqlite.js";
|
|
16
16
|
import { resolveConfig } from "../config.js";
|
|
@@ -263,14 +263,13 @@ export class Mind {
|
|
|
263
263
|
* where one turn ends and the next begins. */
|
|
264
264
|
beginConversation(state) {
|
|
265
265
|
const id = this._nextConvId++;
|
|
266
|
-
// Build the initial pyramid: from scratch for a new conversation,
|
|
267
|
-
// or from the saved context bytes when restoring.
|
|
268
266
|
const initBytes = state?.context ?? new Uint8Array(0);
|
|
269
|
-
const
|
|
267
|
+
const initBoundaries = state?.boundaries ? [...state.boundaries] : [];
|
|
268
|
+
const tree = bytesToTree(this.space, this.alphabet, initBytes, undefined, undefined, initBoundaries.length > 0 ? initBoundaries : undefined);
|
|
270
269
|
this._conversations.set(id, {
|
|
271
|
-
|
|
270
|
+
tree,
|
|
272
271
|
bytes: initBytes,
|
|
273
|
-
boundaries:
|
|
272
|
+
boundaries: initBoundaries,
|
|
274
273
|
perceiveMemo: new Map(),
|
|
275
274
|
recogniseMemo: new Map(),
|
|
276
275
|
climbMemo: new Map(),
|
|
@@ -316,21 +315,18 @@ export class Mind {
|
|
|
316
315
|
* place a context grows ({@link addTurn} and {@link respondTurn} both
|
|
317
316
|
* come through here), so the append semantics cannot drift. */
|
|
318
317
|
_growContext(data, turnBytes) {
|
|
319
|
-
const prevLen = data.
|
|
318
|
+
const prevLen = data.bytes.length;
|
|
320
319
|
// An empty turn neither grows the context nor marks a boundary —
|
|
321
320
|
// boundaries are documented strictly increasing, and a zero-length
|
|
322
|
-
// "turn" is no turn.
|
|
323
|
-
// refold below O(1), so the existing tree is returned unchanged.
|
|
321
|
+
// "turn" is no turn. Nothing changed, so the existing tree stands.
|
|
324
322
|
const grow = turnBytes.length > 0;
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
: turnBytes;
|
|
330
|
-
if (grow && prevLen > 0)
|
|
323
|
+
if (!grow)
|
|
324
|
+
return data.tree;
|
|
325
|
+
const grown = prevLen > 0 ? concat2(data.bytes, turnBytes) : turnBytes;
|
|
326
|
+
if (prevLen > 0)
|
|
331
327
|
data.boundaries.push(prevLen);
|
|
332
|
-
const
|
|
333
|
-
data.
|
|
328
|
+
const tree = bytesToTree(this.space, this.alphabet, grown, undefined, undefined, data.boundaries.length > 0 ? data.boundaries : undefined);
|
|
329
|
+
data.tree = tree;
|
|
334
330
|
data.bytes = grown;
|
|
335
331
|
data.perceiveMemo.set(latin1Key(grown), tree);
|
|
336
332
|
return tree;
|
|
@@ -377,42 +373,15 @@ export class Mind {
|
|
|
377
373
|
this.canon = this._canonFor(typeof turn === "string" ? textCanon : null);
|
|
378
374
|
this.canonMemo = this.canon ? new Map() : null;
|
|
379
375
|
try {
|
|
380
|
-
//
|
|
381
|
-
//
|
|
382
|
-
//
|
|
383
|
-
//
|
|
384
|
-
//
|
|
385
|
-
//
|
|
386
|
-
//
|
|
387
|
-
//
|
|
388
|
-
|
|
389
|
-
const suffixStart = data.boundaries[data.boundaries.length - 1];
|
|
390
|
-
if (suffixStart < newContext.length) {
|
|
391
|
-
const suffixBytes = newContext.subarray(suffixStart);
|
|
392
|
-
const suffixRec = recognise(this, suffixBytes);
|
|
393
|
-
const fullRec = recognise(this, newContext);
|
|
394
|
-
const seen = new Set(fullRec.sites.map((s) => `${s.start},${s.end}`));
|
|
395
|
-
const mergedSites = [...fullRec.sites];
|
|
396
|
-
for (const site of suffixRec.sites) {
|
|
397
|
-
const absStart = suffixStart + site.start;
|
|
398
|
-
const absEnd = suffixStart + site.end;
|
|
399
|
-
const key = `${absStart},${absEnd}`;
|
|
400
|
-
if (!seen.has(key)) {
|
|
401
|
-
seen.add(key);
|
|
402
|
-
mergedSites.push({
|
|
403
|
-
start: absStart,
|
|
404
|
-
end: absEnd,
|
|
405
|
-
payload: site.payload,
|
|
406
|
-
});
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
data.recogniseMemo.set(latin1Key(newContext), {
|
|
410
|
-
sites: mergedSites,
|
|
411
|
-
leaves: fullRec.leaves,
|
|
412
|
-
splits: fullRec.splits,
|
|
413
|
-
});
|
|
414
|
-
}
|
|
415
|
-
}
|
|
376
|
+
// No recognise-memo pre-seeding here: that used to be necessary
|
|
377
|
+
// because the flat/positional fold lost visibility into an earlier
|
|
378
|
+
// turn's own structure once later bytes shifted its position
|
|
379
|
+
// (foldTree no longer visited the turn's root node). The
|
|
380
|
+
// STABLE-PREFIX fold (see {@link ConversationData}) makes every
|
|
381
|
+
// turn's subtree independent of what follows it by construction, so
|
|
382
|
+
// recognise() finds it correctly on its own, first-touch, exactly
|
|
383
|
+
// once per turn — no workaround needed, and none pre-empting
|
|
384
|
+
// recognise()'s own memo with a partial result.
|
|
416
385
|
const top = this.trace?.enter("respondTurn", [
|
|
417
386
|
rItem(newContext, "query"),
|
|
418
387
|
]);
|
|
@@ -15,15 +15,22 @@ export declare function latin1Key(bytes: Uint8Array): string;
|
|
|
15
15
|
* CALLER — who assembled the multi-turn context — knows where those
|
|
16
16
|
* boundaries are; the geometry never guesses them from the bytes. */
|
|
17
17
|
export declare function perceive(ctx: MindContext, input: Input, leafAt?: (i: number) => number | null, lookup?: (ids: number[]) => number | null, boundaries?: readonly number[]): Sema;
|
|
18
|
-
/** The DEPOSIT-shaped perceive
|
|
19
|
-
* perception
|
|
20
|
-
* for exact recall),
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
|
|
18
|
+
/** The DEPOSIT-shaped perceive. A FIRST-SEEN input takes the PLAIN fold
|
|
19
|
+
* (bit-identical to inference perception of a standalone query — that
|
|
20
|
+
* structural train/inference agreement is load-bearing for exact recall),
|
|
21
|
+
* computed incrementally via the fold's level pyramid
|
|
22
|
+
* ({@link bytesToTreePyramid}). An input that EXTENDS a previously
|
|
23
|
+
* deposited one is a conversation context grown by one turn — the cached
|
|
24
|
+
* prefix length IS the turn boundary (derived from the deposit sequence
|
|
25
|
+
* itself, never from content conventions) — and takes the STABLE-PREFIX
|
|
26
|
+
* fold over the accumulated boundaries, bit-identical to the boundary
|
|
27
|
+
* fold query-time conversation perception uses, so the trained context
|
|
28
|
+
* node and the query's context subtree are the SAME node. Segment folds
|
|
29
|
+
* reuse across deposits ({@link stablePrefixFoldIncremental}) — O(turn)
|
|
30
|
+
* instead of O(context) per turn. The fold state is purely a cache; the
|
|
31
|
+
* boundary accumulation is what an evicted chain loses (falling back to
|
|
32
|
+
* the plain fold, the pre-boundary shape — a warm replay restores it). */
|
|
33
|
+
export declare function perceiveDeposit(ctx: MindContext, bytes: Uint8Array, conversational?: boolean): Sema;
|
|
27
34
|
/** The raw bytes of an input — modality-neutral conversion. */
|
|
28
35
|
export declare function inputBytes(ctx: MindContext, input: Input): Uint8Array;
|
|
29
36
|
/** Convenience: the gist vector of a byte span. */
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
//
|
|
3
3
|
// Address — bytes → node (perceive, foldTree, resolve)
|
|
4
4
|
// Read — node → bytes (read)
|
|
5
|
-
import { bytesToTree, bytesToTreePyramid, gridToTree, hilbertBytes, stackGrids, } from "../geometry.js";
|
|
5
|
+
import { bytesToTree, bytesToTreePyramid, gridToTree, hilbertBytes, stablePrefixFoldIncremental, stackGrids, } from "../geometry.js";
|
|
6
6
|
import { canonHash } from "../canon.js";
|
|
7
7
|
import { bytesEqual } from "../bytes.js";
|
|
8
8
|
import { ALL } from "./types.js";
|
|
@@ -58,29 +58,65 @@ export function perceive(ctx, input, leafAt, lookup, boundaries) {
|
|
|
58
58
|
}
|
|
59
59
|
return gridToTree(ctx.space, ctx.alphabet, input);
|
|
60
60
|
}
|
|
61
|
-
/** The DEPOSIT-shaped perceive
|
|
62
|
-
* perception
|
|
63
|
-
* for exact recall),
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
|
|
61
|
+
/** The DEPOSIT-shaped perceive. A FIRST-SEEN input takes the PLAIN fold
|
|
62
|
+
* (bit-identical to inference perception of a standalone query — that
|
|
63
|
+
* structural train/inference agreement is load-bearing for exact recall),
|
|
64
|
+
* computed incrementally via the fold's level pyramid
|
|
65
|
+
* ({@link bytesToTreePyramid}). An input that EXTENDS a previously
|
|
66
|
+
* deposited one is a conversation context grown by one turn — the cached
|
|
67
|
+
* prefix length IS the turn boundary (derived from the deposit sequence
|
|
68
|
+
* itself, never from content conventions) — and takes the STABLE-PREFIX
|
|
69
|
+
* fold over the accumulated boundaries, bit-identical to the boundary
|
|
70
|
+
* fold query-time conversation perception uses, so the trained context
|
|
71
|
+
* node and the query's context subtree are the SAME node. Segment folds
|
|
72
|
+
* reuse across deposits ({@link stablePrefixFoldIncremental}) — O(turn)
|
|
73
|
+
* instead of O(context) per turn. The fold state is purely a cache; the
|
|
74
|
+
* boundary accumulation is what an evicted chain loses (falling back to
|
|
75
|
+
* the plain fold, the pre-boundary shape — a warm replay restores it). */
|
|
76
|
+
export function perceiveDeposit(ctx, bytes, conversational = false) {
|
|
70
77
|
let prev;
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
78
|
+
let prefixLen = 0;
|
|
79
|
+
// Cache consult (both boundary lookup and stable-prefix reuse) is scoped
|
|
80
|
+
// to conversational deposits only — a bare, unrelated fact whose bytes
|
|
81
|
+
// happen to extend an earlier deposit is NOT a conversation turn, and
|
|
82
|
+
// must keep the plain fold so it shares structure with ITS OWN prior
|
|
83
|
+
// deposits, not fragment against a coincidental byte-prefix.
|
|
84
|
+
if (conversational) {
|
|
85
|
+
// Longest cached PROPER prefix first.
|
|
86
|
+
const lens = [...ctx._depositLens]
|
|
87
|
+
.filter((L) => L >= 2 && L < bytes.length)
|
|
88
|
+
.sort((a, b) => b - a);
|
|
89
|
+
for (const L of lens) {
|
|
90
|
+
const hit = ctx._depositTrees.get(latin1Key(bytes.subarray(0, L)));
|
|
91
|
+
// The suffix must bytes-equal the hit's OWN recorded continuation —
|
|
92
|
+
// proof this deposit is that turn's actual next turn, not a fact
|
|
93
|
+
// that coincidentally shares its byte prefix.
|
|
94
|
+
if (hit !== undefined && hit.nextBytes !== undefined &&
|
|
95
|
+
bytesEqual(hit.nextBytes, bytes.subarray(L))) {
|
|
96
|
+
prev = hit;
|
|
97
|
+
prefixLen = L;
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
80
100
|
}
|
|
81
101
|
}
|
|
82
|
-
|
|
83
|
-
|
|
102
|
+
let tree;
|
|
103
|
+
let entry;
|
|
104
|
+
if (prev !== undefined) {
|
|
105
|
+
const boundaries = [...prev.boundaries, prefixLen];
|
|
106
|
+
const folded = stablePrefixFoldIncremental(ctx.space, ctx.alphabet, bytes, boundaries, prev.stable);
|
|
107
|
+
tree = folded.tree;
|
|
108
|
+
entry = { boundaries, stable: folded.fold };
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
const plain = bytesToTreePyramid(ctx.space, ctx.alphabet, bytes);
|
|
112
|
+
tree = plain.tree;
|
|
113
|
+
entry = { boundaries: [], pyramid: plain.pyramid };
|
|
114
|
+
}
|
|
115
|
+
// Only a conversational deposit writes the cache too — otherwise a bare
|
|
116
|
+
// fact's plain fold could later be misread as a conversation's turn-zero
|
|
117
|
+
// boundary by an unrelated conversational deposit that happens to extend
|
|
118
|
+
// its bytes.
|
|
119
|
+
if (conversational && bytes.length >= 2) {
|
|
84
120
|
// The lengths set drifts as the map evicts; past the probe budget the
|
|
85
121
|
// drift itself becomes the cost (each stale length is an O(len) key
|
|
86
122
|
// build), so both reset together — losing only warm-up on live chains.
|
|
@@ -88,7 +124,7 @@ export function perceiveDeposit(ctx, bytes) {
|
|
|
88
124
|
ctx._depositLens.clear();
|
|
89
125
|
ctx._depositTrees.clear();
|
|
90
126
|
}
|
|
91
|
-
ctx._depositTrees.set(latin1Key(bytes),
|
|
127
|
+
ctx._depositTrees.set(latin1Key(bytes), entry);
|
|
92
128
|
ctx._depositLens.add(bytes.length);
|
|
93
129
|
}
|
|
94
130
|
return tree;
|
package/dist/src/mind/types.d.ts
CHANGED
|
@@ -6,7 +6,25 @@ import type { Alphabet } from "../alphabet.js";
|
|
|
6
6
|
import type { MindConfig } from "../config.js";
|
|
7
7
|
import type { GraphSearch, Leaf, Seg, Site } from "./graph-search.js";
|
|
8
8
|
import type { Rationale } from "./rationale.js";
|
|
9
|
-
import type { FoldPyramid, Grid } from "../geometry.js";
|
|
9
|
+
import type { FoldPyramid, Grid, StableFold } from "../geometry.js";
|
|
10
|
+
/** One {@link MindContext._depositTrees} entry — see that field's doc. */
|
|
11
|
+
export interface DepositCacheEntry {
|
|
12
|
+
/** Turn boundaries accumulated over this content's deposit chain —
|
|
13
|
+
* strictly increasing proper offsets, each a previously-deposited
|
|
14
|
+
* whole-context length. Empty for a first-seen (single-turn) input. */
|
|
15
|
+
boundaries: number[];
|
|
16
|
+
/** Plain-fold pyramid (first-seen inputs only). */
|
|
17
|
+
pyramid?: FoldPyramid;
|
|
18
|
+
/** Stable-prefix segment folds (grown-context inputs only). */
|
|
19
|
+
stable?: StableFold;
|
|
20
|
+
/** The continuation bytes this ctxInput was paired with in ingestPair, if
|
|
21
|
+
* any — the ONLY thing that makes a later, longer ctxInput a genuine next
|
|
22
|
+
* TURN of the same conversation rather than an unrelated fact that
|
|
23
|
+
* happens to share this one's byte prefix (e.g. "2+2" vs. "2+2=5"). A
|
|
24
|
+
* later deposit only takes this entry as its stable-prefix `prev` when
|
|
25
|
+
* its own suffix bytes-equal this exactly. */
|
|
26
|
+
nextBytes?: Uint8Array;
|
|
27
|
+
}
|
|
10
28
|
export type Input = string | Uint8Array | Grid | Grid[];
|
|
11
29
|
/** The host capabilities GraphSearch consults during a cover. MindContext
|
|
12
30
|
* extends this so the Mind can pass itself as the host. */
|
|
@@ -153,15 +171,19 @@ export interface MindContext extends GraphSearchHost {
|
|
|
153
171
|
* never invalidated. Bounded LRU (byte-sized); a miss only re-perceives,
|
|
154
172
|
* never a correctness risk. */
|
|
155
173
|
_gistCache: BoundedMap<number, Vec>;
|
|
156
|
-
/** DEPOSIT-path
|
|
157
|
-
*
|
|
158
|
-
* whose
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
-
|
|
174
|
+
/** DEPOSIT-path perception cache: content key (latin1) of a deposited
|
|
175
|
+
* input → its accumulated turn BOUNDARIES plus reusable fold state. A
|
|
176
|
+
* deposit whose content extends a cached entry IS a conversation context
|
|
177
|
+
* grown by one turn — the cached length is the new boundary — so it
|
|
178
|
+
* folds with the SAME stable-prefix fold query-time perception uses
|
|
179
|
+
* (structural train/inference agreement, load-bearing for recall),
|
|
180
|
+
* reusing every already-folded segment via `stable` (see StableFold) —
|
|
181
|
+
* O(turn) per deposit instead of O(context). A first-seen input keeps
|
|
182
|
+
* the plain fold and caches its `pyramid` (see FoldPyramid). Purely a
|
|
183
|
+
* performance cache for the FOLD STATE; the boundaries are semantic but
|
|
184
|
+
* derived only from the deposit sequence itself (an evicted chain falls
|
|
185
|
+
* back to plain-fold behavior, exactly the pre-boundary shape). */
|
|
186
|
+
_depositTrees: BoundedMap<string, DepositCacheEntry>;
|
|
165
187
|
/** The byte lengths present in {@link _depositTrees} — the candidate
|
|
166
188
|
* prefix lengths probed (longest first). Drifts on eviction (a stale
|
|
167
189
|
* length only costs a miss); cleared with the map when it outgrows the
|
package/package.json
CHANGED
package/src/geometry.ts
CHANGED
|
@@ -145,7 +145,7 @@ export function coverageBar(_maxGroup: number, D: number): number {
|
|
|
145
145
|
|
|
146
146
|
// ---- types ----
|
|
147
147
|
|
|
148
|
-
interface Folded {
|
|
148
|
+
export interface Folded {
|
|
149
149
|
tree: Sema;
|
|
150
150
|
/** Byte length of the subtree — carried incrementally so the stable-prefix
|
|
151
151
|
* boundary scan never re-walks subtrees (the old per-level walk was
|
|
@@ -383,6 +383,66 @@ function stablePrefixFold(
|
|
|
383
383
|
return cur.tree;
|
|
384
384
|
}
|
|
385
385
|
|
|
386
|
+
/** A stable-prefix fold's reusable state: the segment edge offsets and each
|
|
387
|
+
* segment's independently-folded root ({@link riverFoldRaw} output). A
|
|
388
|
+
* grown stream whose boundary set EXTENDS a previous fold's reuses every
|
|
389
|
+
* matching segment's Folded unchanged (segments fold independently by
|
|
390
|
+
* construction, so reuse is bit-identical to refolding) and folds only the
|
|
391
|
+
* new right-edge segment — O(turn) per extension, the stable-prefix
|
|
392
|
+
* counterpart of {@link FoldPyramid}. Purely a cache: the produced tree
|
|
393
|
+
* never depends on cache state. */
|
|
394
|
+
export interface StableFold {
|
|
395
|
+
edges: number[];
|
|
396
|
+
segs: Folded[];
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/** {@link stablePrefixFold} with incremental segment reuse — same cuts, same
|
|
400
|
+
* segment folds, same left-nested join, same single root normalize; `prev`
|
|
401
|
+
* only elides recomputing segments whose [start,end) offsets it already
|
|
402
|
+
* folded over a byte-identical prefix (the caller keys the cache by
|
|
403
|
+
* content). Requires a non-empty effective boundary set. */
|
|
404
|
+
export function stablePrefixFoldIncremental(
|
|
405
|
+
space: Space,
|
|
406
|
+
alphabet: Alphabet,
|
|
407
|
+
bytes: Uint8Array,
|
|
408
|
+
boundaries: readonly number[],
|
|
409
|
+
prev?: StableFold,
|
|
410
|
+
): { tree: Sema; fold: StableFold } {
|
|
411
|
+
const cuts: number[] = [];
|
|
412
|
+
let prevB = 0;
|
|
413
|
+
for (const b of boundaries) {
|
|
414
|
+
if (b > prevB && b < bytes.length) {
|
|
415
|
+
cuts.push(b);
|
|
416
|
+
prevB = b;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
const edges = [0, ...cuts, bytes.length];
|
|
420
|
+
const segs: Folded[] = [];
|
|
421
|
+
for (let i = 0; i + 1 < edges.length; i++) {
|
|
422
|
+
const hit = prev !== undefined && prev.edges[i] === edges[i] &&
|
|
423
|
+
prev.edges[i + 1] === edges[i + 1]
|
|
424
|
+
? prev.segs[i]
|
|
425
|
+
: undefined;
|
|
426
|
+
segs.push(
|
|
427
|
+
hit ??
|
|
428
|
+
riverFoldRaw(
|
|
429
|
+
space,
|
|
430
|
+
bytesToLeaves(alphabet, bytes.subarray(edges[i], edges[i + 1])),
|
|
431
|
+
),
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
if (segs.length === 1) {
|
|
435
|
+
// Degenerate boundary set — the plain fold, as stablePrefixFold does.
|
|
436
|
+
const tree =
|
|
437
|
+
riverFold(space, bytesToLeaves(alphabet, bytes), bytes.length).tree;
|
|
438
|
+
return { tree, fold: { edges, segs } };
|
|
439
|
+
}
|
|
440
|
+
let cur = segs[0];
|
|
441
|
+
for (let i = 1; i < segs.length; i++) cur = fold2(space, cur, segs[i]);
|
|
442
|
+
normalize(cur.tree.v);
|
|
443
|
+
return { tree: cur.tree, fold: { edges, segs } };
|
|
444
|
+
}
|
|
445
|
+
|
|
386
446
|
/** Join two folded items as one 2-kid branch — the top-level join of the
|
|
387
447
|
* stable-prefix fold, identical FP ops to foldSlice's seat-bound
|
|
388
448
|
* accumulation over a group of two. Unnormalized (interior). */
|
package/src/mind/attention.ts
CHANGED
|
@@ -867,6 +867,28 @@ async function crossRegionVotes(
|
|
|
867
867
|
const rb = regions[cand[b]];
|
|
868
868
|
if (!strong.has(cand[a]) && !strong.has(cand[b])) continue;
|
|
869
869
|
if (ra.end >= rb.start) continue; // overlap or adjacent — nothing between
|
|
870
|
+
// Candidates strictly BETWEEN ra and rb (cand is sorted by start, so
|
|
871
|
+
// that is exactly cand[a+1 .. b-1]) that already cast their OWN vote —
|
|
872
|
+
// genuine, individually-corroborated evidence about what fills the gap
|
|
873
|
+
// — gate the container search below: a joint container is binding
|
|
874
|
+
// evidence only when it is CONSISTENT with that evidence, i.e. its own
|
|
875
|
+
// bytes actually contain what the between-region says. This is the
|
|
876
|
+
// n-ary composition's normal shape (a between-attribute's bytes DO
|
|
877
|
+
// recur inside the joint container, credited as an "extra" below) as
|
|
878
|
+
// opposed to a container that silently substitutes something else for
|
|
879
|
+
// it (e.g. bridging past "Italy" to a container whose interior is
|
|
880
|
+
// "Japan" — a different, contradicting learnt whole).
|
|
881
|
+
// Only a KNOWN (content-addressed, exact) between-region qualifies —
|
|
882
|
+
// an approximate region's resonance climbing "somewhere" is ordinary
|
|
883
|
+
// noise (any ANN query returns SOME nearest neighbour), not evidence
|
|
884
|
+
// this specific gap already means something specific.
|
|
885
|
+
const between: number[] = [];
|
|
886
|
+
for (let m = a + 1; m < b; m++) {
|
|
887
|
+
if (
|
|
888
|
+
strong.has(cand[m]) && !consumed.has(cand[m]) &&
|
|
889
|
+
regions[cand[m]].known
|
|
890
|
+
) between.push(cand[m]);
|
|
891
|
+
}
|
|
870
892
|
// A single KNOWN region covering both: the whole form is already a
|
|
871
893
|
// stored identity that votes directly; its pieces add nothing.
|
|
872
894
|
if (
|
|
@@ -932,6 +954,22 @@ async function crossRegionVotes(
|
|
|
932
954
|
);
|
|
933
955
|
if (indexOf(query, joined, 0) >= 0) continue; // query says it itself
|
|
934
956
|
}
|
|
957
|
+
// CONTRADICTION GUARD: a between-region already carrying its own
|
|
958
|
+
// vote must actually recur in this container's bytes — otherwise
|
|
959
|
+
// the container is a different learnt whole that happens to share
|
|
960
|
+
// ra/rb, and letting it stand in for the gap would silently
|
|
961
|
+
// override evidence the query itself already resolved there.
|
|
962
|
+
if (
|
|
963
|
+
between.some((bi) =>
|
|
964
|
+
indexOf(
|
|
965
|
+
bytes,
|
|
966
|
+
query.subarray(regions[bi].start, regions[bi].end),
|
|
967
|
+
0,
|
|
968
|
+
) < 0
|
|
969
|
+
)
|
|
970
|
+
) {
|
|
971
|
+
continue;
|
|
972
|
+
}
|
|
935
973
|
let cov = left.length + right.length;
|
|
936
974
|
const extras: number[] = [];
|
|
937
975
|
for (const ei of cand) {
|
package/src/mind/learning.ts
CHANGED
|
@@ -9,6 +9,7 @@ import type { Input, MindContext } from "./types.js";
|
|
|
9
9
|
import { changedNodes } from "./types.js";
|
|
10
10
|
import {
|
|
11
11
|
inputBytes,
|
|
12
|
+
latin1Key,
|
|
12
13
|
perceive,
|
|
13
14
|
perceiveDeposit,
|
|
14
15
|
resolve,
|
|
@@ -119,6 +120,7 @@ export async function deposit(
|
|
|
119
120
|
ctx: MindContext,
|
|
120
121
|
input: Input,
|
|
121
122
|
track: boolean,
|
|
123
|
+
conversational = false,
|
|
122
124
|
): Promise<
|
|
123
125
|
{ tree: Sema; rootId: number; ids: Map<Sema, number>; changed: Sema[] }
|
|
124
126
|
> {
|
|
@@ -129,8 +131,13 @@ export async function deposit(
|
|
|
129
131
|
// (no store-probe fallback): a knownPrefixLength scan on every novel fact
|
|
130
132
|
// would cost O(n²) hashing, while conversation replays are always warm —
|
|
131
133
|
// re-deposition replays from the first turn, rebuilding the cache as it
|
|
132
|
-
// goes.
|
|
133
|
-
|
|
134
|
+
// goes. `conversational` scopes the STABLE-PREFIX variant (turn-boundary
|
|
135
|
+
// folding, matching query-time perception) to ingestPair's own growing
|
|
136
|
+
// context argument — a bare ingestOne deposit whose bytes merely happen
|
|
137
|
+
// to extend an earlier UNRELATED deposit (no conversational relationship)
|
|
138
|
+
// must keep the plain fold, or two coincidentally-prefix-sharing facts
|
|
139
|
+
// would stop sharing structure with each other.
|
|
140
|
+
const tree = perceiveDeposit(ctx, bytes, conversational);
|
|
134
141
|
|
|
135
142
|
const ids = new Map<Sema, number>();
|
|
136
143
|
const rootId = await internTreeIds(ctx, tree, ids);
|
|
@@ -214,10 +221,20 @@ export async function ingestPair(
|
|
|
214
221
|
ctxInput: Input,
|
|
215
222
|
cont: Input,
|
|
216
223
|
): Promise<void> {
|
|
217
|
-
const c = await deposit(ctx, ctxInput, true);
|
|
224
|
+
const c = await deposit(ctx, ctxInput, true, true);
|
|
218
225
|
const cont_ = await deposit(ctx, cont, false);
|
|
219
226
|
const ctxId = c.rootId, contId = cont_.rootId;
|
|
220
227
|
|
|
228
|
+
// Stamp this turn's continuation onto its own cache entry — the proof a
|
|
229
|
+
// FUTURE, longer ctxInput needs (see perceiveDeposit) to recognise itself
|
|
230
|
+
// as this conversation's genuine next turn rather than an unrelated fact
|
|
231
|
+
// that happens to share this ctxInput's byte prefix.
|
|
232
|
+
{
|
|
233
|
+
const ctxBytes = inputBytes(ctx, ctxInput);
|
|
234
|
+
const entry = ctx._depositTrees.get(latin1Key(ctxBytes));
|
|
235
|
+
if (entry !== undefined) entry.nextBytes = inputBytes(ctx, cont);
|
|
236
|
+
}
|
|
237
|
+
|
|
221
238
|
await ctx.store.link(ctxId, contId);
|
|
222
239
|
await propagateSuffixes(ctx, ctxId, contId);
|
|
223
240
|
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
import type { MindContext } from "../types.js";
|
|
13
13
|
import type { ComputedResult, Site } from "../graph-search.js";
|
|
14
|
+
import { bytesEqual, indexOf } from "../../bytes.js";
|
|
14
15
|
import { read, resolve } from "../primitives.js";
|
|
15
16
|
import { guidedFirst } from "../traverse.js";
|
|
16
17
|
import { conceptHop } from "../match.js";
|
|
@@ -217,6 +218,43 @@ export const coverMechanism: PipelineMechanism = {
|
|
|
217
218
|
|
|
218
219
|
if (segs === null) return [];
|
|
219
220
|
|
|
221
|
+
// A chosen span's SUBSTITUTED bytes (an edge followed from a recognised
|
|
222
|
+
// site, not the site's own literal text read back) that equal a byte
|
|
223
|
+
// span the query ALREADY CONTAINS elsewhere restates part of the
|
|
224
|
+
// question — never an answer (the same principle recall.ts's tiers
|
|
225
|
+
// apply to a whole-query projection: "a projection that is a proper
|
|
226
|
+
// byte-subspan of the query restates part of the question"). A
|
|
227
|
+
// recognised site that is itself an entire PRIOR TURN of a multi-turn
|
|
228
|
+
// query is exactly this shape: it carries a genuine learnt
|
|
229
|
+
// continuation, but that continuation is something the asker already
|
|
230
|
+
// said moments later in the SAME query, not a new answer — the cover
|
|
231
|
+
// search has no notion of "turn" to gate this itself, so the check
|
|
232
|
+
// belongs here, over the derivation it already chose.
|
|
233
|
+
const W = ctx.space.maxGroup;
|
|
234
|
+
for (const s of segs) {
|
|
235
|
+
if (!s.rec) continue;
|
|
236
|
+
const literal = s.j - s.i === s.bytes.length &&
|
|
237
|
+
bytesEqual(s.bytes, query.subarray(s.i, s.j));
|
|
238
|
+
if (literal) continue;
|
|
239
|
+
// A span shorter than one river window is exactly the "ice"/"c"
|
|
240
|
+
// case: a chain hop's short terminal coincidentally recurring as a
|
|
241
|
+
// substring of an unrelated longer word. Below the quantum, byte
|
|
242
|
+
// overlap is chance, not evidence — the same floor identityBar and
|
|
243
|
+
// reachThreshold hold every other structural-overlap claim to.
|
|
244
|
+
if (
|
|
245
|
+
s.bytes.length >= W && s.bytes.length < query.length &&
|
|
246
|
+
indexOf(query, s.bytes, 0) >= 0
|
|
247
|
+
) {
|
|
248
|
+
ctx.trace?.step(
|
|
249
|
+
"restatedSpan",
|
|
250
|
+
[rItem(s.bytes, "substituted", s.node, [s.i, s.j])],
|
|
251
|
+
[],
|
|
252
|
+
"the chosen span's substitution already occurs elsewhere in the query — restates it, not an answer",
|
|
253
|
+
);
|
|
254
|
+
return [];
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
220
258
|
const composed = liftAnswer(segs, query.length);
|
|
221
259
|
if (composed === null) return [];
|
|
222
260
|
|
|
@@ -113,10 +113,38 @@ export async function recallByResonance(
|
|
|
113
113
|
maximal.push(s);
|
|
114
114
|
maxEnd = s.end;
|
|
115
115
|
}
|
|
116
|
-
|
|
116
|
+
// The wrapper must actually BE scaffolding: RC8's own premise is "the
|
|
117
|
+
// wrapper is scaffolding; the argument is the span that leads
|
|
118
|
+
// somewhere" ("How do you say 'thank you' in French?" — everything
|
|
119
|
+
// outside the argument is a small fixed template). When the query
|
|
120
|
+
// instead has ANOTHER substantial recognised form (≥ W2, the same
|
|
121
|
+
// constituent bar the argument itself must clear) sitting OUTSIDE the
|
|
122
|
+
// chosen argument, the query is not one argument in a wrapper — it is
|
|
123
|
+
// several complete, independently-meaningful pieces (a multi-turn
|
|
124
|
+
// conversation's own accumulated turns are exactly this shape), and
|
|
125
|
+
// binding to the argument's continuation would answer past content
|
|
126
|
+
// the query itself already carries forward. Derived from the same W2
|
|
127
|
+
// bar the argument itself is held to, never a separate tuned number.
|
|
128
|
+
const hasSubstantialOutside = maximal.length === 1 &&
|
|
129
|
+
pre.rec.sites.some((s) =>
|
|
130
|
+
s.end - s.start >= W2 &&
|
|
131
|
+
(s.end <= maximal[0].start || s.start >= maximal[0].end)
|
|
132
|
+
);
|
|
133
|
+
if (maximal.length === 1 && !hasSubstantialOutside) {
|
|
117
134
|
const arg = maximal[0];
|
|
118
135
|
const g = await follow(ctx, arg.payload, queryGist);
|
|
119
|
-
|
|
136
|
+
// The same "no restated fragment" guard tier 2 applies below (§ "the
|
|
137
|
+
// anchor cleared the consensus floor..."): a followed continuation
|
|
138
|
+
// that is itself a proper byte-subspan of the QUERY restates part of
|
|
139
|
+
// the question — never an answer. A multi-turn query's own later
|
|
140
|
+
// turns are exact, content-addressed matches for exactly this reason
|
|
141
|
+
// (each turn is its own previously-learnt form), so without this
|
|
142
|
+
// guard the argument's OWN later restatement in the same
|
|
143
|
+
// conversation reads as if it were the next thing to say.
|
|
144
|
+
if (
|
|
145
|
+
g !== null && g.length > 0 &&
|
|
146
|
+
!(g.length < query.length && indexOf(query, g, 0) >= 0)
|
|
147
|
+
) {
|
|
120
148
|
return ground(
|
|
121
149
|
g,
|
|
122
150
|
"argument binding — the query's sole edge-source constituent, continuation followed",
|
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
|
-
|
|
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<
|
|
248
|
-
|
|
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
|
|
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
|
-
|
|
526
|
+
tree,
|
|
526
527
|
bytes: initBytes,
|
|
527
|
-
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.
|
|
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.
|
|
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
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
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
|
-
|
|
588
|
+
undefined,
|
|
589
|
+
undefined,
|
|
590
|
+
data.boundaries.length > 0 ? data.boundaries : undefined,
|
|
592
591
|
);
|
|
593
|
-
data.
|
|
592
|
+
data.tree = tree;
|
|
594
593
|
data.bytes = grown;
|
|
595
594
|
data.perceiveMemo.set(latin1Key(grown), tree);
|
|
596
595
|
return tree;
|
|
@@ -644,43 +643,15 @@ export class Mind implements MindContext {
|
|
|
644
643
|
this.canonMemo = this.canon ? new Map() : null;
|
|
645
644
|
|
|
646
645
|
try {
|
|
647
|
-
//
|
|
648
|
-
//
|
|
649
|
-
//
|
|
650
|
-
//
|
|
651
|
-
//
|
|
652
|
-
//
|
|
653
|
-
//
|
|
654
|
-
//
|
|
655
|
-
|
|
656
|
-
const suffixStart = data.boundaries[data.boundaries.length - 1];
|
|
657
|
-
if (suffixStart < newContext.length) {
|
|
658
|
-
const suffixBytes = newContext.subarray(suffixStart);
|
|
659
|
-
const suffixRec = recognise(this, suffixBytes);
|
|
660
|
-
const fullRec = recognise(this, newContext);
|
|
661
|
-
const seen = new Set(fullRec.sites.map((s) => `${s.start},${s.end}`));
|
|
662
|
-
const mergedSites = [...fullRec.sites];
|
|
663
|
-
for (const site of suffixRec.sites) {
|
|
664
|
-
const absStart = suffixStart + site.start;
|
|
665
|
-
const absEnd = suffixStart + site.end;
|
|
666
|
-
const key = `${absStart},${absEnd}`;
|
|
667
|
-
if (!seen.has(key)) {
|
|
668
|
-
seen.add(key);
|
|
669
|
-
mergedSites.push({
|
|
670
|
-
start: absStart,
|
|
671
|
-
end: absEnd,
|
|
672
|
-
payload: site.payload,
|
|
673
|
-
});
|
|
674
|
-
}
|
|
675
|
-
}
|
|
676
|
-
data.recogniseMemo.set(latin1Key(newContext), {
|
|
677
|
-
sites: mergedSites,
|
|
678
|
-
leaves: fullRec.leaves,
|
|
679
|
-
splits: fullRec.splits,
|
|
680
|
-
});
|
|
681
|
-
}
|
|
682
|
-
}
|
|
683
|
-
|
|
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.
|
|
684
655
|
const top = this.trace?.enter("respondTurn", [
|
|
685
656
|
rItem(newContext, "query"),
|
|
686
657
|
]);
|
package/src/mind/primitives.ts
CHANGED
|
@@ -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
|
|
97
|
-
* perception
|
|
98
|
-
* for exact recall),
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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),
|
|
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
|
|
193
|
-
*
|
|
194
|
-
* whose
|
|
195
|
-
*
|
|
196
|
-
*
|
|
197
|
-
*
|
|
198
|
-
*
|
|
199
|
-
*
|
|
200
|
-
|
|
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
|