@polycode-projects/the-mechanical-code-talker 4.1.1 → 4.1.2

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.
Files changed (55) hide show
  1. package/README.md +31 -18
  2. package/bin/tmct.mjs +3 -0
  3. package/data/templates/responses.jsonl +3 -0
  4. package/package.json +2 -1
  5. package/src/adapters/memory/core.mjs +1358 -196
  6. package/src/adapters/memory/inspect.mjs +11 -0
  7. package/src/adapters/memory/shacl.mjs +38 -0
  8. package/src/adapters/p2p/webrtc-transport.mjs +28 -5
  9. package/src/domain/ask-vocab.mjs +39 -0
  10. package/src/domain/ask.mjs +183 -34
  11. package/src/domain/grammar/assert.mjs +8 -2
  12. package/src/domain/hanoi-board.mjs +232 -0
  13. package/src/domain/ingest-facts.mjs +120 -0
  14. package/src/domain/interpret/normalize.mjs +49 -0
  15. package/src/domain/memory/compaction.mjs +284 -0
  16. package/src/domain/memory/resolution.mjs +171 -0
  17. package/src/domain/memory/trust.mjs +175 -5
  18. package/src/domain/memory-facts.mjs +139 -0
  19. package/src/domain/p2p/facts.mjs +21 -0
  20. package/src/domain/p2p/peer-id.mjs +15 -0
  21. package/src/domain/p2p/provenance-relabel.mjs +13 -2
  22. package/src/domain/p2p/sync-filter.mjs +5 -1
  23. package/src/domain/p2p/wire.mjs +7 -4
  24. package/src/domain/scene-compose.mjs +2 -2
  25. package/src/domain/sprite-facts.mjs +0 -0
  26. package/src/services/adventure-viz.mjs +5 -1
  27. package/src/services/adventure.mjs +70 -44
  28. package/src/services/chat-page-viz.mjs +381 -310
  29. package/src/services/chat.mjs +273 -155
  30. package/src/services/code-explorer-viz.mjs +141 -54
  31. package/src/services/index.mjs +1 -1
  32. package/src/services/ingest-viz.mjs +134 -9
  33. package/src/services/ledger-viz.mjs +7 -4
  34. package/src/services/memory-panel-viz.mjs +8 -3
  35. package/src/services/mud-turn.mjs +11 -8
  36. package/src/services/mud-viz.mjs +441 -206
  37. package/src/services/p2p-room.mjs +110 -23
  38. package/src/services/plan-viz.mjs +63 -4
  39. package/src/services/research-viz.mjs +18 -7
  40. package/src/services/share-overlay-viz.mjs +623 -0
  41. package/src/services/spider-fly-viz.mjs +2 -2
  42. package/src/services/sprite-catalog-viz.mjs +303 -78
  43. package/src/surfaces/web/adventure-browser-entry.mjs +27 -5
  44. package/src/surfaces/web/chat-browser-entry.mjs +37 -10
  45. package/src/surfaces/web/code-explorer-browser-entry.mjs +4 -3
  46. package/src/surfaces/web/ingest-browser-entry.mjs +73 -12
  47. package/src/surfaces/web/ledger-browser-entry.mjs +32 -7
  48. package/src/surfaces/web/memory-ask-browser.bundle.js +149 -116
  49. package/src/surfaces/web/mud-browser-entry.mjs +38 -7
  50. package/src/surfaces/web/p2p-browser-entry.mjs +1 -1
  51. package/src/surfaces/web/plan-browser-entry.mjs +33 -2
  52. package/src/surfaces/web/research-browser-entry.mjs +11 -19
  53. package/src/surfaces/web/sprites-browser-entry.mjs +39 -8
  54. package/src/surfaces/web/tmct-surface.mjs +12 -0
  55. package/src/surfaces/web/turn-session.mjs +10 -3
@@ -32,6 +32,7 @@ import {
32
32
  createInMemoryStore, appendFacts, appendRule, loadMemory, readFactRows, removeFacts,
33
33
  } from "../../adapters/memory/core.mjs";
34
34
  import { parseEntities } from "../../domain/codegraph.mjs";
35
+ import { memoryFactGraphPayload } from "../../domain/memory-facts.mjs";
35
36
  import { loadLexicon } from "../../domain/grammar/lexicon.mjs";
36
37
  import {
37
38
  foldWorldState, worldActionRows, worldDigestRows, roomAffordances,
@@ -62,7 +63,10 @@ import { graphAsk, enginePlan } from "./engine-surface.mjs";
62
63
  * are minted, fewer leaves the ones nobody is playing out of the world
63
64
  * altogether.
64
65
  *
65
- * Returns `{ memoryDir, graph, windows, snapshot }`. `windows` is a plain object
66
+ * Returns `{ memoryDir, codeGraph, graph, refreshGraph, windows, snapshot }`.
67
+ * `codeGraph` is the known-empty index the turn engine and a scripted
68
+ * autoplayTick read; `graph` is this world's own memory store projected for
69
+ * `ask()` (refreshGraph rebuilds it on demand). `windows` is a plain object
66
70
  * keyed by character id, each value `{ character, turn, autoplayTick,
67
71
  * visitedRoomIds, turnsTaken, isOutOfPlay, outOfPlayReason }`. `snapshot()` is
68
72
  * the one OMNISCIENT read this module exposes — the central world map's own
@@ -89,7 +93,11 @@ export async function createMudSession(worldPayload, { characters = [], epoch =
89
93
  // way rather than through the "play <world>" opener (which needs a
90
94
  // shipped "player" individual mud-garden deliberately has none of).
91
95
  const planHolder = { state: { adventure: { world: worldPayload.name } } };
92
- const graph = parseEntities({ individuals: [], objectProperties: [] });
96
+ // A known-empty code graph: code-structure questions get the same honest
97
+ // no-code-graph answer an un-pointed CLI session gives, never a crash — the
98
+ // turn engine (and a scripted autoplayTick) keep reading THIS one for their
99
+ // own in-turn code lane.
100
+ const codeGraph = parseEntities({ individuals: [], objectProperties: [] });
93
101
  // The world's own minted ids ("groundhog-1", "carrot-2") are declared as
94
102
  // vocabulary inside the adventure lane itself, for the length of one world
95
103
  // command — see adventure.mjs's own worldLexicon. This page hands over the
@@ -101,6 +109,17 @@ export async function createMudSession(worldPayload, { characters = [], epoch =
101
109
  return { rows, state: foldWorldState(worldActionRows(rows)) };
102
110
  }
103
111
 
112
+ // What `tmct.ask()` traverses is a different graph: this shared world's own
113
+ // memory store, projected through memoryFactGraphPayload. Rebuilt on demand
114
+ // rather than once at open, because every character's turn grows the store.
115
+ // Built off readWorld()'s own rows rather than a second readFactRows call.
116
+ let memoryGraph = parseEntities({ individuals: [], objectProperties: [] });
117
+ async function refreshGraph() {
118
+ const { rows } = await readWorld();
119
+ memoryGraph = parseEntities(memoryFactGraphPayload(rows));
120
+ return memoryGraph;
121
+ }
122
+
104
123
  async function roomOf(character) {
105
124
  const { state } = await readWorld();
106
125
  return state.placements.get(character)?.object ?? null;
@@ -126,10 +145,10 @@ export async function createMudSession(worldPayload, { characters = [], epoch =
126
145
  if (startRoom) visitedRoomIds.add(startRoom);
127
146
 
128
147
  const turnSession = createTurnSession({
129
- memoryDir, graph, lexicon, sessionId: character,
148
+ memoryDir, graph: codeGraph, lexicon, sessionId: character,
130
149
  vocabHint: 'Try a world command ("dig north", "eat the carrot-1"), or ask "what food do you know about".',
131
150
  buildExtraOptions: () => ({
132
- uiContext: "browser", actingSubject: character, planState: planHolder.state,
151
+ actingSubject: character, planState: planHolder.state,
133
152
  }),
134
153
  captureExtraState: async (result, state) => {
135
154
  if ("planState" in result) planHolder.state = state.planState;
@@ -158,7 +177,7 @@ export async function createMudSession(worldPayload, { characters = [], epoch =
158
177
  * note }` unmodified, so the caller can render the speech-bubble/
159
178
  * dig-flourish triggers straight off `actions`. */
160
179
  async autoplayTick(k) {
161
- const result = await runMudTurn(character, { world: worldPayload.name, memoryDir, env: {}, graph, k });
180
+ const result = await runMudTurn(character, { world: worldPayload.name, memoryDir, env: {}, graph: codeGraph, k });
162
181
  // A turn that ended in starvation still happened, and still counts —
163
182
  // only a DECLINED turn (no room to act in) leaves the tally alone.
164
183
  if (result.room) turnsTaken += 1;
@@ -272,7 +291,13 @@ export async function createMudSession(worldPayload, { characters = [], epoch =
272
291
  return claims.length;
273
292
  }
274
293
 
275
- return { memoryDir, graph, windows, snapshot, applyEdit, wave, claimCharacters };
294
+ return {
295
+ memoryDir,
296
+ codeGraph,
297
+ get graph() { return memoryGraph; },
298
+ refreshGraph,
299
+ windows, snapshot, applyEdit, wave, claimCharacters,
300
+ };
276
301
  }
277
302
 
278
303
  /** `count` entries drawn at random from `roster`, in random order, without
@@ -378,7 +403,13 @@ publishTmctSurface({
378
403
  }
379
404
  return characterWindow.turn(line);
380
405
  },
381
- ask: graphAsk,
406
+ // The memory projection is rebuilt first, so a direct tmct.ask() sees every
407
+ // fact any character's turn has written into this shared world since the
408
+ // last one.
409
+ ask: async (request, options, session) => {
410
+ await session.refreshGraph();
411
+ return graphAsk(request, options, session);
412
+ },
382
413
  plan: enginePlan,
383
414
  page: {
384
415
  pickMudRoster, expandMudRoster, mintedCharacterFacts, worldFactsForCast,
@@ -26,7 +26,7 @@ export {
26
26
  ROOM_FAILED,
27
27
  } from "../../services/p2p-room.mjs";
28
28
  export { createTransport } from "../../adapters/p2p/webrtc-transport.mjs";
29
- export { generatePeerId, generateWorldId, generateDisplayName } from "../../domain/p2p/peer-id.mjs";
29
+ export { generatePeerId, generateWorldId, generateDisplayName, generateNodeId } from "../../domain/p2p/peer-id.mjs";
30
30
  export { chatSyncableFacts, mudSyncableFacts } from "../../domain/p2p/sync-filter.mjs";
31
31
  export { decodeInviteBlob, encodeInviteBlob } from "../../domain/p2p/wire.mjs";
32
32
  export {
@@ -29,6 +29,7 @@ import { parseEntities } from "../../domain/codegraph.mjs";
29
29
  import { loadLexicon } from "../../domain/grammar/lexicon.mjs";
30
30
  import { DEFAULT_GAME_CONFIG } from "../../domain/game-config.mjs";
31
31
  import { hanoiLessonSentences } from "../../domain/hanoi-lesson.mjs";
32
+ import { hanoiBoardRows, hanoiBoardGraphPayload } from "../../domain/hanoi-board.mjs";
32
33
  import { computeBlocksLayout, planToPageData, renderInputsFromPlan } from "../../services/plan-viz.mjs";
33
34
  import { planToPddl } from "../../services/plan-pddl.mjs";
34
35
  import { createTurnSession } from "./turn-session.mjs";
@@ -86,7 +87,29 @@ export async function createPlanSession({ diskCount = 3, maxDepth = DEFAULT_GAME
86
87
  if (r.plan) plan = r.plan;
87
88
  }
88
89
 
89
- return { memoryDir, sessionId, graph, diskCount, maxDepth, plan, turn: session.turn };
90
+ // The board `tmct.ask()` traverses. The plan lane's own states are rows, not
91
+ // a graph, so an ask over this session used to meet an empty one and miss
92
+ // every question about the puzzle in front of the visitor. hanoi-board.mjs
93
+ // projects one position into `{individuals, objectProperties}`; the page
94
+ // calls `showBoard` whenever it mounts a fresh plan or the transport moves
95
+ // the step, so what ask() reads is what the board shows.
96
+ let boardPlan = plan;
97
+ let boardStep = 0;
98
+ let boardGraph = null;
99
+ function showBoard({ plan: nextPlan = boardPlan, step = boardStep } = {}) {
100
+ boardPlan = nextPlan;
101
+ boardStep = Math.max(0, Math.floor(Number(step) || 0));
102
+ boardGraph = parseEntities(hanoiBoardGraphPayload(hanoiBoardRows({ plan: boardPlan, step: boardStep })));
103
+ return boardGraph;
104
+ }
105
+ showBoard();
106
+
107
+ return {
108
+ memoryDir, sessionId, graph, diskCount, maxDepth, plan, turn: session.turn,
109
+ showBoard,
110
+ get boardGraph() { return boardGraph; },
111
+ get boardStep() { return boardStep; },
112
+ };
90
113
  }
91
114
 
92
115
  // `tmct.page` keeps the board layout and the PDDL/OWL-RDF formatting the
@@ -95,9 +118,17 @@ export async function createPlanSession({ diskCount = 3, maxDepth = DEFAULT_GAME
95
118
  // CAPABILITY planner, not the puzzle solver: a typed "solve it" is a
96
119
  // conversational turn like any other, so the page reaches the hanoi plan
97
120
  // through `tmct.turn("solve it", { maxDepth })` and reads `.plan` off it.
121
+ //
122
+ // `tmct.ask(q, { step })` answers over the projected BOARD rather than the
123
+ // session's own (empty) code graph, so a question about the puzzle is grounded
124
+ // in the position on screen. Passing `step` moves the board first, which is
125
+ // how the page keeps the two in step while the transport scrubs.
98
126
  publishTmctSurface({
99
127
  open: createPlanSession,
100
- ask: graphAsk,
128
+ ask: (request, options, session) => {
129
+ if (options?.step != null) session.showBoard({ step: options.step });
130
+ return graphAsk(request, options, { graph: session.boardGraph, memoryDir: session.memoryDir });
131
+ },
101
132
  plan: enginePlan,
102
133
  page: { computeBlocksLayout, planToPageData, renderInputsFromPlan, planToPddl, registerWinkModel },
103
134
  });
@@ -43,13 +43,6 @@ import { publishTmctSurface } from "./tmct-surface.mjs";
43
43
  import { enginePlan } from "./engine-surface.mjs";
44
44
  import { exportFactsJsonl } from "./memory-stats.mjs";
45
45
 
46
- // The Fact individual's first-write-wins timestamp, read straight off the
47
- // stored attribute (mgx:createdAt) so a "recently learned" ordering never
48
- // has to re-parse a provenance tag — the research lane's own tags carry a
49
- // depth, not a timestamp, so the attribute is the one field every source
50
- // shares.
51
- const CREATED_AT_ATTR = "mgx:createdAt";
52
-
53
46
  /**
54
47
  * The source key ONE provenance tag folds to, for the page's per-source
55
48
  * checkboxes/history. `sessionIds` tells a teach tag apart: a teach:chat tag
@@ -102,18 +95,17 @@ const clonePayload = (payload) => {
102
95
  try { return structuredClone(payload); } catch { return JSON.parse(JSON.stringify(payload)); }
103
96
  };
104
97
 
105
- /** Fact rows plus the createdAt attribute readFactRows drops, in one pass over
106
- * the loaded memory — the "recently learned" panels want the timestamp, the
107
- * ask filter wants the id, both want the provenance. */
98
+ /** Fact rows plus the createdAt readFactRows keeps per assertion rather than per
99
+ * row — the "recently learned" panels want the timestamp, the ask filter wants
100
+ * the id, both want the provenance. A triple asserted by several sources was
101
+ * first learned when the EARLIEST of them said it, which is the moment those
102
+ * panels are ordering by. */
108
103
  async function factRowsWithCreatedAt(memoryDir) {
109
104
  const memory = await loadMemory(memoryDir);
110
- const createdById = new Map();
111
- for (const ind of memory?.individuals || []) {
112
- if (ind?.class !== "Fact") continue;
113
- const at = (ind.attributes || []).find((a) => a?.prop === CREATED_AT_ATTR || a?.key === CREATED_AT_ATTR)?.value || "";
114
- createdById.set(ind.id, at);
115
- }
116
- return readFactRows(memory).map((row) => ({ ...row, createdAt: createdById.get(row.id) || "" }));
105
+ return readFactRows(memory).map((row) => {
106
+ const stamps = (row.assertions || []).map((a) => a.createdAt).filter(Boolean).sort();
107
+ return { ...row, createdAt: stamps[0] || "" };
108
+ });
117
109
  }
118
110
 
119
111
  const SOURCE_ORDER = ["taught", "ingest", "research"];
@@ -212,7 +204,7 @@ export function createResearchSession({ seedPayload = null, vocabSeeded = false,
212
204
 
213
205
  const graph = parseEntities({ individuals: [], objectProperties: [] });
214
206
  const lexicon = loadLexicon();
215
- const vocabHint = vocabExampleHint(vocabSeeded);
207
+ const vocabHint = vocabExampleHint(vocabSeeded, "browser");
216
208
  // A DISTINCT id so an ingested fact's teach tag is told apart from a typed
217
209
  // teach turn's by session id alone — the whole reason the two growth paths
218
210
  // stay separable in the source panel.
@@ -236,7 +228,7 @@ export function createResearchSession({ seedPayload = null, vocabSeeded = false,
236
228
  sessionId: globalThis.crypto?.randomUUID?.() ?? String(Date.now()),
237
229
  buildExtraOptions: () => ({
238
230
  researchConfig, liveReference: liveReferenceOn, onLiveLookup,
239
- uiContext: "browser", synthesisBudget: synthesisBudgetOn,
231
+ synthesisBudget: synthesisBudgetOn,
240
232
  }),
241
233
  captureExtraState: (result) => {
242
234
  if (typeof result.liveReference === "boolean" || result.liveReference === "supplement" || result.liveReference === "always") {
@@ -18,36 +18,62 @@
18
18
  // a typed class name through ask.mjs's resolveObject, so it needs the real
19
19
  // resolver in the page rather than a self-contained function the page could
20
20
  // splice in as text.
21
- import { createInMemoryStore, appendFacts, normFactTerm } from "../../adapters/memory/core.mjs";
21
+ //
22
+ // `tmct.ask()` needs a real graph to traverse (engine-surface.mjs's graphAsk
23
+ // dispatches tmct_ask over `session.graph`), so this session projects its own
24
+ // store into one via spriteFactGraphPayload — sprite-facts.mjs's own
25
+ // counterpart to spider-fly-browser-entry.mjs's worldRelationGraphPayload
26
+ // use. `refreshGraph()` rebuilds it from the store's CURRENT rows (not just
27
+ // the embedded factRows), so a fact taught mid-session is visible to a later
28
+ // ask() the same way spider-fly's board rebuild is; `turn()` and the
29
+ // published `ask` route both call it first, mirroring spider-fly's exact
30
+ // wiring points.
31
+ import { createInMemoryStore, appendFacts, normFactTerm, loadMemory, readFactRows } from "../../adapters/memory/core.mjs";
22
32
  import { extractSceneItems } from "../../domain/scene-compose.mjs";
23
33
  import { parseEntities } from "../../domain/codegraph.mjs";
24
34
  import { loadLexicon } from "../../domain/grammar/lexicon.mjs";
25
- import { SPRITE_FACTS_PROVENANCE } from "../../domain/sprite-facts.mjs";
35
+ import { SPRITE_FACTS_PROVENANCE, spriteFactGraphPayload } from "../../domain/sprite-facts.mjs";
26
36
  import { registerWinkModel } from "../../adapters/wink-model.mjs";
27
37
  import { createTurnSession } from "./turn-session.mjs";
28
38
  import { publishTmctSurface } from "./tmct-surface.mjs";
29
39
  import { graphAsk, enginePlan } from "./engine-surface.mjs";
30
40
 
31
41
  /** A live in-memory chat session seeded with the embedded sprite-facts rows.
32
- * Returns { memoryDir, sessionId, graph, factCount, turn }. */
42
+ * Returns { memoryDir, sessionId, graph, refreshGraph, factCount, turn }. */
33
43
  export async function createSpriteCatalogSession({ factRows = [] } = {}) {
34
44
  const memoryDir = createInMemoryStore();
35
45
  await appendFacts(memoryDir, factRows.map((f) => ({
36
46
  subject: f.subject, predicate: f.predicate, object: f.object, provenance: SPRITE_FACTS_PROVENANCE,
37
47
  })));
38
48
 
39
- const graph = parseEntities({ individuals: [], objectProperties: [] });
49
+ let graph = parseEntities({ individuals: [], objectProperties: [] });
50
+ async function refreshGraph() {
51
+ const rows = readFactRows(await loadMemory(memoryDir));
52
+ graph = parseEntities(spriteFactGraphPayload(rows));
53
+ }
54
+ await refreshGraph();
55
+
40
56
  const lexicon = loadLexicon();
41
57
  const sessionId = globalThis.crypto?.randomUUID?.() ?? String(Date.now());
42
58
 
43
- const session = createTurnSession({ memoryDir, graph, lexicon, sessionId, vocabHint: "" });
59
+ // `graph` is re-read here, not captured once: createTurnSession binds its
60
+ // own copy at creation, and refreshGraph() reassigns this closure's own
61
+ // variable on every call.
62
+ const turnSession = createTurnSession({
63
+ memoryDir, graph, lexicon, sessionId, vocabHint: "",
64
+ buildExtraOptions: () => ({ graph }),
65
+ });
44
66
 
45
67
  return {
46
68
  memoryDir,
47
69
  sessionId,
48
- graph,
70
+ get graph() { return graph; },
71
+ refreshGraph,
49
72
  factCount: factRows.length,
50
- turn: session.turn,
73
+ async turn(line) {
74
+ await refreshGraph();
75
+ return turnSession.turn(line);
76
+ },
51
77
  };
52
78
  }
53
79
 
@@ -58,7 +84,12 @@ export async function createSpriteCatalogSession({ factRows = [] } = {}) {
58
84
  // rather than answering anything.
59
85
  publishTmctSurface({
60
86
  open: createSpriteCatalogSession,
61
- ask: graphAsk,
87
+ // The graph is rebuilt first, so a direct tmct.ask() call (not just a
88
+ // typed chat turn) sees any fact taught since the last one.
89
+ ask: async (request, options, session) => {
90
+ await session.refreshGraph();
91
+ return graphAsk(request, options, session);
92
+ },
62
93
  plan: enginePlan,
63
94
  page: { registerWinkModel, normFactTerm, extractSceneItems },
64
95
  });
@@ -25,6 +25,18 @@
25
25
  // them apart at a glance, which is exactly what eleven flat bags made
26
26
  // impossible.
27
27
  //
28
+ // Two more members show up on some pages, by convention rather than by
29
+ // anything this function returns: `tmct.ready` is the page's own boot
30
+ // promise (chat, ingest), and `tmct.lastSave` is its last background-save
31
+ // record, `{at, ms}` (chat, adventure). Neither can be threaded through
32
+ // `publishTmctSurface(...)` itself — boot is still running when this
33
+ // function returns, so the page's own script sets each once it reaches
34
+ // that point (see chat-page-viz.mjs, ingest-viz.mjs, adventure-viz.mjs).
35
+ // Each page's older bare global — window.tmctChatReady, tmctIngestReady,
36
+ // tmctChatLastSave, tmctAdventureLastSave — keeps working unchanged;
37
+ // `tmct.ready` / `tmct.lastSave` just reach the same value under the one
38
+ // `tmct.*` name everything else on this page already uses.
39
+ //
28
40
  // `ask` and `plan` are supplied per page rather than fixed here, because what
29
41
  // a page can ground a question against genuinely differs: code-explorer holds
30
42
  // a code graph, spider-fly holds a live board projected into one, the ledger
@@ -9,8 +9,15 @@
9
9
  // capture, and a return shape covering every field one caller or another
10
10
  // reads back.
11
11
  //
12
+ // Every caller of this wrapper is a page by construction, so `uiContext:
13
+ // "browser"` is a DEFAULT here rather than something each entry remembers to
14
+ // pass. It is what makes the engine's dead-ends name an exit a page can take
15
+ // ("teach me a fact") instead of the CLI's `tmct index`/`--repo`/`tmct init`.
16
+ // Five of the nine already passed it and four had never got round to it, which
17
+ // is exactly the drift a shared default removes.
18
+ //
12
19
  // The nine differ in what they hand `runTurn` beyond the common core
13
- // (`uiContext`, `synthesisBudget`, `gameConfig`, `researchConfig`,
20
+ // (`synthesisBudget`, `gameConfig`, `researchConfig`,
14
21
  // `actingSubject`, a per-character `sessionId`...) and in what they do with a
15
22
  // turn's result besides the standard focus/last/planState/researchState fold
16
23
  // (sync an externally-held plan holder, grow a visited-rooms set, bump a
@@ -41,7 +48,7 @@ function turnErrorFallback(message) {
41
48
  *
42
49
  * `buildExtraOptions(state, callArgs)` (optional) returns extra `runTurn`
43
50
  * options to merge OVER the defaults below — anything a specific page needs
44
- * (`uiContext: "browser"`, `synthesisBudget`, `gameConfig`, `researchConfig`,
51
+ * (`synthesisBudget`, `gameConfig`, `researchConfig`,
45
52
  * a per-character `actingSubject`/`sessionId`, or a `planState` read from an
46
53
  * external holder instead of this closure's own). `state` is
47
54
  * `{ focus, last, planState, researchState }` as this turn is about to run;
@@ -88,7 +95,7 @@ export function createTurnSession({
88
95
  try {
89
96
  result = await runTurn(line, {
90
97
  config: null, source: null, graph, focus, last, memoryDir, sessionId,
91
- env: {}, lexicon, vocabHint, planState, researchState,
98
+ env: {}, lexicon, vocabHint, planState, researchState, uiContext: "browser",
92
99
  ...extra,
93
100
  });
94
101
  } catch (e) {