@gamaze/hicortex 0.23.1 → 0.24.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.
- package/assets/dashboard.html +64 -13
- package/dist/calibration.d.ts +31 -0
- package/dist/calibration.js +38 -1
- package/dist/capture.d.ts +7 -0
- package/dist/capture.js +10 -1
- package/dist/dashboard.d.ts +32 -7
- package/dist/dashboard.js +50 -10
- package/dist/db.js +45 -0
- package/dist/dedup.js +2 -2
- package/dist/distill-queue.d.ts +203 -0
- package/dist/distill-queue.js +440 -0
- package/dist/health.d.ts +13 -1
- package/dist/health.js +6 -1
- package/dist/hosted-boot.d.ts +1 -1
- package/dist/index.js +17 -2
- package/dist/learnings-identity.js +20 -1
- package/dist/localhost-bypass.js +1 -1
- package/dist/mcp-server.js +92 -7
- package/dist/nightly.js +88 -1
- package/dist/nofit.d.ts +1 -1
- package/dist/nofit.js +1 -1
- package/dist/schema-prototypes.d.ts +1 -1
- package/dist/schema-prototypes.js +1 -1
- package/dist/status.d.ts +11 -0
- package/dist/status.js +25 -0
- package/dist/types.d.ts +20 -0
- package/hermes-plugin/hicortex/README.md +13 -6
- package/hermes-plugin/hicortex/__init__.py +7 -0
- package/hermes-plugin/hicortex/client.py +64 -2
- package/hermes-plugin/hicortex/plugin.yaml +1 -1
- package/hermes-plugin/hicortex/provider.py +24 -1
- package/opencode-plugin/hicortex/index.ts +24 -1
- package/package.json +2 -1
- package/pi-extension/hicortex/index.ts +24 -1
- package/server.json +2 -2
- package/dist/eval/decay-eval.d.ts +0 -111
- package/dist/eval/decay-eval.js +0 -214
- package/dist/eval/dups.d.ts +0 -100
- package/dist/eval/dups.js +0 -174
- package/dist/eval/eval-clock.d.ts +0 -32
- package/dist/eval/eval-clock.js +0 -47
- package/dist/eval/eval-db.d.ts +0 -25
- package/dist/eval/eval-db.js +0 -67
- package/dist/eval/graph-eval.d.ts +0 -89
- package/dist/eval/graph-eval.js +0 -246
- package/dist/eval/importance-eval.d.ts +0 -85
- package/dist/eval/importance-eval.js +0 -286
- package/dist/eval/planted-eval.d.ts +0 -30
- package/dist/eval/planted-eval.js +0 -122
- package/dist/eval/planted-fixtures.d.ts +0 -107
- package/dist/eval/planted-fixtures.js +0 -283
- package/dist/eval/planted-harness.d.ts +0 -183
- package/dist/eval/planted-harness.js +0 -651
- package/dist/eval/ranking-battery.d.ts +0 -125
- package/dist/eval/ranking-battery.js +0 -289
- package/dist/eval/ranking-eval.d.ts +0 -61
- package/dist/eval/ranking-eval.js +0 -554
- package/dist/eval/ranking-fixtures.d.ts +0 -117
- package/dist/eval/ranking-fixtures.js +0 -485
- package/dist/eval/recall-sweep.d.ts +0 -87
- package/dist/eval/recall-sweep.js +0 -1030
- package/dist/eval/reflection-census.d.ts +0 -19
- package/dist/eval/reflection-census.js +0 -25
- package/dist/eval/relevance-eval.d.ts +0 -178
- package/dist/eval/relevance-eval.js +0 -2240
- package/dist/eval/run-eval.d.ts +0 -20
- package/dist/eval/run-eval.js +0 -299
package/assets/dashboard.html
CHANGED
|
@@ -76,7 +76,7 @@
|
|
|
76
76
|
badged "best match" (server composite order kept — ranking is #425).
|
|
77
77
|
All-below-floor renders "no matches for '<q>'" — NN junk never shows.
|
|
78
78
|
- ONE-SCREEN LAYOUT: header + band + field fit the first viewport at
|
|
79
|
-
~1440×900 (
|
|
79
|
+
~1440×900 (reference resolution; 1280×800 verified). Band compacted; the
|
|
80
80
|
field takes the viewport remainder (clamp with a measured offset —
|
|
81
81
|
see the #brainwrap CSS note). Scroll stays available as overflow.
|
|
82
82
|
|
|
@@ -574,7 +574,7 @@
|
|
|
574
574
|
|
|
575
575
|
/* ---- the brain-field ---- */
|
|
576
576
|
/* ONE-SCREEN AT-REST LAYOUT (#409 polish, owner ruling 2026-09-13): header
|
|
577
|
-
+ stats band + field fit the first viewport at ~1440×900 (
|
|
577
|
+
+ stats band + field fit the first viewport at ~1440×900 (that is the
|
|
578
578
|
reference; 1280×800 verified too). The field takes the viewport REMAINDER
|
|
579
579
|
— the subtracted constant is the MEASURED header+band+paddings total on
|
|
580
580
|
the reference store (Chrome 152, headless): 412px at the 4-column width,
|
|
@@ -865,6 +865,15 @@
|
|
|
865
865
|
/* #423 phase 3: the capture-pause badge — the same amber family as the
|
|
866
866
|
held badge (an operator state, not an error) */
|
|
867
867
|
.cap-paused{ color:var(--amber); margin-left:5px; font-size:9px; letter-spacing:.08em; }
|
|
868
|
+
/* #530 follow-up: the distill-inbox row — stale badge + bar fill in the
|
|
869
|
+
amber family once the oldest queued item passes the payload's ECHOED warn
|
|
870
|
+
age. [hidden] must win over .cap-row2's flex (author display beats the
|
|
871
|
+
UA's hidden — the #billing-banner idiom) so a pre-#529 server shows
|
|
872
|
+
nothing, theme tokens only */
|
|
873
|
+
.cap-qrow[hidden]{ display:none; }
|
|
874
|
+
.cap-stale{ color:var(--amber); margin-left:5px; }
|
|
875
|
+
.cap-qrow .fill.warn{ background:var(--amber); }
|
|
876
|
+
.cap-qrow.stale .cap-dot{ background:var(--amber); }
|
|
868
877
|
|
|
869
878
|
/* ================= the memory dropdown — opens UNDER the left search ================= */
|
|
870
879
|
#panel{
|
|
@@ -1277,6 +1286,11 @@
|
|
|
1277
1286
|
<div class="brow">
|
|
1278
1287
|
<div class="bseg" id="g-capture">
|
|
1279
1288
|
<div class="gh"><span class="t">Capture health</span><span class="sub" id="cap-sub">memory counts</span></div>
|
|
1289
|
+
<!-- #530 follow-up: the distill inbox — the drain-side counterpart of
|
|
1290
|
+
this card's delivery-side health. Pinned above the scrollable
|
|
1291
|
+
#cap-rows; hidden until buildCapture's payload guard unhides it
|
|
1292
|
+
(pre-#529 servers omit the block). -->
|
|
1293
|
+
<div class="cap-row2 cap-qrow" id="cap-queue" hidden></div>
|
|
1280
1294
|
<div id="cap-rows"></div>
|
|
1281
1295
|
</div>
|
|
1282
1296
|
<div class="bseg" id="g-roll">
|
|
@@ -1395,7 +1409,7 @@
|
|
|
1395
1409
|
</label>
|
|
1396
1410
|
<label class="mm-check"><input type="checkbox" id="mm-think"> enable thinking <small>(local endpoints only)</small></label>
|
|
1397
1411
|
</div>
|
|
1398
|
-
<div class="mm-note">applies on server restart</div>
|
|
1412
|
+
<div class="mm-note" id="mm-note">applies on server restart</div>
|
|
1399
1413
|
<div class="mm-foot">
|
|
1400
1414
|
<button class="btn" id="mm-cancel">Cancel</button>
|
|
1401
1415
|
<button class="btn save" id="mm-save">Save</button>
|
|
@@ -2414,7 +2428,7 @@ function draw(now, dt){
|
|
|
2414
2428
|
if(L.rk>=lodK) continue; // zoom LOD (lines only)
|
|
2415
2429
|
const a=mems.get(L.a), b=mems.get(L.b);
|
|
2416
2430
|
if(!a||!b||!aliveAt(a,t)||!aliveAt(b,t)) continue;
|
|
2417
|
-
/* #474 final polish: the both-endpoints-in-window gate killed the mesh — links draw between ANY placed memories (
|
|
2431
|
+
/* #474 final polish: the both-endpoints-in-window gate killed the mesh — links draw between ANY placed memories (reference parity: the full LOD-tamed mesh), endpoints carry positions regardless of window membership */
|
|
2418
2432
|
if(a._emerged===false||b._emerged===false) continue; // thread waits for both endpoints
|
|
2419
2433
|
const [ax,ay]=bpos(a,now,t), [bx,by]=bpos(b,now,t);
|
|
2420
2434
|
const ctrl=segCtrlN(ax,ay,bx,by,(L.use>18?1:-1));
|
|
@@ -3398,8 +3412,8 @@ function renderRecBand(ups){
|
|
|
3398
3412
|
/* No trend arrow (owner 2026-09-20: removed — unclear). Its renderer and
|
|
3399
3413
|
the element writes are removed WITH it: a leftover write to a removed
|
|
3400
3414
|
element is a boot-killing TypeError whenever the recall band carries
|
|
3401
|
-
real data (
|
|
3402
|
-
was ever seeded). */
|
|
3415
|
+
real data (a production instance on 0.22.2: "0 memories" — boot died
|
|
3416
|
+
before the counter was ever seeded). */
|
|
3403
3417
|
}
|
|
3404
3418
|
|
|
3405
3419
|
/* ---------- Memory Precision — Level 1, the pushed index (#476). A compact
|
|
@@ -3509,32 +3523,44 @@ function renderModelCard(){
|
|
|
3509
3523
|
only as api_key_set), writes PUT with the subset the user touched. The
|
|
3510
3524
|
api-key input is WRITE-ONLY: prefilled with nothing, sent only when
|
|
3511
3525
|
non-empty ("leave blank to keep"). enable_thinking is sent only when the
|
|
3512
|
-
user toggles the checkbox (tri-state not needed).
|
|
3526
|
+
user toggles the checkbox (tri-state not needed).
|
|
3527
|
+
Hosted mode (#514): the GET carries managed:true — the hosting service
|
|
3528
|
+
owns the endpoint + credentials, so their inputs are disabled and never
|
|
3529
|
+
ride the PUT body (a save cannot hit the server's managed-field 403). */
|
|
3513
3530
|
let mmThinkTouched=false;
|
|
3514
3531
|
function openModelModal(){
|
|
3515
3532
|
if(!M) return; // no settings payload → no dialog (degrade)
|
|
3516
3533
|
const mdl=el('model-modal');
|
|
3534
|
+
const managed=!!M.managed;
|
|
3517
3535
|
el('mm-backend').value=M.backend||''; // '' = auto / OpenAI-compatible
|
|
3518
3536
|
el('mm-baseurl').value=M.base_url||'';
|
|
3519
3537
|
el('mm-model').value=M.model||'';
|
|
3520
3538
|
const ak=el('mm-apikey'); ak.value='';
|
|
3521
|
-
ak.placeholder=
|
|
3539
|
+
ak.placeholder=managed?'managed by the hosting service'
|
|
3540
|
+
:(M.api_key_set?'configured — leave blank to keep':'api key (optional for local endpoints)');
|
|
3522
3541
|
el('mm-maxtok').value=M.max_tokens!=null?String(M.max_tokens):'';
|
|
3523
3542
|
el('mm-think').checked=!!M.enable_thinking;
|
|
3524
3543
|
mmThinkTouched=false;
|
|
3544
|
+
for(const id of ['mm-backend','mm-baseurl','mm-apikey']) el(id).disabled=managed;
|
|
3545
|
+
el('mm-note').textContent=managed
|
|
3546
|
+
?'endpoint and credentials are managed by the hosting service — applies on server restart'
|
|
3547
|
+
:'applies on server restart';
|
|
3525
3548
|
mdl.hidden=false;
|
|
3526
|
-
el('mm-baseurl').focus();
|
|
3549
|
+
(managed?el('mm-model'):el('mm-baseurl')).focus();
|
|
3527
3550
|
}
|
|
3528
3551
|
function closeModelModal(){ el('model-modal').hidden=true; }
|
|
3529
3552
|
async function saveModelSettings(){
|
|
3530
3553
|
/* Build the PUT body from CURRENT field state: empty base_url/model/max
|
|
3531
3554
|
tokens = clear (null server-side); backend '' clears the named-backend
|
|
3532
|
-
key; api_key only rides when typed; enable_thinking only when toggled.
|
|
3555
|
+
key; api_key only rides when typed; enable_thinking only when toggled.
|
|
3556
|
+
Managed (hosted) mode: backend/base_url/api_key never ride the body. */
|
|
3533
3557
|
const body={};
|
|
3534
|
-
|
|
3535
|
-
|
|
3558
|
+
if(!M.managed){
|
|
3559
|
+
body.backend=el('mm-backend').value;
|
|
3560
|
+
const bu=el('mm-baseurl').value.trim(); body.base_url=bu?bu:null;
|
|
3561
|
+
const ak=el('mm-apikey').value; if(ak) body.api_key=ak;
|
|
3562
|
+
}
|
|
3536
3563
|
const mo=el('mm-model').value.trim(); body.model=mo?mo:null;
|
|
3537
|
-
const ak=el('mm-apikey').value; if(ak) body.api_key=ak;
|
|
3538
3564
|
const mt=el('mm-maxtok').value.trim(); body.max_tokens=mt?Number(mt):null;
|
|
3539
3565
|
if(mmThinkTouched) body.enable_thinking=el('mm-think').checked;
|
|
3540
3566
|
try{
|
|
@@ -3754,6 +3780,31 @@ function agoLabel(iso){
|
|
|
3754
3780
|
function buildCapture(){
|
|
3755
3781
|
const host=el('cap-rows');
|
|
3756
3782
|
host.innerHTML='';
|
|
3783
|
+
/* #530 follow-up: the distill-inbox row — rendered FIRST because every
|
|
3784
|
+
render branch below returns early (a call after them would only run in
|
|
3785
|
+
the degrade). The absent-block guard keeps it hidden on a pre-#529
|
|
3786
|
+
server; the warn threshold is always the payload's echo (#476
|
|
3787
|
+
degrade-literal precedent), never a page literal. */
|
|
3788
|
+
const qrow=el('cap-queue');
|
|
3789
|
+
qrow.innerHTML='';
|
|
3790
|
+
qrow.className='cap-row2 cap-qrow';
|
|
3791
|
+
const q=D&&D.distill_queue?D.distill_queue:null;
|
|
3792
|
+
qrow.hidden=!q;
|
|
3793
|
+
if(q){
|
|
3794
|
+
const thr=q.warn_age_hours||24;
|
|
3795
|
+
const oldest=(typeof q.oldest_age_hours==='number')?q.oldest_age_hours:null;
|
|
3796
|
+
const stale=oldest!==null&&oldest>thr;
|
|
3797
|
+
if(stale) qrow.classList.add('stale');
|
|
3798
|
+
qrow.title='Distill inbox — deliveries are stored durably at POST time and distilled by the scheduled nightly run before consolidation'
|
|
3799
|
+
+(stale?' — the oldest item is '+oldest+' h old (warn age '+thr+' h): the scheduled drain is behind':'');
|
|
3800
|
+
qrow.innerHTML='<span class="cap-dot"></span>'
|
|
3801
|
+
+'<span class="hn">Distill inbox</span>'
|
|
3802
|
+
+'<span class="cap-bar"><span class="fill'+(stale?' warn':'')+'" style="width:'+(oldest===null?0:Math.min(1,oldest/thr)*100)+'%"></span></span>'
|
|
3803
|
+
+'<span class="st"><b>'+fmt(q.depth||0)+'</b> queued'
|
|
3804
|
+
+(oldest===null?' · inbox empty':' · oldest '+(oldest>=48?(oldest/24).toFixed(1)+'d':oldest.toFixed(1)+'h'))
|
|
3805
|
+
+(stale?'<span class="cap-stale" title="oldest queued item past the '+thr+' h warn age — the scheduled drain is behind">⚠ stale</span>':'')
|
|
3806
|
+
+'</span>';
|
|
3807
|
+
}
|
|
3757
3808
|
const sub=el('cap-sub');
|
|
3758
3809
|
const ch=D&&D.capture_health?D.capture_health:null;
|
|
3759
3810
|
/* fix round 7: the rolling-window view — the card shows the normal 30 day
|
package/dist/calibration.d.ts
CHANGED
|
@@ -416,3 +416,34 @@ export declare function resolveOllamaFlushEvery(): number;
|
|
|
416
416
|
* HICORTEX_OLLAMA_FLUSH_WAIT_MS wins; invalid warns and keeps
|
|
417
417
|
* OLLAMA_FLUSH_WAIT_MS. */
|
|
418
418
|
export declare function resolveOllamaFlushWaitMs(): number;
|
|
419
|
+
/**
|
|
420
|
+
* Re-check interval (ms) while the drain's yield signal reports the LLM
|
|
421
|
+
* endpoint busy (#529 owner ruling 05.10.2026: interactive use always
|
|
422
|
+
* yields). The drain waits one interval between probe re-checks instead of
|
|
423
|
+
* polling in a tight loop; 30 s is short enough that the run resumes within
|
|
424
|
+
* half a minute of the owner going idle, long enough not to hammer the
|
|
425
|
+
* signal endpoint while a long interactive session runs.
|
|
426
|
+
*/
|
|
427
|
+
export declare const DRAIN_YIELD_POLL_MS = 30000;
|
|
428
|
+
/**
|
|
429
|
+
* Per-item cap (ms) on yield waits (#529). The run deadline is the primary
|
|
430
|
+
* bound; this cap is the fail-open backstop so a misconfigured signal that
|
|
431
|
+
* always answers "busy" cannot stall one item past half an hour — the item
|
|
432
|
+
* proceeds and the next between-items checks decide again. 30 min matches
|
|
433
|
+
* the order of a long local-model generation, the longest legitimate busy.
|
|
434
|
+
*/
|
|
435
|
+
export declare const DRAIN_YIELD_WAIT_CAP_MS: number;
|
|
436
|
+
/**
|
|
437
|
+
* Timeout (ms) for ONE yield-probe GET (#529). Deliberately short: the probe
|
|
438
|
+
* sits between drain items, and an endpoint that answers busy-signals at all
|
|
439
|
+
* answers them fast — anything slower reads as unreachable (fail-open).
|
|
440
|
+
*/
|
|
441
|
+
export declare const DRAIN_YIELD_PROBE_TIMEOUT_MS = 3000;
|
|
442
|
+
/**
|
|
443
|
+
* Oldest-item age (hours) above which the distill inbox warns in
|
|
444
|
+
* `hicortex status` and on the dashboard (#529). 24 h ≈ two missed scheduled
|
|
445
|
+
* runs on the default 2×/day consolidation grid — items older than that are
|
|
446
|
+
* not "waiting for tonight" anymore, they are stuck, and the operator should
|
|
447
|
+
* look at the drain's last outcome.
|
|
448
|
+
*/
|
|
449
|
+
export declare const DRAIN_QUEUE_WARN_AGE_HOURS = 24;
|
package/dist/calibration.js
CHANGED
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
*/
|
|
26
26
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
27
27
|
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.DEDUP_AUTO_MERGE_THRESHOLD = exports.IMPORTANCE_SETTLE_WINDOW_DAYS = 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.SCOPE_AFFINITY_WEIGHT = exports.SUPERSEDED_DEMOTION = exports.RECENCY_HEAD_DAYS = exports.RECENCY_HEAD_WEIGHT = exports.RECENCY_HOURLY_DECAY = exports.SCORE_RECENCY_WEIGHT = exports.CONNECTIONS_SATURATION_DEGREE = exports.SCORE_CONNECTIONS_WEIGHT = exports.SCORE_STRENGTH_WEIGHT = exports.SCORE_SIMILARITY_WEIGHT = exports.VOLATILE_GATE_MAX_CHARS = exports.VOLATILE_STATUS_FILTER = exports.MEMORY_PRECISION_PROMPT_EXCERPT_CHARS = exports.MEMORY_PRECISION_DIVERGENCE_MIN_SHOWN = exports.MEMORY_PRECISION_REDUNDANT_ABOVE = exports.MEMORY_PRECISION_WINDOW_DAYS = 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
|
-
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 = void 0;
|
|
28
|
+
exports.DRAIN_QUEUE_WARN_AGE_HOURS = exports.DRAIN_YIELD_PROBE_TIMEOUT_MS = exports.DRAIN_YIELD_WAIT_CAP_MS = exports.DRAIN_YIELD_POLL_MS = 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 = void 0;
|
|
29
29
|
exports.resolveNumCtx = resolveNumCtx;
|
|
30
30
|
exports.resolveOllamaFlushEvery = resolveOllamaFlushEvery;
|
|
31
31
|
exports.resolveOllamaFlushWaitMs = resolveOllamaFlushWaitMs;
|
|
@@ -514,3 +514,40 @@ function resolveOllamaFlushWaitMs() {
|
|
|
514
514
|
console.warn(`[hicortex] env HICORTEX_OLLAMA_FLUSH_WAIT_MS=${JSON.stringify(raw)} is not a positive finite number — using default ${exports.OLLAMA_FLUSH_WAIT_MS}.`);
|
|
515
515
|
return exports.OLLAMA_FLUSH_WAIT_MS;
|
|
516
516
|
}
|
|
517
|
+
// ---------------------------------------------------------------------------
|
|
518
|
+
// Distill inbox / drain family (#529) — delivery-compute separation. Delivery
|
|
519
|
+
// (cheap, must never lose data) stores segments durably; compute (expensive,
|
|
520
|
+
// scheduled) happens only inside the nightly's drain stage. These constants
|
|
521
|
+
// shape the drain's interactive-yield seam and the inbox's visibility signal.
|
|
522
|
+
// ---------------------------------------------------------------------------
|
|
523
|
+
/**
|
|
524
|
+
* Re-check interval (ms) while the drain's yield signal reports the LLM
|
|
525
|
+
* endpoint busy (#529 owner ruling 05.10.2026: interactive use always
|
|
526
|
+
* yields). The drain waits one interval between probe re-checks instead of
|
|
527
|
+
* polling in a tight loop; 30 s is short enough that the run resumes within
|
|
528
|
+
* half a minute of the owner going idle, long enough not to hammer the
|
|
529
|
+
* signal endpoint while a long interactive session runs.
|
|
530
|
+
*/
|
|
531
|
+
exports.DRAIN_YIELD_POLL_MS = 30_000;
|
|
532
|
+
/**
|
|
533
|
+
* Per-item cap (ms) on yield waits (#529). The run deadline is the primary
|
|
534
|
+
* bound; this cap is the fail-open backstop so a misconfigured signal that
|
|
535
|
+
* always answers "busy" cannot stall one item past half an hour — the item
|
|
536
|
+
* proceeds and the next between-items checks decide again. 30 min matches
|
|
537
|
+
* the order of a long local-model generation, the longest legitimate busy.
|
|
538
|
+
*/
|
|
539
|
+
exports.DRAIN_YIELD_WAIT_CAP_MS = 30 * 60_000;
|
|
540
|
+
/**
|
|
541
|
+
* Timeout (ms) for ONE yield-probe GET (#529). Deliberately short: the probe
|
|
542
|
+
* sits between drain items, and an endpoint that answers busy-signals at all
|
|
543
|
+
* answers them fast — anything slower reads as unreachable (fail-open).
|
|
544
|
+
*/
|
|
545
|
+
exports.DRAIN_YIELD_PROBE_TIMEOUT_MS = 3_000;
|
|
546
|
+
/**
|
|
547
|
+
* Oldest-item age (hours) above which the distill inbox warns in
|
|
548
|
+
* `hicortex status` and on the dashboard (#529). 24 h ≈ two missed scheduled
|
|
549
|
+
* runs on the default 2×/day consolidation grid — items older than that are
|
|
550
|
+
* not "waiting for tonight" anymore, they are stuck, and the operator should
|
|
551
|
+
* look at the drain's last outcome.
|
|
552
|
+
*/
|
|
553
|
+
exports.DRAIN_QUEUE_WARN_AGE_HOURS = 24;
|
package/dist/capture.d.ts
CHANGED
|
@@ -90,6 +90,13 @@ export interface PostResult {
|
|
|
90
90
|
completion: number;
|
|
91
91
|
total: number;
|
|
92
92
|
};
|
|
93
|
+
/**
|
|
94
|
+
* #529: the 201 confirmed durable QUEUEING, not distillation — a queue-mode
|
|
95
|
+
* server stores the segment and the nightly's drain distills it in the
|
|
96
|
+
* scheduled run. Contract-wise identical to any other 201 (the cursor
|
|
97
|
+
* advances); the capture loop only swaps its log line on it.
|
|
98
|
+
*/
|
|
99
|
+
queued?: boolean;
|
|
93
100
|
}
|
|
94
101
|
export type PostFn = (body: DistillBody) => Promise<PostResult>;
|
|
95
102
|
export interface CaptureOptions {
|
package/dist/capture.js
CHANGED
|
@@ -301,7 +301,16 @@ async function captureBatches(batches, opts) {
|
|
|
301
301
|
}
|
|
302
302
|
if (advancesBoundary)
|
|
303
303
|
lastConfirmedEnd = seg.segEnd;
|
|
304
|
-
|
|
304
|
+
// #529: a queue-mode 201 means "durably stored, distilled on the
|
|
305
|
+
// server's next scheduled run" — same confirmation, different log
|
|
306
|
+
// line, so a nightly log never reads "0 memories" as a failure when
|
|
307
|
+
// the server simply queued the segment.
|
|
308
|
+
if (result.queued) {
|
|
309
|
+
console.log(`[hicortex] Queued on server (segment ${body.segment_id}) — distilled on the next scheduled run`);
|
|
310
|
+
}
|
|
311
|
+
else {
|
|
312
|
+
console.log(`[hicortex] → ${result.distilled ?? 0} memories (segment ${body.segment_id})`);
|
|
313
|
+
}
|
|
305
314
|
for (const d of result.dropped ?? []) {
|
|
306
315
|
// #489: the response array carries BOTH gate kinds (substance +
|
|
307
316
|
// volatility) — the server's own log names the exact gate; this
|
package/dist/dashboard.d.ts
CHANGED
|
@@ -268,6 +268,20 @@ export interface DashboardData {
|
|
|
268
268
|
* pre-#476 recall-card rendering when the block is absent.
|
|
269
269
|
*/
|
|
270
270
|
memory_precision: MemoryPrecision;
|
|
271
|
+
/**
|
|
272
|
+
* #529 — the distill inbox: live queue depth + oldest-item age (the
|
|
273
|
+
* drain-side health signal; delivery-side health is capture_health
|
|
274
|
+
* above). ALWAYS present; `oldest_age_hours` null when empty. The warn
|
|
275
|
+
* threshold is ECHOED like every threshold (the page renders its own
|
|
276
|
+
* stale badge from it, never hardcodes 24). Live view only — no snapshot
|
|
277
|
+
* history (depth is ~0 at snapshot time by construction). A pre-#529
|
|
278
|
+
* server omits the block; the page guards.
|
|
279
|
+
*/
|
|
280
|
+
distill_queue: {
|
|
281
|
+
depth: number;
|
|
282
|
+
oldest_age_hours: number | null;
|
|
283
|
+
warn_age_hours: number;
|
|
284
|
+
};
|
|
271
285
|
digest: {
|
|
272
286
|
date: string | null;
|
|
273
287
|
run_at: string | null;
|
|
@@ -667,15 +681,19 @@ export interface DashboardModelSettings {
|
|
|
667
681
|
/** Constant true — the daemon resolves config at boot, so every write
|
|
668
682
|
* lands on restart. The modal footnotes it. */
|
|
669
683
|
applies_on_restart: true;
|
|
684
|
+
/** Present ONLY in hosted mode — the console uses it to disable the managed inputs (#514). */
|
|
685
|
+
managed?: true;
|
|
670
686
|
}
|
|
671
687
|
/**
|
|
672
688
|
* The pure GET handler: echo the CONFIG values raw (null when unset — the UI
|
|
673
689
|
* shows defaults as placeholders, so this never resolves them) + the runtime
|
|
674
|
-
* provider from the daemon's in-memory llmConfig.
|
|
690
|
+
* provider from the daemon's in-memory llmConfig. `hostedMode` (#514) only
|
|
691
|
+
* adds `managed: true` to the body — self-hosted responses never carry the
|
|
692
|
+
* key at all (byte-identical wire).
|
|
675
693
|
*/
|
|
676
694
|
export declare function handleDashboardModelGet(config: Record<string, unknown> | null | undefined, llmConfig: {
|
|
677
695
|
provider: string;
|
|
678
|
-
} | null): {
|
|
696
|
+
} | null, hostedMode?: boolean): {
|
|
679
697
|
status: 200;
|
|
680
698
|
body: DashboardModelSettings;
|
|
681
699
|
};
|
|
@@ -684,10 +702,14 @@ export declare function handleDashboardModelGet(config: Record<string, unknown>
|
|
|
684
702
|
* injected writer (which THROWS on a malformed config — the adapter maps that
|
|
685
703
|
* to a 500 and the file stays untouched), answer with the fresh GET shape
|
|
686
704
|
* built from the post-write config. `null` REMOVES a config key.
|
|
705
|
+
*
|
|
706
|
+
* `hostedMode` (#514): the managed fields (see HOSTED_MANAGED_MODEL_KEYS) are
|
|
707
|
+
* rejected BEFORE any validation or persistence — a refused PUT writes
|
|
708
|
+
* nothing, not even the allowed keys that rode the same body.
|
|
687
709
|
*/
|
|
688
710
|
export declare function handleDashboardModelPut(body: unknown, persist: (updates: Record<string, unknown>) => Record<string, unknown>, getLlmConfig: () => {
|
|
689
711
|
provider: string;
|
|
690
|
-
} | null): {
|
|
712
|
+
} | null, hostedMode?: boolean): {
|
|
691
713
|
status: number;
|
|
692
714
|
body: DashboardModelSettings | {
|
|
693
715
|
error: string;
|
|
@@ -696,21 +718,24 @@ export declare function handleDashboardModelPut(body: unknown, persist: (updates
|
|
|
696
718
|
/**
|
|
697
719
|
* Express adapter for GET /dashboard/model. Bearer-only by construction
|
|
698
720
|
* (mounted after createAuthMiddleware — NO exemption, unlike the page shells:
|
|
699
|
-
* this carries install config). Failures surface as a 500 {error}.
|
|
721
|
+
* this carries install config). Failures surface as a 500 {error}. The
|
|
722
|
+
* boot-resolved `hostedMode` flag (#514) threads straight through to the pure
|
|
723
|
+
* handler — it is a boot constant, not a per-request value.
|
|
700
724
|
*/
|
|
701
725
|
export declare function dashboardModelGetHandler(getConfig: () => Record<string, unknown> | null | undefined, getLlmConfig: () => {
|
|
702
726
|
provider: string;
|
|
703
|
-
} | null): express.RequestHandler;
|
|
727
|
+
} | null, hostedMode?: boolean): express.RequestHandler;
|
|
704
728
|
/**
|
|
705
729
|
* Express adapter for PUT /dashboard/model. Same auth posture as the GET.
|
|
706
730
|
* The injected persist closure owns the config path (the server passes
|
|
707
731
|
* init.ts persistConfigUpdates over stateDir/config.json); its load/persist
|
|
708
732
|
* failures (malformed config, unwritable file) map to a 500 {error} with the
|
|
709
|
-
* file left untouched — never a silent partial write.
|
|
733
|
+
* file left untouched — never a silent partial write. `hostedMode` (#514)
|
|
734
|
+
* threads to the pure handler's managed-field gate.
|
|
710
735
|
*/
|
|
711
736
|
export declare function dashboardModelPutHandler(persist: (updates: Record<string, unknown>) => Record<string, unknown>, getLlmConfig: () => {
|
|
712
737
|
provider: string;
|
|
713
|
-
} | null): express.RequestHandler;
|
|
738
|
+
} | null, hostedMode?: boolean): express.RequestHandler;
|
|
714
739
|
/** The PUT's body: {machine?: string|null, harness: string, paused: boolean}. */
|
|
715
740
|
export declare function handleDashboardCapturePausePut(body: unknown, setPause: (machine: string, harness: string, paused: boolean) => string | null): {
|
|
716
741
|
status: number;
|
package/dist/dashboard.js
CHANGED
|
@@ -46,6 +46,7 @@ const config_read_js_1 = require("./config-read.js");
|
|
|
46
46
|
const consolidate_js_1 = require("./consolidate.js");
|
|
47
47
|
const capture_health_js_1 = require("./capture-health.js");
|
|
48
48
|
const recall_precision_js_1 = require("./recall-precision.js");
|
|
49
|
+
const distill_queue_js_1 = require("./distill-queue.js");
|
|
49
50
|
const capture_pause_js_1 = require("./capture-pause.js");
|
|
50
51
|
const state_js_1 = require("./state.js");
|
|
51
52
|
const retrieval_js_1 = require("./retrieval.js");
|
|
@@ -585,6 +586,13 @@ function handleDashboardData(db, query, config) {
|
|
|
585
586
|
shown_sum: live.adoption?.shown_sum ?? 0,
|
|
586
587
|
used_sum: live.adoption?.used_sum ?? 0,
|
|
587
588
|
}),
|
|
589
|
+
// #529: live inbox state — depth + oldest age + the echoed warn
|
|
590
|
+
// threshold. Live only (no series): depth at snapshot time is ~0 by
|
|
591
|
+
// construction, so history would be a flat line.
|
|
592
|
+
distill_queue: {
|
|
593
|
+
...(0, distill_queue_js_1.readQueueStats)(db),
|
|
594
|
+
warn_age_hours: calibration_js_1.DRAIN_QUEUE_WARN_AGE_HOURS,
|
|
595
|
+
},
|
|
588
596
|
// #423 phase 3: pauses + presence in one block — one source of truth
|
|
589
597
|
// for the rail's dots, toggles and the capture card's PAUSED badges.
|
|
590
598
|
fleet: {
|
|
@@ -1005,9 +1013,11 @@ function configString(v) {
|
|
|
1005
1013
|
/**
|
|
1006
1014
|
* The pure GET handler: echo the CONFIG values raw (null when unset — the UI
|
|
1007
1015
|
* shows defaults as placeholders, so this never resolves them) + the runtime
|
|
1008
|
-
* provider from the daemon's in-memory llmConfig.
|
|
1016
|
+
* provider from the daemon's in-memory llmConfig. `hostedMode` (#514) only
|
|
1017
|
+
* adds `managed: true` to the body — self-hosted responses never carry the
|
|
1018
|
+
* key at all (byte-identical wire).
|
|
1009
1019
|
*/
|
|
1010
|
-
function handleDashboardModelGet(config, llmConfig) {
|
|
1020
|
+
function handleDashboardModelGet(config, llmConfig, hostedMode = false) {
|
|
1011
1021
|
const cfg = config ?? {};
|
|
1012
1022
|
return {
|
|
1013
1023
|
status: 200,
|
|
@@ -1022,6 +1032,7 @@ function handleDashboardModelGet(config, llmConfig) {
|
|
|
1022
1032
|
enable_thinking: typeof cfg.enableThinking === "boolean" ? cfg.enableThinking : null,
|
|
1023
1033
|
api_key_set: Boolean(configString(cfg.llmApiKey)),
|
|
1024
1034
|
applies_on_restart: true,
|
|
1035
|
+
...(hostedMode ? { managed: true } : {}),
|
|
1025
1036
|
},
|
|
1026
1037
|
};
|
|
1027
1038
|
}
|
|
@@ -1034,6 +1045,12 @@ const MODEL_PUT_KEYS = {
|
|
|
1034
1045
|
max_tokens: "maxTokens",
|
|
1035
1046
|
enable_thinking: "enableThinking",
|
|
1036
1047
|
};
|
|
1048
|
+
/** #514 — hosted mode: these model-settings fields are controlled by the
|
|
1049
|
+
* hosting service (it owns the endpoint and the credentials), so the PUT
|
|
1050
|
+
* rejects them wholesale. Key PRESENCE in the body is the trigger — the
|
|
1051
|
+
* value is never inspected (null or nested garbage is a change attempt like
|
|
1052
|
+
* any other). Self-hosted installs never hit this gate. */
|
|
1053
|
+
const HOSTED_MANAGED_MODEL_KEYS = ["backend", "base_url", "api_key"];
|
|
1037
1054
|
/** The backends init ever writes (absence of llmBackend + baseUrl+apiKey =
|
|
1038
1055
|
* the openai-compat path). "" clears (the modal's "auto" option). */
|
|
1039
1056
|
const MODEL_BACKEND_VALUES = new Set(["", "ollama", "claude-cli"]);
|
|
@@ -1042,12 +1059,32 @@ const MODEL_BACKEND_VALUES = new Set(["", "ollama", "claude-cli"]);
|
|
|
1042
1059
|
* injected writer (which THROWS on a malformed config — the adapter maps that
|
|
1043
1060
|
* to a 500 and the file stays untouched), answer with the fresh GET shape
|
|
1044
1061
|
* built from the post-write config. `null` REMOVES a config key.
|
|
1062
|
+
*
|
|
1063
|
+
* `hostedMode` (#514): the managed fields (see HOSTED_MANAGED_MODEL_KEYS) are
|
|
1064
|
+
* rejected BEFORE any validation or persistence — a refused PUT writes
|
|
1065
|
+
* nothing, not even the allowed keys that rode the same body.
|
|
1045
1066
|
*/
|
|
1046
|
-
function handleDashboardModelPut(body, persist, getLlmConfig) {
|
|
1067
|
+
function handleDashboardModelPut(body, persist, getLlmConfig, hostedMode = false) {
|
|
1047
1068
|
if (body === null || typeof body !== "object" || Array.isArray(body)) {
|
|
1048
1069
|
return { status: 400, body: { error: "Body must be a JSON object of model settings" } };
|
|
1049
1070
|
}
|
|
1050
1071
|
const input = body;
|
|
1072
|
+
// #514 hosted-mode gate: the hosting service owns the endpoint and the
|
|
1073
|
+
// credentials, so a tenant PUT may not touch them. Key PRESENCE is the
|
|
1074
|
+
// trigger; the rejection precedes validation AND persistence, so nothing
|
|
1075
|
+
// is written on a refused PUT (atomicity for mixed allowed+blocked bodies).
|
|
1076
|
+
if (hostedMode) {
|
|
1077
|
+
const attempted = HOSTED_MANAGED_MODEL_KEYS.filter((k) => k in input);
|
|
1078
|
+
if (attempted.length > 0) {
|
|
1079
|
+
const named = attempted.map((k) => `'${k}'`).join(", ");
|
|
1080
|
+
return {
|
|
1081
|
+
status: 403,
|
|
1082
|
+
body: {
|
|
1083
|
+
error: `${named} ${attempted.length === 1 ? "is" : "are"} managed by the hosting service and cannot be changed here (managed fields: ${HOSTED_MANAGED_MODEL_KEYS.join(", ")})`,
|
|
1084
|
+
},
|
|
1085
|
+
};
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1051
1088
|
const updates = {};
|
|
1052
1089
|
for (const [key, value] of Object.entries(input)) {
|
|
1053
1090
|
const configKey = MODEL_PUT_KEYS[key];
|
|
@@ -1129,17 +1166,19 @@ function handleDashboardModelPut(body, persist, getLlmConfig) {
|
|
|
1129
1166
|
// persist THROWS on a malformed config (strict load) — propagated to the
|
|
1130
1167
|
// adapter → 500, file untouched. On success it returns the fresh config.
|
|
1131
1168
|
const fresh = persist(updates);
|
|
1132
|
-
return handleDashboardModelGet(fresh, getLlmConfig());
|
|
1169
|
+
return handleDashboardModelGet(fresh, getLlmConfig(), hostedMode);
|
|
1133
1170
|
}
|
|
1134
1171
|
/**
|
|
1135
1172
|
* Express adapter for GET /dashboard/model. Bearer-only by construction
|
|
1136
1173
|
* (mounted after createAuthMiddleware — NO exemption, unlike the page shells:
|
|
1137
|
-
* this carries install config). Failures surface as a 500 {error}.
|
|
1174
|
+
* this carries install config). Failures surface as a 500 {error}. The
|
|
1175
|
+
* boot-resolved `hostedMode` flag (#514) threads straight through to the pure
|
|
1176
|
+
* handler — it is a boot constant, not a per-request value.
|
|
1138
1177
|
*/
|
|
1139
|
-
function dashboardModelGetHandler(getConfig, getLlmConfig) {
|
|
1178
|
+
function dashboardModelGetHandler(getConfig, getLlmConfig, hostedMode = false) {
|
|
1140
1179
|
return (_req, res) => {
|
|
1141
1180
|
try {
|
|
1142
|
-
const { status, body } = handleDashboardModelGet(getConfig(), getLlmConfig());
|
|
1181
|
+
const { status, body } = handleDashboardModelGet(getConfig(), getLlmConfig(), hostedMode);
|
|
1143
1182
|
res.status(status).json(body);
|
|
1144
1183
|
}
|
|
1145
1184
|
catch (err) {
|
|
@@ -1152,12 +1191,13 @@ function dashboardModelGetHandler(getConfig, getLlmConfig) {
|
|
|
1152
1191
|
* The injected persist closure owns the config path (the server passes
|
|
1153
1192
|
* init.ts persistConfigUpdates over stateDir/config.json); its load/persist
|
|
1154
1193
|
* failures (malformed config, unwritable file) map to a 500 {error} with the
|
|
1155
|
-
* file left untouched — never a silent partial write.
|
|
1194
|
+
* file left untouched — never a silent partial write. `hostedMode` (#514)
|
|
1195
|
+
* threads to the pure handler's managed-field gate.
|
|
1156
1196
|
*/
|
|
1157
|
-
function dashboardModelPutHandler(persist, getLlmConfig) {
|
|
1197
|
+
function dashboardModelPutHandler(persist, getLlmConfig, hostedMode = false) {
|
|
1158
1198
|
return (req, res) => {
|
|
1159
1199
|
try {
|
|
1160
|
-
const { status, body } = handleDashboardModelPut(req.body ?? null, persist, getLlmConfig);
|
|
1200
|
+
const { status, body } = handleDashboardModelPut(req.body ?? null, persist, getLlmConfig, hostedMode);
|
|
1161
1201
|
res.status(status).json(body);
|
|
1162
1202
|
}
|
|
1163
1203
|
catch (err) {
|
package/dist/db.js
CHANGED
|
@@ -763,6 +763,51 @@ const MIGRATIONS = [
|
|
|
763
763
|
`);
|
|
764
764
|
},
|
|
765
765
|
},
|
|
766
|
+
{
|
|
767
|
+
version: 24,
|
|
768
|
+
name: "distill_queue",
|
|
769
|
+
up: (db) => {
|
|
770
|
+
// #529 — the durable distill inbox. POST /distill in queue mode stores
|
|
771
|
+
// the REDACTED segment here and answers 201 immediately (no LLM call);
|
|
772
|
+
// the nightly drain distills rows oldest-first and deletes each row in
|
|
773
|
+
// the same transaction as its memory inserts. One table in the shared
|
|
774
|
+
// SQLite DB — one store, same backup and tooling as the corpus (owner
|
|
775
|
+
// decision; not files). The row carries every /distill wire field needed
|
|
776
|
+
// to re-distill later (attribution, project, session_date, privacy) so
|
|
777
|
+
// the drain reproduces the sync path's insert exactly.
|
|
778
|
+
//
|
|
779
|
+
// UNIQUE(session_id, segment_id) is the delivery-time idempotency key: a
|
|
780
|
+
// queued-but-undistilled segment re-POST answers 200 skipped (the
|
|
781
|
+
// handler's prechecks consult this table), and a failed drain attempt
|
|
782
|
+
// never duplicates on retry. Legacy no-segment posts normalize
|
|
783
|
+
// segment_id ''. session_id stays NULL for posts that carry no session
|
|
784
|
+
// key — SQLite unique indexes treat NULLs as distinct, so those rows
|
|
785
|
+
// (which have no dedup key by construction, same as the sync path's
|
|
786
|
+
// sourceSession-undefined inserts) each enqueue separately.
|
|
787
|
+
// Idempotent: IF NOT EXISTS (the v18/v21 pattern — plain CREATE).
|
|
788
|
+
db.exec(`
|
|
789
|
+
CREATE TABLE IF NOT EXISTS distill_queue (
|
|
790
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
791
|
+
session_id TEXT,
|
|
792
|
+
segment_id TEXT NOT NULL DEFAULT '',
|
|
793
|
+
source_agent TEXT,
|
|
794
|
+
source_agent_id TEXT,
|
|
795
|
+
source_domain TEXT,
|
|
796
|
+
source_machine TEXT,
|
|
797
|
+
project TEXT,
|
|
798
|
+
session_date TEXT,
|
|
799
|
+
privacy TEXT,
|
|
800
|
+
text TEXT NOT NULL,
|
|
801
|
+
arrived_at TEXT NOT NULL
|
|
802
|
+
)
|
|
803
|
+
`);
|
|
804
|
+
db.exec(`
|
|
805
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_distill_queue_session_segment
|
|
806
|
+
ON distill_queue(session_id, segment_id)
|
|
807
|
+
`);
|
|
808
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_distill_queue_arrived ON distill_queue(arrived_at)");
|
|
809
|
+
},
|
|
810
|
+
},
|
|
766
811
|
];
|
|
767
812
|
/**
|
|
768
813
|
* Run all pending migrations against the database.
|
package/dist/dedup.js
CHANGED
|
@@ -670,8 +670,8 @@ async function runDedup(options = {}) {
|
|
|
670
670
|
}
|
|
671
671
|
for (const c of plan.mismatchSkipped) {
|
|
672
672
|
// #206 decision 2: project_mismatch is the only skip reason left on
|
|
673
|
-
// this rail (the source_agent rail was removed) — the
|
|
674
|
-
// sizes the project rail alone off this line.
|
|
673
|
+
// this rail (the source_agent rail was removed) — the reference dry
|
|
674
|
+
// run sizes the project rail alone off this line.
|
|
675
675
|
console.log(`[hicortex] SKIPPED (project_mismatch): ${c.memberIds.map((id) => id.slice(0, 8)).join(", ")}`);
|
|
676
676
|
}
|
|
677
677
|
// #393 guard-C: listed for review like the mismatch clusters — a
|