@ouro.bot/cli 0.1.0-alpha.53 → 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,14 @@
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
+ },
4
12
  {
5
13
  "version": "0.1.0-alpha.53",
6
14
  "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");
@@ -93,6 +93,8 @@ function parseStatusPayload(data) {
93
93
  socketPath: stringField(overview.socketPath) ?? "unknown",
94
94
  version: stringField(overview.version) ?? "unknown",
95
95
  lastUpdated: stringField(overview.lastUpdated) ?? "unknown",
96
+ repoRoot: stringField(overview.repoRoot) ?? "unknown",
97
+ configFingerprint: stringField(overview.configFingerprint) ?? "unknown",
96
98
  workerCount: numberField(overview.workerCount) ?? 0,
97
99
  senseCount: numberField(overview.senseCount) ?? 0,
98
100
  entryPath: stringField(overview.entryPath) ?? "unknown",
@@ -212,14 +214,28 @@ async function ensureDaemonRunning(deps) {
212
214
  const alive = await deps.checkSocketAlive(deps.socketPath);
213
215
  if (alive) {
214
216
  const localRuntime = (0, runtime_metadata_1.getRuntimeMetadata)();
217
+ let runningRuntimePromise = null;
218
+ const fetchRunningRuntimeMetadata = async () => {
219
+ runningRuntimePromise ??= (async () => {
220
+ const status = await deps.sendCommand(deps.socketPath, { kind: "daemon.status" });
221
+ const payload = parseStatusPayload(status.data);
222
+ return {
223
+ version: payload?.overview.version ?? "unknown",
224
+ lastUpdated: payload?.overview.lastUpdated ?? "unknown",
225
+ repoRoot: payload?.overview.repoRoot ?? "unknown",
226
+ configFingerprint: payload?.overview.configFingerprint ?? "unknown",
227
+ };
228
+ })();
229
+ return runningRuntimePromise;
230
+ };
215
231
  return (0, daemon_runtime_sync_1.ensureCurrentDaemonRuntime)({
216
232
  socketPath: deps.socketPath,
217
233
  localVersion: localRuntime.version,
218
- fetchRunningVersion: async () => {
219
- const status = await deps.sendCommand(deps.socketPath, { kind: "daemon.status" });
220
- const payload = parseStatusPayload(status.data);
221
- return payload?.overview.version ?? "unknown";
222
- },
234
+ localLastUpdated: localRuntime.lastUpdated,
235
+ localRepoRoot: localRuntime.repoRoot,
236
+ localConfigFingerprint: localRuntime.configFingerprint,
237
+ fetchRunningVersion: async () => (await fetchRunningRuntimeMetadata()).version,
238
+ fetchRunningRuntimeMetadata,
223
239
  stopDaemon: async () => {
224
240
  await deps.sendCommand(deps.socketPath, { kind: "daemon.stop" });
225
241
  },
@@ -285,6 +301,8 @@ function buildStoppedStatusPayload(socketPath) {
285
301
  socketPath,
286
302
  version: metadata.version,
287
303
  lastUpdated: metadata.lastUpdated,
304
+ repoRoot: metadata.repoRoot,
305
+ configFingerprint: metadata.configFingerprint,
288
306
  workerCount: 0,
289
307
  senseCount: 0,
290
308
  entryPath: path.join(repoRoot, "dist", "heart", "daemon", "daemon-entry.js"),
@@ -732,9 +750,13 @@ function defaultEnsureDaemonBootPersistence(socketPath) {
732
750
  }
733
751
  const homeDir = os.homedir();
734
752
  const launchdDeps = {
753
+ exec: (cmd) => { (0, child_process_1.execSync)(cmd, { stdio: "ignore" }); },
735
754
  writeFile: (filePath, content) => fs.writeFileSync(filePath, content, "utf-8"),
755
+ removeFile: (filePath) => fs.rmSync(filePath, { force: true }),
756
+ existsFile: (filePath) => fs.existsSync(filePath),
736
757
  mkdirp: (dir) => fs.mkdirSync(dir, { recursive: true }),
737
758
  homeDir,
759
+ userUid: process.getuid?.() ?? 0,
738
760
  };
739
761
  const entryPath = path.join((0, identity_1.getRepoRoot)(), "dist", "heart", "daemon", "daemon-entry.js");
740
762
  /* v8 ignore next -- covered via mock in daemon-cli-defaults.test.ts; v8 on CI attributes the real fs.existsSync branch to the non-mock load @preserve */
@@ -748,7 +770,7 @@ function defaultEnsureDaemonBootPersistence(socketPath) {
748
770
  });
749
771
  }
750
772
  const logDir = path.join(homeDir, ".agentstate", "daemon", "logs");
751
- (0, launchd_1.writeLaunchAgentPlist)(launchdDeps, {
773
+ (0, launchd_1.installLaunchAgent)(launchdDeps, {
752
774
  nodePath: process.execPath,
753
775
  entryPath,
754
776
  socketPath,
@@ -5,16 +5,82 @@ const runtime_1 = require("../../nerves/runtime");
5
5
  function isKnownVersion(version) {
6
6
  return version !== "unknown" && version.trim().length > 0;
7
7
  }
8
+ function isKnownRuntimeValue(value) {
9
+ return typeof value === "string" && value.trim().length > 0 && value !== "unknown";
10
+ }
8
11
  function formatErrorReason(error) {
9
12
  return error instanceof Error ? error.message : String(error);
10
13
  }
14
+ function normalizeRuntimeIdentity(value) {
15
+ return {
16
+ version: typeof value.version === "string" ? value.version : "unknown",
17
+ lastUpdated: typeof value.lastUpdated === "string" ? value.lastUpdated : "unknown",
18
+ repoRoot: typeof value.repoRoot === "string" ? value.repoRoot : "unknown",
19
+ configFingerprint: typeof value.configFingerprint === "string" ? value.configFingerprint : "unknown",
20
+ };
21
+ }
22
+ async function readRunningRuntimeIdentity(deps) {
23
+ if (!deps.fetchRunningRuntimeMetadata) {
24
+ return normalizeRuntimeIdentity({
25
+ version: await deps.fetchRunningVersion(),
26
+ });
27
+ }
28
+ const metadata = normalizeRuntimeIdentity(await deps.fetchRunningRuntimeMetadata());
29
+ if (isKnownVersion(metadata.version))
30
+ return metadata;
31
+ return normalizeRuntimeIdentity({
32
+ ...metadata,
33
+ version: await deps.fetchRunningVersion(),
34
+ });
35
+ }
36
+ function collectRuntimeDriftReasons(local, running) {
37
+ const reasons = [];
38
+ const comparableVersions = isKnownVersion(local.version) && isKnownVersion(running.version);
39
+ if (comparableVersions && local.version !== running.version) {
40
+ reasons.push({ key: "version", label: "version", local: local.version, running: running.version });
41
+ }
42
+ if (comparableVersions && isKnownRuntimeValue(local.lastUpdated) && isKnownRuntimeValue(running.lastUpdated) && local.lastUpdated !== running.lastUpdated) {
43
+ reasons.push({ key: "lastUpdated", label: "last updated", local: local.lastUpdated, running: running.lastUpdated });
44
+ }
45
+ if (isKnownRuntimeValue(local.repoRoot) && isKnownRuntimeValue(running.repoRoot) && local.repoRoot !== running.repoRoot) {
46
+ reasons.push({ key: "repoRoot", label: "code path", local: local.repoRoot, running: running.repoRoot });
47
+ }
48
+ if (isKnownRuntimeValue(local.configFingerprint)
49
+ && isKnownRuntimeValue(running.configFingerprint)
50
+ && local.configFingerprint !== running.configFingerprint) {
51
+ reasons.push({
52
+ key: "configFingerprint",
53
+ label: "config fingerprint",
54
+ local: local.configFingerprint,
55
+ running: running.configFingerprint,
56
+ });
57
+ }
58
+ return reasons;
59
+ }
60
+ function formatRuntimeValue(reason) {
61
+ if (reason.key === "configFingerprint") {
62
+ return `${reason.running.slice(0, 12)} -> ${reason.local.slice(0, 12)}`;
63
+ }
64
+ return `${reason.running} -> ${reason.local}`;
65
+ }
66
+ function formatRuntimeDriftSummary(reasons) {
67
+ return reasons.map((reason) => `${reason.label} ${formatRuntimeValue(reason)}`).join("; ");
68
+ }
11
69
  async function ensureCurrentDaemonRuntime(deps) {
70
+ const localRuntime = normalizeRuntimeIdentity({
71
+ version: deps.localVersion,
72
+ lastUpdated: deps.localLastUpdated,
73
+ repoRoot: deps.localRepoRoot,
74
+ configFingerprint: deps.localConfigFingerprint,
75
+ });
12
76
  try {
13
- const 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
  }
@@ -45,6 +45,9 @@ exports.DAEMON_PLIST_LABEL = "bot.ouro.daemon";
45
45
  function plistFilePath(homeDir) {
46
46
  return path.join(homeDir, "Library", "LaunchAgents", `${exports.DAEMON_PLIST_LABEL}.plist`);
47
47
  }
48
+ function userLaunchDomain(userUid) {
49
+ return `gui/${userUid}`;
50
+ }
48
51
  function generateDaemonPlist(options) {
49
52
  (0, runtime_1.emitNervesEvent)({
50
53
  component: "daemon",
@@ -105,15 +108,16 @@ function installLaunchAgent(deps, options) {
105
108
  meta: { entryPath: options.entryPath, socketPath: options.socketPath },
106
109
  });
107
110
  const fullPath = plistFilePath(deps.homeDir);
111
+ const domain = userLaunchDomain(deps.userUid);
108
112
  // Unload existing (best effort) for idempotent re-install
109
113
  if (deps.existsFile(fullPath)) {
110
114
  try {
111
- deps.exec(`launchctl unload "${fullPath}"`);
115
+ deps.exec(`launchctl bootout ${domain} "${fullPath}"`);
112
116
  }
113
117
  catch { /* best effort */ }
114
118
  }
115
119
  writeLaunchAgentPlist(deps, options);
116
- deps.exec(`launchctl load "${fullPath}"`);
120
+ deps.exec(`launchctl bootstrap ${domain} "${fullPath}"`);
117
121
  (0, runtime_1.emitNervesEvent)({
118
122
  component: "daemon",
119
123
  event: "daemon.launchd_installed",
@@ -129,9 +133,10 @@ function uninstallLaunchAgent(deps) {
129
133
  meta: {},
130
134
  });
131
135
  const fullPath = plistFilePath(deps.homeDir);
136
+ const domain = userLaunchDomain(deps.userUid);
132
137
  if (deps.existsFile(fullPath)) {
133
138
  try {
134
- deps.exec(`launchctl unload "${fullPath}"`);
139
+ deps.exec(`launchctl bootout ${domain} "${fullPath}"`);
135
140
  }
136
141
  catch { /* best effort */ }
137
142
  deps.removeFile(fullPath);
@@ -34,7 +34,9 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.getRuntimeMetadata = getRuntimeMetadata;
37
+ const crypto_1 = require("crypto");
37
38
  const fs = __importStar(require("fs"));
39
+ const os = __importStar(require("os"));
38
40
  const path = __importStar(require("path"));
39
41
  const childProcess = __importStar(require("child_process"));
40
42
  const identity_1 = require("../identity");
@@ -85,10 +87,98 @@ function readLastUpdated(repoRoot, packageJsonPath, statSyncImpl, execFileSyncIm
85
87
  return { value: UNKNOWN_METADATA, source: "unknown" };
86
88
  }
87
89
  }
90
+ function readHomeDir() {
91
+ const homedirImpl = optionalFunction(os, "homedir");
92
+ if (!homedirImpl) {
93
+ return null;
94
+ }
95
+ try {
96
+ return homedirImpl.call(os);
97
+ }
98
+ catch {
99
+ return null;
100
+ }
101
+ }
102
+ function listConfigTargets(bundlesRoot, secretsRoot, daemonLoggingPath, readdirSyncImpl) {
103
+ if (!readdirSyncImpl)
104
+ return [];
105
+ const targets = new Set();
106
+ if (daemonLoggingPath) {
107
+ targets.add(daemonLoggingPath);
108
+ }
109
+ try {
110
+ const bundleEntries = readdirSyncImpl(bundlesRoot, { withFileTypes: true });
111
+ for (const entry of bundleEntries) {
112
+ if (!entry.isDirectory() || !entry.name.endsWith(".ouro"))
113
+ continue;
114
+ targets.add(path.join(bundlesRoot, entry.name, "agent.json"));
115
+ }
116
+ }
117
+ catch {
118
+ // ignore unreadable bundle roots
119
+ }
120
+ if (secretsRoot) {
121
+ try {
122
+ const secretEntries = readdirSyncImpl(secretsRoot, { withFileTypes: true });
123
+ for (const entry of secretEntries) {
124
+ if (!entry.isDirectory())
125
+ continue;
126
+ targets.add(path.join(secretsRoot, entry.name, "secrets.json"));
127
+ }
128
+ }
129
+ catch {
130
+ // ignore unreadable secrets roots
131
+ }
132
+ }
133
+ return [...targets].sort();
134
+ }
135
+ function readConfigFingerprint(targets, readFileSyncImpl, existsSyncImpl) {
136
+ if (!readFileSyncImpl || !existsSyncImpl) {
137
+ return {
138
+ value: UNKNOWN_METADATA,
139
+ source: "unknown",
140
+ trackedFiles: targets.length,
141
+ presentFiles: 0,
142
+ };
143
+ }
144
+ const hash = (0, crypto_1.createHash)("sha256");
145
+ let presentFiles = 0;
146
+ for (const target of targets) {
147
+ hash.update(target);
148
+ hash.update("\0");
149
+ if (!existsSyncImpl(target)) {
150
+ hash.update("missing");
151
+ hash.update("\0");
152
+ continue;
153
+ }
154
+ presentFiles += 1;
155
+ hash.update("present");
156
+ hash.update("\0");
157
+ try {
158
+ hash.update(readFileSyncImpl(target, "utf-8"));
159
+ }
160
+ catch {
161
+ hash.update("unreadable");
162
+ }
163
+ hash.update("\0");
164
+ }
165
+ return {
166
+ value: hash.digest("hex"),
167
+ source: "content-hash",
168
+ trackedFiles: targets.length,
169
+ presentFiles,
170
+ };
171
+ }
88
172
  function getRuntimeMetadata(deps = {}) {
89
173
  const repoRoot = deps.repoRoot ?? (0, identity_1.getRepoRoot)();
174
+ const bundlesRoot = deps.bundlesRoot ?? (0, identity_1.getAgentBundlesRoot)();
175
+ const homeDir = readHomeDir();
176
+ const secretsRoot = deps.secretsRoot ?? (homeDir ? path.join(homeDir, ".agentsecrets") : null);
177
+ const daemonLoggingPath = deps.daemonLoggingPath ?? (homeDir ? path.join(homeDir, ".agentstate", "daemon", "logging.json") : null);
90
178
  const readFileSyncImpl = deps.readFileSync ?? optionalFunction(fs, "readFileSync")?.bind(fs) ?? null;
91
179
  const statSyncImpl = deps.statSync ?? optionalFunction(fs, "statSync")?.bind(fs) ?? null;
180
+ const readdirSyncImpl = deps.readdirSync ?? optionalFunction(fs, "readdirSync")?.bind(fs) ?? null;
181
+ const existsSyncImpl = deps.existsSync ?? optionalFunction(fs, "existsSync")?.bind(fs) ?? null;
92
182
  const execFileSyncImpl = deps.execFileSync
93
183
  ?? optionalFunction(childProcess, "execFileSync")?.bind(childProcess)
94
184
  ?? null;
@@ -101,6 +191,8 @@ function getRuntimeMetadata(deps = {}) {
101
191
  throw new Error("git unavailable");
102
192
  }))
103
193
  : { value: UNKNOWN_METADATA, source: "unknown" };
194
+ const configTargets = listConfigTargets(bundlesRoot, secretsRoot, daemonLoggingPath, readdirSyncImpl);
195
+ const configFingerprint = readConfigFingerprint(configTargets, readFileSyncImpl, existsSyncImpl);
104
196
  (0, runtime_1.emitNervesEvent)({
105
197
  component: "daemon",
106
198
  event: "daemon.runtime_metadata_read",
@@ -109,10 +201,19 @@ function getRuntimeMetadata(deps = {}) {
109
201
  version,
110
202
  lastUpdated: lastUpdated.value,
111
203
  lastUpdatedSource: lastUpdated.source,
204
+ repoRoot,
205
+ configFingerprint: configFingerprint.value === UNKNOWN_METADATA
206
+ ? UNKNOWN_METADATA
207
+ : configFingerprint.value.slice(0, 12),
208
+ configFingerprintSource: configFingerprint.source,
209
+ configTrackedFiles: configFingerprint.trackedFiles,
210
+ configPresentFiles: configFingerprint.presentFiles,
112
211
  },
113
212
  });
114
213
  return {
115
214
  version,
116
215
  lastUpdated: lastUpdated.value,
216
+ repoRoot,
217
+ configFingerprint: configFingerprint.value,
117
218
  };
118
219
  }
@@ -45,6 +45,7 @@ const process_manager_1 = require("./process-manager");
45
45
  const DEFAULT_TEAMS_PORT = 3978;
46
46
  const DEFAULT_BLUEBUBBLES_PORT = 18790;
47
47
  const DEFAULT_BLUEBUBBLES_WEBHOOK_PATH = "/bluebubbles-webhook";
48
+ const BLUEBUBBLES_RUNTIME_FRESHNESS_WINDOW_MS = 90_000;
48
49
  function defaultSenses() {
49
50
  return {
50
51
  cli: { ...identity_1.DEFAULT_AGENT_SENSES.cli },
@@ -176,20 +177,36 @@ function runtimeInfoFor(status) {
176
177
  return { runtime: "running" };
177
178
  return { runtime: "error" };
178
179
  }
180
+ function blueBubblesRuntimeStateIsFresh(lastCheckedAt, now = Date.now()) {
181
+ if (!lastCheckedAt) {
182
+ return false;
183
+ }
184
+ const checkedAt = Date.parse(lastCheckedAt);
185
+ if (!Number.isFinite(checkedAt)) {
186
+ return false;
187
+ }
188
+ return checkedAt >= now - BLUEBUBBLES_RUNTIME_FRESHNESS_WINDOW_MS;
189
+ }
179
190
  function readBlueBubblesRuntimeFacts(agent, bundlesRoot, snapshot) {
180
191
  const agentRoot = path.join(bundlesRoot, `${agent}.ouro`);
181
192
  const runtimePath = path.join(agentRoot, "state", "senses", "bluebubbles", "runtime.json");
182
- if (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.53",
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",