@polycode-projects/the-mechanical-code-talker 2.10.2 → 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.
- package/README.md +66 -10
- package/bin/tmct.mjs +5 -2
- package/package.json +3 -1
- package/src/adapters/memory/core.mjs +8 -1
- package/src/domain/cli-verbs.mjs +2 -0
- package/src/domain/domain.mjs +49 -21
- package/src/domain/memory/trust.mjs +21 -2
- package/src/domain/sense-split.mjs +203 -0
- package/src/services/adventure.mjs +73 -1
- package/src/services/chat-page-viz.mjs +87 -19
- package/src/services/chat-session.mjs +12 -7
- package/src/services/chat.mjs +384 -38
- package/src/services/code-explorer-viz.mjs +16 -2
- package/src/services/extract-facts.mjs +293 -81
- package/src/services/fold.mjs +1 -1
- package/src/services/ingest-viz.mjs +388 -0
- package/src/services/ledger-viz.mjs +110 -0
- package/src/services/session-log-format.mjs +64 -0
- package/src/services/sessions.mjs +56 -22
- package/src/services/spider-fly-turn.mjs +119 -1
- package/src/services/spider-fly-viz.mjs +21 -19
- package/src/surfaces/web/chat-browser-entry.mjs +10 -6
- package/src/surfaces/web/ingest-browser-entry.mjs +126 -0
- package/src/surfaces/web/ledger-browser-entry.mjs +15 -2
- package/src/surfaces/web/memory-ask-browser.bundle.js +128 -119
- package/src/tools/definitions.mjs +14 -0
- package/src/tools/handlers/index.mjs +2 -0
- package/src/tools/handlers/tmct-ingest.mjs +43 -0
- package/src/tools/server.mjs +5 -2
|
@@ -408,9 +408,21 @@ const BACKGROUND_FACT_PHRASES = {
|
|
|
408
408
|
"mgx:atLocation": "is found in",
|
|
409
409
|
};
|
|
410
410
|
|
|
411
|
+
/** True when a fact row is provably owned by a NON-world source (a merged
|
|
412
|
+
* corpus, a reference pack, a taught assert) rather than the loaded world. A
|
|
413
|
+
* row with no provenance is not "non-world" — a hand-built digest view carries
|
|
414
|
+
* none, and the room-look filter keeps those. World facts and their @turn
|
|
415
|
+
* snapshots tag as `world:<name>[:turnN]`. */
|
|
416
|
+
function isNonWorldSourced(row) {
|
|
417
|
+
const prov = String(row.provenance || "").trim();
|
|
418
|
+
return prov !== "" && !prov.startsWith("world:");
|
|
419
|
+
}
|
|
420
|
+
|
|
411
421
|
/** The digest's fact view: current placements (folded), exits, typing and
|
|
412
422
|
* every other surviving fact, with phrase predicates and sentence-cased
|
|
413
|
-
* subjects so the pipeline's sentence splitter sees real sentences.
|
|
423
|
+
* subjects so the pipeline's sentence splitter sees real sentences. Room text
|
|
424
|
+
* is world-sourced only — a merged corpus's overlap on a room's own vocabulary
|
|
425
|
+
* never leaks into the description. Pure. */
|
|
414
426
|
export function worldDigestRows(rows, state) {
|
|
415
427
|
const out = [];
|
|
416
428
|
const seen = new Set();
|
|
@@ -440,6 +452,12 @@ export function worldDigestRows(rows, state) {
|
|
|
440
452
|
}
|
|
441
453
|
for (const row of rows || []) {
|
|
442
454
|
if (SNAPSHOT_RE.test(row.subject)) continue; // folded above
|
|
455
|
+
// Room text comes from the world source only. A merged corpus overlaps a
|
|
456
|
+
// room's own vocabulary ("library rdfs:subClassOf literary study"), and
|
|
457
|
+
// without this those rows leak into the room description as stray sentences.
|
|
458
|
+
// A row with no provenance (a hand-built test view) is kept — the filter
|
|
459
|
+
// only drops rows a non-world source provably owns.
|
|
460
|
+
if (isNonWorldSourced(row)) continue;
|
|
443
461
|
if (PLACEMENT_PREDICATES.has(row.predicate)) continue; // folded above
|
|
444
462
|
if (VIEW_EXCLUDED_PREDICATES.has(row.predicate)) continue;
|
|
445
463
|
const exit = EXIT_PREDICATE_RE.exec(row.predicate);
|
|
@@ -909,6 +927,58 @@ async function worldOpennessAnswer(line, { memoryDir }) {
|
|
|
909
927
|
);
|
|
910
928
|
}
|
|
911
929
|
|
|
930
|
+
// The in-game orientation asides — "where am I", "what can I do", "what is the
|
|
931
|
+
// quest/goal". Without a world-state answer these fall through to the ordinary
|
|
932
|
+
// lanes and misroute: "where am I" reads "I" as a module name, "what can I do"
|
|
933
|
+
// walls, and "what is the goal" answers from corpus vocabulary about the word
|
|
934
|
+
// "goal". A live world answers each from its own fold first.
|
|
935
|
+
const WORLD_WHERE_AM_I_RE = /^where\s+am\s+i(?:\s+now)?[?.!\s]*$/i;
|
|
936
|
+
const WORLD_OPTIONS_RE = /^(?:what\s+can\s+i\s+do(?:\s+(?:here|now))?|what\s+are\s+my\s+options|what\s+(?:should|do)\s+i\s+do(?:\s+(?:here|now))?|what\s+now)[?.!\s]*$/i;
|
|
937
|
+
const WORLD_QUEST_RE = /^(?:what(?:'s|\s+is)\s+(?:the\s+|my\s+)?(?:quest|goal|objective|mission|aim)|what\s+am\s+i\s+(?:trying\s+to\s+do|(?:supposed|meant)\s+to\s+do)|what\s+do\s+i\s+do\s+here)[?.!\s]*$/i;
|
|
938
|
+
|
|
939
|
+
/** The in-game orientation asides, answered from the world fold: the player's
|
|
940
|
+
* room, the room's real affordances, and the world's objective. Null when the
|
|
941
|
+
* line is none of them, so an ordinary question keeps its lane. */
|
|
942
|
+
async function worldContextAnswer(line, { memoryDir }) {
|
|
943
|
+
const l = String(line).trim();
|
|
944
|
+
const asksWhere = WORLD_WHERE_AM_I_RE.test(l);
|
|
945
|
+
const asksOptions = WORLD_OPTIONS_RE.test(l);
|
|
946
|
+
const asksQuest = WORLD_QUEST_RE.test(l);
|
|
947
|
+
if (!asksWhere && !asksOptions && !asksQuest) return null;
|
|
948
|
+
let rows;
|
|
949
|
+
try { rows = readFactRows(await loadMemory(memoryDir)); } catch { return null; }
|
|
950
|
+
const state = foldWorldState(rows);
|
|
951
|
+
const here = state.placements.get("player")?.object ?? null;
|
|
952
|
+
|
|
953
|
+
if (asksWhere) {
|
|
954
|
+
return here
|
|
955
|
+
? answer(`you are in the ${here}.`, "ADVENTURE — where-am-I aside: the player's own room from the current world fold", { goal: "check where you are" })
|
|
956
|
+
: answer("the world has no written player position yet.", "ADVENTURE — where-am-I aside: no player placement", { miss: true, goal: "check where you are" });
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
if (asksOptions) {
|
|
960
|
+
const actions = here ? roomAffordances(rows, state, here) : [];
|
|
961
|
+
return answer(
|
|
962
|
+
actions.length ? `you can: ${actions.join(", ")}.` : `nothing obvious here — say "look" to look around${here ? ` the ${here}` : ""}.`,
|
|
963
|
+
`ADVENTURE — options aside: the ${here}'s roomAffordances, the same list "look" appends`,
|
|
964
|
+
{ goal: "see what you can do here" },
|
|
965
|
+
);
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
const objectiveId = rows.find((r) => r.predicate === "mgx:is-objective" && r.object === "true")?.subject ?? null;
|
|
969
|
+
return objectiveId
|
|
970
|
+
? answer(
|
|
971
|
+
`your goal is to find the ${objectiveId} and pick it up.`,
|
|
972
|
+
`ADVENTURE — quest aside: the world's objective (${objectiveId}), named without spoiling where it is`,
|
|
973
|
+
{ goal: `find the ${objectiveId}` },
|
|
974
|
+
)
|
|
975
|
+
: answer(
|
|
976
|
+
`this world sets no explicit goal — explore it, and say "look" to see your options.`,
|
|
977
|
+
"ADVENTURE — quest aside: no objective marker in this world",
|
|
978
|
+
{ goal: "explore the world" },
|
|
979
|
+
);
|
|
980
|
+
}
|
|
981
|
+
|
|
912
982
|
async function inventoryAnswer({ memoryDir, graph }) {
|
|
913
983
|
const memory = await loadMemory(memoryDir);
|
|
914
984
|
const rows = readFactRows(memory);
|
|
@@ -1033,5 +1103,7 @@ export async function adventureTurn(line, { planHolder, memoryDir, sessionId = "
|
|
|
1033
1103
|
if (whereAside) return whereAside;
|
|
1034
1104
|
const opennessAside = await worldOpennessAnswer(line, { memoryDir });
|
|
1035
1105
|
if (opennessAside) return opennessAside;
|
|
1106
|
+
const contextAside = await worldContextAnswer(line, { memoryDir });
|
|
1107
|
+
if (contextAside) return contextAside;
|
|
1036
1108
|
return null; // a mid-game aside — the ordinary lanes answer, world untouched
|
|
1037
1109
|
}
|
|
@@ -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
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
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
|
-
*
|
|
126
|
-
*
|
|
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
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
|
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>→</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’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
|
-
|
|
767
|
-
|
|
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
|
|
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}.
|
|
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(
|
|
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(
|
|
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,
|
|
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
|
-
|
|
382
|
-
|
|
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
|
-
|
|
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));
|