@polycode-projects/the-mechanical-code-talker 2.11.6 → 2.11.9
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/bin/tmct.mjs +20 -8
- package/package.json +2 -1
- package/src/adapters/toml-config.mjs +2 -0
- package/src/domain/ask-vocab.mjs +32 -0
- package/src/domain/ask.mjs +89 -4
- package/src/domain/domain.mjs +14 -0
- package/src/domain/interpret/strategies/keywords.mjs +18 -2
- package/src/domain/reference-pack.mjs +15 -3
- package/src/services/adventure-viz.mjs +77 -23
- package/src/services/chat-page-viz.mjs +149 -3
- package/src/services/chat.mjs +207 -47
- package/src/services/extensions.mjs +9 -2
- package/src/services/extract-facts.mjs +74 -7
- package/src/services/init.mjs +21 -2
- package/src/services/ledger-viz.mjs +1 -3
- package/src/services/research-viz.mjs +672 -0
- package/src/surfaces/http/server-http.mjs +172 -3
- package/src/surfaces/web/chat-browser-entry.mjs +21 -1
- package/src/surfaces/web/memory-ask-browser.bundle.js +119 -119
- package/src/surfaces/web/research-browser-entry.mjs +319 -0
|
@@ -117,6 +117,26 @@ export function loadProgressLine(parts) {
|
|
|
117
117
|
: "loading the engine… " + mb(loaded) + " MB";
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
+
/**
|
|
121
|
+
* The "researched this session" panel's own reading of a settled research
|
|
122
|
+
* turn's answer — the passage it read and where it read it, straight off
|
|
123
|
+
* research.mjs's own `renderResearchAnswer` shape (`term — summary (source:
|
|
124
|
+
* research article "title", Simple English Wikipedia, CC BY-SA 4.0 — url)`),
|
|
125
|
+
* never a second fetch: the citation text is already the retrieved passage,
|
|
126
|
+
* this just pulls its pieces apart for display. Returns null on anything
|
|
127
|
+
* else (a miss, a status/stop reply, or text that doesn't open this way) —
|
|
128
|
+
* an honest "nothing to show" rather than a guessed passage.
|
|
129
|
+
*
|
|
130
|
+
* Self-contained, `.toString()`-splice safe — the same discipline every
|
|
131
|
+
* other pure export in this module holds.
|
|
132
|
+
*/
|
|
133
|
+
export function parseResearchAnswer(answer) {
|
|
134
|
+
const text = String(answer || "");
|
|
135
|
+
const m = /^(.*?) — ([\s\S]*?) \(source: research article "([^"]+)", Simple English Wikipedia, CC BY-SA 4\.0 — (\S+)\)/.exec(text);
|
|
136
|
+
if (!m) return null;
|
|
137
|
+
return { term: m[1], passage: m[2], title: m[3], url: m[4] };
|
|
138
|
+
}
|
|
139
|
+
|
|
120
140
|
/**
|
|
121
141
|
* The exported transcript as ONE Markdown document, in the SAME shape the
|
|
122
142
|
* Node CLI/TUI's own .tmct/session-<id>.md writes (session-log-format.mjs,
|
|
@@ -286,6 +306,24 @@ ${THEME_TOKENS_CSS}
|
|
|
286
306
|
.statsPanel .forget-btn:hover { color: var(--ink); }
|
|
287
307
|
.statsPanel .persist-note { color: var(--muted); font-size: .64rem; margin: .4rem 0 0; }
|
|
288
308
|
|
|
309
|
+
/* the "researched this session" panel: its own section under the memory
|
|
310
|
+
stats, filled from window.tmctChat.researchedFactRows() plus each
|
|
311
|
+
settled research turn's own answer text — the passage tmct actually
|
|
312
|
+
read, the article it read it from, and the facts that passage grounded.
|
|
313
|
+
A sibling section, not folded into #statsPanelStats — that div's own
|
|
314
|
+
re-render (renderStatsPanelInto) clears and rebuilds its children on
|
|
315
|
+
every turn, which would wipe this section's own history too if it lived
|
|
316
|
+
inside it. */
|
|
317
|
+
#researchedPanel:not(:empty) { margin-top: 1rem; padding-top: 1rem; border-top: 1px solid var(--line); }
|
|
318
|
+
.statsPanel .researched-item { margin: 0 0 .9rem; padding-bottom: .8rem; border-bottom: 1px dashed var(--line); }
|
|
319
|
+
.statsPanel .researched-item:last-child { border-bottom: none; padding-bottom: 0; margin-bottom: 0; }
|
|
320
|
+
.statsPanel .researched-title { display: block; color: var(--ink); font-weight: 600; }
|
|
321
|
+
.statsPanel .researched-passage { display: block; color: var(--muted); margin: .3rem 0; }
|
|
322
|
+
.statsPanel .researched-link { display: inline-block; margin: 0 0 .3rem; }
|
|
323
|
+
.statsPanel .researched-facts { margin: .3rem 0 0; padding-left: 1.1rem; }
|
|
324
|
+
.statsPanel .researched-facts li { margin: .12rem 0; }
|
|
325
|
+
.statsPanel .researched-none { color: var(--muted); font-style: italic; margin: .3rem 0 0; }
|
|
326
|
+
|
|
289
327
|
@media (max-width: 860px) {
|
|
290
328
|
.statsPanel { display: none; }
|
|
291
329
|
}
|
|
@@ -361,8 +399,9 @@ ${THEME_TOKENS_CSS}
|
|
|
361
399
|
</form>
|
|
362
400
|
<div class="statusline" id="status">loading the engine…</div>
|
|
363
401
|
</div>
|
|
364
|
-
<aside class="statsPanel" id="statsPanel" aria-label="This session's memory">
|
|
365
|
-
<p class="empty">loading memory stats…</p>
|
|
402
|
+
<aside class="statsPanel" id="statsPanel" aria-label="This session's memory and research">
|
|
403
|
+
<div id="statsPanelStats"><p class="empty">loading memory stats…</p></div>
|
|
404
|
+
<div id="researchedPanel"></div>
|
|
366
405
|
</aside>
|
|
367
406
|
<script src="./chat-browser.bundle.js"></script>
|
|
368
407
|
<script>
|
|
@@ -371,6 +410,7 @@ ${THEME_TOKENS_CSS}
|
|
|
371
410
|
const provBucketFor = ${provBucketFor.toString()};
|
|
372
411
|
const provenanceChipFor = ${provenanceChipFor.toString()};
|
|
373
412
|
const loadProgressLine = ${loadProgressLine.toString()};
|
|
413
|
+
const parseResearchAnswer = ${parseResearchAnswer.toString()};
|
|
374
414
|
const sessionLogTimeOfDay = ${sessionLogTimeOfDay.toString()};
|
|
375
415
|
const sessionLogHeaderMarkdown = ${sessionLogHeaderMarkdown.toString()};
|
|
376
416
|
const sessionLogTurnMarkdown = ${sessionLogTurnMarkdown.toString()};
|
|
@@ -390,7 +430,8 @@ ${THEME_TOKENS_CSS}
|
|
|
390
430
|
const inputEl = el("composerInput");
|
|
391
431
|
const sendBtn = el("composerSend");
|
|
392
432
|
const statusEl = el("status");
|
|
393
|
-
const statsPanelEl = el("
|
|
433
|
+
const statsPanelEl = el("statsPanelStats");
|
|
434
|
+
const researchedPanelEl = el("researchedPanel");
|
|
394
435
|
const wikiModeFieldset = el("wikiMode");
|
|
395
436
|
const wikiModeRadios = Array.prototype.slice.call(wikiModeFieldset.querySelectorAll('input[type="radio"]'));
|
|
396
437
|
const synthSliderEl = el("synthSlider");
|
|
@@ -708,6 +749,98 @@ ${THEME_TOKENS_CSS}
|
|
|
708
749
|
});
|
|
709
750
|
}
|
|
710
751
|
|
|
752
|
+
// ---- researched this session: what "research <topic>" has actually read
|
|
753
|
+
// and grounded so far — each entry pairs the passage a settled research
|
|
754
|
+
// turn's own answer cites (parseResearchAnswer, off the SAME "(source:
|
|
755
|
+
// research article ...)" text the chat bubble already shows) with the real
|
|
756
|
+
// facts that turn stored, read back through window.tmctChat.
|
|
757
|
+
// researchedFactRows(memoryDir) rather than re-deriving them from the
|
|
758
|
+
// answer text — the citation names WHERE tmct read, the fact rows name
|
|
759
|
+
// WHAT it kept, and this panel never invents either from the other.
|
|
760
|
+
const researchedEntries = [];
|
|
761
|
+
const researchedFactKeysSeen = new Set();
|
|
762
|
+
|
|
763
|
+
function renderResearchedPanel() {
|
|
764
|
+
researchedPanelEl.textContent = "";
|
|
765
|
+
researchedPanelEl.appendChild(Object.assign(document.createElement("h2"), { textContent: "researched this session" }));
|
|
766
|
+
if (!researchedEntries.length) {
|
|
767
|
+
const empty = document.createElement("p");
|
|
768
|
+
empty.className = "empty";
|
|
769
|
+
empty.textContent = 'nothing yet — ask it to "research <a topic>" and what it reads, with the passage and the facts it grounded, lands here.';
|
|
770
|
+
researchedPanelEl.appendChild(empty);
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
for (const entry of researchedEntries.slice(-8).reverse()) {
|
|
774
|
+
const item = document.createElement("div");
|
|
775
|
+
item.className = "researched-item";
|
|
776
|
+
const title = document.createElement("span");
|
|
777
|
+
title.className = "researched-title";
|
|
778
|
+
title.textContent = entry.title || "(untitled)";
|
|
779
|
+
item.appendChild(title);
|
|
780
|
+
if (entry.passage) {
|
|
781
|
+
const passage = document.createElement("span");
|
|
782
|
+
passage.className = "researched-passage";
|
|
783
|
+
passage.textContent = entry.passage;
|
|
784
|
+
item.appendChild(passage);
|
|
785
|
+
}
|
|
786
|
+
if (entry.url) {
|
|
787
|
+
const link = document.createElement("a");
|
|
788
|
+
link.className = "researched-link";
|
|
789
|
+
link.href = entry.url;
|
|
790
|
+
link.target = "_blank";
|
|
791
|
+
link.rel = "noopener noreferrer";
|
|
792
|
+
link.textContent = "source \\u2197";
|
|
793
|
+
item.appendChild(link);
|
|
794
|
+
}
|
|
795
|
+
if (entry.facts.length) {
|
|
796
|
+
const list = document.createElement("ul");
|
|
797
|
+
list.className = "researched-facts";
|
|
798
|
+
for (const fact of entry.facts) {
|
|
799
|
+
const li = document.createElement("li");
|
|
800
|
+
li.textContent = fact.subject + " " + fact.predicate + " " + fact.object;
|
|
801
|
+
list.appendChild(li);
|
|
802
|
+
}
|
|
803
|
+
item.appendChild(list);
|
|
804
|
+
} else {
|
|
805
|
+
const none = document.createElement("p");
|
|
806
|
+
none.className = "researched-none";
|
|
807
|
+
none.textContent = "no new fact grounded from this passage.";
|
|
808
|
+
item.appendChild(none);
|
|
809
|
+
}
|
|
810
|
+
researchedPanelEl.appendChild(item);
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
/** After a settled, non-miss research turn: read back the facts that turn
|
|
815
|
+
* actually stored (a set-diff against every research fact seen so far, so
|
|
816
|
+
* a later step never re-lists an earlier one's facts) and pair them with
|
|
817
|
+
* this turn's own cited passage. A turn that grounded nothing new (an
|
|
818
|
+
* empty article, or a re-fetch of an already-known one) still gets its
|
|
819
|
+
* own entry — the passage was still read, even where nothing new stuck. */
|
|
820
|
+
async function noteResearchLearned(result) {
|
|
821
|
+
if (result.research === undefined || !result.record || result.record.miss) return;
|
|
822
|
+
if (!window.tmctChat.researchedFactRows || !window.tmctChatSession) return;
|
|
823
|
+
let rows;
|
|
824
|
+
try { rows = await window.tmctChat.researchedFactRows(window.tmctChatSession.memoryDir); }
|
|
825
|
+
catch { return; }
|
|
826
|
+
const newFacts = [];
|
|
827
|
+
for (const row of rows) {
|
|
828
|
+
const key = row.subject + "|" + row.predicate + "|" + row.object;
|
|
829
|
+
if (researchedFactKeysSeen.has(key)) continue;
|
|
830
|
+
researchedFactKeysSeen.add(key);
|
|
831
|
+
newFacts.push(row);
|
|
832
|
+
}
|
|
833
|
+
const parsed = parseResearchAnswer(result.answer);
|
|
834
|
+
if (!parsed && !newFacts.length) return;
|
|
835
|
+
researchedEntries.push({
|
|
836
|
+
title: parsed ? parsed.title : "",
|
|
837
|
+
passage: parsed ? parsed.passage : "",
|
|
838
|
+
url: parsed ? parsed.url : "",
|
|
839
|
+
facts: newFacts,
|
|
840
|
+
});
|
|
841
|
+
renderResearchedPanel();
|
|
842
|
+
}
|
|
843
|
+
|
|
711
844
|
// "supplement" (typed /wiki supplement only) has no radio; the statusline
|
|
712
845
|
// still names it, read straight off the session's own liveReference getter
|
|
713
846
|
// rather than the last radio the page itself set.
|
|
@@ -791,6 +924,7 @@ ${THEME_TOKENS_CSS}
|
|
|
791
924
|
// that changed nothing costs at most one coalesced write.
|
|
792
925
|
if (result.record && result.record.via !== "command") scheduleSave();
|
|
793
926
|
await renderStatsPanel(); // a teach or learned-load turn grew this session's memory; a plain ask leaves it unchanged either way
|
|
927
|
+
await noteResearchLearned(result);
|
|
794
928
|
} catch (err) {
|
|
795
929
|
settleAssistantBubble(pendingRow,
|
|
796
930
|
"something went wrong answering that (" + (err && err.message ? err.message : err) + ") \\u2014 try rephrasing",
|
|
@@ -1018,6 +1152,18 @@ ${THEME_TOKENS_CSS}
|
|
|
1018
1152
|
addSystemLine("tmct \\u2014 the real engine, running in this page \\u2014 " + statsSummaryLine(stats, bandLabelFor)
|
|
1019
1153
|
+ "." + restoredNote + " Ask it something, or teach it a fact of your own.");
|
|
1020
1154
|
await renderStatsPanel(stats);
|
|
1155
|
+
// A restored session may already carry earlier research facts (they
|
|
1156
|
+
// persist with everything else this session taught) — seed the
|
|
1157
|
+
// seen-set from them so a later research turn only reports what's
|
|
1158
|
+
// actually new, without fabricating passages for a visit this page
|
|
1159
|
+
// was never open to read.
|
|
1160
|
+
if (window.tmctChat.researchedFactRows) {
|
|
1161
|
+
try {
|
|
1162
|
+
const existingResearch = await window.tmctChat.researchedFactRows(window.tmctChatSession.memoryDir);
|
|
1163
|
+
for (const row of existingResearch) researchedFactKeysSeen.add(row.subject + "|" + row.predicate + "|" + row.object);
|
|
1164
|
+
} catch { /* best-effort seeding only — a fresh session has none to seed */ }
|
|
1165
|
+
}
|
|
1166
|
+
renderResearchedPanel();
|
|
1021
1167
|
inputEl.placeholder = seedPayload ? 'try "what is a dog" or "list facts"' : window.tmctChat.vocabExampleHint(false);
|
|
1022
1168
|
renderStatus();
|
|
1023
1169
|
setBusy(false);
|
package/src/services/chat.mjs
CHANGED
|
@@ -4233,7 +4233,7 @@ async function teachExclusionReason(sentence) {
|
|
|
4233
4233
|
}
|
|
4234
4234
|
export { teachExclusionReason };
|
|
4235
4235
|
|
|
4236
|
-
async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cache = null, planHolder = null, graph = null }) {
|
|
4236
|
+
async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cache = null, planHolder = null, graph = null, gameConfig = DEFAULT_GAME_CONFIG }) {
|
|
4237
4237
|
// A closed discourse-marker preamble ahead of a teach sentence ("howdy
|
|
4238
4238
|
// pardner, remember that TaskController is fragile") would otherwise
|
|
4239
4239
|
// corrupt TEACH_RE's own match, so strip it first. applyPreambleFrames is
|
|
@@ -4349,7 +4349,7 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cac
|
|
|
4349
4349
|
if (memoryDir && !QUESTION_LEAD_RE.test(conjSrc) && /\s+and\s+/i.test(conjSrc)
|
|
4350
4350
|
&& !(await hasMidSentenceInterrogative(conjSrc))) {
|
|
4351
4351
|
const rewrap = (half) => (wrapped != null ? `remember that ${half}` : half);
|
|
4352
|
-
const recurse = (half) => teachLane(rewrap(half), { memoryDir, sessionId, lexicon, cache, planHolder });
|
|
4352
|
+
const recurse = (half) => teachLane(rewrap(half), { memoryDir, sessionId, lexicon, cache, planHolder, gameConfig });
|
|
4353
4353
|
const stripNoted = (t) => String(t).replace(/^noted — remembered(?:\s+\d+\s+facts?)?:\s*/i, "").trim();
|
|
4354
4354
|
const shared = conjSrc.match(/^(.+?)\s+and\s+((?:is|are|has|have|can)\b.+)$/i);
|
|
4355
4355
|
const sharedSubject = shared ? shared[1].match(/^(.+?)\s+(?:is|are|has|have|can)\b/i)?.[1]?.trim() : null;
|
|
@@ -4569,13 +4569,15 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cac
|
|
|
4569
4569
|
}
|
|
4570
4570
|
}
|
|
4571
4571
|
|
|
4572
|
-
// MID-PLAN BOARD TEACH — a locative fact about a piece the LIVE plan's
|
|
4573
|
-
//
|
|
4574
|
-
//
|
|
4575
|
-
//
|
|
4576
|
-
//
|
|
4577
|
-
//
|
|
4578
|
-
//
|
|
4572
|
+
// MID-PLAN BOARD TEACH — a locative fact about a piece the LIVE plan's moves
|
|
4573
|
+
// touch is accepted and the plan is re-searched from the moved board, never
|
|
4574
|
+
// confirmed-then-contradicted by the next move. The change is written as a
|
|
4575
|
+
// NEW whole-board @step snapshot layer (never a base fact — a base write here
|
|
4576
|
+
// would sit under the standing snapshots and trip the contradictory-board
|
|
4577
|
+
// check on the next solve), then the goal is re-searched from the board as it
|
|
4578
|
+
// now stands. Scoped to the locative teach shape over the plan's own pieces;
|
|
4579
|
+
// every other teach (new vocabulary, new pieces, rules) is untouched, and
|
|
4580
|
+
// with no live plan nothing changes at all.
|
|
4579
4581
|
{
|
|
4580
4582
|
const livePlan = planHolder?.state && !planHolder.state.done
|
|
4581
4583
|
&& Array.isArray(planHolder.state.actions) && planHolder.state.actions.length
|
|
@@ -4583,13 +4585,49 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cac
|
|
|
4583
4585
|
const boardSrc = (wrapped ?? raw).replace(/[.!?]+\s*$/, "");
|
|
4584
4586
|
const board = livePlan ? boardSrc.match(BOARD_TEACH_LOCATIVE_RE) : null;
|
|
4585
4587
|
if (board && memoryDir && !QUESTION_LEAD_RE.test(boardSrc)) {
|
|
4586
|
-
const { normFactTerm } = await import("../adapters/memory/core.mjs");
|
|
4588
|
+
const { normFactTerm, appendFact } = await import("../adapters/memory/core.mjs");
|
|
4587
4589
|
const planPieces = new Set(livePlan.actions.flatMap((a) => [normFactTerm(a.subject), normFactTerm(a.target)]));
|
|
4588
4590
|
if (planPieces.has(normFactTerm(board[1])) || planPieces.has(normFactTerm(board[4]))) {
|
|
4591
|
+
const { maxSnapshotStep } = await import("../domain/domain.mjs");
|
|
4592
|
+
const { factRows, domain, state } = await loadPlanContext(memoryDir);
|
|
4593
|
+
// The single-placement change over the current fold: same subject and
|
|
4594
|
+
// predicate, new object. Written as the whole mutated board under a
|
|
4595
|
+
// fresh @step layer, so stateFromFacts reads it as the live board and no
|
|
4596
|
+
// base fact is left to contradict the next solve.
|
|
4597
|
+
const subject = normFactTerm(board[1]);
|
|
4598
|
+
const predicate = `mgx:${board[2].toLowerCase()}-${board[3].toLowerCase()}`;
|
|
4599
|
+
const object = normFactTerm(board[4]);
|
|
4600
|
+
const mutated = state.filter((r) => !(r.subject === subject && r.predicate === predicate));
|
|
4601
|
+
mutated.push({ subject, predicate, object });
|
|
4602
|
+
const layer = maxSnapshotStep(factRows, domain) + 1;
|
|
4603
|
+
for (const r of mutated) {
|
|
4604
|
+
await appendFact(memoryDir, {
|
|
4605
|
+
subject: `${r.subject}@step${layer}`, predicate: r.predicate, object: r.object,
|
|
4606
|
+
provenance: `plan:${sessionId || "chat"}:teach-replan:step${layer}`,
|
|
4607
|
+
});
|
|
4608
|
+
}
|
|
4589
4609
|
const at = livePlan.cursor > 0 ? `step ${livePlan.cursor} of ${livePlan.actions.length}` : `0 of ${livePlan.actions.length} moves made`;
|
|
4610
|
+
const goalText = livePlan.goalText ?? livePlan.goalTexts?.join("; ") ?? "the held goal";
|
|
4611
|
+
const remembered = `noted — remembered: "${board[0]}".`;
|
|
4612
|
+
const replan = await solveHeldGoals({ memoryDir, planHolder, gameConfig });
|
|
4613
|
+
if (replan.plan) {
|
|
4614
|
+
const moves = replan.plan.actions.map((a, i) => `${i + 1}. ${a.label}`).join("; ");
|
|
4615
|
+
return {
|
|
4616
|
+
text: `${remembered} That changes the board the live plan was standing on (${at}, toward: ${goalText}), so I replanned from the board as it now stands: ${moves}. Say "next" to make move 1.`,
|
|
4617
|
+
via: "plan", miss: false,
|
|
4618
|
+
};
|
|
4619
|
+
}
|
|
4620
|
+
// The write STANDS, but nothing reaches the goal from the moved board:
|
|
4621
|
+
// the old plan is dropped (goals kept, plan reset) and the failed replan
|
|
4622
|
+
// is named, never a silent success.
|
|
4623
|
+
const maxDepth = gameConfig?.planning?.maxDepth ?? DEFAULT_GAME_CONFIG.planning.maxDepth;
|
|
4624
|
+
planHolder.state = {
|
|
4625
|
+
goals: livePlan.goals, goalTexts: livePlan.goalTexts,
|
|
4626
|
+
actions: null, states: null, stepGoals: null, cursor: 0, done: false,
|
|
4627
|
+
};
|
|
4590
4628
|
return {
|
|
4591
|
-
text:
|
|
4592
|
-
via: "
|
|
4629
|
+
text: `${remembered} That changes the board the live plan was standing on (${at}, toward: ${goalText}) — from this new board no plan reaches the goal within ${maxDepth} moves, so the old plan is dropped. Re-teach the board or say "forget the goal".`,
|
|
4630
|
+
via: "plan", miss: false,
|
|
4593
4631
|
};
|
|
4594
4632
|
}
|
|
4595
4633
|
}
|
|
@@ -5405,6 +5443,17 @@ const MODULE_ORIENT_RE = new RegExp(`^what\\s+does\\s+(.+?)\\s+do${TRAILING_ADVE
|
|
|
5405
5443
|
* ending safe — a syntactic match against a term that isn't a real entity
|
|
5406
5444
|
* simply falls through unchanged, same as every other lane in this file. */
|
|
5407
5445
|
const MODULE_ORIENT_SVO_RE = new RegExp(`^what\\s+(.+?)\\s+does${TRAILING_ADVERB_RE}\\??$`, "i");
|
|
5446
|
+
/** "whats X do" / "what's X do" / "what is X do" — the CONTRACTED phrasing of
|
|
5447
|
+
* "what does X do", where the auxiliary collapses into the "what's"/"whats"
|
|
5448
|
+
* opener and "do" trails the term. MODULE_ORIENT_RE's own "does BEFORE the
|
|
5449
|
+
* term" anchor never sees it, and MODULE_ORIENT_SVO_RE needs a literal "what "
|
|
5450
|
+
* (with a space) so the bare "whats" spelling escapes that too. Safe to end
|
|
5451
|
+
* this loosely because the lane's exact-unique resolveEntity gate below is
|
|
5452
|
+
* still the sole authority — same argument as MODULE_ORIENT_SVO_RE: a term
|
|
5453
|
+
* that is not a real unique entity (a pronoun subject "whats it do", a
|
|
5454
|
+
* non-word) simply declines. The "what(?:'s|s|\s+is)" opener mirrors
|
|
5455
|
+
* MODULE_PURPOSE_RE's tolerance for the apostrophe-less "whats" contraction. */
|
|
5456
|
+
const MODULE_ORIENT_IS_DO_RE = new RegExp(`^what(?:'s|s|\\s+is)\\s+(.+?)\\s+do${TRAILING_ADVERB_RE}\\??$`, "i");
|
|
5408
5457
|
// Purpose/identity phrasing: "whats X for"/"what's X
|
|
5409
5458
|
// about"/"what is X for", the sibling of "what does X do" that asks for the
|
|
5410
5459
|
// SAME module-grain overview. Deliberately does NOT claim the literal noun
|
|
@@ -5474,7 +5523,7 @@ async function moduleOrientLane(query, { graph }) {
|
|
|
5474
5523
|
// (stripFillerWords already eats "please"/"could you" as filler; the politeness
|
|
5475
5524
|
// regex only adds the "explain [to me]" wrapper on top).
|
|
5476
5525
|
q = stripFillerWords(applyPreambleFrames(correctMisspellings(q))).replace(MODULE_ORIENT_POLITENESS_RE, "");
|
|
5477
|
-
const m = q.match(MODULE_ORIENT_RE) || q.match(MODULE_PURPOSE_OF_RE) || q.match(MODULE_PURPOSE_RE) || q.match(MODULE_ORIENT_SVO_RE);
|
|
5526
|
+
const m = q.match(MODULE_ORIENT_RE) || q.match(MODULE_PURPOSE_OF_RE) || q.match(MODULE_PURPOSE_RE) || q.match(MODULE_ORIENT_SVO_RE) || q.match(MODULE_ORIENT_IS_DO_RE);
|
|
5478
5527
|
// "what does src/core/store.mjs do" already reached the overview; the bare
|
|
5479
5528
|
// path and "what is <path>" did not, so the same module answered one
|
|
5480
5529
|
// phrasing and walled two. Both are claimed here rather than in ask.mjs,
|
|
@@ -8526,7 +8575,19 @@ async function factReadBackReaders(memoryDir, query, envelope, miss, graph = nul
|
|
|
8526
8575
|
}
|
|
8527
8576
|
}
|
|
8528
8577
|
}
|
|
8529
|
-
|
|
8578
|
+
// An isa-shaped FIRST turn on a pristine store falls THROUGH to the isa
|
|
8579
|
+
// reader below rather than taking the empty-store bail-out: that reader's
|
|
8580
|
+
// body tolerates rows=[] end-to-end (every derived array is empty) and
|
|
8581
|
+
// lands on the specific "I don't know X at all yet — teach me" closer, so
|
|
8582
|
+
// the very first "is X a Y" no longer hits the generic grammar wall just
|
|
8583
|
+
// because nothing has been taught yet. The graph inherits-bridge above
|
|
8584
|
+
// already answers the code-entity direct/converse cases before this point.
|
|
8585
|
+
// A leading "there" subject is existential ("is there a class called X"),
|
|
8586
|
+
// which ISA_ASK_RE also matches but a LATER existence lane owns and answers
|
|
8587
|
+
// better — it keeps the bail-out, mirroring this block's own emptyIsAdj
|
|
8588
|
+
// "there" exclusion above. Every OTHER empty-store shape keeps the bail-out.
|
|
8589
|
+
const fallThroughIsa = qHedge.match(ISA_ASK_RE) || matchWhyIsa(q);
|
|
8590
|
+
if (!((fallThroughIsa && !/^there\b/i.test(fallThroughIsa[1].trim())) || CONFIRM_TAG_RE.test(q))) return null;
|
|
8530
8591
|
}
|
|
8531
8592
|
const isa = rows.filter((f) => ISA_PREDICATES.has(f.predicate));
|
|
8532
8593
|
const byTrust = (a, b) => b.trust - a.trust;
|
|
@@ -9997,6 +10058,14 @@ const STACCATO_LEAKED_CONNECTIVES = new Set(["and", "also", "so", "then", "now"]
|
|
|
9997
10058
|
* named `edithistory`) is never mistaken for the pronoun. */
|
|
9998
10059
|
const PRONOUN_IN_QUERY_RE = new RegExp(`\\b(?:${[...CONTEXT_WORDS].join("|")})\\b`, "i");
|
|
9999
10060
|
|
|
10061
|
+
/** The referring pronouns an EMBEDDED "what about X, <wh-clause>" swap replaces
|
|
10062
|
+
* — CONTEXT_WORDS (it/this/that/here) plus the personal pronouns
|
|
10063
|
+
* PRONOUN_IN_QUERY_RE deliberately omits (he/she/they/them): the embedded
|
|
10064
|
+
* clause's own subject/object, not a prior-turn antecedent, so a subject
|
|
10065
|
+
* pronoun like "he" that never appears in a code-graph query still has to be
|
|
10066
|
+
* swappable here. */
|
|
10067
|
+
const EMBEDDED_PRONOUN_RE = /\b(?:it|this|that|here|he|she|they|them)\b/i;
|
|
10068
|
+
|
|
10000
10069
|
/** DISCOURSE CONTINUATION: "what about X" carries the PRIOR
|
|
10001
10070
|
* turn's question shape across the turn boundary — re-asking it with X in place of
|
|
10002
10071
|
* the previous subject/object. Returns the reconstructed query (parsed like any
|
|
@@ -10007,6 +10076,32 @@ function discourseRewrite(query, last) {
|
|
|
10007
10076
|
let newSubj;
|
|
10008
10077
|
if (m) {
|
|
10009
10078
|
newSubj = m[1].trim();
|
|
10079
|
+
// An embedded question spliced into the "what about" subject ("what about
|
|
10080
|
+
// the store, what it do") must NEVER be substituted into the prior turn's
|
|
10081
|
+
// shape — that inherits the prior turn's DIRECTION onto a question asking
|
|
10082
|
+
// the opposite ("who uses store.mjs" then "…what it do" would answer "who
|
|
10083
|
+
// uses the store"). Split on the interior wh-clause and re-read the
|
|
10084
|
+
// remainder against a CLOSED micro-set; a clause outside it is an honest
|
|
10085
|
+
// miss, never the prior-turn substitution below. A comma NOT followed by a
|
|
10086
|
+
// wh-word ("what about the store, please") never matches and keeps its
|
|
10087
|
+
// ordinary swap.
|
|
10088
|
+
const embedded = newSubj.match(/^(.+?),\s*(what|who|which|where|how)\b\s*(.*)$/i);
|
|
10089
|
+
if (embedded) {
|
|
10090
|
+
const embSubj = embedded[1].trim();
|
|
10091
|
+
const wh = embedded[2].toLowerCase();
|
|
10092
|
+
const rest = embedded[3].trim();
|
|
10093
|
+
// "what [it/this/that/he/she] do(es)" → the module overview of the new
|
|
10094
|
+
// subject, which MODULE_ORIENT_RE serves verbatim.
|
|
10095
|
+
if (wh === "what" && /^(?:he|she|it|this|that)?\s*do(?:es)?$/i.test(rest)) {
|
|
10096
|
+
return `what does ${embSubj} do`;
|
|
10097
|
+
}
|
|
10098
|
+
// A wh-clause carrying its OWN pronoun ("what does it call") → swap that
|
|
10099
|
+
// pronoun for the new subject and ask the clause standalone.
|
|
10100
|
+
if (EMBEDDED_PRONOUN_RE.test(rest)) {
|
|
10101
|
+
return `${wh} ${rest.replace(EMBEDDED_PRONOUN_RE, () => embSubj)}`;
|
|
10102
|
+
}
|
|
10103
|
+
return null;
|
|
10104
|
+
}
|
|
10010
10105
|
} else {
|
|
10011
10106
|
const sm = String(query).match(STACCATO_SWAP_RE);
|
|
10012
10107
|
const cand = sm?.[1]?.trim();
|
|
@@ -11243,14 +11338,16 @@ async function planLaneAnswer(query, { memoryDir, planHolder, sessionId = "", ga
|
|
|
11243
11338
|
const wantsSolve = PLAN_SOLVE_RE.test(q);
|
|
11244
11339
|
const wantsLegal = LEGAL_MOVES_RE.test(q);
|
|
11245
11340
|
if (!wantsSolve && !wantsLegal) return null;
|
|
11341
|
+
if (wantsSolve) return solveHeldGoals({ memoryDir, planHolder, gameConfig });
|
|
11246
11342
|
|
|
11343
|
+
// "what moves are legal now" — one ply off the current board, no search.
|
|
11247
11344
|
let ctx;
|
|
11248
11345
|
try {
|
|
11249
11346
|
ctx = await loadPlanContext(memoryDir);
|
|
11250
11347
|
} catch (err) {
|
|
11251
11348
|
return { text: `I can't read the taught domain: ${err?.message ?? err}`, via: "plan", deduced: "plan a move sequence", note: "plan lane — domain load failed" };
|
|
11252
11349
|
}
|
|
11253
|
-
const { domain, state
|
|
11350
|
+
const { domain, state } = ctx;
|
|
11254
11351
|
if (!domain.actions.length) {
|
|
11255
11352
|
return {
|
|
11256
11353
|
text: `no action rules taught yet — teach the game first (e.g. "you can move a disk onto a peg").`,
|
|
@@ -11263,30 +11360,55 @@ async function planLaneAnswer(query, { memoryDir, planHolder, sessionId = "", ga
|
|
|
11263
11360
|
via: "plan", deduced: "plan a move sequence (no state yet)", note: "plan lane — honest decline: empty state",
|
|
11264
11361
|
};
|
|
11265
11362
|
}
|
|
11266
|
-
const { movesFromRules,
|
|
11267
|
-
|
|
11268
|
-
|
|
11269
|
-
|
|
11270
|
-
|
|
11271
|
-
|
|
11272
|
-
|
|
11273
|
-
if (err instanceof PlanBudgetError) {
|
|
11274
|
-
return { text: `too many possible moves to enumerate here (${err.message}) — narrow the classes involved.`, via: "plan", deduced: "list the legal moves (budget exceeded)", note: "plan lane — budget decline" };
|
|
11275
|
-
}
|
|
11276
|
-
throw err;
|
|
11277
|
-
}
|
|
11278
|
-
if (!moves.length) {
|
|
11279
|
-
return { text: "no legal moves from the current state.", via: "plan", deduced: "list the legal moves (none)", note: "plan lane — legal moves: none" };
|
|
11363
|
+
const { movesFromRules, PlanBudgetError } = await import("../domain/domain.mjs");
|
|
11364
|
+
let moves;
|
|
11365
|
+
try {
|
|
11366
|
+
moves = movesFromRules(state, domain, { scope: "taught" });
|
|
11367
|
+
} catch (err) {
|
|
11368
|
+
if (err instanceof PlanBudgetError) {
|
|
11369
|
+
return { text: `too many possible moves to enumerate here (${err.message}) — narrow the classes involved.`, via: "plan", deduced: "list the legal moves (budget exceeded)", note: "plan lane — budget decline" };
|
|
11280
11370
|
}
|
|
11281
|
-
|
|
11371
|
+
throw err;
|
|
11372
|
+
}
|
|
11373
|
+
if (!moves.length) {
|
|
11374
|
+
return { text: "no legal moves from the current state.", via: "plan", deduced: "list the legal moves (none)", note: "plan lane — legal moves: none" };
|
|
11375
|
+
}
|
|
11376
|
+
const lines = moves.map((m, i) => ` ${i + 1}. ${actionLabel(m.action.name, m.action.subject, m.action.target)}`);
|
|
11377
|
+
return {
|
|
11378
|
+
text: `${moves.length} legal move${moves.length === 1 ? "" : "s"} from here:\n${lines.join("\n")}`,
|
|
11379
|
+
via: "plan", deduced: "list the legal moves from the current state",
|
|
11380
|
+
note: "plan lane — movesFromRules over the current snapshot, one ply, no search",
|
|
11381
|
+
};
|
|
11382
|
+
}
|
|
11383
|
+
|
|
11384
|
+
/** Search the taught rules for a shortest sequence to the held goal(s) from the
|
|
11385
|
+
* CURRENT board fold (the newest @stepK snapshot, else the taught board).
|
|
11386
|
+
* Mints the plan onto planHolder.state and returns the plan-found reply on
|
|
11387
|
+
* success; on any missing precondition or an unreachable goal it returns the
|
|
11388
|
+
* matching honest decline and leaves planHolder.state untouched. Shared by the
|
|
11389
|
+
* plan lane's "solve it" and by the two drift sites that re-search after the
|
|
11390
|
+
* board moves under a live plan. */
|
|
11391
|
+
async function solveHeldGoals({ memoryDir, planHolder, gameConfig = DEFAULT_GAME_CONFIG }) {
|
|
11392
|
+
let ctx;
|
|
11393
|
+
try {
|
|
11394
|
+
ctx = await loadPlanContext(memoryDir);
|
|
11395
|
+
} catch (err) {
|
|
11396
|
+
return { text: `I can't read the taught domain: ${err?.message ?? err}`, via: "plan", deduced: "plan a move sequence", note: "plan lane — domain load failed" };
|
|
11397
|
+
}
|
|
11398
|
+
const { domain, state, factRows } = ctx;
|
|
11399
|
+
if (!domain.actions.length) {
|
|
11282
11400
|
return {
|
|
11283
|
-
text:
|
|
11284
|
-
via: "plan", deduced: "
|
|
11285
|
-
note: "plan lane — movesFromRules over the current snapshot, one ply, no search",
|
|
11401
|
+
text: `no action rules taught yet — teach the game first (e.g. "you can move a disk onto a peg").`,
|
|
11402
|
+
via: "plan", deduced: "plan a move sequence (no action rules yet)", note: "plan lane — honest decline: no action rules",
|
|
11286
11403
|
};
|
|
11287
11404
|
}
|
|
11288
|
-
|
|
11289
|
-
|
|
11405
|
+
if (!state.length) {
|
|
11406
|
+
return {
|
|
11407
|
+
text: `no current state taught yet — state the board first (e.g. "disk-1 rests on peg-a").`,
|
|
11408
|
+
via: "plan", deduced: "plan a move sequence (no state yet)", note: "plan lane — honest decline: empty state",
|
|
11409
|
+
};
|
|
11410
|
+
}
|
|
11411
|
+
const { movesFromRules, stateKeyFor, compileGoal, PlanBudgetError, maxSnapshotStep } = await import("../domain/domain.mjs");
|
|
11290
11412
|
if (!planHolder.state?.goals?.length) {
|
|
11291
11413
|
return {
|
|
11292
11414
|
text: `no goal set yet — teach one first (e.g. "the goal is that every disk rests on peg-c").`,
|
|
@@ -11403,8 +11525,13 @@ async function planLaneAnswer(query, { memoryDir, planHolder, sessionId = "", ga
|
|
|
11403
11525
|
// instead of an honest miss — see PLAN_WHY_SHORTEST_RE's own call site.
|
|
11404
11526
|
const becauseText = `you taught me the "${ruleNames}" rule${domain.actions.length === 1 ? "" : "s"}`
|
|
11405
11527
|
+ `${ordering.length ? ` and ${ordering.length} ordering fact${ordering.length === 1 ? "" : "s"}` : ""}.`;
|
|
11528
|
+
// The snapshot layer a fresh plan's step writes stack ABOVE: 0 on an
|
|
11529
|
+
// untouched board, K after a prior plan left @stepK rows standing. Without it
|
|
11530
|
+
// a replan's step 1 would write @step1 below the standing @stepK layer and be
|
|
11531
|
+
// read as stale by stateFromFacts (which prefers the newest snapshot).
|
|
11532
|
+
const stepBase = maxSnapshotStep(factRows, domain);
|
|
11406
11533
|
planHolder.state = {
|
|
11407
|
-
...planHolder.state, actions, states: found.states, stepGoals, cursor: 0, done: false, goalText, becauseText,
|
|
11534
|
+
...planHolder.state, actions, states: found.states, stepGoals, cursor: 0, done: false, goalText, becauseText, stepBase,
|
|
11408
11535
|
};
|
|
11409
11536
|
const moveLines = actions.map((a, i) => ` ${i + 1}. ${a.label}`);
|
|
11410
11537
|
// A piece with no taught position is an ASSUMPTION the plan silently makes
|
|
@@ -11440,23 +11567,27 @@ async function planLaneAnswer(query, { memoryDir, planHolder, sessionId = "", ga
|
|
|
11440
11567
|
/** Execute the active plan's next move: append the successor snapshot's rows
|
|
11441
11568
|
* as @stepK facts, advance the cursor, and on the final step re-read the
|
|
11442
11569
|
* store and confirm the goal from the WRITTEN facts (never assumed). */
|
|
11443
|
-
async function executePlanStep(planHolder, { memoryDir, sessionId = "" }) {
|
|
11570
|
+
async function executePlanStep(planHolder, { memoryDir, sessionId = "", gameConfig = DEFAULT_GAME_CONFIG }) {
|
|
11444
11571
|
const ps = planHolder.state;
|
|
11445
11572
|
const k = ps.cursor + 1;
|
|
11573
|
+
// The snapshot index the board rows are written under: it stacks above any
|
|
11574
|
+
// layer standing when the plan was minted (stepBase), while k stays the plan's
|
|
11575
|
+
// own 1-of-N move counter. On a fresh board stepBase is 0 and snap === k.
|
|
11576
|
+
const snap = (ps.stepBase ?? 0) + k;
|
|
11446
11577
|
const action = ps.actions[ps.cursor];
|
|
11447
11578
|
const rows = ps.states[k];
|
|
11448
11579
|
const { appendFact, loadMemory, readFactRows } = await import("../adapters/memory/core.mjs");
|
|
11449
11580
|
for (const row of rows) {
|
|
11450
11581
|
await appendFact(memoryDir, {
|
|
11451
|
-
subject: `${row.subject}@step${
|
|
11452
|
-
provenance: `plan:${sessionId || "chat"}:step${
|
|
11582
|
+
subject: `${row.subject}@step${snap}`, predicate: row.predicate, object: row.object,
|
|
11583
|
+
provenance: `plan:${sessionId || "chat"}:step${snap}`,
|
|
11453
11584
|
});
|
|
11454
11585
|
}
|
|
11455
11586
|
planHolder.state = { ...ps, cursor: k };
|
|
11456
11587
|
const boardLine = rows.map((r) => `${r.subject} ${predicatePhrase(r.predicate)} ${r.object}`).join("; ");
|
|
11457
11588
|
if (k < ps.actions.length) {
|
|
11458
11589
|
return {
|
|
11459
|
-
text: `moved — ${action.label} (step ${k} of ${ps.actions.length}). board@step${
|
|
11590
|
+
text: `moved — ${action.label} (step ${k} of ${ps.actions.length}). board@step${snap}: ${boardLine}`,
|
|
11460
11591
|
deduced: ps.stepGoals[k] ? ps.stepGoals[k] : `continue the plan (step ${k + 1} of ${ps.actions.length})`,
|
|
11461
11592
|
};
|
|
11462
11593
|
}
|
|
@@ -11468,12 +11599,31 @@ async function executePlanStep(planHolder, { memoryDir, sessionId = "" }) {
|
|
|
11468
11599
|
const domain = compileDomain(factRows, readRuleRows(payload));
|
|
11469
11600
|
const finalState = stateFromFacts(factRows, domain);
|
|
11470
11601
|
const holds = compileGoal(ps.goals, domain, { scope: "taught" })(finalState);
|
|
11602
|
+
const movedLine = `moved — ${action.label} (step ${k} of ${ps.actions.length}). board@step${snap}: ${boardLine}`;
|
|
11603
|
+
if (holds) {
|
|
11604
|
+
planHolder.state = { ...planHolder.state, done: true };
|
|
11605
|
+
return {
|
|
11606
|
+
text: `${movedLine}\n\ndone — ${ps.goalText} (checked against board@step${snap}'s written facts, not assumed).`,
|
|
11607
|
+
deduced: `goal reached — ${ps.goalText} (${k} of ${k} steps)`,
|
|
11608
|
+
};
|
|
11609
|
+
}
|
|
11610
|
+
// The final board doesn't reach the goal — the plan or the board drifted.
|
|
11611
|
+
// Before settling for the miss, re-search from the board as it now stands: a
|
|
11612
|
+
// found plan is disclosed and held (never a silent success), a miss keeps the
|
|
11613
|
+
// honest failure and names the failed replan.
|
|
11614
|
+
const replan = await solveHeldGoals({ memoryDir, planHolder, gameConfig });
|
|
11615
|
+
if (replan.plan) {
|
|
11616
|
+
const moves = replan.plan.actions.map((a, i) => `${i + 1}. ${a.label}`).join("; ");
|
|
11617
|
+
return {
|
|
11618
|
+
text: `${movedLine}\n\nBUT the goal does NOT hold against the written facts — the state drifted, so I replanned from board@step${snap}: ${moves}. Say "next" to continue.`,
|
|
11619
|
+
deduced: "plan finished but the goal check failed — replanned from the drifted board",
|
|
11620
|
+
};
|
|
11621
|
+
}
|
|
11622
|
+
const maxDepth = gameConfig?.planning?.maxDepth ?? DEFAULT_GAME_CONFIG.planning.maxDepth;
|
|
11471
11623
|
planHolder.state = { ...planHolder.state, done: true };
|
|
11472
11624
|
return {
|
|
11473
|
-
text:
|
|
11474
|
-
|
|
11475
|
-
: `moved — ${action.label} (step ${k} of ${ps.actions.length}). board@step${k}: ${boardLine}\n\nBUT the goal does NOT hold against the written facts — the plan or the state drifted; re-teach the state and solve again.`,
|
|
11476
|
-
deduced: holds ? `goal reached — ${ps.goalText} (${k} of ${k} steps)` : "plan finished but the goal check failed",
|
|
11625
|
+
text: `${movedLine}\n\nBUT the goal does NOT hold against the written facts — the plan or the state drifted; re-teach the state and solve again — I looked for a new plan from board@step${snap} and found none within ${maxDepth} moves.`,
|
|
11626
|
+
deduced: "plan finished but the goal check failed",
|
|
11477
11627
|
};
|
|
11478
11628
|
}
|
|
11479
11629
|
|
|
@@ -12091,6 +12241,16 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
12091
12241
|
const staccatoSwapMatch = String(query).match(STACCATO_SWAP_RE);
|
|
12092
12242
|
const isStaccatoSwap = !!(last?.query && staccatoSwapMatch && NAME_TOKEN_RE.test(staccatoSwapMatch[1]?.trim() || ""));
|
|
12093
12243
|
const isWhatAboutContinuation = !!(last?.query && WHAT_ABOUT_RE.test(String(query))) || isStaccatoSwap;
|
|
12244
|
+
// A bare vague-touch OPENER ("wat about validate", "tell me about store.mjs")
|
|
12245
|
+
// whose term resolves to a UNIQUE graph entity is a genuine describe request,
|
|
12246
|
+
// not small talk — defer past the conversational card so describeWrapperAnswer
|
|
12247
|
+
// (4d, below) serves its module/entity overview. Distinct from
|
|
12248
|
+
// isWhatAboutContinuation above, which needs a prior turn: this fires on the
|
|
12249
|
+
// FIRST turn too, and covers the "tell me about"/"explain" surfaces
|
|
12250
|
+
// vagueTouchTermOf reads. resolveEntity already declines on ambiguity, so an
|
|
12251
|
+
// ambiguous or unknown term ("wat about xyzzy") keeps today's orientation card.
|
|
12252
|
+
const vagueTouchTerm = graph ? vagueTouchTermOf(String(query)) : null;
|
|
12253
|
+
const isVagueTouchResolvable = !!(vagueTouchTerm && await resolveEntity(graph, vagueTouchTerm));
|
|
12094
12254
|
// Same exemption for "describe it"/"tell me about that" — needs the SAME
|
|
12095
12255
|
// deferral to reach describeWrapperAnswer's focus-aware pronoun resolution.
|
|
12096
12256
|
// Gated on an actual standing focus, same honest-decline discipline as
|
|
@@ -12202,7 +12362,7 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
12202
12362
|
} catch { /* leave false — the ordinary path decides */ }
|
|
12203
12363
|
}
|
|
12204
12364
|
}
|
|
12205
|
-
const conversationalCandidateBaseGate = !handled && miss && !envelope?.parsed && !isWhatAboutContinuation && !isDescribePronounContinuation && !isExplainTouch && !isStaccatoNegation && !isVagueRelationTouch && !isStaccatoComparative && !isStaccatoPronounNoFocus && !isPluralMembershipTeach && !isBareRelationalVerbTeach;
|
|
12365
|
+
const conversationalCandidateBaseGate = !handled && miss && !envelope?.parsed && !isWhatAboutContinuation && !isVagueTouchResolvable && !isDescribePronounContinuation && !isExplainTouch && !isStaccatoNegation && !isVagueRelationTouch && !isStaccatoComparative && !isStaccatoPronounNoFocus && !isPluralMembershipTeach && !isBareRelationalVerbTeach;
|
|
12206
12366
|
// A turn whose pronoun was bound to a vocabulary antecedent is PROVABLY a
|
|
12207
12367
|
// fact question ("can it bark" → "can dog bark") — never conversational,
|
|
12208
12368
|
// however short. Without this, the substituted 3-worder still trips
|
|
@@ -12594,7 +12754,7 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
12594
12754
|
// (4) #2 TEACH lane — a teach-shaped would-miss nothing above answered: route to
|
|
12595
12755
|
// memory, or say what CAN be remembered (LOUD), never the wall / a silent drop.
|
|
12596
12756
|
if (miss && recordMiss && via === "composed") {
|
|
12597
|
-
const taught = await teachLane(query, { memoryDir, sessionId, lexicon, cache, planHolder, graph });
|
|
12757
|
+
const taught = await teachLane(query, { memoryDir, sessionId, lexicon, cache, planHolder, graph, gameConfig });
|
|
12598
12758
|
if (taught) {
|
|
12599
12759
|
answer = taught.text; via = taught.via; recordMiss = taught.miss;
|
|
12600
12760
|
if (!taught.miss) dialogueLaneOverride = "teach";
|
|
@@ -14437,7 +14597,7 @@ export async function runTurn(input, { config, source = defaultSource, graph = n
|
|
|
14437
14597
|
if (memoryDir && PLAN_NEXT_RE.test(workingLine)
|
|
14438
14598
|
&& planHolder.state && !planHolder.state.done
|
|
14439
14599
|
&& Array.isArray(planHolder.state.actions) && planHolder.state.cursor < planHolder.state.actions.length) {
|
|
14440
|
-
const step = await executePlanStep(planHolder, { memoryDir, sessionId });
|
|
14600
|
+
const step = await executePlanStep(planHolder, { memoryDir, sessionId, gameConfig: resolvedGameConfig });
|
|
14441
14601
|
note(trace, `goal: ${step.deduced}`);
|
|
14442
14602
|
note(trace, "lane: PLAN NEXT — executed the active plan's next move as an @stepK snapshot write");
|
|
14443
14603
|
const stepTurn = plainTurn(workingLine, step.text, { via: "plan", focus });
|
|
@@ -256,9 +256,14 @@ export async function resolveExtensions(repoRoot, { configFile } = {}) {
|
|
|
256
256
|
*
|
|
257
257
|
* FAILURE-TOLERANT per bundle: one bad third-party pack's seedMemory throw is caught and
|
|
258
258
|
* recorded as `perBundle[name].error` while every other bundle still seeds normally.
|
|
259
|
-
* Returns `{ appended, skipped, total, perBundle: { name: {appended,skipped,total,error?} } }`.
|
|
260
|
-
|
|
259
|
+
* Returns `{ appended, skipped, total, perBundle: { name: {appended,skipped,total,error?} } }`.
|
|
260
|
+
*
|
|
261
|
+
* `opts.captureUnknownContext`/`opts.unknownContextLimit` (both optional) forward to
|
|
262
|
+
* every seedMemory call unchanged — the tmct.toml `[seed]` knob (toml-config.mjs)
|
|
263
|
+
* applies uniformly across whichever bundles are active, not per-bundle. */
|
|
264
|
+
export async function seedActiveCorpusEntries(repo, entries, opts = {}) {
|
|
261
265
|
const { seedMemory } = await import("../adapters/corpus/conceptnet.mjs");
|
|
266
|
+
const { captureUnknownContext, unknownContextLimit } = opts;
|
|
262
267
|
const perBundle = {};
|
|
263
268
|
let appended = 0;
|
|
264
269
|
let skipped = 0;
|
|
@@ -274,6 +279,8 @@ export async function seedActiveCorpusEntries(repo, entries) {
|
|
|
274
279
|
provenancePrefix: entry.provenancePrefix,
|
|
275
280
|
limit: entry.limit,
|
|
276
281
|
prefer: entry.prefer,
|
|
282
|
+
captureUnknownContext,
|
|
283
|
+
unknownContextLimit,
|
|
277
284
|
});
|
|
278
285
|
perBundle[name] = { appended: res.appended, skipped: res.skipped, total: res.total };
|
|
279
286
|
appended += res.appended;
|