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/index.js
CHANGED
|
@@ -614,9 +614,205 @@ function gitSnapshot(brainRoot) {
|
|
|
614
614
|
import { watch } from "fs";
|
|
615
615
|
|
|
616
616
|
// src/emit.ts
|
|
617
|
-
import { existsSync as
|
|
618
|
-
import { join as
|
|
619
|
-
|
|
617
|
+
import { existsSync as existsSync10, readFileSync as readFileSync3, writeFileSync as writeFileSync7, mkdirSync as mkdirSync5 } from "fs";
|
|
618
|
+
import { join as join11, dirname as dirname2 } from "path";
|
|
619
|
+
|
|
620
|
+
// src/candidates.ts
|
|
621
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync4, readdirSync as readdirSync7, renameSync as renameSync3, rmSync, statSync as statSync3 } from "fs";
|
|
622
|
+
import { join as join10, dirname, relative as relative3 } from "path";
|
|
623
|
+
|
|
624
|
+
// src/episode.ts
|
|
625
|
+
import { readdirSync as readdirSync6, readFileSync as readFileSync2, writeFileSync as writeFileSync6, mkdirSync as mkdirSync3, existsSync as existsSync8 } from "fs";
|
|
626
|
+
import { join as join9 } from "path";
|
|
627
|
+
var MAX_EPISODES = 100;
|
|
628
|
+
var SESSION_LOG_DIR = "hippocampus/session_log";
|
|
629
|
+
function logEpisode(brainRoot, type, path, detail, extra) {
|
|
630
|
+
const logDir = join9(brainRoot, SESSION_LOG_DIR);
|
|
631
|
+
if (!existsSync8(logDir)) {
|
|
632
|
+
mkdirSync3(logDir, { recursive: true });
|
|
633
|
+
}
|
|
634
|
+
const nextSlot = getNextSlot(logDir);
|
|
635
|
+
const episode = {
|
|
636
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
637
|
+
type,
|
|
638
|
+
path,
|
|
639
|
+
detail,
|
|
640
|
+
...extra?.outcome ? { outcome: extra.outcome } : {},
|
|
641
|
+
...extra?.neurons ? { neurons: extra.neurons } : {}
|
|
642
|
+
};
|
|
643
|
+
writeFileSync6(
|
|
644
|
+
join9(logDir, `memory${nextSlot}.neuron`),
|
|
645
|
+
JSON.stringify(episode),
|
|
646
|
+
"utf8"
|
|
647
|
+
);
|
|
648
|
+
}
|
|
649
|
+
function readEpisodes(brainRoot) {
|
|
650
|
+
const logDir = join9(brainRoot, SESSION_LOG_DIR);
|
|
651
|
+
if (!existsSync8(logDir)) return [];
|
|
652
|
+
const episodes = [];
|
|
653
|
+
let entries;
|
|
654
|
+
try {
|
|
655
|
+
entries = readdirSync6(logDir);
|
|
656
|
+
} catch {
|
|
657
|
+
return [];
|
|
658
|
+
}
|
|
659
|
+
for (const entry of entries) {
|
|
660
|
+
if (!entry.startsWith("memory") || !entry.endsWith(".neuron")) continue;
|
|
661
|
+
try {
|
|
662
|
+
const content = readFileSync2(join9(logDir, entry), "utf8");
|
|
663
|
+
if (content.trim()) {
|
|
664
|
+
episodes.push(JSON.parse(content));
|
|
665
|
+
}
|
|
666
|
+
} catch {
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
episodes.sort((a, b) => a.ts.localeCompare(b.ts));
|
|
670
|
+
return episodes;
|
|
671
|
+
}
|
|
672
|
+
function getNextSlot(logDir) {
|
|
673
|
+
let maxSlot = 0;
|
|
674
|
+
try {
|
|
675
|
+
for (const entry of readdirSync6(logDir)) {
|
|
676
|
+
if (entry.startsWith("memory") && entry.endsWith(".neuron")) {
|
|
677
|
+
const n = parseInt(entry.replace("memory", "").replace(".neuron", ""), 10);
|
|
678
|
+
if (!isNaN(n) && n > maxSlot) maxSlot = n;
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
} catch {
|
|
682
|
+
}
|
|
683
|
+
const next = maxSlot + 1;
|
|
684
|
+
return next > MAX_EPISODES ? maxSlot % MAX_EPISODES + 1 : next;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// src/candidates.ts
|
|
688
|
+
var CANDIDATE_THRESHOLD = 3;
|
|
689
|
+
var CANDIDATE_DECAY_DAYS = 14;
|
|
690
|
+
var CANDIDATE_SEGMENT = "_candidates";
|
|
691
|
+
function toCandidatePath(neuronPath) {
|
|
692
|
+
const slash = neuronPath.indexOf("/");
|
|
693
|
+
if (slash === -1) throw new Error(`Invalid neuron path (missing region): ${neuronPath}`);
|
|
694
|
+
return `${neuronPath.slice(0, slash)}/${CANDIDATE_SEGMENT}/${neuronPath.slice(slash + 1)}`;
|
|
695
|
+
}
|
|
696
|
+
function fromCandidatePath(candidatePath) {
|
|
697
|
+
return candidatePath.replace(`/${CANDIDATE_SEGMENT}/`, "/");
|
|
698
|
+
}
|
|
699
|
+
function growCandidate(brainRoot, neuronPath) {
|
|
700
|
+
const candidatePath = toCandidatePath(neuronPath);
|
|
701
|
+
const result = growNeuron(brainRoot, candidatePath);
|
|
702
|
+
if (result.counter >= CANDIDATE_THRESHOLD) {
|
|
703
|
+
const ok = moveCandidate(brainRoot, candidatePath, neuronPath);
|
|
704
|
+
if (ok) propagateToShared(brainRoot, neuronPath);
|
|
705
|
+
return { ...result, path: ok ? neuronPath : result.path, promoted: ok };
|
|
706
|
+
}
|
|
707
|
+
console.log(` \u{1F331} candidate (${result.counter}/${CANDIDATE_THRESHOLD}): ${candidatePath}`);
|
|
708
|
+
return { ...result, promoted: false };
|
|
709
|
+
}
|
|
710
|
+
function moveCandidate(brainRoot, candidatePath, targetPath) {
|
|
711
|
+
const src = join10(brainRoot, candidatePath);
|
|
712
|
+
if (!existsSync9(src)) return false;
|
|
713
|
+
const dst = join10(brainRoot, targetPath);
|
|
714
|
+
if (existsSync9(dst)) {
|
|
715
|
+
fireNeuron(brainRoot, targetPath);
|
|
716
|
+
rmSync(src, { recursive: true, force: true });
|
|
717
|
+
} else {
|
|
718
|
+
mkdirSync4(dirname(dst), { recursive: true });
|
|
719
|
+
renameSync3(src, dst);
|
|
720
|
+
}
|
|
721
|
+
console.log(`\u{1F393} promoted: ${candidatePath} \u2192 ${targetPath}`);
|
|
722
|
+
return true;
|
|
723
|
+
}
|
|
724
|
+
function promoteCandidates(brainRoot) {
|
|
725
|
+
const promoted = [];
|
|
726
|
+
const decayed = [];
|
|
727
|
+
const decayMs = CANDIDATE_DECAY_DAYS * 24 * 60 * 60 * 1e3;
|
|
728
|
+
const now = Date.now();
|
|
729
|
+
for (const region of REGIONS) {
|
|
730
|
+
const candidateRoot = join10(brainRoot, region, CANDIDATE_SEGMENT);
|
|
731
|
+
walkNeuronDirs(candidateRoot, (neuronDir) => {
|
|
732
|
+
const rel = relative3(join10(brainRoot, region), neuronDir);
|
|
733
|
+
const candidatePath = `${region}/${rel}`;
|
|
734
|
+
const targetPath = fromCandidatePath(candidatePath);
|
|
735
|
+
const counter = readCounter(neuronDir);
|
|
736
|
+
const mtime = statSync3(neuronDir).mtimeMs;
|
|
737
|
+
if (counter >= CANDIDATE_THRESHOLD) {
|
|
738
|
+
moveCandidate(brainRoot, candidatePath, targetPath);
|
|
739
|
+
propagateToShared(brainRoot, targetPath);
|
|
740
|
+
promoted.push(targetPath);
|
|
741
|
+
} else if (now - mtime > decayMs) {
|
|
742
|
+
rmSync(neuronDir, { recursive: true, force: true });
|
|
743
|
+
decayed.push(candidatePath);
|
|
744
|
+
console.log(`\u{1F480} candidate decayed: ${candidatePath}`);
|
|
745
|
+
}
|
|
746
|
+
});
|
|
747
|
+
}
|
|
748
|
+
return { promoted, decayed };
|
|
749
|
+
}
|
|
750
|
+
function listCandidates(brainRoot) {
|
|
751
|
+
const results = [];
|
|
752
|
+
const now = Date.now();
|
|
753
|
+
for (const region of REGIONS) {
|
|
754
|
+
const candidateRoot = join10(brainRoot, region, CANDIDATE_SEGMENT);
|
|
755
|
+
walkNeuronDirs(candidateRoot, (neuronDir) => {
|
|
756
|
+
const rel = relative3(join10(brainRoot, region), neuronDir);
|
|
757
|
+
const candidatePath = `${region}/${rel}`;
|
|
758
|
+
const targetPath = fromCandidatePath(candidatePath);
|
|
759
|
+
const counter = readCounter(neuronDir);
|
|
760
|
+
const mtime = statSync3(neuronDir).mtimeMs;
|
|
761
|
+
const daysInactive = Math.floor((now - mtime) / (24 * 60 * 60 * 1e3));
|
|
762
|
+
results.push({ candidatePath, targetPath, counter, daysInactive });
|
|
763
|
+
});
|
|
764
|
+
}
|
|
765
|
+
return results;
|
|
766
|
+
}
|
|
767
|
+
function walkNeuronDirs(dir, cb) {
|
|
768
|
+
if (!existsSync9(dir)) return;
|
|
769
|
+
try {
|
|
770
|
+
const entries = readdirSync7(dir, { withFileTypes: true });
|
|
771
|
+
const hasNeuron = entries.some((e) => e.isFile() && e.name.endsWith(".neuron"));
|
|
772
|
+
if (hasNeuron) {
|
|
773
|
+
cb(dir);
|
|
774
|
+
return;
|
|
775
|
+
}
|
|
776
|
+
for (const entry of entries) {
|
|
777
|
+
if (entry.isDirectory() && !entry.name.startsWith(".")) {
|
|
778
|
+
walkNeuronDirs(join10(dir, entry.name), cb);
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
} catch {
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
function readCounter(dir) {
|
|
785
|
+
try {
|
|
786
|
+
const files = readdirSync7(dir).filter((f) => /^\d+\.neuron$/.test(f));
|
|
787
|
+
if (files.length === 0) return 0;
|
|
788
|
+
return Math.max(...files.map((f) => parseInt(f, 10)));
|
|
789
|
+
} catch {
|
|
790
|
+
return 0;
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
function propagateToShared(brainRoot, targetPath) {
|
|
794
|
+
try {
|
|
795
|
+
const agentsIdx = brainRoot.indexOf("/agents/");
|
|
796
|
+
if (agentsIdx === -1) return false;
|
|
797
|
+
const multiBrainRoot = brainRoot.slice(0, agentsIdx);
|
|
798
|
+
const sharedRoot = join10(multiBrainRoot, "shared");
|
|
799
|
+
if (!existsSync9(sharedRoot)) return false;
|
|
800
|
+
const episodes = readEpisodes(brainRoot);
|
|
801
|
+
const neuronName = targetPath.split("/").pop() || "";
|
|
802
|
+
const hasRelevantEpisode = episodes.some(
|
|
803
|
+
(ep) => PROPAGATION_EPISODE_TYPES.includes(ep.type) && (ep.path.includes(neuronName) || ep.detail.includes(neuronName))
|
|
804
|
+
);
|
|
805
|
+
if (!hasRelevantEpisode) return false;
|
|
806
|
+
growNeuron(sharedRoot, targetPath);
|
|
807
|
+
console.log(` \u{1F4E1} propagated to shared: ${targetPath}`);
|
|
808
|
+
return true;
|
|
809
|
+
} catch {
|
|
810
|
+
return false;
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
// src/emit.ts
|
|
815
|
+
function emitBootstrap(result, brain, brainRoot) {
|
|
620
816
|
const lines = [];
|
|
621
817
|
const now = (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d+Z$/, "");
|
|
622
818
|
lines.push(MARKER_START);
|
|
@@ -672,6 +868,19 @@ function emitBootstrap(result, brain) {
|
|
|
672
868
|
lines.push(`| ${icon} ${region.name} | ${active.length} | ${activation} |`);
|
|
673
869
|
}
|
|
674
870
|
lines.push("");
|
|
871
|
+
if (brainRoot) {
|
|
872
|
+
const candidates = listCandidates(brainRoot);
|
|
873
|
+
if (candidates.length > 0) {
|
|
874
|
+
const top = candidates.slice(0, 5);
|
|
875
|
+
lines.push("### Provisional Rules (evaluating)");
|
|
876
|
+
lines.push("Follow these during this session. They graduate after 3 sessions.");
|
|
877
|
+
for (const c of top) {
|
|
878
|
+
const bar = "\u2588".repeat(c.counter) + "\u2591".repeat(Math.max(0, 3 - c.counter));
|
|
879
|
+
lines.push(`- ${bar} ${pathToSentence(c.targetPath)}`);
|
|
880
|
+
}
|
|
881
|
+
lines.push("");
|
|
882
|
+
}
|
|
883
|
+
}
|
|
675
884
|
lines.push(MARKER_END);
|
|
676
885
|
return lines.join("\n");
|
|
677
886
|
}
|
|
@@ -766,7 +975,7 @@ function emitRegionRules(region) {
|
|
|
766
975
|
function emitToTarget(brainRoot, target) {
|
|
767
976
|
const brain = scanBrain(brainRoot);
|
|
768
977
|
const result = runSubsumption(brain);
|
|
769
|
-
const content = emitBootstrap(result, brain);
|
|
978
|
+
const content = emitBootstrap(result, brain, brainRoot);
|
|
770
979
|
if (target === "all") {
|
|
771
980
|
for (const [name, filePath] of Object.entries(EMIT_TARGETS)) {
|
|
772
981
|
writeTarget(filePath, content);
|
|
@@ -782,33 +991,33 @@ function emitToTarget(brainRoot, target) {
|
|
|
782
991
|
}
|
|
783
992
|
function writeAllTiers(brainRoot, result, brain) {
|
|
784
993
|
const indexContent = emitIndex(result, brain);
|
|
785
|
-
|
|
994
|
+
writeFileSync7(join11(brainRoot, "_index.md"), indexContent, "utf8");
|
|
786
995
|
for (const region of result.activeRegions) {
|
|
787
|
-
if (
|
|
996
|
+
if (existsSync10(region.path)) {
|
|
788
997
|
const rulesContent = emitRegionRules(region);
|
|
789
|
-
|
|
998
|
+
writeFileSync7(join11(region.path, "_rules.md"), rulesContent, "utf8");
|
|
790
999
|
}
|
|
791
1000
|
}
|
|
792
1001
|
}
|
|
793
1002
|
function writeTarget(filePath, content) {
|
|
794
|
-
const dir =
|
|
795
|
-
if (dir !== "." && !
|
|
796
|
-
|
|
1003
|
+
const dir = dirname2(filePath);
|
|
1004
|
+
if (dir !== "." && !existsSync10(dir)) {
|
|
1005
|
+
mkdirSync5(dir, { recursive: true });
|
|
797
1006
|
}
|
|
798
|
-
if (
|
|
799
|
-
const existing =
|
|
1007
|
+
if (existsSync10(filePath)) {
|
|
1008
|
+
const existing = readFileSync3(filePath, "utf8");
|
|
800
1009
|
const startIdx = existing.indexOf(MARKER_START);
|
|
801
1010
|
const endIdx = existing.indexOf(MARKER_END);
|
|
802
1011
|
if (startIdx !== -1 && endIdx !== -1) {
|
|
803
1012
|
const before = existing.slice(0, startIdx);
|
|
804
1013
|
const after = existing.slice(endIdx + MARKER_END.length);
|
|
805
|
-
|
|
1014
|
+
writeFileSync7(filePath, before + content + after, "utf8");
|
|
806
1015
|
return;
|
|
807
1016
|
}
|
|
808
|
-
|
|
1017
|
+
writeFileSync7(filePath, content + "\n\n" + existing, "utf8");
|
|
809
1018
|
return;
|
|
810
1019
|
}
|
|
811
|
-
|
|
1020
|
+
writeFileSync7(filePath, content, "utf8");
|
|
812
1021
|
}
|
|
813
1022
|
function printDiag(brain, result) {
|
|
814
1023
|
console.log("");
|
|
@@ -897,8 +1106,8 @@ function computeHash(result) {
|
|
|
897
1106
|
}
|
|
898
1107
|
|
|
899
1108
|
// src/init.ts
|
|
900
|
-
import { mkdirSync as
|
|
901
|
-
import { join as
|
|
1109
|
+
import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync8, readFileSync as readFileSync4, existsSync as existsSync11, readdirSync as readdirSync8, appendFileSync } from "fs";
|
|
1110
|
+
import { join as join12, dirname as dirname3 } from "path";
|
|
902
1111
|
var REGION_TEMPLATES = {
|
|
903
1112
|
brainstem: {
|
|
904
1113
|
description: "Absolute principles. Immutable. Read-only conscience.\nP0 \u2014 highest priority. bomb here halts EVERYTHING.",
|
|
@@ -930,22 +1139,22 @@ var REGION_TEMPLATES = {
|
|
|
930
1139
|
}
|
|
931
1140
|
};
|
|
932
1141
|
function initBrain(brainPath) {
|
|
933
|
-
if (
|
|
934
|
-
const entries =
|
|
1142
|
+
if (existsSync11(brainPath)) {
|
|
1143
|
+
const entries = readdirSync8(brainPath);
|
|
935
1144
|
if (entries.some((e) => REGIONS.includes(e))) {
|
|
936
1145
|
console.log(`\u26A0\uFE0F Brain already exists at ${brainPath}`);
|
|
937
1146
|
return;
|
|
938
1147
|
}
|
|
939
1148
|
}
|
|
940
|
-
|
|
1149
|
+
mkdirSync6(brainPath, { recursive: true });
|
|
941
1150
|
for (const regionName of REGIONS) {
|
|
942
|
-
const regionDir =
|
|
943
|
-
|
|
1151
|
+
const regionDir = join12(brainPath, regionName);
|
|
1152
|
+
mkdirSync6(regionDir, { recursive: true });
|
|
944
1153
|
const template = REGION_TEMPLATES[regionName];
|
|
945
1154
|
const icon = REGION_ICONS[regionName];
|
|
946
1155
|
const ko = REGION_KO[regionName];
|
|
947
|
-
|
|
948
|
-
|
|
1156
|
+
writeFileSync8(
|
|
1157
|
+
join12(regionDir, "_rules.md"),
|
|
949
1158
|
`# ${icon} ${regionName} (${ko})
|
|
950
1159
|
|
|
951
1160
|
${template.description}
|
|
@@ -953,15 +1162,15 @@ ${template.description}
|
|
|
953
1162
|
"utf8"
|
|
954
1163
|
);
|
|
955
1164
|
for (const starter of template.starters) {
|
|
956
|
-
const neuronDir =
|
|
957
|
-
|
|
958
|
-
|
|
1165
|
+
const neuronDir = join12(regionDir, starter);
|
|
1166
|
+
mkdirSync6(neuronDir, { recursive: true });
|
|
1167
|
+
writeFileSync8(join12(neuronDir, "1.neuron"), "", "utf8");
|
|
959
1168
|
}
|
|
960
1169
|
}
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
1170
|
+
mkdirSync6(join12(brainPath, "_agents", "global_inbox"), { recursive: true });
|
|
1171
|
+
mkdirSync6(join12(brainPath, "skills"), { recursive: true });
|
|
1172
|
+
writeFileSync8(
|
|
1173
|
+
join12(brainPath, "skills", "_rules.md"),
|
|
965
1174
|
"# Skills Library\n\nExecutable patterns learned through experience.\nNot part of the subsumption cascade \u2014 retrieval only.\n",
|
|
966
1175
|
"utf8"
|
|
967
1176
|
);
|
|
@@ -974,13 +1183,13 @@ ${template.description}
|
|
|
974
1183
|
console.log(` hebbian emit claude --brain ${brainPath}`);
|
|
975
1184
|
}
|
|
976
1185
|
function autoGitignore(brainPath) {
|
|
977
|
-
let dir =
|
|
1186
|
+
let dir = dirname3(brainPath);
|
|
978
1187
|
for (let i = 0; i < 10; i++) {
|
|
979
|
-
if (
|
|
980
|
-
const gitignorePath =
|
|
1188
|
+
if (existsSync11(join12(dir, ".git"))) {
|
|
1189
|
+
const gitignorePath = join12(dir, ".gitignore");
|
|
981
1190
|
const brainDirName = brainPath.replace(dir + "/", "") + "/";
|
|
982
|
-
if (
|
|
983
|
-
const content =
|
|
1191
|
+
if (existsSync11(gitignorePath)) {
|
|
1192
|
+
const content = readFileSync4(gitignorePath, "utf8");
|
|
984
1193
|
if (content.includes(brainDirName) || content.includes(brainDirName.replace(/\/$/, ""))) {
|
|
985
1194
|
return;
|
|
986
1195
|
}
|
|
@@ -992,7 +1201,7 @@ ${brainDirName}
|
|
|
992
1201
|
console.log(` \u{1F4DD} Added ${brainDirName} to .gitignore`);
|
|
993
1202
|
return;
|
|
994
1203
|
}
|
|
995
|
-
const parent =
|
|
1204
|
+
const parent = dirname3(dir);
|
|
996
1205
|
if (parent === dir) break;
|
|
997
1206
|
dir = parent;
|
|
998
1207
|
}
|
|
@@ -1004,202 +1213,6 @@ import { createServer } from "http";
|
|
|
1004
1213
|
// src/inbox.ts
|
|
1005
1214
|
import { readFileSync as readFileSync5, writeFileSync as writeFileSync9, existsSync as existsSync12, mkdirSync as mkdirSync7 } from "fs";
|
|
1006
1215
|
import { join as join13 } from "path";
|
|
1007
|
-
|
|
1008
|
-
// src/candidates.ts
|
|
1009
|
-
import { existsSync as existsSync11, mkdirSync as mkdirSync6, readdirSync as readdirSync8, renameSync as renameSync3, rmSync, statSync as statSync3 } from "fs";
|
|
1010
|
-
import { join as join12, dirname as dirname3, relative as relative3 } from "path";
|
|
1011
|
-
|
|
1012
|
-
// src/episode.ts
|
|
1013
|
-
import { readdirSync as readdirSync7, readFileSync as readFileSync4, writeFileSync as writeFileSync8, mkdirSync as mkdirSync5, existsSync as existsSync10 } from "fs";
|
|
1014
|
-
import { join as join11 } from "path";
|
|
1015
|
-
var MAX_EPISODES = 100;
|
|
1016
|
-
var SESSION_LOG_DIR = "hippocampus/session_log";
|
|
1017
|
-
function logEpisode(brainRoot, type, path, detail, extra) {
|
|
1018
|
-
const logDir = join11(brainRoot, SESSION_LOG_DIR);
|
|
1019
|
-
if (!existsSync10(logDir)) {
|
|
1020
|
-
mkdirSync5(logDir, { recursive: true });
|
|
1021
|
-
}
|
|
1022
|
-
const nextSlot = getNextSlot(logDir);
|
|
1023
|
-
const episode = {
|
|
1024
|
-
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1025
|
-
type,
|
|
1026
|
-
path,
|
|
1027
|
-
detail,
|
|
1028
|
-
...extra?.outcome ? { outcome: extra.outcome } : {},
|
|
1029
|
-
...extra?.neurons ? { neurons: extra.neurons } : {}
|
|
1030
|
-
};
|
|
1031
|
-
writeFileSync8(
|
|
1032
|
-
join11(logDir, `memory${nextSlot}.neuron`),
|
|
1033
|
-
JSON.stringify(episode),
|
|
1034
|
-
"utf8"
|
|
1035
|
-
);
|
|
1036
|
-
}
|
|
1037
|
-
function readEpisodes(brainRoot) {
|
|
1038
|
-
const logDir = join11(brainRoot, SESSION_LOG_DIR);
|
|
1039
|
-
if (!existsSync10(logDir)) return [];
|
|
1040
|
-
const episodes = [];
|
|
1041
|
-
let entries;
|
|
1042
|
-
try {
|
|
1043
|
-
entries = readdirSync7(logDir);
|
|
1044
|
-
} catch {
|
|
1045
|
-
return [];
|
|
1046
|
-
}
|
|
1047
|
-
for (const entry of entries) {
|
|
1048
|
-
if (!entry.startsWith("memory") || !entry.endsWith(".neuron")) continue;
|
|
1049
|
-
try {
|
|
1050
|
-
const content = readFileSync4(join11(logDir, entry), "utf8");
|
|
1051
|
-
if (content.trim()) {
|
|
1052
|
-
episodes.push(JSON.parse(content));
|
|
1053
|
-
}
|
|
1054
|
-
} catch {
|
|
1055
|
-
}
|
|
1056
|
-
}
|
|
1057
|
-
episodes.sort((a, b) => a.ts.localeCompare(b.ts));
|
|
1058
|
-
return episodes;
|
|
1059
|
-
}
|
|
1060
|
-
function getNextSlot(logDir) {
|
|
1061
|
-
let maxSlot = 0;
|
|
1062
|
-
try {
|
|
1063
|
-
for (const entry of readdirSync7(logDir)) {
|
|
1064
|
-
if (entry.startsWith("memory") && entry.endsWith(".neuron")) {
|
|
1065
|
-
const n = parseInt(entry.replace("memory", "").replace(".neuron", ""), 10);
|
|
1066
|
-
if (!isNaN(n) && n > maxSlot) maxSlot = n;
|
|
1067
|
-
}
|
|
1068
|
-
}
|
|
1069
|
-
} catch {
|
|
1070
|
-
}
|
|
1071
|
-
const next = maxSlot + 1;
|
|
1072
|
-
return next > MAX_EPISODES ? maxSlot % MAX_EPISODES + 1 : next;
|
|
1073
|
-
}
|
|
1074
|
-
|
|
1075
|
-
// src/candidates.ts
|
|
1076
|
-
var CANDIDATE_THRESHOLD = 3;
|
|
1077
|
-
var CANDIDATE_DECAY_DAYS = 14;
|
|
1078
|
-
var CANDIDATE_SEGMENT = "_candidates";
|
|
1079
|
-
function toCandidatePath(neuronPath) {
|
|
1080
|
-
const slash = neuronPath.indexOf("/");
|
|
1081
|
-
if (slash === -1) throw new Error(`Invalid neuron path (missing region): ${neuronPath}`);
|
|
1082
|
-
return `${neuronPath.slice(0, slash)}/${CANDIDATE_SEGMENT}/${neuronPath.slice(slash + 1)}`;
|
|
1083
|
-
}
|
|
1084
|
-
function fromCandidatePath(candidatePath) {
|
|
1085
|
-
return candidatePath.replace(`/${CANDIDATE_SEGMENT}/`, "/");
|
|
1086
|
-
}
|
|
1087
|
-
function growCandidate(brainRoot, neuronPath) {
|
|
1088
|
-
const candidatePath = toCandidatePath(neuronPath);
|
|
1089
|
-
const result = growNeuron(brainRoot, candidatePath);
|
|
1090
|
-
if (result.counter >= CANDIDATE_THRESHOLD) {
|
|
1091
|
-
const ok = moveCandidate(brainRoot, candidatePath, neuronPath);
|
|
1092
|
-
if (ok) propagateToShared(brainRoot, neuronPath);
|
|
1093
|
-
return { ...result, path: ok ? neuronPath : result.path, promoted: ok };
|
|
1094
|
-
}
|
|
1095
|
-
console.log(` \u{1F331} candidate (${result.counter}/${CANDIDATE_THRESHOLD}): ${candidatePath}`);
|
|
1096
|
-
return { ...result, promoted: false };
|
|
1097
|
-
}
|
|
1098
|
-
function moveCandidate(brainRoot, candidatePath, targetPath) {
|
|
1099
|
-
const src = join12(brainRoot, candidatePath);
|
|
1100
|
-
if (!existsSync11(src)) return false;
|
|
1101
|
-
const dst = join12(brainRoot, targetPath);
|
|
1102
|
-
if (existsSync11(dst)) {
|
|
1103
|
-
fireNeuron(brainRoot, targetPath);
|
|
1104
|
-
rmSync(src, { recursive: true, force: true });
|
|
1105
|
-
} else {
|
|
1106
|
-
mkdirSync6(dirname3(dst), { recursive: true });
|
|
1107
|
-
renameSync3(src, dst);
|
|
1108
|
-
}
|
|
1109
|
-
console.log(`\u{1F393} promoted: ${candidatePath} \u2192 ${targetPath}`);
|
|
1110
|
-
return true;
|
|
1111
|
-
}
|
|
1112
|
-
function promoteCandidates(brainRoot) {
|
|
1113
|
-
const promoted = [];
|
|
1114
|
-
const decayed = [];
|
|
1115
|
-
const decayMs = CANDIDATE_DECAY_DAYS * 24 * 60 * 60 * 1e3;
|
|
1116
|
-
const now = Date.now();
|
|
1117
|
-
for (const region of REGIONS) {
|
|
1118
|
-
const candidateRoot = join12(brainRoot, region, CANDIDATE_SEGMENT);
|
|
1119
|
-
walkNeuronDirs(candidateRoot, (neuronDir) => {
|
|
1120
|
-
const rel = relative3(join12(brainRoot, region), neuronDir);
|
|
1121
|
-
const candidatePath = `${region}/${rel}`;
|
|
1122
|
-
const targetPath = fromCandidatePath(candidatePath);
|
|
1123
|
-
const counter = readCounter(neuronDir);
|
|
1124
|
-
const mtime = statSync3(neuronDir).mtimeMs;
|
|
1125
|
-
if (counter >= CANDIDATE_THRESHOLD) {
|
|
1126
|
-
moveCandidate(brainRoot, candidatePath, targetPath);
|
|
1127
|
-
propagateToShared(brainRoot, targetPath);
|
|
1128
|
-
promoted.push(targetPath);
|
|
1129
|
-
} else if (now - mtime > decayMs) {
|
|
1130
|
-
rmSync(neuronDir, { recursive: true, force: true });
|
|
1131
|
-
decayed.push(candidatePath);
|
|
1132
|
-
console.log(`\u{1F480} candidate decayed: ${candidatePath}`);
|
|
1133
|
-
}
|
|
1134
|
-
});
|
|
1135
|
-
}
|
|
1136
|
-
return { promoted, decayed };
|
|
1137
|
-
}
|
|
1138
|
-
function listCandidates(brainRoot) {
|
|
1139
|
-
const results = [];
|
|
1140
|
-
const now = Date.now();
|
|
1141
|
-
for (const region of REGIONS) {
|
|
1142
|
-
const candidateRoot = join12(brainRoot, region, CANDIDATE_SEGMENT);
|
|
1143
|
-
walkNeuronDirs(candidateRoot, (neuronDir) => {
|
|
1144
|
-
const rel = relative3(join12(brainRoot, region), neuronDir);
|
|
1145
|
-
const candidatePath = `${region}/${rel}`;
|
|
1146
|
-
const targetPath = fromCandidatePath(candidatePath);
|
|
1147
|
-
const counter = readCounter(neuronDir);
|
|
1148
|
-
const mtime = statSync3(neuronDir).mtimeMs;
|
|
1149
|
-
const daysInactive = Math.floor((now - mtime) / (24 * 60 * 60 * 1e3));
|
|
1150
|
-
results.push({ candidatePath, targetPath, counter, daysInactive });
|
|
1151
|
-
});
|
|
1152
|
-
}
|
|
1153
|
-
return results;
|
|
1154
|
-
}
|
|
1155
|
-
function walkNeuronDirs(dir, cb) {
|
|
1156
|
-
if (!existsSync11(dir)) return;
|
|
1157
|
-
try {
|
|
1158
|
-
const entries = readdirSync8(dir, { withFileTypes: true });
|
|
1159
|
-
const hasNeuron = entries.some((e) => e.isFile() && e.name.endsWith(".neuron"));
|
|
1160
|
-
if (hasNeuron) {
|
|
1161
|
-
cb(dir);
|
|
1162
|
-
return;
|
|
1163
|
-
}
|
|
1164
|
-
for (const entry of entries) {
|
|
1165
|
-
if (entry.isDirectory() && !entry.name.startsWith(".")) {
|
|
1166
|
-
walkNeuronDirs(join12(dir, entry.name), cb);
|
|
1167
|
-
}
|
|
1168
|
-
}
|
|
1169
|
-
} catch {
|
|
1170
|
-
}
|
|
1171
|
-
}
|
|
1172
|
-
function readCounter(dir) {
|
|
1173
|
-
try {
|
|
1174
|
-
const files = readdirSync8(dir).filter((f) => /^\d+\.neuron$/.test(f));
|
|
1175
|
-
if (files.length === 0) return 0;
|
|
1176
|
-
return Math.max(...files.map((f) => parseInt(f, 10)));
|
|
1177
|
-
} catch {
|
|
1178
|
-
return 0;
|
|
1179
|
-
}
|
|
1180
|
-
}
|
|
1181
|
-
function propagateToShared(brainRoot, targetPath) {
|
|
1182
|
-
try {
|
|
1183
|
-
const agentsIdx = brainRoot.indexOf("/agents/");
|
|
1184
|
-
if (agentsIdx === -1) return false;
|
|
1185
|
-
const multiBrainRoot = brainRoot.slice(0, agentsIdx);
|
|
1186
|
-
const sharedRoot = join12(multiBrainRoot, "shared");
|
|
1187
|
-
if (!existsSync11(sharedRoot)) return false;
|
|
1188
|
-
const episodes = readEpisodes(brainRoot);
|
|
1189
|
-
const neuronName = targetPath.split("/").pop() || "";
|
|
1190
|
-
const hasRelevantEpisode = episodes.some(
|
|
1191
|
-
(ep) => PROPAGATION_EPISODE_TYPES.includes(ep.type) && (ep.path.includes(neuronName) || ep.detail.includes(neuronName))
|
|
1192
|
-
);
|
|
1193
|
-
if (!hasRelevantEpisode) return false;
|
|
1194
|
-
growNeuron(sharedRoot, targetPath);
|
|
1195
|
-
console.log(` \u{1F4E1} propagated to shared: ${targetPath}`);
|
|
1196
|
-
return true;
|
|
1197
|
-
} catch {
|
|
1198
|
-
return false;
|
|
1199
|
-
}
|
|
1200
|
-
}
|
|
1201
|
-
|
|
1202
|
-
// src/inbox.ts
|
|
1203
1216
|
var INBOX_DIR = "_inbox";
|
|
1204
1217
|
var CORRECTIONS_FILE = "corrections.jsonl";
|
|
1205
1218
|
var DOPAMINE_ALLOWED_ROLES = ["pm", "admin", "lead"];
|
|
@@ -1762,24 +1775,21 @@ var NEGATION_PATTERNS = [
|
|
|
1762
1775
|
/\bnever\b/i,
|
|
1763
1776
|
/\binstead\b/i,
|
|
1764
1777
|
/^no[,.\s!]/i,
|
|
1765
|
-
/\bdon[''\u2019]?t\s+use\b/i,
|
|
1766
1778
|
/\bavoid\b/i,
|
|
1767
|
-
// Korean negation —
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
/쓰지\s*않/
|
|
1775
|
-
// "do not use" specifically
|
|
1779
|
+
// Korean negation — require AI-directed imperative context:
|
|
1780
|
+
// "X하지 마" (don't X) — must have a verb object before 지 마
|
|
1781
|
+
/[을를은는도이가]\s*[가-힣]+지\s*마/,
|
|
1782
|
+
// "X 하면 안 돼" (must not X) — conditional + prohibition
|
|
1783
|
+
/하면\s*안\s*돼/,
|
|
1784
|
+
// "X 쓰지 마" (don't use X) — explicit "don't use"
|
|
1785
|
+
/쓰지\s*마/
|
|
1776
1786
|
];
|
|
1777
1787
|
var AFFIRMATION_PATTERNS = [
|
|
1778
|
-
/\balways\b/i,
|
|
1779
1788
|
/\bshould\s+always\b/i,
|
|
1780
1789
|
/\buse\s+\w+\s+instead\b/i,
|
|
1781
|
-
// Korean affirmation
|
|
1782
|
-
|
|
1790
|
+
// Korean affirmation — require directive context
|
|
1791
|
+
/항상\s*[가-힣]+[해하]/
|
|
1792
|
+
// "항상 X해" (always do X)
|
|
1783
1793
|
];
|
|
1784
1794
|
var MUST_PATTERNS = [
|
|
1785
1795
|
/\bmust\b/i,
|
|
@@ -1841,7 +1851,8 @@ function digestTranscript(brainRoot, transcriptPath, sessionId) {
|
|
|
1841
1851
|
console.log(`\u{1F527} digest: ${toolFailures.length} tool failure(s), ${retries.length} retry pattern(s) logged`);
|
|
1842
1852
|
}
|
|
1843
1853
|
const corrections = extractCorrections(messages);
|
|
1844
|
-
|
|
1854
|
+
const fired = autoFireCandidates(brainRoot, corrections);
|
|
1855
|
+
if (corrections.length === 0 && toolFailures.length === 0 && fired === 0) {
|
|
1845
1856
|
console.log(`\u{1F4DD} digest: no corrections found in session ${resolvedSessionId}`);
|
|
1846
1857
|
writeAuditLog(brainRoot, resolvedSessionId, [], totalLines);
|
|
1847
1858
|
return { corrections: 0, skipped: messages.length, toolFailures: toolFailures.length, transcriptPath, sessionId: resolvedSessionId };
|
|
@@ -1998,12 +2009,15 @@ function extractCorrections(messages) {
|
|
|
1998
2009
|
const corrections = [];
|
|
1999
2010
|
for (const text of messages) {
|
|
2000
2011
|
if (corrections.length >= MAX_CORRECTIONS_PER_SESSION) break;
|
|
2012
|
+
const trimmed = text.trim();
|
|
2001
2013
|
if (text.length < MIN_CORRECTION_LENGTH) continue;
|
|
2002
|
-
if (/^[\/!]/.test(
|
|
2003
|
-
if (
|
|
2004
|
-
if (/^<[a-zA-Z]/.test(
|
|
2005
|
-
if (/^Base directory for this skill:/i.test(
|
|
2006
|
-
if (/^[•·▸▶\-\*]\s/.test(
|
|
2014
|
+
if (/^[\/!]/.test(trimmed)) continue;
|
|
2015
|
+
if (/[??]\s*[!!]?\s*$/.test(trimmed)) continue;
|
|
2016
|
+
if (/^<[a-zA-Z]/.test(trimmed)) continue;
|
|
2017
|
+
if (/^Base directory for this skill:/i.test(trimmed)) continue;
|
|
2018
|
+
if (/^[•·▸▶\-\*]\s/.test(trimmed)) continue;
|
|
2019
|
+
if (/<[a-zA-Z][a-zA-Z-]*>/.test(trimmed) && /<\/[a-zA-Z]/.test(trimmed)) continue;
|
|
2020
|
+
if (isNarrativeKorean(trimmed)) continue;
|
|
2007
2021
|
const correction = detectCorrection(text);
|
|
2008
2022
|
if (correction) {
|
|
2009
2023
|
corrections.push(correction);
|
|
@@ -2011,12 +2025,63 @@ function extractCorrections(messages) {
|
|
|
2011
2025
|
}
|
|
2012
2026
|
return corrections;
|
|
2013
2027
|
}
|
|
2028
|
+
function isNarrativeKorean(text) {
|
|
2029
|
+
const NARRATIVE_MARKERS = [
|
|
2030
|
+
/이유는/,
|
|
2031
|
+
// "the reason is..."
|
|
2032
|
+
/예를\s*들면/,
|
|
2033
|
+
// "for example..."
|
|
2034
|
+
/그래서/,
|
|
2035
|
+
// "so/therefore..."
|
|
2036
|
+
/그런데/,
|
|
2037
|
+
// "but then..."
|
|
2038
|
+
/왜냐하면/,
|
|
2039
|
+
// "because..."
|
|
2040
|
+
/거든/,
|
|
2041
|
+
// "...you see" (explanatory)
|
|
2042
|
+
/있었[던거어]/,
|
|
2043
|
+
// "there was..." (past narrative)
|
|
2044
|
+
/중인데/,
|
|
2045
|
+
// "...in the middle of" (ongoing situation)
|
|
2046
|
+
/거 같은데/,
|
|
2047
|
+
// "it seems like..." (speculation)
|
|
2048
|
+
/어떻게\s*생각/
|
|
2049
|
+
// "what do you think?" (asking opinion)
|
|
2050
|
+
];
|
|
2051
|
+
const markerCount = NARRATIVE_MARKERS.filter((p) => p.test(text)).length;
|
|
2052
|
+
return markerCount >= 2;
|
|
2053
|
+
}
|
|
2054
|
+
function autoFireCandidates(brainRoot, corrections) {
|
|
2055
|
+
if (corrections.length > 0) return 0;
|
|
2056
|
+
const candidates = listCandidates(brainRoot);
|
|
2057
|
+
if (candidates.length === 0) return 0;
|
|
2058
|
+
let fired = 0;
|
|
2059
|
+
for (const cand of candidates) {
|
|
2060
|
+
try {
|
|
2061
|
+
const newCounter = fireNeuron(brainRoot, cand.candidatePath);
|
|
2062
|
+
fired++;
|
|
2063
|
+
if (newCounter >= CANDIDATE_THRESHOLD) {
|
|
2064
|
+
growCandidate(brainRoot, cand.targetPath);
|
|
2065
|
+
}
|
|
2066
|
+
} catch {
|
|
2067
|
+
}
|
|
2068
|
+
}
|
|
2069
|
+
if (fired > 0) {
|
|
2070
|
+
console.log(`\u{1F504} agent-evaluator: ${fired} candidate(s) advanced (session without corrections)`);
|
|
2071
|
+
}
|
|
2072
|
+
return fired;
|
|
2073
|
+
}
|
|
2014
2074
|
function detectCorrection(text) {
|
|
2015
2075
|
const isNegation = NEGATION_PATTERNS.some((p) => p.test(text));
|
|
2016
2076
|
const isMust = MUST_PATTERNS.some((p) => p.test(text));
|
|
2017
2077
|
const isWarn = WARN_PATTERNS.some((p) => p.test(text));
|
|
2018
2078
|
const isAffirmation = AFFIRMATION_PATTERNS.some((p) => p.test(text));
|
|
2019
2079
|
if (!isNegation && !isMust && !isWarn && !isAffirmation) return null;
|
|
2080
|
+
const categories = [isNegation, isMust, isWarn, isAffirmation].filter(Boolean).length;
|
|
2081
|
+
const koreanRatio = (text.match(/[\uAC00-\uD7AF]/g) || []).length / Math.max(text.length, 1);
|
|
2082
|
+
if (koreanRatio > 0.3 && categories < 2) {
|
|
2083
|
+
if (text.length > 100) return null;
|
|
2084
|
+
}
|
|
2020
2085
|
let prefix;
|
|
2021
2086
|
if (isNegation) prefix = "NO";
|
|
2022
2087
|
else if (isMust) prefix = "MUST";
|