@ouro.bot/cli 0.1.0-alpha.519 → 0.1.0-alpha.520
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 +27 -0
- package/dist/heart/agent-entry.js +1 -1
- package/dist/heart/auth/auth-flow.js +1 -0
- package/dist/heart/core.js +12 -6
- package/dist/heart/daemon/agent-config-check.js +1 -1
- package/dist/heart/daemon/cli-exec.js +106 -83
- package/dist/heart/daemon/daemon-health.js +1 -0
- package/dist/heart/daemon/daemon.js +85 -6
- package/dist/heart/daemon/process-manager.js +223 -82
- package/dist/heart/daemon/sense-manager.js +86 -5
- package/dist/heart/daemon/startup-tui.js +67 -1
- package/dist/heart/outlook/readers/mail.js +2 -2
- package/dist/heart/provider-credentials.js +2 -1
- package/dist/heart/runtime-credentials.js +107 -0
- package/dist/heart/session-events.js +8 -9
- package/dist/heart/session-transcript.js +82 -6
- package/dist/heart/turn-context.js +12 -3
- package/dist/mailroom/reader.js +9 -0
- package/dist/mind/prompt.js +12 -10
- package/dist/repertoire/bitwarden-store.js +55 -13
- package/dist/repertoire/tools-bridge.js +1 -0
- package/dist/repertoire/tools-mail.js +11 -11
- package/dist/repertoire/tools-session.js +1 -0
- package/dist/senses/bluebubbles/entry.js +7 -3
- package/dist/senses/bluebubbles/index.js +45 -9
- package/dist/senses/cli-entry.js +1 -1
- package/dist/senses/teams-entry.js +1 -1
- package/package.json +1 -1
|
@@ -53,6 +53,7 @@ class DaemonProcessManager {
|
|
|
53
53
|
clearTimeoutFn;
|
|
54
54
|
cooldownRecoveryMs;
|
|
55
55
|
maxCooldownRetries;
|
|
56
|
+
startupStaleAfterMs;
|
|
56
57
|
existsSyncFn;
|
|
57
58
|
configCheckFn;
|
|
58
59
|
statusWriterFn;
|
|
@@ -98,6 +99,29 @@ class DaemonProcessManager {
|
|
|
98
99
|
});
|
|
99
100
|
}
|
|
100
101
|
}
|
|
102
|
+
currentTimeMs() {
|
|
103
|
+
const value = this.now();
|
|
104
|
+
return Number.isFinite(value) ? value : Date.now();
|
|
105
|
+
}
|
|
106
|
+
isStartAttemptCurrent(state, attemptId) {
|
|
107
|
+
return state.startAttemptId === attemptId;
|
|
108
|
+
}
|
|
109
|
+
markConfigCheckFailed(state, errorReason, fixHint) {
|
|
110
|
+
const agent = state.config.name;
|
|
111
|
+
state.snapshot.status = "crashed";
|
|
112
|
+
state.snapshot.errorReason = errorReason;
|
|
113
|
+
state.snapshot.fixHint = fixHint;
|
|
114
|
+
(0, runtime_1.emitNervesEvent)({
|
|
115
|
+
level: "error",
|
|
116
|
+
component: "daemon",
|
|
117
|
+
event: "daemon.agent_config_invalid",
|
|
118
|
+
message: errorReason,
|
|
119
|
+
meta: { agent, fix: fixHint },
|
|
120
|
+
});
|
|
121
|
+
this.writeStatus(agent, `[daemon] ${agent}: ${errorReason}\n` +
|
|
122
|
+
(fixHint ? ` Fix: ${fixHint}\n` : ""));
|
|
123
|
+
this.notifySnapshotChange(state.snapshot);
|
|
124
|
+
}
|
|
101
125
|
constructor(options) {
|
|
102
126
|
this.maxRestartsPerHour = options.maxRestartsPerHour ?? 10;
|
|
103
127
|
this.stabilityThresholdMs = options.stabilityThresholdMs ?? 60_000;
|
|
@@ -105,6 +129,7 @@ class DaemonProcessManager {
|
|
|
105
129
|
this.maxBackoffMs = options.maxBackoffMs ?? 60_000;
|
|
106
130
|
this.cooldownRecoveryMs = options.cooldownRecoveryMs ?? 5 * 60 * 1_000;
|
|
107
131
|
this.maxCooldownRetries = options.maxCooldownRetries ?? 3;
|
|
132
|
+
this.startupStaleAfterMs = options.startupStaleAfterMs ?? 45_000;
|
|
108
133
|
this.spawnFn = options.spawn ?? ((command, args, spawnOptions) => (0, child_process_1.spawn)(command, args, spawnOptions));
|
|
109
134
|
this.now = options.now ?? (() => Date.now());
|
|
110
135
|
this.setTimeoutFn = options.setTimeoutFn ?? ((cb, delay) => setTimeout(cb, delay));
|
|
@@ -119,6 +144,9 @@ class DaemonProcessManager {
|
|
|
119
144
|
this.agents.set(agent.name, {
|
|
120
145
|
config: agent,
|
|
121
146
|
process: null,
|
|
147
|
+
startInFlight: false,
|
|
148
|
+
startAttemptedAtMs: null,
|
|
149
|
+
startAttemptId: 0,
|
|
122
150
|
restartTimer: null,
|
|
123
151
|
crashTimestamps: [],
|
|
124
152
|
stopRequested: false,
|
|
@@ -149,107 +177,193 @@ class DaemonProcessManager {
|
|
|
149
177
|
}
|
|
150
178
|
}
|
|
151
179
|
}
|
|
180
|
+
triggerAutoStartAgents() {
|
|
181
|
+
for (const state of this.agents.values()) {
|
|
182
|
+
if (!state.config.autoStart)
|
|
183
|
+
continue;
|
|
184
|
+
void this.startAgent(state.config.name).catch((error) => {
|
|
185
|
+
const errorReason = error instanceof Error ? error.message : String(error);
|
|
186
|
+
this.markConfigCheckFailed(state, `agent startup threw before the worker could run: ${errorReason}`, "Run 'ouro doctor' for diagnostics, then retry 'ouro up'.");
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
}
|
|
152
190
|
async startAgent(agent) {
|
|
153
191
|
const state = this.requireAgent(agent);
|
|
154
|
-
if (state.process)
|
|
192
|
+
if (state.process || state.startInFlight)
|
|
155
193
|
return;
|
|
156
|
-
|
|
157
|
-
state.
|
|
158
|
-
state.
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
194
|
+
const attemptId = state.startAttemptId + 1;
|
|
195
|
+
state.startAttemptId = attemptId;
|
|
196
|
+
state.startInFlight = true;
|
|
197
|
+
state.startAttemptedAtMs = this.currentTimeMs();
|
|
198
|
+
try {
|
|
199
|
+
this.clearRestartTimer(state);
|
|
200
|
+
state.stopRequested = false;
|
|
201
|
+
state.snapshot.status = "starting";
|
|
202
|
+
if (this.configCheckFn) {
|
|
203
|
+
let result;
|
|
204
|
+
try {
|
|
205
|
+
result = await this.configCheckFn(agent);
|
|
206
|
+
}
|
|
207
|
+
catch (error) {
|
|
208
|
+
const errorReason = error instanceof Error ? error.message : String(error);
|
|
209
|
+
this.markConfigCheckFailed(state, `agent config validation threw: ${errorReason}`, "Run 'ouro doctor' for diagnostics, then retry 'ouro up'.");
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
if (!this.isStartAttemptCurrent(state, attemptId))
|
|
213
|
+
return;
|
|
214
|
+
if (state.stopRequested) {
|
|
215
|
+
state.snapshot.status = "stopped";
|
|
216
|
+
state.snapshot.pid = null;
|
|
217
|
+
this.notifySnapshotChange(state.snapshot);
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
if (result.skip) {
|
|
221
|
+
state.snapshot.status = "stopped";
|
|
222
|
+
state.snapshot.errorReason = null;
|
|
223
|
+
state.snapshot.fixHint = null;
|
|
224
|
+
(0, runtime_1.emitNervesEvent)({
|
|
225
|
+
component: "daemon",
|
|
226
|
+
event: "daemon.agent_config_skipped",
|
|
227
|
+
message: result.error ?? "agent start skipped by config check",
|
|
228
|
+
meta: { agent, fix: result.fix ?? null },
|
|
229
|
+
});
|
|
230
|
+
this.notifySnapshotChange(state.snapshot);
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
if (!result.ok) {
|
|
234
|
+
this.markConfigCheckFailed(state, result.error ?? "agent config validation failed", result.fix ?? null);
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
/* v8 ignore next -- defensive duplicate stale-start guard before spawn preparation @preserve */
|
|
238
|
+
if (!this.isStartAttemptCurrent(state, attemptId))
|
|
239
|
+
return;
|
|
240
|
+
// Config check passed — clear any prior error so the pulse stops
|
|
241
|
+
// reporting the broken state. This is the recovery path: the user
|
|
242
|
+
// fixed their secrets/config, the next startAgent attempt sees a
|
|
243
|
+
// valid config, and the pulse goes quiet.
|
|
163
244
|
state.snapshot.errorReason = null;
|
|
164
245
|
state.snapshot.fixHint = null;
|
|
165
|
-
(0, runtime_1.emitNervesEvent)({
|
|
166
|
-
component: "daemon",
|
|
167
|
-
event: "daemon.agent_config_skipped",
|
|
168
|
-
message: result.error ?? "agent start skipped by config check",
|
|
169
|
-
meta: { agent, fix: result.fix ?? null },
|
|
170
|
-
});
|
|
171
|
-
this.notifySnapshotChange(state.snapshot);
|
|
172
|
-
return;
|
|
173
246
|
}
|
|
174
|
-
|
|
247
|
+
const runCwd = (0, identity_1.getRepoRoot)();
|
|
248
|
+
const entryScript = path.join((0, identity_1.getRepoRoot)(), "dist", state.config.entry);
|
|
249
|
+
if (this.existsSyncFn && !this.existsSyncFn(entryScript)) {
|
|
175
250
|
state.snapshot.status = "crashed";
|
|
176
|
-
// Surface the error and fix to the snapshot so sibling agents can
|
|
177
|
-
// read it via the pulse. Without this, the diagnosis stayed
|
|
178
|
-
// trapped in the nerves event and stderr — visible to humans
|
|
179
|
-
// running `ouro status` or grepping logs, but invisible to
|
|
180
|
-
// peer agents trying to coordinate around the broken state.
|
|
181
|
-
state.snapshot.errorReason = result.error ?? "agent config validation failed";
|
|
182
|
-
state.snapshot.fixHint = result.fix ?? null;
|
|
183
251
|
(0, runtime_1.emitNervesEvent)({
|
|
184
252
|
level: "error",
|
|
185
253
|
component: "daemon",
|
|
186
|
-
event: "daemon.
|
|
187
|
-
message:
|
|
188
|
-
meta: { agent,
|
|
254
|
+
event: "daemon.agent_entry_missing",
|
|
255
|
+
message: "agent entry script does not exist — cannot spawn. Run 'ouro daemon install' from the correct location.",
|
|
256
|
+
meta: { agent, entryScript },
|
|
189
257
|
});
|
|
190
|
-
this.writeStatus(agent, `[daemon] ${agent}: ${result.error}\n` +
|
|
191
|
-
(result.fix ? ` Fix: ${result.fix}\n` : ""));
|
|
192
258
|
this.notifySnapshotChange(state.snapshot);
|
|
193
259
|
return;
|
|
194
260
|
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
261
|
+
/* v8 ignore next -- defensive duplicate stale-start guard immediately before spawn @preserve */
|
|
262
|
+
if (!this.isStartAttemptCurrent(state, attemptId))
|
|
263
|
+
return;
|
|
264
|
+
if (state.stopRequested) {
|
|
265
|
+
state.snapshot.status = "stopped";
|
|
266
|
+
state.snapshot.pid = null;
|
|
267
|
+
this.notifySnapshotChange(state.snapshot);
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
const args = [entryScript, "--agent", state.config.agentArg ?? agent, ...(state.config.args ?? [])];
|
|
271
|
+
const child = this.spawnFn("node", args, {
|
|
272
|
+
cwd: runCwd,
|
|
273
|
+
env: state.config.env ? { ...process.env, ...state.config.env } : process.env,
|
|
274
|
+
stdio: ["ignore", "ignore", "ignore", "ipc"],
|
|
275
|
+
});
|
|
276
|
+
/* v8 ignore next 7 -- defensive: spawn should always return a ChildProcess @preserve */
|
|
277
|
+
if (!child) {
|
|
278
|
+
state.snapshot.status = "crashed";
|
|
279
|
+
(0, runtime_1.emitNervesEvent)({ level: "error", component: "daemon", event: "daemon.agent_spawn_failed", message: "spawn returned null", meta: { agent } });
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
if (!this.isStartAttemptCurrent(state, attemptId)) {
|
|
283
|
+
try {
|
|
284
|
+
child.kill("SIGTERM");
|
|
285
|
+
}
|
|
286
|
+
catch {
|
|
287
|
+
(0, runtime_1.emitNervesEvent)({
|
|
288
|
+
level: "warn",
|
|
289
|
+
component: "daemon",
|
|
290
|
+
event: "daemon.agent_stop_error",
|
|
291
|
+
message: "failed to stop stale managed agent startup",
|
|
292
|
+
meta: { agent },
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
state.process = child;
|
|
298
|
+
state.snapshot.status = "running";
|
|
299
|
+
state.snapshot.pid = child.pid ?? null;
|
|
300
|
+
state.snapshot.startedAt = new Date(this.currentTimeMs()).toISOString();
|
|
301
|
+
const bootstrap = state.config.getRuntimeCredentialBootstrap?.() ?? null;
|
|
302
|
+
if (bootstrap) {
|
|
303
|
+
const message = {
|
|
304
|
+
type: "ouro.runtimeCredentialBootstrap",
|
|
305
|
+
agentName: bootstrap.agentName,
|
|
306
|
+
};
|
|
307
|
+
if (bootstrap.runtimeConfig)
|
|
308
|
+
message.runtimeConfig = bootstrap.runtimeConfig;
|
|
309
|
+
if (bootstrap.machineRuntimeConfig)
|
|
310
|
+
message.machineRuntimeConfig = bootstrap.machineRuntimeConfig;
|
|
311
|
+
if (bootstrap.machineId)
|
|
312
|
+
message.machineId = bootstrap.machineId;
|
|
313
|
+
if (bootstrap.providerCredentialRecords)
|
|
314
|
+
message.providerCredentialRecords = bootstrap.providerCredentialRecords;
|
|
315
|
+
try {
|
|
316
|
+
child.send?.(message);
|
|
317
|
+
(0, runtime_1.emitNervesEvent)({
|
|
318
|
+
component: "daemon",
|
|
319
|
+
event: "daemon.agent_runtime_credentials_bootstrap_sent",
|
|
320
|
+
message: "sent runtime credential bootstrap to managed agent process",
|
|
321
|
+
meta: {
|
|
322
|
+
agent,
|
|
323
|
+
runtimeConfig: !!bootstrap.runtimeConfig,
|
|
324
|
+
machineRuntimeConfig: !!bootstrap.machineRuntimeConfig,
|
|
325
|
+
providerCredentialRecords: bootstrap.providerCredentialRecords?.length ?? 0,
|
|
326
|
+
},
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
catch (error) {
|
|
330
|
+
(0, runtime_1.emitNervesEvent)({
|
|
331
|
+
level: "warn",
|
|
332
|
+
component: "daemon",
|
|
333
|
+
event: "daemon.agent_ipc_send_error",
|
|
334
|
+
message: "failed to send runtime credential bootstrap to managed agent process",
|
|
335
|
+
meta: { agent, error: error instanceof Error ? error.message : String(error) },
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
}
|
|
206
339
|
(0, runtime_1.emitNervesEvent)({
|
|
207
|
-
level: "error",
|
|
208
340
|
component: "daemon",
|
|
209
|
-
event: "daemon.
|
|
210
|
-
message: "
|
|
211
|
-
meta: { agent,
|
|
341
|
+
event: "daemon.agent_started",
|
|
342
|
+
message: "daemon started managed agent process",
|
|
343
|
+
meta: { agent, pid: child.pid ?? null, cwd: runCwd },
|
|
212
344
|
});
|
|
213
345
|
this.notifySnapshotChange(state.snapshot);
|
|
214
|
-
|
|
346
|
+
/* v8 ignore start — child process error handler; requires real spawn to trigger */
|
|
347
|
+
child.on("error", (err) => {
|
|
348
|
+
(0, runtime_1.emitNervesEvent)({
|
|
349
|
+
level: "warn",
|
|
350
|
+
component: "daemon",
|
|
351
|
+
event: "daemon.agent_process_error",
|
|
352
|
+
message: "managed agent process emitted error",
|
|
353
|
+
meta: { agent, error: err.message },
|
|
354
|
+
});
|
|
355
|
+
});
|
|
356
|
+
/* v8 ignore stop */
|
|
357
|
+
child.once("exit", (code, signal) => {
|
|
358
|
+
this.onExit(state, code, signal);
|
|
359
|
+
});
|
|
215
360
|
}
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
});
|
|
222
|
-
/* v8 ignore next 7 -- defensive: spawn should always return a ChildProcess @preserve */
|
|
223
|
-
if (!child) {
|
|
224
|
-
state.snapshot.status = "crashed";
|
|
225
|
-
(0, runtime_1.emitNervesEvent)({ level: "error", component: "daemon", event: "daemon.agent_spawn_failed", message: "spawn returned null", meta: { agent } });
|
|
226
|
-
return;
|
|
361
|
+
finally {
|
|
362
|
+
if (this.isStartAttemptCurrent(state, attemptId)) {
|
|
363
|
+
state.startInFlight = false;
|
|
364
|
+
state.startAttemptedAtMs = null;
|
|
365
|
+
}
|
|
227
366
|
}
|
|
228
|
-
state.process = child;
|
|
229
|
-
state.snapshot.status = "running";
|
|
230
|
-
state.snapshot.pid = child.pid ?? null;
|
|
231
|
-
state.snapshot.startedAt = new Date(this.now()).toISOString();
|
|
232
|
-
(0, runtime_1.emitNervesEvent)({
|
|
233
|
-
component: "daemon",
|
|
234
|
-
event: "daemon.agent_started",
|
|
235
|
-
message: "daemon started managed agent process",
|
|
236
|
-
meta: { agent, pid: child.pid ?? null, cwd: runCwd },
|
|
237
|
-
});
|
|
238
|
-
this.notifySnapshotChange(state.snapshot);
|
|
239
|
-
/* v8 ignore start — child process error handler; requires real spawn to trigger */
|
|
240
|
-
child.on("error", (err) => {
|
|
241
|
-
(0, runtime_1.emitNervesEvent)({
|
|
242
|
-
level: "warn",
|
|
243
|
-
component: "daemon",
|
|
244
|
-
event: "daemon.agent_process_error",
|
|
245
|
-
message: "managed agent process emitted error",
|
|
246
|
-
meta: { agent, error: err.message },
|
|
247
|
-
});
|
|
248
|
-
});
|
|
249
|
-
/* v8 ignore stop */
|
|
250
|
-
child.once("exit", (code, signal) => {
|
|
251
|
-
this.onExit(state, code, signal);
|
|
252
|
-
});
|
|
253
367
|
}
|
|
254
368
|
async stopAgent(agent) {
|
|
255
369
|
const state = this.requireAgent(agent);
|
|
@@ -281,6 +395,31 @@ class DaemonProcessManager {
|
|
|
281
395
|
this.notifySnapshotChange(state.snapshot);
|
|
282
396
|
}
|
|
283
397
|
async restartAgent(agent) {
|
|
398
|
+
const state = this.requireAgent(agent);
|
|
399
|
+
if (state.startInFlight && !state.process) {
|
|
400
|
+
const startedAt = state.startAttemptedAtMs;
|
|
401
|
+
/* v8 ignore next -- defensive: startInFlight always records a start timestamp @preserve */
|
|
402
|
+
const elapsedMs = startedAt === null ? 0 : this.currentTimeMs() - startedAt;
|
|
403
|
+
if (elapsedMs < this.startupStaleAfterMs) {
|
|
404
|
+
(0, runtime_1.emitNervesEvent)({
|
|
405
|
+
component: "daemon",
|
|
406
|
+
event: "daemon.agent_restart_deferred",
|
|
407
|
+
message: "managed agent restart skipped while startup is already in flight",
|
|
408
|
+
meta: { agent, elapsedMs, startupStaleAfterMs: this.startupStaleAfterMs },
|
|
409
|
+
});
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
(0, runtime_1.emitNervesEvent)({
|
|
413
|
+
level: "warn",
|
|
414
|
+
component: "daemon",
|
|
415
|
+
event: "daemon.agent_startup_stale_recovered",
|
|
416
|
+
message: "managed agent startup was stale; clearing the pending attempt before restart",
|
|
417
|
+
meta: { agent, elapsedMs, startupStaleAfterMs: this.startupStaleAfterMs },
|
|
418
|
+
});
|
|
419
|
+
state.startAttemptId += 1;
|
|
420
|
+
state.startInFlight = false;
|
|
421
|
+
state.startAttemptedAtMs = null;
|
|
422
|
+
}
|
|
284
423
|
await this.stopAgent(agent);
|
|
285
424
|
await this.startAgent(agent);
|
|
286
425
|
}
|
|
@@ -316,11 +455,13 @@ class DaemonProcessManager {
|
|
|
316
455
|
if (!state.process)
|
|
317
456
|
return;
|
|
318
457
|
state.process = null;
|
|
458
|
+
state.startInFlight = false;
|
|
459
|
+
state.startAttemptedAtMs = null;
|
|
319
460
|
state.snapshot.pid = null;
|
|
320
461
|
state.snapshot.lastExitCode = code;
|
|
321
462
|
state.snapshot.lastSignal = signal;
|
|
322
463
|
const crashed = !state.stopRequested && code !== 0;
|
|
323
|
-
const now = this.
|
|
464
|
+
const now = this.currentTimeMs();
|
|
324
465
|
const startedAt = state.snapshot.startedAt ? Date.parse(state.snapshot.startedAt) : now;
|
|
325
466
|
const runDuration = Math.max(0, now - startedAt);
|
|
326
467
|
(0, runtime_1.emitNervesEvent)({
|
|
@@ -40,6 +40,7 @@ const path = __importStar(require("path"));
|
|
|
40
40
|
const runtime_1 = require("../../nerves/runtime");
|
|
41
41
|
const identity_1 = require("../identity");
|
|
42
42
|
const runtime_credentials_1 = require("../runtime-credentials");
|
|
43
|
+
const provider_credentials_1 = require("../provider-credentials");
|
|
43
44
|
const sense_truth_1 = require("../sense-truth");
|
|
44
45
|
const machine_identity_1 = require("../machine-identity");
|
|
45
46
|
const process_manager_1 = require("./process-manager");
|
|
@@ -223,6 +224,25 @@ function managedSenseEntry(sense) {
|
|
|
223
224
|
return "senses/bluebubbles/entry.js";
|
|
224
225
|
return "senses/mail-entry.js";
|
|
225
226
|
}
|
|
227
|
+
function runtimeCredentialBootstrapFor(agent, sense) {
|
|
228
|
+
const runtime = (0, runtime_credentials_1.readRuntimeCredentialConfig)(agent);
|
|
229
|
+
const machineId = sense === "bluebubbles" ? currentMachineId() : undefined;
|
|
230
|
+
const machine = sense === "bluebubbles" ? (0, runtime_credentials_1.readMachineRuntimeCredentialConfig)(agent) : null;
|
|
231
|
+
const providerPool = (0, provider_credentials_1.readProviderCredentialPool)(agent);
|
|
232
|
+
const providerCredentialRecords = providerPool.ok
|
|
233
|
+
? Object.values(providerPool.pool.providers).filter((record) => !!record)
|
|
234
|
+
: [];
|
|
235
|
+
const bootstrap = {
|
|
236
|
+
agentName: agent,
|
|
237
|
+
runtimeConfig: runtime.ok ? runtime.config : undefined,
|
|
238
|
+
machineRuntimeConfig: machine?.ok ? machine.config : undefined,
|
|
239
|
+
machineId,
|
|
240
|
+
providerCredentialRecords: providerCredentialRecords.length > 0 ? providerCredentialRecords : undefined,
|
|
241
|
+
};
|
|
242
|
+
if (!bootstrap.runtimeConfig && !bootstrap.machineRuntimeConfig && !bootstrap.providerCredentialRecords)
|
|
243
|
+
return null;
|
|
244
|
+
return bootstrap;
|
|
245
|
+
}
|
|
226
246
|
function blueBubblesRuntimeStateIsFresh(lastCheckedAt, now = Date.now()) {
|
|
227
247
|
if (!lastCheckedAt) {
|
|
228
248
|
return false;
|
|
@@ -279,6 +299,7 @@ function readBlueBubblesRuntimeFacts(agent, bundlesRoot, snapshot) {
|
|
|
279
299
|
class DaemonSenseManager {
|
|
280
300
|
processManager;
|
|
281
301
|
contexts;
|
|
302
|
+
pendingConfigRefreshes = new Set();
|
|
282
303
|
bundlesRoot;
|
|
283
304
|
constructor(options) {
|
|
284
305
|
const bundlesRoot = options.bundlesRoot ?? path.join(os.homedir(), "AgentBundles");
|
|
@@ -297,6 +318,7 @@ class DaemonSenseManager {
|
|
|
297
318
|
entry: managedSenseEntry(sense),
|
|
298
319
|
channel: sense,
|
|
299
320
|
autoStart: true,
|
|
321
|
+
getRuntimeCredentialBootstrap: () => runtimeCredentialBootstrapFor(agent, sense),
|
|
300
322
|
}));
|
|
301
323
|
});
|
|
302
324
|
this.processManager = options.processManager ?? new process_manager_1.DaemonProcessManager({
|
|
@@ -308,14 +330,11 @@ class DaemonSenseManager {
|
|
|
308
330
|
const context = this.contexts.get(parsed.agent);
|
|
309
331
|
if (!context)
|
|
310
332
|
return { ok: true };
|
|
311
|
-
|
|
312
|
-
const machineRefreshed = parsed.sense === "bluebubbles"
|
|
313
|
-
? await (0, runtime_credentials_1.refreshMachineRuntimeCredentialConfig)(parsed.agent, currentMachineId(), { preserveCachedOnFailure: true })
|
|
314
|
-
: (0, runtime_credentials_1.readMachineRuntimeCredentialConfig)(parsed.agent);
|
|
315
|
-
context.facts = senseFactsFromRuntimeConfig(parsed.agent, context.senses, refreshed, machineRefreshed);
|
|
333
|
+
context.facts = senseFactsFromRuntimeConfig(parsed.agent, context.senses, (0, runtime_credentials_1.readRuntimeCredentialConfig)(parsed.agent), (0, runtime_credentials_1.readMachineRuntimeCredentialConfig)(parsed.agent));
|
|
316
334
|
const fact = context.facts[parsed.sense];
|
|
317
335
|
if (fact.configured)
|
|
318
336
|
return { ok: true };
|
|
337
|
+
this.scheduleSenseConfigRefresh(name, parsed);
|
|
319
338
|
if (fact.optional) {
|
|
320
339
|
return {
|
|
321
340
|
ok: false,
|
|
@@ -325,6 +344,7 @@ class DaemonSenseManager {
|
|
|
325
344
|
}
|
|
326
345
|
return {
|
|
327
346
|
ok: false,
|
|
347
|
+
skip: true,
|
|
328
348
|
error: `${parsed.sense} is enabled for ${parsed.agent} but runtime credentials are not ready: ${fact.detail}`,
|
|
329
349
|
fix: senseRepairHint(parsed.agent, parsed.sense),
|
|
330
350
|
};
|
|
@@ -340,9 +360,70 @@ class DaemonSenseManager {
|
|
|
340
360
|
},
|
|
341
361
|
});
|
|
342
362
|
}
|
|
363
|
+
scheduleSenseConfigRefresh(name, parsed) {
|
|
364
|
+
if (this.pendingConfigRefreshes.has(name))
|
|
365
|
+
return;
|
|
366
|
+
this.pendingConfigRefreshes.add(name);
|
|
367
|
+
void this.refreshSenseConfigAndRetry(name, parsed);
|
|
368
|
+
}
|
|
369
|
+
async refreshSenseConfigAndRetry(name, parsed) {
|
|
370
|
+
try {
|
|
371
|
+
const refreshed = await (0, runtime_credentials_1.refreshRuntimeCredentialConfig)(parsed.agent, { preserveCachedOnFailure: true });
|
|
372
|
+
const machineRefreshed = parsed.sense === "bluebubbles"
|
|
373
|
+
? await (0, runtime_credentials_1.refreshMachineRuntimeCredentialConfig)(parsed.agent, currentMachineId(), { preserveCachedOnFailure: true })
|
|
374
|
+
: (0, runtime_credentials_1.readMachineRuntimeCredentialConfig)(parsed.agent);
|
|
375
|
+
const context = this.contexts.get(parsed.agent);
|
|
376
|
+
/* v8 ignore next -- defensive: config refreshes are only scheduled for known agent contexts @preserve */
|
|
377
|
+
if (!context)
|
|
378
|
+
return;
|
|
379
|
+
context.facts = senseFactsFromRuntimeConfig(parsed.agent, context.senses, refreshed, machineRefreshed);
|
|
380
|
+
if (!context.facts[parsed.sense].configured)
|
|
381
|
+
return;
|
|
382
|
+
setTimeout(() => {
|
|
383
|
+
void this.processManager.startAgent?.(name).catch((error) => {
|
|
384
|
+
(0, runtime_1.emitNervesEvent)({
|
|
385
|
+
level: "error",
|
|
386
|
+
component: "channels",
|
|
387
|
+
event: "channel.daemon_sense_autostart_error",
|
|
388
|
+
message: "sense autostart failed",
|
|
389
|
+
/* v8 ignore next -- defensive: process manager rejects with Error instances in normal use @preserve */
|
|
390
|
+
meta: { error: error instanceof Error ? error.message : String(error) },
|
|
391
|
+
});
|
|
392
|
+
});
|
|
393
|
+
}, 0);
|
|
394
|
+
}
|
|
395
|
+
catch (error) {
|
|
396
|
+
(0, runtime_1.emitNervesEvent)({
|
|
397
|
+
level: "error",
|
|
398
|
+
component: "channels",
|
|
399
|
+
event: "channel.daemon_sense_autostart_error",
|
|
400
|
+
message: "sense config refresh failed",
|
|
401
|
+
/* v8 ignore next -- defensive: runtime credential refresh rejects with Error instances in normal use @preserve */
|
|
402
|
+
meta: { error: error instanceof Error ? error.message : String(error) },
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
finally {
|
|
406
|
+
this.pendingConfigRefreshes.delete(name);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
343
409
|
async startAutoStartSenses() {
|
|
344
410
|
await this.processManager.startAutoStartAgents();
|
|
345
411
|
}
|
|
412
|
+
triggerAutoStartSenses() {
|
|
413
|
+
if (this.processManager.triggerAutoStartAgents) {
|
|
414
|
+
this.processManager.triggerAutoStartAgents();
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
void this.processManager.startAutoStartAgents().catch((error) => {
|
|
418
|
+
(0, runtime_1.emitNervesEvent)({
|
|
419
|
+
level: "error",
|
|
420
|
+
component: "channels",
|
|
421
|
+
event: "channel.daemon_sense_autostart_error",
|
|
422
|
+
message: "sense autostart failed",
|
|
423
|
+
meta: { error: error instanceof Error ? error.message : String(error) },
|
|
424
|
+
});
|
|
425
|
+
});
|
|
426
|
+
}
|
|
346
427
|
async stopAll() {
|
|
347
428
|
await this.processManager.stopAll();
|
|
348
429
|
}
|
|
@@ -22,6 +22,7 @@ const terminal_ui_1 = require("./terminal-ui");
|
|
|
22
22
|
const SPINNER_FRAMES = "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏";
|
|
23
23
|
const STABILITY_THRESHOLD_MS = 5_000;
|
|
24
24
|
const POLL_INTERVAL_MS = 500;
|
|
25
|
+
const STARTUP_POLL_TIMEOUT_MS = 180_000;
|
|
25
26
|
// ── ANSI helpers ──
|
|
26
27
|
const RESET = "\x1b[0m";
|
|
27
28
|
const BOLD = "\x1b[1m";
|
|
@@ -133,6 +134,7 @@ function renderFinalSummary(result, isTTY) {
|
|
|
133
134
|
*/
|
|
134
135
|
async function pollDaemonStartup(deps) {
|
|
135
136
|
const startTime = deps.now();
|
|
137
|
+
const maxWaitMs = deps.maxWaitMs ?? STARTUP_POLL_TIMEOUT_MS;
|
|
136
138
|
let prevLineCount = 0;
|
|
137
139
|
const isTTY = deps.isTTY ?? true;
|
|
138
140
|
const isAlive = deps.isProcessAlive ?? defaultIsProcessAlive;
|
|
@@ -182,8 +184,29 @@ async function pollDaemonStartup(deps) {
|
|
|
182
184
|
degraded: [{ agent: "daemon", errorReason: errorMsg, fixHint: "check daemon logs or run `ouro doctor`" }],
|
|
183
185
|
};
|
|
184
186
|
}
|
|
185
|
-
// Show what the daemon is doing from its log
|
|
186
187
|
const latestEvent = deps.readLatestDaemonEvent?.() ?? null;
|
|
188
|
+
if (elapsed >= maxWaitMs) {
|
|
189
|
+
const errorMsg = latestEvent
|
|
190
|
+
? `daemon did not answer within ${(maxWaitMs / 1000).toFixed(0)}s; latest event: ${latestEvent}`
|
|
191
|
+
: `daemon did not answer within ${(maxWaitMs / 1000).toFixed(0)}s`;
|
|
192
|
+
const result = {
|
|
193
|
+
stable: [],
|
|
194
|
+
degraded: [{ agent: "daemon", errorReason: errorMsg, fixHint: "check daemon logs or run `ouro doctor`" }],
|
|
195
|
+
};
|
|
196
|
+
if (shouldRender) {
|
|
197
|
+
const summary = renderFinalSummary(result, isTTY);
|
|
198
|
+
deps.writeRaw(summary);
|
|
199
|
+
}
|
|
200
|
+
(0, runtime_1.emitNervesEvent)({
|
|
201
|
+
level: "error",
|
|
202
|
+
component: "daemon",
|
|
203
|
+
event: "daemon.startup_poll_timeout",
|
|
204
|
+
message: "daemon startup polling timed out before the daemon answered",
|
|
205
|
+
meta: { socketPath: deps.socketPath, daemonPid: deps.daemonPid, elapsedMs: elapsed, maxWaitMs, lastEvent: latestEvent },
|
|
206
|
+
});
|
|
207
|
+
return result;
|
|
208
|
+
}
|
|
209
|
+
// Show what the daemon is doing from its log
|
|
187
210
|
reportProgress([
|
|
188
211
|
"waiting for Ouro to answer",
|
|
189
212
|
latestEvent ? `- latest daemon event: ${latestEvent}` : "- background service is still starting",
|
|
@@ -223,10 +246,53 @@ async function pollDaemonStartup(deps) {
|
|
|
223
246
|
});
|
|
224
247
|
return result;
|
|
225
248
|
}
|
|
249
|
+
if (elapsed >= maxWaitMs) {
|
|
250
|
+
const result = buildTimedOutStartupResult(payload, assessment, maxWaitMs);
|
|
251
|
+
if (shouldRender) {
|
|
252
|
+
const summary = renderFinalSummary(result, isTTY);
|
|
253
|
+
deps.writeRaw(summary);
|
|
254
|
+
}
|
|
255
|
+
(0, runtime_1.emitNervesEvent)({
|
|
256
|
+
level: "error",
|
|
257
|
+
component: "daemon",
|
|
258
|
+
event: "daemon.startup_poll_timeout",
|
|
259
|
+
message: "daemon startup polling timed out with unresolved workers",
|
|
260
|
+
meta: {
|
|
261
|
+
socketPath: deps.socketPath,
|
|
262
|
+
daemonPid: deps.daemonPid,
|
|
263
|
+
elapsedMs: elapsed,
|
|
264
|
+
maxWaitMs,
|
|
265
|
+
stableCount: result.stable.length,
|
|
266
|
+
degradedCount: result.degraded.length,
|
|
267
|
+
},
|
|
268
|
+
});
|
|
269
|
+
return result;
|
|
270
|
+
}
|
|
226
271
|
}
|
|
227
272
|
await deps.sleep(POLL_INTERVAL_MS);
|
|
228
273
|
}
|
|
229
274
|
}
|
|
275
|
+
function buildTimedOutStartupResult(payload, assessment, maxWaitMs) {
|
|
276
|
+
const stable = [...assessment.stable];
|
|
277
|
+
const degraded = [...assessment.degraded];
|
|
278
|
+
const stableAgents = new Set(stable);
|
|
279
|
+
const degradedAgents = new Set();
|
|
280
|
+
for (const worker of degraded)
|
|
281
|
+
degradedAgents.add(worker.agent);
|
|
282
|
+
const seconds = (maxWaitMs / 1000).toFixed(0);
|
|
283
|
+
for (const worker of payload.workers) {
|
|
284
|
+
if (stableAgents.has(worker.agent) || degradedAgents.has(worker.agent))
|
|
285
|
+
continue;
|
|
286
|
+
degraded.push({
|
|
287
|
+
agent: worker.agent,
|
|
288
|
+
errorReason: `startup timed out after ${seconds}s while worker was ${worker.status}`,
|
|
289
|
+
fixHint: worker.errorReason
|
|
290
|
+
? worker.fixHint ?? "check daemon logs"
|
|
291
|
+
: "run `ouro status` or `ouro doctor` for current worker details; the daemon is still answering",
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
return { stable, degraded };
|
|
295
|
+
}
|
|
230
296
|
function formatStartupWorkerLine(payload) {
|
|
231
297
|
const base = `- ${payload.agent}/${payload.worker}: ${payload.status}`;
|
|
232
298
|
if (payload.status === "crashed" && payload.errorReason) {
|
|
@@ -255,7 +255,7 @@ function statusFromReaderFailure(reason) {
|
|
|
255
255
|
return reason;
|
|
256
256
|
}
|
|
257
257
|
async function readMailView(agentName) {
|
|
258
|
-
const resolved = (0, reader_1.
|
|
258
|
+
const resolved = await (0, reader_1.resolveMailroomReaderWithRefresh)(agentName);
|
|
259
259
|
if (!resolved.ok) {
|
|
260
260
|
const status = statusFromReaderFailure(resolved.reason);
|
|
261
261
|
emitMailRead(agentName, "list", status);
|
|
@@ -300,7 +300,7 @@ async function readMailView(agentName) {
|
|
|
300
300
|
}
|
|
301
301
|
}
|
|
302
302
|
async function readMailMessageView(agentName, messageId) {
|
|
303
|
-
const resolved = (0, reader_1.
|
|
303
|
+
const resolved = await (0, reader_1.resolveMailroomReaderWithRefresh)(agentName);
|
|
304
304
|
if (!resolved.ok) {
|
|
305
305
|
const status = statusFromReaderFailure(resolved.reason);
|
|
306
306
|
emitMailRead(agentName, "message", status);
|
|
@@ -347,7 +347,7 @@ async function readProviderCredentialRecord(agentName, provider, options = {}) {
|
|
|
347
347
|
const cached = readCachedProviderCredentialRecord(agentName, provider);
|
|
348
348
|
if (cached.ok || options.refreshIfMissing === false)
|
|
349
349
|
return cached;
|
|
350
|
-
return resultForProvider(await refreshProviderCredentialPool(agentName), provider);
|
|
350
|
+
return resultForProvider(await refreshProviderCredentialPool(agentName, { providers: [provider] }), provider);
|
|
351
351
|
}
|
|
352
352
|
async function upsertProviderCredential(input) {
|
|
353
353
|
const updatedAt = (input.now ?? new Date()).toISOString();
|
|
@@ -374,6 +374,7 @@ async function upsertProviderCredential(input) {
|
|
|
374
374
|
});
|
|
375
375
|
input.onProgress?.(`refreshing local provider snapshot from ${input.agentName}'s vault...`);
|
|
376
376
|
const refreshResult = await refreshProviderCredentialPool(input.agentName, {
|
|
377
|
+
providers: [input.provider],
|
|
377
378
|
onProgress: input.onProgress,
|
|
378
379
|
});
|
|
379
380
|
if (!refreshResult.ok) {
|