@polycode-projects/the-mechanical-code-talker 5.0.5 → 5.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/README.md +78 -19
  2. package/bin/tmct.mjs +63 -2
  3. package/package.json +1 -1
  4. package/src/adapters/memory/core.mjs +23 -0
  5. package/src/domain/ask-vocab.mjs +19 -0
  6. package/src/domain/ask.mjs +10 -5
  7. package/src/domain/codegraph.mjs +23 -9
  8. package/src/domain/game-config.mjs +12 -0
  9. package/src/domain/interpret/strategies/keywords.mjs +30 -1
  10. package/src/domain/memory/capability.mjs +15 -11
  11. package/src/domain/router/drive.mjs +36 -17
  12. package/src/domain/router/resolver.mjs +63 -17
  13. package/src/domain/spider-fly-world.mjs +2 -2
  14. package/src/domain/sprite-templates.mjs +19 -7
  15. package/src/domain/syllogise.mjs +16 -6
  16. package/src/domain/town-square-world.mjs +1 -1
  17. package/src/services/adventure-viz.mjs +5 -2
  18. package/src/services/adventure.mjs +8 -1
  19. package/src/services/chat-page-viz.mjs +123 -25
  20. package/src/services/chat-session.mjs +60 -10
  21. package/src/services/chat.mjs +328 -36
  22. package/src/services/code-explorer-viz.mjs +3 -2
  23. package/src/services/extract-facts.mjs +47 -7
  24. package/src/services/ingest-viz.mjs +113 -29
  25. package/src/services/ledger-viz.mjs +9 -4
  26. package/src/services/memory-panel-viz.mjs +44 -0
  27. package/src/services/mud-viz.mjs +21 -3
  28. package/src/services/mudiii-scene.mjs +407 -36
  29. package/src/services/mudiii-turn.mjs +65 -9
  30. package/src/services/mudiii-viz.mjs +810 -157
  31. package/src/services/p2p-room.mjs +1 -1
  32. package/src/services/plan-viz.mjs +26 -4
  33. package/src/services/predator-prey.mjs +141 -37
  34. package/src/services/research-viz.mjs +17 -23
  35. package/src/services/spider-fly-turn.mjs +7 -1
  36. package/src/services/spider-fly-viz.mjs +13 -5
  37. package/src/services/sprite-catalog-viz.mjs +3 -2
  38. package/src/services/viz-theme.mjs +20 -0
  39. package/src/services/viz-ticker.mjs +15 -2
  40. package/src/surfaces/http/server-http.mjs +90 -13
  41. package/src/surfaces/web/memory-ask-browser.bundle.js +125 -125
  42. package/src/surfaces/web/mud-browser-entry.mjs +33 -1
  43. package/src/surfaces/web/mudiii-browser-entry.mjs +70 -34
  44. package/src/surfaces/web/tmct-surface.mjs +18 -6
  45. package/src/tools/handlers/tmct-ask.mjs +15 -2
  46. package/src/tools/server.mjs +31 -2
@@ -15,6 +15,7 @@
15
15
  // HTTP surface.
16
16
 
17
17
  import { createServer } from "node:http";
18
+ import { stat } from "node:fs/promises";
18
19
  import { runTurn, selectTool, capabilityPlanDeps } from "../../services/chat.mjs";
19
20
  import { TOOLS, dispatchTool } from "../../tools/server.mjs";
20
21
  import { runCapabilityPlan, buildCapabilityPlanCtx, declaredCapabilityNames } from "../../domain/router/drive.mjs";
@@ -143,7 +144,28 @@ function withRestNote(text, rest) {
143
144
  * - a mapped, declared graph tool → tool_use
144
145
  * - otherwise → end_turn text via runTurn
145
146
  */
146
- export async function respondToMessages(body, { config, graph, source = defaultSource } = {}) {
147
+ /** How many Fact individuals a store snapshot holds. Facts only: an ordinary
148
+ * turn records an Utterance and a Session too, and counting those made a game
149
+ * move or a cited lookup read as a teach that went nowhere. */
150
+ function countStoredFacts(snapshot) {
151
+ const individuals = snapshot?.payload?.individuals;
152
+ if (!Array.isArray(individuals)) return 0;
153
+ return individuals.reduce((n, i) => n + ((i?.class || "") === "Fact" ? 1 : 0), 0);
154
+ }
155
+
156
+ /** The memory store's vocabulary reader over a throwaway copy of `memoryDir` —
157
+ * the seam a cold tool call gets so `tmct_ask` here answers what chat answers
158
+ * over the same repo. Null when the server was started without a store. */
159
+ async function memoryFactLookup(memoryDir) {
160
+ if (!memoryDir) return null;
161
+ const { readOnlyMemorySnapshot } = await import("../../adapters/memory/core.mjs");
162
+ const snapshot = await readOnlyMemorySnapshot(memoryDir);
163
+ if (!snapshot) return null;
164
+ const { factAnswer } = await import("../../services/chat.mjs");
165
+ return (query, envelope) => factAnswer(snapshot, query, envelope, true);
166
+ }
167
+
168
+ export async function respondToMessages(body, { config, graph, memoryDir = null, source = defaultSource } = {}) {
147
169
  const { model, messages, tools } = body || {};
148
170
  const declaredNames = new Set(
149
171
  (Array.isArray(tools) ? tools : []).map((t) => t && t.name).filter(Boolean),
@@ -185,7 +207,7 @@ export async function respondToMessages(body, { config, graph, source = defaultS
185
207
  return msg;
186
208
  }
187
209
  try {
188
- const out = await dispatchTool(name, input, { config, source });
210
+ const out = await dispatchTool(name, input, { config, source, factLookup: await memoryFactLookup(memoryDir) });
189
211
  const msg = assistantMessage(model, [{ type: "text", text: withRestNote(out, rest) }], "end_turn");
190
212
  msg.tmct_checked_call = { name, input, problems: [] };
191
213
  return msg;
@@ -225,10 +247,25 @@ export async function respondToMessages(body, { config, graph, source = defaultS
225
247
  }
226
248
  }
227
249
 
228
- // text answer: the cited, read-only answer the chat surface gives. memoryDir is
229
- // null so the endpoint is PURE no session artifacts, no writes, deterministic.
230
- const { answer } = await runTurn(userText, { config, graph, source, memoryDir: null });
231
- return assistantMessage(model, [{ type: "text", text: answer }], "end_turn");
250
+ // text answer: the cited, read-only answer the chat surface gives, over the
251
+ // same repo's memory store without it, a term chat answers came back from
252
+ // this endpoint as a miss. The store is handed over as a throwaway in-memory
253
+ // COPY, so the endpoint stays PURE: reads see the real facts, and anything a
254
+ // turn would write lands in the copy rather than on disk.
255
+ const { readOnlyMemorySnapshot } = await import("../../adapters/memory/core.mjs");
256
+ const snapshot = await readOnlyMemorySnapshot(memoryDir);
257
+ const factsBefore = countStoredFacts(snapshot);
258
+ const { answer } = await runTurn(userText, { config, graph, source, memoryDir: snapshot });
259
+ // A teach turn lands in the copy and confirms itself. Say plainly that the
260
+ // fact went nowhere, rather than leaving "noted — remembered" as the last
261
+ // word on a write this endpoint never makes.
262
+ const wrote = countStoredFacts(snapshot) > factsBefore;
263
+ // A game's opening move writes board facts the same way a teach writes one,
264
+ // so the advice names the turn rather than assuming a fact was taught.
265
+ const text = wrote
266
+ ? `${answer}\n(nothing was stored — this endpoint reads the memory store and never writes to it. Run the same turn in a chat session to keep what it writes.)`
267
+ : answer;
268
+ return assistantMessage(model, [{ type: "text", text }], "end_turn");
232
269
  }
233
270
 
234
271
  /** The capability names a /v1/plan request restricts its plan to (its `tools`
@@ -308,9 +345,48 @@ function sendError(res, status, type, message) {
308
345
  sendJson(res, status, { type: "error", error: { type, message } });
309
346
  }
310
347
 
348
+ /** A stamp of every graph file's size and mtime. Two equal stamps mean the
349
+ * parsed graph in hand is still the graph on disk. */
350
+ async function graphFileStamp(config) {
351
+ const files = config.graphFiles?.length ? config.graphFiles : [config.graphFile];
352
+ const parts = [];
353
+ for (const f of files) {
354
+ try {
355
+ const s = await stat(f);
356
+ parts.push(`${f}:${s.size}:${s.mtimeMs}`);
357
+ } catch { parts.push(`${f}:absent`); }
358
+ }
359
+ return parts.join("|");
360
+ }
361
+
362
+ /**
363
+ * A graph reader that re-parses when the artifact underneath it changes. The
364
+ * cold tool route loads the graph per call (dispatchTool → loadGraph), so a
365
+ * text answer served from a graph parsed at startup and a tool answer served
366
+ * from the file disagreed about the same repo the moment anything reindexed it
367
+ * mid-run. Stat-guarded, so an unchanged file costs one stat rather than a
368
+ * re-parse.
369
+ */
370
+ function reloadingGraph(config, source) {
371
+ let stamp = null;
372
+ let graph = null;
373
+ return async () => {
374
+ const now = await graphFileStamp(config);
375
+ if (graph && now === stamp) return graph;
376
+ // source.fetchEntities keys its own per-process cache on the file PATH
377
+ // alone, so a same-path rewrite would still serve the payload parsed at
378
+ // startup. Drop it before re-reading; a stamp only changes when the bytes
379
+ // on disk did.
380
+ if (graph) source.clearCache?.();
381
+ graph = parseEntities(await source.fetchEntities(config));
382
+ stamp = now;
383
+ return graph;
384
+ };
385
+ }
386
+
311
387
  /**
312
- * Start the HTTP server. Loads the graph once (tolerant: a missing artifact is
313
- * the empty bootstrap graph, never an error). Returns { server, url, host, port,
388
+ * Start the HTTP server. The graph is read tolerantly: a missing artifact is
389
+ * the empty bootstrap graph, never an error. Returns { server, url, host, port,
314
390
  * config, close } — `close()` shuts the socket cleanly (no hanging handles).
315
391
  *
316
392
  * config — { graphFile } (build via configFor(repoPath) in bin/tmct.mjs)
@@ -319,9 +395,10 @@ function sendError(res, status, type, message) {
319
395
  */
320
396
  export async function startServer({ config, host = "127.0.0.1", port = 0, source = defaultSource, memoryDir = null } = {}) {
321
397
  if (!config || !config.graphFile) throw new Error("startServer requires config.graphFile");
322
- // Load the graph once, up front. A missing artifact loads as the empty
323
- // bootstrap graph runTurn tolerates it (an honest empty/orienting answer).
324
- const graph = parseEntities(await source.fetchEntities(config));
398
+ const currentGraph = reloadingGraph(config, source);
399
+ // Parse once up front so a listening server has already paid for the common
400
+ // case, and so a broken artifact surfaces before the first request.
401
+ await currentGraph();
325
402
 
326
403
  const server = createServer(async (req, res) => {
327
404
  try {
@@ -352,7 +429,7 @@ export async function startServer({ config, host = "127.0.0.1", port = 0, source
352
429
  sendError(res, 400, "invalid_request_error", `unknown tools name(s): ${unknown.join(", ")}; registered capabilities: ${declared.join(", ")}`);
353
430
  return;
354
431
  }
355
- const out = await respondToPlan(body, { config, graph, memoryDir, source });
432
+ const out = await respondToPlan(body, { config, graph: await currentGraph(), memoryDir, source });
356
433
  sendJson(res, 200, out);
357
434
  return;
358
435
  }
@@ -375,7 +452,7 @@ export async function startServer({ config, host = "127.0.0.1", port = 0, source
375
452
  sendError(res, 400, "invalid_request_error", "`messages` array is required");
376
453
  return;
377
454
  }
378
- const out = await respondToMessages(body, { config, graph, source });
455
+ const out = await respondToMessages(body, { config, graph: await currentGraph(), memoryDir, source });
379
456
  sendJson(res, 200, out);
380
457
  } catch (e) {
381
458
  sendError(res, 500, "api_error", e && e.message ? e.message : String(e));