@gamaze/hicortex 0.21.0 → 0.22.0

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.
@@ -15,17 +15,22 @@
15
15
  PORTED FROM mockups/409-r6-flight.html (the design of record — its banner
16
16
  documents every behavior; the engine below is that page with the seeded
17
17
  data layer replaced by fetches). Live-data substitutions:
18
- /dashboard/data?range=90d — band numbers, snapshot series, digest
18
+ /dashboard/data?range=Rd — band numbers, snapshot series, digest
19
19
  /dashboard/field — the whole field (memories + real links)
20
- /dashboard/events?days=90 — the replay ledger (real nights)
20
+ /dashboard/events?days=R — the replay ledger (real nights)
21
21
  /health + /health/detail — live dot (30 s poll) + model label
22
22
  /account + /account/token — the account menu (harvested from 0.20.4)
23
23
  /search /memory /update /delete /identity — the browser + agent rail
24
+ (R = the range selector's value: 30 by default, 7/90/180 opt-in — #452)
24
25
 
25
26
  PHASE 2 (#422 — operator surfaces) LANDED:
26
27
  - Capture health: real /distill accounting rows (posts · sessions · MB ·
27
28
  held per machine × harness) via /dashboard/data capture_health; the
28
29
  phase-1 counts-only rows remain as the degrade.
30
+ Fix round 7 (#409, owner ruling 2026-09-14 — "the normal 30 day as the
31
+ other cards"): the card's face is the ROLLING window (window_rows,
32
+ window_days), the newest night rides as a per-row secondary suffix;
33
+ retention == window (CAPTURE_HEALTH_WINDOW_DAYS).
29
34
  - Activity bars: five segments (added + merged + the three graduation
30
35
  deltas from snapshot stage_counts; two-segment rendering when buckets
31
36
  are absent).
@@ -132,6 +137,30 @@
132
137
  stays marked; the visible word failed to communicate). Vocabulary
133
138
  amendment: Low / HEALTHY / Overfetching. Marker + trend stay.
134
139
 
140
+ REPLAY + RANGE (#452, owner directives 2026-09-16):
141
+ - 30-DAY DEFAULT FOR ALL VIEWS: the replay ledger, the band and the
142
+ snapshot series all default to 30 days (server EVENTS_DEFAULT_DAYS
143
+ 90 → 30; the client's every fetch is selector-driven). Longer
144
+ ranges are OPT-IN via a compact 7 / 30 / 90 / 180 segmented control
145
+ in the replay foot — explicit click only, never auto-loaded
146
+ (SUPERSEDES the 2026-09-12 no-dropdowns ruling). No persistence:
147
+ every load starts at 30 (loads fast).
148
+ - PLAY ONCE, THEN REST: the replay auto-starts as before (~3.6 s after
149
+ boot), walks the ledger ONCE to today, and STOPS there — no loop
150
+ (SUPERSEDES the 2026-09-13 auto-loop/no-buttons ruling). Controls in
151
+ the replay foot, styled like "return to now": pause/resume (holds
152
+ position mid-cruise; hides once the playback completed — nothing is
153
+ playing) and "replay again" (appears once completed; restarts from
154
+ the range start). Drag/scrub behavior is UNCHANGED: a manual take
155
+ pauses the cruise and re-engages ~12 s after idle, continuing
156
+ toward today — where it now stops instead of looping.
157
+ - DATE LABEL ON THE SCRUBBER: the cursor dot carries a small mono
158
+ label above it (the night being viewed, "now" at the end).
159
+ - TRUTH-MANAGEMENT CAPTIONS: Superseded / Retracted / Rewritten ride
160
+ the events ledger alongside learned/enriched/merged (server-side
161
+ sources in dashboard.ts; superseded/retracted bloom in the fading
162
+ color — demoted — rewritten in the truth color).
163
+
135
164
  REMAINING DEGRADATIONS (planned, per the #409 phase table — not gaps):
136
165
  - Recall card: no raw shown/used counts (the per-memory adoption sums
137
166
  are not in any payload yet).
@@ -561,11 +590,25 @@
561
590
  pointer-events:none; transition:opacity 1.4s; z-index:5;
562
591
  }
563
592
  .hint em{ font-style:normal; color:var(--dim); }
564
- /* replay — inside the brain panel; drag-only (no play button anywhere) */
593
+ /* replay — inside the brain panel; drag to scrub, or let the cruise walk
594
+ the ledger ONCE to today (#452: range pills + pause / replay again) */
565
595
  .replay-foot{ border-top:1px solid var(--line); padding:7px 12px 8px; flex:none; background:color-mix(in srgb, var(--panel) 88%, transparent); }
566
596
  #replay{ display:flex; align-items:center; gap:12px; }
567
597
  #replay.off{ opacity:.45; pointer-events:none; }
598
+ /* the range selector stays LIVE even when the instrument is off — a wider
599
+ window can reveal ≥2 nights and revive the replay (#452) */
600
+ #replay.off #rp-range{ opacity:1; pointer-events:auto; }
568
601
  #rp-label{ font-size:10px; color:var(--faint); text-transform:uppercase; letter-spacing:.14em; white-space:nowrap; flex:none; }
602
+ /* #452 range selector — the .idpill.seg idiom (mono pills, active = green) */
603
+ #rp-range{ display:inline-flex; gap:3px; flex:none; }
604
+ .rp-seg{
605
+ font-size:9px; font-family:var(--mono); letter-spacing:.05em; color:var(--faint);
606
+ border:1px solid var(--line2); border-radius:999px; padding:3px 8px; white-space:nowrap;
607
+ transition:color .15s, border-color .15s;
608
+ }
609
+ .rp-seg:hover{ color:var(--dim); }
610
+ .rp-seg[aria-pressed="true"]{ color:var(--green-mid); border-color:color-mix(in srgb, var(--green) 40%, transparent); }
611
+ html:not([data-theme="light"]) .rp-seg[aria-pressed="true"]{ box-shadow:0 0 10px rgba(61,255,122,.12); }
569
612
  #rp-track{ flex:1; position:relative; height:26px; cursor:pointer; touch-action:none; min-width:60px; }
570
613
  #rp-rail{ position:absolute; left:0; right:0; top:11px; height:3px; border-radius:2px; background:var(--tbar-bg); }
571
614
  #rp-fill{ position:absolute; left:0; top:11px; height:3px; border-radius:2px; width:100%;
@@ -577,14 +620,23 @@
577
620
  }
578
621
  html:not([data-theme="light"]) #rp-cursor{ background:#0d1f14; box-shadow:0 0 12px rgba(61,255,122,.7); }
579
622
  html[data-theme="light"] #rp-cursor{ box-shadow:0 1px 4px rgba(56,54,42,.3); }
623
+ /* #452: the night being viewed, floating above the scrub dot (the
624
+ #rp-dates span styling; overflow is visible — it may overlap the canvas
625
+ bottom edge like .hint does) */
626
+ #rp-date{
627
+ position:absolute; top:-13px; transform:translateX(-50%); white-space:nowrap;
628
+ font-size:9px; color:var(--faint); font-family:var(--mono); pointer-events:none; z-index:3;
629
+ display:none;
630
+ }
631
+ #rp-date.show{ display:block; }
580
632
  .rp-dot{ position:absolute; top:9px; width:5px; height:5px; border-radius:50%; margin-left:-2.5px; background:var(--line2); }
581
633
  .rp-dot.g{ background:var(--green-mid); }
582
634
  .rp-dot.w{ background:var(--dim); }
583
635
  #rp-dates{ position:absolute; left:0; right:0; top:21px; height:12px; }
584
636
  #rp-dates span{ position:absolute; transform:translateX(-50%); font-size:9px; color:var(--faint); font-family:var(--mono); }
585
- #rp-now{ display:none; font-size:10.5px; color:var(--green-mid); border:1px solid color-mix(in srgb, var(--green) 40%, transparent); border-radius:999px; padding:5px 11px; white-space:nowrap; }
586
- #rp-now.show{ display:inline-block; }
587
- html:not([data-theme="light"]) #rp-now{ box-shadow:0 0 12px rgba(61,255,122,.15); }
637
+ #rp-now,#rp-pause,#rp-again{ display:none; font-size:10.5px; color:var(--green-mid); border:1px solid color-mix(in srgb, var(--green) 40%, transparent); border-radius:999px; padding:5px 11px; white-space:nowrap; }
638
+ #rp-now.show,#rp-pause.show,#rp-again.show{ display:inline-block; }
639
+ html:not([data-theme="light"]) #rp-now,html:not([data-theme="light"]) #rp-pause,html:not([data-theme="light"]) #rp-again{ box-shadow:0 0 12px rgba(61,255,122,.15); }
588
640
 
589
641
  /* ---- agents rail — the ONLY thing beside the brain, rightmost ---- */
590
642
  #agentscol{
@@ -768,7 +820,7 @@
768
820
  /* capture health — phase 2: per machine × harness rows from /distill
769
821
  accounting (posts/sessions/MB/held); the phase-1 counts-only rows remain
770
822
  as the degrade for old servers / nothing recorded yet */
771
- .cap-row2{ display:flex; align-items:center; gap:9px; padding:2.5px 0; border-bottom:1px solid color-mix(in srgb, var(--line) 60%, transparent); }
823
+ .cap-row2{ display:flex; flex-wrap:wrap; align-items:center; gap:1px 9px; padding:2.5px 0; border-bottom:1px solid color-mix(in srgb, var(--line) 60%, transparent); }
772
824
  .cap-row2:last-of-type{ border-bottom:none; }
773
825
  .cap-dot{ width:6px; height:6px; border-radius:50%; flex:none; background:var(--green-mid); }
774
826
  .cap-row2 .hn{ font:11px var(--mono); color:var(--text); flex:0 0 148px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
@@ -779,6 +831,12 @@
779
831
  .cap-bar .fill{ height:100%; border-radius:2.5px; background:var(--green-mid); }
780
832
  .cap-row2 .st{ font-size:10px; color:var(--faint); font-family:var(--mono); white-space:nowrap; }
781
833
  .cap-row2 .st b{ color:var(--dim); font-weight:500; }
834
+ /* fix round 7: the newest-night sub-line on the rolling-window rows — a
835
+ full-basis child wraps under the row's primary line (the suffix does NOT
836
+ fit beside "N sess · X MB" without pushing past the card edge); same
837
+ faint mono tone, theme token only, both themes free. Rows without a
838
+ newest night stay single-line. */
839
+ .cap-row2 .cap-ton{ flex-basis:100%; text-align:right; font:10px var(--mono); color:var(--faint); line-height:1.1; }
782
840
  .cap-held{ color:var(--amber); margin-left:5px; }
783
841
  /* #423 phase 3: the capture-pause badge — the same amber family as the
784
842
  held badge (an operator state, not an error) */
@@ -963,6 +1021,7 @@
963
1021
  .brow{ grid-template-columns:1fr; }
964
1022
  .mrow .sn{ font-size:10px; }
965
1023
  #rp-label{ display:none; }
1024
+ #replay{ flex-wrap:wrap; } /* #452: range pills + controls wrap rather than crush the track */
966
1025
  #rp-dates span:nth-child(n+3){ display:none; }
967
1026
  .acct-trigger .who{ display:none; }
968
1027
  .field-head{ padding:8px 12px 7px; }
@@ -1180,13 +1239,17 @@
1180
1239
  </div>
1181
1240
  <div class="replay-foot">
1182
1241
  <div id="replay">
1242
+ <div id="rp-range" role="group" aria-label="range"></div>
1183
1243
  <span id="rp-label">nights</span>
1184
1244
  <div id="rp-track">
1185
1245
  <div id="rp-rail"></div>
1186
1246
  <div id="rp-fill"></div>
1187
1247
  <div id="rp-dates"></div>
1188
1248
  <div id="rp-cursor"></div>
1249
+ <span id="rp-date"></span>
1189
1250
  </div>
1251
+ <button id="rp-pause">pause</button>
1252
+ <button id="rp-again">replay again</button>
1190
1253
  <button id="rp-now">return to now</button>
1191
1254
  </div>
1192
1255
  </div>
@@ -1307,7 +1370,7 @@ if(STILL){ document.body.classList.remove('booting'); document.body.classList.ad
1307
1370
  /* =====================================================================
1308
1371
  The approved #409 r6-flight engine, ported to live data. One canvas,
1309
1372
  three verbs (look / travel / time) plus the quiet fourth: WATCH — ambient
1310
- signals loop slowly along REAL stored links, never behind a play button.
1373
+ signals loop slowly along REAL stored links, always on.
1311
1374
  Stages are DERIVED presentation (server-side via /dashboard/field) over
1312
1375
  the one numeric strength signal — never stored state.
1313
1376
  ===================================================================== */
@@ -1424,9 +1487,12 @@ function bubbleRadius(s){ return 6 + Math.sqrt(clamp(s??0,0,1))*3; }
1424
1487
  function depthSizeDivisor(dz){ return Math.pow(Math.max(.14,dz),0.35); }
1425
1488
 
1426
1489
  /* ---------- live-data state ---------- */
1427
- let D=null; // /dashboard/data?range=90d
1490
+ let RANGE=30; // days — the range selector's value (#452; 30-day default for
1491
+ // ALL views, owner directive 2026-09-16). NO persistence: every
1492
+ // load starts at 30 — it loads fast; longer is opt-in per click.
1493
+ let D=null; // /dashboard/data?range=RANGE d — band, series, digest
1428
1494
  let F=null; // /dashboard/field
1429
- let EV=null; // /dashboard/events?days=90
1495
+ let EV=null; // /dashboard/events?days=RANGE — the replay ledger
1430
1496
  let M=null; // /dashboard/model (#422) — null on failure: label-only degrade, no configure pill
1431
1497
  let TOTAL=0, removedNow=0;
1432
1498
  /* the store's static LIVE count (headline.live_memories ?? total): the
@@ -1637,13 +1703,19 @@ function buildWorld(){
1637
1703
 
1638
1704
  // the flattened replay ledger (events within a night fire in cruise order);
1639
1705
  // added/merged reuse the staggered born/merge times so blooms and births
1640
- // coincide; enriched/linked carry their own salted staggers.
1706
+ // coincide; enriched/linked carry their own salted staggers. #452 adds the
1707
+ // truth-management captions — stagger order enriched .55 < retracted .65 <
1708
+ // superseded .70 < rewritten .75 < learned .80 keeps the cruise sane (each
1709
+ // new type gets its own salt, so per-id positions never collide).
1641
1710
  EVENTS = [];
1642
1711
  NIGHTS.forEach((n,i)=>{
1643
1712
  for(const id of n.added) EVENTS.push({t:bornOf.get(id), type:'learned', id, cap:'Learned — “'+trunc(titleOf(id),52)+'”'});
1644
1713
  for(const id of n.enriched) EVENTS.push({t:i+.55+stag(id,19)*.08, type:'enriched', id, cap:'Enriched — “'+trunc(titleOf(id),52)+'”'});
1645
1714
  for(const mg of n.merged) EVENTS.push({t:mergeOf.get(mg.loser).t, type:'merged', loser:mg.loser, id:mg.loser, cap:'Merged — “'+trunc(titleOf(mg.loser),40)+'”'});
1646
1715
  for(const lk of n.linked) EVENTS.push({t:i+.90+stag(lk.a+lk.b,23)*.08, type:'linked', a:lk.a, b:lk.b, cap:null});
1716
+ for(const sp of n.superseded) EVENTS.push({t:i+.70+stag(sp.old,29)*.08, type:'superseded', id:sp.old, cap:'Superseded — “'+trunc(titleOf(sp.old),52)+'”'});
1717
+ for(const rt of n.retracted) EVENTS.push({t:i+.65+stag(rt.old,31)*.08, type:'retracted', id:rt.old, cap:'Retracted — “'+trunc(titleOf(rt.old),52)+'”'});
1718
+ for(const id of n.rewritten) EVENTS.push({t:i+.75+stag(id,37)*.08, type:'rewritten', id, cap:'Rewritten — “'+trunc(titleOf(id),52)+'”'});
1647
1719
  });
1648
1720
  EVENTS.sort((x,y)=>x.t-y.t);
1649
1721
 
@@ -2487,14 +2559,21 @@ function toast(msg, ms){
2487
2559
  function wake(){ lastActivity=performance.now(); }
2488
2560
 
2489
2561
  /* ---------- replay — time travel: drag to take the helm, else the G1
2490
- auto-cruise walks the REAL event ledger (one memory action at a time,
2491
- looping; still NO play button — cruising is the ambient default).
2562
+ auto-cruise walks the REAL event ledger (one memory action at a time)
2563
+ ONCE to today, then rests there (#452, owner ruling 2026-09-16 —
2564
+ supersedes the 2026-09-13 auto-loop). Pause/resume + "replay again" sit
2565
+ in the foot beside "return to now"; a manual take still pauses the
2566
+ cruise and re-engages ~12 s after idle, continuing toward today — where
2567
+ it now stops instead of looping.
2492
2568
  With fewer than 2 nights the instrument renders its "now" state and
2493
2569
  disables itself (degrade quietly). ---------- */
2494
2570
  const track=el('rp-track');
2495
2571
  function trackFrac(e){ const r=track.getBoundingClientRect(); return clamp((e.clientX-r.left)/r.width,0,1); }
2496
2572
  let autoPlay=false, autoEvIdx=0, autoNextAt=0, manualUntil=3600, pendingApply=null;
2573
+ let paused=false; /* #452: pause holds the cruise position; resume continues */
2574
+ let played=false; /* #452: a full playback completed → rest at today, never loop */
2497
2575
  function manualTake(){ autoPlay=false; pendingApply=null; manualUntil=performance.now()+12000; wake(); }
2576
+ function setPaused(v){ paused=v; el('rp-pause').textContent=paused?'resume':'pause'; }
2498
2577
  /* HERALD — before each cruise step's change, a bright pulse flies in along
2499
2578
  a REAL link touching the target (else from its nearest same-domain
2500
2579
  neighbor) and LANDS: the update we see is the arrival */
@@ -2518,18 +2597,25 @@ function heraldTo(id){
2518
2597
  signals.push({ path:p, i:0, s:0, dur:.45, tail:[], showcase:true, herald:true }); /* dur is seconds */
2519
2598
  }
2520
2599
  function autoTick(now){
2521
- if(REDUCED||booting||!REPLAY_OK) return;
2600
+ if(REDUCED||booting||!REPLAY_OK||paused) return;
2522
2601
  if(!autoPlay){ if(now>manualUntil && !scrubbing) autoPlay=true; else return; }
2523
2602
  if(pendingApply){
2524
2603
  if(now>=pendingApply.at){ seek(pendingApply.t); pendingApply=null; autoNextAt=now+820; }
2525
2604
  return; /* the herald is in flight */
2526
2605
  }
2527
2606
  if(now<autoNextAt) return;
2528
- if(T>=NOW_T-.001){ /* a beat at "now", then loop from the start */
2607
+ if(T>=NOW_T-.001){
2608
+ /* a beat at "now": re-kick to the range start ONLY before the first
2609
+ full playback; after it, REST here — plays once, never loops (#452) */
2610
+ if(played){ autoPlay=false; autoNextAt=now+3200; return; }
2529
2611
  autoEvIdx=0; autoNextAt=now+3200; seek(.001); return;
2530
2612
  }
2531
2613
  while(autoEvIdx<EVENTS.length && EVENTS[autoEvIdx].t<=T+.0001) autoEvIdx++;
2532
- if(autoEvIdx>=EVENTS.length){ autoNextAt=now+3200; seek(WINDOW); return; }
2614
+ if(autoEvIdx>=EVENTS.length){
2615
+ /* ledger exhausted: arrive at today and complete the playback (the
2616
+ flags go BEFORE the seek so onTChanged's control swap sees them) */
2617
+ autoNextAt=now+3200; played=true; autoPlay=false; seek(WINDOW); return;
2618
+ }
2533
2619
  const ev=EVENTS[autoEvIdx++];
2534
2620
  if(ev.id && signals.length<8){ heraldTo(ev.id); pendingApply={t:ev.t+.001, at:now+430}; }
2535
2621
  else { seek(ev.t+.001); autoNextAt=now+820; }
@@ -2550,7 +2636,10 @@ function onTChanged(fromSeek){
2550
2636
  const b=mems.get(ev.id);
2551
2637
  if(!b||!aliveAt(b,ev.t)||b._sx===undefined) continue;
2552
2638
  if(blooms.length>=6) break;
2553
- const rgb= ev.type==='merged'? PAL.stages.fading.rgb : (ev.type==='learned'? PAL.stages.forming.rgb : PAL.stages.truth.rgb);
2639
+ /* #452: superseded/retracted bloom in the FADING color (demoted
2640
+ claims); rewritten falls to the default truth color (a rewrite is
2641
+ the claim surviving, corrected) */
2642
+ const rgb= ev.type==='merged'||ev.type==='superseded'||ev.type==='retracted'? PAL.stages.fading.rgb : (ev.type==='learned'? PAL.stages.forming.rgb : PAL.stages.truth.rgb);
2554
2643
  blooms.push({x:b._sx,y:b._sy,r0:2,r1:24+b._sr,t0:performance.now(),dur:1400,cap:ev.cap,rgb});
2555
2644
  }
2556
2645
  }
@@ -2559,6 +2648,18 @@ function onTChanged(fromSeek){
2559
2648
  el('rp-cursor').style.left=(T/WINDOW*100)+'%';
2560
2649
  const isNow=T>=NOW_T-.001;
2561
2650
  el('rp-now').classList.toggle('show',!isNow&&REPLAY_OK);
2651
+ /* #452 date label above the scrub dot — same left % as the cursor, the
2652
+ night being viewed ("now" at the end); hidden when there is no ledger */
2653
+ const dlab=el('rp-date');
2654
+ if(REPLAY_OK&&NIGHTS.length){
2655
+ dlab.style.left=(T/WINDOW*100)+'%';
2656
+ dlab.textContent=isNow?'now':fmtDay(NIGHTS[Math.min(Math.floor(T),NIGHTS.length-1)].date);
2657
+ dlab.classList.add('show');
2658
+ }else dlab.classList.remove('show');
2659
+ /* #452 lifecycle pills: pause while a playback is live, "replay again"
2660
+ once it completed — at most ONE of them beside "return to now" */
2661
+ el('rp-pause').classList.toggle('show',REPLAY_OK&&!played);
2662
+ el('rp-again').classList.toggle('show',REPLAY_OK&&played);
2562
2663
  updateMeasures();
2563
2664
  /* #422 owner ruling: the overview counter is STATIC (the store's live
2564
2665
  count, set once at boot + after writes) — scrubbing the replay no longer
@@ -2566,10 +2667,13 @@ function onTChanged(fromSeek){
2566
2667
  dirty=true;
2567
2668
  }
2568
2669
  /* warp-to-now — accelerates through every remaining event, blooms firing,
2569
- and arrives ~2.4 s later (ease in-out) */
2670
+ and arrives ~2.4 s later (ease in-out). #452: arriving at now by hand
2671
+ completes the playback too — the cruise rests instead of looping after */
2570
2672
  let warpUntil=0, warpFrom=0;
2571
2673
  function warpToNow(){
2572
2674
  manualTake();
2675
+ setPaused(false);
2676
+ played=true;
2573
2677
  if(REDUCED||T>=NOW_T-.001||!REPLAY_OK){ seek(WINDOW); return; }
2574
2678
  warpFrom=T; warpUntil=performance.now()+2400;
2575
2679
  }
@@ -2582,6 +2686,72 @@ function warpTick(now){
2582
2686
  }
2583
2687
  el('rp-now').onclick=warpToNow;
2584
2688
 
2689
+ /* #452 — pause/resume + replay again. Pause holds the cruise position
2690
+ (autoTick returns early, pending heralds included); resume continues
2691
+ toward today. Replay again restarts from the range start and plays once.
2692
+ Visibility is onTChanged's job: pause renders only while a playback is
2693
+ live, "replay again" only once one completed — least clutter. */
2694
+ el('rp-pause').onclick=()=>{
2695
+ setPaused(!paused);
2696
+ if(!paused) autoPlay=true; /* resume continues the cruise immediately */
2697
+ wake();
2698
+ };
2699
+ el('rp-again').onclick=()=>{
2700
+ manualTake(); setPaused(false);
2701
+ played=false; autoEvIdx=0; pendingApply=null; autoPlay=true;
2702
+ seek(.001); wake();
2703
+ };
2704
+
2705
+ /* #452 — the range selector: 7/30/90/180 days (the owner-fixed option
2706
+ set), default 30, NO persistence — every load starts at 30 (loads fast).
2707
+ A click refetches BOTH range-scoped sources and rebuilds the world
2708
+ exactly like boot — the agents rail included (its "last night" line
2709
+ reads NIGHTS, so it must follow the new window); the replay then
2710
+ auto-starts on the new range via the idle re-engage (mirrors boot).
2711
+ Fail-soft: a failed fetch toasts, keeps the OLD data and the OLD pills
2712
+ — never a half-switched view. One switch at a time (refreshAfterWrite's
2713
+ refreshInFlight pattern): a click while a switch is in flight is
2714
+ IGNORED — a racing second fetch could resolve out of order and leave
2715
+ pills and data disagreeing. */
2716
+ const RANGE_OPTIONS=[7,30,90,180];
2717
+ function renderRangePills(){
2718
+ const host=el('rp-range'); host.innerHTML='';
2719
+ for(const n of RANGE_OPTIONS){
2720
+ const b=document.createElement('button');
2721
+ b.type='button'; b.className='rp-seg'; b.textContent=n+'d';
2722
+ b.title='last '+n+' days';
2723
+ b.setAttribute('aria-pressed', String(n===RANGE));
2724
+ b.onclick=()=>{ if(n!==RANGE) switchRange(n); };
2725
+ host.appendChild(b);
2726
+ }
2727
+ }
2728
+ let rangeSwitchInFlight=false;
2729
+ async function switchRange(n){
2730
+ if(rangeSwitchInFlight) return; rangeSwitchInFlight=true;
2731
+ const prev=RANGE; RANGE=n; renderRangePills();
2732
+ try{
2733
+ const [dd,ee]=await Promise.all([
2734
+ api('/dashboard/data?range='+RANGE+'d'),
2735
+ api('/dashboard/events?days='+RANGE),
2736
+ ]);
2737
+ D=dd;
2738
+ if(ee&&Array.isArray(ee.nights)){ EV=ee; NIGHTS=EV.nights; } else { NIGHTS=[]; }
2739
+ }catch(e){
2740
+ RANGE=prev; renderRangePills();
2741
+ toast('Range switch failed: '+e.message);
2742
+ return;
2743
+ }finally{
2744
+ rangeSwitchInFlight=false;
2745
+ }
2746
+ buildWorld(); buildPaths(); buildTimeline(); renderBand(); buildRoll(); buildCapture(); buildAgents();
2747
+ /* reset the replay over the new window (boot's pattern): rest at "now"
2748
+ first, the idle re-engage then walks the new range once from the start */
2749
+ T=WINDOW; lastT=WINDOW; played=false; autoEvIdx=0; pendingApply=null; autoPlay=false;
2750
+ setPaused(false);
2751
+ onTChanged(true);
2752
+ }
2753
+ renderRangePills();
2754
+
2585
2755
  /* the timeline: dots per night (green when busy adding, grey when the
2586
2756
  night merged ≥3 — the prototype's thresholds), date labels at the ends */
2587
2757
  function buildTimeline(){
@@ -2594,6 +2764,7 @@ function buildTimeline(){
2594
2764
  dots.style.cssText='position:absolute;inset:0;pointer-events:none;';
2595
2765
  if(REPLAY_OK){
2596
2766
  label.textContent=NIGHTS.length+' nights';
2767
+ el('replay').classList.remove('off'); /* a range switch can revive the instrument (#452) */
2597
2768
  const s1=document.createElement('span'); s1.style.left='0%'; s1.style.transform='none';
2598
2769
  s1.textContent=fmtDay(NIGHTS[0].date); dates.appendChild(s1);
2599
2770
  const s2=document.createElement('span'); s2.style.left='100%'; s2.style.transform='translateX(-100%)';
@@ -2603,7 +2774,10 @@ function buildTimeline(){
2603
2774
  d.style.left=((i+.5)/WINDOW*100)+'%';
2604
2775
  if(mergedL[i]>=3) d.classList.add('w');
2605
2776
  else if(learnedL[i]>=8) d.classList.add('g');
2606
- d.title=fmtDay(n.date)+' — learned '+(learnedL[i]||0)+(enrichedL[i]?' · enriched '+enrichedL[i]:'')+(mergedL[i]?' · merged '+mergedL[i]:'');
2777
+ d.title=fmtDay(n.date)+' — learned '+(learnedL[i]||0)+(enrichedL[i]?' · enriched '+enrichedL[i]:'')+(mergedL[i]?' · merged '+mergedL[i]:'')
2778
+ +(n.superseded.length?' · superseded '+n.superseded.length:'')
2779
+ +(n.retracted.length?' · retracted '+n.retracted.length:'')
2780
+ +(n.rewritten.length?' · rewritten '+n.rewritten.length:'');
2607
2781
  frag.appendChild(d);
2608
2782
  });
2609
2783
  }else{
@@ -3231,7 +3405,14 @@ function buildRoll(){
3231
3405
 
3232
3406
  /* ---------- capture health — phase 2: per machine × harness rows from the
3233
3407
  /dashboard/data capture_health accounting; the counts-only rows remain
3234
- the degrade for old servers / nothing recorded yet ---------- */
3408
+ the degrade for old servers / nothing recorded yet.
3409
+ Fix round 7 (owner ruling 2026-09-14 — "the normal 30 day as the other
3410
+ cards"): the card's face is the ROLLING window (capture_health
3411
+ window_rows, summed over the last window_days calendar days); the newest
3412
+ night rides as a per-row secondary suffix ("tonight: N sess" / the
3413
+ night's date when it is not today). Degradation ladder: pre-round-7
3414
+ server (no window_rows) → the newest-night view below; no activity rows
3415
+ at all → the counts-only rows. ---------- */
3235
3416
  /* ---------- machine × harness grouping (#424 review fixes 4+5 + the owner's
3236
3417
  model-B direction): source_agent is "harness/profile" and each context may
3237
3418
  carry a source_machine stamp (migration v15+). The rows a stats card and
@@ -3343,6 +3524,72 @@ function buildCapture(){
3343
3524
  host.innerHTML='';
3344
3525
  const sub=el('cap-sub');
3345
3526
  const ch=D&&D.capture_health?D.capture_health:null;
3527
+ /* fix round 7: the rolling-window view — the card shows the normal 30 day
3528
+ like the band's other cards. window_rows is ABSENT on a pre-round-7
3529
+ server (and empty when nothing is recorded) → fall through to the
3530
+ newest-night view / counts degrade below. */
3531
+ if(ch&&Array.isArray(ch.window_rows)&&ch.window_rows.length){
3532
+ const wd=ch.window_days||30;
3533
+ if(sub) sub.textContent='last '+wd+' days';
3534
+ /* group the window's machine × agent rows into machine × harness
3535
+ bundles — same grouping as every other card surface */
3536
+ const groups=new Map();
3537
+ for(const r of ch.window_rows){
3538
+ const agent=String(r.agent||'unknown');
3539
+ const harness=agent.split('/')[0]||agent;
3540
+ const key=(r.machine||'')+'|'+harness;
3541
+ let g=groups.get(key);
3542
+ if(!g){ g={machine:r.machine||null, harness, posts:0, sessions:0, bytes:0, held:0, retried:0, agents:[]}; groups.set(key,g); }
3543
+ g.posts+=r.posts||0; g.sessions+=r.sessions||0; g.bytes+=r.bytes||0;
3544
+ g.held+=r.held||0; g.retried+=r.retried||0;
3545
+ g.agents.push(agent+' — '+r.posts+' post'+(r.posts===1?'':'s')+' · '+r.sessions+' sess · '+mbFmt(r.bytes||0)
3546
+ +(r.held?' · '+r.held+' held':'')+(r.retried?' · '+r.retried+' retried':''));
3547
+ }
3548
+ /* newest-night secondary: per-bundle sessions + held off capture_health
3549
+ rows (the newest day with rows). held keys the badge — a held post is
3550
+ actionable NOW (the client retries it next run); the window's own
3551
+ held/retried live in the row tooltip. */
3552
+ const night=new Map();
3553
+ if(ch.day&&Array.isArray(ch.rows)) for(const r of ch.rows){
3554
+ const agent=String(r.agent||'unknown');
3555
+ const harness=agent.split('/')[0]||agent;
3556
+ const key=(r.machine||'')+'|'+harness;
3557
+ const prev=night.get(key)||{sessions:0,held:0};
3558
+ prev.sessions+=r.sessions||0; prev.held+=r.held||0;
3559
+ night.set(key,prev);
3560
+ }
3561
+ const today=new Date().toISOString().slice(0,10);
3562
+ const nightLabel=ch.day===today?'tonight':fmtDay(ch.day);
3563
+ const gs2=[...groups.values()].sort((a,b)=>b.bytes-a.bytes);
3564
+ const maxB=gs2[0].bytes;
3565
+ const pausedKeys=fleetPausesByKey();
3566
+ for(const g of gs2){
3567
+ const n=night.get((g.machine||'')+'|'+g.harness);
3568
+ const row=document.createElement('div');
3569
+ row.className='cap-row2';
3570
+ const label=g.machine?g.machine+' · '+g.harness:g.harness;
3571
+ const paused=pausedKeys.has((g.machine||'')+'|'+g.harness);
3572
+ row.title=label+' — last '+wd+' days: '+fmt(g.sessions)+' sessions · '+mbFmt(g.bytes)+' posted'
3573
+ +(g.held>0?' · '+g.held+' held in window':'')
3574
+ +(g.retried>0?' · '+g.retried+' retried in window':'')
3575
+ +(n?' · '+nightLabel+': '+n.sessions+' sess'+(n.held>0?' ('+n.held+' held)':''):'')
3576
+ +(paused?' · CAPTURE PAUSED (posts are skipped — sessions are not backfilled)':'')
3577
+ +':\n'+g.agents.join('\n');
3578
+ row.innerHTML='<span class="cap-dot"></span>'+
3579
+ '<span class="hn" title="'+esc(label)+'">'+esc(label)+'</span>'+
3580
+ '<span class="cap-bar"><span class="fill" style="width:'+(g.bytes/Math.max(maxB,1)*100)+'%"></span></span>'+
3581
+ '<span class="st"><b>'+fmt(g.sessions)+'</b> sess · '+mbFmt(g.bytes)
3582
+ +(paused?'<span class="cap-paused" title="capture paused — posts are skipped (200) and sessions are not backfilled">PAUSED</span>':'')
3583
+ +(n&&n.held>0?'<span class="cap-held" title="'+nightLabel+' — failed posts the client will retry — cursor held">&#9888; '+n.held+' held</span>':'')
3584
+ +'</span>'
3585
+ /* the newest-night sub-line — a full-basis flex child, so it wraps
3586
+ UNDER the primary line (the suffix never fits beside it; see the
3587
+ .cap-ton rule) and only exists for bundles with newest-night rows */
3588
+ +(n?'<span class="cap-ton" title="newest capture night ('+nightLabel+') — '+n.sessions+' session'+(n.sessions===1?'':'s')+' posted that night">'+nightLabel+': '+n.sessions+' sess</span>':'');
3589
+ host.appendChild(row);
3590
+ }
3591
+ return;
3592
+ }
3346
3593
  if(ch&&Array.isArray(ch.rows)&&ch.rows.length){
3347
3594
  /* phase-2 live view: the /distill accounting rows, grouped machine ×
3348
3595
  harness — width encodes capture volume (each bundle's byte share of
@@ -3421,7 +3668,9 @@ function buildCapture(){
3421
3668
  function lastNightByAgent(){
3422
3669
  for(let i=NIGHTS.length-1;i>=0;i--){
3423
3670
  const n=NIGHTS[i];
3424
- if(n.added.length+n.enriched.length+n.merged.length+n.linked.length>0) return {date:n.date,by:n.by_agent};
3671
+ /* #452: the truth-management events count too — a superseded-only night
3672
+ is a real night */
3673
+ if(n.added.length+n.enriched.length+n.merged.length+n.linked.length+n.superseded.length+n.retracted.length+n.rewritten.length>0) return {date:n.date,by:n.by_agent};
3425
3674
  }
3426
3675
  return null;
3427
3676
  }
@@ -3659,7 +3908,7 @@ function buildAgents(){
3659
3908
  * counts — the paused state and dots re-derive from the fresh payload. */
3660
3909
  async function refreshFleet(){
3661
3910
  try{
3662
- const dd=await api('/dashboard/data?range=90d');
3911
+ const dd=await api('/dashboard/data?range='+RANGE+'d');
3663
3912
  D=dd;
3664
3913
  buildAgents();
3665
3914
  buildCapture();
@@ -4040,7 +4289,7 @@ let refreshInFlight=false;
4040
4289
  async function refreshAfterWrite(){
4041
4290
  if(refreshInFlight) return; refreshInFlight=true;
4042
4291
  try{
4043
- const [ff,dd]=await Promise.all([api('/dashboard/field'),api('/dashboard/data?range=90d').catch(()=>null)]);
4292
+ const [ff,dd]=await Promise.all([api('/dashboard/field'),api('/dashboard/data?range='+RANGE+'d').catch(()=>null)]);
4044
4293
  if(ff){ F=ff; buildWorld(); }
4045
4294
  if(dd){ D=dd; }
4046
4295
  renderBand(); buildRoll(); buildCapture(); renderLegend(); renderFeel();
@@ -4351,9 +4600,9 @@ async function pollHealth(){
4351
4600
  ===================================================================== */
4352
4601
  async function boot(){
4353
4602
  const [dd,ff,ee,hh,hd,mm]=await Promise.allSettled([
4354
- api('/dashboard/data?range=90d'),
4603
+ api('/dashboard/data?range='+RANGE+'d'),
4355
4604
  api('/dashboard/field'),
4356
- api('/dashboard/events?days=90'),
4605
+ api('/dashboard/events?days='+RANGE),
4357
4606
  fetch('/health'),
4358
4607
  api('/health/detail'),
4359
4608
  api('/dashboard/model'),
@@ -28,6 +28,36 @@
28
28
  * ranking. Long-term remembering is the product; time preference stays mild.
29
29
  */
30
30
  export declare const DECAY_HALF_LIFE_DAYS = 365;
31
+ /**
32
+ * Per-use strength promotion rate (#448, the strength model's upward path):
33
+ * one application of `promotion = PROMOTION_RATE × (1 − S / IMPORTANCE_CEILING)
34
+ * × S^(−0.3)` per new use (S = stored base_strength), iterated once per
35
+ * access-count delta by the nightly stagePromotion stage and clamped at the
36
+ * ceiling. Calibrated so the first access on a median-0.35 memory gives ~+0.03
37
+ * (0.35 → 0.3786). Shape grounded in LTP saturation curves (Cao & Harris
38
+ * 2014), the power law of practice (α ≈ 0.3, Newell & Rosenbloom 1981), and
39
+ * spaced-repetition stability multipliers (S^−0.53, SuperMemo SM-17,
40
+ * n > 60,000 real repetitions): bigger boosts for weaker memories,
41
+ * saturating near the ceiling, never fully stopping (asymptotic). The decay
42
+ * side is untouched — a memory's fade RATE no longer depends on its use or
43
+ * link history (the #448 hardening removal); use raises the STORED score
44
+ * instead, and abandonment fades it on the same 365-day clock as everything
45
+ * else. RELEASE-MANAGED per this module's evolution contract — no config key.
46
+ */
47
+ export declare const PROMOTION_RATE = 0.033;
48
+ /**
49
+ * Lower bound on the promotion formula's S input (#453 review Minor finding,
50
+ * owner decision 2026-09-16). base_strength 0.0 is a writable value (legacy
51
+ * rows, operator edits), and S^(−0.3) has a zero-singularity: unfloored, one
52
+ * access on a 0.0 row computed delta = 0.033 × (1 − 0/0.95) × 0^−0.3 =
53
+ * Infinity, and the ceiling clamp turned that into a leap straight to 0.95.
54
+ * Floored at 0.01, the same access lands at 0.033 × (1 − 0.01/0.95) ×
55
+ * 0.01^(−0.3) ≈ +0.13 (0.0 → ≈0.14) — the biggest first boost the formula
56
+ * can give, bounded and far from the ceiling. The floor clamps the FORMULA
57
+ * INPUT only: storage keeps a 0.0 row at 0.0 until it is used, and the
58
+ * promoted result is nonzero from then on. RELEASE-MANAGED like PROMOTION_RATE.
59
+ */
60
+ export declare const PROMOTION_STRENGTH_FLOOR = 0.01;
31
61
  /** Default k for retrieve() (/search without an explicit limit). #192. */
32
62
  export declare const SEARCH_LIMIT = 8;
33
63
  /** Default k for searchRecent() (/recent without an explicit limit). #192. */
@@ -24,7 +24,7 @@
24
24
  * the removal is never silent.
25
25
  */
26
26
  Object.defineProperty(exports, "__esModule", { value: true });
27
- exports.DIAGNOSTIC_ENV_TIER = exports.OLLAMA_FLUSH_WAIT_MS = exports.OLLAMA_FLUSH_EVERY = exports.NUM_CTX = exports.DEFAULT_FIRST_RUN_LOOKBACK_DAYS = exports.ENRICH_STRENGTH_DELTA = exports.STAGE_TRUTH_STRENGTH = exports.STAGE_BELIEF_STRENGTH = exports.STAGE_FADING_STRENGTH = exports.STAGE_FADING_DAYS = exports.WEAK_PRIMARY_FLOOR = exports.CORRECTION_REWRITE_MIN_CONFIDENCE = exports.CORRECTION_MIN_SIMILARITY = exports.SUPERSESSION_MIN_SIMILARITY = exports.DEDUP_AUTO_MERGE_THRESHOLD = exports.IMPORTANCE_CEILING = exports.BM25_WEIGHT_DOMAIN = exports.BM25_WEIGHT_PROJECT = exports.BM25_WEIGHT_BODY = exports.BOTH_CHANNEL_BOOST = exports.RRF_VECTOR_WEIGHT = exports.RRF_FTS_WEIGHT = exports.RRF_COMPOSITE_WEIGHT = exports.RRF_K = exports.DOMAIN_AFFINITY_WEIGHT = exports.PROJECT_AFFINITY_WEIGHT = exports.SUPERSEDED_DEMOTION = exports.FRESHNESS_BOOST_WEIGHT = exports.FRESHNESS_BOOST_DAYS = exports.SCORE_RECENCY_WEIGHT = exports.SCORE_CONNECTIONS_WEIGHT = exports.SCORE_STRENGTH_WEIGHT = exports.SCORE_SIMILARITY_WEIGHT = exports.RECALL_USES_AXIS_MAX = exports.RECALL_USES_NORMAL_MAX = exports.RECALL_USES_LOW_MAX = exports.RECALL_RESHOW_TURNS = exports.NOVELTY_FLOOR_SLOTS = exports.RECALL_TITLE_CHARS = exports.RECALL_MIN_PROMPT_CHARS = exports.RECALL_MAX_ITEMS = exports.RECALL_MIN_SIMILARITY = exports.SESSION_INTENT_WEIGHT = exports.COLD_EXPOSURE_SLOTS = exports.RECENT_WINDOW_DAYS = exports.RECENT_LIMIT = exports.SEARCH_LIMIT = exports.DECAY_HALF_LIFE_DAYS = void 0;
27
+ exports.DIAGNOSTIC_ENV_TIER = exports.OLLAMA_FLUSH_WAIT_MS = exports.OLLAMA_FLUSH_EVERY = exports.NUM_CTX = exports.DEFAULT_FIRST_RUN_LOOKBACK_DAYS = exports.ENRICH_STRENGTH_DELTA = exports.STAGE_TRUTH_STRENGTH = exports.STAGE_BELIEF_STRENGTH = exports.STAGE_FADING_STRENGTH = exports.STAGE_FADING_DAYS = exports.WEAK_PRIMARY_FLOOR = exports.CORRECTION_REWRITE_MIN_CONFIDENCE = exports.CORRECTION_MIN_SIMILARITY = exports.SUPERSESSION_MIN_SIMILARITY = exports.DEDUP_AUTO_MERGE_THRESHOLD = exports.IMPORTANCE_CEILING = exports.BM25_WEIGHT_DOMAIN = exports.BM25_WEIGHT_PROJECT = exports.BM25_WEIGHT_BODY = exports.BOTH_CHANNEL_BOOST = exports.RRF_VECTOR_WEIGHT = exports.RRF_FTS_WEIGHT = exports.RRF_COMPOSITE_WEIGHT = exports.RRF_K = exports.DOMAIN_AFFINITY_WEIGHT = exports.PROJECT_AFFINITY_WEIGHT = exports.SUPERSEDED_DEMOTION = exports.FRESHNESS_BOOST_WEIGHT = exports.FRESHNESS_BOOST_DAYS = exports.SCORE_RECENCY_WEIGHT = exports.SCORE_CONNECTIONS_WEIGHT = exports.SCORE_STRENGTH_WEIGHT = exports.SCORE_SIMILARITY_WEIGHT = exports.RECALL_USES_AXIS_MAX = exports.RECALL_USES_NORMAL_MAX = exports.RECALL_USES_LOW_MAX = exports.RECALL_RESHOW_TURNS = exports.NOVELTY_FLOOR_SLOTS = exports.RECALL_TITLE_CHARS = exports.RECALL_MIN_PROMPT_CHARS = exports.RECALL_MAX_ITEMS = exports.RECALL_MIN_SIMILARITY = exports.SESSION_INTENT_WEIGHT = exports.COLD_EXPOSURE_SLOTS = exports.RECENT_WINDOW_DAYS = exports.RECENT_LIMIT = exports.SEARCH_LIMIT = exports.PROMOTION_STRENGTH_FLOOR = exports.PROMOTION_RATE = exports.DECAY_HALF_LIFE_DAYS = void 0;
28
28
  exports.resolveNumCtx = resolveNumCtx;
29
29
  exports.resolveOllamaFlushEvery = resolveOllamaFlushEvery;
30
30
  exports.resolveOllamaFlushWaitMs = resolveOllamaFlushWaitMs;
@@ -39,6 +39,36 @@ exports.resolveOllamaFlushWaitMs = resolveOllamaFlushWaitMs;
39
39
  * ranking. Long-term remembering is the product; time preference stays mild.
40
40
  */
41
41
  exports.DECAY_HALF_LIFE_DAYS = 365;
42
+ /**
43
+ * Per-use strength promotion rate (#448, the strength model's upward path):
44
+ * one application of `promotion = PROMOTION_RATE × (1 − S / IMPORTANCE_CEILING)
45
+ * × S^(−0.3)` per new use (S = stored base_strength), iterated once per
46
+ * access-count delta by the nightly stagePromotion stage and clamped at the
47
+ * ceiling. Calibrated so the first access on a median-0.35 memory gives ~+0.03
48
+ * (0.35 → 0.3786). Shape grounded in LTP saturation curves (Cao & Harris
49
+ * 2014), the power law of practice (α ≈ 0.3, Newell & Rosenbloom 1981), and
50
+ * spaced-repetition stability multipliers (S^−0.53, SuperMemo SM-17,
51
+ * n > 60,000 real repetitions): bigger boosts for weaker memories,
52
+ * saturating near the ceiling, never fully stopping (asymptotic). The decay
53
+ * side is untouched — a memory's fade RATE no longer depends on its use or
54
+ * link history (the #448 hardening removal); use raises the STORED score
55
+ * instead, and abandonment fades it on the same 365-day clock as everything
56
+ * else. RELEASE-MANAGED per this module's evolution contract — no config key.
57
+ */
58
+ exports.PROMOTION_RATE = 0.033;
59
+ /**
60
+ * Lower bound on the promotion formula's S input (#453 review Minor finding,
61
+ * owner decision 2026-09-16). base_strength 0.0 is a writable value (legacy
62
+ * rows, operator edits), and S^(−0.3) has a zero-singularity: unfloored, one
63
+ * access on a 0.0 row computed delta = 0.033 × (1 − 0/0.95) × 0^−0.3 =
64
+ * Infinity, and the ceiling clamp turned that into a leap straight to 0.95.
65
+ * Floored at 0.01, the same access lands at 0.033 × (1 − 0.01/0.95) ×
66
+ * 0.01^(−0.3) ≈ +0.13 (0.0 → ≈0.14) — the biggest first boost the formula
67
+ * can give, bounded and far from the ceiling. The floor clamps the FORMULA
68
+ * INPUT only: storage keeps a 0.0 row at 0.0 until it is used, and the
69
+ * promoted result is nonzero from then on. RELEASE-MANAGED like PROMOTION_RATE.
70
+ */
71
+ exports.PROMOTION_STRENGTH_FLOOR = 0.01;
42
72
  /** Default k for retrieve() (/search without an explicit limit). #192. */
43
73
  exports.SEARCH_LIMIT = 8;
44
74
  /** Default k for searchRecent() (/recent without an explicit limit). #192. */