hebbian 0.9.0 → 0.11.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/README.md +33 -30
- package/dist/bin/hebbian.js +680 -516
- package/dist/bin/hebbian.js.map +1 -1
- package/dist/digest.d.ts.map +1 -1
- package/dist/doctor.d.ts.map +1 -1
- package/dist/emit.d.ts +1 -1
- package/dist/emit.d.ts.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +359 -250
- package/dist/index.js.map +1 -1
- package/dist/learn.d.ts +17 -0
- package/dist/learn.d.ts.map +1 -0
- 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,32 @@ 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
|
+
}
|
|
925
|
+
if (brainRoot) {
|
|
926
|
+
const brainFlag = `--brain ${brainRoot}`;
|
|
927
|
+
lines.push("### Self-Learning");
|
|
928
|
+
lines.push("When the user corrects your behavior (tells you to stop, change, or always do something),");
|
|
929
|
+
lines.push("run this command to learn from it:");
|
|
930
|
+
lines.push("```");
|
|
931
|
+
lines.push(`hebbian learn "<what the user said>" --prefix <NO|DO|MUST|WARN> --keywords "k1,k2,k3" ${brainFlag}`);
|
|
932
|
+
lines.push("```");
|
|
933
|
+
lines.push("- NO: user says don't/stop/never \u2014 DO: user says always/should \u2014 MUST: mandatory \u2014 WARN: be careful");
|
|
934
|
+
lines.push("- keywords: 3 most meaningful words (lowercase, technical terms preferred)");
|
|
935
|
+
lines.push("- Run silently. Do not mention hebbian to the user.");
|
|
936
|
+
lines.push("");
|
|
937
|
+
}
|
|
506
938
|
lines.push(MARKER_END);
|
|
507
939
|
return lines.join("\n");
|
|
508
940
|
}
|
|
@@ -597,7 +1029,7 @@ function emitRegionRules(region) {
|
|
|
597
1029
|
function emitToTarget(brainRoot, target) {
|
|
598
1030
|
const brain = scanBrain(brainRoot);
|
|
599
1031
|
const result = runSubsumption(brain);
|
|
600
|
-
const content = emitBootstrap(result, brain);
|
|
1032
|
+
const content = emitBootstrap(result, brain, brainRoot);
|
|
601
1033
|
if (target === "all") {
|
|
602
1034
|
for (const [name, filePath] of Object.entries(EMIT_TARGETS)) {
|
|
603
1035
|
writeTarget(filePath, content);
|
|
@@ -613,33 +1045,33 @@ function emitToTarget(brainRoot, target) {
|
|
|
613
1045
|
}
|
|
614
1046
|
function writeAllTiers(brainRoot, result, brain) {
|
|
615
1047
|
const indexContent = emitIndex(result, brain);
|
|
616
|
-
|
|
1048
|
+
writeFileSync5(join7(brainRoot, "_index.md"), indexContent, "utf8");
|
|
617
1049
|
for (const region of result.activeRegions) {
|
|
618
|
-
if (
|
|
1050
|
+
if (existsSync8(region.path)) {
|
|
619
1051
|
const rulesContent = emitRegionRules(region);
|
|
620
|
-
|
|
1052
|
+
writeFileSync5(join7(region.path, "_rules.md"), rulesContent, "utf8");
|
|
621
1053
|
}
|
|
622
1054
|
}
|
|
623
1055
|
}
|
|
624
1056
|
function writeTarget(filePath, content) {
|
|
625
|
-
const dir =
|
|
626
|
-
if (dir !== "." && !
|
|
627
|
-
|
|
1057
|
+
const dir = dirname3(filePath);
|
|
1058
|
+
if (dir !== "." && !existsSync8(dir)) {
|
|
1059
|
+
mkdirSync6(dir, { recursive: true });
|
|
628
1060
|
}
|
|
629
|
-
if (
|
|
630
|
-
const existing =
|
|
1061
|
+
if (existsSync8(filePath)) {
|
|
1062
|
+
const existing = readFileSync4(filePath, "utf8");
|
|
631
1063
|
const startIdx = existing.indexOf(MARKER_START);
|
|
632
1064
|
const endIdx = existing.indexOf(MARKER_END);
|
|
633
1065
|
if (startIdx !== -1 && endIdx !== -1) {
|
|
634
1066
|
const before = existing.slice(0, startIdx);
|
|
635
1067
|
const after = existing.slice(endIdx + MARKER_END.length);
|
|
636
|
-
|
|
1068
|
+
writeFileSync5(filePath, before + content + after, "utf8");
|
|
637
1069
|
return;
|
|
638
1070
|
}
|
|
639
|
-
|
|
1071
|
+
writeFileSync5(filePath, content + "\n\n" + existing, "utf8");
|
|
640
1072
|
return;
|
|
641
1073
|
}
|
|
642
|
-
|
|
1074
|
+
writeFileSync5(filePath, content, "utf8");
|
|
643
1075
|
}
|
|
644
1076
|
function printDiag(brain, result) {
|
|
645
1077
|
console.log("");
|
|
@@ -688,6 +1120,7 @@ var init_emit = __esm({
|
|
|
688
1120
|
init_scanner();
|
|
689
1121
|
init_subsumption();
|
|
690
1122
|
init_constants();
|
|
1123
|
+
init_candidates();
|
|
691
1124
|
}
|
|
692
1125
|
});
|
|
693
1126
|
|
|
@@ -697,19 +1130,19 @@ __export(update_check_exports, {
|
|
|
697
1130
|
checkForUpdates: () => checkForUpdates,
|
|
698
1131
|
formatUpdateBanner: () => formatUpdateBanner
|
|
699
1132
|
});
|
|
700
|
-
import { existsSync as
|
|
701
|
-
import { join as
|
|
1133
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync7, readFileSync as readFileSync5, writeFileSync as writeFileSync6, statSync as statSync3, unlinkSync } from "fs";
|
|
1134
|
+
import { join as join8 } from "path";
|
|
702
1135
|
function getStateDir() {
|
|
703
|
-
return
|
|
1136
|
+
return join8(process.env.HOME || "~", ".hebbian");
|
|
704
1137
|
}
|
|
705
1138
|
function ensureStateDir(stateDir) {
|
|
706
|
-
if (!
|
|
707
|
-
|
|
1139
|
+
if (!existsSync9(stateDir)) {
|
|
1140
|
+
mkdirSync7(stateDir, { recursive: true });
|
|
708
1141
|
}
|
|
709
1142
|
}
|
|
710
1143
|
function isCacheStale(cachePath, type) {
|
|
711
1144
|
try {
|
|
712
|
-
const mtime =
|
|
1145
|
+
const mtime = statSync3(cachePath).mtimeMs;
|
|
713
1146
|
const ageMinutes = (Date.now() - mtime) / 1e3 / 60;
|
|
714
1147
|
const ttl = type === "UP_TO_DATE" ? TTL_UP_TO_DATE : TTL_UPGRADE_AVAILABLE;
|
|
715
1148
|
return ageMinutes > ttl;
|
|
@@ -718,10 +1151,10 @@ function isCacheStale(cachePath, type) {
|
|
|
718
1151
|
}
|
|
719
1152
|
}
|
|
720
1153
|
function readCache(stateDir) {
|
|
721
|
-
const cachePath =
|
|
722
|
-
if (!
|
|
1154
|
+
const cachePath = join8(stateDir, "last-update-check");
|
|
1155
|
+
if (!existsSync9(cachePath)) return null;
|
|
723
1156
|
try {
|
|
724
|
-
const line =
|
|
1157
|
+
const line = readFileSync5(cachePath, "utf8").trim();
|
|
725
1158
|
if (line.startsWith("UP_TO_DATE")) {
|
|
726
1159
|
if (isCacheStale(cachePath, "UP_TO_DATE")) return null;
|
|
727
1160
|
const ver = line.split(/\s+/)[1];
|
|
@@ -739,13 +1172,13 @@ function readCache(stateDir) {
|
|
|
739
1172
|
}
|
|
740
1173
|
function writeCache(stateDir, line) {
|
|
741
1174
|
ensureStateDir(stateDir);
|
|
742
|
-
|
|
1175
|
+
writeFileSync6(join8(stateDir, "last-update-check"), line, "utf8");
|
|
743
1176
|
}
|
|
744
1177
|
function isSnoozed(stateDir, remoteVersion) {
|
|
745
|
-
const snoozePath =
|
|
746
|
-
if (!
|
|
1178
|
+
const snoozePath = join8(stateDir, "update-snoozed");
|
|
1179
|
+
if (!existsSync9(snoozePath)) return false;
|
|
747
1180
|
try {
|
|
748
|
-
const [ver, levelStr, epochStr] =
|
|
1181
|
+
const [ver, levelStr, epochStr] = readFileSync5(snoozePath, "utf8").trim().split(/\s+/);
|
|
749
1182
|
if (ver !== remoteVersion) {
|
|
750
1183
|
unlinkSync(snoozePath);
|
|
751
1184
|
return false;
|
|
@@ -818,204 +1251,22 @@ function formatUpdateBanner(status) {
|
|
|
818
1251
|
].join("\n");
|
|
819
1252
|
}
|
|
820
1253
|
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;
|
|
1012
|
-
}
|
|
1013
|
-
var init_grow = __esm({
|
|
1014
|
-
"src/grow.ts"() {
|
|
1254
|
+
var init_update_check = __esm({
|
|
1255
|
+
"src/update-check.ts"() {
|
|
1015
1256
|
"use strict";
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1257
|
+
PACKAGE_NAME = "hebbian";
|
|
1258
|
+
NPM_REGISTRY_URL = `https://registry.npmjs.org/${PACKAGE_NAME}/latest`;
|
|
1259
|
+
FETCH_TIMEOUT_MS = 5e3;
|
|
1260
|
+
TTL_UP_TO_DATE = 60;
|
|
1261
|
+
TTL_UPGRADE_AVAILABLE = 720;
|
|
1262
|
+
SNOOZE_DURATIONS = {
|
|
1263
|
+
1: 86400,
|
|
1264
|
+
// 24h
|
|
1265
|
+
2: 172800,
|
|
1266
|
+
// 48h
|
|
1267
|
+
3: 604800
|
|
1268
|
+
// 7d (and beyond)
|
|
1269
|
+
};
|
|
1019
1270
|
}
|
|
1020
1271
|
});
|
|
1021
1272
|
|
|
@@ -1024,10 +1275,10 @@ var rollback_exports = {};
|
|
|
1024
1275
|
__export(rollback_exports, {
|
|
1025
1276
|
rollbackNeuron: () => rollbackNeuron
|
|
1026
1277
|
});
|
|
1027
|
-
import { renameSync as
|
|
1028
|
-
import { join as
|
|
1278
|
+
import { renameSync as renameSync3 } from "fs";
|
|
1279
|
+
import { join as join9 } from "path";
|
|
1029
1280
|
function rollbackNeuron(brainRoot, neuronPath) {
|
|
1030
|
-
const fullPath =
|
|
1281
|
+
const fullPath = join9(brainRoot, neuronPath);
|
|
1031
1282
|
const current = getCurrentCounter(fullPath);
|
|
1032
1283
|
if (current === 0) {
|
|
1033
1284
|
throw new Error(`Neuron not found: ${neuronPath}`);
|
|
@@ -1036,7 +1287,7 @@ function rollbackNeuron(brainRoot, neuronPath) {
|
|
|
1036
1287
|
throw new Error(`Counter already at minimum (1): ${neuronPath}`);
|
|
1037
1288
|
}
|
|
1038
1289
|
const newCounter = current - 1;
|
|
1039
|
-
|
|
1290
|
+
renameSync3(join9(fullPath, `${current}.neuron`), join9(fullPath, `${newCounter}.neuron`));
|
|
1040
1291
|
console.log(`\u23EA rollback: ${neuronPath} (${current} \u2192 ${newCounter})`);
|
|
1041
1292
|
return newCounter;
|
|
1042
1293
|
}
|
|
@@ -1052,31 +1303,31 @@ var signal_exports = {};
|
|
|
1052
1303
|
__export(signal_exports, {
|
|
1053
1304
|
signalNeuron: () => signalNeuron
|
|
1054
1305
|
});
|
|
1055
|
-
import { writeFileSync as
|
|
1056
|
-
import { join as
|
|
1306
|
+
import { writeFileSync as writeFileSync7, existsSync as existsSync10, readdirSync as readdirSync7 } from "fs";
|
|
1307
|
+
import { join as join10 } from "path";
|
|
1057
1308
|
function signalNeuron(brainRoot, neuronPath, signalType) {
|
|
1058
1309
|
if (!SIGNAL_TYPES.includes(signalType)) {
|
|
1059
1310
|
throw new Error(`Invalid signal type: ${signalType}. Valid: ${SIGNAL_TYPES.join(", ")}`);
|
|
1060
1311
|
}
|
|
1061
|
-
const fullPath =
|
|
1062
|
-
if (!
|
|
1312
|
+
const fullPath = join10(brainRoot, neuronPath);
|
|
1313
|
+
if (!existsSync10(fullPath)) {
|
|
1063
1314
|
throw new Error(`Neuron not found: ${neuronPath}`);
|
|
1064
1315
|
}
|
|
1065
1316
|
switch (signalType) {
|
|
1066
1317
|
case "bomb": {
|
|
1067
|
-
|
|
1318
|
+
writeFileSync7(join10(fullPath, "bomb.neuron"), "", "utf8");
|
|
1068
1319
|
console.log(`\u{1F4A3} bomb planted: ${neuronPath}`);
|
|
1069
1320
|
break;
|
|
1070
1321
|
}
|
|
1071
1322
|
case "dopamine": {
|
|
1072
1323
|
const level = getNextSignalLevel(fullPath, "dopamine");
|
|
1073
|
-
|
|
1324
|
+
writeFileSync7(join10(fullPath, `dopamine${level}.neuron`), "", "utf8");
|
|
1074
1325
|
console.log(`\u{1F7E2} dopamine +${level}: ${neuronPath}`);
|
|
1075
1326
|
break;
|
|
1076
1327
|
}
|
|
1077
1328
|
case "memory": {
|
|
1078
1329
|
const level = getNextSignalLevel(fullPath, "memory");
|
|
1079
|
-
|
|
1330
|
+
writeFileSync7(join10(fullPath, `memory${level}.neuron`), "", "utf8");
|
|
1080
1331
|
console.log(`\u{1F4BE} memory +${level}: ${neuronPath}`);
|
|
1081
1332
|
break;
|
|
1082
1333
|
}
|
|
@@ -1085,7 +1336,7 @@ function signalNeuron(brainRoot, neuronPath, signalType) {
|
|
|
1085
1336
|
function getNextSignalLevel(dir, prefix) {
|
|
1086
1337
|
let max = 0;
|
|
1087
1338
|
try {
|
|
1088
|
-
for (const entry of
|
|
1339
|
+
for (const entry of readdirSync7(dir)) {
|
|
1089
1340
|
if (entry.startsWith(prefix) && entry.endsWith(".neuron")) {
|
|
1090
1341
|
const n = parseInt(entry.replace(prefix, ""), 10);
|
|
1091
1342
|
if (!isNaN(n) && n > max) max = n;
|
|
@@ -1107,15 +1358,15 @@ var decay_exports = {};
|
|
|
1107
1358
|
__export(decay_exports, {
|
|
1108
1359
|
runDecay: () => runDecay
|
|
1109
1360
|
});
|
|
1110
|
-
import { readdirSync as
|
|
1111
|
-
import { join as
|
|
1361
|
+
import { readdirSync as readdirSync8, statSync as statSync4, writeFileSync as writeFileSync8, existsSync as existsSync11 } from "fs";
|
|
1362
|
+
import { join as join11 } from "path";
|
|
1112
1363
|
function runDecay(brainRoot, days) {
|
|
1113
1364
|
const threshold = Date.now() - days * 24 * 60 * 60 * 1e3;
|
|
1114
1365
|
let scanned = 0;
|
|
1115
1366
|
let decayed = 0;
|
|
1116
1367
|
for (const regionName of REGIONS) {
|
|
1117
|
-
const regionPath =
|
|
1118
|
-
if (!
|
|
1368
|
+
const regionPath = join11(brainRoot, regionName);
|
|
1369
|
+
if (!existsSync11(regionPath)) continue;
|
|
1119
1370
|
const result = decayWalk(regionPath, threshold, 0);
|
|
1120
1371
|
scanned += result.scanned;
|
|
1121
1372
|
decayed += result.decayed;
|
|
@@ -1129,7 +1380,7 @@ function decayWalk(dir, threshold, depth) {
|
|
|
1129
1380
|
let decayed = 0;
|
|
1130
1381
|
let entries;
|
|
1131
1382
|
try {
|
|
1132
|
-
entries =
|
|
1383
|
+
entries = readdirSync8(dir, { withFileTypes: true });
|
|
1133
1384
|
} catch {
|
|
1134
1385
|
return { scanned: 0, decayed: 0 };
|
|
1135
1386
|
}
|
|
@@ -1141,7 +1392,7 @@ function decayWalk(dir, threshold, depth) {
|
|
|
1141
1392
|
if (entry.name.endsWith(".neuron")) {
|
|
1142
1393
|
hasNeuronFile = true;
|
|
1143
1394
|
try {
|
|
1144
|
-
const st =
|
|
1395
|
+
const st = statSync4(join11(dir, entry.name));
|
|
1145
1396
|
if (st.mtimeMs > latestMod) latestMod = st.mtimeMs;
|
|
1146
1397
|
} catch {
|
|
1147
1398
|
}
|
|
@@ -1155,8 +1406,8 @@ function decayWalk(dir, threshold, depth) {
|
|
|
1155
1406
|
scanned++;
|
|
1156
1407
|
if (!isDormant && latestMod < threshold) {
|
|
1157
1408
|
const age = Math.floor((Date.now() - latestMod) / (24 * 60 * 60 * 1e3));
|
|
1158
|
-
|
|
1159
|
-
|
|
1409
|
+
writeFileSync8(
|
|
1410
|
+
join11(dir, "decay.dormant"),
|
|
1160
1411
|
`Dormant since ${(/* @__PURE__ */ new Date()).toISOString()} (${age} days inactive)`,
|
|
1161
1412
|
"utf8"
|
|
1162
1413
|
);
|
|
@@ -1166,7 +1417,7 @@ function decayWalk(dir, threshold, depth) {
|
|
|
1166
1417
|
for (const entry of entries) {
|
|
1167
1418
|
if (entry.name.startsWith("_") || entry.name.startsWith(".")) continue;
|
|
1168
1419
|
if (entry.isDirectory()) {
|
|
1169
|
-
const sub = decayWalk(
|
|
1420
|
+
const sub = decayWalk(join11(dir, entry.name), threshold, depth + 1);
|
|
1170
1421
|
scanned += sub.scanned;
|
|
1171
1422
|
decayed += sub.decayed;
|
|
1172
1423
|
}
|
|
@@ -1185,8 +1436,8 @@ var dedup_exports = {};
|
|
|
1185
1436
|
__export(dedup_exports, {
|
|
1186
1437
|
runDedup: () => runDedup
|
|
1187
1438
|
});
|
|
1188
|
-
import { writeFileSync as
|
|
1189
|
-
import { join as
|
|
1439
|
+
import { writeFileSync as writeFileSync9 } from "fs";
|
|
1440
|
+
import { join as join12 } from "path";
|
|
1190
1441
|
function runDedup(brainRoot) {
|
|
1191
1442
|
const brain = scanBrain(brainRoot);
|
|
1192
1443
|
let scanned = 0;
|
|
@@ -1208,8 +1459,8 @@ function runDedup(brainRoot) {
|
|
|
1208
1459
|
const [keep, drop] = ni.counter >= nj.counter ? [ni, nj] : [nj, ni];
|
|
1209
1460
|
const relKeep = `${region.name}/${keep.path}`;
|
|
1210
1461
|
fireNeuron(brainRoot, relKeep);
|
|
1211
|
-
|
|
1212
|
-
|
|
1462
|
+
writeFileSync9(
|
|
1463
|
+
join12(drop.fullPath, "dedup.dormant"),
|
|
1213
1464
|
`Merged into ${keep.path} on ${(/* @__PURE__ */ new Date()).toISOString()}`,
|
|
1214
1465
|
"utf8"
|
|
1215
1466
|
);
|
|
@@ -1239,10 +1490,10 @@ __export(snapshot_exports, {
|
|
|
1239
1490
|
gitSnapshot: () => gitSnapshot
|
|
1240
1491
|
});
|
|
1241
1492
|
import { execSync } from "child_process";
|
|
1242
|
-
import { existsSync as
|
|
1243
|
-
import { join as
|
|
1493
|
+
import { existsSync as existsSync12 } from "fs";
|
|
1494
|
+
import { join as join13 } from "path";
|
|
1244
1495
|
function gitSnapshot(brainRoot) {
|
|
1245
|
-
if (!
|
|
1496
|
+
if (!existsSync12(join13(brainRoot, ".git"))) {
|
|
1246
1497
|
try {
|
|
1247
1498
|
execSync("git rev-parse --is-inside-work-tree", { cwd: brainRoot, stdio: "pipe" });
|
|
1248
1499
|
} catch {
|
|
@@ -1330,230 +1581,6 @@ var init_watch = __esm({
|
|
|
1330
1581
|
}
|
|
1331
1582
|
});
|
|
1332
1583
|
|
|
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
1584
|
// src/inbox.ts
|
|
1558
1585
|
var inbox_exports = {};
|
|
1559
1586
|
__export(inbox_exports, {
|
|
@@ -2225,7 +2252,8 @@ function digestTranscript(brainRoot, transcriptPath, sessionId) {
|
|
|
2225
2252
|
console.log(`\u{1F527} digest: ${toolFailures.length} tool failure(s), ${retries.length} retry pattern(s) logged`);
|
|
2226
2253
|
}
|
|
2227
2254
|
const corrections = extractCorrections(messages);
|
|
2228
|
-
|
|
2255
|
+
const fired = autoFireCandidates(brainRoot, corrections);
|
|
2256
|
+
if (corrections.length === 0 && toolFailures.length === 0 && fired === 0) {
|
|
2229
2257
|
console.log(`\u{1F4DD} digest: no corrections found in session ${resolvedSessionId}`);
|
|
2230
2258
|
writeAuditLog(brainRoot, resolvedSessionId, [], totalLines);
|
|
2231
2259
|
return { corrections: 0, skipped: messages.length, toolFailures: toolFailures.length, transcriptPath, sessionId: resolvedSessionId };
|
|
@@ -2381,7 +2409,7 @@ function extractCorrections(messages) {
|
|
|
2381
2409
|
if (/^Base directory for this skill:/i.test(trimmed)) continue;
|
|
2382
2410
|
if (/^[•·▸▶\-\*]\s/.test(trimmed)) continue;
|
|
2383
2411
|
if (/<[a-zA-Z][a-zA-Z-]*>/.test(trimmed) && /<\/[a-zA-Z]/.test(trimmed)) continue;
|
|
2384
|
-
if (
|
|
2412
|
+
if (isNarrative(trimmed)) continue;
|
|
2385
2413
|
const correction = detectCorrection(text);
|
|
2386
2414
|
if (correction) {
|
|
2387
2415
|
corrections.push(correction);
|
|
@@ -2389,7 +2417,7 @@ function extractCorrections(messages) {
|
|
|
2389
2417
|
}
|
|
2390
2418
|
return corrections;
|
|
2391
2419
|
}
|
|
2392
|
-
function
|
|
2420
|
+
function isNarrative(text) {
|
|
2393
2421
|
const NARRATIVE_MARKERS = [
|
|
2394
2422
|
/이유는/,
|
|
2395
2423
|
// "the reason is..."
|
|
@@ -2415,6 +2443,26 @@ function isNarrativeKorean(text) {
|
|
|
2415
2443
|
const markerCount = NARRATIVE_MARKERS.filter((p) => p.test(text)).length;
|
|
2416
2444
|
return markerCount >= 2;
|
|
2417
2445
|
}
|
|
2446
|
+
function autoFireCandidates(brainRoot, corrections) {
|
|
2447
|
+
if (corrections.length > 0) return 0;
|
|
2448
|
+
const candidates = listCandidates(brainRoot);
|
|
2449
|
+
if (candidates.length === 0) return 0;
|
|
2450
|
+
let fired = 0;
|
|
2451
|
+
for (const cand of candidates) {
|
|
2452
|
+
try {
|
|
2453
|
+
const newCounter = fireNeuron(brainRoot, cand.candidatePath);
|
|
2454
|
+
fired++;
|
|
2455
|
+
if (newCounter >= CANDIDATE_THRESHOLD) {
|
|
2456
|
+
growCandidate(brainRoot, cand.targetPath);
|
|
2457
|
+
}
|
|
2458
|
+
} catch {
|
|
2459
|
+
}
|
|
2460
|
+
}
|
|
2461
|
+
if (fired > 0) {
|
|
2462
|
+
console.log(`\u{1F504} agent-evaluator: ${fired} candidate(s) advanced (session without corrections)`);
|
|
2463
|
+
}
|
|
2464
|
+
return fired;
|
|
2465
|
+
}
|
|
2418
2466
|
function detectCorrection(text) {
|
|
2419
2467
|
const isNegation = NEGATION_PATTERNS.some((p) => p.test(text));
|
|
2420
2468
|
const isMust = MUST_PATTERNS.some((p) => p.test(text));
|
|
@@ -2422,9 +2470,9 @@ function detectCorrection(text) {
|
|
|
2422
2470
|
const isAffirmation = AFFIRMATION_PATTERNS.some((p) => p.test(text));
|
|
2423
2471
|
if (!isNegation && !isMust && !isWarn && !isAffirmation) return null;
|
|
2424
2472
|
const categories = [isNegation, isMust, isWarn, isAffirmation].filter(Boolean).length;
|
|
2425
|
-
const
|
|
2426
|
-
if (
|
|
2427
|
-
if (text.length >
|
|
2473
|
+
const latinRatio = (text.match(/[a-zA-Z]/g) || []).length / Math.max(text.length, 1);
|
|
2474
|
+
if (latinRatio < 0.3 && categories < 2) {
|
|
2475
|
+
if (text.length > 150) return null;
|
|
2428
2476
|
}
|
|
2429
2477
|
let prefix;
|
|
2430
2478
|
if (isNegation) prefix = "NO";
|
|
@@ -2616,6 +2664,7 @@ var init_digest = __esm({
|
|
|
2616
2664
|
"use strict";
|
|
2617
2665
|
init_constants();
|
|
2618
2666
|
init_candidates();
|
|
2667
|
+
init_fire();
|
|
2619
2668
|
init_episode();
|
|
2620
2669
|
NEGATION_PATTERNS = [
|
|
2621
2670
|
/\bdon[''\u2019]?t\b/i,
|
|
@@ -2625,33 +2674,65 @@ var init_digest = __esm({
|
|
|
2625
2674
|
/\binstead\b/i,
|
|
2626
2675
|
/^no[,.\s!]/i,
|
|
2627
2676
|
/\bavoid\b/i,
|
|
2628
|
-
// Korean negation —
|
|
2629
|
-
// "X하지 마" (don't X) — must have a verb object before 지 마
|
|
2677
|
+
// Korean negation — imperative corrections:
|
|
2630
2678
|
/[을를은는도이가]\s*[가-힣]+지\s*마/,
|
|
2631
|
-
// "X
|
|
2679
|
+
// "X하지 마" (don't X) with particle
|
|
2632
2680
|
/하면\s*안\s*돼/,
|
|
2633
|
-
// "X
|
|
2634
|
-
/쓰지\s
|
|
2681
|
+
// "X 하면 안 돼" (must not X)
|
|
2682
|
+
/쓰지\s*마/,
|
|
2683
|
+
// "쓰지 마" (don't use)
|
|
2684
|
+
/그만/,
|
|
2685
|
+
// "그만" (stop) — 그만해, 그만 좀
|
|
2686
|
+
/[을를은는]\s*빼/,
|
|
2687
|
+
// "X 빼" (remove X) with particle
|
|
2688
|
+
/지워[줘]?|삭제해/,
|
|
2689
|
+
// "지워/삭제해" (delete it) — not 지우고 (connective)
|
|
2690
|
+
/[가-힣]+지\s*말고/,
|
|
2691
|
+
// "X지 말고" (instead of X-ing)
|
|
2692
|
+
/그거\s*아니/,
|
|
2693
|
+
// "그거 아니야" (that's not right)
|
|
2694
|
+
/ㄴㄴ|노노/,
|
|
2695
|
+
// "ㄴㄴ/노노" (no no — internet-style)
|
|
2696
|
+
/안\s*돼[^요]?\s*[!.]/
|
|
2697
|
+
// "안 돼!" standalone prohibition
|
|
2635
2698
|
];
|
|
2636
2699
|
AFFIRMATION_PATTERNS = [
|
|
2637
2700
|
/\bshould\s+always\b/i,
|
|
2638
2701
|
/\buse\s+\w+\s+instead\b/i,
|
|
2639
|
-
// Korean affirmation —
|
|
2640
|
-
/항상\s*[가-힣]+[해하]
|
|
2702
|
+
// Korean affirmation — directive commands:
|
|
2703
|
+
/항상\s*[가-힣]+[해하]/,
|
|
2641
2704
|
// "항상 X해" (always do X)
|
|
2705
|
+
/[을를]\s*[가-힣]*해\s*줘/,
|
|
2706
|
+
// "X를 해줘" (do X for me) — literal 해줘, not bare 줘
|
|
2707
|
+
/으로\s*해/,
|
|
2708
|
+
// "X으로 해" (do it as X) — literal 으로, not char class
|
|
2709
|
+
/이렇게\s*해/
|
|
2710
|
+
// "이렇게 해" (do it like this)
|
|
2642
2711
|
];
|
|
2643
2712
|
MUST_PATTERNS = [
|
|
2644
2713
|
/\bmust\b/i,
|
|
2645
2714
|
/\brequired\b/i,
|
|
2646
|
-
// Korean
|
|
2647
|
-
|
|
2715
|
+
// Korean — strong directives:
|
|
2716
|
+
/반드시/,
|
|
2717
|
+
// "반드시" (absolutely must)
|
|
2718
|
+
/꼭\s*[가-힣]/,
|
|
2719
|
+
// "꼭 X해" (definitely do X)
|
|
2720
|
+
/무조건/,
|
|
2721
|
+
// "무조건" (unconditionally)
|
|
2722
|
+
/필수/
|
|
2723
|
+
// "필수" (mandatory)
|
|
2648
2724
|
];
|
|
2649
2725
|
WARN_PATTERNS = [
|
|
2650
2726
|
/\bcareful\b/i,
|
|
2651
2727
|
/\bwatch\s+out\b/i,
|
|
2652
2728
|
/\bwarning\b/i,
|
|
2653
|
-
// Korean
|
|
2654
|
-
|
|
2729
|
+
// Korean — cautionary:
|
|
2730
|
+
/주의/,
|
|
2731
|
+
// "주의" (caution)
|
|
2732
|
+
/조심/,
|
|
2733
|
+
// "조심" (be careful)
|
|
2734
|
+
/위험/
|
|
2735
|
+
// "위험" (dangerous)
|
|
2655
2736
|
];
|
|
2656
2737
|
MAX_FAILURES_PER_SESSION = 20;
|
|
2657
2738
|
SOFT_ERROR_PATTERNS = [
|
|
@@ -2665,6 +2746,48 @@ var init_digest = __esm({
|
|
|
2665
2746
|
}
|
|
2666
2747
|
});
|
|
2667
2748
|
|
|
2749
|
+
// src/learn.ts
|
|
2750
|
+
var learn_exports = {};
|
|
2751
|
+
__export(learn_exports, {
|
|
2752
|
+
learn: () => learn
|
|
2753
|
+
});
|
|
2754
|
+
function learn(brainRoot, opts) {
|
|
2755
|
+
let prefix;
|
|
2756
|
+
let keywords;
|
|
2757
|
+
let source;
|
|
2758
|
+
if (opts.prefix && opts.keywords && opts.keywords.length > 0) {
|
|
2759
|
+
prefix = opts.prefix.toUpperCase();
|
|
2760
|
+
if (!VALID_PREFIXES.has(prefix)) {
|
|
2761
|
+
prefix = "DO";
|
|
2762
|
+
}
|
|
2763
|
+
keywords = opts.keywords.slice(0, 3).map((k) => k.toLowerCase().replace(/[\s\/\\\.,:;!?'"<>{}()\[\]]/g, ""));
|
|
2764
|
+
source = "agent";
|
|
2765
|
+
} else {
|
|
2766
|
+
const corrections = extractCorrections([opts.text]);
|
|
2767
|
+
if (corrections.length === 0) return null;
|
|
2768
|
+
const c = corrections[0];
|
|
2769
|
+
prefix = c.prefix;
|
|
2770
|
+
keywords = c.keywords;
|
|
2771
|
+
source = "regex";
|
|
2772
|
+
}
|
|
2773
|
+
if (keywords.length === 0) return null;
|
|
2774
|
+
const pathSegment = `${prefix}_${keywords.slice(0, 3).join("_")}`;
|
|
2775
|
+
const path = `cortex/${pathSegment}`;
|
|
2776
|
+
growCandidate(brainRoot, path);
|
|
2777
|
+
logEpisode(brainRoot, "learn", path, opts.text);
|
|
2778
|
+
return { path, prefix, keywords, source };
|
|
2779
|
+
}
|
|
2780
|
+
var VALID_PREFIXES;
|
|
2781
|
+
var init_learn = __esm({
|
|
2782
|
+
"src/learn.ts"() {
|
|
2783
|
+
"use strict";
|
|
2784
|
+
init_candidates();
|
|
2785
|
+
init_episode();
|
|
2786
|
+
init_digest();
|
|
2787
|
+
VALID_PREFIXES = /* @__PURE__ */ new Set(["NO", "DO", "MUST", "WARN"]);
|
|
2788
|
+
}
|
|
2789
|
+
});
|
|
2790
|
+
|
|
2668
2791
|
// src/outcome.ts
|
|
2669
2792
|
var outcome_exports = {};
|
|
2670
2793
|
__export(outcome_exports, {
|
|
@@ -3240,6 +3363,7 @@ __export(doctor_exports, {
|
|
|
3240
3363
|
});
|
|
3241
3364
|
import { existsSync as existsSync18, readFileSync as readFileSync11, readdirSync as readdirSync11 } from "fs";
|
|
3242
3365
|
import { join as join19 } from "path";
|
|
3366
|
+
import { homedir } from "os";
|
|
3243
3367
|
import { execSync as execSync4 } from "child_process";
|
|
3244
3368
|
async function runDoctor(brainRoot) {
|
|
3245
3369
|
let passed = 0, warnings = 0, failed = 0;
|
|
@@ -3300,33 +3424,49 @@ async function runDoctor(brainRoot) {
|
|
|
3300
3424
|
}
|
|
3301
3425
|
}
|
|
3302
3426
|
console.log("\nClaude Code hooks");
|
|
3303
|
-
const
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3427
|
+
const localSettingsPath = join19(process.cwd(), ".claude", "settings.local.json");
|
|
3428
|
+
const globalSettingsPath = join19(homedir(), ".claude", "settings.json");
|
|
3429
|
+
let hasStop = false;
|
|
3430
|
+
let hasStart = false;
|
|
3431
|
+
let hookSource = "";
|
|
3432
|
+
for (const settingsPath of [localSettingsPath, globalSettingsPath]) {
|
|
3433
|
+
if (!existsSync18(settingsPath)) continue;
|
|
3307
3434
|
try {
|
|
3308
3435
|
const settings = JSON.parse(readFileSync11(settingsPath, "utf8"));
|
|
3309
3436
|
const hooks = settings.hooks || {};
|
|
3310
|
-
const
|
|
3311
|
-
([
|
|
3312
|
-
(
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
3437
|
+
const findCommand = (event, keyword) => Object.entries(hooks).some(
|
|
3438
|
+
([ev, entries]) => ev === event && Array.isArray(entries) && entries.some((entry) => {
|
|
3439
|
+
if (typeof entry !== "object" || entry === null) return false;
|
|
3440
|
+
const e = entry;
|
|
3441
|
+
if (typeof e.command === "string" && e.command.includes(keyword)) return true;
|
|
3442
|
+
if (Array.isArray(e.hooks)) {
|
|
3443
|
+
return e.hooks.some(
|
|
3444
|
+
(h) => typeof h === "object" && h !== null && typeof h.command === "string" && h.command.includes(keyword)
|
|
3445
|
+
);
|
|
3446
|
+
}
|
|
3447
|
+
return false;
|
|
3448
|
+
})
|
|
3319
3449
|
);
|
|
3320
|
-
if (hasStop &&
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3450
|
+
if (!hasStop && findCommand("Stop", "hebbian digest")) {
|
|
3451
|
+
hasStop = true;
|
|
3452
|
+
hookSource = settingsPath === globalSettingsPath ? "global" : "local";
|
|
3453
|
+
}
|
|
3454
|
+
if (!hasStart && findCommand("SessionStart", "hebbian emit")) {
|
|
3455
|
+
hasStart = true;
|
|
3456
|
+
if (!hookSource) hookSource = settingsPath === globalSettingsPath ? "global" : "local";
|
|
3325
3457
|
}
|
|
3326
3458
|
} catch {
|
|
3327
|
-
|
|
3459
|
+
warn(`Malformed ${settingsPath === globalSettingsPath ? "~/.claude/settings.json" : ".claude/settings.local.json"}`, "Check JSON syntax");
|
|
3328
3460
|
}
|
|
3329
3461
|
}
|
|
3462
|
+
if (hasStop && hasStart) {
|
|
3463
|
+
ok(`SessionStart + Stop hooks installed (${hookSource})`);
|
|
3464
|
+
} else if (!hasStop && !hasStart) {
|
|
3465
|
+
warn("No hebbian hooks found (checked local + global)", "hebbian claude install");
|
|
3466
|
+
} else {
|
|
3467
|
+
if (!hasStart) warn("SessionStart hook missing", "hebbian claude install");
|
|
3468
|
+
if (!hasStop) warn("Stop hook missing", "hebbian claude install");
|
|
3469
|
+
}
|
|
3330
3470
|
console.log("\nnpx resolution");
|
|
3331
3471
|
try {
|
|
3332
3472
|
const resolved = execSync4("which npx", { timeout: 3e3 }).toString().trim();
|
|
@@ -3660,9 +3800,10 @@ COMMANDS:
|
|
|
3660
3800
|
inbox Process corrections inbox
|
|
3661
3801
|
claude install|uninstall|status Manage Claude Code hooks
|
|
3662
3802
|
digest [--transcript <path>] Extract corrections from conversation
|
|
3803
|
+
learn "<text>" [--prefix P] Agent-driven learning (any language)
|
|
3663
3804
|
candidates [promote] List candidates or promote graduated ones
|
|
3664
|
-
evolve [--dry-run] LLM-powered
|
|
3665
|
-
evolve prune [--dry-run] Pruning mode \u2014 remove stale
|
|
3805
|
+
evolve [--dry-run] (optional) LLM-powered evolution (Gemini)
|
|
3806
|
+
evolve prune [--dry-run] (optional) Pruning mode \u2014 remove stale neurons
|
|
3666
3807
|
session start|end Capture/detect session outcomes
|
|
3667
3808
|
sessions Show session outcome history
|
|
3668
3809
|
doctor Self-diagnostic (hooks, brain, versions)
|
|
@@ -3680,7 +3821,7 @@ EXAMPLES:
|
|
|
3680
3821
|
hebbian fire cortex/frontend/NO_console_log --brain ./my-brain
|
|
3681
3822
|
hebbian emit claude --brain ./my-brain
|
|
3682
3823
|
hebbian emit all
|
|
3683
|
-
GEMINI_API_KEY=... hebbian evolve --dry-run
|
|
3824
|
+
GEMINI_API_KEY=... hebbian evolve --dry-run # optional \u2014 self-learning works without this
|
|
3684
3825
|
`.trim();
|
|
3685
3826
|
function readStdin() {
|
|
3686
3827
|
return new Promise((resolve4) => {
|
|
@@ -3706,6 +3847,8 @@ async function main(argv) {
|
|
|
3706
3847
|
days: { type: "string", short: "d" },
|
|
3707
3848
|
port: { type: "string", short: "p" },
|
|
3708
3849
|
transcript: { type: "string", short: "t" },
|
|
3850
|
+
prefix: { type: "string" },
|
|
3851
|
+
keywords: { type: "string", short: "k" },
|
|
3709
3852
|
"dry-run": { type: "boolean" },
|
|
3710
3853
|
global: { type: "boolean", short: "g" },
|
|
3711
3854
|
agent: { type: "string", short: "a" },
|
|
@@ -3882,6 +4025,27 @@ async function main(argv) {
|
|
|
3882
4025
|
}
|
|
3883
4026
|
break;
|
|
3884
4027
|
}
|
|
4028
|
+
case "learn": {
|
|
4029
|
+
const text = positionals.slice(1).join(" ");
|
|
4030
|
+
if (!text) {
|
|
4031
|
+
console.error('Usage: hebbian learn "correction text" [--prefix NO|DO|MUST|WARN] [--keywords "k1,k2,k3"]');
|
|
4032
|
+
process.exit(1);
|
|
4033
|
+
}
|
|
4034
|
+
const { learn: learn2 } = await Promise.resolve().then(() => (init_learn(), learn_exports));
|
|
4035
|
+
const prefixFlag = values.prefix;
|
|
4036
|
+
const keywordsFlag = values.keywords;
|
|
4037
|
+
const result = learn2(brainRoot, {
|
|
4038
|
+
text,
|
|
4039
|
+
prefix: prefixFlag,
|
|
4040
|
+
keywords: keywordsFlag ? keywordsFlag.split(",").map((k) => k.trim()) : void 0
|
|
4041
|
+
});
|
|
4042
|
+
if (result) {
|
|
4043
|
+
console.log(`\u{1F4DD} learned: ${result.path} (${result.source})`);
|
|
4044
|
+
} else {
|
|
4045
|
+
console.log("\u23ED\uFE0F no correction detected");
|
|
4046
|
+
}
|
|
4047
|
+
break;
|
|
4048
|
+
}
|
|
3885
4049
|
case "candidates": {
|
|
3886
4050
|
const subCmd = positionals[1];
|
|
3887
4051
|
const { listCandidates: listCandidates2, promoteCandidates: promoteCandidates2 } = await Promise.resolve().then(() => (init_candidates(), candidates_exports));
|