@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 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": [
@@ -138,7 +138,7 @@ function defaultRuntimeConfig() {
138
138
  integrations: { ...DEFAULT_SECRETS_TEMPLATE.integrations },
139
139
  };
140
140
  }
141
- let _cachedConfig = null;
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
- _cachedConfig = deepMerge(defaultRuntimeConfig(), sanitizedFileData);
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 _cachedConfig;
237
+ return config;
243
238
  }
244
239
  function resetConfigCache() {
245
- _cachedConfig = null;
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
- _cachedConfig = deepMerge(_cachedConfig, partial);
249
+ _runtimeConfigOverride = deepMerge((_runtimeConfigOverride ?? {}), partial);
256
250
  }
257
251
  function getAzureConfig() {
258
252
  const config = loadConfig();
@@ -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
- if (!_providerRuntime) {
44
- try {
45
- _providerRuntime = createProviderRegistry().resolve();
46
- }
47
- catch (error) {
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
- return _providerRuntime;
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 call to getProviderRuntime()
77
- * re-creates it from current config. Used by the adoption specialist to
78
- * switch provider context without restarting the process.
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 model = getModel();
128
+ const provider = (0, identity_1.loadAgentConfig)().provider;
106
129
  const providerLabelBuilders = {
107
- azure: () => `azure openai (${(0, config_1.getAzureConfig)().deployment || "default"}, model: ${model})`,
108
- anthropic: () => `anthropic (${model})`,
109
- minimax: () => `minimax (${model})`,
110
- "openai-codex": () => `openai codex (${model})`,
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[getProvider()]();
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
- fetchRunningVersion: async () => {
214
- const status = await deps.sendCommand(deps.socketPath, { kind: "daemon.status" });
215
- const payload = parseStatusPayload(status.data);
216
- return payload?.overview.version ?? "unknown";
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.writeLaunchAgentPlist)(launchdDeps, {
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 runningVersion = await deps.fetchRunningVersion();
77
+ const runningRuntime = await readRunningRuntimeIdentity(deps);
78
+ const runningVersion = runningRuntime.version;
79
+ const driftReasons = collectRuntimeDriftReasons(localRuntime, runningRuntime);
14
80
  let result;
15
- if (isKnownVersion(deps.localVersion) &&
16
- isKnownVersion(runningVersion) &&
17
- runningVersion !== deps.localVersion) {
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: `daemon already running (${deps.socketPath}; could not replace stale daemon ${runningVersion} -> ${deps.localVersion}: ${reason})`,
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: { socketPath: deps.socketPath, localVersion: deps.localVersion, runningVersion, action: "stale_replace_failed", reason },
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: `restarted stale daemon from ${runningVersion} to ${deps.localVersion} (pid ${started.pid ?? "unknown"})`,
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: { socketPath: deps.socketPath, localVersion: deps.localVersion, runningVersion, action: "stale_restarted", pid: started.pid ?? null },
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(deps.localVersion) || !isKnownVersion(runningVersion)) {
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: { socketPath: deps.socketPath, localVersion: deps.localVersion, runningVersion, action: "unknown_version" },
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: { socketPath: deps.socketPath, localVersion: deps.localVersion, action: "status_lookup_failed", reason },
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: { socketPath: deps.socketPath, localVersion: deps.localVersion, action: "already_current" },
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 unload "${fullPath}"`);
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 load "${fullPath}"`);
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 unload "${fullPath}"`);
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 (snapshot?.runtime !== "running" || !fs.existsSync(runtimePath)) {
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
- return { runtime: snapshot.runtime };
206
+ if (state.upstreamStatus === "ok") {
207
+ return { runtime: "running" };
208
+ }
209
+ return { runtime: snapshot?.runtime };
193
210
  }
194
211
  class DaemonSenseManager {
195
212
  processManager;
@@ -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
- * Caches the result after first load.
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
- _cachedAgentConfig = {
315
+ const config = {
325
316
  version,
326
317
  enabled,
327
- provider: rawProvider,
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 _cachedAgentConfig;
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
- * Clear only the cached agent config while preserving the resolved agent identity.
360
- * Used when a running agent should pick up updated disk-backed config on the next turn.
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
- _cachedAgentConfig = null;
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.52",
3
+ "version": "0.1.0-alpha.54",
4
4
  "main": "dist/heart/daemon/ouro-entry.js",
5
5
  "bin": {
6
6
  "cli": "dist/heart/daemon/ouro-bot-entry.js",