@polycode-projects/the-mechanical-code-talker 4.1.0 → 4.1.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.
Files changed (60) hide show
  1. package/README.md +31 -18
  2. package/bin/tmct.mjs +3 -0
  3. package/data/templates/responses.jsonl +3 -0
  4. package/package.json +2 -1
  5. package/src/adapters/memory/core.mjs +1358 -196
  6. package/src/adapters/memory/inspect.mjs +11 -0
  7. package/src/adapters/memory/shacl.mjs +38 -0
  8. package/src/adapters/p2p/webrtc-transport.mjs +28 -5
  9. package/src/domain/ask-vocab.mjs +39 -0
  10. package/src/domain/ask.mjs +183 -34
  11. package/src/domain/grammar/assert.mjs +8 -2
  12. package/src/domain/hanoi-board.mjs +232 -0
  13. package/src/domain/ingest-facts.mjs +120 -0
  14. package/src/domain/interpret/normalize.mjs +49 -0
  15. package/src/domain/memory/compaction.mjs +284 -0
  16. package/src/domain/memory/resolution.mjs +171 -0
  17. package/src/domain/memory/trust.mjs +175 -5
  18. package/src/domain/memory-facts.mjs +139 -0
  19. package/src/domain/p2p/facts.mjs +21 -0
  20. package/src/domain/p2p/peer-id.mjs +15 -0
  21. package/src/domain/p2p/provenance-relabel.mjs +13 -2
  22. package/src/domain/p2p/sync-filter.mjs +5 -1
  23. package/src/domain/p2p/wire.mjs +7 -4
  24. package/src/domain/scene-compose.mjs +2 -2
  25. package/src/domain/sprite-facts.mjs +0 -0
  26. package/src/domain/sprite-request.mjs +1 -1
  27. package/src/services/adventure-viz.mjs +43 -39
  28. package/src/services/adventure.mjs +70 -44
  29. package/src/services/chat-page-viz.mjs +403 -332
  30. package/src/services/chat.mjs +274 -156
  31. package/src/services/code-explorer-viz.mjs +142 -55
  32. package/src/services/index.mjs +1 -1
  33. package/src/services/ingest-viz.mjs +153 -28
  34. package/src/services/ledger-viz.mjs +47 -47
  35. package/src/services/memory-panel-viz.mjs +8 -3
  36. package/src/services/mud-turn.mjs +11 -8
  37. package/src/services/mud-viz.mjs +474 -234
  38. package/src/services/p2p-room.mjs +110 -23
  39. package/src/services/plan-viz.mjs +72 -13
  40. package/src/services/research-viz.mjs +35 -24
  41. package/src/services/share-overlay-viz.mjs +623 -0
  42. package/src/services/spider-fly-viz.mjs +24 -24
  43. package/src/services/sprite-catalog-viz.mjs +307 -82
  44. package/src/surfaces/web/adventure-browser-entry.mjs +46 -25
  45. package/src/surfaces/web/chat-browser-entry.mjs +55 -9
  46. package/src/surfaces/web/code-explorer-browser-entry.mjs +30 -16
  47. package/src/surfaces/web/engine-surface.mjs +82 -0
  48. package/src/surfaces/web/ingest-browser-entry.mjs +81 -11
  49. package/src/surfaces/web/ledger-browser-entry.mjs +47 -14
  50. package/src/surfaces/web/memory-ask-browser-entry.mjs +55 -13
  51. package/src/surfaces/web/memory-ask-browser.bundle.js +149 -116
  52. package/src/surfaces/web/mud-browser-entry.mjs +77 -25
  53. package/src/surfaces/web/p2p-browser-entry.mjs +1 -1
  54. package/src/surfaces/web/plan-browser-entry.mjs +49 -11
  55. package/src/surfaces/web/research-browser-entry.mjs +33 -24
  56. package/src/surfaces/web/spider-fly-browser-entry.mjs +32 -13
  57. package/src/surfaces/web/sprites-browser-entry.mjs +51 -11
  58. package/src/surfaces/web/tmct-surface.mjs +159 -0
  59. package/src/surfaces/web/turn-session.mjs +16 -5
  60. package/src/tools/server.mjs +13 -6
@@ -145,13 +145,16 @@ const trustTierFor = (trust) => (trust >= 0.85 ? 3 : trust >= 0.5 ? 2 : 1);
145
145
  * build-demo-site.mjs consumes directly). Returns
146
146
  * { rows, terms, edges, focus, contradictions, worthALook, payload, meta }. */
147
147
  export function computeLedgerDataFromPayload(payload, { focus, term, rowLimit = LEDGER_ROW_LIMIT_DEFAULT } = {}) {
148
- const individuals = payload?.individuals || [];
149
- const indById = new Map(individuals.map((i) => [i?.id, i]));
150
148
  const factRows = readFactRows(payload);
151
149
 
152
150
  const rows = factRows.map((r) => {
153
- const ind = indById.get(r.id);
154
- const createdAt = (ind?.attributes || []).find((a) => a?.key === "createdAt")?.value || "";
151
+ // A row is now a GROUP of one-or-more per-source assertion records
152
+ // (PLAN_FACT.md's re-key), so there is no single individual to join
153
+ // against by r.id any more — r.id is the group's own id, distinct from
154
+ // every member record's id. The newest assertion's own createdAt is what
155
+ // "when was this learned" means for a row the ledger already sorts
156
+ // newest-first.
157
+ const createdAt = (r.assertions || []).reduce((newest, a) => (a.createdAt > newest ? a.createdAt : newest), "");
155
158
  return {
156
159
  id: r.id, s: r.subject, p: r.predicate, o: r.object,
157
160
  phrase: phraseFor(r.predicate),
@@ -549,7 +552,7 @@ function sparkCaptionHtml(stats) {
549
552
  * self-contained document with no external requests. Only
550
553
  * scripts/build-demo-site.mjs, which builds the sibling bundle itself
551
554
  * first, passes `true`. The dock's own runtime code below ALSO gates on
552
- * `typeof tmctLedger !== "undefined"` regardless — the two checks answer
555
+ * `typeof tmct !== "undefined"` regardless — the two checks answer
553
556
  * different questions (did this render even offer the reference; did the
554
557
  * browser actually manage to load it), and both must hold for the live
555
558
  * path to run. */
@@ -564,7 +567,7 @@ export function renderLedgerHtml({ rows, terms, edges, focus, contradictions, wo
564
567
  // really in this payload, otherwise a real term from this graph. Left as
565
568
  // the query-only wording even when the live bundle is offered — the dock's
566
569
  // own script swaps it for a teach-aware placeholder the moment it confirms
567
- // tmctLedger actually loaded (never claimed ahead of that confirmation).
570
+ // the live engine actually loaded (never claimed ahead of that confirmation).
568
571
  // The example term skips anything under 3 characters — the SAME floor the
569
572
  // dock's own miss-tips apply below — so a graph whose highest-degree term
570
573
  // is a short stopword-shaped fragment (init:large corpora carry plenty:
@@ -575,7 +578,7 @@ export function renderLedgerHtml({ rows, terms, edges, focus, contradictions, wo
575
578
  ? "who is the grandfather of ishmael"
576
579
  : (exampleTerm ? `ask the graph… e.g. what is ${exampleTerm.term}` : "ask the graph…");
577
580
  // The paste-and-drop ingest panel plus the JSONL export ride the LIVE
578
- // engine (window.tmctLedger): both teach into and read the dock's own
581
+ // engine (window.tmct): both teach into and read the dock's own
579
582
  // session store, so they appear only where that bundle is offered — the
580
583
  // demo site, never the CLI's self-contained tmct viz page.
581
584
  const ingestHtml = ledgerBundleAvailable
@@ -888,14 +891,13 @@ ${ledgerBundleAvailable ? `<script src="./ledger-browser.bundle.js"></script>` :
888
891
  // exists it points at that session's store, which teach/research grow in
889
892
  // place, so a digest of a just-taught term reads the fresh facts.
890
893
  let getLivePayload = () => PAYLOAD;
891
- // Whichever loaded bundle carries the browser digest helper — the demo
892
- // ledger's own live engine (tmctLedger) or the committed memory-ask engine
893
- // the CLI viz page inlines (tmctMemoryAsk). Null before either loads, and the
894
- // focus card then keeps whatever server-computed digest it shipped.
894
+ // The browser digest helper, from whichever engine this page loaded — the
895
+ // demo ledger's live one or the committed query-only one the CLI viz page
896
+ // inlines. Both publish it in the same place now, so there is one lookup
897
+ // rather than two. Null before either loads, and the focus card then keeps
898
+ // whatever server-computed digest it shipped.
895
899
  const digestHelper = () =>
896
- (typeof tmctLedger !== "undefined" && tmctLedger && tmctLedger.digestTermFromPayloadBrowser)
897
- || (typeof tmctMemoryAsk !== "undefined" && tmctMemoryAsk && tmctMemoryAsk.digestTermFromPayloadBrowser)
898
- || null;
900
+ (typeof tmct !== "undefined" && tmct.page && tmct.page.digestTermFromPayloadBrowser) || null;
899
901
  // The digest for one term, computed live in the browser from the embedded
900
902
  // structure table, or null when no structures were embedded, no engine has
901
903
  // loaded, or the term holds nothing to compose.
@@ -1138,9 +1140,9 @@ ${ledgerBundleAvailable ? `<script src="./ledger-browser.bundle.js"></script>` :
1138
1140
  }
1139
1141
 
1140
1142
  // ---- the chat dock: LIVE teach+ask over the sibling ledger-browser.bundle.js
1141
- // (window.tmctLedger) when the demo build offered it AND it actually
1142
- // loaded; falls back to the read-only tmctMemoryAsk engine below
1143
- // unchanged from before this page could teach at all — otherwise.
1143
+ // (window.tmct) when the demo build offered it AND it actually
1144
+ // loaded; falls back to the read-only engine below, which publishes the
1145
+ // same surface as a stand-in and says so through tmct.fallback.
1144
1146
  // bin/tmct.mjs's own \`tmct viz\` output never offers the live bundle
1145
1147
  // (renderLedgerHtml's own ledgerBundleAvailable defaults false there), so
1146
1148
  // this branch is simply never reachable on a CLI-generated page.
@@ -1148,7 +1150,7 @@ ${ledgerBundleAvailable ? `<script src="./ledger-browser.bundle.js"></script>` :
1148
1150
  const createTicker = ${createTicker.toString()};
1149
1151
  const prefersReducedMotion = ${prefersReducedMotion.toString()};
1150
1152
  const chatForm = el("chatform");
1151
- if (chatForm && typeof tmctLedger !== "undefined" && typeof tmctLedger.createLedgerSession === "function") {
1153
+ if (chatForm && typeof tmct !== "undefined" && !tmct.fallback) {
1152
1154
  const log = el("chatlog");
1153
1155
  const chatqEl = el("chatq");
1154
1156
  chatqEl.placeholder = 'ask or teach the graph\\u2026 e.g. "blue is a peg"';
@@ -1183,7 +1185,7 @@ ${ledgerBundleAvailable ? `<script src="./ledger-browser.bundle.js"></script>` :
1183
1185
  import("./vendor/wink.js"),
1184
1186
  winkTimeout(WINK_LOAD_TIMEOUT_MS, "wink vendor asset load timed out"),
1185
1187
  ]);
1186
- tmctLedger.registerWinkModel(() => ({ winkNLP: mod.winkNLP, model: mod.model }));
1188
+ tmct.page.registerWinkModel(() => ({ winkNLP: mod.winkNLP, model: mod.model }));
1187
1189
  } catch (err) {
1188
1190
  // eslint-disable-next-line no-console
1189
1191
  console.warn("tmct ledger: the wink vendor asset failed to load, continuing without the lemma/POS tier", err);
@@ -1196,7 +1198,7 @@ ${ledgerBundleAvailable ? `<script src="./ledger-browser.bundle.js"></script>` :
1196
1198
  async function ensureSession() {
1197
1199
  if (session) return session;
1198
1200
  await tryLoadWink();
1199
- session = await tmctLedger.createLedgerSession({ seedPayload: PAYLOAD });
1201
+ session = await tmct.open({ seedPayload: PAYLOAD });
1200
1202
  // From here the live store is the source of truth for the digest, so a
1201
1203
  // digest of a term taught this session reads its fresh facts — the store
1202
1204
  // grows in place, so this one closure stays current.
@@ -1214,15 +1216,15 @@ ${ledgerBundleAvailable ? `<script src="./ledger-browser.bundle.js"></script>` :
1214
1216
  chatqEl.disabled = true;
1215
1217
  withLock(async () => {
1216
1218
  try {
1217
- const s = await ensureSession();
1218
- const result = await s.turn(q);
1219
+ const live = await ensureSession();
1220
+ const result = await tmct.turn(q);
1219
1221
  const record = result.record;
1220
1222
  const taught = !!record && record.miss === false && (record.via === "assert" || record.via === "retract");
1221
1223
  const body = esc(result.answer).replace(/\\n/g, "<br>");
1222
1224
  pending.className = "a" + (taught ? " taught" : (record && record.miss ? " miss" : ""));
1223
1225
  pending.innerHTML = taught ? '<span class="tag">taught</span>' + body : body;
1224
1226
  if (taught) {
1225
- const fresh = tmctLedger.computeLedgerDataFromPayload(s.memoryDir.payload, {});
1227
+ const fresh = tmct.page.computeLedgerDataFromPayload(live.memoryDir.payload, {});
1226
1228
  applyLedgerData(fresh);
1227
1229
  } else if (!(record && record.miss)) {
1228
1230
  // Only a genuine answer (never a miss) tries to resolve a
@@ -1231,7 +1233,7 @@ ${ledgerBundleAvailable ? `<script src="./ledger-browser.bundle.js"></script>` :
1231
1233
  // ordinary English word (e.g. "run", if the graph happens to
1232
1234
  // hold it), and resolveAnsweredTerm has no way to tell that
1233
1235
  // apart from the term genuinely being discussed.
1234
- const hit = resolveAnsweredTerm(result.answer, q, LEDGER.terms, tmctLedger.normFactTerm);
1236
+ const hit = resolveAnsweredTerm(result.answer, q, LEDGER.terms, tmct.page.normFactTerm);
1235
1237
  if (hit) refocusWithLabel(hit, q);
1236
1238
  }
1237
1239
  } catch {
@@ -1246,7 +1248,7 @@ ${ledgerBundleAvailable ? `<script src="./ledger-browser.bundle.js"></script>` :
1246
1248
 
1247
1249
  // ---- ingest + export: bring your own text, take the graph away ---------
1248
1250
  // The ingest panel runs pasted/dropped/browsed text through the SAME dock
1249
- // session, one sentence at a time (tmctLedger.splitSentences, then
1251
+ // session, one sentence at a time (tmct.page.splitSentences, then
1250
1252
  // s.turn), keeping only the sentences the recognizer grounds — then
1251
1253
  // re-derives the whole ledger so the new facts can be examined in place.
1252
1254
  // Export serializes that same session store to canonical JSONL.
@@ -1282,13 +1284,13 @@ ${ledgerBundleAvailable ? `<script src="./ledger-browser.bundle.js"></script>` :
1282
1284
  withLock(async () => {
1283
1285
  try {
1284
1286
  const s = await ensureSession();
1285
- const sentences = tmctLedger.splitSentences(text);
1287
+ const sentences = tmct.page.splitSentences(text);
1286
1288
  let grounded = 0;
1287
1289
  for (const sentence of sentences) {
1288
- const r = await s.turn(sentence);
1290
+ const r = await tmct.turn(sentence);
1289
1291
  if (r.record && r.record.miss === false && r.record.via === "assert") grounded += 1;
1290
1292
  }
1291
- const fresh = tmctLedger.computeLedgerDataFromPayload(s.memoryDir.payload, {});
1293
+ const fresh = tmct.page.computeLedgerDataFromPayload(s.memoryDir.payload, {});
1292
1294
  applyLedgerData(fresh);
1293
1295
  const skipped = sentences.length - grounded;
1294
1296
  statusEl.textContent = countLabel(sentences.length, "sentence")
@@ -1308,7 +1310,7 @@ ${ledgerBundleAvailable ? `<script src="./ledger-browser.bundle.js"></script>` :
1308
1310
  withLock(async () => {
1309
1311
  try {
1310
1312
  const s = await ensureSession();
1311
- const jsonl = await tmctLedger.exportFactsJsonl(s.memoryDir);
1313
+ const jsonl = await tmct.page.exportFactsJsonl(s.memoryDir);
1312
1314
  const blob = new Blob([jsonl], { type: "application/x-ndjson" });
1313
1315
  const url = URL.createObjectURL(blob);
1314
1316
  const link = document.createElement("a");
@@ -1354,8 +1356,8 @@ ${ledgerBundleAvailable ? `<script src="./ledger-browser.bundle.js"></script>` :
1354
1356
  const pending = addLine("a pending", "researching\\u2026");
1355
1357
  return withLock(async () => {
1356
1358
  try {
1357
- const s = await ensureSession();
1358
- const result = await s.turn(q);
1359
+ const live = await ensureSession();
1360
+ const result = await tmct.turn(q);
1359
1361
  const missed = !result.record || Boolean(result.record.miss);
1360
1362
  pending.className = "a" + (missed ? " miss" : "");
1361
1363
  pending.innerHTML = esc(result.answer).replace(/\\n/g, "<br>");
@@ -1363,7 +1365,7 @@ ${ledgerBundleAvailable ? `<script src="./ledger-browser.bundle.js"></script>` :
1363
1365
  researchQueue = result.research;
1364
1366
  renderResearchControls();
1365
1367
  if (!missed) {
1366
- const fresh = tmctLedger.computeLedgerDataFromPayload(s.memoryDir.payload, {});
1368
+ const fresh = tmct.page.computeLedgerDataFromPayload(live.memoryDir.payload, {});
1367
1369
  applyLedgerData(fresh);
1368
1370
  }
1369
1371
  }
@@ -1406,9 +1408,11 @@ ${ledgerBundleAvailable ? `<script src="./ledger-browser.bundle.js"></script>` :
1406
1408
  else researchTicker.play();
1407
1409
  });
1408
1410
  }
1409
- } else if (chatForm && typeof tmctMemoryAsk !== "undefined") {
1410
- const memHandle = tmctMemoryAsk.createInMemoryStore();
1411
- memHandle.payload = PAYLOAD;
1411
+ } else if (chatForm && typeof tmct !== "undefined") {
1412
+ // The query-only dock. One session over the page's own embedded payload,
1413
+ // and every line goes to tmct.ask the cascade that used to be chained
1414
+ // here by hand now lives behind that one call.
1415
+ const opened = tmct.open({ payload: PAYLOAD });
1412
1416
  const log = el("chatlog");
1413
1417
  const addLine = (cls, html) => {
1414
1418
  const d = document.createElement("div");
@@ -1423,20 +1427,16 @@ ${ledgerBundleAvailable ? `<script src="./ledger-browser.bundle.js"></script>` :
1423
1427
  input.value = "";
1424
1428
  addLine("u", esc(q));
1425
1429
  (async () => {
1426
- let fact = null;
1427
- try { fact = await tmctMemoryAsk.factAnswer(memHandle, q, null, true, {}); } catch { fact = null; }
1428
- // runAsk's own cascade is factAnswer ?? factReadBack; chain the same
1429
- // way when the bundle exposes the second reader (relation chases
1430
- // "who is the grandfather of ishmael" — live there, not in factAnswer).
1431
- if (!(fact && fact.text) && typeof tmctMemoryAsk.factReadBack === "function") {
1432
- try { fact = await tmctMemoryAsk.factReadBack(memHandle, q, null, true, null); } catch { fact = null; }
1433
- }
1434
- if (fact && fact.text) {
1435
- addLine("a", esc(fact.text).replace(/\\n/g, "<br>"));
1430
+ await opened;
1431
+ let answer = null;
1432
+ let data = null;
1433
+ try { ({ answer, data } = await tmct.ask(q)); } catch { answer = null; data = null; }
1434
+ if (answer) {
1435
+ addLine("a", esc(answer).replace(/\\n/g, "<br>"));
1436
1436
  // Same formatting contract as chat's withGoalLine: capitalized,
1437
1437
  // full-stop-terminated, rendered only when the engine deduced one.
1438
- if (fact.goal) addLine("a goal", "Goal (inferred): " + esc(fact.goal.charAt(0).toUpperCase() + fact.goal.slice(1)) + ".");
1439
- const hit = resolveAnsweredTerm(fact.text, q, LEDGER.terms, tmctMemoryAsk.normFactTerm);
1438
+ if (data && data.goal) addLine("a goal", "Goal (inferred): " + esc(data.goal.charAt(0).toUpperCase() + data.goal.slice(1)) + ".");
1439
+ const hit = resolveAnsweredTerm(answer, q, LEDGER.terms, tmct.page.normFactTerm);
1440
1440
  if (hit) refocusWithLabel(hit, q);
1441
1441
  } else {
1442
1442
  const tips = LEDGER.terms.filter((t) => t.term.length >= 3).slice(0, 2)
@@ -75,12 +75,17 @@ export async function clearSiteAssetCaches() {
75
75
 
76
76
  /** Fetch `url` reading the body as a stream, reporting (loadedBytes,
77
77
  * totalBytes) after every chunk — total is 0 when the response carries no
78
- * Content-Length. Resolves to a Blob of the whole body. Falls back to a
79
- * single-shot blob() read when the runtime has no streaming body reader. */
78
+ * Content-Length, OR when Content-Encoding is set: the stream this function
79
+ * reads is always the DECOMPRESSED body (the browser decompresses before
80
+ * handing it to a reader), but Content-Length names the compressed wire
81
+ * size — reporting that as "total" against decompressed "loaded" bytes
82
+ * makes progress read as over 100% almost immediately. Resolves to a Blob
83
+ * of the whole body. Falls back to a single-shot blob() read when the
84
+ * runtime has no streaming body reader. */
80
85
  export async function fetchWithProgress(url, onProgress) {
81
86
  const res = await fetch(url);
82
87
  if (!res.ok) throw new Error("HTTP " + res.status);
83
- const total = Number(res.headers.get("content-length")) || 0;
88
+ const total = res.headers.get("content-encoding") ? 0 : Number(res.headers.get("content-length")) || 0;
84
89
  if (!res.body || !res.body.getReader) {
85
90
  const blob = await res.blob();
86
91
  onProgress(blob.size, total || blob.size);
@@ -55,7 +55,7 @@ import {
55
55
  foldWorldState, worldActionRows, runWorldCommand, recordTold, recordExamined,
56
56
  recordMassDrain, personKnowledgeLines, objectClassChain, diggableDirections,
57
57
  isOutOfPlay, outOfPlayReasonOf, outOfPlayPhrase, massDrainPerTurnOf,
58
- parseSnapshotSubject,
58
+ parseSnapshotSubject, characterTestimonyTag,
59
59
  } from "./adventure.mjs";
60
60
 
61
61
  const FOOD_CLASS = "food";
@@ -267,6 +267,9 @@ export async function runMudTurn(character, {
267
267
  const opened = await readWorld(memoryDir);
268
268
  const room = opened.state.placements.get(character)?.object ?? null;
269
269
  const turn = k ?? opened.state.turnCount + 1;
270
+ // The run this turn belongs to, stamped onto the testimony it writes so a
271
+ // recast's first turns outrank whatever the replaced run had to say.
272
+ const epoch = opened.state.epoch;
270
273
  const actions = [];
271
274
  const notes = [];
272
275
  const learnedBefore = knownTopics(opened.rows, opened.state, character);
@@ -300,7 +303,7 @@ export async function runMudTurn(character, {
300
303
  notes.push(`MUD — ${step}: ${reason}`);
301
304
  };
302
305
 
303
- await investigateRoom({ character, turn, room, memoryDir, cache, actions, notes, runCommand, recordSkip });
306
+ await investigateRoom({ character, turn, epoch, room, memoryDir, cache, actions, notes, runCommand, recordSkip });
304
307
 
305
308
  const walked = await readWorld(memoryDir);
306
309
  const walkedRoom = walked.state.placements.get(character)?.object ?? room;
@@ -397,7 +400,7 @@ async function exploreUnvisited({ character, memoryDir, runCommand, recordSkip }
397
400
  * same empty greeting every turn for the rest of the run. The talk and the
398
401
  * examine write testimony, which never folds into the playable state; only
399
402
  * the manipulation touches the world. */
400
- async function investigateRoom({ character, turn, room, memoryDir, cache, recordSkip, runCommand, actions, notes }) {
403
+ async function investigateRoom({ character, turn, epoch = 0, room, memoryDir, cache, recordSkip, runCommand, actions, notes }) {
401
404
  const { rows, state } = await readWorld(memoryDir);
402
405
  const roomMates = castIn(state, room, character);
403
406
  const alreadyKnown = knownTopics(rows, state, character);
@@ -420,7 +423,7 @@ async function investigateRoom({ character, turn, room, memoryDir, cache, record
420
423
  // drops out of worthSpeakingTo, and drops back in the moment either side
421
424
  // learns a food the other has not heard of.
422
425
  if (!alreadyKnown.has(teller)) {
423
- await recordExamined(memoryDir, { observer: character, thing: teller, k: turn, cache });
426
+ await recordExamined(memoryDir, { observer: character, thing: teller, k: turn, epoch, cache });
424
427
  alreadyKnown.add(teller);
425
428
  }
426
429
  if (!told) {
@@ -433,13 +436,13 @@ async function investigateRoom({ character, turn, room, memoryDir, cache, record
433
436
  });
434
437
  notes.push(`MUD — talk: ${character} greeted ${teller}; ${teller} knows of no food to share`);
435
438
  } else {
436
- await recordTold(memoryDir, { asker: character, teller, thing: told, k: turn, cache });
439
+ await recordTold(memoryDir, { asker: character, teller, thing: told, k: turn, epoch, cache });
437
440
  alreadyKnown.add(told);
438
441
  actions.push({
439
442
  step: "investigate", kind: "ask", teller, thing: told, miss: false,
440
443
  text: `the ${character} asks the ${teller} about food, and hears about the ${told}.`,
441
444
  });
442
- notes.push(`MUD — ask: ${teller} told ${character} about ${told}; written as mud:${teller}:turn${turn}`);
445
+ notes.push(`MUD — ask: ${teller} told ${character} about ${told}; written as ${characterTestimonyTag(teller, turn, { epoch })}`);
443
446
  }
444
447
  }
445
448
 
@@ -452,13 +455,13 @@ async function investigateRoom({ character, turn, room, memoryDir, cache, record
452
455
  if (!examined) {
453
456
  recordSkip("investigate", "nothing unexamined stands here", "");
454
457
  } else {
455
- await recordExamined(memoryDir, { observer: character, thing: examined, k: turn, cache });
458
+ await recordExamined(memoryDir, { observer: character, thing: examined, k: turn, epoch, cache });
456
459
  alreadyKnown.add(examined);
457
460
  actions.push({
458
461
  step: "investigate", kind: "examine", thing: examined, miss: false,
459
462
  text: `the ${character} examines the ${examined}.`,
460
463
  });
461
- notes.push(`MUD — examine: ${character} looked at ${examined}; written as mud:${character}:turn${turn}`);
464
+ notes.push(`MUD — examine: ${character} looked at ${examined}; written as ${characterTestimonyTag(character, turn, { epoch })}`);
462
465
  }
463
466
 
464
467
  await manipulateSomething({ character, turn, room, memoryDir, runCommand, recordSkip });