@ouro.bot/cli 0.1.0-alpha.152 → 0.1.0-alpha.154
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 +23 -0
- package/dist/heart/daemon/daemon-cli.js +20 -2
- package/dist/heart/daemon/daemon-health.js +136 -0
- package/dist/heart/daemon/daemon-tombstone.js +19 -2
- package/dist/heart/daemon/habit-scheduler.js +114 -0
- package/dist/heart/daemon/safe-mode.js +161 -0
- package/dist/heart/tool-activity-callbacks.js +30 -0
- package/dist/heart/tool-description.js +84 -0
- package/dist/nerves/event-buffer.js +111 -0
- package/dist/senses/bluebubbles.js +73 -58
- package/dist/senses/cli.js +45 -23
- package/dist/senses/commands.js +32 -0
- package/dist/senses/inner-dialog.js +12 -0
- package/dist/senses/pipeline.js +36 -0
- package/dist/senses/teams.js +46 -35
- package/package.json +1 -1
- package/dist/senses/debug-activity.js +0 -154
package/changelog.json
CHANGED
|
@@ -1,6 +1,29 @@
|
|
|
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.154",
|
|
6
|
+
"changes": [
|
|
7
|
+
"feat: clean tool status messages \u2014 human-readable by default, /debug toggle",
|
|
8
|
+
"humanReadableToolDescription derives from tool name+args, not hardcoded map",
|
|
9
|
+
"Shared tool activity callbacks (DRY) \u2014 senses only provide render function",
|
|
10
|
+
"Slash command handling moved to pipeline \u2014 all senses get /debug for free",
|
|
11
|
+
"BlueBubbles: one clean iMessage per tool, not raw shared work: processing"
|
|
12
|
+
]
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"version": "0.1.0-alpha.153",
|
|
16
|
+
"changes": [
|
|
17
|
+
"Daemon health file: DaemonHealthWriter writes atomic daemon-health.json on every state change. readHealth() standalone reader for CLI and agent processes.",
|
|
18
|
+
"Nerves event buffer: createBufferedSink wraps sinks with bounded ring buffer (1000 events), auto-flushes on recovery, TTL discards after 5 min of unhealthy sink.",
|
|
19
|
+
"Cron verification + timer fallback: after cron sync, verifyCronEntries checks launchctl/crontab for registered habits. Unverified habits get in-process setTimeout fallback. getDegradedHabits() for health reporting.",
|
|
20
|
+
"Periodic reconciliation: 30s initial delay then 5-minute setTimeout chain re-scans habits and re-verifies cron entries.",
|
|
21
|
+
"Safe mode: detectSafeMode reads crash history (recentCrashes in tombstone). 3+ crashes in 5 minutes triggers safe mode - daemon stays alive, socket works, agents/habits paused. ouro up --force writes override file.",
|
|
22
|
+
"Health nerves sink: createHealthNervesSink registers as global sink, listens for daemon events (agent_started, agent_exit, habit_fire, safe_mode_entered, etc.), triggers debounced health writes.",
|
|
23
|
+
"ouro status health fallback: when daemon socket is unreachable, reads health file to show last-known status, safe mode, and degraded components.",
|
|
24
|
+
"Degraded state in agent dialog: habit turns read daemon-health.json and append scheduling degradation nudge when components are degraded."
|
|
25
|
+
]
|
|
26
|
+
},
|
|
4
27
|
{
|
|
5
28
|
"version": "0.1.0-alpha.152",
|
|
6
29
|
"changes": [
|
|
@@ -72,6 +72,7 @@ const ouro_bot_global_installer_1 = require("./ouro-bot-global-installer");
|
|
|
72
72
|
const launchd_1 = require("./launchd");
|
|
73
73
|
const socket_client_1 = require("./socket-client");
|
|
74
74
|
const daemon_tombstone_1 = require("./daemon-tombstone");
|
|
75
|
+
const daemon_health_1 = require("./daemon-health");
|
|
75
76
|
const session_activity_1 = require("../session-activity");
|
|
76
77
|
const auth_flow_1 = require("./auth-flow");
|
|
77
78
|
function stringField(value) {
|
|
@@ -350,7 +351,7 @@ function buildStoppedStatusPayload(socketPath) {
|
|
|
350
351
|
workers: [],
|
|
351
352
|
};
|
|
352
353
|
}
|
|
353
|
-
function daemonUnavailableStatusOutput(socketPath) {
|
|
354
|
+
function daemonUnavailableStatusOutput(socketPath, healthFilePath) {
|
|
354
355
|
/* v8 ignore start — tombstone read tested in daemon-status-tombstone.test; branch misreported @preserve */
|
|
355
356
|
const tombstone = (0, daemon_tombstone_1.readDaemonTombstone)();
|
|
356
357
|
const deathLine = tombstone
|
|
@@ -371,6 +372,23 @@ function daemonUnavailableStatusOutput(socketPath) {
|
|
|
371
372
|
lines.push("");
|
|
372
373
|
/* v8 ignore stop */
|
|
373
374
|
}
|
|
375
|
+
// Read health file for last-known state (best-effort)
|
|
376
|
+
const resolvedHealthPath = healthFilePath ?? (0, daemon_health_1.getDefaultHealthPath)();
|
|
377
|
+
const health = (0, daemon_health_1.readHealth)(resolvedHealthPath);
|
|
378
|
+
if (health) {
|
|
379
|
+
lines.push(`Last known status: ${health.status} (pid ${health.pid}, uptime ${health.uptimeSeconds}s)`);
|
|
380
|
+
if (health.safeMode?.active) {
|
|
381
|
+
lines.push(`SAFE MODE: ${health.safeMode.reason}`);
|
|
382
|
+
}
|
|
383
|
+
if (health.degraded.length > 0) {
|
|
384
|
+
lines.push("");
|
|
385
|
+
lines.push("Degraded:");
|
|
386
|
+
for (const d of health.degraded) {
|
|
387
|
+
lines.push(` ${d.component}: ${d.reason} (since ${d.since})`);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
lines.push("");
|
|
391
|
+
}
|
|
374
392
|
lines.push("daemon not running; run `ouro up`");
|
|
375
393
|
return lines.join("\n");
|
|
376
394
|
}
|
|
@@ -3128,7 +3146,7 @@ async function runOuroCli(args, deps = createDefaultOuroCliDeps()) {
|
|
|
3128
3146
|
return message;
|
|
3129
3147
|
}
|
|
3130
3148
|
if (command.kind === "daemon.status" && isDaemonUnavailableError(error)) {
|
|
3131
|
-
const message = daemonUnavailableStatusOutput(deps.socketPath);
|
|
3149
|
+
const message = daemonUnavailableStatusOutput(deps.socketPath, deps.healthFilePath);
|
|
3132
3150
|
deps.writeStdout(message);
|
|
3133
3151
|
return message;
|
|
3134
3152
|
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.HEALTH_TRACKED_EVENTS = exports.DaemonHealthWriter = void 0;
|
|
37
|
+
exports.getDefaultHealthPath = getDefaultHealthPath;
|
|
38
|
+
exports.createHealthNervesSink = createHealthNervesSink;
|
|
39
|
+
exports.readHealth = readHealth;
|
|
40
|
+
const fs = __importStar(require("fs"));
|
|
41
|
+
const os = __importStar(require("os"));
|
|
42
|
+
const path = __importStar(require("path"));
|
|
43
|
+
const runtime_1 = require("../../nerves/runtime");
|
|
44
|
+
class DaemonHealthWriter {
|
|
45
|
+
healthPath;
|
|
46
|
+
constructor(healthPath) {
|
|
47
|
+
this.healthPath = healthPath;
|
|
48
|
+
}
|
|
49
|
+
writeHealth(state) {
|
|
50
|
+
try {
|
|
51
|
+
const dir = path.dirname(this.healthPath);
|
|
52
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
53
|
+
const tmpPath = `${this.healthPath}.tmp.${process.pid}`;
|
|
54
|
+
fs.writeFileSync(tmpPath, JSON.stringify(state, null, 2) + "\n", "utf-8");
|
|
55
|
+
fs.renameSync(tmpPath, this.healthPath);
|
|
56
|
+
(0, runtime_1.emitNervesEvent)({
|
|
57
|
+
component: "daemon",
|
|
58
|
+
event: "daemon.health_written",
|
|
59
|
+
message: "daemon health file written",
|
|
60
|
+
meta: { path: this.healthPath, status: state.status },
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
// Best-effort: if we can't write, don't crash the daemon.
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
exports.DaemonHealthWriter = DaemonHealthWriter;
|
|
69
|
+
function getDefaultHealthPath() {
|
|
70
|
+
return path.join(os.homedir(), ".ouro-cli", "daemon-health.json");
|
|
71
|
+
}
|
|
72
|
+
/** Events that trigger a debounced health file write */
|
|
73
|
+
exports.HEALTH_TRACKED_EVENTS = new Set([
|
|
74
|
+
"daemon.habit_cron_verification_failed",
|
|
75
|
+
"daemon.habit_fire",
|
|
76
|
+
"daemon.agent_exit",
|
|
77
|
+
"daemon.agent_started",
|
|
78
|
+
"daemon.agent_restart_exhausted",
|
|
79
|
+
"daemon.agent_permanent_failure",
|
|
80
|
+
"daemon.agent_cooldown_recovery",
|
|
81
|
+
"daemon.safe_mode_entered",
|
|
82
|
+
"daemon.habit_scheduler_start",
|
|
83
|
+
]);
|
|
84
|
+
/**
|
|
85
|
+
* Creates a nerves LogSink that triggers debounced health writes on relevant events.
|
|
86
|
+
* Components don't know about the health writer — they just emit events.
|
|
87
|
+
*/
|
|
88
|
+
function createHealthNervesSink(writer, getState) {
|
|
89
|
+
let debounceTimer = null;
|
|
90
|
+
return (entry) => {
|
|
91
|
+
if (!exports.HEALTH_TRACKED_EVENTS.has(entry.event)) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
// Debounce: max once per second
|
|
95
|
+
if (debounceTimer !== null) {
|
|
96
|
+
clearTimeout(debounceTimer);
|
|
97
|
+
}
|
|
98
|
+
debounceTimer = setTimeout(() => {
|
|
99
|
+
debounceTimer = null;
|
|
100
|
+
const state = getState();
|
|
101
|
+
writer.writeHealth(state);
|
|
102
|
+
}, 1000);
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
function readHealth(healthPath) {
|
|
106
|
+
try {
|
|
107
|
+
const raw = fs.readFileSync(healthPath, "utf-8");
|
|
108
|
+
const parsed = JSON.parse(raw);
|
|
109
|
+
if (typeof parsed.status !== "string" ||
|
|
110
|
+
typeof parsed.mode !== "string" ||
|
|
111
|
+
typeof parsed.pid !== "number" ||
|
|
112
|
+
typeof parsed.startedAt !== "string" ||
|
|
113
|
+
typeof parsed.uptimeSeconds !== "number" ||
|
|
114
|
+
!Array.isArray(parsed.degraded) ||
|
|
115
|
+
typeof parsed.agents !== "object" ||
|
|
116
|
+
parsed.agents === null ||
|
|
117
|
+
typeof parsed.habits !== "object" ||
|
|
118
|
+
parsed.habits === null) {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
return {
|
|
122
|
+
status: parsed.status,
|
|
123
|
+
mode: parsed.mode,
|
|
124
|
+
pid: parsed.pid,
|
|
125
|
+
startedAt: parsed.startedAt,
|
|
126
|
+
uptimeSeconds: parsed.uptimeSeconds,
|
|
127
|
+
safeMode: parsed.safeMode,
|
|
128
|
+
degraded: parsed.degraded,
|
|
129
|
+
agents: parsed.agents,
|
|
130
|
+
habits: parsed.habits,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
@@ -53,15 +53,29 @@ function setTombstonePath(p) {
|
|
|
53
53
|
_tombstonePath = p;
|
|
54
54
|
}
|
|
55
55
|
function writeDaemonTombstone(reason, error) {
|
|
56
|
+
const now = new Date().toISOString();
|
|
57
|
+
const filePath = getTombstonePath();
|
|
58
|
+
// Read existing recentCrashes from previous tombstone (best-effort)
|
|
59
|
+
let existingCrashes = [];
|
|
60
|
+
try {
|
|
61
|
+
const raw = fs.readFileSync(filePath, "utf-8");
|
|
62
|
+
const existing = JSON.parse(raw);
|
|
63
|
+
if (Array.isArray(existing.recentCrashes)) {
|
|
64
|
+
existingCrashes = existing.recentCrashes.filter((entry) => typeof entry === "string");
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
// No existing tombstone or unreadable — start fresh
|
|
69
|
+
}
|
|
56
70
|
const tombstone = {
|
|
57
71
|
reason,
|
|
58
72
|
message: error?.message ?? reason,
|
|
59
73
|
stack: error?.stack ?? null,
|
|
60
|
-
timestamp:
|
|
74
|
+
timestamp: now,
|
|
61
75
|
pid: process.pid,
|
|
62
76
|
uptimeSeconds: Math.floor(process.uptime()),
|
|
77
|
+
recentCrashes: [...existingCrashes, now],
|
|
63
78
|
};
|
|
64
|
-
const filePath = getTombstonePath();
|
|
65
79
|
try {
|
|
66
80
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
67
81
|
fs.writeFileSync(filePath, JSON.stringify(tombstone, null, 2) + "\n", "utf-8");
|
|
@@ -98,6 +112,9 @@ function readDaemonTombstone() {
|
|
|
98
112
|
timestamp: parsed.timestamp,
|
|
99
113
|
pid: typeof parsed.pid === "number" ? parsed.pid : 0,
|
|
100
114
|
uptimeSeconds: typeof parsed.uptimeSeconds === "number" ? parsed.uptimeSeconds : 0,
|
|
115
|
+
recentCrashes: Array.isArray(parsed.recentCrashes)
|
|
116
|
+
? parsed.recentCrashes.filter((e) => typeof e === "string")
|
|
117
|
+
: [],
|
|
101
118
|
};
|
|
102
119
|
}
|
|
103
120
|
catch {
|
|
@@ -45,15 +45,22 @@ class HabitScheduler {
|
|
|
45
45
|
osCronManager;
|
|
46
46
|
onHabitFire;
|
|
47
47
|
deps;
|
|
48
|
+
execForVerify;
|
|
49
|
+
platform;
|
|
48
50
|
watcher = null;
|
|
49
51
|
debounceTimer = null;
|
|
50
52
|
parseErrors = [];
|
|
53
|
+
timerFallbacks = new Map();
|
|
54
|
+
degradedHabitNames = new Map();
|
|
55
|
+
periodicTimer = null;
|
|
51
56
|
constructor(options) {
|
|
52
57
|
this.agent = options.agent;
|
|
53
58
|
this.habitsDir = options.habitsDir;
|
|
54
59
|
this.osCronManager = options.osCronManager;
|
|
55
60
|
this.onHabitFire = options.onHabitFire;
|
|
56
61
|
this.deps = options.deps;
|
|
62
|
+
this.execForVerify = options.execForVerify;
|
|
63
|
+
this.platform = options.platform ?? process.platform;
|
|
57
64
|
}
|
|
58
65
|
start() {
|
|
59
66
|
(0, runtime_1.emitNervesEvent)({
|
|
@@ -65,6 +72,7 @@ class HabitScheduler {
|
|
|
65
72
|
const habits = this.scanHabits();
|
|
66
73
|
const jobs = this.buildJobs(habits);
|
|
67
74
|
this.osCronManager.sync(jobs);
|
|
75
|
+
this.verifyCronAndCreateFallbacks(jobs);
|
|
68
76
|
// Fire overdue habits immediately
|
|
69
77
|
for (const habit of habits) {
|
|
70
78
|
if (habit.status !== "active")
|
|
@@ -105,9 +113,12 @@ class HabitScheduler {
|
|
|
105
113
|
message: "habit scheduler reconciling",
|
|
106
114
|
meta: { agent: this.agent },
|
|
107
115
|
});
|
|
116
|
+
// Clear ALL existing timers FIRST to prevent overlap window
|
|
117
|
+
this.clearAllTimerFallbacks();
|
|
108
118
|
const habits = this.scanHabits();
|
|
109
119
|
const jobs = this.buildJobs(habits);
|
|
110
120
|
this.osCronManager.sync(jobs);
|
|
121
|
+
this.verifyCronAndCreateFallbacks(jobs);
|
|
111
122
|
}
|
|
112
123
|
stop() {
|
|
113
124
|
(0, runtime_1.emitNervesEvent)({
|
|
@@ -116,6 +127,8 @@ class HabitScheduler {
|
|
|
116
127
|
message: "habit scheduler stopping",
|
|
117
128
|
meta: { agent: this.agent },
|
|
118
129
|
});
|
|
130
|
+
this.stopPeriodicReconciliation();
|
|
131
|
+
this.clearAllTimerFallbacks();
|
|
119
132
|
this.osCronManager.removeAll();
|
|
120
133
|
}
|
|
121
134
|
listOverdueHabits() {
|
|
@@ -187,6 +200,107 @@ class HabitScheduler {
|
|
|
187
200
|
this.watcher = null;
|
|
188
201
|
}
|
|
189
202
|
}
|
|
203
|
+
getDegradedHabits() {
|
|
204
|
+
const result = [];
|
|
205
|
+
for (const [name, reason] of this.degradedHabitNames) {
|
|
206
|
+
result.push({ name, reason });
|
|
207
|
+
}
|
|
208
|
+
return result;
|
|
209
|
+
}
|
|
210
|
+
static DEFAULT_PERIODIC_INTERVAL_MS = 300_000; // 5 minutes
|
|
211
|
+
static INITIAL_RECONCILIATION_DELAY_MS = 30_000; // 30 seconds
|
|
212
|
+
startPeriodicReconciliation(intervalMs) {
|
|
213
|
+
const interval = intervalMs ?? HabitScheduler.DEFAULT_PERIODIC_INTERVAL_MS;
|
|
214
|
+
// First reconciliation after a short delay (30s)
|
|
215
|
+
this.periodicTimer = setTimeout(() => {
|
|
216
|
+
this.reconcile();
|
|
217
|
+
this.scheduleNextReconciliation(interval);
|
|
218
|
+
}, HabitScheduler.INITIAL_RECONCILIATION_DELAY_MS);
|
|
219
|
+
}
|
|
220
|
+
stopPeriodicReconciliation() {
|
|
221
|
+
if (this.periodicTimer !== null) {
|
|
222
|
+
clearTimeout(this.periodicTimer);
|
|
223
|
+
this.periodicTimer = null;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
scheduleNextReconciliation(intervalMs) {
|
|
227
|
+
this.periodicTimer = setTimeout(() => {
|
|
228
|
+
this.reconcile();
|
|
229
|
+
this.scheduleNextReconciliation(intervalMs);
|
|
230
|
+
}, intervalMs);
|
|
231
|
+
}
|
|
232
|
+
verifyCronAndCreateFallbacks(jobs) {
|
|
233
|
+
if (!this.execForVerify)
|
|
234
|
+
return;
|
|
235
|
+
const verifiedLabels = this.verifyCronEntries();
|
|
236
|
+
for (const job of jobs) {
|
|
237
|
+
const label = `bot.ouro.${job.agent}.${job.taskId}`;
|
|
238
|
+
const isVerified = this.platform === "darwin"
|
|
239
|
+
? verifiedLabels.has(label)
|
|
240
|
+
: verifiedLabels.has(job.taskId);
|
|
241
|
+
if (!isVerified) {
|
|
242
|
+
(0, runtime_1.emitNervesEvent)({
|
|
243
|
+
component: "daemon",
|
|
244
|
+
event: "daemon.habit_cron_verification_failed",
|
|
245
|
+
message: `cron verification failed for habit: ${job.taskId}`,
|
|
246
|
+
meta: { habitName: job.taskId, agent: job.agent, label },
|
|
247
|
+
});
|
|
248
|
+
// Parse cadence from the original habit file for timer interval
|
|
249
|
+
const habitFile = this.getHabitFile(job.taskId);
|
|
250
|
+
const ms = habitFile?.cadence ? (0, cadence_1.parseCadenceToMs)(habitFile.cadence) : null;
|
|
251
|
+
if (ms !== null) {
|
|
252
|
+
this.createTimerFallback(job.taskId, ms);
|
|
253
|
+
}
|
|
254
|
+
this.degradedHabitNames.set(job.taskId, "cron registration failed — using timer fallback");
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
verifyCronEntries() {
|
|
259
|
+
const verified = new Set();
|
|
260
|
+
try {
|
|
261
|
+
if (this.platform === "darwin") {
|
|
262
|
+
const output = this.execForVerify("launchctl list");
|
|
263
|
+
const lines = output.split("\n");
|
|
264
|
+
for (const line of lines) {
|
|
265
|
+
const match = line.match(/bot\.ouro\.\S+\.\S+/);
|
|
266
|
+
if (match) {
|
|
267
|
+
verified.add(match[0]);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
else {
|
|
272
|
+
const output = this.execForVerify("crontab -l");
|
|
273
|
+
const lines = output.split("\n");
|
|
274
|
+
for (const line of lines) {
|
|
275
|
+
const match = line.match(/ouro poke \S+ --habit (\S+)/);
|
|
276
|
+
if (match) {
|
|
277
|
+
verified.add(match[1]);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
catch {
|
|
283
|
+
// Verification command failed — return empty set (all habits unverified)
|
|
284
|
+
}
|
|
285
|
+
return verified;
|
|
286
|
+
}
|
|
287
|
+
createTimerFallback(habitName, cadenceMs) {
|
|
288
|
+
const schedule = () => {
|
|
289
|
+
const timer = setTimeout(() => {
|
|
290
|
+
this.onHabitFire(habitName);
|
|
291
|
+
schedule();
|
|
292
|
+
}, cadenceMs);
|
|
293
|
+
this.timerFallbacks.set(habitName, timer);
|
|
294
|
+
};
|
|
295
|
+
schedule();
|
|
296
|
+
}
|
|
297
|
+
clearAllTimerFallbacks() {
|
|
298
|
+
for (const timer of this.timerFallbacks.values()) {
|
|
299
|
+
clearTimeout(timer);
|
|
300
|
+
}
|
|
301
|
+
this.timerFallbacks.clear();
|
|
302
|
+
this.degradedHabitNames.clear();
|
|
303
|
+
}
|
|
190
304
|
scanHabits() {
|
|
191
305
|
let files;
|
|
192
306
|
try {
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.SAFE_MODE_OVERRIDE_FILENAME = void 0;
|
|
37
|
+
exports.detectSafeMode = detectSafeMode;
|
|
38
|
+
exports.pruneOldCrashes = pruneOldCrashes;
|
|
39
|
+
const fs = __importStar(require("fs"));
|
|
40
|
+
const path = __importStar(require("path"));
|
|
41
|
+
const runtime_1 = require("../../nerves/runtime");
|
|
42
|
+
exports.SAFE_MODE_OVERRIDE_FILENAME = "safe-mode-override.json";
|
|
43
|
+
/** 3+ crashes within this window triggers safe mode */
|
|
44
|
+
const CRASH_WINDOW_MS = 5 * 60 * 1000; // 5 minutes
|
|
45
|
+
const CRASH_THRESHOLD = 3;
|
|
46
|
+
/**
|
|
47
|
+
* Reads crash history from the tombstone file and determines if safe mode should be active.
|
|
48
|
+
* Returns active if 3+ crashes occurred within 5 minutes.
|
|
49
|
+
*
|
|
50
|
+
* Safe mode is bypassed when:
|
|
51
|
+
* - devMode is true
|
|
52
|
+
* - A safe-mode override file exists (written by `ouro up --force`)
|
|
53
|
+
*/
|
|
54
|
+
function detectSafeMode(tombstonePath, options) {
|
|
55
|
+
const inactive = { active: false, reason: "", enteredAt: "" };
|
|
56
|
+
// Dev mode: never enter safe mode
|
|
57
|
+
if (options?.devMode) {
|
|
58
|
+
return inactive;
|
|
59
|
+
}
|
|
60
|
+
// Check for override file (--force)
|
|
61
|
+
const overridePath = path.join(path.dirname(tombstonePath), exports.SAFE_MODE_OVERRIDE_FILENAME);
|
|
62
|
+
try {
|
|
63
|
+
if (fs.existsSync(overridePath)) {
|
|
64
|
+
return inactive;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
// Best-effort check
|
|
69
|
+
}
|
|
70
|
+
// Read tombstone
|
|
71
|
+
let parsed;
|
|
72
|
+
try {
|
|
73
|
+
const raw = fs.readFileSync(tombstonePath, "utf-8");
|
|
74
|
+
parsed = JSON.parse(raw);
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return inactive;
|
|
78
|
+
}
|
|
79
|
+
// Extract recentCrashes
|
|
80
|
+
if (!Array.isArray(parsed.recentCrashes)) {
|
|
81
|
+
return inactive;
|
|
82
|
+
}
|
|
83
|
+
const nowMs = options?.now ? options.now() : Date.now();
|
|
84
|
+
const windowStart = nowMs - CRASH_WINDOW_MS;
|
|
85
|
+
// Filter to valid string timestamps within the crash window
|
|
86
|
+
const recentInWindow = parsed.recentCrashes.filter((entry) => {
|
|
87
|
+
if (typeof entry !== "string")
|
|
88
|
+
return false;
|
|
89
|
+
const ts = new Date(entry).getTime();
|
|
90
|
+
if (isNaN(ts))
|
|
91
|
+
return false;
|
|
92
|
+
return ts >= windowStart;
|
|
93
|
+
});
|
|
94
|
+
if (recentInWindow.length < CRASH_THRESHOLD) {
|
|
95
|
+
return inactive;
|
|
96
|
+
}
|
|
97
|
+
const result = {
|
|
98
|
+
active: true,
|
|
99
|
+
reason: `crash loop detected: ${recentInWindow.length} crashes in last 5 minutes`,
|
|
100
|
+
enteredAt: new Date(nowMs).toISOString(),
|
|
101
|
+
};
|
|
102
|
+
(0, runtime_1.emitNervesEvent)({
|
|
103
|
+
level: "error",
|
|
104
|
+
component: "daemon",
|
|
105
|
+
event: "daemon.safe_mode_entered",
|
|
106
|
+
message: result.reason,
|
|
107
|
+
meta: {
|
|
108
|
+
crashCount: recentInWindow.length,
|
|
109
|
+
windowMs: CRASH_WINDOW_MS,
|
|
110
|
+
tombstonePath,
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
return result;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Prunes crash entries older than 5 minutes from the tombstone's recentCrashes.
|
|
117
|
+
* Also removes the safe-mode override file if present.
|
|
118
|
+
* Called after successful startup (uptime > stability threshold).
|
|
119
|
+
*/
|
|
120
|
+
function pruneOldCrashes(tombstonePath, options) {
|
|
121
|
+
// Remove override file
|
|
122
|
+
const overridePath = path.join(path.dirname(tombstonePath), exports.SAFE_MODE_OVERRIDE_FILENAME);
|
|
123
|
+
try {
|
|
124
|
+
if (fs.existsSync(overridePath)) {
|
|
125
|
+
fs.unlinkSync(overridePath);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
// Best-effort
|
|
130
|
+
}
|
|
131
|
+
// Read existing tombstone
|
|
132
|
+
let parsed;
|
|
133
|
+
try {
|
|
134
|
+
const raw = fs.readFileSync(tombstonePath, "utf-8");
|
|
135
|
+
parsed = JSON.parse(raw);
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
if (!Array.isArray(parsed.recentCrashes)) {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
const nowMs = options?.now ? options.now() : Date.now();
|
|
144
|
+
const windowStart = nowMs - CRASH_WINDOW_MS;
|
|
145
|
+
// Keep only entries within the window
|
|
146
|
+
const pruned = parsed.recentCrashes.filter((entry) => {
|
|
147
|
+
if (typeof entry !== "string")
|
|
148
|
+
return false;
|
|
149
|
+
const ts = new Date(entry).getTime();
|
|
150
|
+
if (isNaN(ts))
|
|
151
|
+
return false;
|
|
152
|
+
return ts >= windowStart;
|
|
153
|
+
});
|
|
154
|
+
parsed.recentCrashes = pruned;
|
|
155
|
+
try {
|
|
156
|
+
fs.writeFileSync(tombstonePath, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
// Best-effort
|
|
160
|
+
}
|
|
161
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createToolActivityCallbacks = createToolActivityCallbacks;
|
|
4
|
+
const tool_description_1 = require("./tool-description");
|
|
5
|
+
const runtime_1 = require("../nerves/runtime");
|
|
6
|
+
function createToolActivityCallbacks(options) {
|
|
7
|
+
(0, runtime_1.emitNervesEvent)({
|
|
8
|
+
component: "engine",
|
|
9
|
+
event: "engine.tool_activity_callbacks_created",
|
|
10
|
+
message: "tool activity callbacks initialized",
|
|
11
|
+
meta: { isDebug: options.isDebug },
|
|
12
|
+
});
|
|
13
|
+
return {
|
|
14
|
+
onToolStart(name, args) {
|
|
15
|
+
const description = (0, tool_description_1.humanReadableToolDescription)(name, args);
|
|
16
|
+
if (description === null)
|
|
17
|
+
return; // hidden tool (settle, rest)
|
|
18
|
+
options.onDescription(description);
|
|
19
|
+
},
|
|
20
|
+
onToolEnd(name, summary, success) {
|
|
21
|
+
if (!success) {
|
|
22
|
+
options.onFailure(`${name} failed: ${summary}`);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
if (options.isDebug) {
|
|
26
|
+
options.onResult(`${name}: ${summary}`);
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
}
|