@geravant/sinain 1.22.4 → 1.22.6

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.4",
3
+ "version": "1.22.6",
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": {
@@ -264,7 +264,13 @@ async function exportConceptBundle(
264
264
  { timeout: 30_000, encoding: "utf-8", maxBuffer: 50 * 1024 * 1024 });
265
265
  const parsed = JSON.parse(stdout);
266
266
  // If the export found at least one entity (the root), return it.
267
- if (parsed.stats && parsed.stats.entities > 0) return parsed;
267
+ // BFS always includes the root in `visited`, so entities >= 1 always
268
+ // not a useful guard. Check for actual triples instead, otherwise we
269
+ // accept a bundle that has just the root with no data and never try
270
+ // the next DB. This was the bug that produced empty parloa bundles
271
+ // when entity:parloa lived in the workspace DB but local was checked
272
+ // first and returned an empty (root-only) bundle.
273
+ if (parsed.stats && parsed.stats.triples > 0) return parsed;
268
274
  } catch (e) {
269
275
  // try next DB
270
276
  }
@@ -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,55 @@ 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);
685
752
  conn.send({ type: "ack" });
686
753
  setTimeout(cleanup, 500);
687
754
  resolve(importR);
688
755
  } catch (e) {
756
+ console.warn("[sinain-share:recipient] import threw:", e);
689
757
  cleanup();
690
758
  reject(e);
691
759
  }
@@ -2149,8 +2217,14 @@ export function createAppServer(deps: ServerDeps) {
2149
2217
 
2150
2218
  // New "living Confluence" SPA — search-driven, LLM-rendered pages,
2151
2219
  // bookmarks, retraction, concept transfer.
2220
+ // Cache-Control: no-cache forces browsers to revalidate the SPA HTML
2221
+ // on every navigation. Otherwise, after a sinain-core upgrade the
2222
+ // browser serves stale SPA from cache (bugfixes don't take effect
2223
+ // until the user hard-reloads). With revalidation on, ETag mismatches
2224
+ // are detected immediately and the new SPA is loaded.
2152
2225
  if (req.method === "GET" && url.pathname === "/knowledge/ui") {
2153
2226
  res.setHeader("Content-Type", "text/html");
2227
+ res.setHeader("Cache-Control", "no-cache, must-revalidate");
2154
2228
  res.end(renderKnowledgeUiV2());
2155
2229
  return;
2156
2230
  }
@@ -2159,6 +2233,7 @@ export function createAppServer(deps: ServerDeps) {
2159
2233
  // we just serve the same HTML; client-side router parses location.pathname.
2160
2234
  if (req.method === "GET" && url.pathname.startsWith("/knowledge/ui/")) {
2161
2235
  res.setHeader("Content-Type", "text/html");
2236
+ res.setHeader("Cache-Control", "no-cache, must-revalidate");
2162
2237
  res.end(renderKnowledgeUiV2());
2163
2238
  return;
2164
2239
  }