@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.
Files changed (37) hide show
  1. package/corpus/worlds/manifest.json +9 -9
  2. package/corpus/worlds/shards/town-square-chapel.jsonl.gz +0 -0
  3. package/corpus/worlds/shards/town-square-market.jsonl.gz +0 -0
  4. package/corpus/worlds/shards/town-square.jsonl.gz +0 -0
  5. package/corpus/worlds/src/town-square-chapel.jsonl +1 -1
  6. package/corpus/worlds/src/town-square-market.jsonl +1 -1
  7. package/corpus/worlds/src/town-square.jsonl +1 -1
  8. package/data/mudiii-assets.json +35 -5
  9. package/package.json +1 -1
  10. package/src/adapters/memory/core.mjs +236 -18
  11. package/src/domain/ask-vocab.mjs +1 -1
  12. package/src/domain/ask.mjs +20 -10
  13. package/src/domain/game-config.mjs +10 -1
  14. package/src/domain/interpret/normalize.mjs +4 -0
  15. package/src/domain/memory/retraction.mjs +232 -0
  16. package/src/domain/p2p/sync-filter.mjs +9 -1
  17. package/src/domain/spider-fly-world.mjs +80 -35
  18. package/src/domain/syllogise.mjs +128 -75
  19. package/src/services/adventure-autoplay.mjs +2 -2
  20. package/src/services/adventure-viz.mjs +12 -7
  21. package/src/services/chat.mjs +42 -14
  22. package/src/services/mud-turn.mjs +1 -1
  23. package/src/services/mud-viz.mjs +11 -2
  24. package/src/services/mudiii-scene.mjs +199 -55
  25. package/src/services/mudiii-turn.mjs +82 -1
  26. package/src/services/mudiii-viz.mjs +17 -7
  27. package/src/services/p2p-room.mjs +130 -9
  28. package/src/services/predator-prey.mjs +524 -69
  29. package/src/services/spider-fly-turn.mjs +128 -56
  30. package/src/services/spider-fly-viz.mjs +65 -71
  31. package/src/services/world-teach.mjs +3 -3
  32. package/src/surfaces/web/adventure-browser-entry.mjs +12 -3
  33. package/src/surfaces/web/memory-ask-browser.bundle.js +118 -118
  34. package/src/surfaces/web/mud-browser-entry.mjs +10 -3
  35. package/src/surfaces/web/mudiii-browser-entry.mjs +3 -3
  36. package/src/surfaces/web/spider-fly-browser-entry.mjs +26 -22
  37. 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 { appendFacts, loadMemory, loadNodeId, saveNodeId, readFactRows, normFactTerm, FACT_CLASS } from "../adapters/memory/core.mjs";
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
- cachedRows = readFactRows(await loadMemory(memoryDir));
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) => toWireFacts(row, wireIdentity(), timestamp));
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
- const { ids } = await appendFacts(memoryDir, accepted.map((f) => ({
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
- emit(factsListeners, { merged: ids.length, rows: cachedRows });
364
- return { merged: ids.length };
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).flatMap((row) => toWireFacts(row, wireIdentity(), timestamp));
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).flatMap((row) => toWireFacts(row, wireIdentity(), wireTimestamp));
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());