hebbian 0.7.1 → 0.8.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 +428 -95
- package/dist/bin/hebbian.js.map +1 -1
- package/dist/candidates.d.ts +8 -0
- package/dist/candidates.d.ts.map +1 -1
- package/dist/constants.d.ts +2 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/cron.d.ts +24 -0
- package/dist/cron.d.ts.map +1 -0
- package/dist/evolve.d.ts.map +1 -1
- package/dist/feedback.d.ts +23 -0
- package/dist/feedback.d.ts.map +1 -0
- package/dist/index.d.ts +5 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +351 -77
- package/dist/index.js.map +1 -1
- package/dist/init.d.ts.map +1 -1
- package/dist/scanner.d.ts +6 -1
- package/dist/scanner.d.ts.map +1 -1
- package/dist/types.d.ts +1 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -73,6 +73,8 @@ function resolveSharedBrain(brainRoot) {
|
|
|
73
73
|
}
|
|
74
74
|
var AGENTS_DIR = "agents";
|
|
75
75
|
var SHARED_DIR = "shared";
|
|
76
|
+
var SKILLS_DIR = "skills";
|
|
77
|
+
var PROPAGATION_EPISODE_TYPES = ["tool-failure", "retry-pattern"];
|
|
76
78
|
|
|
77
79
|
// src/scanner.ts
|
|
78
80
|
import { readdirSync, statSync, readFileSync, existsSync as existsSync2 } from "fs";
|
|
@@ -198,6 +200,11 @@ function walkRegion(dir, regionRoot, depth) {
|
|
|
198
200
|
}
|
|
199
201
|
return neurons;
|
|
200
202
|
}
|
|
203
|
+
function scanSkills(brainRoot) {
|
|
204
|
+
const skillsPath = join(brainRoot, SKILLS_DIR);
|
|
205
|
+
if (!existsSync2(skillsPath)) return [];
|
|
206
|
+
return walkRegion(skillsPath, skillsPath, 0);
|
|
207
|
+
}
|
|
201
208
|
function readAxons(regionPath) {
|
|
202
209
|
const axonPath = join(regionPath, ".axon");
|
|
203
210
|
if (!existsSync2(axonPath)) return [];
|
|
@@ -413,8 +420,8 @@ function growNeuron(brainRoot, neuronPath) {
|
|
|
413
420
|
}
|
|
414
421
|
const parts = neuronPath.split("/");
|
|
415
422
|
const regionName = parts[0];
|
|
416
|
-
if (!REGIONS.includes(regionName)) {
|
|
417
|
-
throw new Error(`Invalid region: ${regionName}. Valid: ${REGIONS.join(", ")}`);
|
|
423
|
+
if (regionName !== SKILLS_DIR && !REGIONS.includes(regionName)) {
|
|
424
|
+
throw new Error(`Invalid region: ${regionName}. Valid: ${REGIONS.join(", ")}, ${SKILLS_DIR}`);
|
|
418
425
|
}
|
|
419
426
|
const leafName = parts[parts.length - 1];
|
|
420
427
|
const newPrefix = leafName.match(/^(NO|DO|MUST|WARN)_/)?.[1] || "";
|
|
@@ -952,6 +959,12 @@ ${template.description}
|
|
|
952
959
|
}
|
|
953
960
|
}
|
|
954
961
|
mkdirSync4(join10(brainPath, "_agents", "global_inbox"), { recursive: true });
|
|
962
|
+
mkdirSync4(join10(brainPath, "skills"), { recursive: true });
|
|
963
|
+
writeFileSync7(
|
|
964
|
+
join10(brainPath, "skills", "_rules.md"),
|
|
965
|
+
"# Skills Library\n\nExecutable patterns learned through experience.\nNot part of the subsumption cascade \u2014 retrieval only.\n",
|
|
966
|
+
"utf8"
|
|
967
|
+
);
|
|
955
968
|
autoGitignore(brainPath);
|
|
956
969
|
console.log(`\u{1F9E0} Brain initialized at ${brainPath}`);
|
|
957
970
|
console.log(` 7 regions created: ${REGIONS.join(", ")}`);
|
|
@@ -993,8 +1006,73 @@ import { readFileSync as readFileSync5, writeFileSync as writeFileSync9, existsS
|
|
|
993
1006
|
import { join as join13 } from "path";
|
|
994
1007
|
|
|
995
1008
|
// src/candidates.ts
|
|
996
|
-
import { existsSync as
|
|
997
|
-
import { join as
|
|
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
|
|
998
1076
|
var CANDIDATE_THRESHOLD = 3;
|
|
999
1077
|
var CANDIDATE_DECAY_DAYS = 14;
|
|
1000
1078
|
var CANDIDATE_SEGMENT = "_candidates";
|
|
@@ -1011,20 +1089,21 @@ function growCandidate(brainRoot, neuronPath) {
|
|
|
1011
1089
|
const result = growNeuron(brainRoot, candidatePath);
|
|
1012
1090
|
if (result.counter >= CANDIDATE_THRESHOLD) {
|
|
1013
1091
|
const ok = moveCandidate(brainRoot, candidatePath, neuronPath);
|
|
1092
|
+
if (ok) propagateToShared(brainRoot, neuronPath);
|
|
1014
1093
|
return { ...result, path: ok ? neuronPath : result.path, promoted: ok };
|
|
1015
1094
|
}
|
|
1016
1095
|
console.log(` \u{1F331} candidate (${result.counter}/${CANDIDATE_THRESHOLD}): ${candidatePath}`);
|
|
1017
1096
|
return { ...result, promoted: false };
|
|
1018
1097
|
}
|
|
1019
1098
|
function moveCandidate(brainRoot, candidatePath, targetPath) {
|
|
1020
|
-
const src =
|
|
1021
|
-
if (!
|
|
1022
|
-
const dst =
|
|
1023
|
-
if (
|
|
1099
|
+
const src = join12(brainRoot, candidatePath);
|
|
1100
|
+
if (!existsSync11(src)) return false;
|
|
1101
|
+
const dst = join12(brainRoot, targetPath);
|
|
1102
|
+
if (existsSync11(dst)) {
|
|
1024
1103
|
fireNeuron(brainRoot, targetPath);
|
|
1025
1104
|
rmSync(src, { recursive: true, force: true });
|
|
1026
1105
|
} else {
|
|
1027
|
-
|
|
1106
|
+
mkdirSync6(dirname3(dst), { recursive: true });
|
|
1028
1107
|
renameSync3(src, dst);
|
|
1029
1108
|
}
|
|
1030
1109
|
console.log(`\u{1F393} promoted: ${candidatePath} \u2192 ${targetPath}`);
|
|
@@ -1036,15 +1115,16 @@ function promoteCandidates(brainRoot) {
|
|
|
1036
1115
|
const decayMs = CANDIDATE_DECAY_DAYS * 24 * 60 * 60 * 1e3;
|
|
1037
1116
|
const now = Date.now();
|
|
1038
1117
|
for (const region of REGIONS) {
|
|
1039
|
-
const candidateRoot =
|
|
1118
|
+
const candidateRoot = join12(brainRoot, region, CANDIDATE_SEGMENT);
|
|
1040
1119
|
walkNeuronDirs(candidateRoot, (neuronDir) => {
|
|
1041
|
-
const rel = relative3(
|
|
1120
|
+
const rel = relative3(join12(brainRoot, region), neuronDir);
|
|
1042
1121
|
const candidatePath = `${region}/${rel}`;
|
|
1043
1122
|
const targetPath = fromCandidatePath(candidatePath);
|
|
1044
1123
|
const counter = readCounter(neuronDir);
|
|
1045
1124
|
const mtime = statSync3(neuronDir).mtimeMs;
|
|
1046
1125
|
if (counter >= CANDIDATE_THRESHOLD) {
|
|
1047
1126
|
moveCandidate(brainRoot, candidatePath, targetPath);
|
|
1127
|
+
propagateToShared(brainRoot, targetPath);
|
|
1048
1128
|
promoted.push(targetPath);
|
|
1049
1129
|
} else if (now - mtime > decayMs) {
|
|
1050
1130
|
rmSync(neuronDir, { recursive: true, force: true });
|
|
@@ -1059,9 +1139,9 @@ function listCandidates(brainRoot) {
|
|
|
1059
1139
|
const results = [];
|
|
1060
1140
|
const now = Date.now();
|
|
1061
1141
|
for (const region of REGIONS) {
|
|
1062
|
-
const candidateRoot =
|
|
1142
|
+
const candidateRoot = join12(brainRoot, region, CANDIDATE_SEGMENT);
|
|
1063
1143
|
walkNeuronDirs(candidateRoot, (neuronDir) => {
|
|
1064
|
-
const rel = relative3(
|
|
1144
|
+
const rel = relative3(join12(brainRoot, region), neuronDir);
|
|
1065
1145
|
const candidatePath = `${region}/${rel}`;
|
|
1066
1146
|
const targetPath = fromCandidatePath(candidatePath);
|
|
1067
1147
|
const counter = readCounter(neuronDir);
|
|
@@ -1073,9 +1153,9 @@ function listCandidates(brainRoot) {
|
|
|
1073
1153
|
return results;
|
|
1074
1154
|
}
|
|
1075
1155
|
function walkNeuronDirs(dir, cb) {
|
|
1076
|
-
if (!
|
|
1156
|
+
if (!existsSync11(dir)) return;
|
|
1077
1157
|
try {
|
|
1078
|
-
const entries =
|
|
1158
|
+
const entries = readdirSync8(dir, { withFileTypes: true });
|
|
1079
1159
|
const hasNeuron = entries.some((e) => e.isFile() && e.name.endsWith(".neuron"));
|
|
1080
1160
|
if (hasNeuron) {
|
|
1081
1161
|
cb(dir);
|
|
@@ -1083,7 +1163,7 @@ function walkNeuronDirs(dir, cb) {
|
|
|
1083
1163
|
}
|
|
1084
1164
|
for (const entry of entries) {
|
|
1085
1165
|
if (entry.isDirectory() && !entry.name.startsWith(".")) {
|
|
1086
|
-
walkNeuronDirs(
|
|
1166
|
+
walkNeuronDirs(join12(dir, entry.name), cb);
|
|
1087
1167
|
}
|
|
1088
1168
|
}
|
|
1089
1169
|
} catch {
|
|
@@ -1091,75 +1171,32 @@ function walkNeuronDirs(dir, cb) {
|
|
|
1091
1171
|
}
|
|
1092
1172
|
function readCounter(dir) {
|
|
1093
1173
|
try {
|
|
1094
|
-
const files =
|
|
1174
|
+
const files = readdirSync8(dir).filter((f) => /^\d+\.neuron$/.test(f));
|
|
1095
1175
|
if (files.length === 0) return 0;
|
|
1096
1176
|
return Math.max(...files.map((f) => parseInt(f, 10)));
|
|
1097
1177
|
} catch {
|
|
1098
1178
|
return 0;
|
|
1099
1179
|
}
|
|
1100
1180
|
}
|
|
1101
|
-
|
|
1102
|
-
// src/episode.ts
|
|
1103
|
-
import { readdirSync as readdirSync8, readFileSync as readFileSync4, writeFileSync as writeFileSync8, mkdirSync as mkdirSync6, existsSync as existsSync11 } from "fs";
|
|
1104
|
-
import { join as join12 } from "path";
|
|
1105
|
-
var MAX_EPISODES = 100;
|
|
1106
|
-
var SESSION_LOG_DIR = "hippocampus/session_log";
|
|
1107
|
-
function logEpisode(brainRoot, type, path, detail, extra) {
|
|
1108
|
-
const logDir = join12(brainRoot, SESSION_LOG_DIR);
|
|
1109
|
-
if (!existsSync11(logDir)) {
|
|
1110
|
-
mkdirSync6(logDir, { recursive: true });
|
|
1111
|
-
}
|
|
1112
|
-
const nextSlot = getNextSlot(logDir);
|
|
1113
|
-
const episode = {
|
|
1114
|
-
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1115
|
-
type,
|
|
1116
|
-
path,
|
|
1117
|
-
detail,
|
|
1118
|
-
...extra?.outcome ? { outcome: extra.outcome } : {},
|
|
1119
|
-
...extra?.neurons ? { neurons: extra.neurons } : {}
|
|
1120
|
-
};
|
|
1121
|
-
writeFileSync8(
|
|
1122
|
-
join12(logDir, `memory${nextSlot}.neuron`),
|
|
1123
|
-
JSON.stringify(episode),
|
|
1124
|
-
"utf8"
|
|
1125
|
-
);
|
|
1126
|
-
}
|
|
1127
|
-
function readEpisodes(brainRoot) {
|
|
1128
|
-
const logDir = join12(brainRoot, SESSION_LOG_DIR);
|
|
1129
|
-
if (!existsSync11(logDir)) return [];
|
|
1130
|
-
const episodes = [];
|
|
1131
|
-
let entries;
|
|
1132
|
-
try {
|
|
1133
|
-
entries = readdirSync8(logDir);
|
|
1134
|
-
} catch {
|
|
1135
|
-
return [];
|
|
1136
|
-
}
|
|
1137
|
-
for (const entry of entries) {
|
|
1138
|
-
if (!entry.startsWith("memory") || !entry.endsWith(".neuron")) continue;
|
|
1139
|
-
try {
|
|
1140
|
-
const content = readFileSync4(join12(logDir, entry), "utf8");
|
|
1141
|
-
if (content.trim()) {
|
|
1142
|
-
episodes.push(JSON.parse(content));
|
|
1143
|
-
}
|
|
1144
|
-
} catch {
|
|
1145
|
-
}
|
|
1146
|
-
}
|
|
1147
|
-
episodes.sort((a, b) => a.ts.localeCompare(b.ts));
|
|
1148
|
-
return episodes;
|
|
1149
|
-
}
|
|
1150
|
-
function getNextSlot(logDir) {
|
|
1151
|
-
let maxSlot = 0;
|
|
1181
|
+
function propagateToShared(brainRoot, targetPath) {
|
|
1152
1182
|
try {
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
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;
|
|
1159
1197
|
} catch {
|
|
1198
|
+
return false;
|
|
1160
1199
|
}
|
|
1161
|
-
const next = maxSlot + 1;
|
|
1162
|
-
return next > MAX_EPISODES ? maxSlot % MAX_EPISODES + 1 : next;
|
|
1163
1200
|
}
|
|
1164
1201
|
|
|
1165
1202
|
// src/inbox.ts
|
|
@@ -2566,6 +2603,7 @@ function validateActions(actions, _brain) {
|
|
|
2566
2603
|
return false;
|
|
2567
2604
|
}
|
|
2568
2605
|
const region = action.path.split("/")[0];
|
|
2606
|
+
if (region === SKILLS_DIR) return true;
|
|
2569
2607
|
if (!region || PROTECTED_REGIONS.includes(region)) {
|
|
2570
2608
|
console.log(` \u{1F6E1}\uFE0F blocked: ${action.type} ${action.path} (protected region)`);
|
|
2571
2609
|
return false;
|
|
@@ -2590,7 +2628,11 @@ function executeActions(brainRoot, actions) {
|
|
|
2590
2628
|
fireNeuron(brainRoot, action.path);
|
|
2591
2629
|
break;
|
|
2592
2630
|
case "grow":
|
|
2593
|
-
|
|
2631
|
+
if (action.path.startsWith(SKILLS_DIR + "/")) {
|
|
2632
|
+
growNeuron(brainRoot, action.path);
|
|
2633
|
+
} else {
|
|
2634
|
+
growCandidate(brainRoot, action.path);
|
|
2635
|
+
}
|
|
2594
2636
|
break;
|
|
2595
2637
|
case "signal":
|
|
2596
2638
|
signalNeuron(brainRoot, action.path, action.signal || "dopamine");
|
|
@@ -2626,6 +2668,226 @@ function actionIcon(type) {
|
|
|
2626
2668
|
return "\u2753";
|
|
2627
2669
|
}
|
|
2628
2670
|
}
|
|
2671
|
+
|
|
2672
|
+
// src/cron.ts
|
|
2673
|
+
import { writeFileSync as writeFileSync14, existsSync as existsSync17, unlinkSync } from "fs";
|
|
2674
|
+
import { join as join18 } from "path";
|
|
2675
|
+
import { execSync as execSync4 } from "child_process";
|
|
2676
|
+
var PLIST_LABEL = "com.hebbian.nightly-prune";
|
|
2677
|
+
var FEEDBACK_PLIST_LABEL = "com.hebbian.feedback";
|
|
2678
|
+
function getLaunchAgentsDir() {
|
|
2679
|
+
return join18(process.env.HOME || "~", "Library", "LaunchAgents");
|
|
2680
|
+
}
|
|
2681
|
+
function getPlistPath(label) {
|
|
2682
|
+
return join18(getLaunchAgentsDir(), `${label}.plist`);
|
|
2683
|
+
}
|
|
2684
|
+
function getNpxPath() {
|
|
2685
|
+
try {
|
|
2686
|
+
return execSync4("which npx", { encoding: "utf8" }).trim();
|
|
2687
|
+
} catch {
|
|
2688
|
+
return "/opt/homebrew/bin/npx";
|
|
2689
|
+
}
|
|
2690
|
+
}
|
|
2691
|
+
function generatePrunePlist(brainRoot, hour = 2, minute = 0) {
|
|
2692
|
+
const npx = getNpxPath();
|
|
2693
|
+
const apiKey = process.env.GEMINI_API_KEY || "";
|
|
2694
|
+
const home = process.env.HOME || "/Users/sweetheart";
|
|
2695
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
2696
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
2697
|
+
<plist version="1.0">
|
|
2698
|
+
<dict>
|
|
2699
|
+
<key>Label</key>
|
|
2700
|
+
<string>${PLIST_LABEL}</string>
|
|
2701
|
+
<key>ProgramArguments</key>
|
|
2702
|
+
<array>
|
|
2703
|
+
<string>${npx}</string>
|
|
2704
|
+
<string>hebbian</string>
|
|
2705
|
+
<string>evolve</string>
|
|
2706
|
+
<string>prune</string>
|
|
2707
|
+
<string>--brain</string>
|
|
2708
|
+
<string>${brainRoot}</string>
|
|
2709
|
+
</array>
|
|
2710
|
+
<key>EnvironmentVariables</key>
|
|
2711
|
+
<dict>
|
|
2712
|
+
<key>GEMINI_API_KEY</key>
|
|
2713
|
+
<string>${apiKey}</string>
|
|
2714
|
+
<key>PATH</key>
|
|
2715
|
+
<string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin</string>
|
|
2716
|
+
</dict>
|
|
2717
|
+
<key>StartCalendarInterval</key>
|
|
2718
|
+
<dict>
|
|
2719
|
+
<key>Hour</key>
|
|
2720
|
+
<integer>${hour}</integer>
|
|
2721
|
+
<key>Minute</key>
|
|
2722
|
+
<integer>${minute}</integer>
|
|
2723
|
+
</dict>
|
|
2724
|
+
<key>StandardOutPath</key>
|
|
2725
|
+
<string>${home}/Library/Logs/hebbian-prune.log</string>
|
|
2726
|
+
<key>StandardErrorPath</key>
|
|
2727
|
+
<string>${home}/Library/Logs/hebbian-prune.log</string>
|
|
2728
|
+
</dict>
|
|
2729
|
+
</plist>`;
|
|
2730
|
+
}
|
|
2731
|
+
function generateFeedbackPlist(brainRoot, intervalMinutes = 15) {
|
|
2732
|
+
const npx = getNpxPath();
|
|
2733
|
+
const home = process.env.HOME || "/Users/sweetheart";
|
|
2734
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
2735
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
2736
|
+
<plist version="1.0">
|
|
2737
|
+
<dict>
|
|
2738
|
+
<key>Label</key>
|
|
2739
|
+
<string>${FEEDBACK_PLIST_LABEL}</string>
|
|
2740
|
+
<key>ProgramArguments</key>
|
|
2741
|
+
<array>
|
|
2742
|
+
<string>${npx}</string>
|
|
2743
|
+
<string>hebbian</string>
|
|
2744
|
+
<string>feedback</string>
|
|
2745
|
+
<string>scan</string>
|
|
2746
|
+
<string>--brain</string>
|
|
2747
|
+
<string>${brainRoot}</string>
|
|
2748
|
+
</array>
|
|
2749
|
+
<key>EnvironmentVariables</key>
|
|
2750
|
+
<dict>
|
|
2751
|
+
<key>PATH</key>
|
|
2752
|
+
<string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin</string>
|
|
2753
|
+
</dict>
|
|
2754
|
+
<key>StartInterval</key>
|
|
2755
|
+
<integer>${intervalMinutes * 60}</integer>
|
|
2756
|
+
<key>StandardOutPath</key>
|
|
2757
|
+
<string>${home}/Library/Logs/hebbian-feedback.log</string>
|
|
2758
|
+
<key>StandardErrorPath</key>
|
|
2759
|
+
<string>${home}/Library/Logs/hebbian-feedback.log</string>
|
|
2760
|
+
</dict>
|
|
2761
|
+
</plist>`;
|
|
2762
|
+
}
|
|
2763
|
+
function installCron(brainRoot, type = "prune") {
|
|
2764
|
+
const label = type === "prune" ? PLIST_LABEL : FEEDBACK_PLIST_LABEL;
|
|
2765
|
+
const plistPath = getPlistPath(label);
|
|
2766
|
+
const plistContent = type === "prune" ? generatePrunePlist(brainRoot) : generateFeedbackPlist(brainRoot);
|
|
2767
|
+
try {
|
|
2768
|
+
execSync4(`launchctl unload ${plistPath} 2>/dev/null`, { encoding: "utf8" });
|
|
2769
|
+
} catch {
|
|
2770
|
+
}
|
|
2771
|
+
writeFileSync14(plistPath, plistContent, "utf8");
|
|
2772
|
+
execSync4(`launchctl load ${plistPath}`, { encoding: "utf8" });
|
|
2773
|
+
console.log(`\u2705 ${type} cron installed: ${plistPath}`);
|
|
2774
|
+
}
|
|
2775
|
+
function uninstallCron(type = "prune") {
|
|
2776
|
+
const label = type === "prune" ? PLIST_LABEL : FEEDBACK_PLIST_LABEL;
|
|
2777
|
+
const plistPath = getPlistPath(label);
|
|
2778
|
+
if (!existsSync17(plistPath)) {
|
|
2779
|
+
console.log(`\u26A0\uFE0F ${type} cron not installed`);
|
|
2780
|
+
return;
|
|
2781
|
+
}
|
|
2782
|
+
try {
|
|
2783
|
+
execSync4(`launchctl unload ${plistPath}`, { encoding: "utf8" });
|
|
2784
|
+
} catch {
|
|
2785
|
+
}
|
|
2786
|
+
unlinkSync(plistPath);
|
|
2787
|
+
console.log(`\u{1F5D1}\uFE0F ${type} cron uninstalled`);
|
|
2788
|
+
}
|
|
2789
|
+
function checkCron(type = "prune") {
|
|
2790
|
+
const label = type === "prune" ? PLIST_LABEL : FEEDBACK_PLIST_LABEL;
|
|
2791
|
+
const plistPath = getPlistPath(label);
|
|
2792
|
+
return { installed: existsSync17(plistPath), path: plistPath };
|
|
2793
|
+
}
|
|
2794
|
+
|
|
2795
|
+
// src/feedback.ts
|
|
2796
|
+
import { existsSync as existsSync18, readdirSync as readdirSync11, statSync as statSync5, readFileSync as readFileSync10, writeFileSync as writeFileSync15, mkdirSync as mkdirSync11 } from "fs";
|
|
2797
|
+
import { join as join19 } from "path";
|
|
2798
|
+
var WATERMARK_FILE = "_feedback_watermark.json";
|
|
2799
|
+
var WARN_PREFIX = "WARN_shared_";
|
|
2800
|
+
function scanSharedBrain(brainRoot) {
|
|
2801
|
+
const sharedRoot = join19(brainRoot, SHARED_DIR);
|
|
2802
|
+
if (!existsSync18(sharedRoot)) return [];
|
|
2803
|
+
const watermark = readWatermark(sharedRoot);
|
|
2804
|
+
const deltas = [];
|
|
2805
|
+
for (const region of REGIONS) {
|
|
2806
|
+
const regionPath = join19(sharedRoot, region);
|
|
2807
|
+
if (!existsSync18(regionPath)) continue;
|
|
2808
|
+
walkForNeurons(regionPath, regionPath, (neuronDir, counter) => {
|
|
2809
|
+
const modTime = statSync5(neuronDir).mtime;
|
|
2810
|
+
if (modTime.getTime() <= watermark) return;
|
|
2811
|
+
const name = neuronDir.split("/").pop() || "";
|
|
2812
|
+
if (name.startsWith(WARN_PREFIX)) return;
|
|
2813
|
+
const relPath = region + "/" + neuronDir.slice(regionPath.length + 1);
|
|
2814
|
+
deltas.push({ path: relPath, counter, modTime });
|
|
2815
|
+
});
|
|
2816
|
+
}
|
|
2817
|
+
return deltas;
|
|
2818
|
+
}
|
|
2819
|
+
function propagateToAgents(brainRoot, deltas) {
|
|
2820
|
+
const agentsDir = join19(brainRoot, AGENTS_DIR);
|
|
2821
|
+
if (!existsSync18(agentsDir) || deltas.length === 0) {
|
|
2822
|
+
return { scanned: deltas.length, propagated: 0, agents: [] };
|
|
2823
|
+
}
|
|
2824
|
+
const agentNames = readdirSync11(agentsDir, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".") && !e.name.startsWith("_")).map((e) => e.name);
|
|
2825
|
+
let propagated = 0;
|
|
2826
|
+
const touchedAgents = /* @__PURE__ */ new Set();
|
|
2827
|
+
for (const delta of deltas) {
|
|
2828
|
+
const neuronName = delta.path.split("/").pop() || "";
|
|
2829
|
+
const warnPath = delta.path.replace(/\/([^/]+)$/, `/${WARN_PREFIX}${neuronName}`);
|
|
2830
|
+
for (const agent of agentNames) {
|
|
2831
|
+
const agentBrain = join19(agentsDir, agent);
|
|
2832
|
+
if (!existsSync18(join19(agentBrain, "cortex")) && !existsSync18(join19(agentBrain, "brainstem"))) continue;
|
|
2833
|
+
try {
|
|
2834
|
+
growNeuron(agentBrain, warnPath);
|
|
2835
|
+
logEpisode(agentBrain, "feedback", warnPath, `shared learning: ${delta.path}`);
|
|
2836
|
+
propagated++;
|
|
2837
|
+
touchedAgents.add(agent);
|
|
2838
|
+
} catch {
|
|
2839
|
+
}
|
|
2840
|
+
}
|
|
2841
|
+
}
|
|
2842
|
+
return { scanned: deltas.length, propagated, agents: [...touchedAgents] };
|
|
2843
|
+
}
|
|
2844
|
+
function runFeedback(brainRoot) {
|
|
2845
|
+
const deltas = scanSharedBrain(brainRoot);
|
|
2846
|
+
if (deltas.length === 0) {
|
|
2847
|
+
console.log("\u{1F4E1} feedback: no new shared neurons");
|
|
2848
|
+
return { scanned: 0, propagated: 0, agents: [] };
|
|
2849
|
+
}
|
|
2850
|
+
const result = propagateToAgents(brainRoot, deltas);
|
|
2851
|
+
const latestTime = Math.max(...deltas.map((d) => d.modTime.getTime()));
|
|
2852
|
+
writeWatermark(join19(brainRoot, SHARED_DIR), latestTime);
|
|
2853
|
+
console.log(`\u{1F4E1} feedback: ${result.scanned} shared neuron(s) \u2192 ${result.propagated} warning(s) to ${result.agents.join(", ")}`);
|
|
2854
|
+
return result;
|
|
2855
|
+
}
|
|
2856
|
+
function readWatermark(sharedRoot) {
|
|
2857
|
+
const wmPath = join19(sharedRoot, WATERMARK_FILE);
|
|
2858
|
+
if (!existsSync18(wmPath)) return 0;
|
|
2859
|
+
try {
|
|
2860
|
+
const data = JSON.parse(readFileSync10(wmPath, "utf8"));
|
|
2861
|
+
return data.timestamp || 0;
|
|
2862
|
+
} catch {
|
|
2863
|
+
return 0;
|
|
2864
|
+
}
|
|
2865
|
+
}
|
|
2866
|
+
function writeWatermark(sharedRoot, timestamp) {
|
|
2867
|
+
const wmPath = join19(sharedRoot, WATERMARK_FILE);
|
|
2868
|
+
mkdirSync11(sharedRoot, { recursive: true });
|
|
2869
|
+
writeFileSync15(wmPath, JSON.stringify({ timestamp, ts: new Date(timestamp).toISOString() }), "utf8");
|
|
2870
|
+
}
|
|
2871
|
+
function walkForNeurons(dir, regionRoot, cb) {
|
|
2872
|
+
let entries;
|
|
2873
|
+
try {
|
|
2874
|
+
entries = readdirSync11(dir, { withFileTypes: true });
|
|
2875
|
+
} catch {
|
|
2876
|
+
return;
|
|
2877
|
+
}
|
|
2878
|
+
const neuronFiles = entries.filter((e) => e.isFile() && /^\d+\.neuron$/.test(e.name));
|
|
2879
|
+
if (neuronFiles.length > 0) {
|
|
2880
|
+
const counter = Math.max(...neuronFiles.map((f) => parseInt(f.name, 10)));
|
|
2881
|
+
cb(dir, counter);
|
|
2882
|
+
return;
|
|
2883
|
+
}
|
|
2884
|
+
for (const entry of entries) {
|
|
2885
|
+
if (entry.name.startsWith("_") || entry.name.startsWith(".")) continue;
|
|
2886
|
+
if (entry.isDirectory()) {
|
|
2887
|
+
walkForNeurons(join19(dir, entry.name), regionRoot, cb);
|
|
2888
|
+
}
|
|
2889
|
+
}
|
|
2890
|
+
}
|
|
2629
2891
|
export {
|
|
2630
2892
|
AGENTS_DIR,
|
|
2631
2893
|
DECAY_DAYS,
|
|
@@ -2640,6 +2902,7 @@ export {
|
|
|
2640
2902
|
MAX_DEPTH,
|
|
2641
2903
|
MIN_CORRECTION_LENGTH,
|
|
2642
2904
|
OUTCOME_TYPES,
|
|
2905
|
+
PROPAGATION_EPISODE_TYPES,
|
|
2643
2906
|
PROTECTED_REGIONS_CONTRA,
|
|
2644
2907
|
REGIONS,
|
|
2645
2908
|
REGION_ICONS,
|
|
@@ -2648,10 +2911,12 @@ export {
|
|
|
2648
2911
|
SESSION_STATE_DIR,
|
|
2649
2912
|
SHARED_DIR,
|
|
2650
2913
|
SIGNAL_TYPES,
|
|
2914
|
+
SKILLS_DIR,
|
|
2651
2915
|
SPOTLIGHT_DAYS,
|
|
2652
2916
|
appendCorrection,
|
|
2653
2917
|
buildOutcomeSummary,
|
|
2654
2918
|
captureSessionStart,
|
|
2919
|
+
checkCron,
|
|
2655
2920
|
checkHooks,
|
|
2656
2921
|
classifyOutcome,
|
|
2657
2922
|
clearReports,
|
|
@@ -2668,6 +2933,8 @@ export {
|
|
|
2668
2933
|
extractCorrections,
|
|
2669
2934
|
fireNeuron,
|
|
2670
2935
|
fromCandidatePath,
|
|
2936
|
+
generateFeedbackPlist,
|
|
2937
|
+
generatePrunePlist,
|
|
2671
2938
|
getCurrentCounter,
|
|
2672
2939
|
getLastActivity,
|
|
2673
2940
|
getPendingReports,
|
|
@@ -2675,6 +2942,7 @@ export {
|
|
|
2675
2942
|
growCandidate,
|
|
2676
2943
|
growNeuron,
|
|
2677
2944
|
initBrain,
|
|
2945
|
+
installCron,
|
|
2678
2946
|
installHooks,
|
|
2679
2947
|
jaccardSimilarity,
|
|
2680
2948
|
listCandidates,
|
|
@@ -2683,6 +2951,8 @@ export {
|
|
|
2683
2951
|
printDiag,
|
|
2684
2952
|
processInbox,
|
|
2685
2953
|
promoteCandidates,
|
|
2954
|
+
propagateToAgents,
|
|
2955
|
+
propagateToShared,
|
|
2686
2956
|
readEpisodes,
|
|
2687
2957
|
readHookInput,
|
|
2688
2958
|
resolveAgentBrain,
|
|
@@ -2692,14 +2962,18 @@ export {
|
|
|
2692
2962
|
runDecay,
|
|
2693
2963
|
runDedup,
|
|
2694
2964
|
runEvolve,
|
|
2965
|
+
runFeedback,
|
|
2695
2966
|
runSubsumption,
|
|
2696
2967
|
scanBrain,
|
|
2968
|
+
scanSharedBrain,
|
|
2969
|
+
scanSkills,
|
|
2697
2970
|
signalNeuron,
|
|
2698
2971
|
startAPI,
|
|
2699
2972
|
startWatch,
|
|
2700
2973
|
stem,
|
|
2701
2974
|
toCandidatePath,
|
|
2702
2975
|
tokenize,
|
|
2976
|
+
uninstallCron,
|
|
2703
2977
|
uninstallHooks,
|
|
2704
2978
|
writeAllTiers
|
|
2705
2979
|
};
|