@polycode-projects/the-mechanical-code-talker 5.0.2 → 5.0.4
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/corpus/worlds/manifest.json +9 -9
- package/corpus/worlds/shards/town-square-chapel.jsonl.gz +0 -0
- package/corpus/worlds/shards/town-square-market.jsonl.gz +0 -0
- package/corpus/worlds/shards/town-square.jsonl.gz +0 -0
- package/corpus/worlds/src/town-square-chapel.jsonl +1 -1
- package/corpus/worlds/src/town-square-market.jsonl +1 -1
- package/corpus/worlds/src/town-square.jsonl +1 -1
- package/data/mudiii-assets.json +35 -5
- package/package.json +1 -1
- package/src/adapters/memory/core.mjs +236 -18
- package/src/domain/ask-vocab.mjs +1 -1
- package/src/domain/ask.mjs +20 -10
- package/src/domain/game-config.mjs +10 -1
- package/src/domain/interpret/normalize.mjs +4 -0
- package/src/domain/memory/retraction.mjs +232 -0
- package/src/domain/p2p/sync-filter.mjs +9 -1
- package/src/domain/spider-fly-world.mjs +80 -35
- package/src/domain/syllogise.mjs +128 -75
- package/src/services/adventure-autoplay.mjs +2 -2
- package/src/services/adventure-viz.mjs +12 -7
- package/src/services/chat.mjs +42 -14
- package/src/services/mud-turn.mjs +1 -1
- package/src/services/mud-viz.mjs +11 -2
- package/src/services/mudiii-scene.mjs +199 -55
- package/src/services/mudiii-turn.mjs +82 -1
- package/src/services/mudiii-viz.mjs +17 -7
- package/src/services/p2p-room.mjs +130 -9
- package/src/services/predator-prey.mjs +524 -69
- package/src/services/spider-fly-turn.mjs +128 -56
- package/src/services/spider-fly-viz.mjs +65 -71
- package/src/services/world-teach.mjs +3 -3
- package/src/surfaces/web/adventure-browser-entry.mjs +12 -3
- package/src/surfaces/web/memory-ask-browser.bundle.js +118 -118
- package/src/surfaces/web/mud-browser-entry.mjs +10 -3
- package/src/surfaces/web/mudiii-browser-entry.mjs +3 -3
- package/src/surfaces/web/spider-fly-browser-entry.mjs +26 -22
- package/src/services/spider-fly.mjs +0 -943
|
@@ -30,7 +30,13 @@ import {
|
|
|
30
30
|
WAVED_PREDICATE,
|
|
31
31
|
} from "../domain/p2p/facts.mjs";
|
|
32
32
|
import { generateNodeId } from "../domain/p2p/peer-id.mjs";
|
|
33
|
-
import {
|
|
33
|
+
import {
|
|
34
|
+
RETRACTION_PREDICATE, encodeRetractionValue, decodeRetractionValue,
|
|
35
|
+
} from "../domain/memory/retraction.mjs";
|
|
36
|
+
import {
|
|
37
|
+
appendFacts, appendRetractions, loadMemory, loadNodeId, saveNodeId,
|
|
38
|
+
readFactRows, readRetractions, normFactTerm, factGroupId, factRecordIdForTag, FACT_CLASS,
|
|
39
|
+
} from "../adapters/memory/core.mjs";
|
|
34
40
|
|
|
35
41
|
export const ROOM_IDLE = "idle";
|
|
36
42
|
export const ROOM_SHARING = "sharing";
|
|
@@ -156,6 +162,40 @@ const toWireFacts = (row, identity, fallbackTimestamp) => {
|
|
|
156
162
|
}));
|
|
157
163
|
};
|
|
158
164
|
|
|
165
|
+
/** The record ids one provenance string keys once its tags have been relabelled
|
|
166
|
+
* for the wire. Two stores file one assertion under two ids — here under the
|
|
167
|
+
* chat session that taught it, at the peer under the node that sent it — so a
|
|
168
|
+
* retraction that named only the ids it sees locally would bite on one store
|
|
169
|
+
* and miss the other. Naming both is what makes it land either way. */
|
|
170
|
+
function broadcastRecordIds(groupId, provenance, identity, fallbackTimestamp) {
|
|
171
|
+
const ids = [];
|
|
172
|
+
for (const { tag } of wireProvenanceTags(provenance, identity, fallbackTimestamp)) {
|
|
173
|
+
if (tag) ids.push(factRecordIdForTag(groupId, tag));
|
|
174
|
+
}
|
|
175
|
+
return ids;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** A retraction goes out as ONE fact carrying every tag, not one per tag. The
|
|
179
|
+
* per-tag split exists because appendFacts unions an incoming tag against the
|
|
180
|
+
* ones it stores; a retraction merges through its own union instead, and the
|
|
181
|
+
* tags it carries are already relabel fixpoints. */
|
|
182
|
+
const toWireRetraction = (record, identity, fallbackTimestamp) => {
|
|
183
|
+
const groupId = factGroupId(record.subject);
|
|
184
|
+
const { retractedAt, ids } = decodeRetractionValue(record.object);
|
|
185
|
+
const tags = wireProvenanceTags(record.provenance, identity, fallbackTimestamp)
|
|
186
|
+
.map(({ tag }) => tag)
|
|
187
|
+
.filter(Boolean);
|
|
188
|
+
return {
|
|
189
|
+
subject: record.subject,
|
|
190
|
+
predicate: RETRACTION_PREDICATE,
|
|
191
|
+
object: encodeRetractionValue(retractedAt, [
|
|
192
|
+
...ids,
|
|
193
|
+
...broadcastRecordIds(groupId, record.provenance, identity, fallbackTimestamp),
|
|
194
|
+
]),
|
|
195
|
+
provenance: tags.join(" | "),
|
|
196
|
+
};
|
|
197
|
+
};
|
|
198
|
+
|
|
159
199
|
/** A room: one shared world, one local fact store, and however many direct
|
|
160
200
|
* peer connections have been made into it.
|
|
161
201
|
*
|
|
@@ -219,6 +259,14 @@ export function createP2pRoom({
|
|
|
219
259
|
// fact that arrived from a peer being re-broadcast as if we had authored it.
|
|
220
260
|
const seenProvenanceById = new Map();
|
|
221
261
|
let cachedRows = [];
|
|
262
|
+
// A retraction is not a fact row — readFactRows can never return one, because
|
|
263
|
+
// the whole point of the record is that the rows it names are gone. So it
|
|
264
|
+
// gets its own cache and its own diff, keyed on what it actually carries: the
|
|
265
|
+
// ids it suppressed. Those grow by union as peers merge, and a grown record
|
|
266
|
+
// is a change worth broadcasting even though its provenance never moved.
|
|
267
|
+
let cachedRetractions = [];
|
|
268
|
+
const seenRetractionValueById = new Map();
|
|
269
|
+
const retractionDiffValue = (fact) => `${fact.provenance}${fact.object}`;
|
|
222
270
|
|
|
223
271
|
// Store-touching work runs one job at a time, in arrival order. Every path
|
|
224
272
|
// that reads or writes memoryDir/seenProvenanceById/cachedRows crosses at
|
|
@@ -263,10 +311,53 @@ export function createP2pRoom({
|
|
|
263
311
|
}
|
|
264
312
|
|
|
265
313
|
async function refreshRows() {
|
|
266
|
-
|
|
314
|
+
const memory = await loadMemory(memoryDir);
|
|
315
|
+
cachedRows = readFactRows(memory);
|
|
316
|
+
cachedRetractions = readRetractions(memory);
|
|
267
317
|
return cachedRows;
|
|
268
318
|
}
|
|
269
319
|
|
|
320
|
+
const toWire = (row, identity, timestamp) => (row.predicate === RETRACTION_PREDICATE
|
|
321
|
+
? [toWireRetraction(row, identity, timestamp)]
|
|
322
|
+
: toWireFacts(row, identity, timestamp));
|
|
323
|
+
|
|
324
|
+
/** Fold the broadcast form of this node's own retractions back into the store.
|
|
325
|
+
* A retraction made here names the ids this store files the assertion under;
|
|
326
|
+
* a peer relaying that same assertion back sends it under the id the RELABEL
|
|
327
|
+
* produces. Merging the broadcast form in is what makes the local refusal
|
|
328
|
+
* cover it, and it is a union, so it settles after one pass. */
|
|
329
|
+
async function alignOwnRetractions() {
|
|
330
|
+
if (!cachedRetractions.length) return;
|
|
331
|
+
const identity = wireIdentity();
|
|
332
|
+
const timestamp = now();
|
|
333
|
+
const expanded = cachedRetractions.map((record) => toWireRetraction(record, identity, timestamp));
|
|
334
|
+
if (expanded.every((wire, i) => wire.object === cachedRetractions[i].object)) return;
|
|
335
|
+
await appendRetractions(memoryDir, expanded);
|
|
336
|
+
await refreshRows();
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/** The other side of the same re-keying: a retraction that names this node's
|
|
340
|
+
* broadcast id for an assertion also names whatever id this store files that
|
|
341
|
+
* same assertion under. Without it a peer could retract a fact it learned
|
|
342
|
+
* from here and the origin would keep reading its own copy. */
|
|
343
|
+
function localiseRetraction(fact) {
|
|
344
|
+
const groupId = factGroupId(fact.subject);
|
|
345
|
+
const { retractedAt, ids } = decodeRetractionValue(fact.object);
|
|
346
|
+
const named = new Set(ids);
|
|
347
|
+
const row = cachedRows.find((r) => r.id === groupId);
|
|
348
|
+
if (!row) return fact;
|
|
349
|
+
const identity = wireIdentity();
|
|
350
|
+
const timestamp = now();
|
|
351
|
+
const alsoNamed = [];
|
|
352
|
+
for (const assertion of row.assertions || []) {
|
|
353
|
+
if (named.has(assertion.id)) continue;
|
|
354
|
+
const asBroadcast = broadcastRecordIds(groupId, assertion.provenance, identity, timestamp);
|
|
355
|
+
if (asBroadcast.some((id) => named.has(id))) alsoNamed.push(assertion.id);
|
|
356
|
+
}
|
|
357
|
+
if (!alsoNamed.length) return fact;
|
|
358
|
+
return { ...fact, object: encodeRetractionValue(retractedAt, [...ids, ...alsoNamed]) };
|
|
359
|
+
}
|
|
360
|
+
|
|
270
361
|
/** readFactRows walks `individuals` in array order and the fold reads them in
|
|
271
362
|
* that order, so two peers holding an identical fact set can still fold it
|
|
272
363
|
* differently while their arrival orders differ. Sorting the Fact
|
|
@@ -293,6 +384,7 @@ export function createP2pRoom({
|
|
|
293
384
|
async function baselineSeen() {
|
|
294
385
|
await refreshRows();
|
|
295
386
|
for (const row of cachedRows) seenProvenanceById.set(row.id, row.provenance);
|
|
387
|
+
for (const fact of cachedRetractions) seenRetractionValueById.set(fact.id, retractionDiffValue(fact));
|
|
296
388
|
}
|
|
297
389
|
|
|
298
390
|
async function ensureStarted() {
|
|
@@ -322,17 +414,24 @@ export function createP2pRoom({
|
|
|
322
414
|
async function flushLocalChange() {
|
|
323
415
|
await ensureStarted();
|
|
324
416
|
await refreshRows();
|
|
417
|
+
await alignOwnRetractions();
|
|
325
418
|
const changed = [];
|
|
326
419
|
for (const row of cachedRows) {
|
|
327
420
|
if (seenProvenanceById.get(row.id) === row.provenance) continue;
|
|
328
421
|
seenProvenanceById.set(row.id, row.provenance);
|
|
329
422
|
changed.push(row);
|
|
330
423
|
}
|
|
424
|
+
for (const fact of cachedRetractions) {
|
|
425
|
+
const value = retractionDiffValue(fact);
|
|
426
|
+
if (seenRetractionValueById.get(fact.id) === value) continue;
|
|
427
|
+
seenRetractionValueById.set(fact.id, value);
|
|
428
|
+
changed.push(fact);
|
|
429
|
+
}
|
|
331
430
|
if (!changed.length) return { broadcast: 0 };
|
|
332
431
|
const targets = connectedPeers();
|
|
333
432
|
if (!targets.length) return { broadcast: 0 };
|
|
334
433
|
const timestamp = now();
|
|
335
|
-
const facts = changed.flatMap((row) =>
|
|
434
|
+
const facts = changed.flatMap((row) => toWire(row, wireIdentity(), timestamp));
|
|
336
435
|
broadcast(opMessage({ from: myPeerId, facts }));
|
|
337
436
|
return { broadcast: changed.length };
|
|
338
437
|
}
|
|
@@ -343,7 +442,17 @@ export function createP2pRoom({
|
|
|
343
442
|
// Flush first: a local fact still waiting to be diffed would otherwise be
|
|
344
443
|
// recorded as merged below and never leave this browser.
|
|
345
444
|
await flushLocalChange();
|
|
346
|
-
|
|
445
|
+
// Retractions land BEFORE the assertions in the same batch, so a peer
|
|
446
|
+
// sending both the fact and the record that suppresses it lands them in the
|
|
447
|
+
// order that leaves the fact out. Either order converges — the read fold
|
|
448
|
+
// strips a record a retraction covers however late the retraction arrives —
|
|
449
|
+
// but doing it this way keeps a suppressed record from being written at all.
|
|
450
|
+
const retractions = accepted.filter((f) => f.predicate === RETRACTION_PREDICATE);
|
|
451
|
+
if (retractions.length) await appendRetractions(memoryDir, retractions.map(localiseRetraction));
|
|
452
|
+
const assertions = retractions.length
|
|
453
|
+
? accepted.filter((f) => f.predicate !== RETRACTION_PREDICATE)
|
|
454
|
+
: accepted;
|
|
455
|
+
const { ids } = assertions.length ? await appendFacts(memoryDir, assertions.map((f) => ({
|
|
347
456
|
subject: f.subject,
|
|
348
457
|
predicate: f.predicate,
|
|
349
458
|
object: f.object,
|
|
@@ -353,15 +462,21 @@ export function createP2pRoom({
|
|
|
353
462
|
// this is the passthrough that lets a dated taught fact's wire fact
|
|
354
463
|
// reach it at all.
|
|
355
464
|
...(typeof f.observedAt === "string" && f.observedAt ? { observedAt: f.observedAt } : {}),
|
|
356
|
-
})));
|
|
465
|
+
}))) : { ids: [] };
|
|
357
466
|
await sortFactIndividualsById();
|
|
358
467
|
await refreshRows();
|
|
359
468
|
const mergedIds = new Set(ids);
|
|
360
469
|
for (const row of cachedRows) {
|
|
361
470
|
if (mergedIds.has(row.id)) seenProvenanceById.set(row.id, row.provenance);
|
|
362
471
|
}
|
|
363
|
-
|
|
364
|
-
|
|
472
|
+
// Same reason the fact baseline above exists: a retraction that arrived
|
|
473
|
+
// from a peer must not go back out as though this node had authored it.
|
|
474
|
+
// Every peer broadcasts its OWN record when it makes one, so the union each
|
|
475
|
+
// peer ends up with is reached from the originals rather than from relays.
|
|
476
|
+
for (const fact of cachedRetractions) seenRetractionValueById.set(fact.id, retractionDiffValue(fact));
|
|
477
|
+
const merged = ids.length + retractions.length;
|
|
478
|
+
emit(factsListeners, { merged, rows: cachedRows });
|
|
479
|
+
return { merged };
|
|
365
480
|
}
|
|
366
481
|
|
|
367
482
|
function registerPeer(peerId, peerDisplayName, transport) {
|
|
@@ -436,8 +551,10 @@ export function createP2pRoom({
|
|
|
436
551
|
case "sync-request": {
|
|
437
552
|
const facts = await withStore(async () => {
|
|
438
553
|
await refreshRows();
|
|
554
|
+
await alignOwnRetractions();
|
|
439
555
|
const timestamp = now();
|
|
440
|
-
return syncableFacts(cachedRows
|
|
556
|
+
return syncableFacts([...cachedRows, ...cachedRetractions])
|
|
557
|
+
.flatMap((row) => toWire(row, wireIdentity(), timestamp));
|
|
441
558
|
});
|
|
442
559
|
send(transport, syncResponseMessage({ facts }));
|
|
443
560
|
return;
|
|
@@ -644,7 +761,9 @@ export function createP2pRoom({
|
|
|
644
761
|
// from reading to every peer as a brand-new node.
|
|
645
762
|
nodeId = await resolveStoreNodeId(memoryDir, nodeId || myNodeId);
|
|
646
763
|
seenProvenanceById.clear();
|
|
764
|
+
seenRetractionValueById.clear();
|
|
647
765
|
cachedRows = [];
|
|
766
|
+
cachedRetractions = [];
|
|
648
767
|
const timestamp = now();
|
|
649
768
|
const identity = [];
|
|
650
769
|
if (worldId && worldName) identity.push(worldNameFact(worldId, worldName, timestamp));
|
|
@@ -655,11 +774,13 @@ export function createP2pRoom({
|
|
|
655
774
|
}
|
|
656
775
|
await refreshRows();
|
|
657
776
|
for (const row of cachedRows) seenProvenanceById.set(row.id, row.provenance);
|
|
777
|
+
for (const fact of cachedRetractions) seenRetractionValueById.set(fact.id, retractionDiffValue(fact));
|
|
658
778
|
const targets = connectedPeers();
|
|
659
779
|
let pushed = 0;
|
|
660
780
|
if (targets.length) {
|
|
661
781
|
const wireTimestamp = now();
|
|
662
|
-
const facts = syncableFacts(cachedRows
|
|
782
|
+
const facts = syncableFacts([...cachedRows, ...cachedRetractions])
|
|
783
|
+
.flatMap((row) => toWire(row, wireIdentity(), wireTimestamp));
|
|
663
784
|
pushed = facts.length;
|
|
664
785
|
if (facts.length) broadcast(opMessage({ from: myPeerId, facts }));
|
|
665
786
|
broadcast(syncRequestMessage());
|