hebbian 0.8.2 → 0.10.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/dist/bin/hebbian.js +587 -520
- package/dist/bin/hebbian.js.map +1 -1
- package/dist/digest.d.ts.map +1 -1
- package/dist/emit.d.ts +1 -1
- package/dist/emit.d.ts.map +1 -1
- package/dist/index.js +317 -252
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/bin/hebbian.js
CHANGED
|
@@ -435,6 +435,412 @@ var init_subsumption = __esm({
|
|
|
435
435
|
}
|
|
436
436
|
});
|
|
437
437
|
|
|
438
|
+
// src/similarity.ts
|
|
439
|
+
function tokenize(name) {
|
|
440
|
+
return name.replace(/([a-z])([A-Z])/g, "$1_$2").replace(/[^a-zA-Z0-9\u3000-\u9FFF\uAC00-\uD7AF]+/g, " ").toLowerCase().split(" ").map(stem).filter((t) => t.length > 1);
|
|
441
|
+
}
|
|
442
|
+
function stem(word) {
|
|
443
|
+
const suffixes = ["ing", "tion", "sion", "ness", "ment", "able", "ible", "ful", "less", "ous", "ive", "ity", "ies", "ed", "er", "es", "ly", "al", "en"];
|
|
444
|
+
for (const suffix of suffixes) {
|
|
445
|
+
if (word.length > suffix.length + 2 && word.endsWith(suffix)) {
|
|
446
|
+
return word.slice(0, -suffix.length);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
return word;
|
|
450
|
+
}
|
|
451
|
+
function jaccardSimilarity(a, b) {
|
|
452
|
+
if (a.length === 0 && b.length === 0) return 1;
|
|
453
|
+
if (a.length === 0 || b.length === 0) return 0;
|
|
454
|
+
const setA = new Set(a);
|
|
455
|
+
const setB = new Set(b);
|
|
456
|
+
let intersection = 0;
|
|
457
|
+
for (const item of setA) {
|
|
458
|
+
if (setB.has(item)) intersection++;
|
|
459
|
+
}
|
|
460
|
+
const union = (/* @__PURE__ */ new Set([...setA, ...setB])).size;
|
|
461
|
+
return intersection / union;
|
|
462
|
+
}
|
|
463
|
+
var init_similarity = __esm({
|
|
464
|
+
"src/similarity.ts"() {
|
|
465
|
+
"use strict";
|
|
466
|
+
}
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
// src/fire.ts
|
|
470
|
+
var fire_exports = {};
|
|
471
|
+
__export(fire_exports, {
|
|
472
|
+
contraNeuron: () => contraNeuron,
|
|
473
|
+
fireNeuron: () => fireNeuron,
|
|
474
|
+
getCurrentContra: () => getCurrentContra,
|
|
475
|
+
getCurrentCounter: () => getCurrentCounter
|
|
476
|
+
});
|
|
477
|
+
import { readdirSync as readdirSync3, renameSync, writeFileSync as writeFileSync2, existsSync as existsSync4, mkdirSync as mkdirSync2 } from "fs";
|
|
478
|
+
import { join as join3 } from "path";
|
|
479
|
+
function fireNeuron(brainRoot, neuronPath) {
|
|
480
|
+
const fullPath = join3(brainRoot, neuronPath);
|
|
481
|
+
if (!existsSync4(fullPath)) {
|
|
482
|
+
mkdirSync2(fullPath, { recursive: true });
|
|
483
|
+
writeFileSync2(join3(fullPath, "1.neuron"), "", "utf8");
|
|
484
|
+
console.log(`\u{1F331} grew + fired: ${neuronPath} (1)`);
|
|
485
|
+
return 1;
|
|
486
|
+
}
|
|
487
|
+
const current = getCurrentCounter(fullPath);
|
|
488
|
+
const newCounter = current + 1;
|
|
489
|
+
if (current > 0) {
|
|
490
|
+
renameSync(join3(fullPath, `${current}.neuron`), join3(fullPath, `${newCounter}.neuron`));
|
|
491
|
+
} else {
|
|
492
|
+
writeFileSync2(join3(fullPath, `${newCounter}.neuron`), "", "utf8");
|
|
493
|
+
}
|
|
494
|
+
console.log(`\u{1F525} fired: ${neuronPath} (${current} \u2192 ${newCounter})`);
|
|
495
|
+
return newCounter;
|
|
496
|
+
}
|
|
497
|
+
function contraNeuron(brainRoot, neuronPath) {
|
|
498
|
+
const fullPath = join3(brainRoot, neuronPath);
|
|
499
|
+
if (!existsSync4(fullPath)) {
|
|
500
|
+
return 0;
|
|
501
|
+
}
|
|
502
|
+
const current = getCurrentContra(fullPath);
|
|
503
|
+
const newContra = current + 1;
|
|
504
|
+
if (current > 0) {
|
|
505
|
+
renameSync(join3(fullPath, `${current}.contra`), join3(fullPath, `${newContra}.contra`));
|
|
506
|
+
} else {
|
|
507
|
+
writeFileSync2(join3(fullPath, `${newContra}.contra`), "", "utf8");
|
|
508
|
+
}
|
|
509
|
+
return newContra;
|
|
510
|
+
}
|
|
511
|
+
function getCurrentContra(dir) {
|
|
512
|
+
let max = 0;
|
|
513
|
+
try {
|
|
514
|
+
for (const entry of readdirSync3(dir)) {
|
|
515
|
+
if (entry.endsWith(".contra")) {
|
|
516
|
+
const n = parseInt(entry, 10);
|
|
517
|
+
if (!isNaN(n) && n > max) max = n;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
} catch {
|
|
521
|
+
}
|
|
522
|
+
return max;
|
|
523
|
+
}
|
|
524
|
+
function getCurrentCounter(dir) {
|
|
525
|
+
let max = 0;
|
|
526
|
+
try {
|
|
527
|
+
for (const entry of readdirSync3(dir)) {
|
|
528
|
+
if (entry.endsWith(".neuron") && !entry.startsWith("dopamine") && !entry.startsWith("memory") && entry !== "bomb.neuron") {
|
|
529
|
+
const n = parseInt(entry, 10);
|
|
530
|
+
if (!isNaN(n) && n > max) max = n;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
} catch {
|
|
534
|
+
}
|
|
535
|
+
return max;
|
|
536
|
+
}
|
|
537
|
+
var init_fire = __esm({
|
|
538
|
+
"src/fire.ts"() {
|
|
539
|
+
"use strict";
|
|
540
|
+
}
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
// src/grow.ts
|
|
544
|
+
var grow_exports = {};
|
|
545
|
+
__export(grow_exports, {
|
|
546
|
+
growNeuron: () => growNeuron
|
|
547
|
+
});
|
|
548
|
+
import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync3, existsSync as existsSync5, readdirSync as readdirSync4 } from "fs";
|
|
549
|
+
import { join as join4, relative as relative2 } from "path";
|
|
550
|
+
function growNeuron(brainRoot, neuronPath) {
|
|
551
|
+
const fullPath = join4(brainRoot, neuronPath);
|
|
552
|
+
if (existsSync5(fullPath)) {
|
|
553
|
+
const counter = fireNeuron(brainRoot, neuronPath);
|
|
554
|
+
return { action: "fired", path: neuronPath, counter };
|
|
555
|
+
}
|
|
556
|
+
if (neuronPath.includes("..") || neuronPath.startsWith("/")) {
|
|
557
|
+
throw new Error(`Invalid neuron path: "${neuronPath}" (path traversal not allowed)`);
|
|
558
|
+
}
|
|
559
|
+
const parts = neuronPath.split("/");
|
|
560
|
+
const regionName = parts[0];
|
|
561
|
+
if (regionName !== SKILLS_DIR && !REGIONS.includes(regionName)) {
|
|
562
|
+
throw new Error(`Invalid region: ${regionName}. Valid: ${REGIONS.join(", ")}, ${SKILLS_DIR}`);
|
|
563
|
+
}
|
|
564
|
+
const leafName = parts[parts.length - 1];
|
|
565
|
+
const newPrefix = leafName.match(/^(NO|DO|MUST|WARN)_/)?.[1] || "";
|
|
566
|
+
const newStripped = leafName.replace(/^(NO|DO|MUST|WARN)_/, "");
|
|
567
|
+
const newTokens = tokenize(newStripped);
|
|
568
|
+
const regionPath = join4(brainRoot, regionName);
|
|
569
|
+
if (existsSync5(regionPath)) {
|
|
570
|
+
const match = findSimilar(regionPath, regionPath, newTokens, newPrefix);
|
|
571
|
+
if (match) {
|
|
572
|
+
const matchRelPath = regionName + "/" + relative2(regionPath, match);
|
|
573
|
+
console.log(`\u{1F504} consolidation: "${neuronPath}" \u2248 "${matchRelPath}" (firing existing)`);
|
|
574
|
+
const counter = fireNeuron(brainRoot, matchRelPath);
|
|
575
|
+
return { action: "fired", path: matchRelPath, counter };
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
mkdirSync3(fullPath, { recursive: true });
|
|
579
|
+
writeFileSync3(join4(fullPath, "1.neuron"), "", "utf8");
|
|
580
|
+
console.log(`\u{1F331} grew: ${neuronPath} (1)`);
|
|
581
|
+
return { action: "grew", path: neuronPath, counter: 1 };
|
|
582
|
+
}
|
|
583
|
+
function findSimilar(dir, regionRoot, targetTokens, targetPrefix) {
|
|
584
|
+
let entries;
|
|
585
|
+
try {
|
|
586
|
+
entries = readdirSync4(dir, { withFileTypes: true });
|
|
587
|
+
} catch {
|
|
588
|
+
return null;
|
|
589
|
+
}
|
|
590
|
+
const hasNeuron = entries.some((e) => e.isFile() && e.name.endsWith(".neuron"));
|
|
591
|
+
if (hasNeuron) {
|
|
592
|
+
const folderName = dir.split("/").pop() || "";
|
|
593
|
+
const existingPrefix = folderName.match(/^(NO|DO|MUST|WARN)_/)?.[1] || "";
|
|
594
|
+
const existingStripped = folderName.replace(/^(NO|DO|MUST|WARN)_/, "");
|
|
595
|
+
const existingTokens = tokenize(existingStripped);
|
|
596
|
+
const similarity = jaccardSimilarity(targetTokens, existingTokens);
|
|
597
|
+
if (targetPrefix !== existingPrefix && targetTokens.length <= 2) {
|
|
598
|
+
} else if (similarity >= JACCARD_THRESHOLD) {
|
|
599
|
+
return dir;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
for (const entry of entries) {
|
|
603
|
+
if (entry.name.startsWith("_") || entry.name.startsWith(".")) continue;
|
|
604
|
+
if (entry.isDirectory()) {
|
|
605
|
+
const match = findSimilar(join4(dir, entry.name), regionRoot, targetTokens, targetPrefix);
|
|
606
|
+
if (match) return match;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
return null;
|
|
610
|
+
}
|
|
611
|
+
var init_grow = __esm({
|
|
612
|
+
"src/grow.ts"() {
|
|
613
|
+
"use strict";
|
|
614
|
+
init_constants();
|
|
615
|
+
init_similarity();
|
|
616
|
+
init_fire();
|
|
617
|
+
}
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
// src/episode.ts
|
|
621
|
+
var episode_exports = {};
|
|
622
|
+
__export(episode_exports, {
|
|
623
|
+
logEpisode: () => logEpisode,
|
|
624
|
+
readEpisodes: () => readEpisodes
|
|
625
|
+
});
|
|
626
|
+
import { readdirSync as readdirSync5, readFileSync as readFileSync3, writeFileSync as writeFileSync4, mkdirSync as mkdirSync4, existsSync as existsSync6 } from "fs";
|
|
627
|
+
import { join as join5 } from "path";
|
|
628
|
+
function logEpisode(brainRoot, type, path, detail, extra) {
|
|
629
|
+
const logDir = join5(brainRoot, SESSION_LOG_DIR);
|
|
630
|
+
if (!existsSync6(logDir)) {
|
|
631
|
+
mkdirSync4(logDir, { recursive: true });
|
|
632
|
+
}
|
|
633
|
+
const nextSlot = getNextSlot(logDir);
|
|
634
|
+
const episode = {
|
|
635
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
636
|
+
type,
|
|
637
|
+
path,
|
|
638
|
+
detail,
|
|
639
|
+
...extra?.outcome ? { outcome: extra.outcome } : {},
|
|
640
|
+
...extra?.neurons ? { neurons: extra.neurons } : {}
|
|
641
|
+
};
|
|
642
|
+
writeFileSync4(
|
|
643
|
+
join5(logDir, `memory${nextSlot}.neuron`),
|
|
644
|
+
JSON.stringify(episode),
|
|
645
|
+
"utf8"
|
|
646
|
+
);
|
|
647
|
+
}
|
|
648
|
+
function readEpisodes(brainRoot) {
|
|
649
|
+
const logDir = join5(brainRoot, SESSION_LOG_DIR);
|
|
650
|
+
if (!existsSync6(logDir)) return [];
|
|
651
|
+
const episodes = [];
|
|
652
|
+
let entries;
|
|
653
|
+
try {
|
|
654
|
+
entries = readdirSync5(logDir);
|
|
655
|
+
} catch {
|
|
656
|
+
return [];
|
|
657
|
+
}
|
|
658
|
+
for (const entry of entries) {
|
|
659
|
+
if (!entry.startsWith("memory") || !entry.endsWith(".neuron")) continue;
|
|
660
|
+
try {
|
|
661
|
+
const content = readFileSync3(join5(logDir, entry), "utf8");
|
|
662
|
+
if (content.trim()) {
|
|
663
|
+
episodes.push(JSON.parse(content));
|
|
664
|
+
}
|
|
665
|
+
} catch {
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
episodes.sort((a, b) => a.ts.localeCompare(b.ts));
|
|
669
|
+
return episodes;
|
|
670
|
+
}
|
|
671
|
+
function getNextSlot(logDir) {
|
|
672
|
+
let maxSlot = 0;
|
|
673
|
+
try {
|
|
674
|
+
for (const entry of readdirSync5(logDir)) {
|
|
675
|
+
if (entry.startsWith("memory") && entry.endsWith(".neuron")) {
|
|
676
|
+
const n = parseInt(entry.replace("memory", "").replace(".neuron", ""), 10);
|
|
677
|
+
if (!isNaN(n) && n > maxSlot) maxSlot = n;
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
} catch {
|
|
681
|
+
}
|
|
682
|
+
const next = maxSlot + 1;
|
|
683
|
+
return next > MAX_EPISODES ? maxSlot % MAX_EPISODES + 1 : next;
|
|
684
|
+
}
|
|
685
|
+
var MAX_EPISODES, SESSION_LOG_DIR;
|
|
686
|
+
var init_episode = __esm({
|
|
687
|
+
"src/episode.ts"() {
|
|
688
|
+
"use strict";
|
|
689
|
+
MAX_EPISODES = 100;
|
|
690
|
+
SESSION_LOG_DIR = "hippocampus/session_log";
|
|
691
|
+
}
|
|
692
|
+
});
|
|
693
|
+
|
|
694
|
+
// src/candidates.ts
|
|
695
|
+
var candidates_exports = {};
|
|
696
|
+
__export(candidates_exports, {
|
|
697
|
+
CANDIDATE_DECAY_DAYS: () => CANDIDATE_DECAY_DAYS,
|
|
698
|
+
CANDIDATE_THRESHOLD: () => CANDIDATE_THRESHOLD,
|
|
699
|
+
fromCandidatePath: () => fromCandidatePath,
|
|
700
|
+
growCandidate: () => growCandidate,
|
|
701
|
+
listCandidates: () => listCandidates,
|
|
702
|
+
promoteCandidates: () => promoteCandidates,
|
|
703
|
+
propagateToShared: () => propagateToShared,
|
|
704
|
+
toCandidatePath: () => toCandidatePath
|
|
705
|
+
});
|
|
706
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync5, readdirSync as readdirSync6, renameSync as renameSync2, rmSync, statSync as statSync2 } from "fs";
|
|
707
|
+
import { join as join6, dirname as dirname2, relative as relative3 } from "path";
|
|
708
|
+
function toCandidatePath(neuronPath) {
|
|
709
|
+
const slash = neuronPath.indexOf("/");
|
|
710
|
+
if (slash === -1) throw new Error(`Invalid neuron path (missing region): ${neuronPath}`);
|
|
711
|
+
return `${neuronPath.slice(0, slash)}/${CANDIDATE_SEGMENT}/${neuronPath.slice(slash + 1)}`;
|
|
712
|
+
}
|
|
713
|
+
function fromCandidatePath(candidatePath) {
|
|
714
|
+
return candidatePath.replace(`/${CANDIDATE_SEGMENT}/`, "/");
|
|
715
|
+
}
|
|
716
|
+
function growCandidate(brainRoot, neuronPath) {
|
|
717
|
+
const candidatePath = toCandidatePath(neuronPath);
|
|
718
|
+
const result = growNeuron(brainRoot, candidatePath);
|
|
719
|
+
if (result.counter >= CANDIDATE_THRESHOLD) {
|
|
720
|
+
const ok = moveCandidate(brainRoot, candidatePath, neuronPath);
|
|
721
|
+
if (ok) propagateToShared(brainRoot, neuronPath);
|
|
722
|
+
return { ...result, path: ok ? neuronPath : result.path, promoted: ok };
|
|
723
|
+
}
|
|
724
|
+
console.log(` \u{1F331} candidate (${result.counter}/${CANDIDATE_THRESHOLD}): ${candidatePath}`);
|
|
725
|
+
return { ...result, promoted: false };
|
|
726
|
+
}
|
|
727
|
+
function moveCandidate(brainRoot, candidatePath, targetPath) {
|
|
728
|
+
const src = join6(brainRoot, candidatePath);
|
|
729
|
+
if (!existsSync7(src)) return false;
|
|
730
|
+
const dst = join6(brainRoot, targetPath);
|
|
731
|
+
if (existsSync7(dst)) {
|
|
732
|
+
fireNeuron(brainRoot, targetPath);
|
|
733
|
+
rmSync(src, { recursive: true, force: true });
|
|
734
|
+
} else {
|
|
735
|
+
mkdirSync5(dirname2(dst), { recursive: true });
|
|
736
|
+
renameSync2(src, dst);
|
|
737
|
+
}
|
|
738
|
+
console.log(`\u{1F393} promoted: ${candidatePath} \u2192 ${targetPath}`);
|
|
739
|
+
return true;
|
|
740
|
+
}
|
|
741
|
+
function promoteCandidates(brainRoot) {
|
|
742
|
+
const promoted = [];
|
|
743
|
+
const decayed = [];
|
|
744
|
+
const decayMs = CANDIDATE_DECAY_DAYS * 24 * 60 * 60 * 1e3;
|
|
745
|
+
const now = Date.now();
|
|
746
|
+
for (const region of REGIONS) {
|
|
747
|
+
const candidateRoot = join6(brainRoot, region, CANDIDATE_SEGMENT);
|
|
748
|
+
walkNeuronDirs(candidateRoot, (neuronDir) => {
|
|
749
|
+
const rel = relative3(join6(brainRoot, region), neuronDir);
|
|
750
|
+
const candidatePath = `${region}/${rel}`;
|
|
751
|
+
const targetPath = fromCandidatePath(candidatePath);
|
|
752
|
+
const counter = readCounter(neuronDir);
|
|
753
|
+
const mtime = statSync2(neuronDir).mtimeMs;
|
|
754
|
+
if (counter >= CANDIDATE_THRESHOLD) {
|
|
755
|
+
moveCandidate(brainRoot, candidatePath, targetPath);
|
|
756
|
+
propagateToShared(brainRoot, targetPath);
|
|
757
|
+
promoted.push(targetPath);
|
|
758
|
+
} else if (now - mtime > decayMs) {
|
|
759
|
+
rmSync(neuronDir, { recursive: true, force: true });
|
|
760
|
+
decayed.push(candidatePath);
|
|
761
|
+
console.log(`\u{1F480} candidate decayed: ${candidatePath}`);
|
|
762
|
+
}
|
|
763
|
+
});
|
|
764
|
+
}
|
|
765
|
+
return { promoted, decayed };
|
|
766
|
+
}
|
|
767
|
+
function listCandidates(brainRoot) {
|
|
768
|
+
const results = [];
|
|
769
|
+
const now = Date.now();
|
|
770
|
+
for (const region of REGIONS) {
|
|
771
|
+
const candidateRoot = join6(brainRoot, region, CANDIDATE_SEGMENT);
|
|
772
|
+
walkNeuronDirs(candidateRoot, (neuronDir) => {
|
|
773
|
+
const rel = relative3(join6(brainRoot, region), neuronDir);
|
|
774
|
+
const candidatePath = `${region}/${rel}`;
|
|
775
|
+
const targetPath = fromCandidatePath(candidatePath);
|
|
776
|
+
const counter = readCounter(neuronDir);
|
|
777
|
+
const mtime = statSync2(neuronDir).mtimeMs;
|
|
778
|
+
const daysInactive = Math.floor((now - mtime) / (24 * 60 * 60 * 1e3));
|
|
779
|
+
results.push({ candidatePath, targetPath, counter, daysInactive });
|
|
780
|
+
});
|
|
781
|
+
}
|
|
782
|
+
return results;
|
|
783
|
+
}
|
|
784
|
+
function walkNeuronDirs(dir, cb) {
|
|
785
|
+
if (!existsSync7(dir)) return;
|
|
786
|
+
try {
|
|
787
|
+
const entries = readdirSync6(dir, { withFileTypes: true });
|
|
788
|
+
const hasNeuron = entries.some((e) => e.isFile() && e.name.endsWith(".neuron"));
|
|
789
|
+
if (hasNeuron) {
|
|
790
|
+
cb(dir);
|
|
791
|
+
return;
|
|
792
|
+
}
|
|
793
|
+
for (const entry of entries) {
|
|
794
|
+
if (entry.isDirectory() && !entry.name.startsWith(".")) {
|
|
795
|
+
walkNeuronDirs(join6(dir, entry.name), cb);
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
} catch {
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
function readCounter(dir) {
|
|
802
|
+
try {
|
|
803
|
+
const files = readdirSync6(dir).filter((f) => /^\d+\.neuron$/.test(f));
|
|
804
|
+
if (files.length === 0) return 0;
|
|
805
|
+
return Math.max(...files.map((f) => parseInt(f, 10)));
|
|
806
|
+
} catch {
|
|
807
|
+
return 0;
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
function propagateToShared(brainRoot, targetPath) {
|
|
811
|
+
try {
|
|
812
|
+
const agentsIdx = brainRoot.indexOf("/agents/");
|
|
813
|
+
if (agentsIdx === -1) return false;
|
|
814
|
+
const multiBrainRoot = brainRoot.slice(0, agentsIdx);
|
|
815
|
+
const sharedRoot = join6(multiBrainRoot, "shared");
|
|
816
|
+
if (!existsSync7(sharedRoot)) return false;
|
|
817
|
+
const episodes = readEpisodes(brainRoot);
|
|
818
|
+
const neuronName = targetPath.split("/").pop() || "";
|
|
819
|
+
const hasRelevantEpisode = episodes.some(
|
|
820
|
+
(ep) => PROPAGATION_EPISODE_TYPES.includes(ep.type) && (ep.path.includes(neuronName) || ep.detail.includes(neuronName))
|
|
821
|
+
);
|
|
822
|
+
if (!hasRelevantEpisode) return false;
|
|
823
|
+
growNeuron(sharedRoot, targetPath);
|
|
824
|
+
console.log(` \u{1F4E1} propagated to shared: ${targetPath}`);
|
|
825
|
+
return true;
|
|
826
|
+
} catch {
|
|
827
|
+
return false;
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
var CANDIDATE_THRESHOLD, CANDIDATE_DECAY_DAYS, CANDIDATE_SEGMENT;
|
|
831
|
+
var init_candidates = __esm({
|
|
832
|
+
"src/candidates.ts"() {
|
|
833
|
+
"use strict";
|
|
834
|
+
init_constants();
|
|
835
|
+
init_grow();
|
|
836
|
+
init_fire();
|
|
837
|
+
init_episode();
|
|
838
|
+
CANDIDATE_THRESHOLD = 3;
|
|
839
|
+
CANDIDATE_DECAY_DAYS = 14;
|
|
840
|
+
CANDIDATE_SEGMENT = "_candidates";
|
|
841
|
+
}
|
|
842
|
+
});
|
|
843
|
+
|
|
438
844
|
// src/emit.ts
|
|
439
845
|
var emit_exports = {};
|
|
440
846
|
__export(emit_exports, {
|
|
@@ -445,9 +851,9 @@ __export(emit_exports, {
|
|
|
445
851
|
printDiag: () => printDiag,
|
|
446
852
|
writeAllTiers: () => writeAllTiers
|
|
447
853
|
});
|
|
448
|
-
import { existsSync as
|
|
449
|
-
import { join as
|
|
450
|
-
function emitBootstrap(result, brain) {
|
|
854
|
+
import { existsSync as existsSync8, readFileSync as readFileSync4, writeFileSync as writeFileSync5, mkdirSync as mkdirSync6 } from "fs";
|
|
855
|
+
import { join as join7, dirname as dirname3 } from "path";
|
|
856
|
+
function emitBootstrap(result, brain, brainRoot) {
|
|
451
857
|
const lines = [];
|
|
452
858
|
const now = (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d+Z$/, "");
|
|
453
859
|
lines.push(MARKER_START);
|
|
@@ -503,6 +909,19 @@ function emitBootstrap(result, brain) {
|
|
|
503
909
|
lines.push(`| ${icon} ${region.name} | ${active.length} | ${activation} |`);
|
|
504
910
|
}
|
|
505
911
|
lines.push("");
|
|
912
|
+
if (brainRoot) {
|
|
913
|
+
const candidates = listCandidates(brainRoot);
|
|
914
|
+
if (candidates.length > 0) {
|
|
915
|
+
const top = candidates.slice(0, 5);
|
|
916
|
+
lines.push("### Provisional Rules (evaluating)");
|
|
917
|
+
lines.push("Follow these during this session. They graduate after 3 sessions.");
|
|
918
|
+
for (const c of top) {
|
|
919
|
+
const bar = "\u2588".repeat(c.counter) + "\u2591".repeat(Math.max(0, 3 - c.counter));
|
|
920
|
+
lines.push(`- ${bar} ${pathToSentence(c.targetPath)}`);
|
|
921
|
+
}
|
|
922
|
+
lines.push("");
|
|
923
|
+
}
|
|
924
|
+
}
|
|
506
925
|
lines.push(MARKER_END);
|
|
507
926
|
return lines.join("\n");
|
|
508
927
|
}
|
|
@@ -597,7 +1016,7 @@ function emitRegionRules(region) {
|
|
|
597
1016
|
function emitToTarget(brainRoot, target) {
|
|
598
1017
|
const brain = scanBrain(brainRoot);
|
|
599
1018
|
const result = runSubsumption(brain);
|
|
600
|
-
const content = emitBootstrap(result, brain);
|
|
1019
|
+
const content = emitBootstrap(result, brain, brainRoot);
|
|
601
1020
|
if (target === "all") {
|
|
602
1021
|
for (const [name, filePath] of Object.entries(EMIT_TARGETS)) {
|
|
603
1022
|
writeTarget(filePath, content);
|
|
@@ -613,33 +1032,33 @@ function emitToTarget(brainRoot, target) {
|
|
|
613
1032
|
}
|
|
614
1033
|
function writeAllTiers(brainRoot, result, brain) {
|
|
615
1034
|
const indexContent = emitIndex(result, brain);
|
|
616
|
-
|
|
1035
|
+
writeFileSync5(join7(brainRoot, "_index.md"), indexContent, "utf8");
|
|
617
1036
|
for (const region of result.activeRegions) {
|
|
618
|
-
if (
|
|
1037
|
+
if (existsSync8(region.path)) {
|
|
619
1038
|
const rulesContent = emitRegionRules(region);
|
|
620
|
-
|
|
1039
|
+
writeFileSync5(join7(region.path, "_rules.md"), rulesContent, "utf8");
|
|
621
1040
|
}
|
|
622
1041
|
}
|
|
623
1042
|
}
|
|
624
1043
|
function writeTarget(filePath, content) {
|
|
625
|
-
const dir =
|
|
626
|
-
if (dir !== "." && !
|
|
627
|
-
|
|
1044
|
+
const dir = dirname3(filePath);
|
|
1045
|
+
if (dir !== "." && !existsSync8(dir)) {
|
|
1046
|
+
mkdirSync6(dir, { recursive: true });
|
|
628
1047
|
}
|
|
629
|
-
if (
|
|
630
|
-
const existing =
|
|
1048
|
+
if (existsSync8(filePath)) {
|
|
1049
|
+
const existing = readFileSync4(filePath, "utf8");
|
|
631
1050
|
const startIdx = existing.indexOf(MARKER_START);
|
|
632
1051
|
const endIdx = existing.indexOf(MARKER_END);
|
|
633
1052
|
if (startIdx !== -1 && endIdx !== -1) {
|
|
634
1053
|
const before = existing.slice(0, startIdx);
|
|
635
1054
|
const after = existing.slice(endIdx + MARKER_END.length);
|
|
636
|
-
|
|
1055
|
+
writeFileSync5(filePath, before + content + after, "utf8");
|
|
637
1056
|
return;
|
|
638
1057
|
}
|
|
639
|
-
|
|
1058
|
+
writeFileSync5(filePath, content + "\n\n" + existing, "utf8");
|
|
640
1059
|
return;
|
|
641
1060
|
}
|
|
642
|
-
|
|
1061
|
+
writeFileSync5(filePath, content, "utf8");
|
|
643
1062
|
}
|
|
644
1063
|
function printDiag(brain, result) {
|
|
645
1064
|
console.log("");
|
|
@@ -688,6 +1107,7 @@ var init_emit = __esm({
|
|
|
688
1107
|
init_scanner();
|
|
689
1108
|
init_subsumption();
|
|
690
1109
|
init_constants();
|
|
1110
|
+
init_candidates();
|
|
691
1111
|
}
|
|
692
1112
|
});
|
|
693
1113
|
|
|
@@ -697,19 +1117,19 @@ __export(update_check_exports, {
|
|
|
697
1117
|
checkForUpdates: () => checkForUpdates,
|
|
698
1118
|
formatUpdateBanner: () => formatUpdateBanner
|
|
699
1119
|
});
|
|
700
|
-
import { existsSync as
|
|
701
|
-
import { join as
|
|
1120
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync7, readFileSync as readFileSync5, writeFileSync as writeFileSync6, statSync as statSync3, unlinkSync } from "fs";
|
|
1121
|
+
import { join as join8 } from "path";
|
|
702
1122
|
function getStateDir() {
|
|
703
|
-
return
|
|
1123
|
+
return join8(process.env.HOME || "~", ".hebbian");
|
|
704
1124
|
}
|
|
705
1125
|
function ensureStateDir(stateDir) {
|
|
706
|
-
if (!
|
|
707
|
-
|
|
1126
|
+
if (!existsSync9(stateDir)) {
|
|
1127
|
+
mkdirSync7(stateDir, { recursive: true });
|
|
708
1128
|
}
|
|
709
1129
|
}
|
|
710
1130
|
function isCacheStale(cachePath, type) {
|
|
711
1131
|
try {
|
|
712
|
-
const mtime =
|
|
1132
|
+
const mtime = statSync3(cachePath).mtimeMs;
|
|
713
1133
|
const ageMinutes = (Date.now() - mtime) / 1e3 / 60;
|
|
714
1134
|
const ttl = type === "UP_TO_DATE" ? TTL_UP_TO_DATE : TTL_UPGRADE_AVAILABLE;
|
|
715
1135
|
return ageMinutes > ttl;
|
|
@@ -718,10 +1138,10 @@ function isCacheStale(cachePath, type) {
|
|
|
718
1138
|
}
|
|
719
1139
|
}
|
|
720
1140
|
function readCache(stateDir) {
|
|
721
|
-
const cachePath =
|
|
722
|
-
if (!
|
|
1141
|
+
const cachePath = join8(stateDir, "last-update-check");
|
|
1142
|
+
if (!existsSync9(cachePath)) return null;
|
|
723
1143
|
try {
|
|
724
|
-
const line =
|
|
1144
|
+
const line = readFileSync5(cachePath, "utf8").trim();
|
|
725
1145
|
if (line.startsWith("UP_TO_DATE")) {
|
|
726
1146
|
if (isCacheStale(cachePath, "UP_TO_DATE")) return null;
|
|
727
1147
|
const ver = line.split(/\s+/)[1];
|
|
@@ -739,13 +1159,13 @@ function readCache(stateDir) {
|
|
|
739
1159
|
}
|
|
740
1160
|
function writeCache(stateDir, line) {
|
|
741
1161
|
ensureStateDir(stateDir);
|
|
742
|
-
|
|
1162
|
+
writeFileSync6(join8(stateDir, "last-update-check"), line, "utf8");
|
|
743
1163
|
}
|
|
744
1164
|
function isSnoozed(stateDir, remoteVersion) {
|
|
745
|
-
const snoozePath =
|
|
746
|
-
if (!
|
|
1165
|
+
const snoozePath = join8(stateDir, "update-snoozed");
|
|
1166
|
+
if (!existsSync9(snoozePath)) return false;
|
|
747
1167
|
try {
|
|
748
|
-
const [ver, levelStr, epochStr] =
|
|
1168
|
+
const [ver, levelStr, epochStr] = readFileSync5(snoozePath, "utf8").trim().split(/\s+/);
|
|
749
1169
|
if (ver !== remoteVersion) {
|
|
750
1170
|
unlinkSync(snoozePath);
|
|
751
1171
|
return false;
|
|
@@ -793,229 +1213,47 @@ async function checkForUpdates(currentVersion) {
|
|
|
793
1213
|
}
|
|
794
1214
|
}
|
|
795
1215
|
}
|
|
796
|
-
const latest = await fetchLatestVersion();
|
|
797
|
-
if (!latest) {
|
|
798
|
-
writeCache(stateDir, `UP_TO_DATE ${currentVersion}`);
|
|
799
|
-
return { type: "up_to_date" };
|
|
800
|
-
}
|
|
801
|
-
if (latest === currentVersion) {
|
|
802
|
-
writeCache(stateDir, `UP_TO_DATE ${currentVersion}`);
|
|
803
|
-
return { type: "up_to_date" };
|
|
804
|
-
}
|
|
805
|
-
writeCache(stateDir, `UPGRADE_AVAILABLE ${currentVersion} ${latest}`);
|
|
806
|
-
if (isSnoozed(stateDir, latest)) {
|
|
807
|
-
return { type: "up_to_date" };
|
|
808
|
-
}
|
|
809
|
-
return { type: "upgrade_available", current: currentVersion, latest };
|
|
810
|
-
}
|
|
811
|
-
function formatUpdateBanner(status) {
|
|
812
|
-
if (status.type !== "upgrade_available") return null;
|
|
813
|
-
return [
|
|
814
|
-
``,
|
|
815
|
-
` \u26A1 hebbian v${status.latest} available (current: v${status.current})`,
|
|
816
|
-
` npm i -g hebbian@latest`,
|
|
817
|
-
``
|
|
818
|
-
].join("\n");
|
|
819
|
-
}
|
|
820
|
-
var PACKAGE_NAME, NPM_REGISTRY_URL, FETCH_TIMEOUT_MS, TTL_UP_TO_DATE, TTL_UPGRADE_AVAILABLE, SNOOZE_DURATIONS;
|
|
821
|
-
var init_update_check = __esm({
|
|
822
|
-
"src/update-check.ts"() {
|
|
823
|
-
"use strict";
|
|
824
|
-
PACKAGE_NAME = "hebbian";
|
|
825
|
-
NPM_REGISTRY_URL = `https://registry.npmjs.org/${PACKAGE_NAME}/latest`;
|
|
826
|
-
FETCH_TIMEOUT_MS = 5e3;
|
|
827
|
-
TTL_UP_TO_DATE = 60;
|
|
828
|
-
TTL_UPGRADE_AVAILABLE = 720;
|
|
829
|
-
SNOOZE_DURATIONS = {
|
|
830
|
-
1: 86400,
|
|
831
|
-
// 24h
|
|
832
|
-
2: 172800,
|
|
833
|
-
// 48h
|
|
834
|
-
3: 604800
|
|
835
|
-
// 7d (and beyond)
|
|
836
|
-
};
|
|
837
|
-
}
|
|
838
|
-
});
|
|
839
|
-
|
|
840
|
-
// src/fire.ts
|
|
841
|
-
var fire_exports = {};
|
|
842
|
-
__export(fire_exports, {
|
|
843
|
-
contraNeuron: () => contraNeuron,
|
|
844
|
-
fireNeuron: () => fireNeuron,
|
|
845
|
-
getCurrentContra: () => getCurrentContra,
|
|
846
|
-
getCurrentCounter: () => getCurrentCounter
|
|
847
|
-
});
|
|
848
|
-
import { readdirSync as readdirSync3, renameSync, writeFileSync as writeFileSync4, existsSync as existsSync6, mkdirSync as mkdirSync4 } from "fs";
|
|
849
|
-
import { join as join5 } from "path";
|
|
850
|
-
function fireNeuron(brainRoot, neuronPath) {
|
|
851
|
-
const fullPath = join5(brainRoot, neuronPath);
|
|
852
|
-
if (!existsSync6(fullPath)) {
|
|
853
|
-
mkdirSync4(fullPath, { recursive: true });
|
|
854
|
-
writeFileSync4(join5(fullPath, "1.neuron"), "", "utf8");
|
|
855
|
-
console.log(`\u{1F331} grew + fired: ${neuronPath} (1)`);
|
|
856
|
-
return 1;
|
|
857
|
-
}
|
|
858
|
-
const current = getCurrentCounter(fullPath);
|
|
859
|
-
const newCounter = current + 1;
|
|
860
|
-
if (current > 0) {
|
|
861
|
-
renameSync(join5(fullPath, `${current}.neuron`), join5(fullPath, `${newCounter}.neuron`));
|
|
862
|
-
} else {
|
|
863
|
-
writeFileSync4(join5(fullPath, `${newCounter}.neuron`), "", "utf8");
|
|
864
|
-
}
|
|
865
|
-
console.log(`\u{1F525} fired: ${neuronPath} (${current} \u2192 ${newCounter})`);
|
|
866
|
-
return newCounter;
|
|
867
|
-
}
|
|
868
|
-
function contraNeuron(brainRoot, neuronPath) {
|
|
869
|
-
const fullPath = join5(brainRoot, neuronPath);
|
|
870
|
-
if (!existsSync6(fullPath)) {
|
|
871
|
-
return 0;
|
|
872
|
-
}
|
|
873
|
-
const current = getCurrentContra(fullPath);
|
|
874
|
-
const newContra = current + 1;
|
|
875
|
-
if (current > 0) {
|
|
876
|
-
renameSync(join5(fullPath, `${current}.contra`), join5(fullPath, `${newContra}.contra`));
|
|
877
|
-
} else {
|
|
878
|
-
writeFileSync4(join5(fullPath, `${newContra}.contra`), "", "utf8");
|
|
879
|
-
}
|
|
880
|
-
return newContra;
|
|
881
|
-
}
|
|
882
|
-
function getCurrentContra(dir) {
|
|
883
|
-
let max = 0;
|
|
884
|
-
try {
|
|
885
|
-
for (const entry of readdirSync3(dir)) {
|
|
886
|
-
if (entry.endsWith(".contra")) {
|
|
887
|
-
const n = parseInt(entry, 10);
|
|
888
|
-
if (!isNaN(n) && n > max) max = n;
|
|
889
|
-
}
|
|
890
|
-
}
|
|
891
|
-
} catch {
|
|
892
|
-
}
|
|
893
|
-
return max;
|
|
894
|
-
}
|
|
895
|
-
function getCurrentCounter(dir) {
|
|
896
|
-
let max = 0;
|
|
897
|
-
try {
|
|
898
|
-
for (const entry of readdirSync3(dir)) {
|
|
899
|
-
if (entry.endsWith(".neuron") && !entry.startsWith("dopamine") && !entry.startsWith("memory") && entry !== "bomb.neuron") {
|
|
900
|
-
const n = parseInt(entry, 10);
|
|
901
|
-
if (!isNaN(n) && n > max) max = n;
|
|
902
|
-
}
|
|
903
|
-
}
|
|
904
|
-
} catch {
|
|
905
|
-
}
|
|
906
|
-
return max;
|
|
907
|
-
}
|
|
908
|
-
var init_fire = __esm({
|
|
909
|
-
"src/fire.ts"() {
|
|
910
|
-
"use strict";
|
|
911
|
-
}
|
|
912
|
-
});
|
|
913
|
-
|
|
914
|
-
// src/similarity.ts
|
|
915
|
-
function tokenize(name) {
|
|
916
|
-
return name.replace(/([a-z])([A-Z])/g, "$1_$2").replace(/[^a-zA-Z0-9\u3000-\u9FFF\uAC00-\uD7AF]+/g, " ").toLowerCase().split(" ").map(stem).filter((t) => t.length > 1);
|
|
917
|
-
}
|
|
918
|
-
function stem(word) {
|
|
919
|
-
const suffixes = ["ing", "tion", "sion", "ness", "ment", "able", "ible", "ful", "less", "ous", "ive", "ity", "ies", "ed", "er", "es", "ly", "al", "en"];
|
|
920
|
-
for (const suffix of suffixes) {
|
|
921
|
-
if (word.length > suffix.length + 2 && word.endsWith(suffix)) {
|
|
922
|
-
return word.slice(0, -suffix.length);
|
|
923
|
-
}
|
|
924
|
-
}
|
|
925
|
-
return word;
|
|
926
|
-
}
|
|
927
|
-
function jaccardSimilarity(a, b) {
|
|
928
|
-
if (a.length === 0 && b.length === 0) return 1;
|
|
929
|
-
if (a.length === 0 || b.length === 0) return 0;
|
|
930
|
-
const setA = new Set(a);
|
|
931
|
-
const setB = new Set(b);
|
|
932
|
-
let intersection = 0;
|
|
933
|
-
for (const item of setA) {
|
|
934
|
-
if (setB.has(item)) intersection++;
|
|
935
|
-
}
|
|
936
|
-
const union = (/* @__PURE__ */ new Set([...setA, ...setB])).size;
|
|
937
|
-
return intersection / union;
|
|
938
|
-
}
|
|
939
|
-
var init_similarity = __esm({
|
|
940
|
-
"src/similarity.ts"() {
|
|
941
|
-
"use strict";
|
|
942
|
-
}
|
|
943
|
-
});
|
|
944
|
-
|
|
945
|
-
// src/grow.ts
|
|
946
|
-
var grow_exports = {};
|
|
947
|
-
__export(grow_exports, {
|
|
948
|
-
growNeuron: () => growNeuron
|
|
949
|
-
});
|
|
950
|
-
import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync5, existsSync as existsSync7, readdirSync as readdirSync4 } from "fs";
|
|
951
|
-
import { join as join6, relative as relative2 } from "path";
|
|
952
|
-
function growNeuron(brainRoot, neuronPath) {
|
|
953
|
-
const fullPath = join6(brainRoot, neuronPath);
|
|
954
|
-
if (existsSync7(fullPath)) {
|
|
955
|
-
const counter = fireNeuron(brainRoot, neuronPath);
|
|
956
|
-
return { action: "fired", path: neuronPath, counter };
|
|
957
|
-
}
|
|
958
|
-
if (neuronPath.includes("..") || neuronPath.startsWith("/")) {
|
|
959
|
-
throw new Error(`Invalid neuron path: "${neuronPath}" (path traversal not allowed)`);
|
|
960
|
-
}
|
|
961
|
-
const parts = neuronPath.split("/");
|
|
962
|
-
const regionName = parts[0];
|
|
963
|
-
if (regionName !== SKILLS_DIR && !REGIONS.includes(regionName)) {
|
|
964
|
-
throw new Error(`Invalid region: ${regionName}. Valid: ${REGIONS.join(", ")}, ${SKILLS_DIR}`);
|
|
965
|
-
}
|
|
966
|
-
const leafName = parts[parts.length - 1];
|
|
967
|
-
const newPrefix = leafName.match(/^(NO|DO|MUST|WARN)_/)?.[1] || "";
|
|
968
|
-
const newStripped = leafName.replace(/^(NO|DO|MUST|WARN)_/, "");
|
|
969
|
-
const newTokens = tokenize(newStripped);
|
|
970
|
-
const regionPath = join6(brainRoot, regionName);
|
|
971
|
-
if (existsSync7(regionPath)) {
|
|
972
|
-
const match = findSimilar(regionPath, regionPath, newTokens, newPrefix);
|
|
973
|
-
if (match) {
|
|
974
|
-
const matchRelPath = regionName + "/" + relative2(regionPath, match);
|
|
975
|
-
console.log(`\u{1F504} consolidation: "${neuronPath}" \u2248 "${matchRelPath}" (firing existing)`);
|
|
976
|
-
const counter = fireNeuron(brainRoot, matchRelPath);
|
|
977
|
-
return { action: "fired", path: matchRelPath, counter };
|
|
978
|
-
}
|
|
979
|
-
}
|
|
980
|
-
mkdirSync5(fullPath, { recursive: true });
|
|
981
|
-
writeFileSync5(join6(fullPath, "1.neuron"), "", "utf8");
|
|
982
|
-
console.log(`\u{1F331} grew: ${neuronPath} (1)`);
|
|
983
|
-
return { action: "grew", path: neuronPath, counter: 1 };
|
|
984
|
-
}
|
|
985
|
-
function findSimilar(dir, regionRoot, targetTokens, targetPrefix) {
|
|
986
|
-
let entries;
|
|
987
|
-
try {
|
|
988
|
-
entries = readdirSync4(dir, { withFileTypes: true });
|
|
989
|
-
} catch {
|
|
990
|
-
return null;
|
|
991
|
-
}
|
|
992
|
-
const hasNeuron = entries.some((e) => e.isFile() && e.name.endsWith(".neuron"));
|
|
993
|
-
if (hasNeuron) {
|
|
994
|
-
const folderName = dir.split("/").pop() || "";
|
|
995
|
-
const existingPrefix = folderName.match(/^(NO|DO|MUST|WARN)_/)?.[1] || "";
|
|
996
|
-
const existingStripped = folderName.replace(/^(NO|DO|MUST|WARN)_/, "");
|
|
997
|
-
const existingTokens = tokenize(existingStripped);
|
|
998
|
-
const similarity = jaccardSimilarity(targetTokens, existingTokens);
|
|
999
|
-
if (targetPrefix !== existingPrefix && targetTokens.length <= 2) {
|
|
1000
|
-
} else if (similarity >= JACCARD_THRESHOLD) {
|
|
1001
|
-
return dir;
|
|
1002
|
-
}
|
|
1003
|
-
}
|
|
1004
|
-
for (const entry of entries) {
|
|
1005
|
-
if (entry.name.startsWith("_") || entry.name.startsWith(".")) continue;
|
|
1006
|
-
if (entry.isDirectory()) {
|
|
1007
|
-
const match = findSimilar(join6(dir, entry.name), regionRoot, targetTokens, targetPrefix);
|
|
1008
|
-
if (match) return match;
|
|
1009
|
-
}
|
|
1010
|
-
}
|
|
1011
|
-
return null;
|
|
1216
|
+
const latest = await fetchLatestVersion();
|
|
1217
|
+
if (!latest) {
|
|
1218
|
+
writeCache(stateDir, `UP_TO_DATE ${currentVersion}`);
|
|
1219
|
+
return { type: "up_to_date" };
|
|
1220
|
+
}
|
|
1221
|
+
if (latest === currentVersion) {
|
|
1222
|
+
writeCache(stateDir, `UP_TO_DATE ${currentVersion}`);
|
|
1223
|
+
return { type: "up_to_date" };
|
|
1224
|
+
}
|
|
1225
|
+
writeCache(stateDir, `UPGRADE_AVAILABLE ${currentVersion} ${latest}`);
|
|
1226
|
+
if (isSnoozed(stateDir, latest)) {
|
|
1227
|
+
return { type: "up_to_date" };
|
|
1228
|
+
}
|
|
1229
|
+
return { type: "upgrade_available", current: currentVersion, latest };
|
|
1012
1230
|
}
|
|
1013
|
-
|
|
1014
|
-
|
|
1231
|
+
function formatUpdateBanner(status) {
|
|
1232
|
+
if (status.type !== "upgrade_available") return null;
|
|
1233
|
+
return [
|
|
1234
|
+
``,
|
|
1235
|
+
` \u26A1 hebbian v${status.latest} available (current: v${status.current})`,
|
|
1236
|
+
` npm i -g hebbian@latest`,
|
|
1237
|
+
``
|
|
1238
|
+
].join("\n");
|
|
1239
|
+
}
|
|
1240
|
+
var PACKAGE_NAME, NPM_REGISTRY_URL, FETCH_TIMEOUT_MS, TTL_UP_TO_DATE, TTL_UPGRADE_AVAILABLE, SNOOZE_DURATIONS;
|
|
1241
|
+
var init_update_check = __esm({
|
|
1242
|
+
"src/update-check.ts"() {
|
|
1015
1243
|
"use strict";
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1244
|
+
PACKAGE_NAME = "hebbian";
|
|
1245
|
+
NPM_REGISTRY_URL = `https://registry.npmjs.org/${PACKAGE_NAME}/latest`;
|
|
1246
|
+
FETCH_TIMEOUT_MS = 5e3;
|
|
1247
|
+
TTL_UP_TO_DATE = 60;
|
|
1248
|
+
TTL_UPGRADE_AVAILABLE = 720;
|
|
1249
|
+
SNOOZE_DURATIONS = {
|
|
1250
|
+
1: 86400,
|
|
1251
|
+
// 24h
|
|
1252
|
+
2: 172800,
|
|
1253
|
+
// 48h
|
|
1254
|
+
3: 604800
|
|
1255
|
+
// 7d (and beyond)
|
|
1256
|
+
};
|
|
1019
1257
|
}
|
|
1020
1258
|
});
|
|
1021
1259
|
|
|
@@ -1024,10 +1262,10 @@ var rollback_exports = {};
|
|
|
1024
1262
|
__export(rollback_exports, {
|
|
1025
1263
|
rollbackNeuron: () => rollbackNeuron
|
|
1026
1264
|
});
|
|
1027
|
-
import { renameSync as
|
|
1028
|
-
import { join as
|
|
1265
|
+
import { renameSync as renameSync3 } from "fs";
|
|
1266
|
+
import { join as join9 } from "path";
|
|
1029
1267
|
function rollbackNeuron(brainRoot, neuronPath) {
|
|
1030
|
-
const fullPath =
|
|
1268
|
+
const fullPath = join9(brainRoot, neuronPath);
|
|
1031
1269
|
const current = getCurrentCounter(fullPath);
|
|
1032
1270
|
if (current === 0) {
|
|
1033
1271
|
throw new Error(`Neuron not found: ${neuronPath}`);
|
|
@@ -1036,7 +1274,7 @@ function rollbackNeuron(brainRoot, neuronPath) {
|
|
|
1036
1274
|
throw new Error(`Counter already at minimum (1): ${neuronPath}`);
|
|
1037
1275
|
}
|
|
1038
1276
|
const newCounter = current - 1;
|
|
1039
|
-
|
|
1277
|
+
renameSync3(join9(fullPath, `${current}.neuron`), join9(fullPath, `${newCounter}.neuron`));
|
|
1040
1278
|
console.log(`\u23EA rollback: ${neuronPath} (${current} \u2192 ${newCounter})`);
|
|
1041
1279
|
return newCounter;
|
|
1042
1280
|
}
|
|
@@ -1052,31 +1290,31 @@ var signal_exports = {};
|
|
|
1052
1290
|
__export(signal_exports, {
|
|
1053
1291
|
signalNeuron: () => signalNeuron
|
|
1054
1292
|
});
|
|
1055
|
-
import { writeFileSync as
|
|
1056
|
-
import { join as
|
|
1293
|
+
import { writeFileSync as writeFileSync7, existsSync as existsSync10, readdirSync as readdirSync7 } from "fs";
|
|
1294
|
+
import { join as join10 } from "path";
|
|
1057
1295
|
function signalNeuron(brainRoot, neuronPath, signalType) {
|
|
1058
1296
|
if (!SIGNAL_TYPES.includes(signalType)) {
|
|
1059
1297
|
throw new Error(`Invalid signal type: ${signalType}. Valid: ${SIGNAL_TYPES.join(", ")}`);
|
|
1060
1298
|
}
|
|
1061
|
-
const fullPath =
|
|
1062
|
-
if (!
|
|
1299
|
+
const fullPath = join10(brainRoot, neuronPath);
|
|
1300
|
+
if (!existsSync10(fullPath)) {
|
|
1063
1301
|
throw new Error(`Neuron not found: ${neuronPath}`);
|
|
1064
1302
|
}
|
|
1065
1303
|
switch (signalType) {
|
|
1066
1304
|
case "bomb": {
|
|
1067
|
-
|
|
1305
|
+
writeFileSync7(join10(fullPath, "bomb.neuron"), "", "utf8");
|
|
1068
1306
|
console.log(`\u{1F4A3} bomb planted: ${neuronPath}`);
|
|
1069
1307
|
break;
|
|
1070
1308
|
}
|
|
1071
1309
|
case "dopamine": {
|
|
1072
1310
|
const level = getNextSignalLevel(fullPath, "dopamine");
|
|
1073
|
-
|
|
1311
|
+
writeFileSync7(join10(fullPath, `dopamine${level}.neuron`), "", "utf8");
|
|
1074
1312
|
console.log(`\u{1F7E2} dopamine +${level}: ${neuronPath}`);
|
|
1075
1313
|
break;
|
|
1076
1314
|
}
|
|
1077
1315
|
case "memory": {
|
|
1078
1316
|
const level = getNextSignalLevel(fullPath, "memory");
|
|
1079
|
-
|
|
1317
|
+
writeFileSync7(join10(fullPath, `memory${level}.neuron`), "", "utf8");
|
|
1080
1318
|
console.log(`\u{1F4BE} memory +${level}: ${neuronPath}`);
|
|
1081
1319
|
break;
|
|
1082
1320
|
}
|
|
@@ -1085,7 +1323,7 @@ function signalNeuron(brainRoot, neuronPath, signalType) {
|
|
|
1085
1323
|
function getNextSignalLevel(dir, prefix) {
|
|
1086
1324
|
let max = 0;
|
|
1087
1325
|
try {
|
|
1088
|
-
for (const entry of
|
|
1326
|
+
for (const entry of readdirSync7(dir)) {
|
|
1089
1327
|
if (entry.startsWith(prefix) && entry.endsWith(".neuron")) {
|
|
1090
1328
|
const n = parseInt(entry.replace(prefix, ""), 10);
|
|
1091
1329
|
if (!isNaN(n) && n > max) max = n;
|
|
@@ -1107,15 +1345,15 @@ var decay_exports = {};
|
|
|
1107
1345
|
__export(decay_exports, {
|
|
1108
1346
|
runDecay: () => runDecay
|
|
1109
1347
|
});
|
|
1110
|
-
import { readdirSync as
|
|
1111
|
-
import { join as
|
|
1348
|
+
import { readdirSync as readdirSync8, statSync as statSync4, writeFileSync as writeFileSync8, existsSync as existsSync11 } from "fs";
|
|
1349
|
+
import { join as join11 } from "path";
|
|
1112
1350
|
function runDecay(brainRoot, days) {
|
|
1113
1351
|
const threshold = Date.now() - days * 24 * 60 * 60 * 1e3;
|
|
1114
1352
|
let scanned = 0;
|
|
1115
1353
|
let decayed = 0;
|
|
1116
1354
|
for (const regionName of REGIONS) {
|
|
1117
|
-
const regionPath =
|
|
1118
|
-
if (!
|
|
1355
|
+
const regionPath = join11(brainRoot, regionName);
|
|
1356
|
+
if (!existsSync11(regionPath)) continue;
|
|
1119
1357
|
const result = decayWalk(regionPath, threshold, 0);
|
|
1120
1358
|
scanned += result.scanned;
|
|
1121
1359
|
decayed += result.decayed;
|
|
@@ -1129,7 +1367,7 @@ function decayWalk(dir, threshold, depth) {
|
|
|
1129
1367
|
let decayed = 0;
|
|
1130
1368
|
let entries;
|
|
1131
1369
|
try {
|
|
1132
|
-
entries =
|
|
1370
|
+
entries = readdirSync8(dir, { withFileTypes: true });
|
|
1133
1371
|
} catch {
|
|
1134
1372
|
return { scanned: 0, decayed: 0 };
|
|
1135
1373
|
}
|
|
@@ -1141,7 +1379,7 @@ function decayWalk(dir, threshold, depth) {
|
|
|
1141
1379
|
if (entry.name.endsWith(".neuron")) {
|
|
1142
1380
|
hasNeuronFile = true;
|
|
1143
1381
|
try {
|
|
1144
|
-
const st =
|
|
1382
|
+
const st = statSync4(join11(dir, entry.name));
|
|
1145
1383
|
if (st.mtimeMs > latestMod) latestMod = st.mtimeMs;
|
|
1146
1384
|
} catch {
|
|
1147
1385
|
}
|
|
@@ -1155,8 +1393,8 @@ function decayWalk(dir, threshold, depth) {
|
|
|
1155
1393
|
scanned++;
|
|
1156
1394
|
if (!isDormant && latestMod < threshold) {
|
|
1157
1395
|
const age = Math.floor((Date.now() - latestMod) / (24 * 60 * 60 * 1e3));
|
|
1158
|
-
|
|
1159
|
-
|
|
1396
|
+
writeFileSync8(
|
|
1397
|
+
join11(dir, "decay.dormant"),
|
|
1160
1398
|
`Dormant since ${(/* @__PURE__ */ new Date()).toISOString()} (${age} days inactive)`,
|
|
1161
1399
|
"utf8"
|
|
1162
1400
|
);
|
|
@@ -1166,7 +1404,7 @@ function decayWalk(dir, threshold, depth) {
|
|
|
1166
1404
|
for (const entry of entries) {
|
|
1167
1405
|
if (entry.name.startsWith("_") || entry.name.startsWith(".")) continue;
|
|
1168
1406
|
if (entry.isDirectory()) {
|
|
1169
|
-
const sub = decayWalk(
|
|
1407
|
+
const sub = decayWalk(join11(dir, entry.name), threshold, depth + 1);
|
|
1170
1408
|
scanned += sub.scanned;
|
|
1171
1409
|
decayed += sub.decayed;
|
|
1172
1410
|
}
|
|
@@ -1185,8 +1423,8 @@ var dedup_exports = {};
|
|
|
1185
1423
|
__export(dedup_exports, {
|
|
1186
1424
|
runDedup: () => runDedup
|
|
1187
1425
|
});
|
|
1188
|
-
import { writeFileSync as
|
|
1189
|
-
import { join as
|
|
1426
|
+
import { writeFileSync as writeFileSync9 } from "fs";
|
|
1427
|
+
import { join as join12 } from "path";
|
|
1190
1428
|
function runDedup(brainRoot) {
|
|
1191
1429
|
const brain = scanBrain(brainRoot);
|
|
1192
1430
|
let scanned = 0;
|
|
@@ -1208,8 +1446,8 @@ function runDedup(brainRoot) {
|
|
|
1208
1446
|
const [keep, drop] = ni.counter >= nj.counter ? [ni, nj] : [nj, ni];
|
|
1209
1447
|
const relKeep = `${region.name}/${keep.path}`;
|
|
1210
1448
|
fireNeuron(brainRoot, relKeep);
|
|
1211
|
-
|
|
1212
|
-
|
|
1449
|
+
writeFileSync9(
|
|
1450
|
+
join12(drop.fullPath, "dedup.dormant"),
|
|
1213
1451
|
`Merged into ${keep.path} on ${(/* @__PURE__ */ new Date()).toISOString()}`,
|
|
1214
1452
|
"utf8"
|
|
1215
1453
|
);
|
|
@@ -1239,10 +1477,10 @@ __export(snapshot_exports, {
|
|
|
1239
1477
|
gitSnapshot: () => gitSnapshot
|
|
1240
1478
|
});
|
|
1241
1479
|
import { execSync } from "child_process";
|
|
1242
|
-
import { existsSync as
|
|
1243
|
-
import { join as
|
|
1480
|
+
import { existsSync as existsSync12 } from "fs";
|
|
1481
|
+
import { join as join13 } from "path";
|
|
1244
1482
|
function gitSnapshot(brainRoot) {
|
|
1245
|
-
if (!
|
|
1483
|
+
if (!existsSync12(join13(brainRoot, ".git"))) {
|
|
1246
1484
|
try {
|
|
1247
1485
|
execSync("git rev-parse --is-inside-work-tree", { cwd: brainRoot, stdio: "pipe" });
|
|
1248
1486
|
} catch {
|
|
@@ -1330,230 +1568,6 @@ var init_watch = __esm({
|
|
|
1330
1568
|
}
|
|
1331
1569
|
});
|
|
1332
1570
|
|
|
1333
|
-
// src/episode.ts
|
|
1334
|
-
var episode_exports = {};
|
|
1335
|
-
__export(episode_exports, {
|
|
1336
|
-
logEpisode: () => logEpisode,
|
|
1337
|
-
readEpisodes: () => readEpisodes
|
|
1338
|
-
});
|
|
1339
|
-
import { readdirSync as readdirSync7, readFileSync as readFileSync5, writeFileSync as writeFileSync9, mkdirSync as mkdirSync6, existsSync as existsSync11 } from "fs";
|
|
1340
|
-
import { join as join12 } from "path";
|
|
1341
|
-
function logEpisode(brainRoot, type, path, detail, extra) {
|
|
1342
|
-
const logDir = join12(brainRoot, SESSION_LOG_DIR);
|
|
1343
|
-
if (!existsSync11(logDir)) {
|
|
1344
|
-
mkdirSync6(logDir, { recursive: true });
|
|
1345
|
-
}
|
|
1346
|
-
const nextSlot = getNextSlot(logDir);
|
|
1347
|
-
const episode = {
|
|
1348
|
-
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1349
|
-
type,
|
|
1350
|
-
path,
|
|
1351
|
-
detail,
|
|
1352
|
-
...extra?.outcome ? { outcome: extra.outcome } : {},
|
|
1353
|
-
...extra?.neurons ? { neurons: extra.neurons } : {}
|
|
1354
|
-
};
|
|
1355
|
-
writeFileSync9(
|
|
1356
|
-
join12(logDir, `memory${nextSlot}.neuron`),
|
|
1357
|
-
JSON.stringify(episode),
|
|
1358
|
-
"utf8"
|
|
1359
|
-
);
|
|
1360
|
-
}
|
|
1361
|
-
function readEpisodes(brainRoot) {
|
|
1362
|
-
const logDir = join12(brainRoot, SESSION_LOG_DIR);
|
|
1363
|
-
if (!existsSync11(logDir)) return [];
|
|
1364
|
-
const episodes = [];
|
|
1365
|
-
let entries;
|
|
1366
|
-
try {
|
|
1367
|
-
entries = readdirSync7(logDir);
|
|
1368
|
-
} catch {
|
|
1369
|
-
return [];
|
|
1370
|
-
}
|
|
1371
|
-
for (const entry of entries) {
|
|
1372
|
-
if (!entry.startsWith("memory") || !entry.endsWith(".neuron")) continue;
|
|
1373
|
-
try {
|
|
1374
|
-
const content = readFileSync5(join12(logDir, entry), "utf8");
|
|
1375
|
-
if (content.trim()) {
|
|
1376
|
-
episodes.push(JSON.parse(content));
|
|
1377
|
-
}
|
|
1378
|
-
} catch {
|
|
1379
|
-
}
|
|
1380
|
-
}
|
|
1381
|
-
episodes.sort((a, b) => a.ts.localeCompare(b.ts));
|
|
1382
|
-
return episodes;
|
|
1383
|
-
}
|
|
1384
|
-
function getNextSlot(logDir) {
|
|
1385
|
-
let maxSlot = 0;
|
|
1386
|
-
try {
|
|
1387
|
-
for (const entry of readdirSync7(logDir)) {
|
|
1388
|
-
if (entry.startsWith("memory") && entry.endsWith(".neuron")) {
|
|
1389
|
-
const n = parseInt(entry.replace("memory", "").replace(".neuron", ""), 10);
|
|
1390
|
-
if (!isNaN(n) && n > maxSlot) maxSlot = n;
|
|
1391
|
-
}
|
|
1392
|
-
}
|
|
1393
|
-
} catch {
|
|
1394
|
-
}
|
|
1395
|
-
const next = maxSlot + 1;
|
|
1396
|
-
return next > MAX_EPISODES ? maxSlot % MAX_EPISODES + 1 : next;
|
|
1397
|
-
}
|
|
1398
|
-
var MAX_EPISODES, SESSION_LOG_DIR;
|
|
1399
|
-
var init_episode = __esm({
|
|
1400
|
-
"src/episode.ts"() {
|
|
1401
|
-
"use strict";
|
|
1402
|
-
MAX_EPISODES = 100;
|
|
1403
|
-
SESSION_LOG_DIR = "hippocampus/session_log";
|
|
1404
|
-
}
|
|
1405
|
-
});
|
|
1406
|
-
|
|
1407
|
-
// src/candidates.ts
|
|
1408
|
-
var candidates_exports = {};
|
|
1409
|
-
__export(candidates_exports, {
|
|
1410
|
-
CANDIDATE_DECAY_DAYS: () => CANDIDATE_DECAY_DAYS,
|
|
1411
|
-
CANDIDATE_THRESHOLD: () => CANDIDATE_THRESHOLD,
|
|
1412
|
-
fromCandidatePath: () => fromCandidatePath,
|
|
1413
|
-
growCandidate: () => growCandidate,
|
|
1414
|
-
listCandidates: () => listCandidates,
|
|
1415
|
-
promoteCandidates: () => promoteCandidates,
|
|
1416
|
-
propagateToShared: () => propagateToShared,
|
|
1417
|
-
toCandidatePath: () => toCandidatePath
|
|
1418
|
-
});
|
|
1419
|
-
import { existsSync as existsSync12, mkdirSync as mkdirSync7, readdirSync as readdirSync8, renameSync as renameSync3, rmSync, statSync as statSync4 } from "fs";
|
|
1420
|
-
import { join as join13, dirname as dirname3, relative as relative3 } from "path";
|
|
1421
|
-
function toCandidatePath(neuronPath) {
|
|
1422
|
-
const slash = neuronPath.indexOf("/");
|
|
1423
|
-
if (slash === -1) throw new Error(`Invalid neuron path (missing region): ${neuronPath}`);
|
|
1424
|
-
return `${neuronPath.slice(0, slash)}/${CANDIDATE_SEGMENT}/${neuronPath.slice(slash + 1)}`;
|
|
1425
|
-
}
|
|
1426
|
-
function fromCandidatePath(candidatePath) {
|
|
1427
|
-
return candidatePath.replace(`/${CANDIDATE_SEGMENT}/`, "/");
|
|
1428
|
-
}
|
|
1429
|
-
function growCandidate(brainRoot, neuronPath) {
|
|
1430
|
-
const candidatePath = toCandidatePath(neuronPath);
|
|
1431
|
-
const result = growNeuron(brainRoot, candidatePath);
|
|
1432
|
-
if (result.counter >= CANDIDATE_THRESHOLD) {
|
|
1433
|
-
const ok = moveCandidate(brainRoot, candidatePath, neuronPath);
|
|
1434
|
-
if (ok) propagateToShared(brainRoot, neuronPath);
|
|
1435
|
-
return { ...result, path: ok ? neuronPath : result.path, promoted: ok };
|
|
1436
|
-
}
|
|
1437
|
-
console.log(` \u{1F331} candidate (${result.counter}/${CANDIDATE_THRESHOLD}): ${candidatePath}`);
|
|
1438
|
-
return { ...result, promoted: false };
|
|
1439
|
-
}
|
|
1440
|
-
function moveCandidate(brainRoot, candidatePath, targetPath) {
|
|
1441
|
-
const src = join13(brainRoot, candidatePath);
|
|
1442
|
-
if (!existsSync12(src)) return false;
|
|
1443
|
-
const dst = join13(brainRoot, targetPath);
|
|
1444
|
-
if (existsSync12(dst)) {
|
|
1445
|
-
fireNeuron(brainRoot, targetPath);
|
|
1446
|
-
rmSync(src, { recursive: true, force: true });
|
|
1447
|
-
} else {
|
|
1448
|
-
mkdirSync7(dirname3(dst), { recursive: true });
|
|
1449
|
-
renameSync3(src, dst);
|
|
1450
|
-
}
|
|
1451
|
-
console.log(`\u{1F393} promoted: ${candidatePath} \u2192 ${targetPath}`);
|
|
1452
|
-
return true;
|
|
1453
|
-
}
|
|
1454
|
-
function promoteCandidates(brainRoot) {
|
|
1455
|
-
const promoted = [];
|
|
1456
|
-
const decayed = [];
|
|
1457
|
-
const decayMs = CANDIDATE_DECAY_DAYS * 24 * 60 * 60 * 1e3;
|
|
1458
|
-
const now = Date.now();
|
|
1459
|
-
for (const region of REGIONS) {
|
|
1460
|
-
const candidateRoot = join13(brainRoot, region, CANDIDATE_SEGMENT);
|
|
1461
|
-
walkNeuronDirs(candidateRoot, (neuronDir) => {
|
|
1462
|
-
const rel = relative3(join13(brainRoot, region), neuronDir);
|
|
1463
|
-
const candidatePath = `${region}/${rel}`;
|
|
1464
|
-
const targetPath = fromCandidatePath(candidatePath);
|
|
1465
|
-
const counter = readCounter(neuronDir);
|
|
1466
|
-
const mtime = statSync4(neuronDir).mtimeMs;
|
|
1467
|
-
if (counter >= CANDIDATE_THRESHOLD) {
|
|
1468
|
-
moveCandidate(brainRoot, candidatePath, targetPath);
|
|
1469
|
-
propagateToShared(brainRoot, targetPath);
|
|
1470
|
-
promoted.push(targetPath);
|
|
1471
|
-
} else if (now - mtime > decayMs) {
|
|
1472
|
-
rmSync(neuronDir, { recursive: true, force: true });
|
|
1473
|
-
decayed.push(candidatePath);
|
|
1474
|
-
console.log(`\u{1F480} candidate decayed: ${candidatePath}`);
|
|
1475
|
-
}
|
|
1476
|
-
});
|
|
1477
|
-
}
|
|
1478
|
-
return { promoted, decayed };
|
|
1479
|
-
}
|
|
1480
|
-
function listCandidates(brainRoot) {
|
|
1481
|
-
const results = [];
|
|
1482
|
-
const now = Date.now();
|
|
1483
|
-
for (const region of REGIONS) {
|
|
1484
|
-
const candidateRoot = join13(brainRoot, region, CANDIDATE_SEGMENT);
|
|
1485
|
-
walkNeuronDirs(candidateRoot, (neuronDir) => {
|
|
1486
|
-
const rel = relative3(join13(brainRoot, region), neuronDir);
|
|
1487
|
-
const candidatePath = `${region}/${rel}`;
|
|
1488
|
-
const targetPath = fromCandidatePath(candidatePath);
|
|
1489
|
-
const counter = readCounter(neuronDir);
|
|
1490
|
-
const mtime = statSync4(neuronDir).mtimeMs;
|
|
1491
|
-
const daysInactive = Math.floor((now - mtime) / (24 * 60 * 60 * 1e3));
|
|
1492
|
-
results.push({ candidatePath, targetPath, counter, daysInactive });
|
|
1493
|
-
});
|
|
1494
|
-
}
|
|
1495
|
-
return results;
|
|
1496
|
-
}
|
|
1497
|
-
function walkNeuronDirs(dir, cb) {
|
|
1498
|
-
if (!existsSync12(dir)) return;
|
|
1499
|
-
try {
|
|
1500
|
-
const entries = readdirSync8(dir, { withFileTypes: true });
|
|
1501
|
-
const hasNeuron = entries.some((e) => e.isFile() && e.name.endsWith(".neuron"));
|
|
1502
|
-
if (hasNeuron) {
|
|
1503
|
-
cb(dir);
|
|
1504
|
-
return;
|
|
1505
|
-
}
|
|
1506
|
-
for (const entry of entries) {
|
|
1507
|
-
if (entry.isDirectory() && !entry.name.startsWith(".")) {
|
|
1508
|
-
walkNeuronDirs(join13(dir, entry.name), cb);
|
|
1509
|
-
}
|
|
1510
|
-
}
|
|
1511
|
-
} catch {
|
|
1512
|
-
}
|
|
1513
|
-
}
|
|
1514
|
-
function readCounter(dir) {
|
|
1515
|
-
try {
|
|
1516
|
-
const files = readdirSync8(dir).filter((f) => /^\d+\.neuron$/.test(f));
|
|
1517
|
-
if (files.length === 0) return 0;
|
|
1518
|
-
return Math.max(...files.map((f) => parseInt(f, 10)));
|
|
1519
|
-
} catch {
|
|
1520
|
-
return 0;
|
|
1521
|
-
}
|
|
1522
|
-
}
|
|
1523
|
-
function propagateToShared(brainRoot, targetPath) {
|
|
1524
|
-
try {
|
|
1525
|
-
const agentsIdx = brainRoot.indexOf("/agents/");
|
|
1526
|
-
if (agentsIdx === -1) return false;
|
|
1527
|
-
const multiBrainRoot = brainRoot.slice(0, agentsIdx);
|
|
1528
|
-
const sharedRoot = join13(multiBrainRoot, "shared");
|
|
1529
|
-
if (!existsSync12(sharedRoot)) return false;
|
|
1530
|
-
const episodes = readEpisodes(brainRoot);
|
|
1531
|
-
const neuronName = targetPath.split("/").pop() || "";
|
|
1532
|
-
const hasRelevantEpisode = episodes.some(
|
|
1533
|
-
(ep) => PROPAGATION_EPISODE_TYPES.includes(ep.type) && (ep.path.includes(neuronName) || ep.detail.includes(neuronName))
|
|
1534
|
-
);
|
|
1535
|
-
if (!hasRelevantEpisode) return false;
|
|
1536
|
-
growNeuron(sharedRoot, targetPath);
|
|
1537
|
-
console.log(` \u{1F4E1} propagated to shared: ${targetPath}`);
|
|
1538
|
-
return true;
|
|
1539
|
-
} catch {
|
|
1540
|
-
return false;
|
|
1541
|
-
}
|
|
1542
|
-
}
|
|
1543
|
-
var CANDIDATE_THRESHOLD, CANDIDATE_DECAY_DAYS, CANDIDATE_SEGMENT;
|
|
1544
|
-
var init_candidates = __esm({
|
|
1545
|
-
"src/candidates.ts"() {
|
|
1546
|
-
"use strict";
|
|
1547
|
-
init_constants();
|
|
1548
|
-
init_grow();
|
|
1549
|
-
init_fire();
|
|
1550
|
-
init_episode();
|
|
1551
|
-
CANDIDATE_THRESHOLD = 3;
|
|
1552
|
-
CANDIDATE_DECAY_DAYS = 14;
|
|
1553
|
-
CANDIDATE_SEGMENT = "_candidates";
|
|
1554
|
-
}
|
|
1555
|
-
});
|
|
1556
|
-
|
|
1557
1571
|
// src/inbox.ts
|
|
1558
1572
|
var inbox_exports = {};
|
|
1559
1573
|
__export(inbox_exports, {
|
|
@@ -2225,7 +2239,8 @@ function digestTranscript(brainRoot, transcriptPath, sessionId) {
|
|
|
2225
2239
|
console.log(`\u{1F527} digest: ${toolFailures.length} tool failure(s), ${retries.length} retry pattern(s) logged`);
|
|
2226
2240
|
}
|
|
2227
2241
|
const corrections = extractCorrections(messages);
|
|
2228
|
-
|
|
2242
|
+
const fired = autoFireCandidates(brainRoot, corrections);
|
|
2243
|
+
if (corrections.length === 0 && toolFailures.length === 0 && fired === 0) {
|
|
2229
2244
|
console.log(`\u{1F4DD} digest: no corrections found in session ${resolvedSessionId}`);
|
|
2230
2245
|
writeAuditLog(brainRoot, resolvedSessionId, [], totalLines);
|
|
2231
2246
|
return { corrections: 0, skipped: messages.length, toolFailures: toolFailures.length, transcriptPath, sessionId: resolvedSessionId };
|
|
@@ -2373,12 +2388,15 @@ function extractCorrections(messages) {
|
|
|
2373
2388
|
const corrections = [];
|
|
2374
2389
|
for (const text of messages) {
|
|
2375
2390
|
if (corrections.length >= MAX_CORRECTIONS_PER_SESSION) break;
|
|
2391
|
+
const trimmed = text.trim();
|
|
2376
2392
|
if (text.length < MIN_CORRECTION_LENGTH) continue;
|
|
2377
|
-
if (/^[\/!]/.test(
|
|
2378
|
-
if (
|
|
2379
|
-
if (/^<[a-zA-Z]/.test(
|
|
2380
|
-
if (/^Base directory for this skill:/i.test(
|
|
2381
|
-
if (/^[•·▸▶\-\*]\s/.test(
|
|
2393
|
+
if (/^[\/!]/.test(trimmed)) continue;
|
|
2394
|
+
if (/[??]\s*[!!]?\s*$/.test(trimmed)) continue;
|
|
2395
|
+
if (/^<[a-zA-Z]/.test(trimmed)) continue;
|
|
2396
|
+
if (/^Base directory for this skill:/i.test(trimmed)) continue;
|
|
2397
|
+
if (/^[•·▸▶\-\*]\s/.test(trimmed)) continue;
|
|
2398
|
+
if (/<[a-zA-Z][a-zA-Z-]*>/.test(trimmed) && /<\/[a-zA-Z]/.test(trimmed)) continue;
|
|
2399
|
+
if (isNarrativeKorean(trimmed)) continue;
|
|
2382
2400
|
const correction = detectCorrection(text);
|
|
2383
2401
|
if (correction) {
|
|
2384
2402
|
corrections.push(correction);
|
|
@@ -2386,12 +2404,63 @@ function extractCorrections(messages) {
|
|
|
2386
2404
|
}
|
|
2387
2405
|
return corrections;
|
|
2388
2406
|
}
|
|
2407
|
+
function isNarrativeKorean(text) {
|
|
2408
|
+
const NARRATIVE_MARKERS = [
|
|
2409
|
+
/이유는/,
|
|
2410
|
+
// "the reason is..."
|
|
2411
|
+
/예를\s*들면/,
|
|
2412
|
+
// "for example..."
|
|
2413
|
+
/그래서/,
|
|
2414
|
+
// "so/therefore..."
|
|
2415
|
+
/그런데/,
|
|
2416
|
+
// "but then..."
|
|
2417
|
+
/왜냐하면/,
|
|
2418
|
+
// "because..."
|
|
2419
|
+
/거든/,
|
|
2420
|
+
// "...you see" (explanatory)
|
|
2421
|
+
/있었[던거어]/,
|
|
2422
|
+
// "there was..." (past narrative)
|
|
2423
|
+
/중인데/,
|
|
2424
|
+
// "...in the middle of" (ongoing situation)
|
|
2425
|
+
/거 같은데/,
|
|
2426
|
+
// "it seems like..." (speculation)
|
|
2427
|
+
/어떻게\s*생각/
|
|
2428
|
+
// "what do you think?" (asking opinion)
|
|
2429
|
+
];
|
|
2430
|
+
const markerCount = NARRATIVE_MARKERS.filter((p) => p.test(text)).length;
|
|
2431
|
+
return markerCount >= 2;
|
|
2432
|
+
}
|
|
2433
|
+
function autoFireCandidates(brainRoot, corrections) {
|
|
2434
|
+
if (corrections.length > 0) return 0;
|
|
2435
|
+
const candidates = listCandidates(brainRoot);
|
|
2436
|
+
if (candidates.length === 0) return 0;
|
|
2437
|
+
let fired = 0;
|
|
2438
|
+
for (const cand of candidates) {
|
|
2439
|
+
try {
|
|
2440
|
+
const newCounter = fireNeuron(brainRoot, cand.candidatePath);
|
|
2441
|
+
fired++;
|
|
2442
|
+
if (newCounter >= CANDIDATE_THRESHOLD) {
|
|
2443
|
+
growCandidate(brainRoot, cand.targetPath);
|
|
2444
|
+
}
|
|
2445
|
+
} catch {
|
|
2446
|
+
}
|
|
2447
|
+
}
|
|
2448
|
+
if (fired > 0) {
|
|
2449
|
+
console.log(`\u{1F504} agent-evaluator: ${fired} candidate(s) advanced (session without corrections)`);
|
|
2450
|
+
}
|
|
2451
|
+
return fired;
|
|
2452
|
+
}
|
|
2389
2453
|
function detectCorrection(text) {
|
|
2390
2454
|
const isNegation = NEGATION_PATTERNS.some((p) => p.test(text));
|
|
2391
2455
|
const isMust = MUST_PATTERNS.some((p) => p.test(text));
|
|
2392
2456
|
const isWarn = WARN_PATTERNS.some((p) => p.test(text));
|
|
2393
2457
|
const isAffirmation = AFFIRMATION_PATTERNS.some((p) => p.test(text));
|
|
2394
2458
|
if (!isNegation && !isMust && !isWarn && !isAffirmation) return null;
|
|
2459
|
+
const categories = [isNegation, isMust, isWarn, isAffirmation].filter(Boolean).length;
|
|
2460
|
+
const koreanRatio = (text.match(/[\uAC00-\uD7AF]/g) || []).length / Math.max(text.length, 1);
|
|
2461
|
+
if (koreanRatio > 0.3 && categories < 2) {
|
|
2462
|
+
if (text.length > 100) return null;
|
|
2463
|
+
}
|
|
2395
2464
|
let prefix;
|
|
2396
2465
|
if (isNegation) prefix = "NO";
|
|
2397
2466
|
else if (isMust) prefix = "MUST";
|
|
@@ -2582,6 +2651,7 @@ var init_digest = __esm({
|
|
|
2582
2651
|
"use strict";
|
|
2583
2652
|
init_constants();
|
|
2584
2653
|
init_candidates();
|
|
2654
|
+
init_fire();
|
|
2585
2655
|
init_episode();
|
|
2586
2656
|
NEGATION_PATTERNS = [
|
|
2587
2657
|
/\bdon[''\u2019]?t\b/i,
|
|
@@ -2590,24 +2660,21 @@ var init_digest = __esm({
|
|
|
2590
2660
|
/\bnever\b/i,
|
|
2591
2661
|
/\binstead\b/i,
|
|
2592
2662
|
/^no[,.\s!]/i,
|
|
2593
|
-
/\bdon[''\u2019]?t\s+use\b/i,
|
|
2594
2663
|
/\bavoid\b/i,
|
|
2595
|
-
// Korean negation —
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
/쓰지\s*않/
|
|
2603
|
-
// "do not use" specifically
|
|
2664
|
+
// Korean negation — require AI-directed imperative context:
|
|
2665
|
+
// "X하지 마" (don't X) — must have a verb object before 지 마
|
|
2666
|
+
/[을를은는도이가]\s*[가-힣]+지\s*마/,
|
|
2667
|
+
// "X 하면 안 돼" (must not X) — conditional + prohibition
|
|
2668
|
+
/하면\s*안\s*돼/,
|
|
2669
|
+
// "X 쓰지 마" (don't use X) — explicit "don't use"
|
|
2670
|
+
/쓰지\s*마/
|
|
2604
2671
|
];
|
|
2605
2672
|
AFFIRMATION_PATTERNS = [
|
|
2606
|
-
/\balways\b/i,
|
|
2607
2673
|
/\bshould\s+always\b/i,
|
|
2608
2674
|
/\buse\s+\w+\s+instead\b/i,
|
|
2609
|
-
// Korean affirmation
|
|
2610
|
-
|
|
2675
|
+
// Korean affirmation — require directive context
|
|
2676
|
+
/항상\s*[가-힣]+[해하]/
|
|
2677
|
+
// "항상 X해" (always do X)
|
|
2611
2678
|
];
|
|
2612
2679
|
MUST_PATTERNS = [
|
|
2613
2680
|
/\bmust\b/i,
|