@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 +14 -2
- package/dist/heart/daemon/auth-flow.js +19 -19
- package/dist/heart/daemon/daemon-entry.js +25 -8
- package/dist/heart/daemon/daemon.js +13 -1
- package/dist/heart/daemon/outlook-http.js +52 -12
- package/dist/heart/daemon/outlook-read.js +288 -0
- package/dist/heart/daemon/staged-restart.js +30 -6
- package/dist/heart/provider-ping.js +7 -1
- package/dist/outlook-ui/assets/index-Ck8agNeO.js +61 -0
- package/dist/outlook-ui/assets/index-LwChZTgL.css +1 -0
- package/dist/outlook-ui/index.html +2 -2
- package/package.json +1 -1
- package/dist/outlook-ui/assets/index-DN6vVYPm.css +0 -1
- package/dist/outlook-ui/assets/index-M1l1tDTX.js +0 -61
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
|
|
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
|
-
|
|
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
|
-
(
|
|
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
|
-
//
|
|
222
|
-
|
|
223
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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 === "/
|
|
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 === "/
|
|
265
|
+
if (pathname === "/api/machine/logs") {
|
|
250
266
|
writeJson(response, 200, hooks.readLogs());
|
|
251
267
|
return;
|
|
252
268
|
}
|
|
253
|
-
// Agent-level endpoints: /
|
|
254
|
-
const agentMatch = /^\/
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|