@polycode-projects/the-mechanical-code-talker 5.0.5 → 5.0.7
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 +78 -19
- package/bin/tmct.mjs +63 -2
- package/package.json +1 -1
- package/src/adapters/memory/core.mjs +23 -0
- package/src/domain/ask-vocab.mjs +19 -0
- package/src/domain/ask.mjs +10 -5
- package/src/domain/codegraph.mjs +23 -9
- package/src/domain/game-config.mjs +12 -0
- package/src/domain/interpret/strategies/keywords.mjs +30 -1
- package/src/domain/memory/capability.mjs +15 -11
- package/src/domain/router/drive.mjs +36 -17
- package/src/domain/router/resolver.mjs +63 -17
- package/src/domain/spider-fly-world.mjs +2 -2
- package/src/domain/sprite-templates.mjs +19 -7
- package/src/domain/syllogise.mjs +16 -6
- package/src/domain/town-square-world.mjs +1 -1
- package/src/services/adventure-viz.mjs +5 -2
- package/src/services/adventure.mjs +8 -1
- package/src/services/chat-page-viz.mjs +123 -25
- package/src/services/chat-session.mjs +60 -10
- package/src/services/chat.mjs +328 -36
- package/src/services/code-explorer-viz.mjs +3 -2
- package/src/services/extract-facts.mjs +47 -7
- package/src/services/ingest-viz.mjs +113 -29
- package/src/services/ledger-viz.mjs +9 -4
- package/src/services/memory-panel-viz.mjs +44 -0
- package/src/services/mud-viz.mjs +21 -3
- package/src/services/mudiii-scene.mjs +407 -36
- package/src/services/mudiii-turn.mjs +65 -9
- package/src/services/mudiii-viz.mjs +810 -157
- package/src/services/p2p-room.mjs +1 -1
- package/src/services/plan-viz.mjs +26 -4
- package/src/services/predator-prey.mjs +141 -37
- package/src/services/research-viz.mjs +17 -23
- package/src/services/spider-fly-turn.mjs +7 -1
- package/src/services/spider-fly-viz.mjs +13 -5
- package/src/services/sprite-catalog-viz.mjs +3 -2
- package/src/services/viz-theme.mjs +20 -0
- package/src/services/viz-ticker.mjs +15 -2
- package/src/surfaces/http/server-http.mjs +90 -13
- package/src/surfaces/web/memory-ask-browser.bundle.js +125 -125
- package/src/surfaces/web/mud-browser-entry.mjs +33 -1
- package/src/surfaces/web/mudiii-browser-entry.mjs +70 -34
- package/src/surfaces/web/tmct-surface.mjs +18 -6
- package/src/tools/handlers/tmct-ask.mjs +15 -2
- package/src/tools/server.mjs +31 -2
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
// input. scripts/build-demo-site.mjs calls it directly and writes the result
|
|
29
29
|
// to public/chat.html, after chat-browser.bundle.js/chat-seed.json already
|
|
30
30
|
// exist (both built earlier in that same script, for the embedded widget).
|
|
31
|
-
import { THEME_TOKENS_CSS, SERIF_STACK, MONO_STACK, escapeHtml } from "./viz-theme.mjs";
|
|
31
|
+
import { THEME_TOKENS_CSS, SERIF_STACK, MONO_STACK, escapeHtml, demoEyebrowHtml, EYEBROW_LINKS_CSS } from "./viz-theme.mjs";
|
|
32
32
|
import {
|
|
33
33
|
SHARE_OVERLAY_CSS, shareOverlayHtml, shareStepStates, activeWaves, offerBlobIn, peerTerm,
|
|
34
34
|
shareMessageFor, replyMessageFor, whatsAppShareUrl, isProbablyMobile, copyTextToClipboard, flashCopyTip,
|
|
@@ -36,7 +36,7 @@ import {
|
|
|
36
36
|
import { provBucketFor } from "./ledger-viz.mjs";
|
|
37
37
|
import { createTicker, prefersReducedMotion } from "./viz-ticker.mjs";
|
|
38
38
|
import { sessionLogTimeOfDay, sessionLogHeaderMarkdown, sessionLogTurnMarkdown } from "./session-log-format.mjs";
|
|
39
|
-
import { bandLabelFor, statsSummaryLine, clearSiteAssetCaches, fetchWithProgress, renderStatsPanelInto } from "./memory-panel-viz.mjs";
|
|
39
|
+
import { bandLabelFor, statsSummaryLine, clearSiteAssetCaches, fetchWithProgress, loadSeedPayload, renderStatsPanelInto } from "./memory-panel-viz.mjs";
|
|
40
40
|
|
|
41
41
|
const DEFAULT_TITLE = "the-mechanical-code-talker — talk to it";
|
|
42
42
|
|
|
@@ -446,7 +446,7 @@ export function transcriptMarkdown(turns, meta, headerMd, turnMd) {
|
|
|
446
446
|
* live digest-bank twin (see chat-browser-entry.mjs) rather than to a
|
|
447
447
|
* client-side digest panel of this page's own; an empty list degrades to the
|
|
448
448
|
* flat list exactly as before this page could digest at all. */
|
|
449
|
-
export function renderChatHtml({ title = DEFAULT_TITLE, digestStructures = [], seedStamp = "" } = {}) {
|
|
449
|
+
export function renderChatHtml({ title = DEFAULT_TITLE, digestStructures = [], seedStamp = "", seedBytes = 0 } = {}) {
|
|
450
450
|
const digestStructuresJson = JSON.stringify(Array.isArray(digestStructures) ? digestStructures : []);
|
|
451
451
|
const legendHtml = PROV_LEGEND.map(
|
|
452
452
|
([key, label]) => `<span class="legend-item"><i class="dot dot-${provKey(key)}"></i>${escapeHtml(label)}</span>`,
|
|
@@ -498,7 +498,9 @@ ${THEME_TOKENS_CSS}
|
|
|
498
498
|
get folded onto a second line by a wide one. */
|
|
499
499
|
.topbar { flex: 0 0 auto; display: flex; align-items: center; gap: 1rem; padding: .55rem 1.1rem; border-bottom: 1px solid var(--line); flex-wrap: wrap; }
|
|
500
500
|
.brand { display: flex; align-items: baseline; gap: .55rem; }
|
|
501
|
+
.visually-hidden { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0; }
|
|
501
502
|
.eyebrow { font-family: ${MONO_STACK}; font-size: .78rem; letter-spacing: .08em; color: var(--muted); }
|
|
503
|
+
${EYEBROW_LINKS_CSS}
|
|
502
504
|
.legend { display: flex; gap: .8rem; margin-left: auto; font-family: ${MONO_STACK}; font-size: .68rem; color: var(--muted); }
|
|
503
505
|
.legend-item { display: inline-flex; align-items: center; gap: .32rem; white-space: nowrap; }
|
|
504
506
|
.dot { width: 7px; height: 7px; border-radius: 50%; display: inline-block; }
|
|
@@ -622,6 +624,15 @@ ${THEME_TOKENS_CSS}
|
|
|
622
624
|
a pill like its neighbours, with the number itself at reading size. */
|
|
623
625
|
.fact-pill { display: inline-flex; align-items: baseline; gap: .34rem; font-family: ${MONO_STACK}; font-size: .68rem; letter-spacing: .06em; text-transform: uppercase; color: var(--muted); border: 1px solid var(--corpus-t1); border-radius: 99px; padding: .2rem .8rem; background: var(--corpus-soft); white-space: nowrap; }
|
|
624
626
|
.fact-pill .fact-pill-value { font-size: .96rem; letter-spacing: 0; font-variant-numeric: tabular-nums; font-weight: 600; color: var(--ink); }
|
|
627
|
+
/* While the starter memory streams in, the pill says so and breathes. The
|
|
628
|
+
graph is a moving state: what it knows now is less than what it will know
|
|
629
|
+
in a moment, and a question asked mid-load gets answered against what has
|
|
630
|
+
actually arrived. */
|
|
631
|
+
.fact-pill[data-state="loading"] { animation: pill-breathe 1.6s ease-in-out infinite; }
|
|
632
|
+
.fact-pill[data-state="loading"] .fact-pill-value { font-size: .68rem; letter-spacing: .06em; text-transform: uppercase; font-weight: 600; }
|
|
633
|
+
.fact-pill[data-state="failed"] { border-color: var(--miss-t1, var(--corpus-t1)); }
|
|
634
|
+
@keyframes pill-breathe { 0%, 100% { opacity: 1; } 50% { opacity: .55; } }
|
|
635
|
+
@media (prefers-reduced-motion: reduce) { .fact-pill[data-state="loading"] { animation: none; } }
|
|
625
636
|
|
|
626
637
|
.chrome { display: flex; align-items: center; gap: .4rem; flex-wrap: wrap; }
|
|
627
638
|
.chrome-btn { font-family: ${SERIF_STACK}; font-size: .8rem; color: var(--muted); border: 1px solid var(--line); border-radius: 99px; padding: .22rem .75rem; background: var(--card); text-decoration: none; display: inline-flex; align-items: center; gap: .32rem; white-space: nowrap; line-height: 1.35; }
|
|
@@ -691,14 +702,15 @@ ${SHARE_OVERLAY_CSS}
|
|
|
691
702
|
<body>
|
|
692
703
|
${shareOverlayHtml({ withTape: true })}
|
|
693
704
|
<div class="chatCol">
|
|
705
|
+
<h1 class="visually-hidden">talk to it</h1>
|
|
694
706
|
<header class="topbar">
|
|
695
707
|
<div class="brand">
|
|
696
|
-
<span class="eyebrow"
|
|
708
|
+
<span class="eyebrow">${demoEyebrowHtml("chat", "chat")}</span>
|
|
697
709
|
</div>
|
|
698
710
|
<div class="chrome">
|
|
699
|
-
<span class="fact-pill" id="factPill" aria-live="polite"
|
|
711
|
+
<span class="fact-pill" id="factPill" aria-live="polite" data-state="loading"
|
|
700
712
|
title="every fact this session's memory holds right now — the starter memory it shipped with plus anything you have taught, researched or ingested">
|
|
701
|
-
<span class="fact-pill-value" id="factPillValue"
|
|
713
|
+
<span class="fact-pill-value" id="factPillValue">loading</span><span id="factPillUnit"> starter memory…</span>
|
|
702
714
|
</span>
|
|
703
715
|
<button type="button" class="state-pill" id="statePill" data-tone="idle"
|
|
704
716
|
title="the shared-world connection; click to open the network panel">
|
|
@@ -777,6 +789,7 @@ ${shareOverlayHtml({ withTape: true })}
|
|
|
777
789
|
const statsSummaryLine = ${statsSummaryLine.toString()};
|
|
778
790
|
const clearSiteAssetCaches = ${clearSiteAssetCaches.toString()};
|
|
779
791
|
const fetchWithProgress = ${fetchWithProgress.toString()};
|
|
792
|
+
const loadSeedPayload = ${loadSeedPayload.toString()};
|
|
780
793
|
const renderStatsPanelInto = ${renderStatsPanelInto.toString()};
|
|
781
794
|
const createTicker = ${createTicker.toString()};
|
|
782
795
|
const prefersReducedMotion = ${prefersReducedMotion.toString()};
|
|
@@ -808,7 +821,9 @@ ${shareOverlayHtml({ withTape: true })}
|
|
|
808
821
|
const inputEl = el("composerInput");
|
|
809
822
|
const sendBtn = el("composerSend");
|
|
810
823
|
const statusEl = el("status");
|
|
824
|
+
const factPillEl = el("factPill");
|
|
811
825
|
const factPillValueEl = el("factPillValue");
|
|
826
|
+
const factPillUnitEl = el("factPillUnit");
|
|
812
827
|
const statsPanelEl = el("statsPanelStats");
|
|
813
828
|
const researchedPanelEl = el("researchedPanel");
|
|
814
829
|
const wikiModeFieldset = el("wikiMode");
|
|
@@ -1017,29 +1032,74 @@ ${shareOverlayHtml({ withTape: true })}
|
|
|
1017
1032
|
// seed to hash (the desktop shell's own render).
|
|
1018
1033
|
const SEED_STAMP = ${JSON.stringify(seedStamp)};
|
|
1019
1034
|
const SEED_QUERY = SEED_STAMP ? "?b=" + SEED_STAMP : "";
|
|
1035
|
+
// The exact byte count of the chat-seed.json this build shipped, measured by
|
|
1036
|
+
// the builder. It is the denominator the progress percentage needs and the
|
|
1037
|
+
// response itself cannot supply: over a compressed transfer Content-Length
|
|
1038
|
+
// names the wire size, while the stream a reader drains yields decompressed
|
|
1039
|
+
// bytes, so dividing one by the other would read past 100% within a second.
|
|
1040
|
+
// Both numbers here are decompressed bytes, so the ratio is a real fraction
|
|
1041
|
+
// of a real file, never an estimate against elapsed time.
|
|
1042
|
+
const SEED_BYTES = ${Number(seedBytes) || 0};
|
|
1020
1043
|
|
|
1021
1044
|
let seedPayload = null;
|
|
1022
1045
|
let seedFacts = 0;
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1046
|
+
let seedLoadedBytes = 0;
|
|
1047
|
+
let seedTotalBytes = SEED_BYTES;
|
|
1048
|
+
let seedPercentShown = -1;
|
|
1049
|
+
|
|
1050
|
+
/**
|
|
1051
|
+
* What the starter memory is doing right now, published as tmct.seed for the
|
|
1052
|
+
* page's own pill and for anything driving this page:
|
|
1053
|
+
*
|
|
1054
|
+
* loading the seed is still coming down (percent, when a real total exists)
|
|
1055
|
+
* indexing it arrived and parsed; the session is being opened from it
|
|
1056
|
+
* ready the session holds it — a question now grounds against the whole seed
|
|
1057
|
+
* failed it did not arrive; this session starts empty, and says so
|
|
1058
|
+
* skipped nothing was asked for (ingest's seed toggle, off)
|
|
1059
|
+
*
|
|
1060
|
+
* There is no moment before "ready" that the page refuses to speak: a
|
|
1061
|
+
* question asked mid-load is answered against what the store actually holds
|
|
1062
|
+
* at that moment, which is the open-world assumption doing its job. The
|
|
1063
|
+
* phases exist so the page can SAY the graph is still growing, and so a test
|
|
1064
|
+
* that asserts on seeded content can wait for the seed to be in.
|
|
1065
|
+
*/
|
|
1066
|
+
function setSeedPhase(phase, extra) {
|
|
1067
|
+
window.tmct.seed = Object.assign({ state: phase, facts: seedFacts }, extra || {});
|
|
1068
|
+
renderFactPill();
|
|
1069
|
+
}
|
|
1070
|
+
setSeedPhase("loading");
|
|
1071
|
+
|
|
1072
|
+
function noteSeedBytes(loaded, total) {
|
|
1073
|
+
seedLoadedBytes = loaded;
|
|
1074
|
+
seedTotalBytes = total;
|
|
1075
|
+
const percent = seedProgressPercent();
|
|
1076
|
+
// One DOM write per whole percentage point, not one per streamed chunk.
|
|
1077
|
+
if (percent === seedPercentShown) return;
|
|
1078
|
+
seedPercentShown = percent;
|
|
1079
|
+
renderFactPill();
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
/** The seed's real progress, 0-100, or null when no true total is known —
|
|
1083
|
+
* the pill then shows that it is loading and no number at all, rather than
|
|
1084
|
+
* a figure inferred from how long it has been going. */
|
|
1085
|
+
function seedProgressPercent() {
|
|
1086
|
+
if (!(seedTotalBytes > 0)) return null;
|
|
1087
|
+
return Math.min(100, Math.floor((seedLoadedBytes / seedTotalBytes) * 100));
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1028
1090
|
async function fetchSeed() {
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
console.warn("tmct chat: chat-seed.json unavailable — starting unseeded", err);
|
|
1040
|
-
}
|
|
1041
|
-
}
|
|
1091
|
+
const outcome = await loadSeedPayload(fetchWithProgress, "./chat-seed.json", SEED_QUERY, (loaded, total) => {
|
|
1092
|
+
noteProgress("seed", loaded, total || SEED_BYTES);
|
|
1093
|
+
noteSeedBytes(loaded, total || SEED_BYTES);
|
|
1094
|
+
});
|
|
1095
|
+
seedPayload = outcome.payload;
|
|
1096
|
+
seedFacts = outcome.status.facts;
|
|
1097
|
+
if (outcome.status.state === "failed") {
|
|
1098
|
+
console.error("tmct chat: chat-seed.json unavailable — starting unseeded (" + outcome.status.error + ")");
|
|
1099
|
+
setSeedPhase("failed", { error: outcome.status.error, attempts: outcome.status.attempts });
|
|
1100
|
+
return;
|
|
1042
1101
|
}
|
|
1102
|
+
setSeedPhase("indexing", { attempts: outcome.status.attempts });
|
|
1043
1103
|
}
|
|
1044
1104
|
const cloneSeed = () => {
|
|
1045
1105
|
if (!seedPayload) return null;
|
|
@@ -1151,7 +1211,7 @@ ${shareOverlayHtml({ withTape: true })}
|
|
|
1151
1211
|
catch { return; }
|
|
1152
1212
|
}
|
|
1153
1213
|
lastStatsTotal = Number(stats.total || 0);
|
|
1154
|
-
|
|
1214
|
+
renderFactPill();
|
|
1155
1215
|
renderStatsPanelInto(statsPanelEl, stats, {
|
|
1156
1216
|
bandLabel: bandLabelFor,
|
|
1157
1217
|
onForget: persist ? forgetEverything : null,
|
|
@@ -1258,6 +1318,41 @@ ${shareOverlayHtml({ withTape: true })}
|
|
|
1258
1318
|
return liveReference === "always" ? "always" : liveReference === "supplement" ? "supplement" : liveReference ? "on" : "off";
|
|
1259
1319
|
}
|
|
1260
1320
|
|
|
1321
|
+
/** The header pill, in whichever of its three states the page is in: the
|
|
1322
|
+
* starter memory still streaming in, the live fact count once there is one,
|
|
1323
|
+
* or a starter memory that never arrived. It is the same pill throughout —
|
|
1324
|
+
* the count it settles on replaces the loading words in place, so a visitor
|
|
1325
|
+
* watching it sees the memory arrive rather than a separate spinner
|
|
1326
|
+
* vanishing. */
|
|
1327
|
+
function renderFactPill() {
|
|
1328
|
+
const seedState = window.tmct.seed ? window.tmct.seed.state : "loading";
|
|
1329
|
+
if (seedState === "loading") {
|
|
1330
|
+
const percent = seedProgressPercent();
|
|
1331
|
+
factPillEl.dataset.state = "loading";
|
|
1332
|
+
factPillValueEl.textContent = percent === null ? "loading" : percent + "%";
|
|
1333
|
+
factPillUnitEl.textContent = percent === null ? " starter memory\\u2026" : " of starter memory";
|
|
1334
|
+
return;
|
|
1335
|
+
}
|
|
1336
|
+
// "ready" lands a moment before the first memoryStats read returns, and
|
|
1337
|
+
// there is no count to show until it does. Holding the indexing words over
|
|
1338
|
+
// that gap keeps the pill from flashing a zero it does not mean.
|
|
1339
|
+
if (seedState === "indexing" || (seedState === "ready" && lastStatsTotal === null)) {
|
|
1340
|
+
factPillEl.dataset.state = "loading";
|
|
1341
|
+
factPillValueEl.textContent = "indexing";
|
|
1342
|
+
factPillUnitEl.textContent = " " + seedFacts.toLocaleString() + " facts\\u2026";
|
|
1343
|
+
return;
|
|
1344
|
+
}
|
|
1345
|
+
if (seedState === "failed" && lastStatsTotal === null) {
|
|
1346
|
+
factPillEl.dataset.state = "failed";
|
|
1347
|
+
factPillValueEl.textContent = "no";
|
|
1348
|
+
factPillUnitEl.textContent = " starter memory";
|
|
1349
|
+
return;
|
|
1350
|
+
}
|
|
1351
|
+
factPillEl.dataset.state = seedState === "failed" ? "failed" : "ready";
|
|
1352
|
+
factPillValueEl.textContent = Number(lastStatsTotal || 0).toLocaleString();
|
|
1353
|
+
factPillUnitEl.textContent = " facts";
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1261
1356
|
function renderStatus() {
|
|
1262
1357
|
const seedPart = seedPayload
|
|
1263
1358
|
? "starter memory: " + seedFacts + " facts"
|
|
@@ -2320,6 +2415,9 @@ ${shareOverlayHtml({ withTape: true })}
|
|
|
2320
2415
|
} else {
|
|
2321
2416
|
await newSession();
|
|
2322
2417
|
}
|
|
2418
|
+
// The session now holds whatever the seed brought, so the phase settles
|
|
2419
|
+
// here rather than when the bytes landed: "ready" means queryable.
|
|
2420
|
+
if (window.tmct.seed.state !== "failed") setSeedPhase("ready");
|
|
2323
2421
|
const stats = await window.tmct.page.memoryStats(window.tmct.session.memoryDir);
|
|
2324
2422
|
if (savedRecord) restoredCount = stats.taught.length;
|
|
2325
2423
|
const restoredNote = savedRecord
|
|
@@ -40,6 +40,12 @@ export const SESSION_LOG_DIR = ".tmct";
|
|
|
40
40
|
/** The base (no-focus) prompt. With a focus set the shell shows `tmct(label)>`. */
|
|
41
41
|
export const PROMPT = "tmct> ";
|
|
42
42
|
|
|
43
|
+
/** What a teach or a retract carries in a session that keeps nothing (--ephemeral,
|
|
44
|
+
* tmct.toml's [graph] read_only, or the "memory" backend). */
|
|
45
|
+
export const DISCARDED_WRITE_NOTE =
|
|
46
|
+
"(this session keeps nothing — the fact is gone when it ends. Run without --ephemeral, "
|
|
47
|
+
+ "or on a stored backend, to keep it.)";
|
|
48
|
+
|
|
43
49
|
// ---- repo-root resolution: default the target to the GIT ROOT, not raw cwd ----
|
|
44
50
|
|
|
45
51
|
/** The git top-level for `cwd`, or null if not in a repo (or git is unavailable).
|
|
@@ -142,6 +148,12 @@ export async function createSession({
|
|
|
142
148
|
// lifetime (four mud.html characters means four sessions, not one session
|
|
143
149
|
// switching identity turn to turn).
|
|
144
150
|
actingSubject,
|
|
151
|
+
// The session-log filesystem seam: mkdir/createWriteStream, injectable so a
|
|
152
|
+
// test can exercise the unwritable-log guard by rejecting at the seam
|
|
153
|
+
// itself rather than by making a real directory unwritable — chmod-based
|
|
154
|
+
// unwritability is invisible to a process running as root (CI's container
|
|
155
|
+
// user), which the real filesystem can't be made to enforce portably.
|
|
156
|
+
logFs = { mkdir, createWriteStream },
|
|
145
157
|
// Marks a world already LIVE for this session, bypassing openAdventure's
|
|
146
158
|
// "play <world>" opener — that opener requires a shipped "player"
|
|
147
159
|
// individual (its own protection against mis-firing on a non-adventure
|
|
@@ -250,6 +262,8 @@ export async function createSession({
|
|
|
250
262
|
// Ephemeral: keep config.graphFile pointing at the READ graph, but divert the
|
|
251
263
|
// write base (repo → logs/memory/sessions) to a throwaway temp dir. The committed
|
|
252
264
|
// target is never touched; the demo's memory simply doesn't persist across runs.
|
|
265
|
+
// The banner still names the repo the user asked about, not the scratch dir.
|
|
266
|
+
const readRepo = repo;
|
|
253
267
|
if (ephemeral) repo = await mkdtemp(join(tmpdir(), "tmct-ephemeral-"));
|
|
254
268
|
|
|
255
269
|
// Load the graph once up front — the banner needs the module count, and focus/`it`
|
|
@@ -286,12 +300,28 @@ export async function createSession({
|
|
|
286
300
|
const sessionId = uuidv7();
|
|
287
301
|
const logDir = join(repo, SESSION_LOG_DIR);
|
|
288
302
|
const sessionsDir = join(repo, SESSIONS_DIR_REL);
|
|
289
|
-
|
|
290
|
-
|
|
303
|
+
// Every session records what it answered, so an unwritable target is the end
|
|
304
|
+
// of the session. Say so once, in one place, whichever step hits it: the
|
|
305
|
+
// mkdir, the stream's open, or the header write below.
|
|
306
|
+
const unwritable = (e) => new Error(
|
|
307
|
+
`cannot write the session log under ${logDir} (${e?.code || e?.message || e}). `
|
|
308
|
+
+ "Every session records what it answered, so chat needs write access there. "
|
|
309
|
+
+ "Point --repo at a writable directory, or run with --ephemeral to keep the session in a temp dir.",
|
|
310
|
+
);
|
|
311
|
+
try {
|
|
312
|
+
await logFs.mkdir(logDir, { recursive: true });
|
|
313
|
+
await logFs.mkdir(sessionsDir, { recursive: true });
|
|
314
|
+
} catch (e) { throw unwritable(e); }
|
|
291
315
|
const logFile = join(logDir, `session-${sessionId}.md`);
|
|
292
316
|
const sidecarFile = join(sessionsDir, `session-${sessionId}.jsonl`);
|
|
293
|
-
const stream = createWriteStream(logFile, { flags: "a" });
|
|
294
|
-
const sidecar = createWriteStream(sidecarFile, { flags: "a" });
|
|
317
|
+
const stream = logFs.createWriteStream(logFile, { flags: "a" });
|
|
318
|
+
const sidecar = logFs.createWriteStream(sidecarFile, { flags: "a" });
|
|
319
|
+
// A stream with no "error" listener turns a failed open into an unhandled
|
|
320
|
+
// 'error' event, which is a raw Node stack trace and a dead process. The
|
|
321
|
+
// write callbacks below reject with the same error, so the listener only has
|
|
322
|
+
// to keep the event handled.
|
|
323
|
+
stream.on("error", () => {});
|
|
324
|
+
sidecar.on("error", () => {});
|
|
295
325
|
// Awaited writes: each chunk is handed to the OS before the turn completes, so a
|
|
296
326
|
// killed session keeps everything up to the last completed turn — in both files.
|
|
297
327
|
const flush = (s, text) =>
|
|
@@ -300,8 +330,14 @@ export async function createSession({
|
|
|
300
330
|
const writeSidecar = (obj) => flush(sidecar, JSON.stringify(obj) + "\n");
|
|
301
331
|
|
|
302
332
|
const startIso = new Date().toISOString();
|
|
303
|
-
|
|
304
|
-
|
|
333
|
+
try {
|
|
334
|
+
await writeLog(sessionLogHeaderMarkdown({ version, sessionId, startedAt: startIso, repo }));
|
|
335
|
+
await writeSidecar({ type: "session", id: sessionId, started: startIso, repo, tmctVersion: version });
|
|
336
|
+
} catch (e) {
|
|
337
|
+
stream.destroy();
|
|
338
|
+
sidecar.destroy();
|
|
339
|
+
throw unwritable(e);
|
|
340
|
+
}
|
|
305
341
|
|
|
306
342
|
// Read-time graph upsert (sessions.mjs): after every turn, the session becomes /
|
|
307
343
|
// stays a first-class Session individual in graph.json (crash-safe: turn n is in
|
|
@@ -357,12 +393,20 @@ export async function createSession({
|
|
|
357
393
|
// entities (the degenerate trap). Both get orienting, non-over-promising banner
|
|
358
394
|
// + greeting messaging rather than a silent dead-end.
|
|
359
395
|
const noCodeGraph = moduleCount === 0;
|
|
396
|
+
// A discarding session must not say the conversation is kept. Ephemeral
|
|
397
|
+
// diverts every write to a temp dir and suppresses the graph upsert; the
|
|
398
|
+
// "memory" backend keeps the store in-process. Both end when the process does.
|
|
399
|
+
const discardsWrites = ephemeral || backendChoice === "memory";
|
|
400
|
+
const whereItGoes = discardsWrites
|
|
401
|
+
? "nothing is written back — this session's facts and log are dropped when it ends"
|
|
402
|
+
: `the conversation is remembered to ${DEFAULT_GRAPH_REL} — log ${logFile}`;
|
|
360
403
|
const bannerLines = [
|
|
361
404
|
noCodeGraph
|
|
362
405
|
// No code graph: honest, orienting messaging — never an error before the prompt.
|
|
363
|
-
? `tmct chat — ${
|
|
364
|
-
|
|
365
|
-
: `tmct chat — ${
|
|
406
|
+
? `tmct chat — ${readRepo} — no code graph loaded — ${empty ? "starting empty" : "graph has no code entities"}; ` +
|
|
407
|
+
`${whereItGoes}`
|
|
408
|
+
: `tmct chat — ${readRepo} — ${moduleCount} module(s) — `
|
|
409
|
+
+ (discardsWrites ? `${whereItGoes}` : `log ${logFile}`),
|
|
366
410
|
// the honest seed line appears ONLY on the run that actually seeded — the count
|
|
367
411
|
// is the TOTAL appended, split into the curated SEON ontology + the ConceptNet band
|
|
368
412
|
// (+ any other active extension bundle, e.g. an activated tier-2 corpus).
|
|
@@ -429,7 +473,13 @@ export async function createSession({
|
|
|
429
473
|
turns += 1;
|
|
430
474
|
return { answer: `Something went wrong answering that (${message}). Try rephrasing, or /help.`, end: false, prompt: promptFor(focus) };
|
|
431
475
|
}
|
|
432
|
-
const {
|
|
476
|
+
const { record, focus: nextFocus, last: nextLast, end, narrate: nextNarrate, liveReference: nextLiveReference } = result;
|
|
477
|
+
// "noted — remembered" reports a durable write. In a session that keeps
|
|
478
|
+
// nothing it is the last word on a fact that dies with the process, so
|
|
479
|
+
// say where the fact actually went.
|
|
480
|
+
const answer = discardsWrites && (record.via === "assert" || record.via === "retract")
|
|
481
|
+
? `${result.answer}\n${DISCARDED_WRITE_NOTE}`
|
|
482
|
+
: result.answer;
|
|
433
483
|
focus = nextFocus;
|
|
434
484
|
last = nextLast;
|
|
435
485
|
if ("planState" in result) planState = result.planState;
|