@ouro.bot/cli 0.1.0-alpha.52 → 0.1.0-alpha.54
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 +17 -0
- package/dist/heart/config.js +9 -15
- package/dist/heart/core.js +64 -38
- package/dist/heart/daemon/daemon-cli.js +47 -6
- package/dist/heart/daemon/daemon-entry.js +41 -1
- package/dist/heart/daemon/daemon-runtime-sync.js +134 -12
- package/dist/heart/daemon/daemon.js +4 -0
- package/dist/heart/daemon/launchd.js +11 -3
- package/dist/heart/daemon/process-manager.js +13 -0
- package/dist/heart/daemon/runtime-metadata.js +101 -0
- package/dist/heart/daemon/runtime-mode.js +67 -0
- package/dist/heart/daemon/sense-manager.js +19 -2
- package/dist/heart/identity.js +8 -18
- package/package.json +1 -1
package/changelog.json
CHANGED
|
@@ -1,6 +1,23 @@
|
|
|
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.54",
|
|
6
|
+
"changes": [
|
|
7
|
+
"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.",
|
|
8
|
+
"`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.",
|
|
9
|
+
"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."
|
|
10
|
+
]
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"version": "0.1.0-alpha.53",
|
|
14
|
+
"changes": [
|
|
15
|
+
"Daemon plist now inherits PATH from the installing process, so child agent spawns can find node regardless of launchd's minimal default environment.",
|
|
16
|
+
"Daemon startup and status output now show the entry path and runtime mode (dev vs production) so it's immediately obvious where the daemon is running from and whether it's a dev repo, worktree, or npm install.",
|
|
17
|
+
"Agent spawn in the process manager now validates the entry script exists before calling spawn, setting the agent to crashed with a clear error instead of an opaque ENOENT.",
|
|
18
|
+
"Plist write warns when the entry path doesn't exist on disk, catching stale worktree paths before they cause daemon crashes."
|
|
19
|
+
]
|
|
20
|
+
},
|
|
4
21
|
{
|
|
5
22
|
"version": "0.1.0-alpha.52",
|
|
6
23
|
"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");
|
|
@@ -55,6 +55,7 @@ const specialist_orchestrator_1 = require("./specialist-orchestrator");
|
|
|
55
55
|
const specialist_prompt_1 = require("./specialist-prompt");
|
|
56
56
|
const specialist_tools_1 = require("./specialist-tools");
|
|
57
57
|
const runtime_metadata_1 = require("./runtime-metadata");
|
|
58
|
+
const runtime_mode_1 = require("./runtime-mode");
|
|
58
59
|
const daemon_runtime_sync_1 = require("./daemon-runtime-sync");
|
|
59
60
|
const agent_discovery_1 = require("./agent-discovery");
|
|
60
61
|
const update_hooks_1 = require("./update-hooks");
|
|
@@ -92,8 +93,12 @@ function parseStatusPayload(data) {
|
|
|
92
93
|
socketPath: stringField(overview.socketPath) ?? "unknown",
|
|
93
94
|
version: stringField(overview.version) ?? "unknown",
|
|
94
95
|
lastUpdated: stringField(overview.lastUpdated) ?? "unknown",
|
|
96
|
+
repoRoot: stringField(overview.repoRoot) ?? "unknown",
|
|
97
|
+
configFingerprint: stringField(overview.configFingerprint) ?? "unknown",
|
|
95
98
|
workerCount: numberField(overview.workerCount) ?? 0,
|
|
96
99
|
senseCount: numberField(overview.senseCount) ?? 0,
|
|
100
|
+
entryPath: stringField(overview.entryPath) ?? "unknown",
|
|
101
|
+
mode: stringField(overview.mode) ?? "unknown",
|
|
97
102
|
};
|
|
98
103
|
const parsedSenses = senses.map((entry) => {
|
|
99
104
|
if (!entry || typeof entry !== "object" || Array.isArray(entry))
|
|
@@ -174,6 +179,8 @@ function formatDaemonStatusOutput(response, fallback) {
|
|
|
174
179
|
["Socket", payload.overview.socketPath],
|
|
175
180
|
["Version", payload.overview.version],
|
|
176
181
|
["Last Updated", payload.overview.lastUpdated],
|
|
182
|
+
["Entry Path", payload.overview.entryPath],
|
|
183
|
+
["Mode", payload.overview.mode],
|
|
177
184
|
["Workers", String(payload.overview.workerCount)],
|
|
178
185
|
["Senses", String(payload.overview.senseCount)],
|
|
179
186
|
["Health", payload.overview.health],
|
|
@@ -207,14 +214,28 @@ async function ensureDaemonRunning(deps) {
|
|
|
207
214
|
const alive = await deps.checkSocketAlive(deps.socketPath);
|
|
208
215
|
if (alive) {
|
|
209
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
|
+
};
|
|
210
231
|
return (0, daemon_runtime_sync_1.ensureCurrentDaemonRuntime)({
|
|
211
232
|
socketPath: deps.socketPath,
|
|
212
233
|
localVersion: localRuntime.version,
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
234
|
+
localLastUpdated: localRuntime.lastUpdated,
|
|
235
|
+
localRepoRoot: localRuntime.repoRoot,
|
|
236
|
+
localConfigFingerprint: localRuntime.configFingerprint,
|
|
237
|
+
fetchRunningVersion: async () => (await fetchRunningRuntimeMetadata()).version,
|
|
238
|
+
fetchRunningRuntimeMetadata,
|
|
218
239
|
stopDaemon: async () => {
|
|
219
240
|
await deps.sendCommand(deps.socketPath, { kind: "daemon.stop" });
|
|
220
241
|
},
|
|
@@ -272,6 +293,7 @@ function formatVersionOutput() {
|
|
|
272
293
|
}
|
|
273
294
|
function buildStoppedStatusPayload(socketPath) {
|
|
274
295
|
const metadata = (0, runtime_metadata_1.getRuntimeMetadata)();
|
|
296
|
+
const repoRoot = (0, identity_1.getRepoRoot)();
|
|
275
297
|
return {
|
|
276
298
|
overview: {
|
|
277
299
|
daemon: "stopped",
|
|
@@ -279,8 +301,12 @@ function buildStoppedStatusPayload(socketPath) {
|
|
|
279
301
|
socketPath,
|
|
280
302
|
version: metadata.version,
|
|
281
303
|
lastUpdated: metadata.lastUpdated,
|
|
304
|
+
repoRoot: metadata.repoRoot,
|
|
305
|
+
configFingerprint: metadata.configFingerprint,
|
|
282
306
|
workerCount: 0,
|
|
283
307
|
senseCount: 0,
|
|
308
|
+
entryPath: path.join(repoRoot, "dist", "heart", "daemon", "daemon-entry.js"),
|
|
309
|
+
mode: (0, runtime_mode_1.detectRuntimeMode)(repoRoot),
|
|
284
310
|
},
|
|
285
311
|
senses: [],
|
|
286
312
|
workers: [],
|
|
@@ -724,17 +750,32 @@ function defaultEnsureDaemonBootPersistence(socketPath) {
|
|
|
724
750
|
}
|
|
725
751
|
const homeDir = os.homedir();
|
|
726
752
|
const launchdDeps = {
|
|
753
|
+
exec: (cmd) => { (0, child_process_1.execSync)(cmd, { stdio: "ignore" }); },
|
|
727
754
|
writeFile: (filePath, content) => fs.writeFileSync(filePath, content, "utf-8"),
|
|
755
|
+
removeFile: (filePath) => fs.rmSync(filePath, { force: true }),
|
|
756
|
+
existsFile: (filePath) => fs.existsSync(filePath),
|
|
728
757
|
mkdirp: (dir) => fs.mkdirSync(dir, { recursive: true }),
|
|
729
758
|
homeDir,
|
|
759
|
+
userUid: process.getuid?.() ?? 0,
|
|
730
760
|
};
|
|
731
761
|
const entryPath = path.join((0, identity_1.getRepoRoot)(), "dist", "heart", "daemon", "daemon-entry.js");
|
|
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 */
|
|
763
|
+
if (!fs.existsSync(entryPath)) {
|
|
764
|
+
(0, runtime_1.emitNervesEvent)({
|
|
765
|
+
level: "warn",
|
|
766
|
+
component: "daemon",
|
|
767
|
+
event: "daemon.entry_path_missing",
|
|
768
|
+
message: "entryPath does not exist on disk — plist may point to a stale location. Run 'ouro daemon install' from the correct location.",
|
|
769
|
+
meta: { entryPath },
|
|
770
|
+
});
|
|
771
|
+
}
|
|
732
772
|
const logDir = path.join(homeDir, ".agentstate", "daemon", "logs");
|
|
733
|
-
(0, launchd_1.
|
|
773
|
+
(0, launchd_1.installLaunchAgent)(launchdDeps, {
|
|
734
774
|
nodePath: process.execPath,
|
|
735
775
|
entryPath,
|
|
736
776
|
socketPath,
|
|
737
777
|
logDir,
|
|
778
|
+
envPath: process.env.PATH,
|
|
738
779
|
});
|
|
739
780
|
}
|
|
740
781
|
async function defaultInstallSubagents() {
|
|
@@ -1,6 +1,41 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
"use strict";
|
|
3
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
4
|
+
if (k2 === undefined) k2 = k;
|
|
5
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
6
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
7
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
8
|
+
}
|
|
9
|
+
Object.defineProperty(o, k2, desc);
|
|
10
|
+
}) : (function(o, m, k, k2) {
|
|
11
|
+
if (k2 === undefined) k2 = k;
|
|
12
|
+
o[k2] = m[k];
|
|
13
|
+
}));
|
|
14
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
15
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
16
|
+
}) : function(o, v) {
|
|
17
|
+
o["default"] = v;
|
|
18
|
+
});
|
|
19
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
20
|
+
var ownKeys = function(o) {
|
|
21
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
22
|
+
var ar = [];
|
|
23
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
24
|
+
return ar;
|
|
25
|
+
};
|
|
26
|
+
return ownKeys(o);
|
|
27
|
+
};
|
|
28
|
+
return function (mod) {
|
|
29
|
+
if (mod && mod.__esModule) return mod;
|
|
30
|
+
var result = {};
|
|
31
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
32
|
+
__setModuleDefault(result, mod);
|
|
33
|
+
return result;
|
|
34
|
+
};
|
|
35
|
+
})();
|
|
3
36
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const path = __importStar(require("path"));
|
|
4
39
|
const process_manager_1 = require("./process-manager");
|
|
5
40
|
const daemon_1 = require("./daemon");
|
|
6
41
|
const runtime_1 = require("../../nerves/runtime");
|
|
@@ -10,6 +45,8 @@ const task_scheduler_1 = require("./task-scheduler");
|
|
|
10
45
|
const runtime_logging_1 = require("./runtime-logging");
|
|
11
46
|
const sense_manager_1 = require("./sense-manager");
|
|
12
47
|
const agent_discovery_1 = require("./agent-discovery");
|
|
48
|
+
const identity_1 = require("../identity");
|
|
49
|
+
const runtime_mode_1 = require("./runtime-mode");
|
|
13
50
|
function parseSocketPath(argv) {
|
|
14
51
|
const socketIndex = argv.indexOf("--socket");
|
|
15
52
|
if (socketIndex >= 0) {
|
|
@@ -21,11 +58,13 @@ function parseSocketPath(argv) {
|
|
|
21
58
|
}
|
|
22
59
|
const socketPath = parseSocketPath(process.argv);
|
|
23
60
|
(0, runtime_logging_1.configureDaemonRuntimeLogger)("daemon");
|
|
61
|
+
const entryPath = path.resolve(__dirname, "daemon-entry.js");
|
|
62
|
+
const mode = (0, runtime_mode_1.detectRuntimeMode)((0, identity_1.getRepoRoot)());
|
|
24
63
|
(0, runtime_1.emitNervesEvent)({
|
|
25
64
|
component: "daemon",
|
|
26
65
|
event: "daemon.entry_start",
|
|
27
66
|
message: "starting daemon entrypoint",
|
|
28
|
-
meta: { socketPath },
|
|
67
|
+
meta: { socketPath, entryPath, mode },
|
|
29
68
|
});
|
|
30
69
|
const managedAgents = (0, agent_discovery_1.listEnabledBundleAgents)();
|
|
31
70
|
const processManager = new process_manager_1.DaemonProcessManager({
|
|
@@ -35,6 +74,7 @@ const processManager = new process_manager_1.DaemonProcessManager({
|
|
|
35
74
|
channel: "inner-dialog",
|
|
36
75
|
autoStart: true,
|
|
37
76
|
})),
|
|
77
|
+
existsSync: fs.existsSync,
|
|
38
78
|
});
|
|
39
79
|
const scheduler = new task_scheduler_1.TaskDrivenScheduler({
|
|
40
80
|
agents: [...managedAgents],
|
|
@@ -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
|
}
|
|
@@ -40,6 +40,7 @@ const path = __importStar(require("path"));
|
|
|
40
40
|
const identity_1 = require("../identity");
|
|
41
41
|
const runtime_1 = require("../../nerves/runtime");
|
|
42
42
|
const runtime_metadata_1 = require("./runtime-metadata");
|
|
43
|
+
const runtime_mode_1 = require("./runtime-mode");
|
|
43
44
|
const update_hooks_1 = require("./update-hooks");
|
|
44
45
|
const bundle_meta_1 = require("./hooks/bundle-meta");
|
|
45
46
|
const bundle_manifest_1 = require("../../mind/bundle-manifest");
|
|
@@ -373,6 +374,7 @@ class OuroDaemon {
|
|
|
373
374
|
const snapshots = this.processManager.listAgentSnapshots();
|
|
374
375
|
const workers = buildWorkerRows(snapshots);
|
|
375
376
|
const senses = this.senseManager?.listSenseRows() ?? [];
|
|
377
|
+
const repoRoot = (0, identity_1.getRepoRoot)();
|
|
376
378
|
const data = {
|
|
377
379
|
overview: {
|
|
378
380
|
daemon: "running",
|
|
@@ -381,6 +383,8 @@ class OuroDaemon {
|
|
|
381
383
|
...(0, runtime_metadata_1.getRuntimeMetadata)(),
|
|
382
384
|
workerCount: workers.length,
|
|
383
385
|
senseCount: senses.length,
|
|
386
|
+
entryPath: path.join(repoRoot, "dist", "heart", "daemon", "daemon-entry.js"),
|
|
387
|
+
mode: (0, runtime_mode_1.detectRuntimeMode)(repoRoot),
|
|
384
388
|
},
|
|
385
389
|
workers,
|
|
386
390
|
senses,
|
|
@@ -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",
|
|
@@ -71,6 +74,9 @@ function generateDaemonPlist(options) {
|
|
|
71
74
|
` <key>KeepAlive</key>`,
|
|
72
75
|
` <true/>`,
|
|
73
76
|
];
|
|
77
|
+
if (options.envPath) {
|
|
78
|
+
lines.push(` <key>EnvironmentVariables</key>`, ` <dict>`, ` <key>PATH</key>`, ` <string>${options.envPath}</string>`, ` </dict>`);
|
|
79
|
+
}
|
|
74
80
|
if (options.logDir) {
|
|
75
81
|
lines.push(` <key>StandardOutPath</key>`, ` <string>${path.join(options.logDir, "ouro-daemon-stdout.log")}</string>`, ` <key>StandardErrorPath</key>`, ` <string>${path.join(options.logDir, "ouro-daemon-stderr.log")}</string>`);
|
|
76
82
|
}
|
|
@@ -102,15 +108,16 @@ function installLaunchAgent(deps, options) {
|
|
|
102
108
|
meta: { entryPath: options.entryPath, socketPath: options.socketPath },
|
|
103
109
|
});
|
|
104
110
|
const fullPath = plistFilePath(deps.homeDir);
|
|
111
|
+
const domain = userLaunchDomain(deps.userUid);
|
|
105
112
|
// Unload existing (best effort) for idempotent re-install
|
|
106
113
|
if (deps.existsFile(fullPath)) {
|
|
107
114
|
try {
|
|
108
|
-
deps.exec(`launchctl
|
|
115
|
+
deps.exec(`launchctl bootout ${domain} "${fullPath}"`);
|
|
109
116
|
}
|
|
110
117
|
catch { /* best effort */ }
|
|
111
118
|
}
|
|
112
119
|
writeLaunchAgentPlist(deps, options);
|
|
113
|
-
deps.exec(`launchctl
|
|
120
|
+
deps.exec(`launchctl bootstrap ${domain} "${fullPath}"`);
|
|
114
121
|
(0, runtime_1.emitNervesEvent)({
|
|
115
122
|
component: "daemon",
|
|
116
123
|
event: "daemon.launchd_installed",
|
|
@@ -126,9 +133,10 @@ function uninstallLaunchAgent(deps) {
|
|
|
126
133
|
meta: {},
|
|
127
134
|
});
|
|
128
135
|
const fullPath = plistFilePath(deps.homeDir);
|
|
136
|
+
const domain = userLaunchDomain(deps.userUid);
|
|
129
137
|
if (deps.existsFile(fullPath)) {
|
|
130
138
|
try {
|
|
131
|
-
deps.exec(`launchctl
|
|
139
|
+
deps.exec(`launchctl bootout ${domain} "${fullPath}"`);
|
|
132
140
|
}
|
|
133
141
|
catch { /* best effort */ }
|
|
134
142
|
deps.removeFile(fullPath);
|
|
@@ -51,6 +51,7 @@ class DaemonProcessManager {
|
|
|
51
51
|
now;
|
|
52
52
|
setTimeoutFn;
|
|
53
53
|
clearTimeoutFn;
|
|
54
|
+
existsSyncFn;
|
|
54
55
|
constructor(options) {
|
|
55
56
|
this.maxRestartsPerHour = options.maxRestartsPerHour ?? 10;
|
|
56
57
|
this.stabilityThresholdMs = options.stabilityThresholdMs ?? 60_000;
|
|
@@ -60,6 +61,7 @@ class DaemonProcessManager {
|
|
|
60
61
|
this.now = options.now ?? (() => Date.now());
|
|
61
62
|
this.setTimeoutFn = options.setTimeoutFn ?? ((cb, delay) => setTimeout(cb, delay));
|
|
62
63
|
this.clearTimeoutFn = options.clearTimeoutFn ?? ((timer) => clearTimeout(timer));
|
|
64
|
+
this.existsSyncFn = options.existsSync ?? null;
|
|
63
65
|
for (const agent of options.agents) {
|
|
64
66
|
this.agents.set(agent.name, {
|
|
65
67
|
config: agent,
|
|
@@ -96,6 +98,17 @@ class DaemonProcessManager {
|
|
|
96
98
|
state.snapshot.status = "starting";
|
|
97
99
|
const runCwd = (0, identity_1.getRepoRoot)();
|
|
98
100
|
const entryScript = path.join((0, identity_1.getRepoRoot)(), "dist", state.config.entry);
|
|
101
|
+
if (this.existsSyncFn && !this.existsSyncFn(entryScript)) {
|
|
102
|
+
state.snapshot.status = "crashed";
|
|
103
|
+
(0, runtime_1.emitNervesEvent)({
|
|
104
|
+
level: "error",
|
|
105
|
+
component: "daemon",
|
|
106
|
+
event: "daemon.agent_entry_missing",
|
|
107
|
+
message: "agent entry script does not exist — cannot spawn. Run 'ouro daemon install' from the correct location.",
|
|
108
|
+
meta: { agent, entryScript },
|
|
109
|
+
});
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
99
112
|
const args = [entryScript, "--agent", state.config.agentArg ?? agent, ...(state.config.args ?? [])];
|
|
100
113
|
const child = this.spawnFn("node", args, {
|
|
101
114
|
cwd: runCwd,
|
|
@@ -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
|
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.detectRuntimeMode = detectRuntimeMode;
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
const runtime_1 = require("../../nerves/runtime");
|
|
40
|
+
function detectRuntimeMode(rootPath, deps = {}) {
|
|
41
|
+
const checkExists = deps.existsSync ?? fs.existsSync;
|
|
42
|
+
// 1. Production: installed via npm
|
|
43
|
+
if (rootPath.includes("node_modules/@ouro.bot/cli") ||
|
|
44
|
+
rootPath.includes("node_modules/ouro.bot")) {
|
|
45
|
+
(0, runtime_1.emitNervesEvent)({
|
|
46
|
+
component: "daemon",
|
|
47
|
+
event: "daemon.runtime_mode_detected",
|
|
48
|
+
message: "detected runtime mode",
|
|
49
|
+
meta: { rootPath, mode: "production" },
|
|
50
|
+
});
|
|
51
|
+
return "production";
|
|
52
|
+
}
|
|
53
|
+
// 2-4. Everything else is dev: worktrees, git repos, unknown paths
|
|
54
|
+
// (conservative default: assume dev unless proven production)
|
|
55
|
+
const reason = rootPath.includes(".claude/worktrees/")
|
|
56
|
+
? "worktree"
|
|
57
|
+
: checkExists(path.join(rootPath, ".git"))
|
|
58
|
+
? "git-repo"
|
|
59
|
+
: "unknown";
|
|
60
|
+
(0, runtime_1.emitNervesEvent)({
|
|
61
|
+
component: "daemon",
|
|
62
|
+
event: "daemon.runtime_mode_detected",
|
|
63
|
+
message: "detected runtime mode",
|
|
64
|
+
meta: { rootPath, mode: "dev", reason },
|
|
65
|
+
});
|
|
66
|
+
return "dev";
|
|
67
|
+
}
|
|
@@ -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
|
}
|