@andespindola/brainlink 0.1.0-beta.103 → 0.1.0-beta.105

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/README.md CHANGED
@@ -600,10 +600,11 @@ The graph UI shows:
600
600
  - double-click on canvas zooms in at cursor position
601
601
  - floating graph totals (notes, links, tags) below the Brainlink title
602
602
  - graph rendering safeguards (batched canvas drawing across graph sizes, edge draw caps, lower redraw rate, zoom-aware interaction)
603
+ - adaptive CPU safeguards for large graphs: idle frame pacing, throttled background physics updates and cached viewport dimensions to reduce redraw/layout overhead while preserving interaction responsiveness
603
604
  - WebGL node and edge acceleration when supported, falling back to Canvas 2D without changing graph behavior
604
605
  - compact macro-to-micro density progression so reset keeps the graph mass oriented and zoom-in separates local neighborhoods progressively
605
606
  - graph camera treats hub-centered navigation as structural only when the hub is dominant; diffuse stress graphs reset and zoom around the full graph mass
606
- - graph LOD progression: graphs up to 1000 notes render directly; larger graphs use one recursive model where each visible level targets up to 999 non-hub nodes, starts from a memory-hub-centered mesh, and each supernode can expand into another same-shape subgraph level (again up to 999 children) with latent fade-in, aggregated real links and local sibling mesh links so org-heavy and stress-50k follow the same structure at different depths; for massive graphs the first expansion starts much deeper in zoom, low-size child levels use slower easing, and expansion is additionally gated by focus readiness (screen-space isolation of the focused parent) so child levels open only when that subgraph is truly centered and separated in view
607
+ - graph LOD progression: hierarchical rendering now follows one recursive graph-of-graphs standard whenever a graph has more than one hierarchy level; each level expands through intermediate subgraph sizes (instead of jumping directly to leaves), starts from a memory-hub-centered mesh, and each supernode can expand into another same-shape subgraph level (up to 999 children) with latent fade-in, aggregated real links and local sibling mesh links so org-heavy-like and stress-50k-like structures share the same layered behavior at different depths; for massive graphs the first expansion starts deeper in zoom and is additionally gated by focus readiness (screen-space isolation of the focused parent) so child levels open only when that subgraph is truly centered and separated in view
607
608
 
608
609
  The server indexes before starting by default. Use `--no-index` to skip that step:
609
610
 
@@ -42,6 +42,10 @@ const dragSettleRounds = 3
42
42
  const wheelZoomExponent = 0.0009
43
43
  const wheelZoomExponentCap = 0.035
44
44
  const wheelZoomModifierBoost = 1.08
45
+ const physicsDragFrameIntervalMs = 16
46
+ const physicsIdleFrameIntervalMs = 78
47
+ const physicsLargeGraphIdleFrameIntervalMs = 108
48
+ const physicsStepDeltaCapMs = 96
45
49
  const state = {
46
50
  graph: { nodes: [], edges: [] },
47
51
  nodes: [],
@@ -67,7 +71,10 @@ const state = {
67
71
  graphSignature: '',
68
72
  graphStatus: '',
69
73
  graphTotals: { nodes: 0, edges: 0 },
74
+ viewport: { width: 320, height: 320 },
70
75
  last: performance.now(),
76
+ lastPhysicsAt: performance.now(),
77
+ physicsRestFrames: 0,
71
78
  offscreenFrameCount: 0,
72
79
  recoveringViewport: false,
73
80
  renderVisibilityDirty: true,
@@ -434,6 +441,7 @@ const resize = () => {
434
441
  glCanvas.width = Math.floor(width * ratio)
435
442
  glCanvas.height = Math.floor(height * ratio)
436
443
  }
444
+ state.viewport = { width, height }
437
445
  ctx.setTransform(ratio, 0, 0, ratio, 0, 0)
438
446
  markRenderDirty()
439
447
  }
@@ -588,7 +596,7 @@ const recomputeVisibility = () => {
588
596
  y: macroHub ? macroHub.y : (bounds.minY + bounds.maxY) / 2
589
597
  }
590
598
  : { x: 0, y: 0 }
591
- const ecosystemGraph = nodes.length > ecosystemActivationNodeThreshold
599
+ const ecosystemGraph = nodes.length > 1
592
600
  ? buildEcosystemGraph(nodes, state.macroCenter, primaryHub)
593
601
  : {
594
602
  clusters: [],
@@ -760,12 +768,29 @@ const ecosystemLayoutSpacingForSize = size => {
760
768
  return 7
761
769
  }
762
770
 
771
+ const buildIntermediateEcosystemSizes = (fromSize, toSize) => {
772
+ if (fromSize <= toSize + 1) {
773
+ return []
774
+ }
775
+ const intermediate = []
776
+ let current = fromSize
777
+ while (current > toSize + 1) {
778
+ const stepped = Math.max(toSize + 1, Math.ceil(current / 3))
779
+ if (stepped >= current) {
780
+ break
781
+ }
782
+ intermediate.push(stepped)
783
+ current = stepped
784
+ }
785
+ return intermediate
786
+ }
787
+
763
788
  const buildEcosystemLevelSizes = nodeCount => {
764
789
  if (nodeCount <= 0) return []
765
- const sizes = []
790
+ const primarySizes = []
766
791
  let currentSize = Math.max(1, Math.ceil(nodeCount / ecosystemLevelNodeCap))
767
792
  while (currentSize >= 1) {
768
- sizes.push(currentSize)
793
+ primarySizes.push(currentSize)
769
794
  if (currentSize === 1) {
770
795
  break
771
796
  }
@@ -775,7 +800,28 @@ const buildEcosystemLevelSizes = nodeCount => {
775
800
  }
776
801
  currentSize = nextSize
777
802
  }
778
- return sizes
803
+ const expandedSizes = []
804
+ for (let index = 0; index < primarySizes.length; index += 1) {
805
+ const size = primarySizes[index]
806
+ if (expandedSizes.length === 0 || expandedSizes[expandedSizes.length - 1] !== size) {
807
+ expandedSizes.push(size)
808
+ }
809
+ const nextSize = primarySizes[index + 1]
810
+ if (!Number.isFinite(nextSize)) {
811
+ continue
812
+ }
813
+ const intermediate = buildIntermediateEcosystemSizes(size, nextSize)
814
+ for (let intermediateIndex = 0; intermediateIndex < intermediate.length; intermediateIndex += 1) {
815
+ const candidate = intermediate[intermediateIndex]
816
+ if (expandedSizes[expandedSizes.length - 1] !== candidate) {
817
+ expandedSizes.push(candidate)
818
+ }
819
+ }
820
+ }
821
+ if (expandedSizes[expandedSizes.length - 1] !== 1) {
822
+ expandedSizes.push(1)
823
+ }
824
+ return expandedSizes
779
825
  }
780
826
 
781
827
  const buildEcosystemExpansionLevels = (levelSizes, nodeCount) => {
@@ -786,12 +832,12 @@ const buildEcosystemExpansionLevels = (levelSizes, nodeCount) => {
786
832
  const maxScale = isMassive
787
833
  ? massiveEcosystemClusterScaleThreshold
788
834
  : ecosystemClusterScaleThreshold
789
- const startScale = isMassive ? 0.82 : 0.18
835
+ const startScale = isMassive ? 1.12 : 0.24
790
836
  const transitionCount = levelSizes.length - 1
791
837
  const usableScale = Math.max(0.08, maxScale - startScale)
792
838
  const step = usableScale / transitionCount
793
- const stride = isMassive ? 0.9 : 0.78
794
- const overlap = isMassive ? 1.28 : 1.75
839
+ const stride = isMassive ? 0.93 : 0.82
840
+ const overlap = isMassive ? 1.22 : 1.62
795
841
  const levels = []
796
842
  for (let index = 0; index < transitionCount; index += 1) {
797
843
  const start = startScale + step * index * stride
@@ -2565,7 +2611,7 @@ const scheduleContentFilterSync = () => {
2565
2611
  }
2566
2612
  }
2567
2613
 
2568
- const tick = delta => {
2614
+ const tick = (delta, now) => {
2569
2615
  const nodes = state.renderNodes.length > 0 ? state.renderNodes : state.visibleNodes
2570
2616
  const edges = state.renderEdges.length > 0 ? state.renderEdges : state.visibleEdges
2571
2617
  const shouldRunPhysics =
@@ -2573,9 +2619,24 @@ const tick = delta => {
2573
2619
  nodes.length <= 320 &&
2574
2620
  state.transform.scale >= 0.08
2575
2621
  if (!shouldRunPhysics) {
2622
+ state.physicsRestFrames = 0
2623
+ return
2624
+ }
2625
+ const isDragging = Boolean(state.pointer.dragNode)
2626
+ const intervalMs = isDragging
2627
+ ? physicsDragFrameIntervalMs
2628
+ : state.nodes.length > largeGraphNodeThreshold
2629
+ ? physicsLargeGraphIdleFrameIntervalMs
2630
+ : physicsIdleFrameIntervalMs
2631
+ if (!isDragging && state.physicsRestFrames >= 18) {
2632
+ return
2633
+ }
2634
+ const elapsedSincePhysics = now - state.lastPhysicsAt
2635
+ if (elapsedSincePhysics < intervalMs) {
2576
2636
  return
2577
2637
  }
2578
- const strength = Math.min(delta / 16, 2)
2638
+ state.lastPhysicsAt = now
2639
+ const strength = Math.min(Math.max(elapsedSincePhysics, delta, 16) / 16, physicsStepDeltaCapMs / 16)
2579
2640
 
2580
2641
  edges.forEach(edge => {
2581
2642
  const source = edge.sourceNode
@@ -2617,6 +2678,7 @@ const tick = delta => {
2617
2678
  }
2618
2679
  }
2619
2680
 
2681
+ let kinetic = 0
2620
2682
  nodes.forEach(node => {
2621
2683
  node.vx = Number.isFinite(node.vx) ? node.vx : 0
2622
2684
  node.vy = Number.isFinite(node.vy) ? node.vy : 0
@@ -2633,7 +2695,16 @@ const tick = delta => {
2633
2695
  node.vy *= 0.88
2634
2696
  node.x += node.vx * strength
2635
2697
  node.y += node.vy * strength
2698
+ kinetic += Math.abs(node.vx) + Math.abs(node.vy)
2636
2699
  })
2700
+ if (isDragging) {
2701
+ state.physicsRestFrames = 0
2702
+ return
2703
+ }
2704
+ const kineticFloor = Math.max(0.16, nodes.length * 0.01)
2705
+ state.physicsRestFrames = kinetic <= kineticFloor
2706
+ ? state.physicsRestFrames + 1
2707
+ : 0
2637
2708
  }
2638
2709
 
2639
2710
  const worldPoint = event => {
@@ -2789,9 +2860,8 @@ const clusterOpacity = cluster =>
2789
2860
  Math.max(0, Math.min(1, Number.isFinite(cluster.lodOpacity) ? cluster.lodOpacity : 1))
2790
2861
 
2791
2862
  const worldViewportBounds = () => {
2792
- const rect = canvas.getBoundingClientRect()
2793
- const width = Math.max(rect.width, 320)
2794
- const height = Math.max(rect.height, 320)
2863
+ const width = Math.max(state.viewport.width, 320)
2864
+ const height = Math.max(state.viewport.height, 320)
2795
2865
  const paddingMultiplier =
2796
2866
  state.nodes.length > massiveGraphNodeThreshold
2797
2867
  ? (state.transform.scale >= 0.6 ? 2.8 : state.transform.scale >= 0.25 ? 2.35 : 1.9)
@@ -2953,7 +3023,7 @@ const computeRenderVisibility = () => {
2953
3023
  ? massiveEcosystemClusterScaleThreshold
2954
3024
  : ecosystemClusterScaleThreshold
2955
3025
  if (
2956
- state.visibleNodes.length > ecosystemActivationNodeThreshold &&
3026
+ state.ecosystemExpansionLevels.length > 0 &&
2957
3027
  state.transform.scale <= ecosystemScaleThreshold &&
2958
3028
  state.ecosystemClusters.length > 0
2959
3029
  ) {
@@ -3136,10 +3206,10 @@ const render = now => {
3136
3206
  state.last = now
3137
3207
  const backgroundFrameIntervalMs =
3138
3208
  state.nodes.length > massiveGraphNodeThreshold
3139
- ? (state.transform.scale < 0.035 ? 130 : state.transform.scale < 0.08 ? 110 : 86)
3209
+ ? (state.transform.scale < 0.035 ? 156 : state.transform.scale < 0.08 ? 128 : 98)
3140
3210
  : state.nodes.length > largeGraphNodeThreshold
3141
- ? 64
3142
- : 16
3211
+ ? 72
3212
+ : 18
3143
3213
  const isInteracting =
3144
3214
  state.pointer.down ||
3145
3215
  state.renderVisibilityDirty ||
@@ -3152,6 +3222,7 @@ const render = now => {
3152
3222
  const rect = canvas.getBoundingClientRect()
3153
3223
  const width = Math.max(rect.width, 320)
3154
3224
  const height = Math.max(rect.height, 320)
3225
+ state.viewport = { width, height }
3155
3226
  sanitizeGraphState()
3156
3227
  if (!hasValidTransform()) {
3157
3228
  resetView()
@@ -3168,7 +3239,7 @@ const render = now => {
3168
3239
  }
3169
3240
 
3170
3241
  computeRenderVisibility()
3171
- tick(delta)
3242
+ tick(delta, now)
3172
3243
  const hasVisibleNodeOnScreen = state.renderNodes.some((node) => isNodeVisibleOnScreen(node, width, height))
3173
3244
  const manualZoomGuardActive = now - state.lastManualZoomAt < zoomRecoveryGuardMs
3174
3245
  const allowViewportAutoRecovery = state.nodes.length <= massiveGraphNodeThreshold
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@andespindola/brainlink",
3
- "version": "0.1.0-beta.103",
3
+ "version": "0.1.0-beta.105",
4
4
  "description": "Local-first knowledge memory for agents with Markdown, backlinks, indexing and context retrieval.",
5
5
  "type": "module",
6
6
  "license": "MIT",