@ouro.bot/cli 0.1.0-alpha.192 → 0.1.0-alpha.194

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,10 +1,22 @@
1
1
  {
2
2
  "_note": "This changelog is maintained as part of the PR/version-bump workflow. Agent-curated, not auto-generated. Agents read this file directly via read_file to understand what changed between versions.",
3
3
  "versions": [
4
+ {
5
+ "version": "0.1.0-alpha.194",
6
+ "changes": [
7
+ "fix(daemon): self-spawn restart \u2014 no longer relies on launchd KeepAlive for staged restarts",
8
+ "fix(daemon): error boundary with circuit breaker \u2014 uncaught exceptions logged and survived, exits only after 10+ in 60s",
9
+ "fix(daemon): EPIPE suppression in uncaughtException handler",
10
+ "fix(daemon): 5-second force-exit timeouts on all shutdown paths",
11
+ "feat: human-facing and agent-facing provider configs",
12
+ "fix(auth): always refresh codex OAuth token, responses API verification",
13
+ "feat: Outlook visibility \u2014 orientation, obligations, changes, self-fix, memory decisions, route migration to /"
14
+ ]
15
+ },
4
16
  {
5
17
  "version": "0.1.0-alpha.192",
6
18
  "changes": [
7
- "refactor: canonical obligations — ActiveWorkFrame as single source of truth for prompt sections",
19
+ "refactor: canonical obligations \u2014 ActiveWorkFrame as single source of truth for prompt sections",
8
20
  "feat(mcp): dynamic server add/remove \u2014 agents can manage MCP servers without daemon restart"
9
21
  ]
10
22
  },
@@ -1198,4 +1210,4 @@
1198
1210
  ]
1199
1211
  }
1200
1212
  ]
1201
- }
1213
+ }
@@ -235,7 +235,7 @@ function readCodexAccessToken(homeDir) {
235
235
  const raw = fs.readFileSync(authPath, "utf8");
236
236
  const parsed = JSON.parse(raw);
237
237
  const token = parsed?.tokens?.access_token;
238
- return typeof token === "string" ? token.trim() : "";
238
+ return typeof token === "string" ? token.trim() : /* v8 ignore next -- defensive: codex login always writes a string token @preserve */ "";
239
239
  }
240
240
  catch {
241
241
  return "";
@@ -302,25 +302,25 @@ async function collectRuntimeAuthCredentials(input, deps) {
302
302
  return { githubToken: token, baseUrl };
303
303
  }
304
304
  if (input.provider === "openai-codex") {
305
- let token = readCodexAccessToken(homeDir);
305
+ // Always run codex login when auth is explicitly requested — stale tokens
306
+ // are indistinguishable from valid ones without an API call, and the user
307
+ // is asking to re-authenticate.
308
+ (0, runtime_1.emitNervesEvent)({
309
+ component: "daemon",
310
+ event: "daemon.auth_codex_login_start",
311
+ message: "starting codex login for runtime auth",
312
+ meta: { agentName: input.agentName },
313
+ });
314
+ const result = spawnSync("codex", ["login"], { stdio: "inherit" });
315
+ if (result.error) {
316
+ throw new Error(`Failed to run 'codex login': ${result.error.message}`);
317
+ }
318
+ if (result.status !== 0) {
319
+ throw new Error(`'codex login' exited with status ${result.status}.`);
320
+ }
321
+ const token = readCodexAccessToken(homeDir);
306
322
  if (!token) {
307
- (0, runtime_1.emitNervesEvent)({
308
- component: "daemon",
309
- event: "daemon.auth_codex_login_start",
310
- message: "starting codex login for runtime auth",
311
- meta: { agentName: input.agentName },
312
- });
313
- const result = spawnSync("codex", ["login"], { stdio: "inherit" });
314
- if (result.error) {
315
- throw new Error(`Failed to run 'codex login': ${result.error.message}`);
316
- }
317
- if (result.status !== 0) {
318
- throw new Error(`'codex login' exited with status ${result.status}.`);
319
- }
320
- token = readCodexAccessToken(homeDir);
321
- if (!token) {
322
- throw new Error("Codex login completed but no token was found in ~/.codex/auth.json. Re-run `codex login` and try again.");
323
- }
323
+ throw new Error("Codex login completed but no token was found in ~/.codex/auth.json. Re-run `codex login` and try again.");
324
324
  }
325
325
  return { oauthAccessToken: token };
326
326
  }
@@ -170,11 +170,9 @@ void daemon.start().then(() => {
170
170
  scheduler.watchForChanges();
171
171
  habitSchedulers.push(scheduler);
172
172
  }
173
+ /* v8 ignore start -- startup failure + signal handlers: call process.exit, untestable in vitest @preserve */
173
174
  }).catch(async (err) => {
174
- /* v8 ignore stop */
175
- /* v8 ignore start — instanceof branch defensive; catch always receives Error in practice @preserve */
176
175
  const error = err instanceof Error ? err : new Error(String(err));
177
- /* v8 ignore stop */
178
176
  (0, daemon_tombstone_1.writeDaemonTombstone)("startupFailure", error);
179
177
  (0, runtime_1.emitNervesEvent)({
180
178
  level: "error",
@@ -183,6 +181,7 @@ void daemon.start().then(() => {
183
181
  message: "daemon entrypoint failed",
184
182
  meta: { error: error.message },
185
183
  });
184
+ setTimeout(() => process.exit(1), 5_000).unref();
186
185
  await daemon.stop();
187
186
  process.exit(1);
188
187
  });
@@ -191,6 +190,7 @@ process.on("SIGINT", () => {
191
190
  s.stopWatching();
192
191
  s.stop();
193
192
  }
193
+ setTimeout(() => process.exit(1), 5_000).unref();
194
194
  void daemon.stop().then(() => process.exit(0));
195
195
  });
196
196
  process.on("SIGTERM", () => {
@@ -198,29 +198,46 @@ process.on("SIGTERM", () => {
198
198
  s.stopWatching();
199
199
  s.stop();
200
200
  }
201
+ setTimeout(() => process.exit(1), 5_000).unref();
201
202
  void daemon.stop().then(() => process.exit(0));
202
203
  });
204
+ /* v8 ignore stop */
203
205
  // Suppress EPIPE on stdout/stderr — normal when detached daemon's parent exits
204
206
  /* v8 ignore start -- EPIPE suppression: only fires when parent process exits @preserve */
205
207
  process.stdout?.on?.("error", () => { });
206
208
  process.stderr?.on?.("error", () => { });
207
209
  /* v8 ignore stop */
208
210
  /* v8 ignore start -- global exception handlers: genuinely untestable in vitest; exercised by real daemon crashes @preserve */
211
+ let _uncaughtCount = 0;
212
+ const CIRCUIT_BREAKER_WINDOW_MS = 60_000;
213
+ const CIRCUIT_BREAKER_MAX = 10;
209
214
  process.on("uncaughtException", (error) => {
210
215
  // EPIPE is normal for detached daemon processes — parent closed the pipe
211
216
  if (error.code === "EPIPE")
212
217
  return;
218
+ _uncaughtCount++;
219
+ setTimeout(() => { _uncaughtCount--; }, CIRCUIT_BREAKER_WINDOW_MS).unref();
213
220
  (0, daemon_tombstone_1.writeDaemonTombstone)("uncaughtException", error);
214
221
  (0, runtime_1.emitNervesEvent)({
215
222
  level: "error",
216
223
  component: "daemon",
217
224
  event: "daemon.uncaught_exception",
218
- message: "uncaught exception in daemon process",
219
- meta: { error: error.message, stack: error.stack ?? null },
225
+ message: "uncaught exception in daemon process (continuing)",
226
+ meta: { error: error.message, stack: error.stack ?? null, uncaughtCount: _uncaughtCount },
220
227
  });
221
- // Graceful 5-second shutdown window
222
- setTimeout(() => process.exit(1), 5_000).unref();
223
- void daemon.stop().then(() => process.exit(1));
228
+ // Circuit breaker: if too many exceptions in a short window, the process
229
+ // is in a bad state — exit so launchd/self-spawn can restart fresh.
230
+ if (_uncaughtCount >= CIRCUIT_BREAKER_MAX) {
231
+ (0, runtime_1.emitNervesEvent)({
232
+ level: "error",
233
+ component: "daemon",
234
+ event: "daemon.circuit_breaker_exit",
235
+ message: `daemon exiting: ${_uncaughtCount} uncaught exceptions in ${CIRCUIT_BREAKER_WINDOW_MS / 1000}s`,
236
+ meta: { uncaughtCount: _uncaughtCount },
237
+ });
238
+ setTimeout(() => process.exit(1), 5_000).unref();
239
+ void daemon.stop().then(() => process.exit(1));
240
+ }
224
241
  });
225
242
  process.on("unhandledRejection", (reason) => {
226
243
  (0, runtime_1.emitNervesEvent)({
@@ -208,7 +208,7 @@ class OuroDaemon {
208
208
  daemon: "running",
209
209
  health: workers.every((worker) => worker.status === "running") ? "ok" : "warn",
210
210
  socketPath: this.socketPath,
211
- outlookUrl: `${this.outlookServer?.origin ?? "http://127.0.0.1:0"}/outlook`,
211
+ outlookUrl: this.outlookServer?.origin ?? "http://127.0.0.1:0",
212
212
  ...(0, runtime_metadata_1.getRuntimeMetadata)(),
213
213
  workerCount: workers.length,
214
214
  senseCount: senses.length,
@@ -236,6 +236,7 @@ class OuroDaemon {
236
236
  // Start periodic update checker (polls npm registry every 30 minutes)
237
237
  // Skip in dev mode — dev builds should not auto-update from npm
238
238
  const bundlesRoot = this.bundlesRoot;
239
+ const daemonSocketPath = this.socketPath;
239
240
  if (this.mode === "dev") {
240
241
  (0, runtime_1.emitNervesEvent)({
241
242
  component: "daemon",
@@ -271,8 +272,19 @@ class OuroDaemon {
271
272
  }
272
273
  },
273
274
  gracefulShutdown: () => daemon.stop(),
275
+ spawnNewDaemon: (entryPath, sock) => {
276
+ const outFd = fs.openSync(os.devNull, "w");
277
+ const errFd = fs.openSync(os.devNull, "w");
278
+ const child = (0, child_process_1.spawn)(process.execPath, [entryPath, "--socket", sock], {
279
+ detached: true,
280
+ stdio: ["ignore", outFd, errFd],
281
+ });
282
+ child.unref();
283
+ return { pid: child.pid ?? null };
284
+ },
274
285
  nodePath: process.execPath,
275
286
  bundlesRoot,
287
+ socketPath: daemonSocketPath,
276
288
  });
277
289
  },
278
290
  /* v8 ignore stop */
@@ -196,6 +196,12 @@ async function startOutlookHttpServer(options = {}) {
196
196
  readAgentBridges: options.readAgentBridges ?? ((agentName) => (0, outlook_read_1.readBridgeInventory)(agentRoot(agentName))),
197
197
  readAgentMemory: options.readAgentMemory ?? ((agentName) => (0, outlook_read_1.readMemoryView)(agentRoot(agentName))),
198
198
  readAgentFriends: options.readAgentFriends ?? ((agentName) => (0, outlook_read_1.readFriendView)(agentName, bundlesRoot ? { bundlesRoot } : undefined)),
199
+ readAgentContinuity: options.readAgentContinuity ?? ((agentName) => (0, outlook_read_1.readOutlookContinuity)(agentRoot(agentName), agentName)),
200
+ readAgentOrientation: options.readAgentOrientation ?? ((agentName) => (0, outlook_read_1.readOrientationView)(agentRoot(agentName), agentName)),
201
+ readAgentObligations: options.readAgentObligations ?? ((agentName) => (0, outlook_read_1.readObligationDetailView)(agentRoot(agentName))),
202
+ readAgentChanges: options.readAgentChanges ?? ((agentName) => (0, outlook_read_1.readChangesView)(agentRoot(agentName))),
203
+ readAgentSelfFix: options.readAgentSelfFix ?? ((agentName) => (0, outlook_read_1.readSelfFixView)(agentRoot(agentName))),
204
+ readAgentMemoryDecisions: options.readAgentMemoryDecisions ?? ((agentName) => (0, outlook_read_1.readMemoryDecisionView)(agentRoot(agentName))),
199
205
  readAgentHabits: options.readAgentHabits ?? ((agentName) => (0, outlook_read_1.readHabitView)(agentRoot(agentName))),
200
206
  readDaemonHealth: options.readDaemonHealth ?? (() => (0, outlook_read_1.readDaemonHealthDeep)(options.healthPath)),
201
207
  readLogs: options.readLogs ?? (() => (0, outlook_read_1.readLogView)(options.logPath ?? null)),
@@ -208,14 +214,8 @@ async function startOutlookHttpServer(options = {}) {
208
214
  }) : null;
209
215
  /* v8 ignore stop */
210
216
  const server = http.createServer((request, response) => {
211
- const pathname = normalizePath(request.url);
217
+ let pathname = normalizePath(request.url);
212
218
  const origin = `http://${host}:${server.address().port}`;
213
- if (pathname === "/outlook/api/events") {
214
- response.writeHead(200, { "content-type": "text/event-stream", "cache-control": "no-cache", "connection": "keep-alive", "access-control-allow-origin": "*" });
215
- response.write(":ok\n\n");
216
- sse.add(response);
217
- return;
218
- }
219
219
  /* v8 ignore start — SPA static asset serving */
220
220
  // Serve built SPA static assets: /assets/*
221
221
  if (pathname.startsWith("/assets/")) {
@@ -235,23 +235,39 @@ async function startOutlookHttpServer(options = {}) {
235
235
  response.end();
236
236
  return;
237
237
  }
238
- if (pathname === "/outlook/api/machine") {
238
+ // Compatibility alias: /outlook/api/* → /api/*
239
+ /* v8 ignore start -- legacy compat path: tested via integration @preserve */
240
+ if (pathname.startsWith("/outlook/api/")) {
241
+ pathname = pathname.slice("/outlook".length);
242
+ }
243
+ else if (pathname === "/outlook/api") {
244
+ pathname = "/api";
245
+ }
246
+ /* v8 ignore stop */
247
+ // SSE event stream
248
+ if (pathname === "/api/events") {
249
+ response.writeHead(200, { "content-type": "text/event-stream", "cache-control": "no-cache", "connection": "keep-alive", "access-control-allow-origin": "*" });
250
+ response.write(":ok\n\n");
251
+ sse.add(response);
252
+ return;
253
+ }
254
+ if (pathname === "/api/machine") {
239
255
  const machine = readMachineState();
240
256
  const machineView = readMachineView?.({ origin, machine });
241
257
  writeJson(response, 200, machineView ?? machine);
242
258
  return;
243
259
  }
244
- if (pathname === "/outlook/api/machine/health") {
260
+ if (pathname === "/api/machine/health") {
245
261
  const health = hooks.readDaemonHealth();
246
262
  writeJson(response, 200, health ?? { status: "unavailable" });
247
263
  return;
248
264
  }
249
- if (pathname === "/outlook/api/machine/logs") {
265
+ if (pathname === "/api/machine/logs") {
250
266
  writeJson(response, 200, hooks.readLogs());
251
267
  return;
252
268
  }
253
- // Agent-level endpoints: /outlook/api/agents/:agent[/:surface[/:params...]]
254
- const agentMatch = /^\/outlook\/api\/agents\/([^/]+)(?:\/(.+))?$/.exec(pathname);
269
+ // Agent-level endpoints: /api/agents/:agent[/:surface[/:params...]]
270
+ const agentMatch = /^\/api\/agents\/([^/]+)(?:\/(.+))?$/.exec(pathname);
255
271
  if (agentMatch) {
256
272
  const agent = decodeURIComponent(agentMatch[1]);
257
273
  const surface = agentMatch[2] ?? null;
@@ -306,6 +322,30 @@ async function startOutlookHttpServer(options = {}) {
306
322
  writeJson(response, 200, hooks.readAgentFriends(agent));
307
323
  return;
308
324
  }
325
+ if (surface === "continuity") {
326
+ writeJson(response, 200, hooks.readAgentContinuity(agent));
327
+ return;
328
+ }
329
+ if (surface === "orientation") {
330
+ writeJson(response, 200, hooks.readAgentOrientation(agent));
331
+ return;
332
+ }
333
+ if (surface === "obligations") {
334
+ writeJson(response, 200, hooks.readAgentObligations(agent));
335
+ return;
336
+ }
337
+ if (surface === "changes") {
338
+ writeJson(response, 200, hooks.readAgentChanges(agent));
339
+ return;
340
+ }
341
+ if (surface === "self-fix") {
342
+ writeJson(response, 200, hooks.readAgentSelfFix(agent));
343
+ return;
344
+ }
345
+ if (surface === "memory-decisions") {
346
+ writeJson(response, 200, hooks.readAgentMemoryDecisions(agent));
347
+ return;
348
+ }
309
349
  /* v8 ignore start — desk prefs write + reads */
310
350
  if (surface === "dismiss-obligation" && request.method === "POST") {
311
351
  let body = "";
@@ -48,6 +48,11 @@ exports.readHabitView = readHabitView;
48
48
  exports.readNeedsMeView = readNeedsMeView;
49
49
  exports.readDeskPrefs = readDeskPrefs;
50
50
  exports.readOutlookContinuity = readOutlookContinuity;
51
+ exports.readOrientationView = readOrientationView;
52
+ exports.readObligationDetailView = readObligationDetailView;
53
+ exports.readChangesView = readChangesView;
54
+ exports.readSelfFixView = readSelfFixView;
55
+ exports.readMemoryDecisionView = readMemoryDecisionView;
51
56
  const fs = __importStar(require("fs"));
52
57
  const path = __importStar(require("path"));
53
58
  const runtime_1 = require("../../nerves/runtime");
@@ -1329,3 +1334,286 @@ function readOutlookContinuity(agentRoot, agentName) {
1329
1334
  },
1330
1335
  };
1331
1336
  }
1337
+ // ---------------------------------------------------------------------------
1338
+ // Orientation reader — daemon-side assembly of "where am I?"
1339
+ // ---------------------------------------------------------------------------
1340
+ function readOrientationView(agentRoot, agentName) {
1341
+ // Read obligations for primary selection
1342
+ let obligations = [];
1343
+ try {
1344
+ obligations = (0, obligations_1.readObligations)(agentRoot);
1345
+ }
1346
+ catch {
1347
+ obligations = [];
1348
+ }
1349
+ const openObligations = obligations.filter(obligations_1.isOpenObligation);
1350
+ // Select primary obligation (most advanced status, then most recent)
1351
+ const statusPriority = {
1352
+ returning: 0,
1353
+ collaborating: 1,
1354
+ in_progress: 2,
1355
+ delegated: 3,
1356
+ accepted: 4,
1357
+ pending: 5,
1358
+ };
1359
+ const sorted = [...openObligations].sort((a, b) => {
1360
+ const sp = (statusPriority[a.status] ?? 99) - (statusPriority[b.status] ?? 99);
1361
+ if (sp !== 0)
1362
+ return sp;
1363
+ const aMs = new Date(a.updatedAt ?? a.createdAt).getTime();
1364
+ const bMs = new Date(b.updatedAt ?? b.createdAt).getTime();
1365
+ return bMs - aMs;
1366
+ });
1367
+ const primary = sorted[0] ?? null;
1368
+ // Read session activity for current session and others
1369
+ let sessions = [];
1370
+ try {
1371
+ sessions = (0, session_activity_1.listSessionActivity)({
1372
+ sessionsDir: path.join(agentRoot, "state", "sessions"),
1373
+ friendsDir: path.join(agentRoot, "friends"),
1374
+ agentName,
1375
+ });
1376
+ }
1377
+ catch {
1378
+ sessions = [];
1379
+ }
1380
+ const sortedSessions = [...sessions].sort((a, b) => b.lastActivityMs - a.lastActivityMs);
1381
+ const currentSession = sortedSessions.length > 0
1382
+ ? {
1383
+ friendId: sortedSessions[0].friendId,
1384
+ channel: sortedSessions[0].channel,
1385
+ key: sortedSessions[0].key,
1386
+ lastActivityAt: sortedSessions[0].lastActivityAt,
1387
+ }
1388
+ : null;
1389
+ const otherActiveSessions = sortedSessions.slice(1).map((s) => ({
1390
+ friendId: s.friendId,
1391
+ friendName: s.friendName,
1392
+ channel: s.channel,
1393
+ key: s.key,
1394
+ lastActivityAt: s.lastActivityAt,
1395
+ }));
1396
+ // Derive center of gravity summary
1397
+ const parts = [];
1398
+ if (primary)
1399
+ parts.push(primary.content);
1400
+ if (openObligations.length > 1)
1401
+ parts.push(`${openObligations.length} open obligations`);
1402
+ if (sessions.length > 0)
1403
+ parts.push(`${sessions.length} active sessions`);
1404
+ const centerOfGravity = parts.length > 0 ? parts.join(" | ") : "idle";
1405
+ const primaryObligation = primary
1406
+ ? {
1407
+ id: primary.id,
1408
+ content: primary.content,
1409
+ status: primary.status,
1410
+ nextAction: primary.nextAction ?? null,
1411
+ waitingOn: primary.meaning?.waitingOn?.detail ?? null,
1412
+ }
1413
+ : null;
1414
+ (0, runtime_1.emitNervesEvent)({
1415
+ component: "heart",
1416
+ event: "heart.outlook_orientation_read",
1417
+ message: `outlook orientation: ${openObligations.length} obligations, ${sessions.length} sessions`,
1418
+ meta: { obligationCount: openObligations.length, sessionCount: sessions.length, primaryId: primary?.id ?? null },
1419
+ });
1420
+ return {
1421
+ currentSession,
1422
+ centerOfGravity,
1423
+ primaryObligation,
1424
+ resumeHandle: null,
1425
+ otherActiveSessions,
1426
+ rawState: null,
1427
+ };
1428
+ }
1429
+ // ---------------------------------------------------------------------------
1430
+ // Obligation detail reader — richer view with primary selection context
1431
+ // ---------------------------------------------------------------------------
1432
+ function readObligationDetailView(agentRoot) {
1433
+ let obligations = [];
1434
+ try {
1435
+ obligations = (0, obligations_1.readObligations)(agentRoot);
1436
+ }
1437
+ catch {
1438
+ obligations = [];
1439
+ }
1440
+ const openObligations = obligations.filter(obligations_1.isOpenObligation);
1441
+ // Select primary (same logic as orientation)
1442
+ const statusPriority = {
1443
+ returning: 0,
1444
+ collaborating: 1,
1445
+ in_progress: 2,
1446
+ delegated: 3,
1447
+ accepted: 4,
1448
+ pending: 5,
1449
+ };
1450
+ const sorted = [...openObligations].sort((a, b) => {
1451
+ const sp = (statusPriority[a.status] ?? 99) - (statusPriority[b.status] ?? 99);
1452
+ if (sp !== 0)
1453
+ return sp;
1454
+ const aMs = new Date(a.updatedAt ?? a.createdAt).getTime();
1455
+ const bMs = new Date(b.updatedAt ?? b.createdAt).getTime();
1456
+ return bMs - aMs;
1457
+ });
1458
+ const primary = sorted[0] ?? null;
1459
+ const items = openObligations.map((ob) => ({
1460
+ id: ob.id,
1461
+ status: ob.status,
1462
+ content: ob.content,
1463
+ updatedAt: ob.updatedAt ?? ob.createdAt,
1464
+ nextAction: ob.nextAction ?? null,
1465
+ origin: ob.origin ?? null,
1466
+ currentSurface: ob.currentSurface ? { kind: ob.currentSurface.kind, label: ob.currentSurface.label } : null,
1467
+ meaning: ob.meaning ? { waitingOn: ob.meaning.waitingOn?.detail ?? null } : null,
1468
+ isPrimary: primary ? ob.id === primary.id : false,
1469
+ }));
1470
+ let primarySelectionReason = null;
1471
+ if (primary) {
1472
+ if (primary.status !== "pending") {
1473
+ primarySelectionReason = `most advanced status: ${primary.status}`;
1474
+ }
1475
+ else {
1476
+ primarySelectionReason = "most recent pending";
1477
+ }
1478
+ }
1479
+ (0, runtime_1.emitNervesEvent)({
1480
+ component: "heart",
1481
+ event: "heart.outlook_obligations_read",
1482
+ message: `outlook obligations: ${openObligations.length} open`,
1483
+ meta: { openCount: openObligations.length, primaryId: primary?.id ?? null },
1484
+ });
1485
+ return {
1486
+ openCount: openObligations.length,
1487
+ primaryId: primary?.id ?? null,
1488
+ primarySelectionReason,
1489
+ items,
1490
+ };
1491
+ }
1492
+ // ---------------------------------------------------------------------------
1493
+ // Changes reader — cross-session drift via snapshot comparison
1494
+ // ---------------------------------------------------------------------------
1495
+ const active_work_1 = require("../active-work");
1496
+ function readChangesView(agentRoot) {
1497
+ const snapshotPath = path.join(agentRoot, "state", "outlook", "active-work-snapshot.json");
1498
+ // Read prior snapshot
1499
+ let previous = null;
1500
+ try {
1501
+ const raw = fs.readFileSync(snapshotPath, "utf-8");
1502
+ previous = JSON.parse(raw);
1503
+ if (!previous.obligationSnapshots || !previous.codingSnapshots)
1504
+ previous = null;
1505
+ }
1506
+ catch {
1507
+ previous = null;
1508
+ }
1509
+ // Build current snapshot from raw state
1510
+ let obligations = [];
1511
+ try {
1512
+ obligations = (0, obligations_1.readObligations)(agentRoot);
1513
+ }
1514
+ catch {
1515
+ obligations = [];
1516
+ }
1517
+ const openObligations = obligations.filter(obligations_1.isOpenObligation);
1518
+ const current = {
1519
+ obligationSnapshots: openObligations.map((ob) => ({
1520
+ id: ob.id,
1521
+ status: ob.status,
1522
+ artifact: ob.currentArtifact?.trim() || null,
1523
+ nextAction: ob.nextAction?.trim() || null,
1524
+ })),
1525
+ codingSnapshots: [],
1526
+ timestamp: new Date().toISOString(),
1527
+ };
1528
+ // Persist current as the new snapshot
1529
+ try {
1530
+ fs.mkdirSync(path.dirname(snapshotPath), { recursive: true });
1531
+ fs.writeFileSync(snapshotPath, JSON.stringify(current, null, 2) + "\n", "utf-8");
1532
+ }
1533
+ catch {
1534
+ // Best effort
1535
+ }
1536
+ if (!previous) {
1537
+ return { changeCount: 0, items: [], snapshotAge: null, formatted: "" };
1538
+ }
1539
+ const changes = (0, active_work_1.detectActiveWorkChanges)(previous, current);
1540
+ const formatted = (0, active_work_1.formatActiveWorkChanges)(changes);
1541
+ (0, runtime_1.emitNervesEvent)({
1542
+ component: "heart",
1543
+ event: "heart.outlook_changes_read",
1544
+ message: `outlook changes: ${changes.length} detected`,
1545
+ meta: { changeCount: changes.length, snapshotAge: previous.timestamp },
1546
+ });
1547
+ return {
1548
+ changeCount: changes.length,
1549
+ items: changes.map((c) => ({ kind: c.kind, id: c.id, from: c.from, to: c.to, summary: c.summary })),
1550
+ snapshotAge: previous.timestamp,
1551
+ formatted,
1552
+ };
1553
+ }
1554
+ // ---------------------------------------------------------------------------
1555
+ // Self-fix workflow reader — derived from task board and coding state
1556
+ // ---------------------------------------------------------------------------
1557
+ function readSelfFixView(agentRoot) {
1558
+ // Derive self-fix state from task scanner
1559
+ let tasks = [];
1560
+ try {
1561
+ const scanned = (0, scanner_1.scanTasks)(path.join(agentRoot, "tasks"));
1562
+ tasks = scanned.tasks.map((t) => ({ name: t.name, title: t.title, status: t.status }));
1563
+ }
1564
+ catch {
1565
+ tasks = [];
1566
+ }
1567
+ // Look for self-fix-related tasks
1568
+ const selfFixTasks = tasks.filter((t) => t.title.toLowerCase().includes("fix") || t.title.toLowerCase().includes("self-fix"));
1569
+ if (selfFixTasks.length === 0) {
1570
+ return { active: false, currentStep: null, steps: [] };
1571
+ }
1572
+ const steps = selfFixTasks.map((t) => ({
1573
+ label: t.title,
1574
+ status: t.status === "done" ? "done" : t.status === "processing" ? "active" : "pending",
1575
+ detail: `task ${t.name}: ${t.status}`,
1576
+ }));
1577
+ const activeStep = steps.find((s) => s.status === "active");
1578
+ (0, runtime_1.emitNervesEvent)({
1579
+ component: "heart",
1580
+ event: "heart.outlook_selffix_read",
1581
+ message: `outlook self-fix: ${selfFixTasks.length} tasks`,
1582
+ meta: { taskCount: selfFixTasks.length, active: !!activeStep },
1583
+ });
1584
+ return {
1585
+ active: !!activeStep,
1586
+ currentStep: activeStep?.label ?? null,
1587
+ steps,
1588
+ };
1589
+ }
1590
+ // ---------------------------------------------------------------------------
1591
+ // Memory decisions reader — append-only JSONL log
1592
+ // ---------------------------------------------------------------------------
1593
+ function readMemoryDecisionView(agentRoot, limit = 50) {
1594
+ const logPath = path.join(agentRoot, "state", "outlook", "memory-decisions.jsonl");
1595
+ let lines = [];
1596
+ try {
1597
+ const raw = fs.readFileSync(logPath, "utf-8");
1598
+ lines = raw.split("\n").filter((l) => l.trim().length > 0);
1599
+ }
1600
+ catch {
1601
+ return { totalCount: 0, items: [] };
1602
+ }
1603
+ const items = [];
1604
+ for (const line of lines) {
1605
+ try {
1606
+ const parsed = JSON.parse(line);
1607
+ if (parsed.kind && parsed.decision && parsed.timestamp) {
1608
+ items.push(parsed);
1609
+ }
1610
+ }
1611
+ catch {
1612
+ // Skip malformed lines
1613
+ }
1614
+ }
1615
+ // Reverse chronological
1616
+ items.reverse();
1617
+ const limited = items.slice(0, limit);
1618
+ return { totalCount: items.length, items: limited };
1619
+ }
@@ -90,25 +90,49 @@ async function performStagedRestart(version, deps) {
90
90
  });
91
91
  return { ok: false, error: `hook runner exited with code ${spawnResult.status}` };
92
92
  }
93
- // Step 4: Graceful shutdown (launchd will restart with new code)
93
+ // Step 4: Graceful shutdown then self-spawn new daemon
94
+ // We can't rely on launchd KeepAlive — the plist may not be loaded.
95
+ // Instead: shut down (releases socket), spawn new daemon from updated code.
94
96
  (0, runtime_1.emitNervesEvent)({
95
97
  component: "daemon",
96
98
  event: "daemon.staged_restart_hooks_passed",
97
- message: "hooks passed, shutting down for restart",
99
+ message: "hooks passed, shutting down and spawning new daemon",
98
100
  meta: { version },
99
101
  });
102
+ let shutdownError;
100
103
  try {
101
104
  await deps.gracefulShutdown();
102
105
  }
103
106
  catch (err) {
104
- const shutdownError = err instanceof Error ? err.message : /* v8 ignore next -- defensive: non-Error catch branch @preserve */ String(err);
107
+ shutdownError = err instanceof Error ? err.message : /* v8 ignore next -- defensive: non-Error catch branch @preserve */ String(err);
105
108
  (0, runtime_1.emitNervesEvent)({
106
109
  component: "daemon",
107
110
  event: "daemon.staged_restart_shutdown_error",
108
- message: "graceful shutdown encountered error",
111
+ message: "graceful shutdown encountered error (continuing with spawn)",
109
112
  meta: { version, error: shutdownError },
110
113
  });
111
- return { ok: true, shutdownError };
112
114
  }
113
- return { ok: true };
115
+ // Spawn new daemon from updated code path
116
+ const newEntry = path.join(newCodePath, "dist", "heart", "daemon", "daemon-entry.js");
117
+ const socketArg = deps.socketPath ?? "/tmp/ouroboros-daemon.sock";
118
+ try {
119
+ const { pid } = deps.spawnNewDaemon(newEntry, socketArg);
120
+ (0, runtime_1.emitNervesEvent)({
121
+ component: "daemon",
122
+ event: "daemon.staged_restart_spawned",
123
+ message: "new daemon spawned successfully",
124
+ meta: { version, pid, entry: newEntry },
125
+ });
126
+ }
127
+ catch (err) {
128
+ const spawnError = err instanceof Error ? err.message : /* v8 ignore next -- defensive: non-Error catch branch @preserve */ String(err);
129
+ (0, runtime_1.emitNervesEvent)({
130
+ component: "daemon",
131
+ event: "daemon.staged_restart_respawn_failed",
132
+ message: "failed to spawn new daemon after shutdown",
133
+ meta: { version, error: spawnError, entry: newEntry },
134
+ });
135
+ return { ok: false, error: `shutdown succeeded but failed to spawn new daemon: ${spawnError}`, shutdownError };
136
+ }
137
+ return { ok: true, shutdownError };
114
138
  }