@ouro.bot/cli 0.1.0-alpha.723 → 0.1.0-alpha.725
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 +295 -275
- package/dist/heart/active-work.js +10 -3
- package/dist/heart/core.js +343 -77
- package/dist/heart/daemon/cli-exec.js +132 -15
- package/dist/heart/daemon/cli-help.js +13 -2
- package/dist/heart/daemon/cli-parse.js +82 -1
- package/dist/heart/daemon/daemon.js +286 -33
- package/dist/heart/daemon/doctor.js +82 -6
- package/dist/heart/daemon/habit-private-wake.js +6 -1
- package/dist/heart/habits/habit-cancel.js +810 -0
- package/dist/heart/habits/habit-lifecycle.js +1327 -0
- package/dist/heart/habits/habit-migration.js +28 -23
- package/dist/heart/habits/habit-parser.js +72 -8
- package/dist/heart/habits/habit-runtime-state.js +3 -24
- package/dist/heart/habits/habit-scheduler.js +268 -50
- package/dist/heart/habits/habit-session.js +16 -0
- package/dist/heart/hatch/hatch-flow.js +6 -4
- package/dist/heart/mailbox/readers/runtime-readers.js +26 -27
- package/dist/heart/orientation-frame.js +58 -1
- package/dist/heart/provider-attempt.js +4 -1
- package/dist/heart/providers/anthropic.js +23 -2
- package/dist/heart/start-of-turn-packet.js +14 -11
- package/dist/heart/streaming.js +565 -185
- package/dist/mind/obligation-steering.js +15 -9
- package/dist/mind/prompt.js +58 -26
- package/dist/nerves/coverage/run-artifacts.js +55 -35
- package/dist/nerves/review/cli.js +2 -2
- package/dist/nerves/review/core.js +14 -7
- package/dist/repertoire/tools-base.js +2 -0
- package/dist/repertoire/tools-habits.js +103 -0
- package/dist/repertoire/tools.js +18 -3
- package/dist/rsvp/cli.js +126 -20
- package/dist/rsvp/diagnostics.js +12 -2
- package/dist/rsvp/habit-policy.js +7 -0
- package/dist/rsvp/habit-stage.js +19 -4
- package/dist/rsvp/native-habit-runner.js +151 -11
- package/dist/rsvp/outbound-state.js +360 -0
- package/dist/senses/bluebubbles/client.js +90 -17
- package/dist/senses/bluebubbles/inbound-log.js +1 -0
- package/dist/senses/bluebubbles/index.js +662 -425
- package/dist/senses/bluebubbles/model.js +57 -13
- package/dist/senses/bluebubbles/mutation-log.js +21 -10
- package/dist/senses/bluebubbles/reaction-policy.js +55 -0
- package/dist/senses/bluebubbles/semantic-receipts.js +1818 -0
- package/dist/senses/cli/tui-store.js +1 -0
- package/dist/senses/cli.js +1 -0
- package/dist/senses/habit-lifecycle-guard.js +24 -0
- package/dist/senses/ingress-evidence.js +2 -0
- package/dist/senses/mail.js +129 -0
- package/dist/senses/pipeline.js +90 -24
- package/dist/senses/private-runtime-worker.js +21 -20
- package/dist/senses/private-runtime.js +79 -29
- package/dist/senses/shared-turn.js +1 -0
- package/dist/senses/teams.js +1 -0
- package/package.json +6 -1
|
@@ -488,6 +488,13 @@ function isValidHabitPokeCommand(command) {
|
|
|
488
488
|
&& typeof command.habitName === "string"
|
|
489
489
|
&& command.habitName.trim().length > 0;
|
|
490
490
|
}
|
|
491
|
+
function isValidHabitProbeCommand(command) {
|
|
492
|
+
return typeof command.agent === "string"
|
|
493
|
+
&& command.agent.trim().length > 0
|
|
494
|
+
&& typeof command.habitName === "string"
|
|
495
|
+
&& command.habitName.trim().length > 0
|
|
496
|
+
&& (command.noSend === undefined || command.noSend === true);
|
|
497
|
+
}
|
|
491
498
|
function taskIdFromTaskPokePrivateWakeCommand(command) {
|
|
492
499
|
if (command.kind !== "private.wake" || command.triggerSource !== "task-poke")
|
|
493
500
|
return null;
|
|
@@ -1162,6 +1169,10 @@ class OuroDaemon {
|
|
|
1162
1169
|
originRefs: [{ kind: "daemon-command", id: "inner.wake" }],
|
|
1163
1170
|
};
|
|
1164
1171
|
}
|
|
1172
|
+
const hasNoSendCapability = command.originRefs?.some((ref) => ref.kind === "capability" && ref.id === "no-send") ?? false;
|
|
1173
|
+
if ((command.noSend === true) !== hasNoSendCapability) {
|
|
1174
|
+
throw new Error("private-runtime no-send capability does not match its bound origin reference");
|
|
1175
|
+
}
|
|
1165
1176
|
return {
|
|
1166
1177
|
agent: command.agent,
|
|
1167
1178
|
origin: "daemon.private.wake",
|
|
@@ -1210,37 +1221,110 @@ class OuroDaemon {
|
|
|
1210
1221
|
],
|
|
1211
1222
|
};
|
|
1212
1223
|
}
|
|
1213
|
-
|
|
1214
|
-
const agentRoot = path.join(this.bundlesRoot, `${
|
|
1215
|
-
const habitPath = path.join(agentRoot, "habits", `${
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1224
|
+
resolveHabitDispatch(options) {
|
|
1225
|
+
const agentRoot = path.join(this.bundlesRoot, `${options.agent}.ouro`);
|
|
1226
|
+
const habitPath = path.join(agentRoot, "habits", `${options.habitName}.md`);
|
|
1227
|
+
const isRsvp = (0, habit_policy_1.isRsvpHabitName)(options.habitName);
|
|
1228
|
+
let readFailure = null;
|
|
1229
|
+
let habit;
|
|
1230
|
+
if (!fs.existsSync(habitPath)) {
|
|
1231
|
+
readFailure = "missing";
|
|
1232
|
+
habit = (0, habit_parser_1.createDegradedHabitFile)(habitPath, "read_error", "", "habit file not found");
|
|
1233
|
+
}
|
|
1234
|
+
else {
|
|
1220
1235
|
try {
|
|
1221
|
-
|
|
1236
|
+
habit = (0, habit_runtime_state_1.applyHabitRuntimeState)(agentRoot, (0, habit_parser_1.parseHabitFile)(fs.readFileSync(habitPath, "utf-8"), habitPath));
|
|
1222
1237
|
}
|
|
1223
1238
|
catch (error) {
|
|
1224
|
-
|
|
1239
|
+
readFailure = "read";
|
|
1240
|
+
habit = (0, habit_parser_1.createDegradedHabitFile)(habitPath, "read_error", "", messageFromHabitPokeError(error));
|
|
1225
1241
|
}
|
|
1226
|
-
if (!parsedHabit.rsvp)
|
|
1227
|
-
return { skipReason: "RSVP habit metadata is required before private runtime wake" };
|
|
1228
1242
|
}
|
|
1229
|
-
if (
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1243
|
+
if (habit.status !== "active") {
|
|
1244
|
+
let skipReason = `habit status ${habit.status} is non-executable`;
|
|
1245
|
+
if (isRsvp && habit.status === "degraded") {
|
|
1246
|
+
if (readFailure === "missing") {
|
|
1247
|
+
skipReason = "RSVP habit file not found";
|
|
1248
|
+
}
|
|
1249
|
+
else {
|
|
1250
|
+
skipReason = `RSVP habit metadata invalid: ${habit.degradedDetail ?? habit.degradedReason}`;
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
else if (readFailure === "missing"
|
|
1254
|
+
&& options.source === "habit-poke"
|
|
1255
|
+
&& options.trigger !== "manual"
|
|
1256
|
+
&& options.trigger !== "poke") {
|
|
1257
|
+
skipReason = "habit file not found";
|
|
1258
|
+
}
|
|
1259
|
+
return { kind: "skipped", habit, skipReason };
|
|
1260
|
+
}
|
|
1261
|
+
if (isRsvp && !habit.rsvp) {
|
|
1262
|
+
return {
|
|
1263
|
+
kind: "skipped",
|
|
1264
|
+
habit,
|
|
1265
|
+
skipReason: "RSVP habit metadata is required before private runtime wake",
|
|
1266
|
+
};
|
|
1267
|
+
}
|
|
1268
|
+
if (options.trigger === "poke" || options.trigger === "manual") {
|
|
1269
|
+
return { kind: "ready", habit };
|
|
1270
|
+
}
|
|
1271
|
+
if (typeof options.occurrenceId === "string" && options.occurrenceId.trim().length > 0) {
|
|
1272
|
+
return { kind: "ready", habit, occurrenceId: options.occurrenceId.trim() };
|
|
1273
|
+
}
|
|
1274
|
+
if (habit.cadence === null) {
|
|
1275
|
+
return { kind: "skipped", habit, skipReason: "habit has no cadence" };
|
|
1276
|
+
}
|
|
1277
|
+
if (habit.lastRun === null) {
|
|
1278
|
+
return { kind: "ready", habit, occurrenceId: `${options.trigger}:first-run:${habit.cadence}` };
|
|
1279
|
+
}
|
|
1280
|
+
return {
|
|
1281
|
+
kind: "ready",
|
|
1282
|
+
habit,
|
|
1283
|
+
occurrenceId: `${options.trigger}:last-run:${habit.lastRun}:cadence:${habit.cadence}`,
|
|
1284
|
+
};
|
|
1285
|
+
}
|
|
1286
|
+
emitHabitDispatchSkipped(options) {
|
|
1287
|
+
(0, runtime_1.emitNervesEvent)({
|
|
1288
|
+
level: "warn",
|
|
1289
|
+
component: "daemon",
|
|
1290
|
+
event: "daemon.habit_dispatch_skipped",
|
|
1291
|
+
message: "habit dispatch rejected by current lifecycle state",
|
|
1292
|
+
meta: {
|
|
1293
|
+
agent: options.agent,
|
|
1294
|
+
habitName: options.habitName,
|
|
1295
|
+
trigger: options.trigger,
|
|
1296
|
+
status: options.resolution.habit.status,
|
|
1297
|
+
degradedReason: options.resolution.habit.status === "degraded"
|
|
1298
|
+
? options.resolution.habit.degradedReason
|
|
1299
|
+
: null,
|
|
1300
|
+
detail: options.resolution.habit.status === "degraded"
|
|
1301
|
+
? options.resolution.habit.degradedDetail
|
|
1302
|
+
: null,
|
|
1303
|
+
reason: options.resolution.skipReason,
|
|
1304
|
+
},
|
|
1305
|
+
});
|
|
1242
1306
|
}
|
|
1243
|
-
|
|
1307
|
+
emitHabitDispatchReady(options) {
|
|
1308
|
+
(0, runtime_1.emitNervesEvent)({
|
|
1309
|
+
component: "daemon",
|
|
1310
|
+
event: "daemon.habit_dispatch_ready",
|
|
1311
|
+
message: "habit dispatch accepted by current lifecycle state",
|
|
1312
|
+
meta: {
|
|
1313
|
+
agent: options.agent,
|
|
1314
|
+
habitName: options.habitName,
|
|
1315
|
+
trigger: options.trigger,
|
|
1316
|
+
status: options.resolution.habit.status,
|
|
1317
|
+
occurrenceId: options.resolution.occurrenceId ?? null,
|
|
1318
|
+
},
|
|
1319
|
+
});
|
|
1320
|
+
}
|
|
1321
|
+
skippedHabitDispatchResponse(options) {
|
|
1322
|
+
return {
|
|
1323
|
+
ok: true,
|
|
1324
|
+
message: `skipped scheduled habit ${options.habitName} for ${options.agent}: ${options.skipReason}`,
|
|
1325
|
+
};
|
|
1326
|
+
}
|
|
1327
|
+
async runNativeRsvpHabit(command, trigger, occurrenceId, noSend = false) {
|
|
1244
1328
|
const runner = this.rsvpHabitRunner ?? (async (input) => {
|
|
1245
1329
|
const { runNativeRsvpHabit } = await Promise.resolve().then(() => __importStar(require("../../rsvp/native-habit-runner")));
|
|
1246
1330
|
return runNativeRsvpHabit(input);
|
|
@@ -1251,7 +1335,48 @@ class OuroDaemon {
|
|
|
1251
1335
|
habitName: command.habitName,
|
|
1252
1336
|
trigger,
|
|
1253
1337
|
...(occurrenceId ? { occurrenceId } : {}),
|
|
1338
|
+
...(noSend ? { noSend: true } : {}),
|
|
1254
1339
|
});
|
|
1340
|
+
if (noSend) {
|
|
1341
|
+
const payload = result.payload;
|
|
1342
|
+
const status = payload?.status;
|
|
1343
|
+
const transportInvocationCount = payload?.transportInvocationCount;
|
|
1344
|
+
const contractError = "native RSVP habit probe violated the immutable no-send result contract";
|
|
1345
|
+
if (!result.ok) {
|
|
1346
|
+
return {
|
|
1347
|
+
ok: false,
|
|
1348
|
+
error: result.message,
|
|
1349
|
+
data: {
|
|
1350
|
+
ok: false,
|
|
1351
|
+
noSend: true,
|
|
1352
|
+
status: typeof status === "string" ? status : "degraded",
|
|
1353
|
+
transportInvocationCount: null,
|
|
1354
|
+
error: result.message,
|
|
1355
|
+
},
|
|
1356
|
+
};
|
|
1357
|
+
}
|
|
1358
|
+
if (payload?.noSend !== true
|
|
1359
|
+
|| typeof status !== "string"
|
|
1360
|
+
|| typeof transportInvocationCount !== "number"
|
|
1361
|
+
|| transportInvocationCount !== 0) {
|
|
1362
|
+
return {
|
|
1363
|
+
ok: false,
|
|
1364
|
+
error: contractError,
|
|
1365
|
+
data: {
|
|
1366
|
+
ok: false,
|
|
1367
|
+
noSend: true,
|
|
1368
|
+
status: typeof status === "string" ? status : "degraded",
|
|
1369
|
+
transportInvocationCount: null,
|
|
1370
|
+
error: contractError,
|
|
1371
|
+
},
|
|
1372
|
+
};
|
|
1373
|
+
}
|
|
1374
|
+
return {
|
|
1375
|
+
ok: true,
|
|
1376
|
+
message: result.message,
|
|
1377
|
+
data: { noSend: true, status, transportInvocationCount: 0 },
|
|
1378
|
+
};
|
|
1379
|
+
}
|
|
1255
1380
|
return {
|
|
1256
1381
|
ok: result.ok,
|
|
1257
1382
|
message: result.message,
|
|
@@ -1281,6 +1406,7 @@ class OuroDaemon {
|
|
|
1281
1406
|
type: "habit",
|
|
1282
1407
|
habitName: habitMessage.habitName,
|
|
1283
1408
|
trigger: habitMessage.trigger,
|
|
1409
|
+
...(habitMessage.noSend ? { noSend: true } : {}),
|
|
1284
1410
|
privateTurnDecision: decision,
|
|
1285
1411
|
};
|
|
1286
1412
|
}
|
|
@@ -1616,8 +1742,43 @@ class OuroDaemon {
|
|
|
1616
1742
|
}
|
|
1617
1743
|
case "inner.wake":
|
|
1618
1744
|
return this.handlePrivateRuntimeWake(command);
|
|
1619
|
-
case "private.wake":
|
|
1745
|
+
case "private.wake": {
|
|
1746
|
+
const scheduledHabit = (0, habit_private_wake_1.habitMessageFromPrivateWakeCommand)(command);
|
|
1747
|
+
if (!scheduledHabit || !this.hasManagedPrivateRuntime(command.agent)) {
|
|
1748
|
+
return this.handlePrivateRuntimeWake(command);
|
|
1749
|
+
}
|
|
1750
|
+
const occurrenceRef = command.originRefs?.find((ref) => ref.kind === "habit-occurrence");
|
|
1751
|
+
const occurrenceId = typeof occurrenceRef?.id === "string" && occurrenceRef.id.trim().length > 0
|
|
1752
|
+
? occurrenceRef.id.trim()
|
|
1753
|
+
: undefined;
|
|
1754
|
+
const resolution = this.resolveHabitDispatch({
|
|
1755
|
+
agent: command.agent,
|
|
1756
|
+
habitName: scheduledHabit.habitName,
|
|
1757
|
+
trigger: scheduledHabit.trigger,
|
|
1758
|
+
source: "private-wake",
|
|
1759
|
+
...(occurrenceId ? { occurrenceId } : {}),
|
|
1760
|
+
});
|
|
1761
|
+
if (resolution.kind === "skipped") {
|
|
1762
|
+
this.emitHabitDispatchSkipped({
|
|
1763
|
+
agent: command.agent,
|
|
1764
|
+
habitName: scheduledHabit.habitName,
|
|
1765
|
+
trigger: scheduledHabit.trigger,
|
|
1766
|
+
resolution,
|
|
1767
|
+
});
|
|
1768
|
+
return this.skippedHabitDispatchResponse({
|
|
1769
|
+
agent: command.agent,
|
|
1770
|
+
habitName: scheduledHabit.habitName,
|
|
1771
|
+
skipReason: resolution.skipReason,
|
|
1772
|
+
});
|
|
1773
|
+
}
|
|
1774
|
+
this.emitHabitDispatchReady({
|
|
1775
|
+
agent: command.agent,
|
|
1776
|
+
habitName: scheduledHabit.habitName,
|
|
1777
|
+
trigger: scheduledHabit.trigger,
|
|
1778
|
+
resolution,
|
|
1779
|
+
});
|
|
1620
1780
|
return this.handlePrivateRuntimeWake(command);
|
|
1781
|
+
}
|
|
1621
1782
|
case "chat.connect":
|
|
1622
1783
|
await this.processManager.startAgent(command.agent);
|
|
1623
1784
|
return {
|
|
@@ -1663,23 +1824,115 @@ class OuroDaemon {
|
|
|
1663
1824
|
error: `No managed agent '${command.agent}' is registered with daemon-managed private runtime.`,
|
|
1664
1825
|
};
|
|
1665
1826
|
}
|
|
1666
|
-
const
|
|
1667
|
-
|
|
1827
|
+
const resolution = this.resolveHabitDispatch({
|
|
1828
|
+
agent: command.agent,
|
|
1829
|
+
habitName: command.habitName,
|
|
1830
|
+
trigger,
|
|
1831
|
+
source: "habit-poke",
|
|
1832
|
+
...(command.occurrenceId ? { occurrenceId: command.occurrenceId } : {}),
|
|
1833
|
+
});
|
|
1834
|
+
if (resolution.kind === "skipped") {
|
|
1835
|
+
this.emitHabitDispatchSkipped({
|
|
1836
|
+
agent: command.agent,
|
|
1837
|
+
habitName: command.habitName,
|
|
1838
|
+
trigger,
|
|
1839
|
+
resolution,
|
|
1840
|
+
});
|
|
1841
|
+
return this.skippedHabitDispatchResponse({
|
|
1842
|
+
agent: command.agent,
|
|
1843
|
+
habitName: command.habitName,
|
|
1844
|
+
skipReason: resolution.skipReason,
|
|
1845
|
+
});
|
|
1846
|
+
}
|
|
1847
|
+
this.emitHabitDispatchReady({
|
|
1848
|
+
agent: command.agent,
|
|
1849
|
+
habitName: command.habitName,
|
|
1850
|
+
trigger,
|
|
1851
|
+
resolution,
|
|
1852
|
+
});
|
|
1853
|
+
if ((0, habit_policy_1.isRsvpHabitName)(command.habitName)) {
|
|
1854
|
+
return this.runNativeRsvpHabit(command, trigger, resolution.occurrenceId);
|
|
1855
|
+
}
|
|
1856
|
+
return this.handlePrivateRuntimeWake((0, habit_private_wake_1.buildHabitPrivateWakeCommand)({
|
|
1857
|
+
agent: command.agent,
|
|
1858
|
+
habitName: command.habitName,
|
|
1859
|
+
trigger,
|
|
1860
|
+
sourceRef: { kind: "daemon-command", id: "habit.poke" },
|
|
1861
|
+
occurrenceId: resolution.occurrenceId,
|
|
1862
|
+
}));
|
|
1863
|
+
}
|
|
1864
|
+
case "habit.probe": {
|
|
1865
|
+
if (!isValidHabitProbeCommand(command)) {
|
|
1866
|
+
return {
|
|
1867
|
+
ok: false,
|
|
1868
|
+
error: "Invalid habit.probe payload: expected non-empty agent/habitName and noSend=true.",
|
|
1869
|
+
};
|
|
1870
|
+
}
|
|
1871
|
+
const trigger = "manual";
|
|
1872
|
+
if (!(0, habit_policy_1.isRsvpHabitName)(command.habitName) && !this.hasManagedPrivateRuntime(command.agent)) {
|
|
1873
|
+
return {
|
|
1874
|
+
ok: false,
|
|
1875
|
+
error: `No managed agent '${command.agent}' is registered with daemon-managed private runtime.`,
|
|
1876
|
+
};
|
|
1877
|
+
}
|
|
1878
|
+
const resolution = this.resolveHabitDispatch({
|
|
1879
|
+
agent: command.agent,
|
|
1880
|
+
habitName: command.habitName,
|
|
1881
|
+
trigger,
|
|
1882
|
+
source: "habit-probe",
|
|
1883
|
+
});
|
|
1884
|
+
if (resolution.kind === "skipped") {
|
|
1885
|
+
this.emitHabitDispatchSkipped({
|
|
1886
|
+
agent: command.agent,
|
|
1887
|
+
habitName: command.habitName,
|
|
1888
|
+
trigger,
|
|
1889
|
+
resolution,
|
|
1890
|
+
});
|
|
1668
1891
|
return {
|
|
1669
1892
|
ok: true,
|
|
1670
|
-
message: `
|
|
1893
|
+
message: `habit probe rejected by lifecycle: status=${resolution.habit.status}`,
|
|
1894
|
+
data: {
|
|
1895
|
+
noSend: true,
|
|
1896
|
+
status: resolution.habit.status,
|
|
1897
|
+
transportInvocationCount: 0,
|
|
1898
|
+
},
|
|
1671
1899
|
};
|
|
1672
1900
|
}
|
|
1901
|
+
this.emitHabitDispatchReady({
|
|
1902
|
+
agent: command.agent,
|
|
1903
|
+
habitName: command.habitName,
|
|
1904
|
+
trigger,
|
|
1905
|
+
resolution,
|
|
1906
|
+
});
|
|
1673
1907
|
if ((0, habit_policy_1.isRsvpHabitName)(command.habitName)) {
|
|
1674
|
-
return this.runNativeRsvpHabit(command, trigger, occurrenceId);
|
|
1908
|
+
return this.runNativeRsvpHabit(command, trigger, resolution.occurrenceId, true);
|
|
1675
1909
|
}
|
|
1676
|
-
|
|
1910
|
+
const wake = await this.handlePrivateRuntimeWake((0, habit_private_wake_1.buildHabitPrivateWakeCommand)({
|
|
1677
1911
|
agent: command.agent,
|
|
1678
1912
|
habitName: command.habitName,
|
|
1679
1913
|
trigger,
|
|
1680
|
-
sourceRef: { kind: "daemon-command", id: "habit.
|
|
1681
|
-
occurrenceId,
|
|
1914
|
+
sourceRef: { kind: "daemon-command", id: "habit.probe" },
|
|
1915
|
+
occurrenceId: resolution.occurrenceId,
|
|
1916
|
+
noSend: true,
|
|
1682
1917
|
}));
|
|
1918
|
+
if (!wake.ok)
|
|
1919
|
+
return wake;
|
|
1920
|
+
const denied = wake.data
|
|
1921
|
+
?.decision?.executable === false;
|
|
1922
|
+
const error = denied
|
|
1923
|
+
? "non-RSVP habit probe was not executed because private-runtime policy denied execution"
|
|
1924
|
+
: "non-RSVP habit probe completion is asynchronous; zero-transport evidence is unverified";
|
|
1925
|
+
return {
|
|
1926
|
+
ok: false,
|
|
1927
|
+
error,
|
|
1928
|
+
data: {
|
|
1929
|
+
ok: false,
|
|
1930
|
+
noSend: true,
|
|
1931
|
+
status: resolution.habit.status,
|
|
1932
|
+
transportInvocationCount: null,
|
|
1933
|
+
error,
|
|
1934
|
+
},
|
|
1935
|
+
};
|
|
1683
1936
|
}
|
|
1684
1937
|
case "await.poke": {
|
|
1685
1938
|
return this.handlePrivateRuntimeWake(this.buildAwaitPrivateWakeCommand(command));
|
|
@@ -99,6 +99,10 @@ function textField(record, key) {
|
|
|
99
99
|
const value = record?.[key];
|
|
100
100
|
return typeof value === "string" ? value.trim() : "";
|
|
101
101
|
}
|
|
102
|
+
function stringArrayField(record, key) {
|
|
103
|
+
const value = record?.[key];
|
|
104
|
+
return Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : [];
|
|
105
|
+
}
|
|
102
106
|
function numberField(record, key, fallback) {
|
|
103
107
|
const value = record?.[key];
|
|
104
108
|
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
@@ -271,6 +275,59 @@ function checkAgents(deps) {
|
|
|
271
275
|
}
|
|
272
276
|
return { name: "Agents", checks };
|
|
273
277
|
}
|
|
278
|
+
/**
|
|
279
|
+
* Mail key coverage, as recorded by the mail sense.
|
|
280
|
+
*
|
|
281
|
+
* Doctor reads the sense's own per-cycle verdict rather than re-deriving it:
|
|
282
|
+
* a health check must need neither network access nor Azure credentials, and
|
|
283
|
+
* the registry that declares the key ids lives in a Blob container in hosted
|
|
284
|
+
* mode. The mail sense already fetches it every cycle, so its record is both
|
|
285
|
+
* the cheapest and the only honest source here.
|
|
286
|
+
*
|
|
287
|
+
* Absent is a `fail` because mail encrypted to a key with no private half is
|
|
288
|
+
* permanently unreadable — unlike an API key, ciphertext cannot be reissued.
|
|
289
|
+
* Anything the sense could not verify is a `warn`, never a `fail`: a locked or
|
|
290
|
+
* unavailable vault must not read as key loss.
|
|
291
|
+
*/
|
|
292
|
+
function mailKeyCoverageCheck(deps, agentDir) {
|
|
293
|
+
const id = "mail.key_coverage";
|
|
294
|
+
const label = `${agentDir} mail key coverage`;
|
|
295
|
+
const statePath = `${deps.bundlesRoot}/${agentDir}/state/senses/mail/runtime.json`;
|
|
296
|
+
const unverified = (detail) => ({ id, label, status: "warn", detail });
|
|
297
|
+
if (!deps.existsSync(statePath)) {
|
|
298
|
+
return unverified(`no mail sense runtime state at ${statePath} — coverage is unverified until the mail sense runs a scan`);
|
|
299
|
+
}
|
|
300
|
+
let coverage;
|
|
301
|
+
try {
|
|
302
|
+
coverage = asRecord(JSON.parse(deps.readFileSync(statePath)).keyCoverage) ?? {};
|
|
303
|
+
}
|
|
304
|
+
catch {
|
|
305
|
+
return unverified(`mail sense runtime state at ${statePath} could not be read`);
|
|
306
|
+
}
|
|
307
|
+
const declaredKeyIds = stringArrayField(coverage, "declaredKeyIds");
|
|
308
|
+
const absentKeyIds = stringArrayField(coverage, "absentKeyIds");
|
|
309
|
+
const checkedAt = textField(coverage, "checkedAt");
|
|
310
|
+
const provenance = checkedAt ? ` (recorded ${checkedAt})` : "";
|
|
311
|
+
if (coverage.status === "covered") {
|
|
312
|
+
return {
|
|
313
|
+
id,
|
|
314
|
+
label,
|
|
315
|
+
status: "pass",
|
|
316
|
+
detail: declaredKeyIds.length === 1
|
|
317
|
+
? `the 1 registry-declared mail key has a private half in the vault${provenance}`
|
|
318
|
+
: `all ${declaredKeyIds.length} registry-declared mail keys have a private half in the vault${provenance}`,
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
if (coverage.status === "absent") {
|
|
322
|
+
return {
|
|
323
|
+
id,
|
|
324
|
+
label,
|
|
325
|
+
status: "fail",
|
|
326
|
+
detail: `the mailroom registry declares mail key${absentKeyIds.length === 1 ? "" : "s"} with no private half in this agent's vault — mail encrypted to ${absentKeyIds.join(", ")} cannot be decrypted and cannot be recovered${provenance}; run \`ouro connect mail --agent ${agentDir.replace(/\.ouro$/, "")}\` and, if Mail Control no longer holds the one-time key, rerun it with --rotate-missing-mail-keys to mint a replacement for future mail`,
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
return unverified(`mail sense could not verify key coverage${provenance}: ${textField(coverage, "reason") || "no reason recorded"}`);
|
|
330
|
+
}
|
|
274
331
|
async function checkSenses(deps) {
|
|
275
332
|
const checks = [];
|
|
276
333
|
const agents = discoverAgents(deps);
|
|
@@ -422,6 +479,10 @@ async function checkSenses(deps) {
|
|
|
422
479
|
mailAutonomyDetail(mailroom),
|
|
423
480
|
].join("; "),
|
|
424
481
|
});
|
|
482
|
+
// Config being well-formed says nothing about whether the vault can
|
|
483
|
+
// still decrypt what the registry points mail at — the 2026-07-24 gap
|
|
484
|
+
// sat behind a passing mail config check for 23 hours.
|
|
485
|
+
checks.push(mailKeyCoverageCheck(deps, agentDir));
|
|
425
486
|
}
|
|
426
487
|
}
|
|
427
488
|
}
|
|
@@ -442,19 +503,34 @@ function checkHabits(deps) {
|
|
|
442
503
|
}
|
|
443
504
|
checks.push({ label: `${agentDir} habits dir`, status: "pass", detail: habitsDir });
|
|
444
505
|
const activeScheduledHabits = [];
|
|
506
|
+
const habitLifecycleRows = [];
|
|
445
507
|
let unreadableHabits = 0;
|
|
446
|
-
for (const file of deps.readdirSync(habitsDir).filter((entry) => entry.endsWith(".md"))) {
|
|
508
|
+
for (const file of deps.readdirSync(habitsDir).filter((entry) => entry.endsWith(".md") && entry !== "README.md").sort()) {
|
|
447
509
|
const filePath = `${habitsDir}/${file}`;
|
|
510
|
+
let habit;
|
|
448
511
|
try {
|
|
449
|
-
|
|
450
|
-
if (habit.status === "active" && habit.cadence && (0, cadence_1.parseCadenceToCron)(habit.cadence) !== null) {
|
|
451
|
-
activeScheduledHabits.push(habit.name);
|
|
452
|
-
}
|
|
512
|
+
habit = (0, habit_parser_1.parseHabitFile)(deps.readFileSync(filePath), filePath);
|
|
453
513
|
}
|
|
454
|
-
catch {
|
|
514
|
+
catch (error) {
|
|
455
515
|
unreadableHabits += 1;
|
|
516
|
+
habit = (0, habit_parser_1.createDegradedHabitFile)(filePath, "read_error", "", error instanceof Error ? error.message : String(error));
|
|
517
|
+
}
|
|
518
|
+
habitLifecycleRows.push(habit);
|
|
519
|
+
if (habit.status === "active" && habit.cadence && (0, cadence_1.parseCadenceToCron)(habit.cadence) !== null) {
|
|
520
|
+
activeScheduledHabits.push(habit.name);
|
|
456
521
|
}
|
|
457
522
|
}
|
|
523
|
+
if (habitLifecycleRows.length > 0) {
|
|
524
|
+
const hasDegradedHabit = habitLifecycleRows.some((habit) => habit.status === "degraded");
|
|
525
|
+
checks.push({
|
|
526
|
+
id: "habits.lifecycle",
|
|
527
|
+
label: `${agentDir} habit lifecycle`,
|
|
528
|
+
status: hasDegradedHabit ? "fail" : "pass",
|
|
529
|
+
detail: habitLifecycleRows.map((habit) => habit.status === "degraded"
|
|
530
|
+
? `${habit.name}=degraded(${habit.degradedReason})`
|
|
531
|
+
: `${habit.name}=${habit.status}`).join(", "),
|
|
532
|
+
});
|
|
533
|
+
}
|
|
458
534
|
if (unreadableHabits > 0) {
|
|
459
535
|
checks.push({
|
|
460
536
|
label: `${agentDir} habit files`,
|
|
@@ -32,7 +32,9 @@ function buildHabitPrivateWakeCommand(options) {
|
|
|
32
32
|
{ kind: "habit-trigger", id: options.trigger },
|
|
33
33
|
{ kind: "habit-occurrence", id: occurrenceId },
|
|
34
34
|
options.sourceRef,
|
|
35
|
+
...(options.noSend ? [{ kind: "capability", id: "no-send" }] : []),
|
|
35
36
|
],
|
|
37
|
+
...(options.noSend ? { noSend: true } : {}),
|
|
36
38
|
};
|
|
37
39
|
}
|
|
38
40
|
function habitMessageFromPrivateWakeCommand(command) {
|
|
@@ -42,8 +44,11 @@ function habitMessageFromPrivateWakeCommand(command) {
|
|
|
42
44
|
return null;
|
|
43
45
|
const habitRef = command.originRefs?.find((ref) => ref.kind === "habit" && typeof ref.id === "string");
|
|
44
46
|
const triggerRef = command.originRefs?.find((ref) => ref.kind === "habit-trigger" && typeof ref.id === "string");
|
|
47
|
+
const noSendRef = command.originRefs?.some((ref) => ref.kind === "capability" && ref.id === "no-send") ?? false;
|
|
45
48
|
if (!habitRef || typeof habitRef.id !== "string" || !triggerRef || typeof triggerRef.id !== "string")
|
|
46
49
|
return null;
|
|
50
|
+
if ((command.noSend === true) !== noSendRef)
|
|
51
|
+
return null;
|
|
47
52
|
const habitName = habitRef.id.trim();
|
|
48
53
|
const trigger = triggerRef.id.trim();
|
|
49
54
|
if (habitName.length === 0)
|
|
@@ -52,5 +57,5 @@ function habitMessageFromPrivateWakeCommand(command) {
|
|
|
52
57
|
return null;
|
|
53
58
|
if (command.triggerSource !== `habit-${trigger}`)
|
|
54
59
|
return null;
|
|
55
|
-
return { habitName, trigger };
|
|
60
|
+
return { habitName, trigger, ...(noSendRef ? { noSend: true } : {}) };
|
|
56
61
|
}
|