@polycode-projects/the-mechanical-code-talker 4.0.1 → 4.1.0
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 +90 -121
- package/src/services/adventure.mjs +97 -35
- package/src/services/chat-page-viz.mjs +32 -16
- package/src/services/chat.mjs +101 -33
- package/src/services/code-explorer-viz.mjs +51 -49
- package/src/services/ingest-viz.mjs +15 -57
- package/src/services/ledger-viz.mjs +47 -27
- 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 +87 -198
- package/src/services/p2p-room.mjs +90 -23
- package/src/services/plan-pddl.mjs +3 -1
- package/src/services/plan-viz.mjs +4 -3
- package/src/services/research-viz.mjs +10 -52
- package/src/services/spider-fly-turn.mjs +14 -22
- package/src/services/spider-fly-viz.mjs +79 -118
- package/src/services/spider-fly.mjs +69 -11
- package/src/services/sprite-catalog-viz.mjs +271 -221
- 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 +49 -33
- package/src/surfaces/web/chat-browser-entry.mjs +30 -105
- package/src/surfaces/web/code-explorer-browser-entry.mjs +168 -24
- package/src/surfaces/web/ingest-browser-entry.mjs +3 -13
- package/src/surfaces/web/ledger-browser-entry.mjs +7 -47
- 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 +28 -28
- package/src/surfaces/web/plan-browser-entry.mjs +22 -40
- package/src/surfaces/web/research-browser-entry.mjs +26 -41
- package/src/surfaces/web/spider-fly-browser-entry.mjs +45 -28
- package/src/surfaces/web/sprites-browser-entry.mjs +14 -27
- package/src/surfaces/web/turn-session.mjs +120 -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 +25 -1
|
@@ -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}`,
|
|
@@ -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
|
|
@@ -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
|
|
|
@@ -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 = "";
|
package/src/services/chat.mjs
CHANGED
|
@@ -953,9 +953,35 @@ async function answerMemoryClassQuery(memoryDir, query) {
|
|
|
953
953
|
// literal quantifier lookup. Placed ahead of it in runTurn: HOW_MANY_ARE_RE
|
|
954
954
|
// reads "there" as a second noun and answers "I was never told a quantifier",
|
|
955
955
|
// stealing the phrasing before a member count ever runs.
|
|
956
|
-
const TAUGHT_CLASS_COUNT_RE = /^how\s+many\s+([a-z][\w-]*)\s*(.*)$/i;
|
|
956
|
+
const TAUGHT_CLASS_COUNT_RE = /^how\s+many\s+([a-z][\w-]*(?:\s+[a-z][\w-]*)*)\s*(.*)$/i;
|
|
957
|
+
|
|
958
|
+
/** The longest leading run of `nounRun`'s words that names a class something was
|
|
959
|
+
* actually taught about, as `{asked, tail, members}` — its taught members and
|
|
960
|
+
* whatever words are left over, joined onto `trailing` as the restrictor tail.
|
|
961
|
+
* A class name is a noun PHRASE, not a word ("sprite class", "body of water"),
|
|
962
|
+
* so the run is tried longest-first and the shortest reading wins only when no
|
|
963
|
+
* longer one is on record. That ordering is what keeps a single-word class
|
|
964
|
+
* carrying a restrictor ("list the animals in the graph") reading exactly as it
|
|
965
|
+
* did when only the first word was ever considered. */
|
|
966
|
+
async function longestTaughtClassInRun(memoryDir, nounRun, trailing, biasByBundle, cache) {
|
|
967
|
+
const words = String(nounRun || "").trim().split(/\s+/).filter(Boolean);
|
|
968
|
+
if (!words.length) return null;
|
|
969
|
+
if (COUNT_NOUNS[words[0].toLowerCase()]) return null; // a real graph-countable class — the code lanes own it
|
|
970
|
+
let normFactTerm;
|
|
971
|
+
try { ({ normFactTerm } = await import("../adapters/memory/core.mjs")); } catch { return null; }
|
|
972
|
+
const rows = await factRows(memoryDir, cache);
|
|
973
|
+
const isa = rows.filter((f) => ISA_PREDICATES.has(f.predicate));
|
|
974
|
+
for (let take = words.length; take >= 1; take -= 1) {
|
|
975
|
+
const asked = words.slice(0, take).join(" ").toLowerCase();
|
|
976
|
+
const variants = factTermVariants(normFactTerm, asked);
|
|
977
|
+
const members = rankByBiasThenTrust(isa.filter((f) => variants.has(f.object)), biasByBundle);
|
|
978
|
+
if (!members.length) continue; // nothing taught under this reading — try a shorter one
|
|
979
|
+
return { asked, tail: [words.slice(take).join(" "), String(trailing || "")].filter(Boolean).join(" ").trim(), members };
|
|
980
|
+
}
|
|
981
|
+
return null;
|
|
982
|
+
}
|
|
957
983
|
|
|
958
|
-
/** Count the taught members of a class named by a
|
|
984
|
+
/** Count the taught members of a class named by a noun phrase ("how many animals
|
|
959
985
|
* are there" → every "X is a kind of animal"). Declines (null) for a real
|
|
960
986
|
* code-countable class (answerCount owns it) or a class nothing was taught
|
|
961
987
|
* about, so structural counts and the quantifier lane are unaffected. */
|
|
@@ -963,22 +989,16 @@ async function answerTaughtClassCount(memoryDir, query, biasByBundle = {}, cache
|
|
|
963
989
|
if (!memoryDir) return null;
|
|
964
990
|
const m = String(query).trim().match(TAUGHT_CLASS_COUNT_RE);
|
|
965
991
|
if (!m) return null;
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
if (
|
|
969
|
-
let normFactTerm;
|
|
970
|
-
try { ({ normFactTerm } = await import("../adapters/memory/core.mjs")); } catch { return null; }
|
|
971
|
-
const rows = await factRows(memoryDir, cache);
|
|
972
|
-
const isa = rows.filter((f) => ISA_PREDICATES.has(f.predicate));
|
|
973
|
-
const variants = factTermVariants(normFactTerm, asked);
|
|
974
|
-
const members = rankByBiasThenTrust(isa.filter((f) => variants.has(f.object)), biasByBundle);
|
|
975
|
-
if (!members.length) return null; // nothing taught under this class name — later lanes own it
|
|
992
|
+
const hit = await longestTaughtClassInRun(memoryDir, m[1], m[2], biasByBundle, cache);
|
|
993
|
+
if (!hit) return null;
|
|
994
|
+
if (!DYNAMIC_TAIL_OK_RE.test(hit.tail)) return null;
|
|
976
995
|
// A member whose SUBJECT is itself a countable graph class ("every class is a
|
|
977
996
|
// component") is an asserted-vocabulary cardinality, not a member enumeration —
|
|
978
997
|
// countFromFacts counts the real class, so defer to it rather than tallying the
|
|
979
998
|
// one class-level fact.
|
|
980
|
-
if (members.some((f) => COUNT_NOUNS[String(f.subject).toLowerCase()])) return null;
|
|
981
|
-
|
|
999
|
+
if (hit.members.some((f) => COUNT_NOUNS[String(f.subject).toLowerCase()])) return null;
|
|
1000
|
+
const n = hit.members.length;
|
|
1001
|
+
return `${n} ${n === 1 ? hit.asked.replace(/s$/, "") : hit.asked}.`;
|
|
982
1002
|
}
|
|
983
1003
|
|
|
984
1004
|
// "list all animals" / "list the animals" — enumerate a taught class's members,
|
|
@@ -986,9 +1006,9 @@ async function answerTaughtClassCount(memoryDir, query, biasByBundle = {}, cache
|
|
|
986
1006
|
// leftovers: at scale the definition lane fills its cap with forward corpus facts
|
|
987
1007
|
// before the reverse-membership listing ever shows, and the conversational
|
|
988
1008
|
// orientation lane claims the bare "list …" phrasing before factReadBack runs.
|
|
989
|
-
const MEMBERSHIP_LIST_RE = /^(?:list|show(?:\s+me)?)\s+(?:all\s+|the\s+)?([a-z][\w-]*)\s*(.*)$/i;
|
|
1009
|
+
const MEMBERSHIP_LIST_RE = /^(?:list|show(?:\s+me)?)\s+(?:all\s+|the\s+)?([a-z][\w-]*(?:\s+[a-z][\w-]*)*)\s*(.*)$/i;
|
|
990
1010
|
|
|
991
|
-
/** List the taught members of a class named by a
|
|
1011
|
+
/** List the taught members of a class named by a noun phrase ("list all animals"
|
|
992
1012
|
* → every "X is a kind of animal"). Declines (null) for a code-countable class
|
|
993
1013
|
* or a class nothing was taught about; declines with a message for a real
|
|
994
1014
|
* restrictor tail rather than answering as if it weren't there. */
|
|
@@ -996,16 +1016,9 @@ async function answerMembershipList(memoryDir, query, biasByBundle = {}, cache =
|
|
|
996
1016
|
if (!memoryDir) return null;
|
|
997
1017
|
const m = String(query).trim().match(MEMBERSHIP_LIST_RE);
|
|
998
1018
|
if (!m) return null;
|
|
999
|
-
const
|
|
1000
|
-
if (
|
|
1001
|
-
const tail
|
|
1002
|
-
let normFactTerm;
|
|
1003
|
-
try { ({ normFactTerm } = await import("../adapters/memory/core.mjs")); } catch { return null; }
|
|
1004
|
-
const rows = await factRows(memoryDir, cache);
|
|
1005
|
-
const isa = rows.filter((f) => ISA_PREDICATES.has(f.predicate));
|
|
1006
|
-
const variants = factTermVariants(normFactTerm, asked);
|
|
1007
|
-
const members = rankByBiasThenTrust(isa.filter((f) => variants.has(f.object)), biasByBundle);
|
|
1008
|
-
if (!members.length) return null; // nothing taught under this class name — later lanes own it
|
|
1019
|
+
const hit = await longestTaughtClassInRun(memoryDir, m[1], m[2], biasByBundle, cache);
|
|
1020
|
+
if (!hit) return null;
|
|
1021
|
+
const { asked, tail, members } = hit;
|
|
1009
1022
|
if (!DYNAMIC_TAIL_OK_RE.test(tail)) {
|
|
1010
1023
|
return {
|
|
1011
1024
|
text: `I can list the ${asked}, but not the "${tail}" part of that question — `
|
|
@@ -7160,6 +7173,16 @@ const LOCATIVE_FACT_PREDICATE_RE = /^mgx:[a-z]+-(?:on|in|at|inside|under|below|a
|
|
|
7160
7173
|
* to the ordinary BARE_WHATIS_RE handling untouched. */
|
|
7161
7174
|
const WHAT_IS_PREP_FACT_RE = new RegExp(`^what(?:'s|\\s+is|\\s+are)\\s+(${PREP_SRC})\\s+(.+?)\\s*[?.!]*$`, "i");
|
|
7162
7175
|
|
|
7176
|
+
/** "what parameters does a person sprite take" / "what materials does a bed
|
|
7177
|
+
* accept" — the object-fronted property question, where the property noun
|
|
7178
|
+
* leads and the verb closes. It reads the same folded verb-plus-noun predicates
|
|
7179
|
+
* the teach path already mints (mgx:take-parameter, mgx:accept-material,
|
|
7180
|
+
* mgx:offer-variant), so the predicate is recovered from the sentence's own two
|
|
7181
|
+
* ends rather than from a table of known property words: m[1] is the noun,
|
|
7182
|
+
* m[2] the subject, m[3] the verb. Consumed by factAnswer's (a-pre6) reader,
|
|
7183
|
+
* which diverts only on a real stored hit. */
|
|
7184
|
+
const OBJECT_FRONTED_PROPERTY_RE = /^what\s+([a-z][\w-]*)\s+(?:does|do)\s+(?:an?\s+|the\s+)?(.+?)\s+([a-z][a-z-]*)\s*[?.!]*$/i;
|
|
7185
|
+
|
|
7163
7186
|
// CAN_ASK_RE's remaining paraphrase-ladder siblings, all over the same
|
|
7164
7187
|
// mgx:capableOf facts:
|
|
7165
7188
|
// - DO_VERB_ASK_RE: the do-support yes/no ("do birds fly", "does a dog
|
|
@@ -7666,6 +7689,35 @@ async function factAnswerReaders(memoryDir, query, envelope, miss, biasByBundle
|
|
|
7666
7689
|
}
|
|
7667
7690
|
}
|
|
7668
7691
|
|
|
7692
|
+
// (a-pre6) "what parameters does a person sprite take" — the object-fronted
|
|
7693
|
+
// property question over a folded verb-plus-noun predicate. Both ends of the
|
|
7694
|
+
// sentence carry half the predicate the teach path minted, so the verb and the
|
|
7695
|
+
// noun are rejoined into mgx:<verb>-<noun> and looked up directly; the noun is
|
|
7696
|
+
// tried through its own plural variants, since a question asks for "parameters"
|
|
7697
|
+
// where the stored predicate names one "parameter". Checked before (a) for the
|
|
7698
|
+
// same reason as its siblings above — the leading "what …" would otherwise be
|
|
7699
|
+
// claimed as one literal term to define — and hit-gated the same way, so a
|
|
7700
|
+
// sentence of this shape with nothing on record falls through untouched.
|
|
7701
|
+
const propertyQ = q.match(OBJECT_FRONTED_PROPERTY_RE);
|
|
7702
|
+
if (propertyQ) {
|
|
7703
|
+
const verb = propertyQ[3].toLowerCase();
|
|
7704
|
+
const subjectVariants = factTermVariants(normFactTerm, propertyQ[2]);
|
|
7705
|
+
const predicates = new Set(
|
|
7706
|
+
[...factTermVariants(normFactTerm, propertyQ[1])].map((noun) => normFactPredicate(`mgx:${verb}-${noun.replace(/\s+/g, "-")}`)),
|
|
7707
|
+
);
|
|
7708
|
+
const hits = (await factRows(memoryDir, cache)).filter(
|
|
7709
|
+
(f) => predicates.has(normFactPredicate(f.predicate)) && subjectVariants.has(normFactTerm(f.subject)),
|
|
7710
|
+
);
|
|
7711
|
+
if (hits.length) {
|
|
7712
|
+
const ranked = rankByBiasThenTrust(uniqueFacts(hits), biasByBundle);
|
|
7713
|
+
const lines = ranked.map(renderFactLine);
|
|
7714
|
+
const shown = lines.slice(0, FACT_ANSWER_CAP);
|
|
7715
|
+
const rest = lines.slice(FACT_ANSWER_CAP);
|
|
7716
|
+
const extra = rest.length ? `\n…and ${rest.length} more — say 'more' to see them.` : "";
|
|
7717
|
+
return { text: shown.join("\n") + extra, replace: true, ...(rest.length ? { pending: { items: rest, noun: "facts" } } : {}) };
|
|
7718
|
+
}
|
|
7719
|
+
}
|
|
7720
|
+
|
|
7669
7721
|
// (a) meta-shaped questions ("what is a module", "what does cache mean") — the
|
|
7670
7722
|
// parsed object term, matched against fact SUBJECTS; consulted for hits (append
|
|
7671
7723
|
// alongside the schema-docs answer) and misses (facts answer alone) alike.
|
|
@@ -12526,11 +12578,16 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
12526
12578
|
let envelope = null;
|
|
12527
12579
|
try {
|
|
12528
12580
|
let text;
|
|
12529
|
-
if (graph && (focus?.id || prev.length)) {
|
|
12530
|
-
// Direct ask()
|
|
12531
|
-
//
|
|
12532
|
-
//
|
|
12533
|
-
//
|
|
12581
|
+
if (graph?.individuals?.length || (graph && (focus?.id || prev.length))) {
|
|
12582
|
+
// Direct ask() whenever the caller HANDED US a graph with something in it.
|
|
12583
|
+
// The focus/prev pair is threaded through it (contextId so "it" binds, prev
|
|
12584
|
+
// for the anaphora node), but neither is what earns the direct call: a
|
|
12585
|
+
// caller that passes a real graph means that graph, and the tmct_ask branch
|
|
12586
|
+
// below reads the CONFIG's graph instead — which an in-process session (a
|
|
12587
|
+
// page's own world facts, say) has no file for. Gating on history alone
|
|
12588
|
+
// refused every cold turn against a perfectly good graph and only started
|
|
12589
|
+
// answering once a focus happened to be set. Builds the SAME delimited
|
|
12590
|
+
// envelope dispatchTool emits, so the parse below is identical either way.
|
|
12534
12591
|
const { ask } = await import("../domain/ask.mjs");
|
|
12535
12592
|
const r = ask(graph, askQuery, { contextId: effectiveContextId, prev });
|
|
12536
12593
|
text = `${r.content}${ASK_ENVELOPE_DELIM}${JSON.stringify(r.tmct_ask, null, 2)}`;
|
|
@@ -14084,9 +14141,20 @@ async function runCommand(line, { config, source, graph, focus, memoryDir, trace
|
|
|
14084
14141
|
if (name === "plan") {
|
|
14085
14142
|
note(trace, "goal: plan/execute a compound or maintenance-goal request over the graph (the capability router)");
|
|
14086
14143
|
if (!argText) return mk("/plan needs a request, e.g. `/plan of the modules impacted by X, which are untested`.", { miss: true });
|
|
14087
|
-
|
|
14144
|
+
// A KNOWN-EMPTY code graph is nothing to plan over, and it is what every
|
|
14145
|
+
// memory-graph page and an un-pointed CLI session actually holds: each of
|
|
14146
|
+
// its parameter slots would bind against an index with no entities in it.
|
|
14147
|
+
// Where there is a memory store, hand the planner that instead — that is
|
|
14148
|
+
// buildCapabilityPlanCtx's memory-only mode, where world facts bind and a
|
|
14149
|
+
// code-graph capability refuses by naming the graph it hasn't got. Only a
|
|
14150
|
+
// graph with something in it plans as a code graph. `source` travels with
|
|
14151
|
+
// it: this turn reuses what it already holds and never loads one mid-turn.
|
|
14152
|
+
const planGraph = graph && !noCodeGraph(graph) ? graph : null;
|
|
14153
|
+
if (!planGraph && !memoryDir) return mk("no graph loaded — /plan needs a code graph or a memory store to plan over.", { miss: true });
|
|
14088
14154
|
const { buildCapabilityPlanCtx, runCapabilityPlan, declaredCapabilityNames } = await import("../domain/router/drive.mjs");
|
|
14089
|
-
const planCtx = await buildCapabilityPlanCtx({
|
|
14155
|
+
const planCtx = await buildCapabilityPlanCtx({
|
|
14156
|
+
...capabilityPlanDeps(), config, source: planGraph ? source : null, tel, graph: planGraph, memoryDir,
|
|
14157
|
+
});
|
|
14090
14158
|
try {
|
|
14091
14159
|
const result = await runCapabilityPlan(argText, declaredCapabilityNames(), planCtx);
|
|
14092
14160
|
if (result.refused) {
|