@polycode-projects/the-mechanical-code-talker 4.0.1 → 4.1.1
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/README.md +2 -1
- package/corpus/sprites/src/sprite-facts.jsonl +375 -8
- package/package.json +1 -1
- package/src/adapters/memory/core.mjs +20 -0
- package/src/domain/ask-vocab.mjs +71 -0
- package/src/domain/ask.mjs +168 -0
- package/src/domain/game-config.mjs +11 -0
- package/src/domain/mud-facts.mjs +15 -0
- package/src/domain/router/drive.mjs +35 -9
- package/src/domain/router/registry.mjs +24 -4
- package/src/domain/router/resolver.mjs +102 -40
- package/src/domain/scene-compose.mjs +117 -0
- package/src/domain/spider-fly-world.mjs +36 -0
- package/src/domain/sprite-facts.mjs +0 -0
- package/src/domain/sprite-request.mjs +156 -0
- package/src/domain/sprite-templates.mjs +161 -14
- package/src/services/adventure-editor.mjs +8 -14
- package/src/services/adventure-viz.mjs +119 -150
- package/src/services/adventure.mjs +97 -35
- package/src/services/chat-page-viz.mjs +64 -48
- package/src/services/chat.mjs +102 -34
- package/src/services/code-explorer-viz.mjs +52 -50
- package/src/services/ingest-viz.mjs +32 -74
- package/src/services/ledger-viz.mjs +87 -70
- package/src/services/memory-panel-viz.mjs +38 -0
- package/src/services/mud-editor.mjs +10 -15
- package/src/services/mud-turn.mjs +6 -6
- package/src/services/mud-viz.mjs +119 -225
- package/src/services/p2p-room.mjs +90 -23
- package/src/services/plan-pddl.mjs +3 -1
- package/src/services/plan-viz.mjs +13 -12
- package/src/services/research-viz.mjs +25 -67
- package/src/services/spider-fly-turn.mjs +14 -22
- package/src/services/spider-fly-viz.mjs +97 -136
- package/src/services/spider-fly.mjs +69 -11
- package/src/services/sprite-catalog-viz.mjs +274 -224
- package/src/services/viz-boot.mjs +71 -0
- package/src/services/viz-room-graph.mjs +203 -0
- package/src/services/viz-theme.mjs +75 -1
- package/src/services/viz-ticker.mjs +22 -0
- package/src/surfaces/web/adventure-browser-entry.mjs +62 -47
- package/src/surfaces/web/chat-browser-entry.mjs +51 -107
- package/src/surfaces/web/code-explorer-browser-entry.mjs +192 -35
- package/src/surfaces/web/engine-surface.mjs +82 -0
- package/src/surfaces/web/ingest-browser-entry.mjs +16 -17
- package/src/surfaces/web/ledger-browser-entry.mjs +24 -56
- package/src/surfaces/web/memory-ask-browser-entry.mjs +55 -13
- package/src/surfaces/web/memory-ask-browser.bundle.js +128 -125
- package/src/surfaces/web/memory-stats.mjs +11 -0
- package/src/surfaces/web/mud-browser-entry.mjs +70 -49
- package/src/surfaces/web/plan-browser-entry.mjs +39 -50
- package/src/surfaces/web/research-browser-entry.mjs +48 -46
- package/src/surfaces/web/spider-fly-browser-entry.mjs +76 -40
- package/src/surfaces/web/sprites-browser-entry.mjs +28 -32
- package/src/surfaces/web/tmct-surface.mjs +147 -0
- package/src/surfaces/web/turn-session.mjs +124 -0
- package/src/tools/definitions.mjs +30 -0
- package/src/tools/handlers/index.mjs +6 -3
- package/src/tools/handlers/kit.mjs +19 -2
- package/src/tools/handlers/tmct-ask.mjs +11 -6
- package/src/tools/handlers/tmct-ingest.mjs +5 -1
- package/src/tools/handlers/tmct-related.mjs +4 -4
- package/src/tools/handlers/tmct-sprite.mjs +147 -0
- package/src/tools/memory-fallthrough.mjs +9 -2
- package/src/tools/server.mjs +37 -6
|
@@ -174,7 +174,43 @@ async function resumedPosition(memoryDir) {
|
|
|
174
174
|
|
|
175
175
|
// ---- the world-state fold ----------------------------------------------------
|
|
176
176
|
|
|
177
|
-
|
|
177
|
+
// A snapshot subject is `base@turnN`, or `base@epochE@turnN` once a recast has
|
|
178
|
+
// moved the world past epoch 0 — the fold below reads both, and epoch-0 writes
|
|
179
|
+
// keep the bare form so an unrecast store never changes shape.
|
|
180
|
+
const SNAPSHOT_RE = /^(.+?)@(?:epoch(\d+)@)?turn(\d+)$/;
|
|
181
|
+
|
|
182
|
+
/** Which run of the world a store is on. Recasting a shared world (mud.html's
|
|
183
|
+
* RESET, a slider, the scenario dropdown) reopens the same deterministic
|
|
184
|
+
* instance ids over a fresh store while peers may still hold the old run's
|
|
185
|
+
* snapshots — this marker is how every reader agrees the world has started
|
|
186
|
+
* over. It is an ordinary add-only fact: each recast appends a larger value,
|
|
187
|
+
* the fold takes the max, and merging two peers' stores converges because max
|
|
188
|
+
* is order-free. */
|
|
189
|
+
export const WORLD_EPOCH_PREDICATE = "mgx:world-epoch";
|
|
190
|
+
const WORLD_EPOCH_SUBJECT = "world";
|
|
191
|
+
|
|
192
|
+
/** The bare triple a recast writes to move the world onto `epoch`. The caller
|
|
193
|
+
* supplies provenance the same way it does for the seed facts, so the marker
|
|
194
|
+
* travels (and folds) as a world row. */
|
|
195
|
+
export function worldEpochFact(epoch) {
|
|
196
|
+
return { subject: WORLD_EPOCH_SUBJECT, predicate: WORLD_EPOCH_PREDICATE, object: String(epoch) };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** `{ base, epoch, turn }` for a snapshot subject, or null for a base subject.
|
|
200
|
+
* The one parser every reader outside this file should use — a local
|
|
201
|
+
* `@turn(\d+)$` regex reads an epoch-stamped subject's base as
|
|
202
|
+
* "mole-1@epoch2", which matches no character. */
|
|
203
|
+
export function parseSnapshotSubject(subject) {
|
|
204
|
+
const m = SNAPSHOT_RE.exec(String(subject || ""));
|
|
205
|
+
return m ? { base: m[1], epoch: m[2] ? Number(m[2]) : 0, turn: Number(m[3]) } : null;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** The snapshot subject a turn writes: bare `@turnN` while the world is on
|
|
209
|
+
* epoch 0 (every store that has never been recast, and every fact written
|
|
210
|
+
* before epochs existed), the epoch-stamped form after. */
|
|
211
|
+
export function snapshotSubject(base, turn, epoch = 0) {
|
|
212
|
+
return epoch > 0 ? `${base}@epoch${epoch}@turn${turn}` : `${base}@turn${turn}`;
|
|
213
|
+
}
|
|
178
214
|
const PLACEMENT_PREDICATES = new Set([
|
|
179
215
|
"mgx:currently-in", "mgx:located-in", "mgx:fixed-in", "mgx:stands-locked-in", "mgx:hidden-in",
|
|
180
216
|
]);
|
|
@@ -242,6 +278,9 @@ const MUD_STATE_PREDICATES = new Set([
|
|
|
242
278
|
KNOWS_ABOUT_PREDICATE,
|
|
243
279
|
DISPLAY_NAME_PREDICATE,
|
|
244
280
|
"rdf:type",
|
|
281
|
+
// Which run the world is on IS live world state: a peer that misses the
|
|
282
|
+
// recast marker keeps folding the old run's snapshots as current.
|
|
283
|
+
WORLD_EPOCH_PREDICATE,
|
|
245
284
|
]);
|
|
246
285
|
|
|
247
286
|
/** Whether `predicate` carries live world state — where a thing stands, what
|
|
@@ -269,41 +308,61 @@ export function worldIndividualNames(rows) {
|
|
|
269
308
|
}
|
|
270
309
|
|
|
271
310
|
/** Fold fact rows into the CURRENT world state: per subject, the newest
|
|
272
|
-
* placement (base row = turn 0,
|
|
273
|
-
* open/closed state, the newest mass, the exit map,
|
|
274
|
-
* largest
|
|
311
|
+
* placement (base row = turn 0 of the current epoch, snapshots override), the
|
|
312
|
+
* newest open/closed state, the newest mass, the exit map, the turn counter
|
|
313
|
+
* (the largest snapshot turn written in the current epoch — derived, never
|
|
314
|
+
* stored), and the epoch itself.
|
|
315
|
+
*
|
|
316
|
+
* Rows rank by the (epoch, turn) pair, so a recast can never be outranked by
|
|
317
|
+
* the run it replaced: a peer's turn-9 snapshot from before the recast loses
|
|
318
|
+
* to a turn-1 snapshot written after it. Base rows carry no stamp of their
|
|
319
|
+
* own and rank as turn 0 of the CURRENT epoch — a recast re-seeds the same
|
|
320
|
+
* deterministic ids, so the shard's own rows are exactly the state the new
|
|
321
|
+
* run starts from. A store with no epoch marker and no stamped snapshot is
|
|
322
|
+
* wholly on epoch 0 and folds as it always has. Pure. */
|
|
275
323
|
export function foldWorldState(factRows) {
|
|
276
|
-
const
|
|
277
|
-
|
|
278
|
-
const
|
|
279
|
-
|
|
324
|
+
const rows = factRows || [];
|
|
325
|
+
let epoch = 0;
|
|
326
|
+
for (const row of rows) {
|
|
327
|
+
if (row.predicate === WORLD_EPOCH_PREDICATE) {
|
|
328
|
+
const marked = Number(row.object);
|
|
329
|
+
if (Number.isInteger(marked) && marked > epoch) epoch = marked;
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
const m = SNAPSHOT_RE.exec(row.subject);
|
|
333
|
+
if (m && m[2] && Number(m[2]) > epoch) epoch = Number(m[2]);
|
|
334
|
+
}
|
|
335
|
+
const placements = new Map(); // subject -> { predicate, object, turn, epoch }
|
|
336
|
+
const positions = new Map(); // subject -> { predicate, object, turn, epoch }
|
|
337
|
+
const openness = new Map(); // subject -> { open, turn, epoch }
|
|
338
|
+
const masses = new Map(); // subject -> { value, turn, epoch }
|
|
280
339
|
const exits = new Map(); // room -> Map(direction -> room)
|
|
281
340
|
let turnCount = 0;
|
|
282
|
-
|
|
341
|
+
const outranks = (rowEpoch, turn, prior) =>
|
|
342
|
+
!prior || rowEpoch > prior.epoch || (rowEpoch === prior.epoch && turn >= prior.turn);
|
|
343
|
+
for (const row of rows) {
|
|
344
|
+
if (row.predicate === WORLD_EPOCH_PREDICATE) continue;
|
|
283
345
|
const m = SNAPSHOT_RE.exec(row.subject);
|
|
284
346
|
const base = m ? m[1] : row.subject;
|
|
285
|
-
const
|
|
286
|
-
|
|
347
|
+
const rowEpoch = m ? (m[2] ? Number(m[2]) : 0) : epoch;
|
|
348
|
+
const turn = m ? Number(m[3]) : 0;
|
|
349
|
+
if (m && rowEpoch === epoch) turnCount = Math.max(turnCount, turn);
|
|
287
350
|
if (PLACEMENT_PREDICATES.has(row.predicate)) {
|
|
288
|
-
|
|
289
|
-
if (!prior || turn >= prior.turn) placements.set(base, { predicate: row.predicate, object: row.object, turn });
|
|
351
|
+
if (outranks(rowEpoch, turn, placements.get(base))) placements.set(base, { predicate: row.predicate, object: row.object, turn, epoch: rowEpoch });
|
|
290
352
|
continue;
|
|
291
353
|
}
|
|
292
354
|
if (POSITION_PREDICATES.has(row.predicate)) {
|
|
293
|
-
|
|
294
|
-
if (!prior || turn >= prior.turn) positions.set(base, { predicate: row.predicate, object: row.object, turn });
|
|
355
|
+
if (outranks(rowEpoch, turn, positions.get(base))) positions.set(base, { predicate: row.predicate, object: row.object, turn, epoch: rowEpoch });
|
|
295
356
|
continue;
|
|
296
357
|
}
|
|
297
358
|
if (row.predicate === OPEN_PREDICATE) {
|
|
298
|
-
|
|
299
|
-
if (!prior || turn >= prior.turn) openness.set(base, { open: row.object === "true", turn });
|
|
359
|
+
if (outranks(rowEpoch, turn, openness.get(base))) openness.set(base, { open: row.object === "true", turn, epoch: rowEpoch });
|
|
300
360
|
continue;
|
|
301
361
|
}
|
|
302
362
|
if (row.predicate === MASS_PREDICATE) {
|
|
303
363
|
const value = Number(row.object);
|
|
304
364
|
if (!Number.isFinite(value)) continue; // masses hold numbers; an unparsable one is no mass at all
|
|
305
|
-
|
|
306
|
-
if (!prior || turn >= prior.turn) masses.set(base, { value, turn });
|
|
365
|
+
if (outranks(rowEpoch, turn, masses.get(base))) masses.set(base, { value, turn, epoch: rowEpoch });
|
|
307
366
|
continue;
|
|
308
367
|
}
|
|
309
368
|
const exit = EXIT_PREDICATE_RE.exec(row.predicate);
|
|
@@ -312,7 +371,7 @@ export function foldWorldState(factRows) {
|
|
|
312
371
|
exits.get(row.subject).set(exit[1], row.object);
|
|
313
372
|
}
|
|
314
373
|
}
|
|
315
|
-
return { placements, positions, openness, masses, exits, turnCount };
|
|
374
|
+
return { placements, positions, openness, masses, exits, turnCount, epoch };
|
|
316
375
|
}
|
|
317
376
|
|
|
318
377
|
/** A subject's CURRENT within-room position, or null. A position goes stale
|
|
@@ -323,7 +382,7 @@ export function currentPosition(state, subject) {
|
|
|
323
382
|
const pos = state.positions.get(subject);
|
|
324
383
|
if (!pos) return null;
|
|
325
384
|
const place = state.placements.get(subject);
|
|
326
|
-
if (place && pos.turn < place.turn) return null;
|
|
385
|
+
if (place && (pos.epoch < place.epoch || (pos.epoch === place.epoch && pos.turn < place.turn))) return null;
|
|
327
386
|
return pos;
|
|
328
387
|
}
|
|
329
388
|
|
|
@@ -551,7 +610,7 @@ export function runNpcPass({ rows, state, k, families, playerRoomAfter }) {
|
|
|
551
610
|
if (!covered) continue;
|
|
552
611
|
const linked = [...(state.exits.get(from)?.values() ?? [])].includes(target);
|
|
553
612
|
if (!linked) continue;
|
|
554
|
-
writes.push({ subject:
|
|
613
|
+
writes.push({ subject: snapshotSubject(npc, k, state.epoch), predicate: effectPredicate, object: target });
|
|
555
614
|
if (playerRoomAfter === target) lines.push(`the ${npc} walks in.`);
|
|
556
615
|
else if (playerRoomAfter === from) lines.push(`the ${npc} leaves.`);
|
|
557
616
|
}
|
|
@@ -704,8 +763,8 @@ export async function recordMassDrain(memoryDir, { world, subject, drainPerTurn,
|
|
|
704
763
|
const left = Math.max(0, Math.round((mass - drainPerTurn) * 100) / 100);
|
|
705
764
|
const k = state.turnCount + 1;
|
|
706
765
|
await writeWorldTurn(memoryDir, world, k, [
|
|
707
|
-
{ subject:
|
|
708
|
-
...(left > 0 ? [] : [{ subject:
|
|
766
|
+
{ subject: snapshotSubject(subject, k, state.epoch), predicate: MASS_PREDICATE, object: String(left) },
|
|
767
|
+
...(left > 0 ? [] : [{ subject: snapshotSubject(subject, k, state.epoch), predicate: "mgx:currently-in", object: STARVED_PLACE }]),
|
|
709
768
|
], cache);
|
|
710
769
|
return { mass: left, starved: left <= 0 };
|
|
711
770
|
}
|
|
@@ -724,6 +783,9 @@ export async function recordMassDrain(memoryDir, { world, subject, drainPerTurn,
|
|
|
724
783
|
// schedule) stay out of the view: hidden means hidden.
|
|
725
784
|
|
|
726
785
|
const VIEW_EXCLUDED_PREDICATES = new Set([
|
|
786
|
+
// The recast counter is bookkeeping, not scenery — "World mgx:world-epoch 2"
|
|
787
|
+
// must never read back as room prose.
|
|
788
|
+
WORLD_EPOCH_PREDICATE,
|
|
727
789
|
"mgx:hidden-in", "mgx:is-open", "mgx:is-npc", "mgx:is-container",
|
|
728
790
|
// A bare number reads as an untranslated triple in room prose ("Mole-1
|
|
729
791
|
// mgx:hasMass 8"). Mass reaches a player through the verbs that change it.
|
|
@@ -1293,7 +1355,7 @@ async function handleGoVerb(ctx) {
|
|
|
1293
1355
|
const predator = predatorIn(rows, state, target);
|
|
1294
1356
|
if (predator) {
|
|
1295
1357
|
await writeWorldTurn(memoryDir, world, k, [
|
|
1296
|
-
{ subject:
|
|
1358
|
+
{ subject: snapshotSubject(actingSubject, k, state.epoch), predicate: "mgx:currently-in", object: CONSUMED_PLACE },
|
|
1297
1359
|
], cache);
|
|
1298
1360
|
return answer(
|
|
1299
1361
|
`you go ${cmd.direction} into the ${target} — and the ${predator} is waiting. It eats the ${actingSubject}. That's the end of its run.`,
|
|
@@ -1302,7 +1364,7 @@ async function handleGoVerb(ctx) {
|
|
|
1302
1364
|
);
|
|
1303
1365
|
}
|
|
1304
1366
|
return commit(
|
|
1305
|
-
[{ subject:
|
|
1367
|
+
[{ subject: snapshotSubject(actingSubject, k, state.epoch), predicate: familyEffectPredicate(family) ?? "mgx:currently-in", object: target }],
|
|
1306
1368
|
`you go ${cmd.direction}. Now in the ${target}.`,
|
|
1307
1369
|
`go — the taught "go" family fired; ${actingSubject} moves ${here} -> ${target}`,
|
|
1308
1370
|
`move through the world (now in the ${target})`,
|
|
@@ -1339,7 +1401,7 @@ async function handleTakeVerb(ctx) {
|
|
|
1339
1401
|
return answer(`I don't see a ${object} here.`, noteFor(`take — ${object} isn't visible in the ${here}; declined, hidden things stay hidden`), { miss: true });
|
|
1340
1402
|
}
|
|
1341
1403
|
return commit(
|
|
1342
|
-
[{ subject:
|
|
1404
|
+
[{ subject: snapshotSubject(object, k, state.epoch), predicate: familyEffectPredicate(family) ?? "mgx:located-in", object: actingSubject }],
|
|
1343
1405
|
`you take the ${object}.`,
|
|
1344
1406
|
`take — the taught "take" family fired; ${object} is now carried`,
|
|
1345
1407
|
`carry the ${object}`,
|
|
@@ -1353,7 +1415,7 @@ async function handleDropOrGiveVerb(ctx) {
|
|
|
1353
1415
|
}
|
|
1354
1416
|
if (cmd.verb === "drop") {
|
|
1355
1417
|
return commit(
|
|
1356
|
-
[{ subject:
|
|
1418
|
+
[{ subject: snapshotSubject(object, k, state.epoch), predicate: familyEffectPredicate(family) ?? "mgx:located-in", object: here }],
|
|
1357
1419
|
`you drop the ${object} in the ${here}.`,
|
|
1358
1420
|
`drop — the taught "drop" family fired; ${object} rests in the ${here}`,
|
|
1359
1421
|
`set the ${object} down`,
|
|
@@ -1364,7 +1426,7 @@ async function handleDropOrGiveVerb(ctx) {
|
|
|
1364
1426
|
return answer(`the ${receiver} isn't here.`, noteFor(`give — ${receiver} isn't one of the cast standing in the ${here}; precondition declined by name`), { miss: true });
|
|
1365
1427
|
}
|
|
1366
1428
|
return commit(
|
|
1367
|
-
[{ subject:
|
|
1429
|
+
[{ subject: snapshotSubject(object, k, state.epoch), predicate: familyEffectPredicate(family) ?? "mgx:located-in", object: receiver }],
|
|
1368
1430
|
`you give the ${object} to the ${receiver}.`,
|
|
1369
1431
|
`give — the taught "give" family fired; the ${receiver} holds the ${object}`,
|
|
1370
1432
|
`hand the ${object} over`,
|
|
@@ -1496,8 +1558,8 @@ async function handleEatVerb(ctx) {
|
|
|
1496
1558
|
await recordGone(memoryDir, { observer: actingSubject, thing: object, k, cache });
|
|
1497
1559
|
return commit(
|
|
1498
1560
|
[
|
|
1499
|
-
{ subject:
|
|
1500
|
-
{ subject:
|
|
1561
|
+
{ subject: snapshotSubject(actingSubject, k, state.epoch), predicate: MASS_PREDICATE, object: String(grown) },
|
|
1562
|
+
{ subject: snapshotSubject(object, k, state.epoch), predicate: "mgx:located-in", object: CONSUMED_PLACE },
|
|
1501
1563
|
],
|
|
1502
1564
|
`you eat the ${object}. It adds ${gained} to your mass, so you weigh ${grown} now.`,
|
|
1503
1565
|
`eat — the ${object}'s ${gained} mass moves onto ${actingSubject} (now ${grown}) and the ${object} leaves the world`,
|
|
@@ -1537,7 +1599,7 @@ async function handlePutVerb(ctx) {
|
|
|
1537
1599
|
);
|
|
1538
1600
|
}
|
|
1539
1601
|
return commit(
|
|
1540
|
-
[{ subject:
|
|
1602
|
+
[{ subject: snapshotSubject(object, k, state.epoch), predicate: familyEffectPredicate(family) ?? "mgx:located-in", object: container }],
|
|
1541
1603
|
`you put the ${object} in the ${container}.`,
|
|
1542
1604
|
`put — the taught "put" family fired; the ${object} now sits in the ${container}`,
|
|
1543
1605
|
`put the ${object} in the ${container}`,
|
|
@@ -1585,7 +1647,7 @@ async function handleContainerVerb(ctx) {
|
|
|
1585
1647
|
return answer(text, noteFor(`${cmd.verb} — the taught "${cmd.verb}" family's ${failed.predicate} precondition declined by name`), { miss: true });
|
|
1586
1648
|
}
|
|
1587
1649
|
const effSubject = roleBinding(effect.subjectRole, ctx.actingSubject, object, domain);
|
|
1588
|
-
const writeIsOpen = { subject:
|
|
1650
|
+
const writeIsOpen = { subject: snapshotSubject(effSubject, k, state.epoch), predicate: effect.predicate, object: effect.value };
|
|
1589
1651
|
|
|
1590
1652
|
if (cmd.verb === "open") {
|
|
1591
1653
|
const revealed = [...state.placements]
|
|
@@ -1595,7 +1657,7 @@ async function handleContainerVerb(ctx) {
|
|
|
1595
1657
|
return commit(
|
|
1596
1658
|
[
|
|
1597
1659
|
writeIsOpen,
|
|
1598
|
-
...revealed.map((thing) => ({ subject:
|
|
1660
|
+
...revealed.map((thing) => ({ subject: snapshotSubject(thing, k, state.epoch), predicate: "mgx:located-in", object })),
|
|
1599
1661
|
],
|
|
1600
1662
|
revealed.length
|
|
1601
1663
|
? `you open the ${object} — inside: the ${revealed.join(", the ")}.`
|
|
@@ -1646,7 +1708,7 @@ async function handleContainerVerb(ctx) {
|
|
|
1646
1708
|
);
|
|
1647
1709
|
}
|
|
1648
1710
|
return commit(
|
|
1649
|
-
[{ subject:
|
|
1711
|
+
[{ subject: snapshotSubject(object, k, state.epoch), predicate: "mgx:fixed-in", object: here }],
|
|
1650
1712
|
`you unlock the ${object} with the ${required}.`,
|
|
1651
1713
|
`unlock — the lock releases; ${object} now stands unlocked (still fixed) in the ${here}`,
|
|
1652
1714
|
`unlock the ${object}`,
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// document shaped exactly like spider-fly-viz.mjs/adventure-viz.mjs's own
|
|
3
3
|
// page-builders — one inlined <style> importing viz-theme.mjs's shared
|
|
4
4
|
// tokens, behaviour as an inlined IIFE — running the full chat engine
|
|
5
|
-
// (chat-browser.bundle.js's globalThis.
|
|
5
|
+
// (chat-browser.bundle.js's globalThis.tmct, chat-seed.json,
|
|
6
6
|
// public/reference-pack/) by same-origin relative paths. The same
|
|
7
7
|
// relationship spider-fly-viz.mjs's own inlined chat dock has with
|
|
8
8
|
// createSpiderFlySession: both call the shared session.turn(line), neither
|
|
@@ -185,6 +185,18 @@ export function tapeRowFor(direction, message) {
|
|
|
185
185
|
return { type: type, direction: direction, detail: detail, family: FAMILIES[type] || "link" };
|
|
186
186
|
}
|
|
187
187
|
|
|
188
|
+
/**
|
|
189
|
+
* The wire tape's own clock stamp for a message as it arrives —
|
|
190
|
+
* `HH:MM:SS.mmm`, local time, zero-padded.
|
|
191
|
+
*
|
|
192
|
+
* Self-contained (no outer refs), `.toString()`-splice safe.
|
|
193
|
+
*/
|
|
194
|
+
export function tapeClock() {
|
|
195
|
+
const at = new Date();
|
|
196
|
+
const pad = (n, width) => String(n).padStart(width, "0");
|
|
197
|
+
return pad(at.getHours(), 2) + ":" + pad(at.getMinutes(), 2) + ":" + pad(at.getSeconds(), 2) + "." + pad(at.getMilliseconds(), 3);
|
|
198
|
+
}
|
|
199
|
+
|
|
188
200
|
/**
|
|
189
201
|
* The node list: every peer this graph knows about, each with the node name it
|
|
190
202
|
* chose and the timestamp of the most recent fact it contributed, most
|
|
@@ -256,6 +268,24 @@ export function nodeInitials(name) {
|
|
|
256
268
|
return "??";
|
|
257
269
|
}
|
|
258
270
|
|
|
271
|
+
/**
|
|
272
|
+
* How long ago a node's most recent fact landed, relative to `nowMs`: "now"
|
|
273
|
+
* under 5s, otherwise the whole seconds/minutes/hours/days, coarsest unit
|
|
274
|
+
* that still reads as one number. `at` null/undefined (a peer with no
|
|
275
|
+
* activity yet) reads as an em dash rather than a bogus duration.
|
|
276
|
+
*
|
|
277
|
+
* Self-contained (no outer refs), `.toString()`-splice safe.
|
|
278
|
+
*/
|
|
279
|
+
export function relativeWhen(at, nowMs) {
|
|
280
|
+
if (at === null || at === undefined) return "—";
|
|
281
|
+
const seconds = Math.max(0, Math.round((nowMs - at) / 1000));
|
|
282
|
+
if (seconds < 5) return "now";
|
|
283
|
+
if (seconds < 60) return seconds + "s";
|
|
284
|
+
if (seconds < 3600) return Math.round(seconds / 60) + "m";
|
|
285
|
+
if (seconds < 86400) return Math.round(seconds / 3600) + "h";
|
|
286
|
+
return Math.round(seconds / 86400) + "d";
|
|
287
|
+
}
|
|
288
|
+
|
|
259
289
|
/**
|
|
260
290
|
* The invite link: this page's own address carrying the offer blob, the world
|
|
261
291
|
* id and the world's name. Any query or fragment the current address already
|
|
@@ -509,7 +539,7 @@ ${THEME_TOKENS_CSS}
|
|
|
509
539
|
|
|
510
540
|
/* the provenance stats panel: what this session's memory holds, docked to
|
|
511
541
|
the right of the chat column (a real layout column, not an overlay) —
|
|
512
|
-
re-rendered after boot and after every turn from window.
|
|
542
|
+
re-rendered after boot and after every turn from window.tmct's own
|
|
513
543
|
memoryStats(), never a second provenance computation. */
|
|
514
544
|
.statsPanel { flex: 0 0 300px; max-width: 300px; overflow-y: auto; border-left: 1px solid var(--line); padding: 1.1rem 1.2rem 1.6rem; font-family: ${MONO_STACK}; font-size: .74rem; line-height: 1.55; }
|
|
515
545
|
.statsPanel h2 { font-size: .66rem; letter-spacing: .07em; text-transform: uppercase; color: var(--muted); margin: 1.3rem 0 .5rem; }
|
|
@@ -524,7 +554,7 @@ ${THEME_TOKENS_CSS}
|
|
|
524
554
|
.statsPanel .persist-note { color: var(--muted); font-size: .64rem; margin: .4rem 0 0; }
|
|
525
555
|
|
|
526
556
|
/* the "researched this session" panel: its own section under the memory
|
|
527
|
-
stats, filled from window.
|
|
557
|
+
stats, filled from window.tmct.page.researchedFactRows() plus each
|
|
528
558
|
settled research turn's own answer text — the passage tmct actually
|
|
529
559
|
read, the article it read it from, and the facts that passage grounded.
|
|
530
560
|
A sibling section, not folded into #statsPanelStats — that div's own
|
|
@@ -902,6 +932,8 @@ ${THEME_TOKENS_CSS}
|
|
|
902
932
|
const nodeInitials = ${nodeInitials.toString()};
|
|
903
933
|
const inviteLinkFor = ${inviteLinkFor.toString()};
|
|
904
934
|
const inviteParamsFrom = ${inviteParamsFrom.toString()};
|
|
935
|
+
const tapeClock = ${tapeClock.toString()};
|
|
936
|
+
const relativeWhen = ${relativeWhen.toString()};
|
|
905
937
|
const DIGEST_STRUCTURES = ${digestStructuresJson};
|
|
906
938
|
const el = (id) => document.getElementById(id);
|
|
907
939
|
|
|
@@ -1092,7 +1124,7 @@ ${THEME_TOKENS_CSS}
|
|
|
1092
1124
|
})(),
|
|
1093
1125
|
stallGuard(),
|
|
1094
1126
|
]);
|
|
1095
|
-
window.
|
|
1127
|
+
window.tmct.page.registerWinkModel(() => ({ winkNLP: mod.winkNLP, model: mod.model }));
|
|
1096
1128
|
winkStatus = "loaded";
|
|
1097
1129
|
} catch (err) {
|
|
1098
1130
|
winkStatus = "unavailable";
|
|
@@ -1138,8 +1170,8 @@ ${THEME_TOKENS_CSS}
|
|
|
1138
1170
|
if (!seedPayload) return null;
|
|
1139
1171
|
try { return structuredClone(seedPayload); } catch { return JSON.parse(JSON.stringify(seedPayload)); }
|
|
1140
1172
|
};
|
|
1141
|
-
function newSession() {
|
|
1142
|
-
return window.
|
|
1173
|
+
async function newSession() {
|
|
1174
|
+
return window.tmct.open({
|
|
1143
1175
|
seedPayload: cloneSeed(),
|
|
1144
1176
|
vocabSeeded: Boolean(seedPayload),
|
|
1145
1177
|
liveReference: liveReferenceForMode(checkedWikiMode()),
|
|
@@ -1173,7 +1205,7 @@ ${THEME_TOKENS_CSS}
|
|
|
1173
1205
|
};
|
|
1174
1206
|
|
|
1175
1207
|
// ---- persistence: what you taught it survives a reload, on this device -
|
|
1176
|
-
// Best-effort IndexedDB (window.
|
|
1208
|
+
// Best-effort IndexedDB (window.tmct.page.openPersistedStore): the whole
|
|
1177
1209
|
// Backend-B payload snapshots after each teach turn, debounced so a burst
|
|
1178
1210
|
// of teaching costs one multi-MB write, not one per fact. The stamp ties a
|
|
1179
1211
|
// snapshot to this deploy (site version) AND this seed (its fact count and
|
|
@@ -1214,14 +1246,14 @@ ${THEME_TOKENS_CSS}
|
|
|
1214
1246
|
// it would keep merging peers' facts into a store nothing reads any more.
|
|
1215
1247
|
// Rejoining is a fresh invite, which is what a dropped node needs anyway.
|
|
1216
1248
|
dropRoom();
|
|
1217
|
-
window.tmctChatSession = newSession();
|
|
1218
|
-
const stats = await window.
|
|
1249
|
+
window.tmctChatSession = await newSession();
|
|
1250
|
+
const stats = await window.tmct.page.memoryStats(window.tmctChatSession.memoryDir);
|
|
1219
1251
|
addSystemLine("forgot everything taught on this device \\u2014 back to the fresh seed (" + statsSummaryLine(stats, bandLabelFor) + ").");
|
|
1220
1252
|
await renderStatsPanel(stats);
|
|
1221
1253
|
}
|
|
1222
1254
|
|
|
1223
1255
|
// ---- memory stats: the boot message's own numbers, and the docked panel -
|
|
1224
|
-
// Both read window.
|
|
1256
|
+
// Both read window.tmct.page.memoryStats(memoryDir) (chat-browser-entry.mjs)
|
|
1225
1257
|
// — one computation, reused, so the boot line and the panel can never
|
|
1226
1258
|
// disagree with each other about what this session's memory holds.
|
|
1227
1259
|
// bandLabelFor/statsSummaryLine/renderStatsPanelInto are the shared
|
|
@@ -1234,8 +1266,8 @@ ${THEME_TOKENS_CSS}
|
|
|
1234
1266
|
* rather than blanking it. */
|
|
1235
1267
|
async function renderStatsPanel(stats) {
|
|
1236
1268
|
if (!stats) {
|
|
1237
|
-
if (!window.tmctChatSession || !window.
|
|
1238
|
-
try { stats = await window.
|
|
1269
|
+
if (!window.tmctChatSession || !window.tmct.page.memoryStats) return;
|
|
1270
|
+
try { stats = await window.tmct.page.memoryStats(window.tmctChatSession.memoryDir); }
|
|
1239
1271
|
catch { return; }
|
|
1240
1272
|
}
|
|
1241
1273
|
factPillValueEl.textContent = Number(stats.total || 0).toLocaleString();
|
|
@@ -1250,7 +1282,7 @@ ${THEME_TOKENS_CSS}
|
|
|
1250
1282
|
// and grounded so far — each entry pairs the passage a settled research
|
|
1251
1283
|
// turn's own answer cites (parseResearchAnswer, off the SAME "(source:
|
|
1252
1284
|
// research article ...)" text the chat bubble already shows) with the real
|
|
1253
|
-
// facts that turn stored, read back through window.
|
|
1285
|
+
// facts that turn stored, read back through window.tmct.
|
|
1254
1286
|
// researchedFactRows(memoryDir) rather than re-deriving them from the
|
|
1255
1287
|
// answer text — the citation names WHERE tmct read, the fact rows name
|
|
1256
1288
|
// WHAT it kept, and this panel never invents either from the other.
|
|
@@ -1316,9 +1348,9 @@ ${THEME_TOKENS_CSS}
|
|
|
1316
1348
|
* own entry — the passage was still read, even where nothing new stuck. */
|
|
1317
1349
|
async function noteResearchLearned(result) {
|
|
1318
1350
|
if (result.research === undefined || !result.record || result.record.miss) return;
|
|
1319
|
-
if (!window.
|
|
1351
|
+
if (!window.tmct.page.researchedFactRows || !window.tmctChatSession) return;
|
|
1320
1352
|
let rows;
|
|
1321
|
-
try { rows = await window.
|
|
1353
|
+
try { rows = await window.tmct.page.researchedFactRows(window.tmctChatSession.memoryDir); }
|
|
1322
1354
|
catch { return; }
|
|
1323
1355
|
const newFacts = [];
|
|
1324
1356
|
for (const row of rows) {
|
|
@@ -1414,7 +1446,7 @@ ${THEME_TOKENS_CSS}
|
|
|
1414
1446
|
setBusy(true);
|
|
1415
1447
|
let result = null;
|
|
1416
1448
|
try {
|
|
1417
|
-
result = await window.
|
|
1449
|
+
result = await window.tmct.turn(q);
|
|
1418
1450
|
settleAssistantBubble(pendingRow, result.answer, result.record);
|
|
1419
1451
|
// Persist on ANY store write, not just a teach turn: a learn-on-miss
|
|
1420
1452
|
// load (a child pack, a reference or live-Wikipedia article, a
|
|
@@ -1555,10 +1587,10 @@ ${THEME_TOKENS_CSS}
|
|
|
1555
1587
|
// CLI paths emit), so what you taught leaves in the standard shape.
|
|
1556
1588
|
el("exportFacts").addEventListener("click", async () => {
|
|
1557
1589
|
const session = window.tmctChatSession;
|
|
1558
|
-
if (!session || !window.
|
|
1590
|
+
if (!session || !window.tmct.page.exportFactsJsonl) return;
|
|
1559
1591
|
let jsonl;
|
|
1560
1592
|
try {
|
|
1561
|
-
jsonl = await window.
|
|
1593
|
+
jsonl = await window.tmct.page.exportFactsJsonl(session.memoryDir);
|
|
1562
1594
|
} catch (err) {
|
|
1563
1595
|
statusEl.textContent = "couldn't export the facts (" + (err && err.message ? err.message : err) + ")";
|
|
1564
1596
|
return;
|
|
@@ -1575,7 +1607,7 @@ ${THEME_TOKENS_CSS}
|
|
|
1575
1607
|
});
|
|
1576
1608
|
|
|
1577
1609
|
// "ingest file" feeds a whole .txt/.md through the SAME session, one
|
|
1578
|
-
// sentence at a time (window.
|
|
1610
|
+
// sentence at a time (window.tmct.page.splitSentences, then session.turn),
|
|
1579
1611
|
// teaching every sentence the recognizer grounds and skipping the rest
|
|
1580
1612
|
// honestly — the same pipeline the ingest page runs, reaching the chat's own
|
|
1581
1613
|
// memory so the taught facts answer questions straight away.
|
|
@@ -1584,7 +1616,7 @@ ${THEME_TOKENS_CSS}
|
|
|
1584
1616
|
const file = e.target.files && e.target.files[0];
|
|
1585
1617
|
e.target.value = "";
|
|
1586
1618
|
const session = window.tmctChatSession;
|
|
1587
|
-
if (!file || busy || !session || !window.
|
|
1619
|
+
if (!file || busy || !session || !window.tmct.page.splitSentences) return;
|
|
1588
1620
|
let text;
|
|
1589
1621
|
try {
|
|
1590
1622
|
text = await file.text();
|
|
@@ -1592,14 +1624,14 @@ ${THEME_TOKENS_CSS}
|
|
|
1592
1624
|
addSystemLine("couldn't read that file (" + (err && err.message ? err.message : err) + ").");
|
|
1593
1625
|
return;
|
|
1594
1626
|
}
|
|
1595
|
-
const sentences = window.
|
|
1627
|
+
const sentences = window.tmct.page.splitSentences(text);
|
|
1596
1628
|
if (!sentences.length) { addSystemLine("nothing to ingest in " + file.name + "."); return; }
|
|
1597
1629
|
setBusy(true);
|
|
1598
1630
|
statusEl.textContent = "ingesting " + file.name + "\\u2026";
|
|
1599
1631
|
let grounded = 0;
|
|
1600
1632
|
try {
|
|
1601
1633
|
for (const sentence of sentences) {
|
|
1602
|
-
const result = await
|
|
1634
|
+
const result = await tmct.turn(sentence);
|
|
1603
1635
|
if (result.record && result.record.via === "assert" && !result.record.miss) grounded += 1;
|
|
1604
1636
|
}
|
|
1605
1637
|
} catch (err) {
|
|
@@ -1827,12 +1859,6 @@ ${THEME_TOKENS_CSS}
|
|
|
1827
1859
|
const tapeCounts = new Map();
|
|
1828
1860
|
let wireMessageCount = 0;
|
|
1829
1861
|
|
|
1830
|
-
function tapeClock() {
|
|
1831
|
-
const at = new Date();
|
|
1832
|
-
const pad = function (n, width) { return String(n).padStart(width, "0"); };
|
|
1833
|
-
return pad(at.getHours(), 2) + ":" + pad(at.getMinutes(), 2) + ":" + pad(at.getSeconds(), 2) + "." + pad(at.getMilliseconds(), 3);
|
|
1834
|
-
}
|
|
1835
|
-
|
|
1836
1862
|
function pushTape(entry) {
|
|
1837
1863
|
// The meter counts real wire messages only; the tape below shows those
|
|
1838
1864
|
// plus the local notes (state changes, a channel opening) that explain
|
|
@@ -1913,16 +1939,6 @@ ${THEME_TOKENS_CSS}
|
|
|
1913
1939
|
answerPanelEl.hidden = !replyOutEl.value || (room && room.state === "connected");
|
|
1914
1940
|
}
|
|
1915
1941
|
|
|
1916
|
-
function relativeWhen(at, nowMs) {
|
|
1917
|
-
if (at === null || at === undefined) return "—";
|
|
1918
|
-
const seconds = Math.max(0, Math.round((nowMs - at) / 1000));
|
|
1919
|
-
if (seconds < 5) return "now";
|
|
1920
|
-
if (seconds < 60) return seconds + "s";
|
|
1921
|
-
if (seconds < 3600) return Math.round(seconds / 60) + "m";
|
|
1922
|
-
if (seconds < 86400) return Math.round(seconds / 3600) + "h";
|
|
1923
|
-
return Math.round(seconds / 86400) + "d";
|
|
1924
|
-
}
|
|
1925
|
-
|
|
1926
1942
|
function renderNodes() {
|
|
1927
1943
|
if (!room || !p2p) {
|
|
1928
1944
|
nodeCountEl.textContent = "";
|
|
@@ -2205,16 +2221,16 @@ ${THEME_TOKENS_CSS}
|
|
|
2205
2221
|
}
|
|
2206
2222
|
|
|
2207
2223
|
async function boot() {
|
|
2208
|
-
if (!window.
|
|
2224
|
+
if (!window.tmct) {
|
|
2209
2225
|
statusEl.textContent = "the chat engine didn't load \\u2014 this page needs its build step (npm run demo:build)";
|
|
2210
2226
|
inputEl.placeholder = "chat engine unavailable";
|
|
2211
2227
|
return;
|
|
2212
2228
|
}
|
|
2213
2229
|
await Promise.all([fetchSeed(), tryLoadWink(), fetchSiteVersion().then((v) => { siteVersion = v; })]);
|
|
2214
2230
|
progressActive = false;
|
|
2215
|
-
window.
|
|
2216
|
-
if (window.
|
|
2217
|
-
persist = window.
|
|
2231
|
+
window.tmct.page.registerReferencePackProvider(fetchPackProvider);
|
|
2232
|
+
if (window.tmct.page.openPersistedStore) {
|
|
2233
|
+
persist = window.tmct.page.openPersistedStore({ storeKey: "chat", stamp: siteVersion + ":" + seedFacts + ":" + SEED_STAMP });
|
|
2218
2234
|
}
|
|
2219
2235
|
const savedRecord = persist ? await persist.load() : null;
|
|
2220
2236
|
const initialMode = readWikiMode();
|
|
@@ -2223,7 +2239,7 @@ ${THEME_TOKENS_CSS}
|
|
|
2223
2239
|
synthSliderEl.value = String(readSynthBudget());
|
|
2224
2240
|
synthValueEl.textContent = synthSliderEl.value;
|
|
2225
2241
|
if (savedRecord && savedRecord.payload) {
|
|
2226
|
-
window.tmctChatSession = window.
|
|
2242
|
+
window.tmctChatSession = await window.tmct.open({
|
|
2227
2243
|
seedPayload: savedRecord.payload,
|
|
2228
2244
|
vocabSeeded: true,
|
|
2229
2245
|
liveReference: liveReferenceForMode(initialMode),
|
|
@@ -2232,9 +2248,9 @@ ${THEME_TOKENS_CSS}
|
|
|
2232
2248
|
digestStructures: DIGEST_STRUCTURES,
|
|
2233
2249
|
});
|
|
2234
2250
|
} else {
|
|
2235
|
-
window.tmctChatSession = newSession();
|
|
2251
|
+
window.tmctChatSession = await newSession();
|
|
2236
2252
|
}
|
|
2237
|
-
const stats = await window.
|
|
2253
|
+
const stats = await window.tmct.page.memoryStats(window.tmctChatSession.memoryDir);
|
|
2238
2254
|
if (savedRecord) restoredCount = stats.taught.length;
|
|
2239
2255
|
const restoredNote = savedRecord
|
|
2240
2256
|
? " Restored " + restoredCount + " taught fact" + (restoredCount === 1 ? "" : "s")
|
|
@@ -2248,14 +2264,14 @@ ${THEME_TOKENS_CSS}
|
|
|
2248
2264
|
// seen-set from them so a later research turn only reports what's
|
|
2249
2265
|
// actually new, without fabricating passages for a visit this page
|
|
2250
2266
|
// was never open to read.
|
|
2251
|
-
if (window.
|
|
2267
|
+
if (window.tmct.page.researchedFactRows) {
|
|
2252
2268
|
try {
|
|
2253
|
-
const existingResearch = await window.
|
|
2269
|
+
const existingResearch = await window.tmct.page.researchedFactRows(window.tmctChatSession.memoryDir);
|
|
2254
2270
|
for (const row of existingResearch) researchedFactKeysSeen.add(row.subject + "|" + row.predicate + "|" + row.object);
|
|
2255
2271
|
} catch { /* best-effort seeding only — a fresh session has none to seed */ }
|
|
2256
2272
|
}
|
|
2257
2273
|
renderResearchedPanel();
|
|
2258
|
-
inputEl.placeholder = seedPayload ? 'try "what is a dog" or "list facts"' : window.
|
|
2274
|
+
inputEl.placeholder = seedPayload ? 'try "what is a dog" or "list facts"' : window.tmct.page.vocabExampleHint(false);
|
|
2259
2275
|
renderStatus();
|
|
2260
2276
|
setBusy(false);
|
|
2261
2277
|
inputEl.focus();
|