@polycode-projects/the-mechanical-code-talker 6.0.12 → 6.0.13
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@polycode-projects/the-mechanical-code-talker",
|
|
3
|
-
"version": "6.0.
|
|
3
|
+
"version": "6.0.13",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "The Mechanical Code Talker (tmct) — a tolerant, offline, $0 chat surface that guides you toward precision queries about a software repository. ELIZA/PARRY-style but domain-obsessed with code. No model calls; indexes a repo on request (tmct index) or reads any producer's graph.",
|
|
@@ -766,12 +766,22 @@ const ROW_NODE_ID_KEY = "nodeId";
|
|
|
766
766
|
* `wrapRowBackendOverSqliteSeed`, the entry point that takes one.
|
|
767
767
|
* `sqliteSeedOverlayRows` are rows that layer over that seed and under the
|
|
768
768
|
* session's own (a turn's retrieved corpus subgraph), read-only exactly as
|
|
769
|
-
* the seed is.
|
|
769
|
+
* the seed is.
|
|
770
|
+
*
|
|
771
|
+
* `copyOnRead: false` hands every reader the assembled payload itself instead
|
|
772
|
+
* of a copy of it. Copying a seed-sized payload is the most expensive thing
|
|
773
|
+
* this backend does — over a second per read at 60k facts — and a caller that
|
|
774
|
+
* drives the whole session through this one handle (a worker cycle, a request
|
|
775
|
+
* handler) has nobody to protect the payload from. Anyone sharing a handle
|
|
776
|
+
* across independent readers leaves the default alone: without the copy, a
|
|
777
|
+
* reader that mutates what it read has changed the store's own view of
|
|
778
|
+
* itself. */
|
|
770
779
|
export function wrapRowBackend(impl, {
|
|
771
780
|
basePayload = null,
|
|
772
781
|
sqliteSeedStore = null,
|
|
773
782
|
sqliteSeedOverlayRows = null,
|
|
774
783
|
onOversizedRow = "throw",
|
|
784
|
+
copyOnRead = true,
|
|
775
785
|
log = undefined,
|
|
776
786
|
} = {}) {
|
|
777
787
|
const problems = rowBackendProblems(impl);
|
|
@@ -795,6 +805,7 @@ export function wrapRowBackend(impl, {
|
|
|
795
805
|
baseRows: null,
|
|
796
806
|
storedRows: null,
|
|
797
807
|
onOversizedRow,
|
|
808
|
+
copyOnRead,
|
|
798
809
|
log,
|
|
799
810
|
};
|
|
800
811
|
}
|
|
@@ -805,8 +816,8 @@ export function wrapRowBackend(impl, {
|
|
|
805
816
|
* never materialized as a row array: the assembled payload is the only copy
|
|
806
817
|
* of it this process holds. `overlayRows` layer over the seed and under the
|
|
807
818
|
* session's own rows. */
|
|
808
|
-
export function wrapRowBackendOverSqliteSeed(impl, sqliteSeedStore, { overlayRows = null, onOversizedRow = "throw", log = undefined } = {}) {
|
|
809
|
-
return wrapRowBackend(impl, { sqliteSeedStore, sqliteSeedOverlayRows: overlayRows, onOversizedRow, log });
|
|
819
|
+
export function wrapRowBackendOverSqliteSeed(impl, sqliteSeedStore, { overlayRows = null, onOversizedRow = "throw", copyOnRead = true, log = undefined } = {}) {
|
|
820
|
+
return wrapRowBackend(impl, { sqliteSeedStore, sqliteSeedOverlayRows: overlayRows, onOversizedRow, copyOnRead, log });
|
|
810
821
|
}
|
|
811
822
|
|
|
812
823
|
/** Drain whatever `readRows()` returned: the contract allows an array or an
|
|
@@ -863,7 +874,7 @@ async function ensureRowPayload(handle) {
|
|
|
863
874
|
prefixes: await readRowMeta(handle, ROW_META_PREFIXES_KEY, seedScalar(handle, "prefixes", empty.prefixes)),
|
|
864
875
|
};
|
|
865
876
|
if (handle.sqliteSeedStore) {
|
|
866
|
-
handle.cachedPayload = assembleSqliteSeededPayload(handle, meta);
|
|
877
|
+
handle.cachedPayload = migrateStoredMemory(assembleSqliteSeededPayload(handle, meta));
|
|
867
878
|
} else {
|
|
868
879
|
// "keep": the base overlay's own rows never reach the wire (persistRowPayload
|
|
869
880
|
// excludes every seed key from every write via seedOnlyKeys below), so the
|
|
@@ -872,16 +883,49 @@ async function ensureRowPayload(handle) {
|
|
|
872
883
|
// band's own high-fan-out property (one edge per fact is normal, not a
|
|
873
884
|
// pathology) and break every read that depends on it.
|
|
874
885
|
handle.baseRows = payloadToRows(handle.basePayload || empty, { onOversizedRow: "keep" });
|
|
875
|
-
handle.cachedPayload = rowsToPayload(overlayRows(handle.baseRows, handle.storedRows), { meta });
|
|
886
|
+
handle.cachedPayload = migrateStoredMemory(rowsToPayload(overlayRows(handle.baseRows, handle.storedRows), { meta }));
|
|
876
887
|
}
|
|
877
888
|
}
|
|
878
889
|
return handle.cachedPayload;
|
|
879
890
|
}
|
|
880
891
|
|
|
881
892
|
/** loadMemory's read for Backend D: one `readRows()` per cold open, then a
|
|
882
|
-
* clone of the assembled payload per call
|
|
893
|
+
* clone of the assembled payload per call — unless the handle was opened
|
|
894
|
+
* `copyOnRead: false`, where the reader gets the assembled payload itself. */
|
|
883
895
|
async function readRowPayload(handle) {
|
|
884
|
-
|
|
896
|
+
const payload = await ensureRowPayload(handle);
|
|
897
|
+
return handle.copyOnRead === false ? payload : cloneJson(payload);
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
/** A payload a mutation can work on without any of it reaching the one it was
|
|
901
|
+
* copied from. Every container a write reaches into is its own: the individuals
|
|
902
|
+
* array and each individual, the edge groups and each group's example list.
|
|
903
|
+
* What stays shared is what a write never changes in place — an individual's
|
|
904
|
+
* `attributes` array is REPLACED by `setAttr`, never pushed onto, and an edge
|
|
905
|
+
* is replaced rather than edited, so both sides keep reading their own.
|
|
906
|
+
*
|
|
907
|
+
* This is `structuredClone`'s job done at the granularity writes actually use.
|
|
908
|
+
* At seed scale the deep copy runs well over a second and a cycle pays it on
|
|
909
|
+
* every fact it grounds; this is a few milliseconds of pointer copying. */
|
|
910
|
+
function mutablePayloadCopy(payload) {
|
|
911
|
+
return {
|
|
912
|
+
...payload,
|
|
913
|
+
individuals: (payload.individuals || []).map((ind) => ({ ...ind })),
|
|
914
|
+
objectProperties: (payload.objectProperties || []).map((group) => ({
|
|
915
|
+
...group, examples: [...(group.examples || [])],
|
|
916
|
+
})),
|
|
917
|
+
};
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
/** Forget a row handle's assembled payload, so the next read rebuilds it from
|
|
921
|
+
* the store. The one thing that must happen after a mutation dies part-way
|
|
922
|
+
* through: the payload it was changing is neither what the store holds nor a
|
|
923
|
+
* coherent graph. */
|
|
924
|
+
function dropAssembledRowPayload(handle) {
|
|
925
|
+
if (!isRowHandle(handle)) return;
|
|
926
|
+
handle.cachedPayload = null;
|
|
927
|
+
handle.storedRows = null;
|
|
928
|
+
handle.baseRows = null;
|
|
885
929
|
}
|
|
886
930
|
|
|
887
931
|
/** A record with its audit stamp removed. `mgx:updatedAt` moves on every
|
|
@@ -935,9 +979,7 @@ async function persistRowPayload(handle, payload) {
|
|
|
935
979
|
await handle.impl.putMeta(ROW_META_MEMORY_KEY, JSON.stringify(payload.memory ?? emptyMemory().memory));
|
|
936
980
|
await handle.impl.putMeta(ROW_META_PREFIXES_KEY, JSON.stringify(payload.prefixes ?? emptyMemory().prefixes));
|
|
937
981
|
} catch (e) {
|
|
938
|
-
handle
|
|
939
|
-
handle.storedRows = null;
|
|
940
|
-
handle.baseRows = null;
|
|
982
|
+
dropAssembledRowPayload(handle);
|
|
941
983
|
throw e;
|
|
942
984
|
}
|
|
943
985
|
const removed = new Set(removals);
|
|
@@ -949,13 +991,13 @@ async function persistRowPayload(handle, payload) {
|
|
|
949
991
|
patchAssembledPayload(handle.cachedPayload, meta, writes);
|
|
950
992
|
} else if (handle.sqliteSeedStore) {
|
|
951
993
|
handle.cachedPayload = null;
|
|
952
|
-
handle.cachedPayload = assembleSqliteSeededPayload(handle, meta);
|
|
994
|
+
handle.cachedPayload = migrateStoredMemory(assembleSqliteSeededPayload(handle, meta));
|
|
953
995
|
} else {
|
|
954
996
|
// Dropped before the rebuild, not after it: the payload this replaces is the
|
|
955
997
|
// largest object the handle holds, and keeping it reachable while the next
|
|
956
998
|
// one assembles doubles the peak for no reason.
|
|
957
999
|
handle.cachedPayload = null;
|
|
958
|
-
handle.cachedPayload = rowsToPayload(overlayRows(handle.baseRows, handle.storedRows), { meta });
|
|
1000
|
+
handle.cachedPayload = migrateStoredMemory(rowsToPayload(overlayRows(handle.baseRows, handle.storedRows), { meta }));
|
|
959
1001
|
}
|
|
960
1002
|
}
|
|
961
1003
|
|
|
@@ -1951,7 +1993,9 @@ export async function snapshotMemory(dir, { retentionVersions } = {}) {
|
|
|
1951
1993
|
export async function loadMemory(dir) {
|
|
1952
1994
|
if (isMemoryHandle(dir)) return migrateStoredMemory(dir.payload);
|
|
1953
1995
|
if (isSqliteHandle(dir)) return migrateStoredMemory(readSqlitePayload(dir));
|
|
1954
|
-
|
|
1996
|
+
// Not migrated here: a row handle migrates the payload once, as it assembles
|
|
1997
|
+
// it, so every read after the first is spared two walks of the whole graph.
|
|
1998
|
+
if (isRowHandle(dir)) return readRowPayload(dir);
|
|
1955
1999
|
let text;
|
|
1956
2000
|
try {
|
|
1957
2001
|
text = await readFile(memoryGraphFile(dir), "utf8");
|
|
@@ -2315,15 +2359,31 @@ function retractionsFor(payload, groupId) {
|
|
|
2315
2359
|
* linear scan in that case. */
|
|
2316
2360
|
const memoryIndexOf = (payload) => payload?.[MEMORY_INDEX] || null;
|
|
2317
2361
|
|
|
2362
|
+
/** Load, change, persist. A row handle always works on a copy here, even one
|
|
2363
|
+
* opened `copyOnRead: false`: a write drops every change that lands on a
|
|
2364
|
+
* seed-owned row, so a mutation applied straight to the assembled payload
|
|
2365
|
+
* would leave the handle holding changes the store refused. The copy is what
|
|
2366
|
+
* keeps "what this handle reads" and "what a fresh handle would assemble" the
|
|
2367
|
+
* same thing.
|
|
2368
|
+
*
|
|
2369
|
+
* A row handle does skip the prose index built here: `persistRowPayload`
|
|
2370
|
+
* re-derives every derived structure from the rows it just wrote, so building
|
|
2371
|
+
* one now builds it twice over the whole graph. */
|
|
2318
2372
|
async function mutateMemory(dir, fn) {
|
|
2319
|
-
const
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2373
|
+
const overRowHandle = isRowHandle(dir);
|
|
2374
|
+
const payload = overRowHandle ? mutablePayloadCopy(await ensureRowPayload(dir)) : await loadMemory(dir);
|
|
2375
|
+
try {
|
|
2376
|
+
buildMemoryIndex(payload);
|
|
2377
|
+
const out = (await fn(payload)) ?? payload;
|
|
2378
|
+
migrateLegacyProvenance(out);
|
|
2379
|
+
recomputeSourceReliability(out);
|
|
2380
|
+
if (!overRowHandle) out.proseIndex = buildProseIndex(out.individuals);
|
|
2381
|
+
await persistMemory(dir, out);
|
|
2382
|
+
return overRowHandle ? dir.cachedPayload : out;
|
|
2383
|
+
} catch (e) {
|
|
2384
|
+
dropAssembledRowPayload(dir);
|
|
2385
|
+
throw e;
|
|
2386
|
+
}
|
|
2327
2387
|
}
|
|
2328
2388
|
|
|
2329
2389
|
const labelOf = (text) => (text.length > LABEL_CAP ? text.slice(0, LABEL_CAP - 1) + "…" : text);
|
|
@@ -2552,7 +2612,10 @@ function recomputeSourceReliability(payload) {
|
|
|
2552
2612
|
if (!Array.isArray(payload?.individuals) || !Array.isArray(payload?.objectProperties)) return;
|
|
2553
2613
|
const rows = readFactRows(payload); // Fact-only — contradiction accounting is inherently Fact-shaped
|
|
2554
2614
|
const contradictedFactIds = new Set();
|
|
2555
|
-
|
|
2615
|
+
// The fold above is the same one findContradictions would take for itself,
|
|
2616
|
+
// and over a seed-sized graph it is half a second. Nothing changes the
|
|
2617
|
+
// payload between the two, so it goes across.
|
|
2618
|
+
for (const group of findContradictions(payload, { factRows: rows })) for (const r of group) contradictedFactIds.add(r.id);
|
|
2556
2619
|
|
|
2557
2620
|
const bySource = new Map(); // sessionSourceId -> { factsAsserted, factsContradicted }
|
|
2558
2621
|
for (const row of rows) {
|
|
@@ -4195,8 +4258,8 @@ export const MULTI_VALUED_PREDICATES = MERGE_PREDICATES;
|
|
|
4195
4258
|
* only what its own clock could not order (the resolver's trust and codepoint
|
|
4196
4259
|
* tie-breaks — see resolveSiblingGroups), so ordinary succession stops reading
|
|
4197
4260
|
* as disagreement; every other predicate keeps the full keep-both contract. */
|
|
4198
|
-
export function findContradictions(memory, { floor = CONTRADICTION_TRUST_FLOOR } = {}) {
|
|
4199
|
-
const rows = readFactRows(memory).filter((r) => r.trust >= floor);
|
|
4261
|
+
export function findContradictions(memory, { floor = CONTRADICTION_TRUST_FLOOR, factRows = null } = {}) {
|
|
4262
|
+
const rows = (factRows || readFactRows(memory)).filter((r) => r.trust >= floor);
|
|
4200
4263
|
const byKey = new Map();
|
|
4201
4264
|
for (const r of rows) {
|
|
4202
4265
|
if (resolutionStrategyFor(r.predicate) === RESOLUTION_MERGE) continue;
|
|
@@ -63,7 +63,7 @@ import { basename, join, resolve } from "node:path";
|
|
|
63
63
|
import { runTurn, uuidv7, stripLeadingDiscourseAdverb } from "./chat.mjs";
|
|
64
64
|
import { beginsWithVowelSound, grammarRules } from "./finish.mjs";
|
|
65
65
|
import { splitSentencesPreservingPaths, stripCitationResidue } from "./sentences.mjs";
|
|
66
|
-
import { loadMemory, readFactRows,
|
|
66
|
+
import { loadMemory, readFactRows, appendFacts, removeFacts } from "../adapters/memory/core.mjs";
|
|
67
67
|
import { loadConfig } from "../adapters/config.mjs";
|
|
68
68
|
import { touchedFactRows } from "../domain/memory/touched-facts.mjs";
|
|
69
69
|
import { normFactTerm } from "../domain/hash.mjs";
|
|
@@ -971,9 +971,14 @@ export async function ingestText(text, {
|
|
|
971
971
|
const subjects = new Set(rows.map((r) => r.subject));
|
|
972
972
|
if (subjects.size === 1) carrySubject = [...subjects][0];
|
|
973
973
|
const tag = `extracted:${sourceTag}`;
|
|
974
|
+
// One write for the whole sentence, not one per row: a write reads
|
|
975
|
+
// and re-derives the whole graph, so N of them cost N times what one
|
|
976
|
+
// carrying the same N rows does. The batch stays inside the sentence
|
|
977
|
+
// — a later sentence still reads everything the earlier ones wrote.
|
|
978
|
+
const writes = [];
|
|
974
979
|
for (const row of rows) {
|
|
975
980
|
const extraction = findingsForRow(row, readingFindings, identifierTerms);
|
|
976
|
-
|
|
981
|
+
writes.push({
|
|
977
982
|
subject: row.subject, predicate: row.predicate, object: row.object,
|
|
978
983
|
provenance: tag, quantifier: row.quantifier || "", observedAt,
|
|
979
984
|
...(extraction.length ? { extraction } : {}),
|
|
@@ -985,6 +990,7 @@ export async function ingestText(text, {
|
|
|
985
990
|
});
|
|
986
991
|
taggedIds.add(row.id);
|
|
987
992
|
}
|
|
993
|
+
await appendFacts(dir, writes);
|
|
988
994
|
continue;
|
|
989
995
|
}
|
|
990
996
|
const ungrounded = ungroundedTermsIn(lastDecline);
|
|
@@ -1009,15 +1015,17 @@ export async function ingestText(text, {
|
|
|
1009
1015
|
if (!keptCandidates.length) continue;
|
|
1010
1016
|
optimisticSentences += 1;
|
|
1011
1017
|
const tag = `optimistic-extract:${sourceTag}`;
|
|
1018
|
+
const candidateWrites = [];
|
|
1012
1019
|
for (const t of keptCandidates) {
|
|
1013
1020
|
const extraction = findingsForRow(t, mintFindings.has(t) ? [mintFindings.get(t)] : [], identifierTerms);
|
|
1014
|
-
|
|
1021
|
+
candidateWrites.push({
|
|
1015
1022
|
subject: t.subject, predicate: t.predicate, object: t.object, provenance: tag, observedAt,
|
|
1016
1023
|
...(extraction.length ? { extraction } : {}),
|
|
1017
1024
|
});
|
|
1018
1025
|
optimisticFacts.push({ ...t, provenance: tag, sentence, ...(extraction.length ? { extraction } : {}) });
|
|
1019
|
-
taggedIds.add(written.id);
|
|
1020
1026
|
}
|
|
1027
|
+
const { ids } = await appendFacts(dir, candidateWrites);
|
|
1028
|
+
for (const id of ids) taggedIds.add(id);
|
|
1021
1029
|
}
|
|
1022
1030
|
}
|
|
1023
1031
|
|
package/src/services/news.mjs
CHANGED
|
@@ -494,6 +494,27 @@ function mergeSnapshotsById(existing, incoming, cap) {
|
|
|
494
494
|
return { items, added };
|
|
495
495
|
}
|
|
496
496
|
|
|
497
|
+
const fetchedAtMs = (snapshot) => {
|
|
498
|
+
const ms = toMs(snapshot?.fetchedAt);
|
|
499
|
+
return Number.isFinite(ms) ? ms : 0;
|
|
500
|
+
};
|
|
501
|
+
|
|
502
|
+
/** True once a snapshot has been through a grounding round. `mergeSnapshotsById`
|
|
503
|
+
* files a snapshot the moment it arrives, so "known" and "grounded" are two
|
|
504
|
+
* different states and only this one means the facts landed. */
|
|
505
|
+
const isGroundedSnapshot = (snapshot) => (snapshot?.processedRounds || 0) > 0;
|
|
506
|
+
|
|
507
|
+
/** One source's fetched-but-not-yet-grounded snapshots, oldest first. A cycle
|
|
508
|
+
* that ran out of time leaves its backlog here, so the next cycle works
|
|
509
|
+
* through that before anything newer and the same article is never ingested
|
|
510
|
+
* twice. Ordered off the snapshots' own fields, so two cycles reading the same
|
|
511
|
+
* state take the same work in the same order. */
|
|
512
|
+
function pendingSnapshotsFor(items, sourceId) {
|
|
513
|
+
return (items || [])
|
|
514
|
+
.filter((snap) => snap?.sourceId === sourceId && !isGroundedSnapshot(snap))
|
|
515
|
+
.sort((a, b) => (fetchedAtMs(a) - fetchedAtMs(b)) || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
516
|
+
}
|
|
517
|
+
|
|
497
518
|
function emptyCycleAccumulator(at) {
|
|
498
519
|
return { at, sentences: 0, recognized: 0, optimisticCount: 0, factsAdded: 0, termsResolved: 0, derived: 0 };
|
|
499
520
|
}
|
|
@@ -548,21 +569,28 @@ export async function pollNewsSources(ctx) {
|
|
|
548
569
|
perSource.push({ sourceId, status: "failed" });
|
|
549
570
|
continue;
|
|
550
571
|
}
|
|
572
|
+
let added = [];
|
|
551
573
|
if (result.notModified) {
|
|
552
574
|
recordSuccess(health, nowVal, "not-modified");
|
|
553
|
-
|
|
554
|
-
|
|
575
|
+
} else {
|
|
576
|
+
recordSuccess(health, nowVal, "ok");
|
|
577
|
+
const merged = mergeSnapshotsById(state.items, result.items, config.itemCap);
|
|
578
|
+
state.items = merged.items;
|
|
579
|
+
added = merged.added;
|
|
580
|
+
newItemsTotal += added.length;
|
|
555
581
|
}
|
|
556
|
-
recordSuccess(health, nowVal, "ok");
|
|
557
|
-
const { items: mergedItems, added } = mergeSnapshotsById(state.items, result.items, config.itemCap);
|
|
558
|
-
state.items = mergedItems;
|
|
559
|
-
newItemsTotal += added.length;
|
|
560
582
|
|
|
583
|
+
// Everything this source has fetched and not yet grounded, not just what
|
|
584
|
+
// arrived on this fetch: a source that answers 304 still has a backlog to
|
|
585
|
+
// finish, and an aborted cycle's leftovers would otherwise sit in the state
|
|
586
|
+
// marked known and never be read again.
|
|
561
587
|
const before = emptyCycleAccumulator(nowVal);
|
|
562
588
|
const after = emptyCycleAccumulator(nowVal);
|
|
563
|
-
|
|
589
|
+
let grounded = 0;
|
|
590
|
+
for (const snapshot of pendingSnapshotsFor(state.items, sourceId)) {
|
|
564
591
|
if (shouldAbort()) { aborted = true; break; }
|
|
565
592
|
const r = await ingestNewsSnapshot(ctx, snapshot);
|
|
593
|
+
grounded += 1;
|
|
566
594
|
after.sentences += r.sentences;
|
|
567
595
|
after.recognized += r.recognized;
|
|
568
596
|
after.optimisticCount += r.optimisticCount;
|
|
@@ -571,10 +599,17 @@ export async function pollNewsSources(ctx) {
|
|
|
571
599
|
factsTotal += r.facts;
|
|
572
600
|
derivedTotal += r.derived;
|
|
573
601
|
}
|
|
574
|
-
if (
|
|
602
|
+
if (grounded) {
|
|
575
603
|
state.metrics = [...(state.metrics || []), cycleMetrics(before, after, { source: sourceId })];
|
|
576
604
|
}
|
|
577
|
-
|
|
605
|
+
const pendingLeft = pendingSnapshotsFor(state.items, sourceId).length;
|
|
606
|
+
perSource.push({
|
|
607
|
+
sourceId,
|
|
608
|
+
status: result.notModified ? "not-modified" : "ok",
|
|
609
|
+
newItems: added.length,
|
|
610
|
+
grounded,
|
|
611
|
+
pending: pendingLeft,
|
|
612
|
+
});
|
|
578
613
|
if (aborted) break;
|
|
579
614
|
}
|
|
580
615
|
|