@polycode-projects/the-mechanical-code-talker 2.9.0 → 2.9.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +209 -67
- 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 +26 -26
- package/src/services/sessions.mjs +15 -3
- package/src/services/spider-fly-viz.mjs +51 -45
- 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); }
|
|
@@ -321,6 +313,10 @@ main { max-width: 880px; margin: 0 auto; padding: 1.4rem 1rem 3rem; }
|
|
|
321
313
|
h1 { font-size: 1.15rem; margin: 0 0 .8rem; }
|
|
322
314
|
.chip { font-family: ${MONO_STACK}; font-size: .68rem; color: var(--muted); border: 1px solid var(--line); border-radius: 99px; padding: .12rem .55rem; }
|
|
323
315
|
.stage { display: grid; grid-template-columns: minmax(0, 1fr) 230px; gap: 1rem; }
|
|
316
|
+
/* grid items refuse to shrink below their content's min-width by default, so
|
|
317
|
+
the 640px board would stretch the track and the whole page with it on a
|
|
318
|
+
phone — let the items shrink and the boardwrap scroll instead. */
|
|
319
|
+
.stage > * { min-width: 0; }
|
|
324
320
|
@media (max-width: 660px) { .stage { grid-template-columns: 1fr; } }
|
|
325
321
|
.boardwrap { overflow-x: auto; }
|
|
326
322
|
.board { position: relative; width: ${BOARD_W}px; height: ${BOARD_H}px; background: var(--card); border: 1px solid var(--line); border-radius: 8px; }
|
|
@@ -432,6 +428,10 @@ const PLAN = ${embedded};
|
|
|
432
428
|
<script>
|
|
433
429
|
(function () {
|
|
434
430
|
"use strict";
|
|
431
|
+
// Best-effort: a copy of this page opened without the sibling worker file
|
|
432
|
+
// (a tmct --render plan --output file, a file:// open) just swallows the
|
|
433
|
+
// registration failure and works exactly as before.
|
|
434
|
+
if ("serviceWorker" in navigator) navigator.serviceWorker.register("./tmct-sw.js").catch(() => {});
|
|
435
435
|
const pageTitleEl = document.getElementById("pageTitle");
|
|
436
436
|
const board = document.getElementById("board");
|
|
437
437
|
const stepLabel = document.getElementById("stepLabel");
|
|
@@ -635,10 +635,10 @@ const PLAN = ${embedded};
|
|
|
635
635
|
// rest on the target" sentence needs a real lemmatiser to reduce
|
|
636
636
|
// "moving" to "move" — without it that one teach sentence honestly
|
|
637
637
|
// 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.
|
|
638
|
+
// wink from the site's shared first-party ./vendor/wink.js and register
|
|
639
|
+
// it, the SAME bounded-race pattern public/tmct-browser.mjs uses: a
|
|
640
|
+
// dynamic import() can neither resolve nor reject on some failures, so
|
|
641
|
+
// an unbounded await would leave a resolve stuck forever.
|
|
642
642
|
// Awaited before EVERY session creation below (idempotent — a second
|
|
643
643
|
// await after the first attempt already settled resolves immediately).
|
|
644
644
|
const WINK_LOAD_TIMEOUT_MS = 8000;
|
|
@@ -648,14 +648,14 @@ const PLAN = ${embedded};
|
|
|
648
648
|
if (winkReady) return winkReady;
|
|
649
649
|
winkReady = (async () => {
|
|
650
650
|
try {
|
|
651
|
-
const
|
|
652
|
-
|
|
653
|
-
winkTimeout(WINK_LOAD_TIMEOUT_MS, "wink
|
|
651
|
+
const mod = await Promise.race([
|
|
652
|
+
import("./vendor/wink.js"),
|
|
653
|
+
winkTimeout(WINK_LOAD_TIMEOUT_MS, "wink vendor asset load timed out"),
|
|
654
654
|
]);
|
|
655
|
-
tmctPlan.registerWinkModel(() => ({ winkNLP, model }));
|
|
655
|
+
tmctPlan.registerWinkModel(() => ({ winkNLP: mod.winkNLP, model: mod.model }));
|
|
656
656
|
} catch (err) {
|
|
657
657
|
// eslint-disable-next-line no-console
|
|
658
|
-
console.warn("tmct plan: wink
|
|
658
|
+
console.warn("tmct plan: the wink vendor asset failed to load, continuing without the lemma/POS tier", err);
|
|
659
659
|
}
|
|
660
660
|
})();
|
|
661
661
|
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: 0 auto;
|
|
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%),
|
|
@@ -336,6 +338,7 @@ ${THEME_TOKENS_CSS}
|
|
|
336
338
|
.sprite.corpse { filter: grayscale(1); opacity: .38; pointer-events: none; }
|
|
337
339
|
.sprite.corpse .sprite-face { transform: none !important; }
|
|
338
340
|
.thread-tip { position: absolute; transform: translate(-50%, -130%); font-family: ${MONO_STACK}; font-size: .68rem; background: var(--ink); color: var(--bg); padding: .1rem .4rem; border-radius: 3px; pointer-events: none; white-space: nowrap; display: none; }
|
|
341
|
+
.stage-left { display: flex; flex-direction: column; gap: 1.2rem; min-width: 0; }
|
|
339
342
|
.side { display: flex; flex-direction: column; gap: .8rem; min-width: 0; }
|
|
340
343
|
/* The console panel shell: a raised beveled plastic face (chrome-shadow-
|
|
341
344
|
raised), never the flat card/hairline the rest of the site uses — the
|
|
@@ -446,7 +449,8 @@ ${THEME_TOKENS_CSS}
|
|
|
446
449
|
.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
450
|
body.preview .side, body.preview .controls-row, body.preview .status, body.preview .tuning { display: none; }
|
|
448
451
|
body.preview main { padding: 0; max-width: none; }
|
|
449
|
-
body.preview .stage { display: block; }
|
|
452
|
+
body.preview .stage, body.preview .stage-left { display: block; }
|
|
453
|
+
body.preview .board-frame { margin: 0; }
|
|
450
454
|
body.preview .eyebrow, body.preview h1 { display: none; }
|
|
451
455
|
</style>
|
|
452
456
|
</head>
|
|
@@ -455,17 +459,38 @@ ${THEME_TOKENS_CSS}
|
|
|
455
459
|
<div class="eyebrow">tmct · spider and fly</div>
|
|
456
460
|
<h1>A spider in its web, a fly on the board — each planning against the other</h1>
|
|
457
461
|
<div class="stage">
|
|
462
|
+
<div class="stage-left">
|
|
463
|
+
<div class="tuning" id="tuning">
|
|
464
|
+
<h2>live tuning — mass loss, spawn rate, vision, per class</h2>
|
|
465
|
+
<div class="tuning-grid">
|
|
466
|
+
<div class="tuning-col spider">
|
|
467
|
+
<h3>spider</h3>
|
|
468
|
+
<label>mass lost/turn <span class="tuning-val" id="tvSpiderMass"></span>
|
|
469
|
+
<input type="range" id="ctlSpiderMass" min="0.1" max="3" step="0.1" disabled></label>
|
|
470
|
+
<label>hatchlings per egg <span class="tuning-val" id="tvSpiderSpawn"></span>
|
|
471
|
+
<input type="range" id="ctlSpiderSpawn" min="1" max="5" step="1" disabled></label>
|
|
472
|
+
<label>vision radius <span class="tuning-val" id="tvSpiderVision"></span>
|
|
473
|
+
<input type="range" id="ctlSpiderVision" min="1" max="8" step="1" disabled></label>
|
|
474
|
+
</div>
|
|
475
|
+
<div class="tuning-col fly">
|
|
476
|
+
<h3>fly</h3>
|
|
477
|
+
<label>mass lost/turn <span class="tuning-val" id="tvFlyMass"></span>
|
|
478
|
+
<input type="range" id="ctlFlyMass" min="0.1" max="3" step="0.1" disabled></label>
|
|
479
|
+
<label>spawns every N turns <span class="tuning-val" id="tvFlySpawn"></span>
|
|
480
|
+
<input type="range" id="ctlFlySpawn" min="1" max="10" step="1" disabled></label>
|
|
481
|
+
<label>vision radius <span class="tuning-val" id="tvFlyVision"></span>
|
|
482
|
+
<input type="range" id="ctlFlyVision" min="1" max="8" step="1" disabled></label>
|
|
483
|
+
</div>
|
|
484
|
+
</div>
|
|
485
|
+
</div>
|
|
458
486
|
<div class="board-frame" id="boardFrame">
|
|
459
487
|
<canvas id="board" width="${BOARD_PX}" height="${BOARD_PX}" aria-label="the 10x10 board"></canvas>
|
|
460
488
|
<canvas id="pov" width="${BOARD_PX}" height="${BOARD_PX}" aria-hidden="true"></canvas>
|
|
461
489
|
<div class="sprite-layer" id="spriteLayer"></div>
|
|
462
490
|
<div class="thread-tip" id="threadTip"></div>
|
|
463
491
|
</div>
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
<h2>agents</h2>
|
|
467
|
-
<div class="hud-list" id="hud"></div>
|
|
468
|
-
</div>
|
|
492
|
+
</div>
|
|
493
|
+
<aside class="side" aria-label="Chat and agents">
|
|
469
494
|
<div class="chat">
|
|
470
495
|
<h2>tell the spider or the fly something</h2>
|
|
471
496
|
<div class="chatlog" id="chatlog" aria-live="polite"></div>
|
|
@@ -483,30 +508,11 @@ ${THEME_TOKENS_CSS}
|
|
|
483
508
|
</div>
|
|
484
509
|
<div class="dynpills" id="dynamicPills" role="group" aria-label="address one individual and feed it a true or false position claim"></div>
|
|
485
510
|
</div>
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
<h2>live tuning — mass loss, spawn rate, vision, per class</h2>
|
|
490
|
-
<div class="tuning-grid">
|
|
491
|
-
<div class="tuning-col spider">
|
|
492
|
-
<h3>spider</h3>
|
|
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>
|
|
511
|
+
<div class="hud">
|
|
512
|
+
<h2>agents</h2>
|
|
513
|
+
<div class="hud-list" id="hud"></div>
|
|
508
514
|
</div>
|
|
509
|
-
</
|
|
515
|
+
</aside>
|
|
510
516
|
</div>
|
|
511
517
|
<div class="controls-row">
|
|
512
518
|
<button id="resetBtn" type="button" disabled>reset</button>
|
|
@@ -519,7 +525,7 @@ ${THEME_TOKENS_CSS}
|
|
|
519
525
|
<script>
|
|
520
526
|
const SPIDERFLY = ${gridData};
|
|
521
527
|
</script>
|
|
522
|
-
|
|
528
|
+
${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `<script src="./spider-fly-browser.bundle.js"></script>`}
|
|
523
529
|
<script>
|
|
524
530
|
(function () {
|
|
525
531
|
"use strict";
|