@polycode-projects/the-mechanical-code-talker 2.8.13 → 2.9.3
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 +50 -28
- package/bin/tmct.mjs +176 -31
- package/corpus/LICENSES.json +7 -0
- package/corpus/sprites/src/sprite-facts.jsonl +1033 -0
- package/corpus/worlds/manifest.json +9 -9
- package/corpus/worlds/shards/ashcombe-hall.jsonl.gz +0 -0
- package/corpus/worlds/shards/spider-fly.jsonl.gz +0 -0
- package/corpus/worlds/src/ashcombe-hall.jsonl +3 -0
- package/corpus/worlds/src/spider-fly.jsonl +17 -0
- package/package.json +37 -34
- package/src/adapters/corpus/wikipedia-live.mjs +145 -0
- package/src/adapters/memory/core.mjs +77 -12
- package/src/adapters/toml-config.mjs +6 -5
- package/src/domain/cli-verbs.mjs +4 -2
- package/src/domain/hanoi-lesson.mjs +10 -0
- package/src/domain/reference-pack.mjs +102 -0
- package/src/domain/spider-fly-world.mjs +54 -1
- package/src/domain/sprite-facts.mjs +0 -0
- package/src/services/adventure-viz.mjs +208 -66
- package/src/services/chat-page-viz.mjs +366 -37
- package/src/services/chat-session.mjs +38 -22
- package/src/services/chat.mjs +199 -20
- package/src/services/fold.mjs +28 -44
- package/src/services/import-file.mjs +7 -6
- package/src/services/init.mjs +10 -5
- package/src/services/ledger-viz.mjs +25 -34
- package/src/services/plan-viz.mjs +22 -26
- package/src/services/sessions.mjs +15 -3
- package/src/services/spider-fly-viz.mjs +52 -49
- package/src/services/sprite-catalog-viz.mjs +187 -3
- package/src/services/viz-theme.mjs +8 -0
- package/src/surfaces/web/adventure-browser-entry.mjs +23 -10
- package/src/surfaces/web/chat-browser-entry.mjs +17 -2
- package/src/surfaces/web/idb-persist.mjs +115 -0
- package/src/surfaces/web/memory-ask-browser.bundle.js +155 -25927
- package/src/surfaces/web/sprites-browser-entry.mjs +68 -0
- package/src/tools/memory-fallthrough.mjs +11 -4
package/src/services/fold.mjs
CHANGED
|
@@ -7,11 +7,14 @@
|
|
|
7
7
|
// The cleaning rules themselves are pure and live in domain/memory/fold.mjs;
|
|
8
8
|
// everything that touches the filesystem or the store lives here.
|
|
9
9
|
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
10
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
11
|
+
import { join } from "node:path";
|
|
12
12
|
import { SESSIONS_DIR_REL, parseSessionJsonl, parseSessionLog } from "./sessions.mjs";
|
|
13
13
|
import { removeBlock, saveBlock } from "../adapters/memory/blocks.mjs";
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
FACT_CLASS, appendCanonicalisedFromEdges, appendFacts, loadMemory,
|
|
16
|
+
openConfiguredMemoryBackend, readFactRows,
|
|
17
|
+
} from "../adapters/memory/core.mjs";
|
|
15
18
|
import { cleanSessionText } from "../domain/memory/fold.mjs";
|
|
16
19
|
import { syllogise } from "../domain/syllogise.mjs";
|
|
17
20
|
|
|
@@ -24,38 +27,15 @@ const LOG_DIR_REL = ".tmct";
|
|
|
24
27
|
// talking; we know exactly what the session touched). They are offline, $0,
|
|
25
28
|
// best-effort side effects: a failure NEVER fails the fold.
|
|
26
29
|
|
|
27
|
-
/** Atomic JSON write of the memory graph (temp-in-dir + rename), used here
|
|
28
|
-
* only to add mgx:canonicalisedFrom edges. */
|
|
29
|
-
async function writeMemoryGraph(repoDir, payload) {
|
|
30
|
-
const file = resolveMemoryGraphFile(repoDir);
|
|
31
|
-
await mkdir(dirname(file), { recursive: true });
|
|
32
|
-
const tmp = `${file}.tmp-${process.pid}-${Math.random().toString(36).slice(2, 10)}`;
|
|
33
|
-
await writeFile(tmp, JSON.stringify(payload));
|
|
34
|
-
await rename(tmp, file);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/** Add mgx:canonicalisedFrom edges (Fact → Utterance) into the payload, deduped
|
|
38
|
-
* by (subject,object) so a re-fold is idempotent — never duplicates a link. */
|
|
39
|
-
function addCanonicalisedFromEdges(payload, links) {
|
|
40
|
-
let group = payload.objectProperties.find((g) => g?.prop === CANONICALISED_FROM_PROP);
|
|
41
|
-
if (!group) {
|
|
42
|
-
group = { predicate: "canonicalisedFrom", prop: CANONICALISED_FROM_PROP, count: 0, examples: [] };
|
|
43
|
-
payload.objectProperties.push(group);
|
|
44
|
-
}
|
|
45
|
-
for (const l of links) {
|
|
46
|
-
group.examples = group.examples.filter((e) => !(e?.subject === l.factId && e?.object === l.uttId));
|
|
47
|
-
group.examples.push({ subject: l.factId, object: l.uttId, subjectLabel: l.factLabel, objectLabel: l.uttLabel });
|
|
48
|
-
}
|
|
49
|
-
group.count = group.examples.length;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
30
|
/** CANONISE + LINK, never replace: for each Fact whose provenance names a
|
|
53
31
|
* `ace:chat:<sessionId>@<ts>` utterance, add an mgx:canonicalisedFrom edge
|
|
54
|
-
* Fact -> Utterance (the utterance itself is left verbatim).
|
|
55
|
-
*
|
|
56
|
-
|
|
32
|
+
* Fact -> Utterance (the utterance itself is left verbatim). `memoryDir` is
|
|
33
|
+
* the already-opened store handle — the write goes through the store seam,
|
|
34
|
+
* never a direct graph-file write. Returns { linked, focus } — `focus` seeds
|
|
35
|
+
* the speculative pass below. */
|
|
36
|
+
async function canoniseLinkSession(memoryDir, sessionId) {
|
|
57
37
|
const focus = new Set();
|
|
58
|
-
const memory = await loadMemory(
|
|
38
|
+
const memory = await loadMemory(memoryDir);
|
|
59
39
|
const individuals = memory.individuals || [];
|
|
60
40
|
if (!individuals.length) return { linked: [], focus };
|
|
61
41
|
const uttById = new Map(individuals.filter((i) => i?.class === "Utterance").map((i) => [i.id, i]));
|
|
@@ -80,24 +60,28 @@ async function canoniseLinkSession(repoDir, sessionId) {
|
|
|
80
60
|
links.push({ factId: r.id, factLabel: factById.get(r.id)?.label || r.id, uttId, uttLabel: utt.label || uttId });
|
|
81
61
|
}
|
|
82
62
|
}
|
|
83
|
-
if (links.length)
|
|
84
|
-
addCanonicalisedFromEdges(memory, links);
|
|
85
|
-
await writeMemoryGraph(repoDir, memory);
|
|
86
|
-
}
|
|
63
|
+
if (links.length) await appendCanonicalisedFromEdges(memoryDir, links);
|
|
87
64
|
return { linked: links, focus };
|
|
88
65
|
}
|
|
89
66
|
|
|
90
|
-
/** The fold-time idle pass (best-effort):
|
|
91
|
-
*
|
|
92
|
-
*
|
|
67
|
+
/** The fold-time idle pass (best-effort): open the repo's configured memory
|
|
68
|
+
* backend once (the fold only ever has the repo path in hand — its callers
|
|
69
|
+
* are file-level), canonise-link each folded session through it, then run one
|
|
70
|
+
* bounded speculative pass scoped to the union footprint (empty footprint ->
|
|
71
|
+
* skipped). Never throws — must never fail a fold. */
|
|
93
72
|
async function speculateOverSessions(repoDir, sessionIds) {
|
|
94
73
|
try {
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
const
|
|
98
|
-
for (const
|
|
74
|
+
const { dir: memoryDir, close } = await openConfiguredMemoryBackend(repoDir);
|
|
75
|
+
try {
|
|
76
|
+
const focus = new Set();
|
|
77
|
+
for (const sid of sessionIds) {
|
|
78
|
+
const { focus: f } = await canoniseLinkSession(memoryDir, sid);
|
|
79
|
+
for (const t of f) focus.add(t);
|
|
80
|
+
}
|
|
81
|
+
if (focus.size) await syllogise(memoryDir, { focus, store: { loadMemory, readFactRows, appendFacts } });
|
|
82
|
+
} finally {
|
|
83
|
+
await close();
|
|
99
84
|
}
|
|
100
|
-
if (focus.size) await syllogise(repoDir, { focus, store: { loadMemory, readFactRows, appendFacts } });
|
|
101
85
|
} catch { /* offline best-effort: a fold never fails on the speculative pass */ }
|
|
102
86
|
}
|
|
103
87
|
|
|
@@ -13,7 +13,7 @@ import { readFile } from "node:fs/promises";
|
|
|
13
13
|
import { basename, resolve } from "node:path";
|
|
14
14
|
|
|
15
15
|
import { runTurn, uuidv7 } from "./chat.mjs";
|
|
16
|
-
import { loadMemory, readFactRows, appendFact,
|
|
16
|
+
import { loadMemory, readFactRows, appendFact, openConfiguredMemoryBackend } from "../adapters/memory/core.mjs";
|
|
17
17
|
import { loadConfig } from "../adapters/config.mjs";
|
|
18
18
|
import { splitSentencesPreservingPaths } from "./sentences.mjs";
|
|
19
19
|
|
|
@@ -25,7 +25,7 @@ import { splitSentencesPreservingPaths } from "./sentences.mjs";
|
|
|
25
25
|
* comments: number, report: string
|
|
26
26
|
* }>}
|
|
27
27
|
*/
|
|
28
|
-
export async function importDefinitionFile(repoRoot, filePath, { env = process.env } = {}) {
|
|
28
|
+
export async function importDefinitionFile(repoRoot, filePath, { env = process.env, memoryDir: injectedMemoryDir = null } = {}) {
|
|
29
29
|
const root = resolve(repoRoot);
|
|
30
30
|
const abs = resolve(root, filePath);
|
|
31
31
|
const sourceTag = `import:${basename(abs)}`;
|
|
@@ -36,10 +36,11 @@ export async function importDefinitionFile(repoRoot, filePath, { env = process.e
|
|
|
36
36
|
const body = lines.filter((l) => !l.trim().startsWith("#")).join("\n");
|
|
37
37
|
const sentences = splitSentencesPreservingPaths(body);
|
|
38
38
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
const
|
|
42
|
-
const
|
|
39
|
+
// An injected handle (a caller mid-session, or a build script pinned to the
|
|
40
|
+
// in-memory backend) is used as-is and left open — the caller owns it.
|
|
41
|
+
const opened = injectedMemoryDir ? null : await openConfiguredMemoryBackend(root, env);
|
|
42
|
+
const memoryDir = injectedMemoryDir ?? opened.dir;
|
|
43
|
+
const close = opened ? opened.close : async () => {};
|
|
43
44
|
const config = loadConfig(env, root);
|
|
44
45
|
|
|
45
46
|
const taught = [];
|
package/src/services/init.mjs
CHANGED
|
@@ -122,10 +122,10 @@ ${seed.limit != null ? `limit = ${Number(seed.limit)}` : "# limit = 500"}
|
|
|
122
122
|
[memory]
|
|
123
123
|
# Storage backend for taught facts + the memory graph.
|
|
124
124
|
# Precedence: --memory-backend flag > TMCT_MEMORY_BACKEND env > this file >
|
|
125
|
-
#
|
|
126
|
-
# "
|
|
125
|
+
# sqlite (the built-in default).
|
|
126
|
+
# "sqlite" — a local SQLite file at .tmct/memory/graph.sqlite. The default.
|
|
127
127
|
# "memory" — in-process only; nothing written to disk (a library caller's option).
|
|
128
|
-
# "
|
|
128
|
+
# "default" — same as leaving this unset: the sqlite default applies.
|
|
129
129
|
backend = ${JSON.stringify(config.memory.backend)}
|
|
130
130
|
`;
|
|
131
131
|
}
|
|
@@ -277,9 +277,14 @@ export async function initRepo(dir, { force = false, seed, env = process.env, pe
|
|
|
277
277
|
if (backendChoice === "memory") {
|
|
278
278
|
seedNote = "seed skipped (memory backend is in-process only — nothing would persist past this command)";
|
|
279
279
|
} else {
|
|
280
|
-
|
|
281
|
-
|
|
280
|
+
// The open itself sits inside the failure-tolerant try: an unopenable
|
|
281
|
+
// store (e.g. a directory squatting on graph.sqlite's path) degrades to
|
|
282
|
+
// an initialised-but-unseeded repo exactly like a broken corpus does.
|
|
283
|
+
let closeMemoryStore = async () => {};
|
|
282
284
|
try {
|
|
285
|
+
const { openMemoryBackend } = await import("../adapters/memory/core.mjs");
|
|
286
|
+
const { dir: memoryDir, close } = await openMemoryBackend(root, backendChoice);
|
|
287
|
+
closeMemoryStore = close;
|
|
283
288
|
const { resolveExtensions, seedActiveCorpusEntries } = await import("./extensions.mjs");
|
|
284
289
|
const { entries } = await resolveExtensions(root);
|
|
285
290
|
// `[seed] limit` caps the tier-1 ConceptNet band specifically (SEON is small
|
|
@@ -536,28 +536,17 @@ export function renderLedgerHtml({ rows, terms, edges, focus, contradictions, wo
|
|
|
536
536
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
537
537
|
<title>${escapeHtml(title)}</title>
|
|
538
538
|
<!--
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
plan.html
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
the
|
|
548
|
-
|
|
549
|
-
import would drag the ~1 MB model into every bundle; only the page's own inline
|
|
550
|
-
script performs this CDN import, the same bounded-race tryLoadWink() pattern
|
|
551
|
-
public/tmct-browser.mjs uses.
|
|
539
|
+
The wink lemma/POS tier loads from ./vendor/wink.js — the site's own shared
|
|
540
|
+
first-party bundle of wink-nlp + wink-eng-lite-web-model (built by
|
|
541
|
+
scripts/build-wink-vendor.mjs), one cached copy shared with chat.html and
|
|
542
|
+
plan.html, no CDN. A copy of this page opened somewhere without the sibling
|
|
543
|
+
asset (the self-contained file tmct viz writes to disk, say) just fails that
|
|
544
|
+
one import and this page's own try/catch degrades the wink tier gracefully.
|
|
545
|
+
Nothing in this file touches wink directly — wink-model.mjs's own header
|
|
546
|
+
explains why a static import would drag the ~1 MB model into every bundle;
|
|
547
|
+
only the page's own inline script performs the import, the same bounded-race
|
|
548
|
+
tryLoadWink() pattern public/tmct-browser.mjs uses.
|
|
552
549
|
-->
|
|
553
|
-
<script type="importmap">
|
|
554
|
-
{
|
|
555
|
-
"imports": {
|
|
556
|
-
"wink-nlp": "https://esm.sh/wink-nlp@2.4.0",
|
|
557
|
-
"wink-eng-lite-web-model": "https://esm.sh/wink-eng-lite-web-model@1.8.1"
|
|
558
|
-
}
|
|
559
|
-
}
|
|
560
|
-
</script>
|
|
561
550
|
<style>
|
|
562
551
|
${THEME_TOKENS_CSS}
|
|
563
552
|
html { background: var(--bg); }
|
|
@@ -723,6 +712,10 @@ ${ledgerBundleAvailable ? `<script src="./ledger-browser.bundle.js"></script>` :
|
|
|
723
712
|
<script>
|
|
724
713
|
(function () {
|
|
725
714
|
"use strict";
|
|
715
|
+
// Best-effort: a copy of this page opened without the sibling worker file
|
|
716
|
+
// (the CLI's own tmct viz output, a file:// open) just swallows the
|
|
717
|
+
// registration failure and works exactly as before.
|
|
718
|
+
if ("serviceWorker" in navigator) navigator.serviceWorker.register("./tmct-sw.js").catch(() => {});
|
|
726
719
|
const DAY = 86400000;
|
|
727
720
|
const facetCounts = ${facetCounts.toString()};
|
|
728
721
|
const el = (id) => document.getElementById(id);
|
|
@@ -996,12 +989,12 @@ ${ledgerBundleAvailable ? `<script src="./ledger-browser.bundle.js"></script>` :
|
|
|
996
989
|
let lock = Promise.resolve();
|
|
997
990
|
const withLock = (fn) => { const run = lock.then(fn, fn); lock = run.catch(() => {}); return run; };
|
|
998
991
|
|
|
999
|
-
// The SAME bounded-race wink
|
|
1000
|
-
//
|
|
1001
|
-
// reject on some failures, so an
|
|
1002
|
-
// stuck forever. Best-effort — a
|
|
1003
|
-
//
|
|
1004
|
-
// optional deps.
|
|
992
|
+
// The SAME bounded-race wink load plan-viz.mjs's own chat-assert dock
|
|
993
|
+
// uses, now against the site's shared first-party ./vendor/wink.js: a
|
|
994
|
+
// dynamic import() can neither resolve nor reject on some failures, so an
|
|
995
|
+
// unbounded await would leave a session stuck forever. Best-effort — a
|
|
996
|
+
// teach sentence that needs the lemma tier just declines honestly without
|
|
997
|
+
// it, same as a checkout missing the optional deps.
|
|
1005
998
|
const WINK_LOAD_TIMEOUT_MS = 8000;
|
|
1006
999
|
const winkTimeout = (ms, reason) => new Promise((_, reject) => setTimeout(() => reject(new Error(reason)), ms));
|
|
1007
1000
|
let winkReady = null;
|
|
@@ -1009,16 +1002,14 @@ ${ledgerBundleAvailable ? `<script src="./ledger-browser.bundle.js"></script>` :
|
|
|
1009
1002
|
if (winkReady) return winkReady;
|
|
1010
1003
|
winkReady = (async () => {
|
|
1011
1004
|
try {
|
|
1012
|
-
const
|
|
1013
|
-
|
|
1014
|
-
winkTimeout(WINK_LOAD_TIMEOUT_MS, "wink
|
|
1005
|
+
const mod = await Promise.race([
|
|
1006
|
+
import("./vendor/wink.js"),
|
|
1007
|
+
winkTimeout(WINK_LOAD_TIMEOUT_MS, "wink vendor asset load timed out"),
|
|
1015
1008
|
]);
|
|
1016
|
-
|
|
1017
|
-
const model = mods[1].default;
|
|
1018
|
-
tmctLedger.registerWinkModel(() => ({ winkNLP: winkNLP, model: model }));
|
|
1009
|
+
tmctLedger.registerWinkModel(() => ({ winkNLP: mod.winkNLP, model: mod.model }));
|
|
1019
1010
|
} catch (err) {
|
|
1020
1011
|
// eslint-disable-next-line no-console
|
|
1021
|
-
console.warn("tmct ledger: wink
|
|
1012
|
+
console.warn("tmct ledger: the wink vendor asset failed to load, continuing without the lemma/POS tier", err);
|
|
1022
1013
|
}
|
|
1023
1014
|
})();
|
|
1024
1015
|
return winkReady;
|
|
@@ -294,24 +294,16 @@ export function renderPlanHtml({ plan, rendersAs = {}, sizeOrder = [], title } =
|
|
|
294
294
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
295
295
|
<title>${escapeHtml(pageTitle)}</title>
|
|
296
296
|
<!--
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
ledger.html
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
297
|
+
The wink lemma/POS tier loads from ./vendor/wink.js — the site's own shared
|
|
298
|
+
first-party bundle of wink-nlp + wink-eng-lite-web-model (built by
|
|
299
|
+
scripts/build-wink-vendor.mjs), one cached copy shared with chat.html and
|
|
300
|
+
ledger.html, no CDN. The bundle itself (./plan-browser.bundle.js) never
|
|
301
|
+
touches wink directly — wink-model.mjs's own header explains why a static
|
|
302
|
+
import would drag the ~1 MB model into every bundle; only the page's own
|
|
303
|
+
inline script performs the import, the same bounded-race tryLoadWink()
|
|
304
|
+
pattern public/tmct-browser.mjs uses, and a failed load degrades to the
|
|
305
|
+
curated + fuzzy tiers, never an error.
|
|
306
306
|
-->
|
|
307
|
-
<script type="importmap">
|
|
308
|
-
{
|
|
309
|
-
"imports": {
|
|
310
|
-
"wink-nlp": "https://esm.sh/wink-nlp@2.4.0",
|
|
311
|
-
"wink-eng-lite-web-model": "https://esm.sh/wink-eng-lite-web-model@1.8.1"
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
</script>
|
|
315
307
|
<style>
|
|
316
308
|
${THEME_TOKENS_CSS}
|
|
317
309
|
html { background: var(--bg); }
|
|
@@ -432,6 +424,10 @@ const PLAN = ${embedded};
|
|
|
432
424
|
<script>
|
|
433
425
|
(function () {
|
|
434
426
|
"use strict";
|
|
427
|
+
// Best-effort: a copy of this page opened without the sibling worker file
|
|
428
|
+
// (a tmct --render plan --output file, a file:// open) just swallows the
|
|
429
|
+
// registration failure and works exactly as before.
|
|
430
|
+
if ("serviceWorker" in navigator) navigator.serviceWorker.register("./tmct-sw.js").catch(() => {});
|
|
435
431
|
const pageTitleEl = document.getElementById("pageTitle");
|
|
436
432
|
const board = document.getElementById("board");
|
|
437
433
|
const stepLabel = document.getElementById("stepLabel");
|
|
@@ -635,10 +631,10 @@ const PLAN = ${embedded};
|
|
|
635
631
|
// rest on the target" sentence needs a real lemmatiser to reduce
|
|
636
632
|
// "moving" to "move" — without it that one teach sentence honestly
|
|
637
633
|
// declines and every position fact taught after it fails in turn. Load
|
|
638
|
-
// wink from the
|
|
639
|
-
// public/tmct-browser.mjs uses: a
|
|
640
|
-
// neither resolve nor reject on some failures, so
|
|
641
|
-
// would leave a resolve stuck forever.
|
|
634
|
+
// wink from the site's shared first-party ./vendor/wink.js and register
|
|
635
|
+
// it, the SAME bounded-race pattern public/tmct-browser.mjs uses: a
|
|
636
|
+
// dynamic import() can neither resolve nor reject on some failures, so
|
|
637
|
+
// an unbounded await would leave a resolve stuck forever.
|
|
642
638
|
// Awaited before EVERY session creation below (idempotent — a second
|
|
643
639
|
// await after the first attempt already settled resolves immediately).
|
|
644
640
|
const WINK_LOAD_TIMEOUT_MS = 8000;
|
|
@@ -648,14 +644,14 @@ const PLAN = ${embedded};
|
|
|
648
644
|
if (winkReady) return winkReady;
|
|
649
645
|
winkReady = (async () => {
|
|
650
646
|
try {
|
|
651
|
-
const
|
|
652
|
-
|
|
653
|
-
winkTimeout(WINK_LOAD_TIMEOUT_MS, "wink
|
|
647
|
+
const mod = await Promise.race([
|
|
648
|
+
import("./vendor/wink.js"),
|
|
649
|
+
winkTimeout(WINK_LOAD_TIMEOUT_MS, "wink vendor asset load timed out"),
|
|
654
650
|
]);
|
|
655
|
-
tmctPlan.registerWinkModel(() => ({ winkNLP, model }));
|
|
651
|
+
tmctPlan.registerWinkModel(() => ({ winkNLP: mod.winkNLP, model: mod.model }));
|
|
656
652
|
} catch (err) {
|
|
657
653
|
// eslint-disable-next-line no-console
|
|
658
|
-
console.warn("tmct plan: wink
|
|
654
|
+
console.warn("tmct plan: the wink vendor asset failed to load, continuing without the lemma/POS tier", err);
|
|
659
655
|
}
|
|
660
656
|
})();
|
|
661
657
|
return winkReady;
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
|
|
19
19
|
import { mkdir, readFile, readdir, rename, writeFile } from "node:fs/promises";
|
|
20
20
|
import { basename, dirname, join } from "node:path";
|
|
21
|
-
import { appendUtterances, CREATED_AT_PROP, UPDATED_AT_PROP } from "../adapters/memory/core.mjs";
|
|
21
|
+
import { appendUtterances, openConfiguredMemoryBackend, CREATED_AT_PROP, UPDATED_AT_PROP } from "../adapters/memory/core.mjs";
|
|
22
22
|
import { turnKey } from "../domain/memory/session-turns.mjs";
|
|
23
23
|
|
|
24
24
|
export const SESSIONS_DIR_REL = join(".tmct", "sessions");
|
|
@@ -193,7 +193,12 @@ function repoDirFromGraphFile(graphFile) {
|
|
|
193
193
|
* change — it already calls appendSessionToGraph every turn. Each recorded turn becomes
|
|
194
194
|
* an a-visitor-said Utterance; the response prose is recovered from the human transcript
|
|
195
195
|
* (the sidecar only records ids) and recorded alongside as a tmct Utterance replying to
|
|
196
|
-
* it.
|
|
196
|
+
* it. The write goes through the repo's CONFIGURED memory backend (the same store the
|
|
197
|
+
* session's taught facts land in), opened fresh per append — never a raw Backend-A
|
|
198
|
+
* graph.json off the repo path, so the fold's canonise-link finds these utterances.
|
|
199
|
+
* Deterministic utterance ids make the per-turn replay idempotent, and each append
|
|
200
|
+
* replays the WHOLE record so far, so the completed-append state is always the full
|
|
201
|
+
* session even if a concurrent writer dropped an earlier turn's rows. Once the sidecar
|
|
197
202
|
* carries its end marker, the session is folded into the text-block corpus
|
|
198
203
|
* (memory/fold.mjs). */
|
|
199
204
|
async function recordSessionMemory(graphFile, record, repoDirOverride = null) {
|
|
@@ -232,7 +237,14 @@ async function recordSessionMemory(graphFile, record, repoDirOverride = null) {
|
|
|
232
237
|
});
|
|
233
238
|
}
|
|
234
239
|
}
|
|
235
|
-
|
|
240
|
+
if (utterances.length) {
|
|
241
|
+
const { dir: memoryDir, close } = await openConfiguredMemoryBackend(repoDir);
|
|
242
|
+
try {
|
|
243
|
+
await appendUtterances(memoryDir, utterances);
|
|
244
|
+
} finally {
|
|
245
|
+
await close();
|
|
246
|
+
}
|
|
247
|
+
}
|
|
236
248
|
|
|
237
249
|
// Session over? The sidecar's end marker is authoritative (chat.mjs writes it
|
|
238
250
|
// before the final upsert). Fold THIS session's transcript into the corpus.
|
|
@@ -32,15 +32,15 @@
|
|
|
32
32
|
// bundle's own real ES exports instead, since (unlike ledger-viz, which
|
|
33
33
|
// reuses a FIXED shared bundle it can't extend for one page's own needs) this
|
|
34
34
|
// page ships its own dedicated bundle and can just export what it needs.
|
|
35
|
-
import { THEME_TOKENS_CSS, SERIF_STACK, MONO_STACK, escapeHtml, embedJson } from "./viz-theme.mjs";
|
|
35
|
+
import { THEME_TOKENS_CSS, SERIF_STACK, MONO_STACK, escapeHtml, embedJson, embedScriptText } from "./viz-theme.mjs";
|
|
36
36
|
import { createTicker } from "./viz-ticker.mjs";
|
|
37
37
|
import { GRID_SIZE, WEB_HOME, WEB_RADIUS, isInWebBlock, cellId } from "../domain/spider-fly-world.mjs";
|
|
38
38
|
import { FLY_INITIAL_MASS, EGG_LAY_MASS_THRESHOLD } from "./spider-fly.mjs";
|
|
39
39
|
import { DEFAULT_GAME_CONFIG } from "../domain/game-config.mjs";
|
|
40
40
|
|
|
41
|
-
// A larger cell than this page's first cut (44px) — the board
|
|
42
|
-
//
|
|
43
|
-
//
|
|
41
|
+
// A larger cell than this page's first cut (44px) — the board sits in its
|
|
42
|
+
// own full-width block, so a bigger cell keeps it reading as the page's
|
|
43
|
+
// main scene rather than a small inset square.
|
|
44
44
|
const CELL_PX = 54;
|
|
45
45
|
const BOARD_PX = CELL_PX * GRID_SIZE;
|
|
46
46
|
const DEFAULT_TITLE = "tmct — the spider and the fly";
|
|
@@ -220,8 +220,13 @@ export function nextCorpses(prevCorpses, prevAgents, agents, turn, lingerTurns =
|
|
|
220
220
|
* `?preview=1` on the page's own URL switches it into the small, auto-
|
|
221
221
|
* playing, non-interactive mode the home page's hero iframe embeds (§11) —
|
|
222
222
|
* one file serves both the hero and the "open full-screen" link, matching
|
|
223
|
-
* how ledger.html/plan.html are each one file embedded two ways.
|
|
224
|
-
|
|
223
|
+
* how ledger.html/plan.html are each one file embedded two ways.
|
|
224
|
+
* `engineBundleJs` (the built spider-fly-browser bundle's own text) inlines
|
|
225
|
+
* the engine into the page instead of the sibling `<script src>`, for the
|
|
226
|
+
* CLI's standalone export — one downloadable file that runs from file://
|
|
227
|
+
* with no sibling assets. Default empty keeps the site build's sibling-file
|
|
228
|
+
* arrangement byte-identical. */
|
|
229
|
+
export function renderSpiderFlyHtml({ title = DEFAULT_TITLE, spriteTemplates = [], engineBundleJs = "" } = {}) {
|
|
225
230
|
const gridData = embedJson({
|
|
226
231
|
gridSize: GRID_SIZE,
|
|
227
232
|
webCells: webCellIds(),
|
|
@@ -294,7 +299,11 @@ ${THEME_TOKENS_CSS}
|
|
|
294
299
|
h1 { font-size: 1.4rem; margin: .3rem 0 .9rem; text-wrap: balance; }
|
|
295
300
|
button { font: inherit; color: inherit; background: none; cursor: pointer; }
|
|
296
301
|
button:focus-visible, input:focus-visible, .sprite:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }
|
|
297
|
-
|
|
302
|
+
/* The top row: the tuning console at roughly two-thirds width on the left,
|
|
303
|
+
the chat/agents column at roughly a quarter on the right — the 8fr/3fr
|
|
304
|
+
split plus the gap approximates that pair of fractions without them
|
|
305
|
+
needing to sum to a full width. */
|
|
306
|
+
.stage { display: grid; grid-template-columns: minmax(0, 8fr) minmax(280px, 3fr); gap: 1.2rem; align-items: start; }
|
|
298
307
|
@media (max-width: 760px) { .stage { grid-template-columns: 1fr; } }
|
|
299
308
|
/* A dusty window corner: a soft light glow near the top-left (WEB_HOME
|
|
300
309
|
already sits near that corner — spider-fly-world.mjs's own header
|
|
@@ -302,14 +311,7 @@ ${THEME_TOKENS_CSS}
|
|
|
302
311
|
in the light. Decoration only — the 10x10 game grid itself is drawn by
|
|
303
312
|
drawBoard() on the canvas beneath, unchanged. */
|
|
304
313
|
.board-frame {
|
|
305
|
-
|
|
306
|
-
(.hud-list caps out at 420px, then scrolls) — align-self keeps this
|
|
307
|
-
fixed-square board centered in whatever row height that produces,
|
|
308
|
-
instead of top-aligning it and leaving a dead gap below once the
|
|
309
|
-
side column outgrows the board. The grid's default align-items:
|
|
310
|
-
stretch would otherwise force this box tall, breaking its own
|
|
311
|
-
aspect-ratio square. */
|
|
312
|
-
align-self: center;
|
|
314
|
+
margin: 1.2rem auto 0;
|
|
313
315
|
position: relative; width: ${BOARD_PX}px; max-width: 100%; aspect-ratio: 1 / 1;
|
|
314
316
|
background:
|
|
315
317
|
radial-gradient(140% 140% at 6% 6%, rgba(255, 241, 199, .55), transparent 52%),
|
|
@@ -446,7 +448,8 @@ ${THEME_TOKENS_CSS}
|
|
|
446
448
|
.status { font-family: ${MONO_STACK}; font-size: .74rem; color: var(--muted); margin-top: .5rem; padding-left: .5rem; border-left: 2px solid var(--chrome-brass); }
|
|
447
449
|
body.preview .side, body.preview .controls-row, body.preview .status, body.preview .tuning { display: none; }
|
|
448
450
|
body.preview main { padding: 0; max-width: none; }
|
|
449
|
-
body.preview .stage { display:
|
|
451
|
+
body.preview .stage { display: none; }
|
|
452
|
+
body.preview .board-frame { margin: 0; }
|
|
450
453
|
body.preview .eyebrow, body.preview h1 { display: none; }
|
|
451
454
|
</style>
|
|
452
455
|
</head>
|
|
@@ -455,17 +458,30 @@ ${THEME_TOKENS_CSS}
|
|
|
455
458
|
<div class="eyebrow">tmct · spider and fly</div>
|
|
456
459
|
<h1>A spider in its web, a fly on the board — each planning against the other</h1>
|
|
457
460
|
<div class="stage">
|
|
458
|
-
<div class="
|
|
459
|
-
<
|
|
460
|
-
<
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
461
|
+
<div class="tuning" id="tuning">
|
|
462
|
+
<h2>live tuning — mass loss, spawn rate, vision, per class</h2>
|
|
463
|
+
<div class="tuning-grid">
|
|
464
|
+
<div class="tuning-col spider">
|
|
465
|
+
<h3>spider</h3>
|
|
466
|
+
<label>mass lost/turn <span class="tuning-val" id="tvSpiderMass"></span>
|
|
467
|
+
<input type="range" id="ctlSpiderMass" min="0.1" max="3" step="0.1" disabled></label>
|
|
468
|
+
<label>hatchlings per egg <span class="tuning-val" id="tvSpiderSpawn"></span>
|
|
469
|
+
<input type="range" id="ctlSpiderSpawn" min="1" max="5" step="1" disabled></label>
|
|
470
|
+
<label>vision radius <span class="tuning-val" id="tvSpiderVision"></span>
|
|
471
|
+
<input type="range" id="ctlSpiderVision" min="1" max="8" step="1" disabled></label>
|
|
472
|
+
</div>
|
|
473
|
+
<div class="tuning-col fly">
|
|
474
|
+
<h3>fly</h3>
|
|
475
|
+
<label>mass lost/turn <span class="tuning-val" id="tvFlyMass"></span>
|
|
476
|
+
<input type="range" id="ctlFlyMass" min="0.1" max="3" step="0.1" disabled></label>
|
|
477
|
+
<label>spawns every N turns <span class="tuning-val" id="tvFlySpawn"></span>
|
|
478
|
+
<input type="range" id="ctlFlySpawn" min="1" max="10" step="1" disabled></label>
|
|
479
|
+
<label>vision radius <span class="tuning-val" id="tvFlyVision"></span>
|
|
480
|
+
<input type="range" id="ctlFlyVision" min="1" max="8" step="1" disabled></label>
|
|
481
|
+
</div>
|
|
468
482
|
</div>
|
|
483
|
+
</div>
|
|
484
|
+
<aside class="side" aria-label="Chat and agents">
|
|
469
485
|
<div class="chat">
|
|
470
486
|
<h2>tell the spider or the fly something</h2>
|
|
471
487
|
<div class="chatlog" id="chatlog" aria-live="polite"></div>
|
|
@@ -483,30 +499,17 @@ ${THEME_TOKENS_CSS}
|
|
|
483
499
|
</div>
|
|
484
500
|
<div class="dynpills" id="dynamicPills" role="group" aria-label="address one individual and feed it a true or false position claim"></div>
|
|
485
501
|
</div>
|
|
502
|
+
<div class="hud">
|
|
503
|
+
<h2>agents</h2>
|
|
504
|
+
<div class="hud-list" id="hud"></div>
|
|
505
|
+
</div>
|
|
486
506
|
</aside>
|
|
487
507
|
</div>
|
|
488
|
-
<div class="
|
|
489
|
-
<
|
|
490
|
-
<
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
<label>mass lost/turn <span class="tuning-val" id="tvSpiderMass"></span>
|
|
494
|
-
<input type="range" id="ctlSpiderMass" min="0.1" max="3" step="0.1" disabled></label>
|
|
495
|
-
<label>hatchlings per egg <span class="tuning-val" id="tvSpiderSpawn"></span>
|
|
496
|
-
<input type="range" id="ctlSpiderSpawn" min="1" max="5" step="1" disabled></label>
|
|
497
|
-
<label>vision radius <span class="tuning-val" id="tvSpiderVision"></span>
|
|
498
|
-
<input type="range" id="ctlSpiderVision" min="1" max="8" step="1" disabled></label>
|
|
499
|
-
</div>
|
|
500
|
-
<div class="tuning-col fly">
|
|
501
|
-
<h3>fly</h3>
|
|
502
|
-
<label>mass lost/turn <span class="tuning-val" id="tvFlyMass"></span>
|
|
503
|
-
<input type="range" id="ctlFlyMass" min="0.1" max="3" step="0.1" disabled></label>
|
|
504
|
-
<label>spawns every N turns <span class="tuning-val" id="tvFlySpawn"></span>
|
|
505
|
-
<input type="range" id="ctlFlySpawn" min="1" max="10" step="1" disabled></label>
|
|
506
|
-
<label>vision radius <span class="tuning-val" id="tvFlyVision"></span>
|
|
507
|
-
<input type="range" id="ctlFlyVision" min="1" max="8" step="1" disabled></label>
|
|
508
|
-
</div>
|
|
509
|
-
</div>
|
|
508
|
+
<div class="board-frame" id="boardFrame">
|
|
509
|
+
<canvas id="board" width="${BOARD_PX}" height="${BOARD_PX}" aria-label="the 10x10 board"></canvas>
|
|
510
|
+
<canvas id="pov" width="${BOARD_PX}" height="${BOARD_PX}" aria-hidden="true"></canvas>
|
|
511
|
+
<div class="sprite-layer" id="spriteLayer"></div>
|
|
512
|
+
<div class="thread-tip" id="threadTip"></div>
|
|
510
513
|
</div>
|
|
511
514
|
<div class="controls-row">
|
|
512
515
|
<button id="resetBtn" type="button" disabled>reset</button>
|
|
@@ -519,7 +522,7 @@ ${THEME_TOKENS_CSS}
|
|
|
519
522
|
<script>
|
|
520
523
|
const SPIDERFLY = ${gridData};
|
|
521
524
|
</script>
|
|
522
|
-
|
|
525
|
+
${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `<script src="./spider-fly-browser.bundle.js"></script>`}
|
|
523
526
|
<script>
|
|
524
527
|
(function () {
|
|
525
528
|
"use strict";
|