@ouro.bot/cli 0.1.0-alpha.53 → 0.1.0-alpha.55
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/changelog.json +16 -0
- package/dist/heart/config.js +9 -15
- package/dist/heart/core.js +64 -38
- package/dist/heart/daemon/daemon-cli.js +28 -6
- package/dist/heart/daemon/daemon-runtime-sync.js +134 -12
- package/dist/heart/daemon/launchd.js +8 -3
- package/dist/heart/daemon/runtime-metadata.js +101 -0
- package/dist/heart/daemon/sense-manager.js +19 -2
- package/dist/heart/identity.js +8 -18
- package/dist/mind/memory.js +17 -23
- package/package.json +1 -1
package/changelog.json
CHANGED
|
@@ -1,6 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"_note": "This changelog is maintained as part of the PR/version-bump workflow. Agent-curated, not auto-generated. Agents read this file directly via read_file to understand what changed between versions.",
|
|
3
3
|
"versions": [
|
|
4
|
+
{
|
|
5
|
+
"version": "0.1.0-alpha.55",
|
|
6
|
+
"changes": [
|
|
7
|
+
"Memory fact dedup now catches paraphrased duplicates via cosine similarity on existing embeddings, so semantically equivalent facts no longer slip past the word-overlap check.",
|
|
8
|
+
"Semantic dedup gracefully handles corrupt JSONL entries with missing or undefined embeddings instead of crashing on bad data.",
|
|
9
|
+
"Cosine similarity is now imported from associative-recall instead of duplicated in the memory module."
|
|
10
|
+
]
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"version": "0.1.0-alpha.54",
|
|
14
|
+
"changes": [
|
|
15
|
+
"Disk-backed runtime config now reloads from agent and secrets files instead of being held behind stale process-level caches, so config changes are picked up truthfully.",
|
|
16
|
+
"`ouro up` now treats runtime drift as a real restart trigger, including repo-root drift, config-fingerprint drift, and stale launchd boot persistence after branch/worktree changes.",
|
|
17
|
+
"BlueBubbles status now prefers fresh runtime health over stale managed-process crash snapshots, preventing false whole-sense `error` reports when the live channel is actually healthy."
|
|
18
|
+
]
|
|
19
|
+
},
|
|
4
20
|
{
|
|
5
21
|
"version": "0.1.0-alpha.53",
|
|
6
22
|
"changes": [
|
package/dist/heart/config.js
CHANGED
|
@@ -138,7 +138,7 @@ function defaultRuntimeConfig() {
|
|
|
138
138
|
integrations: { ...DEFAULT_SECRETS_TEMPLATE.integrations },
|
|
139
139
|
};
|
|
140
140
|
}
|
|
141
|
-
let
|
|
141
|
+
let _runtimeConfigOverride = null;
|
|
142
142
|
let _testContextOverride = null;
|
|
143
143
|
function resolveConfigPath() {
|
|
144
144
|
return (0, identity_1.getAgentSecretsPath)();
|
|
@@ -160,15 +160,6 @@ function deepMerge(defaults, partial) {
|
|
|
160
160
|
return result;
|
|
161
161
|
}
|
|
162
162
|
function loadConfig() {
|
|
163
|
-
if (_cachedConfig) {
|
|
164
|
-
(0, runtime_1.emitNervesEvent)({
|
|
165
|
-
event: "config.load",
|
|
166
|
-
component: "config/identity",
|
|
167
|
-
message: "config loaded from cache",
|
|
168
|
-
meta: { source: "cache" },
|
|
169
|
-
});
|
|
170
|
-
return _cachedConfig;
|
|
171
|
-
}
|
|
172
163
|
const configPath = resolveConfigPath();
|
|
173
164
|
// Auto-create config directory if it doesn't exist
|
|
174
165
|
const configDir = path.dirname(configPath);
|
|
@@ -229,7 +220,10 @@ function loadConfig() {
|
|
|
229
220
|
},
|
|
230
221
|
});
|
|
231
222
|
}
|
|
232
|
-
|
|
223
|
+
const mergedConfig = deepMerge(defaultRuntimeConfig(), sanitizedFileData);
|
|
224
|
+
const config = _runtimeConfigOverride
|
|
225
|
+
? deepMerge(mergedConfig, _runtimeConfigOverride)
|
|
226
|
+
: mergedConfig;
|
|
233
227
|
(0, runtime_1.emitNervesEvent)({
|
|
234
228
|
event: "config.load",
|
|
235
229
|
component: "config/identity",
|
|
@@ -237,22 +231,22 @@ function loadConfig() {
|
|
|
237
231
|
meta: {
|
|
238
232
|
source: "disk",
|
|
239
233
|
used_defaults_only: Object.keys(fileData).length === 0,
|
|
234
|
+
override_applied: _runtimeConfigOverride !== null,
|
|
240
235
|
},
|
|
241
236
|
});
|
|
242
|
-
return
|
|
237
|
+
return config;
|
|
243
238
|
}
|
|
244
239
|
function resetConfigCache() {
|
|
245
|
-
|
|
240
|
+
_runtimeConfigOverride = null;
|
|
246
241
|
_testContextOverride = null;
|
|
247
242
|
}
|
|
248
243
|
function patchRuntimeConfig(partial) {
|
|
249
|
-
loadConfig(); // ensure _cachedConfig exists
|
|
250
244
|
const contextPatch = partial.context;
|
|
251
245
|
if (contextPatch) {
|
|
252
246
|
const base = _testContextOverride ?? identity_1.DEFAULT_AGENT_CONTEXT;
|
|
253
247
|
_testContextOverride = deepMerge(base, contextPatch);
|
|
254
248
|
}
|
|
255
|
-
|
|
249
|
+
_runtimeConfigOverride = deepMerge((_runtimeConfigOverride ?? {}), partial);
|
|
256
250
|
}
|
|
257
251
|
function getAzureConfig() {
|
|
258
252
|
const config = loadConfig();
|
package/dist/heart/core.js
CHANGED
|
@@ -25,6 +25,27 @@ const azure_1 = require("./providers/azure");
|
|
|
25
25
|
const minimax_1 = require("./providers/minimax");
|
|
26
26
|
const openai_codex_1 = require("./providers/openai-codex");
|
|
27
27
|
let _providerRuntime = null;
|
|
28
|
+
function getProviderRuntimeFingerprint() {
|
|
29
|
+
const provider = (0, identity_1.loadAgentConfig)().provider;
|
|
30
|
+
switch (provider) {
|
|
31
|
+
case "azure": {
|
|
32
|
+
const { apiKey, endpoint, deployment, modelName, apiVersion } = (0, config_1.getAzureConfig)();
|
|
33
|
+
return JSON.stringify({ provider, apiKey, endpoint, deployment, modelName, apiVersion });
|
|
34
|
+
}
|
|
35
|
+
case "anthropic": {
|
|
36
|
+
const { model, setupToken } = (0, config_1.getAnthropicConfig)();
|
|
37
|
+
return JSON.stringify({ provider, model, setupToken });
|
|
38
|
+
}
|
|
39
|
+
case "minimax": {
|
|
40
|
+
const { apiKey, model } = (0, config_1.getMinimaxConfig)();
|
|
41
|
+
return JSON.stringify({ provider, apiKey, model });
|
|
42
|
+
}
|
|
43
|
+
case "openai-codex": {
|
|
44
|
+
const { model, oauthAccessToken } = (0, config_1.getOpenAICodexConfig)();
|
|
45
|
+
return JSON.stringify({ provider, model, oauthAccessToken });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
28
49
|
function createProviderRegistry() {
|
|
29
50
|
const factories = {
|
|
30
51
|
azure: azure_1.createAzureProviderRuntime,
|
|
@@ -40,42 +61,44 @@ function createProviderRegistry() {
|
|
|
40
61
|
};
|
|
41
62
|
}
|
|
42
63
|
function getProviderRuntime() {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const msg = error instanceof Error ? error.message : String(error);
|
|
49
|
-
(0, runtime_1.emitNervesEvent)({
|
|
50
|
-
level: "error",
|
|
51
|
-
event: "engine.provider_init_error",
|
|
52
|
-
component: "engine",
|
|
53
|
-
message: msg,
|
|
54
|
-
meta: {},
|
|
55
|
-
});
|
|
56
|
-
// eslint-disable-next-line no-console -- pre-boot guard: provider init failure
|
|
57
|
-
console.error(`\n[fatal] ${msg}\n`);
|
|
58
|
-
process.exit(1);
|
|
59
|
-
throw new Error("unreachable");
|
|
60
|
-
}
|
|
61
|
-
if (!_providerRuntime) {
|
|
62
|
-
(0, runtime_1.emitNervesEvent)({
|
|
63
|
-
level: "error",
|
|
64
|
-
event: "engine.provider_init_error",
|
|
65
|
-
component: "engine",
|
|
66
|
-
message: "provider runtime could not be initialized.",
|
|
67
|
-
meta: {},
|
|
68
|
-
});
|
|
69
|
-
process.exit(1);
|
|
70
|
-
throw new Error("unreachable");
|
|
64
|
+
try {
|
|
65
|
+
const fingerprint = getProviderRuntimeFingerprint();
|
|
66
|
+
if (!_providerRuntime || _providerRuntime.fingerprint !== fingerprint) {
|
|
67
|
+
const runtime = createProviderRegistry().resolve();
|
|
68
|
+
_providerRuntime = runtime ? { fingerprint, runtime } : null;
|
|
71
69
|
}
|
|
72
70
|
}
|
|
73
|
-
|
|
71
|
+
catch (error) {
|
|
72
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
73
|
+
(0, runtime_1.emitNervesEvent)({
|
|
74
|
+
level: "error",
|
|
75
|
+
event: "engine.provider_init_error",
|
|
76
|
+
component: "engine",
|
|
77
|
+
message: msg,
|
|
78
|
+
meta: {},
|
|
79
|
+
});
|
|
80
|
+
// eslint-disable-next-line no-console -- pre-boot guard: provider init failure
|
|
81
|
+
console.error(`\n[fatal] ${msg}\n`);
|
|
82
|
+
process.exit(1);
|
|
83
|
+
throw new Error("unreachable");
|
|
84
|
+
}
|
|
85
|
+
if (!_providerRuntime) {
|
|
86
|
+
(0, runtime_1.emitNervesEvent)({
|
|
87
|
+
level: "error",
|
|
88
|
+
event: "engine.provider_init_error",
|
|
89
|
+
component: "engine",
|
|
90
|
+
message: "provider runtime could not be initialized.",
|
|
91
|
+
meta: {},
|
|
92
|
+
});
|
|
93
|
+
process.exit(1);
|
|
94
|
+
throw new Error("unreachable");
|
|
95
|
+
}
|
|
96
|
+
return _providerRuntime.runtime;
|
|
74
97
|
}
|
|
75
98
|
/**
|
|
76
|
-
* Clear the cached provider runtime so the next
|
|
77
|
-
*
|
|
78
|
-
*
|
|
99
|
+
* Clear the cached provider runtime so the next access re-creates it from
|
|
100
|
+
* current config. Runtime access also auto-refreshes when the selected
|
|
101
|
+
* provider fingerprint changes on disk.
|
|
79
102
|
*/
|
|
80
103
|
function resetProviderRuntime() {
|
|
81
104
|
_providerRuntime = null;
|
|
@@ -102,14 +125,17 @@ function createSummarize() {
|
|
|
102
125
|
};
|
|
103
126
|
}
|
|
104
127
|
function getProviderDisplayLabel() {
|
|
105
|
-
const
|
|
128
|
+
const provider = (0, identity_1.loadAgentConfig)().provider;
|
|
106
129
|
const providerLabelBuilders = {
|
|
107
|
-
azure: () =>
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
130
|
+
azure: () => {
|
|
131
|
+
const config = (0, config_1.getAzureConfig)();
|
|
132
|
+
return `azure openai (${config.deployment || "default"}, model: ${config.modelName || "unknown"})`;
|
|
133
|
+
},
|
|
134
|
+
anthropic: () => `anthropic (${(0, config_1.getAnthropicConfig)().model || "unknown"})`,
|
|
135
|
+
minimax: () => `minimax (${(0, config_1.getMinimaxConfig)().model || "unknown"})`,
|
|
136
|
+
"openai-codex": () => `openai codex (${(0, config_1.getOpenAICodexConfig)().model || "unknown"})`,
|
|
111
137
|
};
|
|
112
|
-
return providerLabelBuilders[
|
|
138
|
+
return providerLabelBuilders[provider]();
|
|
113
139
|
}
|
|
114
140
|
// Re-export tools, execTool, summarizeArgs from ./tools for backward compat
|
|
115
141
|
var tools_2 = require("../repertoire/tools");
|
|
@@ -93,6 +93,8 @@ function parseStatusPayload(data) {
|
|
|
93
93
|
socketPath: stringField(overview.socketPath) ?? "unknown",
|
|
94
94
|
version: stringField(overview.version) ?? "unknown",
|
|
95
95
|
lastUpdated: stringField(overview.lastUpdated) ?? "unknown",
|
|
96
|
+
repoRoot: stringField(overview.repoRoot) ?? "unknown",
|
|
97
|
+
configFingerprint: stringField(overview.configFingerprint) ?? "unknown",
|
|
96
98
|
workerCount: numberField(overview.workerCount) ?? 0,
|
|
97
99
|
senseCount: numberField(overview.senseCount) ?? 0,
|
|
98
100
|
entryPath: stringField(overview.entryPath) ?? "unknown",
|
|
@@ -212,14 +214,28 @@ async function ensureDaemonRunning(deps) {
|
|
|
212
214
|
const alive = await deps.checkSocketAlive(deps.socketPath);
|
|
213
215
|
if (alive) {
|
|
214
216
|
const localRuntime = (0, runtime_metadata_1.getRuntimeMetadata)();
|
|
217
|
+
let runningRuntimePromise = null;
|
|
218
|
+
const fetchRunningRuntimeMetadata = async () => {
|
|
219
|
+
runningRuntimePromise ??= (async () => {
|
|
220
|
+
const status = await deps.sendCommand(deps.socketPath, { kind: "daemon.status" });
|
|
221
|
+
const payload = parseStatusPayload(status.data);
|
|
222
|
+
return {
|
|
223
|
+
version: payload?.overview.version ?? "unknown",
|
|
224
|
+
lastUpdated: payload?.overview.lastUpdated ?? "unknown",
|
|
225
|
+
repoRoot: payload?.overview.repoRoot ?? "unknown",
|
|
226
|
+
configFingerprint: payload?.overview.configFingerprint ?? "unknown",
|
|
227
|
+
};
|
|
228
|
+
})();
|
|
229
|
+
return runningRuntimePromise;
|
|
230
|
+
};
|
|
215
231
|
return (0, daemon_runtime_sync_1.ensureCurrentDaemonRuntime)({
|
|
216
232
|
socketPath: deps.socketPath,
|
|
217
233
|
localVersion: localRuntime.version,
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
234
|
+
localLastUpdated: localRuntime.lastUpdated,
|
|
235
|
+
localRepoRoot: localRuntime.repoRoot,
|
|
236
|
+
localConfigFingerprint: localRuntime.configFingerprint,
|
|
237
|
+
fetchRunningVersion: async () => (await fetchRunningRuntimeMetadata()).version,
|
|
238
|
+
fetchRunningRuntimeMetadata,
|
|
223
239
|
stopDaemon: async () => {
|
|
224
240
|
await deps.sendCommand(deps.socketPath, { kind: "daemon.stop" });
|
|
225
241
|
},
|
|
@@ -285,6 +301,8 @@ function buildStoppedStatusPayload(socketPath) {
|
|
|
285
301
|
socketPath,
|
|
286
302
|
version: metadata.version,
|
|
287
303
|
lastUpdated: metadata.lastUpdated,
|
|
304
|
+
repoRoot: metadata.repoRoot,
|
|
305
|
+
configFingerprint: metadata.configFingerprint,
|
|
288
306
|
workerCount: 0,
|
|
289
307
|
senseCount: 0,
|
|
290
308
|
entryPath: path.join(repoRoot, "dist", "heart", "daemon", "daemon-entry.js"),
|
|
@@ -732,9 +750,13 @@ function defaultEnsureDaemonBootPersistence(socketPath) {
|
|
|
732
750
|
}
|
|
733
751
|
const homeDir = os.homedir();
|
|
734
752
|
const launchdDeps = {
|
|
753
|
+
exec: (cmd) => { (0, child_process_1.execSync)(cmd, { stdio: "ignore" }); },
|
|
735
754
|
writeFile: (filePath, content) => fs.writeFileSync(filePath, content, "utf-8"),
|
|
755
|
+
removeFile: (filePath) => fs.rmSync(filePath, { force: true }),
|
|
756
|
+
existsFile: (filePath) => fs.existsSync(filePath),
|
|
736
757
|
mkdirp: (dir) => fs.mkdirSync(dir, { recursive: true }),
|
|
737
758
|
homeDir,
|
|
759
|
+
userUid: process.getuid?.() ?? 0,
|
|
738
760
|
};
|
|
739
761
|
const entryPath = path.join((0, identity_1.getRepoRoot)(), "dist", "heart", "daemon", "daemon-entry.js");
|
|
740
762
|
/* v8 ignore next -- covered via mock in daemon-cli-defaults.test.ts; v8 on CI attributes the real fs.existsSync branch to the non-mock load @preserve */
|
|
@@ -748,7 +770,7 @@ function defaultEnsureDaemonBootPersistence(socketPath) {
|
|
|
748
770
|
});
|
|
749
771
|
}
|
|
750
772
|
const logDir = path.join(homeDir, ".agentstate", "daemon", "logs");
|
|
751
|
-
(0, launchd_1.
|
|
773
|
+
(0, launchd_1.installLaunchAgent)(launchdDeps, {
|
|
752
774
|
nodePath: process.execPath,
|
|
753
775
|
entryPath,
|
|
754
776
|
socketPath,
|
|
@@ -5,16 +5,82 @@ const runtime_1 = require("../../nerves/runtime");
|
|
|
5
5
|
function isKnownVersion(version) {
|
|
6
6
|
return version !== "unknown" && version.trim().length > 0;
|
|
7
7
|
}
|
|
8
|
+
function isKnownRuntimeValue(value) {
|
|
9
|
+
return typeof value === "string" && value.trim().length > 0 && value !== "unknown";
|
|
10
|
+
}
|
|
8
11
|
function formatErrorReason(error) {
|
|
9
12
|
return error instanceof Error ? error.message : String(error);
|
|
10
13
|
}
|
|
14
|
+
function normalizeRuntimeIdentity(value) {
|
|
15
|
+
return {
|
|
16
|
+
version: typeof value.version === "string" ? value.version : "unknown",
|
|
17
|
+
lastUpdated: typeof value.lastUpdated === "string" ? value.lastUpdated : "unknown",
|
|
18
|
+
repoRoot: typeof value.repoRoot === "string" ? value.repoRoot : "unknown",
|
|
19
|
+
configFingerprint: typeof value.configFingerprint === "string" ? value.configFingerprint : "unknown",
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
async function readRunningRuntimeIdentity(deps) {
|
|
23
|
+
if (!deps.fetchRunningRuntimeMetadata) {
|
|
24
|
+
return normalizeRuntimeIdentity({
|
|
25
|
+
version: await deps.fetchRunningVersion(),
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
const metadata = normalizeRuntimeIdentity(await deps.fetchRunningRuntimeMetadata());
|
|
29
|
+
if (isKnownVersion(metadata.version))
|
|
30
|
+
return metadata;
|
|
31
|
+
return normalizeRuntimeIdentity({
|
|
32
|
+
...metadata,
|
|
33
|
+
version: await deps.fetchRunningVersion(),
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
function collectRuntimeDriftReasons(local, running) {
|
|
37
|
+
const reasons = [];
|
|
38
|
+
const comparableVersions = isKnownVersion(local.version) && isKnownVersion(running.version);
|
|
39
|
+
if (comparableVersions && local.version !== running.version) {
|
|
40
|
+
reasons.push({ key: "version", label: "version", local: local.version, running: running.version });
|
|
41
|
+
}
|
|
42
|
+
if (comparableVersions && isKnownRuntimeValue(local.lastUpdated) && isKnownRuntimeValue(running.lastUpdated) && local.lastUpdated !== running.lastUpdated) {
|
|
43
|
+
reasons.push({ key: "lastUpdated", label: "last updated", local: local.lastUpdated, running: running.lastUpdated });
|
|
44
|
+
}
|
|
45
|
+
if (isKnownRuntimeValue(local.repoRoot) && isKnownRuntimeValue(running.repoRoot) && local.repoRoot !== running.repoRoot) {
|
|
46
|
+
reasons.push({ key: "repoRoot", label: "code path", local: local.repoRoot, running: running.repoRoot });
|
|
47
|
+
}
|
|
48
|
+
if (isKnownRuntimeValue(local.configFingerprint)
|
|
49
|
+
&& isKnownRuntimeValue(running.configFingerprint)
|
|
50
|
+
&& local.configFingerprint !== running.configFingerprint) {
|
|
51
|
+
reasons.push({
|
|
52
|
+
key: "configFingerprint",
|
|
53
|
+
label: "config fingerprint",
|
|
54
|
+
local: local.configFingerprint,
|
|
55
|
+
running: running.configFingerprint,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
return reasons;
|
|
59
|
+
}
|
|
60
|
+
function formatRuntimeValue(reason) {
|
|
61
|
+
if (reason.key === "configFingerprint") {
|
|
62
|
+
return `${reason.running.slice(0, 12)} -> ${reason.local.slice(0, 12)}`;
|
|
63
|
+
}
|
|
64
|
+
return `${reason.running} -> ${reason.local}`;
|
|
65
|
+
}
|
|
66
|
+
function formatRuntimeDriftSummary(reasons) {
|
|
67
|
+
return reasons.map((reason) => `${reason.label} ${formatRuntimeValue(reason)}`).join("; ");
|
|
68
|
+
}
|
|
11
69
|
async function ensureCurrentDaemonRuntime(deps) {
|
|
70
|
+
const localRuntime = normalizeRuntimeIdentity({
|
|
71
|
+
version: deps.localVersion,
|
|
72
|
+
lastUpdated: deps.localLastUpdated,
|
|
73
|
+
repoRoot: deps.localRepoRoot,
|
|
74
|
+
configFingerprint: deps.localConfigFingerprint,
|
|
75
|
+
});
|
|
12
76
|
try {
|
|
13
|
-
const
|
|
77
|
+
const runningRuntime = await readRunningRuntimeIdentity(deps);
|
|
78
|
+
const runningVersion = runningRuntime.version;
|
|
79
|
+
const driftReasons = collectRuntimeDriftReasons(localRuntime, runningRuntime);
|
|
14
80
|
let result;
|
|
15
|
-
if (
|
|
16
|
-
|
|
17
|
-
|
|
81
|
+
if (driftReasons.length > 0) {
|
|
82
|
+
const includesVersionDrift = driftReasons.some((entry) => entry.key === "version");
|
|
83
|
+
const driftSummary = formatRuntimeDriftSummary(driftReasons);
|
|
18
84
|
try {
|
|
19
85
|
await deps.stopDaemon();
|
|
20
86
|
}
|
|
@@ -22,14 +88,29 @@ async function ensureCurrentDaemonRuntime(deps) {
|
|
|
22
88
|
const reason = formatErrorReason(error);
|
|
23
89
|
result = {
|
|
24
90
|
alreadyRunning: true,
|
|
25
|
-
message:
|
|
91
|
+
message: includesVersionDrift
|
|
92
|
+
? `daemon already running (${deps.socketPath}; could not replace stale daemon ${runningVersion} -> ${deps.localVersion}: ${reason})`
|
|
93
|
+
: `daemon already running (${deps.socketPath}; could not replace drifted daemon ${driftSummary}: ${reason})`,
|
|
26
94
|
};
|
|
27
95
|
(0, runtime_1.emitNervesEvent)({
|
|
28
96
|
level: "warn",
|
|
29
97
|
component: "daemon",
|
|
30
98
|
event: "daemon.runtime_sync_decision",
|
|
31
99
|
message: "evaluated daemon runtime sync outcome",
|
|
32
|
-
meta: {
|
|
100
|
+
meta: {
|
|
101
|
+
socketPath: deps.socketPath,
|
|
102
|
+
localVersion: deps.localVersion,
|
|
103
|
+
localLastUpdated: localRuntime.lastUpdated,
|
|
104
|
+
localRepoRoot: localRuntime.repoRoot,
|
|
105
|
+
localConfigFingerprint: localRuntime.configFingerprint,
|
|
106
|
+
runningVersion,
|
|
107
|
+
runningLastUpdated: runningRuntime.lastUpdated,
|
|
108
|
+
runningRepoRoot: runningRuntime.repoRoot,
|
|
109
|
+
runningConfigFingerprint: runningRuntime.configFingerprint,
|
|
110
|
+
action: "stale_replace_failed",
|
|
111
|
+
driftKeys: driftReasons.map((entry) => entry.key),
|
|
112
|
+
reason,
|
|
113
|
+
},
|
|
33
114
|
});
|
|
34
115
|
return result;
|
|
35
116
|
}
|
|
@@ -37,17 +118,32 @@ async function ensureCurrentDaemonRuntime(deps) {
|
|
|
37
118
|
const started = await deps.startDaemonProcess(deps.socketPath);
|
|
38
119
|
result = {
|
|
39
120
|
alreadyRunning: false,
|
|
40
|
-
message:
|
|
121
|
+
message: includesVersionDrift
|
|
122
|
+
? `restarted stale daemon from ${runningVersion} to ${deps.localVersion} (pid ${started.pid ?? "unknown"})`
|
|
123
|
+
: `restarted drifted daemon (${driftSummary}) (pid ${started.pid ?? "unknown"})`,
|
|
41
124
|
};
|
|
42
125
|
(0, runtime_1.emitNervesEvent)({
|
|
43
126
|
component: "daemon",
|
|
44
127
|
event: "daemon.runtime_sync_decision",
|
|
45
128
|
message: "evaluated daemon runtime sync outcome",
|
|
46
|
-
meta: {
|
|
129
|
+
meta: {
|
|
130
|
+
socketPath: deps.socketPath,
|
|
131
|
+
localVersion: deps.localVersion,
|
|
132
|
+
localLastUpdated: localRuntime.lastUpdated,
|
|
133
|
+
localRepoRoot: localRuntime.repoRoot,
|
|
134
|
+
localConfigFingerprint: localRuntime.configFingerprint,
|
|
135
|
+
runningVersion,
|
|
136
|
+
runningLastUpdated: runningRuntime.lastUpdated,
|
|
137
|
+
runningRepoRoot: runningRuntime.repoRoot,
|
|
138
|
+
runningConfigFingerprint: runningRuntime.configFingerprint,
|
|
139
|
+
action: "stale_restarted",
|
|
140
|
+
driftKeys: driftReasons.map((entry) => entry.key),
|
|
141
|
+
pid: started.pid ?? null,
|
|
142
|
+
},
|
|
47
143
|
});
|
|
48
144
|
return result;
|
|
49
145
|
}
|
|
50
|
-
if (!isKnownVersion(
|
|
146
|
+
if (!isKnownVersion(localRuntime.version) || !isKnownVersion(runningVersion)) {
|
|
51
147
|
result = {
|
|
52
148
|
alreadyRunning: true,
|
|
53
149
|
message: `daemon already running (${deps.socketPath}; unable to verify version)`,
|
|
@@ -56,7 +152,18 @@ async function ensureCurrentDaemonRuntime(deps) {
|
|
|
56
152
|
component: "daemon",
|
|
57
153
|
event: "daemon.runtime_sync_decision",
|
|
58
154
|
message: "evaluated daemon runtime sync outcome",
|
|
59
|
-
meta: {
|
|
155
|
+
meta: {
|
|
156
|
+
socketPath: deps.socketPath,
|
|
157
|
+
localVersion: deps.localVersion,
|
|
158
|
+
localLastUpdated: localRuntime.lastUpdated,
|
|
159
|
+
localRepoRoot: localRuntime.repoRoot,
|
|
160
|
+
localConfigFingerprint: localRuntime.configFingerprint,
|
|
161
|
+
runningVersion,
|
|
162
|
+
runningLastUpdated: runningRuntime.lastUpdated,
|
|
163
|
+
runningRepoRoot: runningRuntime.repoRoot,
|
|
164
|
+
runningConfigFingerprint: runningRuntime.configFingerprint,
|
|
165
|
+
action: "unknown_version",
|
|
166
|
+
},
|
|
60
167
|
});
|
|
61
168
|
return result;
|
|
62
169
|
}
|
|
@@ -72,7 +179,15 @@ async function ensureCurrentDaemonRuntime(deps) {
|
|
|
72
179
|
component: "daemon",
|
|
73
180
|
event: "daemon.runtime_sync_decision",
|
|
74
181
|
message: "evaluated daemon runtime sync outcome",
|
|
75
|
-
meta: {
|
|
182
|
+
meta: {
|
|
183
|
+
socketPath: deps.socketPath,
|
|
184
|
+
localVersion: deps.localVersion,
|
|
185
|
+
localLastUpdated: localRuntime.lastUpdated,
|
|
186
|
+
localRepoRoot: localRuntime.repoRoot,
|
|
187
|
+
localConfigFingerprint: localRuntime.configFingerprint,
|
|
188
|
+
action: "status_lookup_failed",
|
|
189
|
+
reason,
|
|
190
|
+
},
|
|
76
191
|
});
|
|
77
192
|
return result;
|
|
78
193
|
}
|
|
@@ -84,7 +199,14 @@ async function ensureCurrentDaemonRuntime(deps) {
|
|
|
84
199
|
component: "daemon",
|
|
85
200
|
event: "daemon.runtime_sync_decision",
|
|
86
201
|
message: "evaluated daemon runtime sync outcome",
|
|
87
|
-
meta: {
|
|
202
|
+
meta: {
|
|
203
|
+
socketPath: deps.socketPath,
|
|
204
|
+
localVersion: deps.localVersion,
|
|
205
|
+
localLastUpdated: localRuntime.lastUpdated,
|
|
206
|
+
localRepoRoot: localRuntime.repoRoot,
|
|
207
|
+
localConfigFingerprint: localRuntime.configFingerprint,
|
|
208
|
+
action: "already_current",
|
|
209
|
+
},
|
|
88
210
|
});
|
|
89
211
|
return result;
|
|
90
212
|
}
|
|
@@ -45,6 +45,9 @@ exports.DAEMON_PLIST_LABEL = "bot.ouro.daemon";
|
|
|
45
45
|
function plistFilePath(homeDir) {
|
|
46
46
|
return path.join(homeDir, "Library", "LaunchAgents", `${exports.DAEMON_PLIST_LABEL}.plist`);
|
|
47
47
|
}
|
|
48
|
+
function userLaunchDomain(userUid) {
|
|
49
|
+
return `gui/${userUid}`;
|
|
50
|
+
}
|
|
48
51
|
function generateDaemonPlist(options) {
|
|
49
52
|
(0, runtime_1.emitNervesEvent)({
|
|
50
53
|
component: "daemon",
|
|
@@ -105,15 +108,16 @@ function installLaunchAgent(deps, options) {
|
|
|
105
108
|
meta: { entryPath: options.entryPath, socketPath: options.socketPath },
|
|
106
109
|
});
|
|
107
110
|
const fullPath = plistFilePath(deps.homeDir);
|
|
111
|
+
const domain = userLaunchDomain(deps.userUid);
|
|
108
112
|
// Unload existing (best effort) for idempotent re-install
|
|
109
113
|
if (deps.existsFile(fullPath)) {
|
|
110
114
|
try {
|
|
111
|
-
deps.exec(`launchctl
|
|
115
|
+
deps.exec(`launchctl bootout ${domain} "${fullPath}"`);
|
|
112
116
|
}
|
|
113
117
|
catch { /* best effort */ }
|
|
114
118
|
}
|
|
115
119
|
writeLaunchAgentPlist(deps, options);
|
|
116
|
-
deps.exec(`launchctl
|
|
120
|
+
deps.exec(`launchctl bootstrap ${domain} "${fullPath}"`);
|
|
117
121
|
(0, runtime_1.emitNervesEvent)({
|
|
118
122
|
component: "daemon",
|
|
119
123
|
event: "daemon.launchd_installed",
|
|
@@ -129,9 +133,10 @@ function uninstallLaunchAgent(deps) {
|
|
|
129
133
|
meta: {},
|
|
130
134
|
});
|
|
131
135
|
const fullPath = plistFilePath(deps.homeDir);
|
|
136
|
+
const domain = userLaunchDomain(deps.userUid);
|
|
132
137
|
if (deps.existsFile(fullPath)) {
|
|
133
138
|
try {
|
|
134
|
-
deps.exec(`launchctl
|
|
139
|
+
deps.exec(`launchctl bootout ${domain} "${fullPath}"`);
|
|
135
140
|
}
|
|
136
141
|
catch { /* best effort */ }
|
|
137
142
|
deps.removeFile(fullPath);
|
|
@@ -34,7 +34,9 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.getRuntimeMetadata = getRuntimeMetadata;
|
|
37
|
+
const crypto_1 = require("crypto");
|
|
37
38
|
const fs = __importStar(require("fs"));
|
|
39
|
+
const os = __importStar(require("os"));
|
|
38
40
|
const path = __importStar(require("path"));
|
|
39
41
|
const childProcess = __importStar(require("child_process"));
|
|
40
42
|
const identity_1 = require("../identity");
|
|
@@ -85,10 +87,98 @@ function readLastUpdated(repoRoot, packageJsonPath, statSyncImpl, execFileSyncIm
|
|
|
85
87
|
return { value: UNKNOWN_METADATA, source: "unknown" };
|
|
86
88
|
}
|
|
87
89
|
}
|
|
90
|
+
function readHomeDir() {
|
|
91
|
+
const homedirImpl = optionalFunction(os, "homedir");
|
|
92
|
+
if (!homedirImpl) {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
try {
|
|
96
|
+
return homedirImpl.call(os);
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function listConfigTargets(bundlesRoot, secretsRoot, daemonLoggingPath, readdirSyncImpl) {
|
|
103
|
+
if (!readdirSyncImpl)
|
|
104
|
+
return [];
|
|
105
|
+
const targets = new Set();
|
|
106
|
+
if (daemonLoggingPath) {
|
|
107
|
+
targets.add(daemonLoggingPath);
|
|
108
|
+
}
|
|
109
|
+
try {
|
|
110
|
+
const bundleEntries = readdirSyncImpl(bundlesRoot, { withFileTypes: true });
|
|
111
|
+
for (const entry of bundleEntries) {
|
|
112
|
+
if (!entry.isDirectory() || !entry.name.endsWith(".ouro"))
|
|
113
|
+
continue;
|
|
114
|
+
targets.add(path.join(bundlesRoot, entry.name, "agent.json"));
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
// ignore unreadable bundle roots
|
|
119
|
+
}
|
|
120
|
+
if (secretsRoot) {
|
|
121
|
+
try {
|
|
122
|
+
const secretEntries = readdirSyncImpl(secretsRoot, { withFileTypes: true });
|
|
123
|
+
for (const entry of secretEntries) {
|
|
124
|
+
if (!entry.isDirectory())
|
|
125
|
+
continue;
|
|
126
|
+
targets.add(path.join(secretsRoot, entry.name, "secrets.json"));
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
// ignore unreadable secrets roots
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return [...targets].sort();
|
|
134
|
+
}
|
|
135
|
+
function readConfigFingerprint(targets, readFileSyncImpl, existsSyncImpl) {
|
|
136
|
+
if (!readFileSyncImpl || !existsSyncImpl) {
|
|
137
|
+
return {
|
|
138
|
+
value: UNKNOWN_METADATA,
|
|
139
|
+
source: "unknown",
|
|
140
|
+
trackedFiles: targets.length,
|
|
141
|
+
presentFiles: 0,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
const hash = (0, crypto_1.createHash)("sha256");
|
|
145
|
+
let presentFiles = 0;
|
|
146
|
+
for (const target of targets) {
|
|
147
|
+
hash.update(target);
|
|
148
|
+
hash.update("\0");
|
|
149
|
+
if (!existsSyncImpl(target)) {
|
|
150
|
+
hash.update("missing");
|
|
151
|
+
hash.update("\0");
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
presentFiles += 1;
|
|
155
|
+
hash.update("present");
|
|
156
|
+
hash.update("\0");
|
|
157
|
+
try {
|
|
158
|
+
hash.update(readFileSyncImpl(target, "utf-8"));
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
hash.update("unreadable");
|
|
162
|
+
}
|
|
163
|
+
hash.update("\0");
|
|
164
|
+
}
|
|
165
|
+
return {
|
|
166
|
+
value: hash.digest("hex"),
|
|
167
|
+
source: "content-hash",
|
|
168
|
+
trackedFiles: targets.length,
|
|
169
|
+
presentFiles,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
88
172
|
function getRuntimeMetadata(deps = {}) {
|
|
89
173
|
const repoRoot = deps.repoRoot ?? (0, identity_1.getRepoRoot)();
|
|
174
|
+
const bundlesRoot = deps.bundlesRoot ?? (0, identity_1.getAgentBundlesRoot)();
|
|
175
|
+
const homeDir = readHomeDir();
|
|
176
|
+
const secretsRoot = deps.secretsRoot ?? (homeDir ? path.join(homeDir, ".agentsecrets") : null);
|
|
177
|
+
const daemonLoggingPath = deps.daemonLoggingPath ?? (homeDir ? path.join(homeDir, ".agentstate", "daemon", "logging.json") : null);
|
|
90
178
|
const readFileSyncImpl = deps.readFileSync ?? optionalFunction(fs, "readFileSync")?.bind(fs) ?? null;
|
|
91
179
|
const statSyncImpl = deps.statSync ?? optionalFunction(fs, "statSync")?.bind(fs) ?? null;
|
|
180
|
+
const readdirSyncImpl = deps.readdirSync ?? optionalFunction(fs, "readdirSync")?.bind(fs) ?? null;
|
|
181
|
+
const existsSyncImpl = deps.existsSync ?? optionalFunction(fs, "existsSync")?.bind(fs) ?? null;
|
|
92
182
|
const execFileSyncImpl = deps.execFileSync
|
|
93
183
|
?? optionalFunction(childProcess, "execFileSync")?.bind(childProcess)
|
|
94
184
|
?? null;
|
|
@@ -101,6 +191,8 @@ function getRuntimeMetadata(deps = {}) {
|
|
|
101
191
|
throw new Error("git unavailable");
|
|
102
192
|
}))
|
|
103
193
|
: { value: UNKNOWN_METADATA, source: "unknown" };
|
|
194
|
+
const configTargets = listConfigTargets(bundlesRoot, secretsRoot, daemonLoggingPath, readdirSyncImpl);
|
|
195
|
+
const configFingerprint = readConfigFingerprint(configTargets, readFileSyncImpl, existsSyncImpl);
|
|
104
196
|
(0, runtime_1.emitNervesEvent)({
|
|
105
197
|
component: "daemon",
|
|
106
198
|
event: "daemon.runtime_metadata_read",
|
|
@@ -109,10 +201,19 @@ function getRuntimeMetadata(deps = {}) {
|
|
|
109
201
|
version,
|
|
110
202
|
lastUpdated: lastUpdated.value,
|
|
111
203
|
lastUpdatedSource: lastUpdated.source,
|
|
204
|
+
repoRoot,
|
|
205
|
+
configFingerprint: configFingerprint.value === UNKNOWN_METADATA
|
|
206
|
+
? UNKNOWN_METADATA
|
|
207
|
+
: configFingerprint.value.slice(0, 12),
|
|
208
|
+
configFingerprintSource: configFingerprint.source,
|
|
209
|
+
configTrackedFiles: configFingerprint.trackedFiles,
|
|
210
|
+
configPresentFiles: configFingerprint.presentFiles,
|
|
112
211
|
},
|
|
113
212
|
});
|
|
114
213
|
return {
|
|
115
214
|
version,
|
|
116
215
|
lastUpdated: lastUpdated.value,
|
|
216
|
+
repoRoot,
|
|
217
|
+
configFingerprint: configFingerprint.value,
|
|
117
218
|
};
|
|
118
219
|
}
|
|
@@ -45,6 +45,7 @@ const process_manager_1 = require("./process-manager");
|
|
|
45
45
|
const DEFAULT_TEAMS_PORT = 3978;
|
|
46
46
|
const DEFAULT_BLUEBUBBLES_PORT = 18790;
|
|
47
47
|
const DEFAULT_BLUEBUBBLES_WEBHOOK_PATH = "/bluebubbles-webhook";
|
|
48
|
+
const BLUEBUBBLES_RUNTIME_FRESHNESS_WINDOW_MS = 90_000;
|
|
48
49
|
function defaultSenses() {
|
|
49
50
|
return {
|
|
50
51
|
cli: { ...identity_1.DEFAULT_AGENT_SENSES.cli },
|
|
@@ -176,20 +177,36 @@ function runtimeInfoFor(status) {
|
|
|
176
177
|
return { runtime: "running" };
|
|
177
178
|
return { runtime: "error" };
|
|
178
179
|
}
|
|
180
|
+
function blueBubblesRuntimeStateIsFresh(lastCheckedAt, now = Date.now()) {
|
|
181
|
+
if (!lastCheckedAt) {
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
const checkedAt = Date.parse(lastCheckedAt);
|
|
185
|
+
if (!Number.isFinite(checkedAt)) {
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
return checkedAt >= now - BLUEBUBBLES_RUNTIME_FRESHNESS_WINDOW_MS;
|
|
189
|
+
}
|
|
179
190
|
function readBlueBubblesRuntimeFacts(agent, bundlesRoot, snapshot) {
|
|
180
191
|
const agentRoot = path.join(bundlesRoot, `${agent}.ouro`);
|
|
181
192
|
const runtimePath = path.join(agentRoot, "state", "senses", "bluebubbles", "runtime.json");
|
|
182
|
-
if (
|
|
193
|
+
if (!fs.existsSync(runtimePath)) {
|
|
183
194
|
return { runtime: snapshot?.runtime };
|
|
184
195
|
}
|
|
185
196
|
const state = (0, bluebubbles_runtime_state_1.readBlueBubblesRuntimeState)(agent, agentRoot);
|
|
197
|
+
if (!blueBubblesRuntimeStateIsFresh(state.lastCheckedAt)) {
|
|
198
|
+
return { runtime: snapshot?.runtime };
|
|
199
|
+
}
|
|
186
200
|
if (state.upstreamStatus === "error") {
|
|
187
201
|
return {
|
|
188
202
|
runtime: "error",
|
|
189
203
|
detail: state.detail,
|
|
190
204
|
};
|
|
191
205
|
}
|
|
192
|
-
|
|
206
|
+
if (state.upstreamStatus === "ok") {
|
|
207
|
+
return { runtime: "running" };
|
|
208
|
+
}
|
|
209
|
+
return { runtime: snapshot?.runtime };
|
|
193
210
|
}
|
|
194
211
|
class DaemonSenseManager {
|
|
195
212
|
processManager;
|
package/dist/heart/identity.js
CHANGED
|
@@ -134,7 +134,6 @@ function buildDefaultAgentTemplate(_agentName) {
|
|
|
134
134
|
};
|
|
135
135
|
}
|
|
136
136
|
let _cachedAgentName = null;
|
|
137
|
-
let _cachedAgentConfig = null;
|
|
138
137
|
let _agentConfigOverride = null;
|
|
139
138
|
/**
|
|
140
139
|
* Parse `--agent <name>` from process.argv.
|
|
@@ -199,22 +198,13 @@ function getAgentSecretsPath(agentName = getAgentName()) {
|
|
|
199
198
|
}
|
|
200
199
|
/**
|
|
201
200
|
* Load and parse `<agentRoot>/agent.json`.
|
|
202
|
-
*
|
|
201
|
+
* Reads the file fresh on each call unless an override is set.
|
|
203
202
|
* Throws descriptive error if file is missing or contains invalid JSON.
|
|
204
203
|
*/
|
|
205
204
|
function loadAgentConfig() {
|
|
206
205
|
if (_agentConfigOverride) {
|
|
207
206
|
return _agentConfigOverride;
|
|
208
207
|
}
|
|
209
|
-
if (_cachedAgentConfig) {
|
|
210
|
-
(0, runtime_1.emitNervesEvent)({
|
|
211
|
-
event: "identity.resolve",
|
|
212
|
-
component: "config/identity",
|
|
213
|
-
message: "loaded agent config from cache",
|
|
214
|
-
meta: { source: "cache" },
|
|
215
|
-
});
|
|
216
|
-
return _cachedAgentConfig;
|
|
217
|
-
}
|
|
218
208
|
const agentRoot = getAgentRoot();
|
|
219
209
|
const configFile = path.join(agentRoot, "agent.json");
|
|
220
210
|
let raw;
|
|
@@ -289,6 +279,7 @@ function loadAgentConfig() {
|
|
|
289
279
|
});
|
|
290
280
|
throw new Error(`agent.json at ${configFile} must include provider: "azure", "minimax", "anthropic", or "openai-codex".`);
|
|
291
281
|
}
|
|
282
|
+
const provider = rawProvider;
|
|
292
283
|
const rawVersion = parsed.version;
|
|
293
284
|
const version = rawVersion === undefined ? 1 : rawVersion;
|
|
294
285
|
if (typeof version !== "number" ||
|
|
@@ -321,10 +312,10 @@ function loadAgentConfig() {
|
|
|
321
312
|
});
|
|
322
313
|
throw new Error(`agent.json at ${configFile} must include enabled as boolean.`);
|
|
323
314
|
}
|
|
324
|
-
|
|
315
|
+
const config = {
|
|
325
316
|
version,
|
|
326
317
|
enabled,
|
|
327
|
-
provider
|
|
318
|
+
provider,
|
|
328
319
|
context: parsed.context,
|
|
329
320
|
logging: parsed.logging,
|
|
330
321
|
senses: normalizeSenses(parsed.senses, configFile),
|
|
@@ -336,7 +327,7 @@ function loadAgentConfig() {
|
|
|
336
327
|
message: "loaded agent config from disk",
|
|
337
328
|
meta: { source: "disk" },
|
|
338
329
|
});
|
|
339
|
-
return
|
|
330
|
+
return config;
|
|
340
331
|
}
|
|
341
332
|
/**
|
|
342
333
|
* Prime the agent name cache explicitly.
|
|
@@ -356,11 +347,11 @@ function setAgentConfigOverride(config) {
|
|
|
356
347
|
_agentConfigOverride = config;
|
|
357
348
|
}
|
|
358
349
|
/**
|
|
359
|
-
*
|
|
360
|
-
*
|
|
350
|
+
* Preserve the compatibility hook for callers that previously cleared cached
|
|
351
|
+
* disk-backed agent config. Agent config is now read fresh on every call.
|
|
361
352
|
*/
|
|
362
353
|
function resetAgentConfigCache() {
|
|
363
|
-
|
|
354
|
+
// No-op: disk-backed agent config is no longer memoized in-process.
|
|
364
355
|
}
|
|
365
356
|
/**
|
|
366
357
|
* Clear all cached identity state.
|
|
@@ -368,6 +359,5 @@ function resetAgentConfigCache() {
|
|
|
368
359
|
*/
|
|
369
360
|
function resetIdentity() {
|
|
370
361
|
_cachedAgentName = null;
|
|
371
|
-
_cachedAgentConfig = null;
|
|
372
362
|
_agentConfigOverride = null;
|
|
373
363
|
}
|
package/dist/mind/memory.js
CHANGED
|
@@ -33,7 +33,6 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.__memoryTestUtils = void 0;
|
|
37
36
|
exports.ensureMemoryStorePaths = ensureMemoryStorePaths;
|
|
38
37
|
exports.appendFactsWithDedup = appendFactsWithDedup;
|
|
39
38
|
exports.readMemoryFacts = readMemoryFacts;
|
|
@@ -46,7 +45,9 @@ const crypto_1 = require("crypto");
|
|
|
46
45
|
const config_1 = require("../heart/config");
|
|
47
46
|
const identity_1 = require("../heart/identity");
|
|
48
47
|
const runtime_1 = require("../nerves/runtime");
|
|
48
|
+
const associative_recall_1 = require("./associative-recall");
|
|
49
49
|
const DEDUP_THRESHOLD = 0.6;
|
|
50
|
+
const SEMANTIC_DEDUP_THRESHOLD = 0.95;
|
|
50
51
|
const ENTITY_TOKEN = /[a-z0-9]+/g;
|
|
51
52
|
const DEFAULT_EMBEDDING_MODEL = "text-embedding-3-small";
|
|
52
53
|
class OpenAIEmbeddingProvider {
|
|
@@ -177,13 +178,24 @@ function appendDailyFact(dailyDir, fact) {
|
|
|
177
178
|
const dayPath = path.join(dailyDir, `${day}.jsonl`);
|
|
178
179
|
fs.appendFileSync(dayPath, `${JSON.stringify(fact)}\n`, "utf8");
|
|
179
180
|
}
|
|
180
|
-
function appendFactsWithDedup(stores, incoming) {
|
|
181
|
+
function appendFactsWithDedup(stores, incoming, options) {
|
|
181
182
|
const existing = readExistingFacts(stores.factsPath);
|
|
182
183
|
const all = [...existing];
|
|
183
184
|
let added = 0;
|
|
184
185
|
let skipped = 0;
|
|
186
|
+
const semanticThreshold = options?.semanticThreshold;
|
|
185
187
|
for (const fact of incoming) {
|
|
186
|
-
const duplicate = all.some((prior) =>
|
|
188
|
+
const duplicate = all.some((prior) => {
|
|
189
|
+
if (overlapScore(prior.text, fact.text) > DEDUP_THRESHOLD)
|
|
190
|
+
return true;
|
|
191
|
+
if (semanticThreshold !== undefined &&
|
|
192
|
+
Array.isArray(fact.embedding) && fact.embedding.length > 0 &&
|
|
193
|
+
Array.isArray(prior.embedding) && prior.embedding.length > 0 &&
|
|
194
|
+
fact.embedding.length === prior.embedding.length) {
|
|
195
|
+
return (0, associative_recall_1.cosineSimilarity)(fact.embedding, prior.embedding) > semanticThreshold;
|
|
196
|
+
}
|
|
197
|
+
return false;
|
|
198
|
+
});
|
|
187
199
|
if (duplicate) {
|
|
188
200
|
skipped++;
|
|
189
201
|
continue;
|
|
@@ -202,24 +214,6 @@ function appendFactsWithDedup(stores, incoming) {
|
|
|
202
214
|
});
|
|
203
215
|
return { added, skipped };
|
|
204
216
|
}
|
|
205
|
-
function cosineSimilarity(left, right) {
|
|
206
|
-
if (left.length === 0 || right.length === 0 || left.length !== right.length)
|
|
207
|
-
return 0;
|
|
208
|
-
let dot = 0;
|
|
209
|
-
let leftNorm = 0;
|
|
210
|
-
let rightNorm = 0;
|
|
211
|
-
for (let i = 0; i < left.length; i += 1) {
|
|
212
|
-
dot += left[i] * right[i];
|
|
213
|
-
leftNorm += left[i] * left[i];
|
|
214
|
-
rightNorm += right[i] * right[i];
|
|
215
|
-
}
|
|
216
|
-
if (leftNorm === 0 || rightNorm === 0)
|
|
217
|
-
return 0;
|
|
218
|
-
return dot / (Math.sqrt(leftNorm) * Math.sqrt(rightNorm));
|
|
219
|
-
}
|
|
220
|
-
exports.__memoryTestUtils = {
|
|
221
|
-
cosineSimilarity,
|
|
222
|
-
};
|
|
223
217
|
function createDefaultEmbeddingProvider() {
|
|
224
218
|
const apiKey = (0, config_1.getOpenAIEmbeddingsApiKey)().trim();
|
|
225
219
|
if (!apiKey)
|
|
@@ -271,7 +265,7 @@ async function saveMemoryFact(options) {
|
|
|
271
265
|
createdAt: (options.now ?? (() => new Date()))().toISOString(),
|
|
272
266
|
embedding,
|
|
273
267
|
};
|
|
274
|
-
return appendFactsWithDedup(stores, [fact]);
|
|
268
|
+
return appendFactsWithDedup(stores, [fact], { semanticThreshold: SEMANTIC_DEDUP_THRESHOLD });
|
|
275
269
|
}
|
|
276
270
|
async function backfillEmbeddings(options) {
|
|
277
271
|
const memoryRoot = options?.memoryRoot ?? path.join((0, identity_1.getAgentRoot)(), "psyche", "memory");
|
|
@@ -372,7 +366,7 @@ async function searchMemoryFacts(query, facts, embeddingProvider) {
|
|
|
372
366
|
.filter((fact) => fact.embedding.length === queryEmbedding.length)
|
|
373
367
|
.map((fact) => ({
|
|
374
368
|
fact,
|
|
375
|
-
score: cosineSimilarity(queryEmbedding, fact.embedding),
|
|
369
|
+
score: (0, associative_recall_1.cosineSimilarity)(queryEmbedding, fact.embedding),
|
|
376
370
|
}))
|
|
377
371
|
.filter((entry) => entry.score > 0)
|
|
378
372
|
.sort((left, right) => right.score - left.score)
|