@geravant/sinain 1.22.5 → 1.22.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geravant/sinain",
3
- "version": "1.22.5",
3
+ "version": "1.22.7",
4
4
  "description": "Ambient intelligence that sees what you see, hears what you hear, and acts on your behalf",
5
5
  "type": "module",
6
6
  "bin": {
@@ -60,14 +60,7 @@ async function queryKnowledgeFactsMulti(entities: string[], maxFacts: number): P
60
60
  `${workspaceDir}/knowledge-graph.db`,
61
61
  ];
62
62
 
63
- // Candidate script paths
64
- const __dir = dirname(fileURLToPath(import.meta.url));
65
- const scriptCandidates = [
66
- resolve(__dir, "..", "..", "sinain-hud-plugin", "sinain-memory", "graph_query.py"),
67
- resolve(__dir, "..", "sinain-memory", "graph_query.py"),
68
- `${resolveWorkspace()}/sinain-memory/graph_query.py`,
69
- ];
70
- const scriptPath = scriptCandidates.find(p => existsSync(p)) || scriptCandidates[0];
63
+ const scriptPath = resolveSinainMemoryScript("graph_query.py");
71
64
 
72
65
  // Step 1: Get candidates from Python (RRF-ranked, no embedding — avoids deadlock)
73
66
  // Request 2x candidates in JSON for re-ranking in Node.js
@@ -142,13 +135,7 @@ async function listKnowledgeEntitiesMulti(max: number): Promise<string> {
142
135
  `${workspaceDir}/knowledge-graph.db`,
143
136
  ];
144
137
 
145
- const __dir = dirname(fileURLToPath(import.meta.url));
146
- const scriptCandidates = [
147
- resolve(__dir, "..", "..", "sinain-hud-plugin", "sinain-memory", "graph_query.py"),
148
- resolve(__dir, "..", "sinain-memory", "graph_query.py"),
149
- `${resolveWorkspace()}/sinain-memory/graph_query.py`,
150
- ];
151
- const scriptPath = scriptCandidates.find(p => existsSync(p)) || scriptCandidates[0];
138
+ const scriptPath = resolveSinainMemoryScript("graph_query.py");
152
139
 
153
140
  const allFacts: any[] = [];
154
141
  for (const dbPath of dbPaths) {
@@ -174,17 +161,33 @@ async function listKnowledgeEntitiesMulti(max: number): Promise<string> {
174
161
  return JSON.stringify(unique.slice(0, max));
175
162
  }
176
163
 
177
- /** Resolve graph_query.py script path. Used by all knowledge-graph subprocess calls. */
178
- function resolveGraphQueryScript(): string {
164
+ /**
165
+ * Resolve a Python script under sinain-memory/. Two layouts are supported:
166
+ *
167
+ * - **dev repo**: code is at `<repo>/sinain-core/src/index.ts`, scripts at
168
+ * `<repo>/sinain-hud-plugin/sinain-memory/<name>` (so `__dir/../../sinain-hud-plugin/sinain-memory`).
169
+ * - **npm-installed**: code is at `node_modules/@geravant/sinain/sinain-core/src/index.ts`,
170
+ * scripts at `node_modules/@geravant/sinain/sinain-memory/<name>` (so `__dir/../../sinain-memory`).
171
+ *
172
+ * Plus a workspace fallback for OpenClaw layouts. ENOENT on the resolved
173
+ * path was Irina's bug — the npm path was missing from candidates.
174
+ */
175
+ function resolveSinainMemoryScript(scriptName: string): string {
179
176
  const __dir = new URL(import.meta.url).pathname.replace(/\/[^/]+$/, "");
180
177
  const candidates = [
181
- `${__dir}/../../sinain-hud-plugin/sinain-memory/graph_query.py`,
182
- `${__dir}/../sinain-memory/graph_query.py`,
183
- `${resolveWorkspace()}/sinain-memory/graph_query.py`,
178
+ `${__dir}/../../sinain-hud-plugin/sinain-memory/${scriptName}`, // dev repo
179
+ `${__dir}/../../sinain-memory/${scriptName}`, // npm install
180
+ `${__dir}/../sinain-memory/${scriptName}`, // legacy
181
+ `${resolveWorkspace()}/sinain-memory/${scriptName}`, // openclaw workspace
184
182
  ];
185
183
  return candidates.find(p => existsSync(p)) || candidates[0];
186
184
  }
187
185
 
186
+ /** Backward-compat alias used by legacy call sites. */
187
+ function resolveGraphQueryScript(): string {
188
+ return resolveSinainMemoryScript("graph_query.py");
189
+ }
190
+
188
191
  /** List of candidate knowledge DB paths (local + workspace). */
189
192
  function resolveKnowledgeDbPaths(): string[] {
190
193
  return [
@@ -237,13 +240,7 @@ async function exportConceptBundle(
237
240
  const { execFile } = await import("node:child_process");
238
241
  const { promisify } = await import("node:util");
239
242
  const pExecFile = promisify(execFile);
240
- const __dir = new URL(import.meta.url).pathname.replace(/\/[^/]+$/, "");
241
- const scriptCandidates = [
242
- `${__dir}/../../sinain-hud-plugin/sinain-memory/concept_export.py`,
243
- `${__dir}/../sinain-memory/concept_export.py`,
244
- `${resolveWorkspace()}/sinain-memory/concept_export.py`,
245
- ];
246
- const scriptPath = scriptCandidates.find(p => existsSync(p)) || scriptCandidates[0];
243
+ const scriptPath = resolveSinainMemoryScript("concept_export.py");
247
244
  const webDbPath = `${resolveLocalMemoryDir()}/web.db`;
248
245
 
249
246
  for (const dbPath of resolveKnowledgeDbPaths()) {
@@ -286,13 +283,7 @@ async function importConceptBundle(
286
283
  const { execFile } = await import("node:child_process");
287
284
  const { promisify } = await import("node:util");
288
285
  const pExecFile = promisify(execFile);
289
- const __dir = new URL(import.meta.url).pathname.replace(/\/[^/]+$/, "");
290
- const scriptCandidates = [
291
- `${__dir}/../../sinain-hud-plugin/sinain-memory/concept_import.py`,
292
- `${__dir}/../sinain-memory/concept_import.py`,
293
- `${resolveWorkspace()}/sinain-memory/concept_import.py`,
294
- ];
295
- const scriptPath = scriptCandidates.find(p => existsSync(p)) || scriptCandidates[0];
286
+ const scriptPath = resolveSinainMemoryScript("concept_import.py");
296
287
  const localDir = resolveLocalMemoryDir();
297
288
  const dbPath = `${localDir}/knowledge-graph.db`;
298
289
  const webDbPath = `${localDir}/web.db`;
@@ -336,13 +327,7 @@ async function retractOrRestoreFact(
336
327
  const { execFile } = await import("node:child_process");
337
328
  const { promisify } = await import("node:util");
338
329
  const pExecFile = promisify(execFile);
339
- const __dir = new URL(import.meta.url).pathname.replace(/\/[^/]+$/, "");
340
- const scriptCandidates = [
341
- `${__dir}/../../sinain-hud-plugin/sinain-memory/retract.py`,
342
- `${__dir}/../sinain-memory/retract.py`,
343
- `${resolveWorkspace()}/sinain-memory/retract.py`,
344
- ];
345
- const scriptPath = scriptCandidates.find(p => existsSync(p)) || scriptCandidates[0];
330
+ const scriptPath = resolveSinainMemoryScript("retract.py");
346
331
  const webDbPath = `${resolveLocalMemoryDir()}/web.db`;
347
332
 
348
333
  // Try DBs in order — the fact lives in one of them.
@@ -383,13 +368,7 @@ async function renderEntityPageMulti(
383
368
  const { execFile } = await import("node:child_process");
384
369
  const { promisify } = await import("node:util");
385
370
  const pExecFile = promisify(execFile);
386
- const __dir = new URL(import.meta.url).pathname.replace(/\/[^/]+$/, "");
387
- const scriptCandidates = [
388
- `${__dir}/../../sinain-hud-plugin/sinain-memory/page_renderer.py`,
389
- `${__dir}/../sinain-memory/page_renderer.py`,
390
- `${resolveWorkspace()}/sinain-memory/page_renderer.py`,
391
- ];
392
- const scriptPath = scriptCandidates.find(p => existsSync(p)) || scriptCandidates[0];
371
+ const scriptPath = resolveSinainMemoryScript("page_renderer.py");
393
372
  const webDbPath = `${resolveLocalMemoryDir()}/web.db`;
394
373
 
395
374
  // Try DBs in order; first one with the entity wins.
@@ -456,10 +435,12 @@ async function queryKnowledgeAsOfMulti(entity: string, date: string): Promise<st
456
435
  `${workspaceDir}/knowledge-graph.db`,
457
436
  ];
458
437
 
438
+ const __dir = dirname(new URL(import.meta.url).pathname);
459
439
  const scriptCandidates = [
460
- `${dirname(new URL(import.meta.url).pathname)}/../sinain-hud-plugin/sinain-memory`,
461
- `${dirname(new URL(import.meta.url).pathname)}/sinain-memory`,
462
- `${resolveWorkspace()}/sinain-memory`,
440
+ `${__dir}/../../sinain-hud-plugin/sinain-memory`, // dev repo
441
+ `${__dir}/../../sinain-memory`, // npm install
442
+ `${__dir}/../sinain-memory`, // legacy
443
+ `${resolveWorkspace()}/sinain-memory`, // workspace
463
444
  ];
464
445
  const scriptsDir = scriptCandidates.find(p => existsSync(`${p}/triplestore.py`)) || scriptCandidates[0];
465
446
 
@@ -499,12 +480,7 @@ async function exportKnowledgeMulti(domain: string | null, max: number): Promise
499
480
  `${workspaceDir}/knowledge-graph.db`,
500
481
  ];
501
482
 
502
- const __dir = dirname(fileURLToPath(import.meta.url));
503
- const scriptCandidates = [
504
- resolve(__dir, "..", "..", "sinain-hud-plugin", "sinain-memory", "graph_query.py"),
505
- `${resolveWorkspace()}/sinain-memory/graph_query.py`,
506
- ];
507
- const scriptPath = scriptCandidates.find(p => existsSync(p)) || scriptCandidates[0];
483
+ const scriptPath = resolveSinainMemoryScript("graph_query.py");
508
484
 
509
485
  const allFacts: any[] = [];
510
486
  for (const dbPath of dbPaths) {
@@ -493,10 +493,57 @@ function ensurePeerJsLoaded() {
493
493
 
494
494
  function newPeer(idOrUndef) {
495
495
  // Honor SHARE_PEERJS_HOST env-injected override. Empty string = peerjs.com default.
496
- const opts = SHARE_PEERJS_HOST ? { host: SHARE_PEERJS_HOST } : {};
496
+ // debug: 3 enables verbose peerjs logging in console critical for diagnosing
497
+ // WebRTC handshake failures (NAT, ICE state transitions, peer-unavailable, etc.).
498
+ const opts = SHARE_PEERJS_HOST ? { host: SHARE_PEERJS_HOST, debug: 3 } : { debug: 3 };
497
499
  return idOrUndef ? new window.Peer(idOrUndef, opts) : new window.Peer(opts);
498
500
  }
499
501
 
502
+ // Attach detailed instrumentation to a peer + its underlying RTCPeerConnections.
503
+ // Logs to console (sinain-share namespace) so diagnostics are visible without
504
+ // any UI changes. Tracks the four state machines that matter for WebRTC: ICE
505
+ // gathering, ICE connection, peerconnection, and signaling.
506
+ function instrumentPeer(peer, label) {
507
+ const tag = "[sinain-share:" + label + "]";
508
+ console.log(tag, "instrumented peer", peer.id || "(no-id-yet)");
509
+ peer.on("open", id => console.log(tag, "peer.open id=" + id));
510
+ peer.on("error", e => console.warn(tag, "peer.error type=" + (e && e.type) + " msg=" + (e && e.message)));
511
+ peer.on("disconnected", () => console.warn(tag, "peer.disconnected (lost broker connection)"));
512
+ peer.on("close", () => console.log(tag, "peer.close (destroyed)"));
513
+ }
514
+
515
+ function instrumentConnection(conn, label) {
516
+ const tag = "[sinain-share:" + label + ":" + (conn.peer || "?") + "]";
517
+ console.log(tag, "new connection, reliable=" + conn.reliable);
518
+ conn.on("open", () => console.log(tag, "conn.open (DataChannel ready)"));
519
+ conn.on("error", e => console.warn(tag, "conn.error", e));
520
+ conn.on("close", () => console.log(tag, "conn.close"));
521
+ conn.on("iceStateChanged", s => console.log(tag, "conn.iceStateChanged →", s));
522
+ // Tap into the underlying RTCPeerConnection. peerjs exposes it as
523
+ // conn.peerConnection (it might not exist immediately — wait a tick).
524
+ setTimeout(() => {
525
+ const pc = conn.peerConnection;
526
+ if (!pc) { console.warn(tag, "no peerConnection exposed"); return; }
527
+ pc.addEventListener("iceconnectionstatechange",
528
+ () => console.log(tag, "iceConnectionState →", pc.iceConnectionState));
529
+ pc.addEventListener("connectionstatechange",
530
+ () => console.log(tag, "connectionState →", pc.connectionState));
531
+ pc.addEventListener("icegatheringstatechange",
532
+ () => console.log(tag, "iceGatheringState →", pc.iceGatheringState));
533
+ pc.addEventListener("signalingstatechange",
534
+ () => console.log(tag, "signalingState →", pc.signalingState));
535
+ pc.addEventListener("icecandidate", (e) => {
536
+ if (!e.candidate) { console.log(tag, "ICE gathering complete"); return; }
537
+ const c = e.candidate;
538
+ // Log candidate type (host = local LAN, srflx = STUN, relay = TURN, prflx = peer-reflexive).
539
+ // If we never see "relay" but only "host"/"srflx" and connection fails, NAT traversal
540
+ // requires TURN — which peerjs cloud doesn't provide.
541
+ const type = (c.candidate.match(/typ (\\S+)/) || [])[1] || "?";
542
+ console.log(tag, "iceCandidate type=" + type + " proto=" + c.protocol + " addr=" + (c.address || "?") + ":" + (c.port || "?"));
543
+ });
544
+ }, 100);
545
+ }
546
+
500
547
  const ShareManager = (() => {
501
548
  // share_token → live Peer instance (sender side only). Bundles are re-fetched
502
549
  // on demand rather than kept in JS memory across resume.
@@ -543,6 +590,7 @@ const ShareManager = (() => {
543
590
  // Peer mode
544
591
  await ensurePeerJsLoaded();
545
592
  const peer = newPeer(token);
593
+ instrumentPeer(peer, "sender:" + token.slice(0, 8));
546
594
  await new Promise((res, rej) => {
547
595
  peer.on("open", () => res());
548
596
  peer.on("error", e => rej(e));
@@ -565,13 +613,17 @@ const ShareManager = (() => {
565
613
 
566
614
  function attachSenderHandlers(peer, token, entity) {
567
615
  peer.on("connection", (conn) => {
616
+ console.log("[sinain-share:sender:" + token.slice(0, 8) + "] inbound connection from", conn.peer);
617
+ instrumentConnection(conn, "sender");
568
618
  patchStatus(token, "connecting");
569
619
  conn.on("open", async () => {
570
620
  try {
571
621
  // Re-fetch bundle each time — keeps memory low and reflects latest state.
572
622
  const bundle = await buildBundle(entity);
623
+ console.log("[sinain-share:sender:" + token.slice(0, 8) + "] sending bundle, " + bundle.length + " bytes");
573
624
  conn.send({ type: "bundle", payload: bundle });
574
625
  } catch (e) {
626
+ console.warn("[sinain-share:sender] buildBundle/send failed:", e);
575
627
  conn.send({ type: "error", message: String(e).slice(0, 200) });
576
628
  conn.close();
577
629
  }
@@ -609,6 +661,7 @@ const ShareManager = (() => {
609
661
  try {
610
662
  await ensurePeerJsLoaded();
611
663
  const peer = newPeer(share.share_token);
664
+ instrumentPeer(peer, "sender-resume:" + share.share_token.slice(0, 8));
612
665
  await new Promise((res, rej) => {
613
666
  peer.on("open", () => res());
614
667
  peer.on("error", e => rej(e));
@@ -652,40 +705,64 @@ const ShareManager = (() => {
652
705
 
653
706
  async function connectAsRecipient(token) {
654
707
  showToast('<span class="spinner"></span> Connecting peer-to-peer…', 30_000);
708
+ console.log("[sinain-share:recipient:" + token.slice(0, 8) + "] connectAsRecipient start");
655
709
  await ensurePeerJsLoaded();
656
710
  const me = newPeer();
711
+ instrumentPeer(me, "recipient:" + token.slice(0, 8));
657
712
  await new Promise((res, rej) => {
658
713
  me.on("open", () => res());
659
714
  me.on("error", e => rej(e));
660
715
  setTimeout(() => rej(new Error("peerjs broker timeout")), 8000);
661
716
  });
717
+ console.log("[sinain-share:recipient:" + token.slice(0, 8) + "] my peer registered, dialing", token);
662
718
  return new Promise((resolve, reject) => {
663
719
  const conn = me.connect(token, { reliable: true });
720
+ instrumentConnection(conn, "recipient");
664
721
  const cleanup = () => { try { conn.close(); } catch {} try { me.destroy(); } catch {} };
665
722
  const openTimeout = setTimeout(() => {
723
+ console.warn("[sinain-share:recipient:" + token.slice(0, 8) + "] 15s timeout — conn.open never fired. Almost certainly NAT traversal failed (no TURN). Look for ICE candidate types above — only host/srflx without 'relay' = TURN missing.");
666
724
  cleanup();
667
725
  reject(new Error("source offline or unreachable"));
668
726
  }, 15_000);
669
- conn.on("open", () => clearTimeout(openTimeout));
670
- conn.on("error", (e) => { cleanup(); reject(e); });
727
+ conn.on("open", () => {
728
+ clearTimeout(openTimeout);
729
+ console.log("[sinain-share:recipient:" + token.slice(0, 8) + "] DataChannel open — waiting for bundle");
730
+ });
731
+ conn.on("error", (e) => {
732
+ console.warn("[sinain-share:recipient:" + token.slice(0, 8) + "] conn.error", e);
733
+ cleanup(); reject(e);
734
+ });
671
735
  conn.on("data", async (msg) => {
672
736
  if (!msg) return;
673
737
  if (msg.type === "error") {
738
+ console.warn("[sinain-share:recipient] source reported error:", msg.message);
674
739
  cleanup();
675
740
  reject(new Error("source error: " + msg.message));
676
741
  return;
677
742
  }
678
743
  if (msg.type === "bundle") {
744
+ console.log("[sinain-share:recipient:" + token.slice(0, 8) + "] bundle received, " + (msg.payload && msg.payload.length) + " bytes — POSTing to /knowledge/concepts/import");
679
745
  try {
680
746
  const importR = await api("/knowledge/concepts/import?conflict=merge", {
681
747
  method: "POST",
682
748
  headers: {"Content-Type": "application/json"},
683
749
  body: msg.payload,
684
750
  });
751
+ console.log("[sinain-share:recipient] import response:", importR);
752
+ // CRITICAL: api() doesn't throw on {ok:false} responses — it
753
+ // returns the parsed body. We MUST check ok explicitly here, or
754
+ // a backend failure (script ENOENT, schema error, etc.) appears
755
+ // to the SPA as success and falls through to MissingConcept.
756
+ if (importR && importR.ok === false) {
757
+ cleanup();
758
+ reject(new Error(importR.error || "import failed"));
759
+ return;
760
+ }
685
761
  conn.send({ type: "ack" });
686
762
  setTimeout(cleanup, 500);
687
763
  resolve(importR);
688
764
  } catch (e) {
765
+ console.warn("[sinain-share:recipient] import threw:", e);
689
766
  cleanup();
690
767
  reject(e);
691
768
  }
@@ -2149,8 +2226,14 @@ export function createAppServer(deps: ServerDeps) {
2149
2226
 
2150
2227
  // New "living Confluence" SPA — search-driven, LLM-rendered pages,
2151
2228
  // bookmarks, retraction, concept transfer.
2229
+ // Cache-Control: no-cache forces browsers to revalidate the SPA HTML
2230
+ // on every navigation. Otherwise, after a sinain-core upgrade the
2231
+ // browser serves stale SPA from cache (bugfixes don't take effect
2232
+ // until the user hard-reloads). With revalidation on, ETag mismatches
2233
+ // are detected immediately and the new SPA is loaded.
2152
2234
  if (req.method === "GET" && url.pathname === "/knowledge/ui") {
2153
2235
  res.setHeader("Content-Type", "text/html");
2236
+ res.setHeader("Cache-Control", "no-cache, must-revalidate");
2154
2237
  res.end(renderKnowledgeUiV2());
2155
2238
  return;
2156
2239
  }
@@ -2159,6 +2242,7 @@ export function createAppServer(deps: ServerDeps) {
2159
2242
  // we just serve the same HTML; client-side router parses location.pathname.
2160
2243
  if (req.method === "GET" && url.pathname.startsWith("/knowledge/ui/")) {
2161
2244
  res.setHeader("Content-Type", "text/html");
2245
+ res.setHeader("Cache-Control", "no-cache, must-revalidate");
2162
2246
  res.end(renderKnowledgeUiV2());
2163
2247
  return;
2164
2248
  }