@polycode-projects/the-mechanical-code-talker 2.10.3 → 2.10.5

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.
@@ -30,6 +30,7 @@
30
30
  // exist (both built earlier in that same script, for the embedded widget).
31
31
  import { THEME_TOKENS_CSS, SERIF_STACK, MONO_STACK, escapeHtml } from "./viz-theme.mjs";
32
32
  import { provBucketFor } from "./ledger-viz.mjs";
33
+ import { sessionLogTimeOfDay, sessionLogHeaderMarkdown, sessionLogTurnMarkdown } from "./session-log-format.mjs";
33
34
 
34
35
  const DEFAULT_TITLE = "the-mechanical-code-talker — talk to it";
35
36
 
@@ -115,25 +116,37 @@ export function loadProgressLine(parts) {
115
116
  }
116
117
 
117
118
  /**
118
- * The exported transcript as one Markdown document: a title line naming the
119
- * site version and the export date, then every turn in order as
120
- * "**you:** ..." / "**tmct:** ...", with the provenance tier in parentheses
121
- * when the turn carried one. Reads the page's transcript MODEL (an array of
122
- * { role, text, chipTier }), never the DOM the message column may
123
- * virtualize long chats someday, and an export must still carry every turn.
119
+ * The exported transcript as ONE Markdown document, in the SAME shape the
120
+ * Node CLI/TUI's own .tmct/session-<id>.md writes (session-log-format.mjs,
121
+ * spliced in beside this function below): a title naming the version and a
122
+ * short session id, one heading per turn at millisecond time-of-day
123
+ * precision, the question as a verbatim blockquote, the answer in a fenced
124
+ * block. No closing session-end line unlike a CLI session's close(), an
125
+ * export can happen mid-conversation, before anything has actually ended.
124
126
  *
125
- * Self-contained (no outer refs), `.toString()`-splice safe the same
126
- * discipline provenanceChipFor/loadProgressLine above hold.
127
+ * Reads the page's transcript MODEL (an array alternating { role: "you" |
128
+ * "tmct", text, chipTier, ts }, one entry per submit and per settled
129
+ * reply), never the DOM — the message column may virtualize long chats
130
+ * someday, and an export must still carry every turn.
131
+ *
132
+ * `headerMd`/`turnMd` are the injected session-log-format.mjs builders
133
+ * (spliced in as their own consts alongside this function) — injected
134
+ * rather than imported so this function stays `.toString()`-splice safe,
135
+ * the same discipline provenanceChipFor's injected `bucketFor` holds.
127
136
  */
128
- export function transcriptMarkdown(turns, meta) {
137
+ export function transcriptMarkdown(turns, meta, headerMd, turnMd) {
129
138
  const version = (meta && meta.version) || "dev";
130
- const date = (meta && meta.date) || "";
131
- const lines = ["# tmct chat — v" + version + (date ? " — " + date : ""), ""];
132
- for (const turn of turns || []) {
133
- const tier = turn.chipTier ? " (" + turn.chipTier + ")" : "";
134
- lines.push("**" + turn.role + ":** " + turn.text + tier, "");
139
+ const sessionId = (meta && meta.sessionId) || "";
140
+ const list = turns || [];
141
+ let doc = headerMd({ version: version, sessionId: sessionId, startedAt: list.length ? list[0].ts : Date.now() });
142
+ let turnNumber = 0;
143
+ for (let i = 0; i < list.length; i += 1) {
144
+ if (list[i].role !== "you") continue;
145
+ turnNumber += 1;
146
+ const reply = list[i + 1] && list[i + 1].role === "tmct" ? list[i + 1] : null;
147
+ doc += turnMd({ startedAt: list[i].ts, turnNumber: turnNumber, query: list[i].text, answer: reply ? reply.text : "" });
135
148
  }
136
- return lines.join("\n");
149
+ return doc;
137
150
  }
138
151
 
139
152
  /** The self-contained "talk to it" full-screen page. Pure — the same output
@@ -304,12 +317,14 @@ ${THEME_TOKENS_CSS}
304
317
  <button type="submit" id="composerSend" aria-label="Send" disabled>&#8594;</button>
305
318
  </div>
306
319
  <div class="composer-tools">
307
- <label class="liveLabel" title="Off by default. When on, a question nothing local can answer also asks en.wikipedia.org — two small requests per lookup, and the answer is cited (CC BY-SA).">
320
+ <label class="liveLabel" title="Off by default. When on, a question nothing local can answer also asks en.wikipedia.org — two small requests per lookup, and the answer is cited (CC BY-SA). Type /wiki supplement to also add a cited Wikipedia read-out under every grounded answer.">
308
321
  <input type="checkbox" id="liveToggle" role="switch" aria-label="ask Wikipedia when I don't know">
309
322
  <span class="toggle-track" aria-hidden="true"><span class="toggle-knob"></span></span>
310
323
  <span>ask Wikipedia when I don&#8217;t know</span>
311
324
  </label>
312
325
  <span class="tool-cluster">
326
+ <button type="button" id="ingestFile" class="tool-btn" title="load a .txt/.md file and teach every fact it recognizes into this session">ingest file</button>
327
+ <input type="file" id="ingestInput" accept=".txt,.md,text/plain,text/markdown" hidden>
313
328
  <button type="button" id="exportMd" class="tool-btn" title="download this conversation as Markdown">export .md</button>
314
329
  <button type="button" id="exportFacts" class="tool-btn" title="download this session's facts as JSONL (the tmct extract shape, provenance included)">export facts</button>
315
330
  <button type="button" id="printChat" class="tool-btn" title="print the whole conversation">print</button>
@@ -329,6 +344,9 @@ ${THEME_TOKENS_CSS}
329
344
  const provBucketFor = ${provBucketFor.toString()};
330
345
  const provenanceChipFor = ${provenanceChipFor.toString()};
331
346
  const loadProgressLine = ${loadProgressLine.toString()};
347
+ const sessionLogTimeOfDay = ${sessionLogTimeOfDay.toString()};
348
+ const sessionLogHeaderMarkdown = ${sessionLogHeaderMarkdown.toString()};
349
+ const sessionLogTurnMarkdown = ${sessionLogTurnMarkdown.toString()};
332
350
  const transcriptMarkdown = ${transcriptMarkdown.toString()};
333
351
  const el = (id) => document.getElementById(id);
334
352
 
@@ -763,8 +781,14 @@ ${THEME_TOKENS_CSS}
763
781
  window.tmctChatSession.turn(q)
764
782
  .then((result) => {
765
783
  settleAssistantBubble(pendingRow, result.answer, result.record);
766
- if (result.record && result.record.via === "assert") scheduleSave();
767
- return renderStatsPanel(); // a teach turn just grew this session's memory; a plain ask leaves it unchanged either way
784
+ // Persist on ANY store write, not just a teach turn: a learn-on-miss
785
+ // load (a child pack, a reference or live-Wikipedia article) and its
786
+ // auto-synthesis also append facts, and those were lost on reload when
787
+ // only via==="assert" saved. Commands write nothing, so they stay out.
788
+ // The save is debounced, so a read-through that changed nothing costs
789
+ // at most one coalesced write.
790
+ if (result.record && result.record.via !== "command") scheduleSave();
791
+ return renderStatsPanel(); // a teach or learned-load turn grew this session's memory; a plain ask leaves it unchanged either way
768
792
  })
769
793
  .catch((err) => settleAssistantBubble(pendingRow,
770
794
  "something went wrong answering that (" + (err && err.message ? err.message : err) + ") \\u2014 try rephrasing",
@@ -791,7 +815,8 @@ ${THEME_TOKENS_CSS}
791
815
  // @media print stylesheet above to un-pin the message column so every
792
816
  // turn reaches paper.
793
817
  el("exportMd").addEventListener("click", () => {
794
- const md = transcriptMarkdown(transcript, { version: siteVersion, date: new Date(Date.now()).toISOString().slice(0, 10) });
818
+ const sessionId = (window.tmctChatSession && window.tmctChatSession.sessionId) || "";
819
+ const md = transcriptMarkdown(transcript, { version: siteVersion, sessionId: sessionId }, sessionLogHeaderMarkdown, sessionLogTurnMarkdown);
795
820
  const blob = new Blob([md], { type: "text/markdown" });
796
821
  const url = URL.createObjectURL(blob);
797
822
  const link = document.createElement("a");
@@ -829,6 +854,49 @@ ${THEME_TOKENS_CSS}
829
854
  setTimeout(() => URL.revokeObjectURL(url), 1000);
830
855
  });
831
856
 
857
+ // "ingest file" feeds a whole .txt/.md through the SAME session, one
858
+ // sentence at a time (window.tmctChat.splitSentences, then session.turn),
859
+ // teaching every sentence the recognizer grounds and skipping the rest
860
+ // honestly — the same pipeline the ingest page runs, reaching the chat's own
861
+ // memory so the taught facts answer questions straight away.
862
+ el("ingestFile").addEventListener("click", () => el("ingestInput").click());
863
+ el("ingestInput").addEventListener("change", async (e) => {
864
+ const file = e.target.files && e.target.files[0];
865
+ e.target.value = "";
866
+ const session = window.tmctChatSession;
867
+ if (!file || busy || !session || !window.tmctChat.splitSentences) return;
868
+ let text;
869
+ try {
870
+ text = await file.text();
871
+ } catch (err) {
872
+ addSystemLine("couldn't read that file (" + (err && err.message ? err.message : err) + ").");
873
+ return;
874
+ }
875
+ const sentences = window.tmctChat.splitSentences(text);
876
+ if (!sentences.length) { addSystemLine("nothing to ingest in " + file.name + "."); return; }
877
+ setBusy(true);
878
+ statusEl.textContent = "ingesting " + file.name + "\\u2026";
879
+ let grounded = 0;
880
+ try {
881
+ for (const sentence of sentences) {
882
+ const result = await session.turn(sentence);
883
+ if (result.record && result.record.via === "assert" && !result.record.miss) grounded += 1;
884
+ }
885
+ } catch (err) {
886
+ addSystemLine("something went wrong ingesting " + file.name + " (" + (err && err.message ? err.message : err) + ").");
887
+ }
888
+ if (grounded) scheduleSave();
889
+ const skipped = sentences.length - grounded;
890
+ addSystemLine("ingested " + file.name + " \\u2014 " + sentences.length + " sentence"
891
+ + (sentences.length === 1 ? "" : "s") + " read, " + grounded + " fact"
892
+ + (grounded === 1 ? "" : "s") + " added"
893
+ + (skipped ? ", " + skipped + " skipped (not a recognized fact shape)" : "") + ".");
894
+ await renderStatsPanel();
895
+ renderStatus();
896
+ setBusy(false);
897
+ inputEl.focus();
898
+ });
899
+
832
900
  // "reset to seed" is the full re-initialisation: drop the persisted payload
833
901
  // outright and reload, so boot re-seeds from the page's shipped seed as if on
834
902
  // a first visit. Harder than "forget everything", which only swaps the live
@@ -28,6 +28,7 @@ import * as defaultSource from "../adapters/source.mjs";
28
28
  import { resolveExtensions, mergedLexiconExtra } from "./extensions.mjs";
29
29
  import { runTurn, hasSeededVocabulary, vocabExampleHint } from "./chat.mjs";
30
30
  import { resolveGameConfig } from "../domain/game-config.mjs";
31
+ import { sessionLogHeaderMarkdown, sessionLogTurnMarkdown, sessionLogEndMarkdown } from "./session-log-format.mjs";
31
32
 
32
33
  /** Where session logs live, relative to the target repo. `.tmct/` is the repo's
33
34
  * one artifact directory (gitignored, machine-local) — flip this single constant
@@ -243,7 +244,7 @@ export async function createSession({
243
244
  const sessionsDir = join(repo, SESSIONS_DIR_REL);
244
245
  await mkdir(logDir, { recursive: true });
245
246
  await mkdir(sessionsDir, { recursive: true });
246
- const logFile = join(logDir, `session-${sessionId}.log`);
247
+ const logFile = join(logDir, `session-${sessionId}.md`);
247
248
  const sidecarFile = join(sessionsDir, `session-${sessionId}.jsonl`);
248
249
  const stream = createWriteStream(logFile, { flags: "a" });
249
250
  const sidecar = createWriteStream(sidecarFile, { flags: "a" });
@@ -255,7 +256,7 @@ export async function createSession({
255
256
  const writeSidecar = (obj) => flush(sidecar, JSON.stringify(obj) + "\n");
256
257
 
257
258
  const startIso = new Date().toISOString();
258
- await writeLog(`# tmct chat ${version} session started ${startIso} repo ${repo}\n\n`);
259
+ await writeLog(sessionLogHeaderMarkdown({ version, sessionId, startedAt: startIso, repo }));
259
260
  await writeSidecar({ type: "session", id: sessionId, started: startIso, repo, tmctVersion: version });
260
261
 
261
262
  // Read-time graph upsert (sessions.mjs): after every turn, the session becomes /
@@ -363,14 +364,14 @@ export async function createSession({
363
364
  } catch (e) {
364
365
  const ts = new Date().toISOString();
365
366
  const message = e instanceof Error ? e.message : String(e);
366
- await writeLog(`${ts}\n> ${line}\nerror: ${message}\n`);
367
+ await writeLog(sessionLogTurnMarkdown({ startedAt: ts, turnNumber: turns + 1, query: line, answer: `error: ${message}` }));
367
368
  const errorRecord = { type: "error", ts, query: line, error: message };
368
369
  await writeSidecar(errorRecord);
369
370
  turnRecords.push(errorRecord);
370
371
  turns += 1;
371
372
  return { answer: `Something went wrong answering that (${message}). Try rephrasing, or /help.`, end: false, prompt: promptFor(focus) };
372
373
  }
373
- const { answer, logLines, record, focus: nextFocus, last: nextLast, end, narrate: nextNarrate, liveReference: nextLiveReference } = result;
374
+ const { answer, record, focus: nextFocus, last: nextLast, end, narrate: nextNarrate, liveReference: nextLiveReference } = result;
374
375
  focus = nextFocus;
375
376
  last = nextLast;
376
377
  if ("planState" in result) planState = result.planState;
@@ -378,8 +379,10 @@ export async function createSession({
378
379
  // same way a focus update does — apply them to this handle's
379
380
  // session-scoped state.
380
381
  if (typeof nextNarrate === "boolean") narrateOn = nextNarrate;
381
- if (typeof nextLiveReference === "boolean") liveReferenceOn = nextLiveReference;
382
- await writeLog(logLines.join("\n") + "\n");
382
+ // tri-state: false (off), true (rescue on a miss), or "supplement" (also
383
+ // append a cited read-out under every grounded answer).
384
+ if (typeof nextLiveReference === "boolean" || nextLiveReference === "supplement") liveReferenceOn = nextLiveReference;
385
+ await writeLog(sessionLogTurnMarkdown({ startedAt: record.ts, turnNumber: turns + 1, query: line, answer }));
383
386
  await writeSidecar(record);
384
387
  turnRecords.push(record);
385
388
  // One telemetry line per dispatched turn (OFF by default → no-op). query.raw is
@@ -401,7 +404,9 @@ export async function createSession({
401
404
  if (closed) return;
402
405
  closed = true;
403
406
  const endIso = new Date().toISOString();
404
- await writeLog(`${endIso}\n> /exit\nsession end ${endIso}\n`);
407
+ const closingTurnNumber = turns + 1;
408
+ await writeLog(sessionLogTurnMarkdown({ startedAt: endIso, turnNumber: closingTurnNumber, query: "/exit", answer: "" }));
409
+ await writeLog(sessionLogEndMarkdown({ endedAt: endIso, turnCount: closingTurnNumber }));
405
410
  await writeSidecar({ type: "end", ts: endIso });
406
411
  await upsertGraph(endIso);
407
412
  await new Promise((resolve) => stream.end(resolve));