@ouro.bot/cli 0.1.0-alpha.595 → 0.1.0-alpha.597
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 +12 -0
- package/dist/heart/awaiting/await-alert.js +146 -0
- package/dist/heart/awaiting/await-expiry.js +108 -0
- package/dist/heart/awaiting/await-loader.js +91 -0
- package/dist/heart/awaiting/await-parser.js +141 -0
- package/dist/heart/awaiting/await-runtime-state.js +97 -0
- package/dist/heart/awaiting/await-scheduler.js +377 -0
- package/dist/heart/commitments.js +35 -4
- package/dist/heart/daemon/cli-parse.js +8 -1
- package/dist/heart/daemon/daemon-entry.js +98 -0
- package/dist/heart/daemon/daemon.js +7 -0
- package/dist/mind/prompt.js +13 -2
- package/dist/repertoire/tools-awaiting.js +360 -0
- package/dist/repertoire/tools-base.js +4 -0
- package/dist/repertoire/tools-obligations.js +142 -0
- package/dist/senses/await-turn-message.js +58 -0
- package/dist/senses/inner-dialog-worker.js +13 -3
- package/dist/senses/inner-dialog.js +42 -0
- package/package.json +1 -1
|
@@ -0,0 +1,377 @@
|
|
|
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.AwaitScheduler = exports.AWAIT_CRON_LABEL_PREFIX_MARKER = void 0;
|
|
37
|
+
const path = __importStar(require("path"));
|
|
38
|
+
const runtime_1 = require("../../nerves/runtime");
|
|
39
|
+
const await_parser_1 = require("./await-parser");
|
|
40
|
+
const await_runtime_state_1 = require("./await-runtime-state");
|
|
41
|
+
const cadence_1 = require("../daemon/cadence");
|
|
42
|
+
const WATCH_DEBOUNCE_MS = 200;
|
|
43
|
+
/** Cron-label namespace prefix to avoid collision with habits. */
|
|
44
|
+
exports.AWAIT_CRON_LABEL_PREFIX_MARKER = "await";
|
|
45
|
+
class AwaitScheduler {
|
|
46
|
+
agent;
|
|
47
|
+
awaitsDir;
|
|
48
|
+
osCronManager;
|
|
49
|
+
onAwaitFire;
|
|
50
|
+
onAwaitExpire;
|
|
51
|
+
deps;
|
|
52
|
+
execForVerify;
|
|
53
|
+
platform;
|
|
54
|
+
watcher = null;
|
|
55
|
+
debounceTimer = null;
|
|
56
|
+
parseErrors = [];
|
|
57
|
+
timerFallbacks = new Map();
|
|
58
|
+
degradedAwaitNames = new Map();
|
|
59
|
+
periodicTimer = null;
|
|
60
|
+
constructor(options) {
|
|
61
|
+
this.agent = options.agent;
|
|
62
|
+
this.awaitsDir = options.awaitsDir;
|
|
63
|
+
this.osCronManager = options.osCronManager;
|
|
64
|
+
this.onAwaitFire = options.onAwaitFire;
|
|
65
|
+
this.onAwaitExpire = options.onAwaitExpire;
|
|
66
|
+
this.deps = options.deps;
|
|
67
|
+
this.execForVerify = options.execForVerify;
|
|
68
|
+
this.platform = options.platform ?? process.platform;
|
|
69
|
+
}
|
|
70
|
+
start() {
|
|
71
|
+
(0, runtime_1.emitNervesEvent)({
|
|
72
|
+
component: "daemon",
|
|
73
|
+
event: "daemon.await_scheduler_start",
|
|
74
|
+
message: "await scheduler starting",
|
|
75
|
+
meta: { agent: this.agent, awaitsDir: this.awaitsDir },
|
|
76
|
+
});
|
|
77
|
+
// Ensure the awaits dir exists before scanning + attaching the file watcher.
|
|
78
|
+
// Without this, fs.watch ENOENTs on cold start and the watcher is never installed,
|
|
79
|
+
// so a freshly-filed first-ever await has to wait for the next periodic reconcile.
|
|
80
|
+
this.deps.mkdir(this.awaitsDir);
|
|
81
|
+
const awaits = this.scanAwaits();
|
|
82
|
+
this.expireOverdueByMaxAge(awaits);
|
|
83
|
+
const remaining = awaits.filter((a) => a.status === "pending" && !this.isExpiredByMaxAge(a));
|
|
84
|
+
const jobs = this.buildJobs(remaining);
|
|
85
|
+
this.osCronManager.sync(jobs);
|
|
86
|
+
this.verifyCronAndCreateFallbacks(jobs);
|
|
87
|
+
this.fireOverdueAwaits(remaining);
|
|
88
|
+
}
|
|
89
|
+
reconcile() {
|
|
90
|
+
(0, runtime_1.emitNervesEvent)({
|
|
91
|
+
component: "daemon",
|
|
92
|
+
event: "daemon.await_scheduler_reconcile",
|
|
93
|
+
message: "await scheduler reconciling",
|
|
94
|
+
meta: { agent: this.agent },
|
|
95
|
+
});
|
|
96
|
+
this.clearAllTimerFallbacks();
|
|
97
|
+
const awaits = this.scanAwaits();
|
|
98
|
+
this.expireOverdueByMaxAge(awaits);
|
|
99
|
+
const remaining = awaits.filter((a) => a.status === "pending" && !this.isExpiredByMaxAge(a));
|
|
100
|
+
const jobs = this.buildJobs(remaining);
|
|
101
|
+
this.osCronManager.sync(jobs);
|
|
102
|
+
this.verifyCronAndCreateFallbacks(jobs);
|
|
103
|
+
this.fireOverdueAwaits(remaining);
|
|
104
|
+
}
|
|
105
|
+
stop() {
|
|
106
|
+
(0, runtime_1.emitNervesEvent)({
|
|
107
|
+
component: "daemon",
|
|
108
|
+
event: "daemon.await_scheduler_end",
|
|
109
|
+
message: "await scheduler stopping",
|
|
110
|
+
meta: { agent: this.agent },
|
|
111
|
+
});
|
|
112
|
+
this.stopPeriodicReconciliation();
|
|
113
|
+
this.clearAllTimerFallbacks();
|
|
114
|
+
this.osCronManager.removeAll();
|
|
115
|
+
}
|
|
116
|
+
getParseErrors() {
|
|
117
|
+
return [...this.parseErrors];
|
|
118
|
+
}
|
|
119
|
+
getDegradedAwaits() {
|
|
120
|
+
const out = [];
|
|
121
|
+
for (const [name, reason] of this.degradedAwaitNames)
|
|
122
|
+
out.push({ name, reason });
|
|
123
|
+
return out;
|
|
124
|
+
}
|
|
125
|
+
getAwaitFile(name) {
|
|
126
|
+
const filePath = path.join(this.awaitsDir, `${name}.md`);
|
|
127
|
+
try {
|
|
128
|
+
const content = this.deps.readFile(filePath, "utf-8");
|
|
129
|
+
return (0, await_runtime_state_1.applyAwaitRuntimeState)(path.dirname(this.awaitsDir), (0, await_parser_1.parseAwaitFile)(content, filePath));
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
watchForChanges() {
|
|
136
|
+
const watchFn = this.deps.watch;
|
|
137
|
+
if (!watchFn)
|
|
138
|
+
return;
|
|
139
|
+
try {
|
|
140
|
+
this.watcher = watchFn(this.awaitsDir, (_event, _filename) => {
|
|
141
|
+
if (this.debounceTimer !== null)
|
|
142
|
+
clearTimeout(this.debounceTimer);
|
|
143
|
+
this.debounceTimer = setTimeout(() => {
|
|
144
|
+
this.debounceTimer = null;
|
|
145
|
+
this.reconcile();
|
|
146
|
+
}, WATCH_DEBOUNCE_MS);
|
|
147
|
+
});
|
|
148
|
+
/* v8 ignore start — ENOENT catch requires real missing directory @preserve */
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
// awaits dir may not exist yet — skip watching silently
|
|
152
|
+
}
|
|
153
|
+
/* v8 ignore stop */
|
|
154
|
+
}
|
|
155
|
+
stopWatching() {
|
|
156
|
+
if (this.debounceTimer !== null) {
|
|
157
|
+
clearTimeout(this.debounceTimer);
|
|
158
|
+
this.debounceTimer = null;
|
|
159
|
+
}
|
|
160
|
+
if (this.watcher !== null) {
|
|
161
|
+
this.watcher.close();
|
|
162
|
+
this.watcher = null;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
static DEFAULT_PERIODIC_INTERVAL_MS = 300_000;
|
|
166
|
+
static INITIAL_RECONCILIATION_DELAY_MS = 30_000;
|
|
167
|
+
startPeriodicReconciliation(intervalMs) {
|
|
168
|
+
const interval = intervalMs ?? AwaitScheduler.DEFAULT_PERIODIC_INTERVAL_MS;
|
|
169
|
+
this.periodicTimer = setTimeout(() => {
|
|
170
|
+
this.reconcile();
|
|
171
|
+
this.scheduleNextReconciliation(interval);
|
|
172
|
+
}, AwaitScheduler.INITIAL_RECONCILIATION_DELAY_MS);
|
|
173
|
+
}
|
|
174
|
+
stopPeriodicReconciliation() {
|
|
175
|
+
if (this.periodicTimer !== null) {
|
|
176
|
+
clearTimeout(this.periodicTimer);
|
|
177
|
+
this.periodicTimer = null;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
scheduleNextReconciliation(intervalMs) {
|
|
181
|
+
this.periodicTimer = setTimeout(() => {
|
|
182
|
+
this.reconcile();
|
|
183
|
+
this.scheduleNextReconciliation(intervalMs);
|
|
184
|
+
}, intervalMs);
|
|
185
|
+
}
|
|
186
|
+
fireOverdueAwaits(awaits) {
|
|
187
|
+
for (const a of awaits) {
|
|
188
|
+
if (!a.cadence)
|
|
189
|
+
continue;
|
|
190
|
+
const cadenceMs = (0, cadence_1.parseCadenceToMs)(a.cadence);
|
|
191
|
+
if (cadenceMs === null)
|
|
192
|
+
continue;
|
|
193
|
+
const nowMs = this.deps.now();
|
|
194
|
+
const lastChecked = a.last_checked ?? null;
|
|
195
|
+
if (lastChecked === null) {
|
|
196
|
+
(0, runtime_1.emitNervesEvent)({
|
|
197
|
+
component: "daemon",
|
|
198
|
+
event: "daemon.await_fire",
|
|
199
|
+
message: "firing overdue await (never checked)",
|
|
200
|
+
meta: { awaitName: a.name, agent: this.agent },
|
|
201
|
+
});
|
|
202
|
+
this.onAwaitFire(a.name);
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
const lastCheckedMs = new Date(lastChecked).getTime();
|
|
206
|
+
const elapsed = nowMs - lastCheckedMs;
|
|
207
|
+
if (elapsed >= cadenceMs) {
|
|
208
|
+
(0, runtime_1.emitNervesEvent)({
|
|
209
|
+
component: "daemon",
|
|
210
|
+
event: "daemon.await_fire",
|
|
211
|
+
message: "firing overdue await",
|
|
212
|
+
meta: { awaitName: a.name, agent: this.agent, elapsedMs: elapsed },
|
|
213
|
+
});
|
|
214
|
+
this.onAwaitFire(a.name);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
isExpiredByMaxAge(a) {
|
|
219
|
+
if (a.status !== "pending")
|
|
220
|
+
return false;
|
|
221
|
+
if (!a.max_age || !a.created_at)
|
|
222
|
+
return false;
|
|
223
|
+
const ageMs = (0, cadence_1.parseCadenceToMs)(a.max_age);
|
|
224
|
+
if (ageMs === null)
|
|
225
|
+
return false;
|
|
226
|
+
const createdMs = new Date(a.created_at).getTime();
|
|
227
|
+
if (!Number.isFinite(createdMs))
|
|
228
|
+
return false;
|
|
229
|
+
return this.deps.now() - createdMs >= ageMs;
|
|
230
|
+
}
|
|
231
|
+
expireOverdueByMaxAge(awaits) {
|
|
232
|
+
for (const a of awaits) {
|
|
233
|
+
if (this.isExpiredByMaxAge(a)) {
|
|
234
|
+
(0, runtime_1.emitNervesEvent)({
|
|
235
|
+
component: "daemon",
|
|
236
|
+
event: "daemon.await_expire",
|
|
237
|
+
message: "await max_age elapsed; expiring",
|
|
238
|
+
meta: { awaitName: a.name, agent: this.agent, max_age: a.max_age, created_at: a.created_at },
|
|
239
|
+
});
|
|
240
|
+
this.onAwaitExpire(a.name);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
verifyCronAndCreateFallbacks(jobs) {
|
|
245
|
+
if (!this.execForVerify)
|
|
246
|
+
return;
|
|
247
|
+
const verifiedLabels = this.verifyCronEntries();
|
|
248
|
+
for (const job of jobs) {
|
|
249
|
+
// job.taskId is already namespaced as "await.<name>". The bare name is
|
|
250
|
+
// what we pass to `--await` and what shows up in the crontab regex
|
|
251
|
+
// capture, so we strip the prefix for linux verification.
|
|
252
|
+
const bareName = job.taskId.startsWith(`${exports.AWAIT_CRON_LABEL_PREFIX_MARKER}.`)
|
|
253
|
+
? job.taskId.slice(exports.AWAIT_CRON_LABEL_PREFIX_MARKER.length + 1)
|
|
254
|
+
: /* v8 ignore next -- defensive: buildJobs always namespaces taskId @preserve */
|
|
255
|
+
job.taskId;
|
|
256
|
+
const label = `bot.ouro.${job.agent}.${job.taskId}`;
|
|
257
|
+
const isVerified = this.platform === "darwin"
|
|
258
|
+
? verifiedLabels.has(label)
|
|
259
|
+
: verifiedLabels.has(bareName);
|
|
260
|
+
if (!isVerified) {
|
|
261
|
+
(0, runtime_1.emitNervesEvent)({
|
|
262
|
+
component: "daemon",
|
|
263
|
+
event: "daemon.await_cron_verification_failed",
|
|
264
|
+
message: `cron verification failed for await: ${bareName}`,
|
|
265
|
+
meta: { awaitName: bareName, agent: job.agent, label },
|
|
266
|
+
});
|
|
267
|
+
const awaitFile = this.getAwaitFile(bareName);
|
|
268
|
+
const ms = awaitFile?.cadence ? (0, cadence_1.parseCadenceToMs)(awaitFile.cadence) : null;
|
|
269
|
+
if (ms !== null)
|
|
270
|
+
this.createTimerFallback(bareName, ms);
|
|
271
|
+
this.degradedAwaitNames.set(bareName, "cron registration failed — using timer fallback");
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
verifyCronEntries() {
|
|
276
|
+
const verified = new Set();
|
|
277
|
+
try {
|
|
278
|
+
if (this.platform === "darwin") {
|
|
279
|
+
const output = this.execForVerify("launchctl list");
|
|
280
|
+
const lines = output.split("\n");
|
|
281
|
+
for (const line of lines) {
|
|
282
|
+
const match = line.match(/bot\.ouro\.\S+\.await\.\S+/);
|
|
283
|
+
if (match)
|
|
284
|
+
verified.add(match[0]);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
else {
|
|
288
|
+
const output = this.execForVerify("crontab -l");
|
|
289
|
+
const lines = output.split("\n");
|
|
290
|
+
for (const line of lines) {
|
|
291
|
+
const match = line.match(/ouro poke \S+ --await (\S+)/);
|
|
292
|
+
if (match)
|
|
293
|
+
verified.add(match[1]);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
catch {
|
|
298
|
+
// best-effort
|
|
299
|
+
}
|
|
300
|
+
return verified;
|
|
301
|
+
}
|
|
302
|
+
createTimerFallback(awaitName, cadenceMs) {
|
|
303
|
+
const schedule = () => {
|
|
304
|
+
const timer = setTimeout(() => {
|
|
305
|
+
this.onAwaitFire(awaitName);
|
|
306
|
+
schedule();
|
|
307
|
+
}, cadenceMs);
|
|
308
|
+
this.timerFallbacks.set(awaitName, timer);
|
|
309
|
+
};
|
|
310
|
+
schedule();
|
|
311
|
+
}
|
|
312
|
+
clearAllTimerFallbacks() {
|
|
313
|
+
for (const timer of this.timerFallbacks.values())
|
|
314
|
+
clearTimeout(timer);
|
|
315
|
+
this.timerFallbacks.clear();
|
|
316
|
+
this.degradedAwaitNames.clear();
|
|
317
|
+
}
|
|
318
|
+
scanAwaits() {
|
|
319
|
+
let files;
|
|
320
|
+
try {
|
|
321
|
+
files = this.deps.readdir(this.awaitsDir);
|
|
322
|
+
}
|
|
323
|
+
catch {
|
|
324
|
+
this.parseErrors = [];
|
|
325
|
+
return [];
|
|
326
|
+
}
|
|
327
|
+
const awaits = [];
|
|
328
|
+
const errors = [];
|
|
329
|
+
for (const file of files) {
|
|
330
|
+
if (!file.endsWith(".md"))
|
|
331
|
+
continue;
|
|
332
|
+
const filePath = path.join(this.awaitsDir, file);
|
|
333
|
+
try {
|
|
334
|
+
const content = this.deps.readFile(filePath, "utf-8");
|
|
335
|
+
const a = (0, await_runtime_state_1.applyAwaitRuntimeState)(path.dirname(this.awaitsDir), (0, await_parser_1.parseAwaitFile)(content, filePath));
|
|
336
|
+
awaits.push(a);
|
|
337
|
+
}
|
|
338
|
+
catch (error) {
|
|
339
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
340
|
+
errors.push({ file, error: errorMessage });
|
|
341
|
+
(0, runtime_1.emitNervesEvent)({
|
|
342
|
+
level: "error",
|
|
343
|
+
component: "daemon",
|
|
344
|
+
event: "daemon.await_parse_error",
|
|
345
|
+
message: "failed to parse await file",
|
|
346
|
+
meta: { file, error: errorMessage, agent: this.agent },
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
this.parseErrors = errors;
|
|
351
|
+
return awaits;
|
|
352
|
+
}
|
|
353
|
+
buildJobs(awaits) {
|
|
354
|
+
const jobs = [];
|
|
355
|
+
for (const a of awaits) {
|
|
356
|
+
/* v8 ignore next -- defensive: callers (start/reconcile) pre-filter to pending awaits @preserve */
|
|
357
|
+
if (a.status !== "pending")
|
|
358
|
+
continue;
|
|
359
|
+
if (!a.cadence)
|
|
360
|
+
continue;
|
|
361
|
+
const cronSchedule = (0, cadence_1.parseCadenceToCron)(a.cadence);
|
|
362
|
+
if (cronSchedule === null)
|
|
363
|
+
continue;
|
|
364
|
+
jobs.push({
|
|
365
|
+
id: `${this.agent}:${exports.AWAIT_CRON_LABEL_PREFIX_MARKER}.${a.name}:cadence`,
|
|
366
|
+
agent: this.agent,
|
|
367
|
+
taskId: `${exports.AWAIT_CRON_LABEL_PREFIX_MARKER}.${a.name}`,
|
|
368
|
+
schedule: cronSchedule,
|
|
369
|
+
lastRun: a.last_checked ?? null,
|
|
370
|
+
command: `${this.deps.ouroPath} poke ${this.agent} --await ${a.name}`,
|
|
371
|
+
taskPath: path.join(this.awaitsDir, `${a.name}.md`),
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
return jobs;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
exports.AwaitScheduler = AwaitScheduler;
|
|
@@ -15,10 +15,11 @@ function describeActiveObligation(obligation) {
|
|
|
15
15
|
}
|
|
16
16
|
return `i owe ${obligation.origin.friendId}: ${obligation.content} (${statusText})`;
|
|
17
17
|
}
|
|
18
|
-
function deriveCommitments(activeWorkFrame, innerJob, pendingObligations) {
|
|
18
|
+
function deriveCommitments(activeWorkFrame, innerJob, pendingObligations, pendingAwaits) {
|
|
19
19
|
const committedTo = [];
|
|
20
20
|
const completionCriteria = [];
|
|
21
21
|
const safeToIgnore = [];
|
|
22
|
+
const awaiting = pendingAwaits ? [...pendingAwaits] : [];
|
|
22
23
|
// Persistent obligations from the obligation store
|
|
23
24
|
// Sort by status priority: investigating/waiting/updating before pending
|
|
24
25
|
if (pendingObligations && pendingObligations.length > 0) {
|
|
@@ -87,11 +88,29 @@ function deriveCommitments(activeWorkFrame, innerJob, pendingObligations) {
|
|
|
87
88
|
component: "engine",
|
|
88
89
|
event: "engine.commitments_derive",
|
|
89
90
|
message: "derived commitments frame",
|
|
90
|
-
meta: { committedCount: committedTo.length, criteriaCount: completionCriteria.length },
|
|
91
|
+
meta: { committedCount: committedTo.length, criteriaCount: completionCriteria.length, awaitingCount: awaiting.length },
|
|
91
92
|
});
|
|
92
|
-
return { committedTo, completionCriteria, safeToIgnore };
|
|
93
|
+
return { committedTo, completionCriteria, safeToIgnore, awaiting };
|
|
93
94
|
}
|
|
94
|
-
function
|
|
95
|
+
function formatRelativeAge(lastCheckedAt, now) {
|
|
96
|
+
if (!lastCheckedAt)
|
|
97
|
+
return "never checked";
|
|
98
|
+
const lastMs = new Date(lastCheckedAt).getTime();
|
|
99
|
+
if (!Number.isFinite(lastMs))
|
|
100
|
+
return "never checked";
|
|
101
|
+
const elapsedMs = now().getTime() - lastMs;
|
|
102
|
+
if (elapsedMs < 60_000)
|
|
103
|
+
return "<1m ago";
|
|
104
|
+
const minutes = Math.floor(elapsedMs / 60_000);
|
|
105
|
+
if (minutes < 60)
|
|
106
|
+
return `${minutes}m ago`;
|
|
107
|
+
const hours = Math.floor(minutes / 60);
|
|
108
|
+
if (hours < 24)
|
|
109
|
+
return `${hours}h ago`;
|
|
110
|
+
const days = Math.floor(hours / 24);
|
|
111
|
+
return `${days}d ago`;
|
|
112
|
+
}
|
|
113
|
+
function formatCommitments(commitments, now = () => new Date()) {
|
|
95
114
|
const sections = [];
|
|
96
115
|
if (commitments.committedTo.length === 0) {
|
|
97
116
|
sections.push("i'm not holding anything specific right now. i'm free to be present.");
|
|
@@ -107,5 +126,17 @@ function formatCommitments(commitments) {
|
|
|
107
126
|
sections.push("");
|
|
108
127
|
sections.push("## what i can let go of");
|
|
109
128
|
sections.push(commitments.safeToIgnore.map((c) => `- ${c}`).join("\n"));
|
|
129
|
+
const awaiting = commitments.awaiting ?? [];
|
|
130
|
+
if (awaiting.length > 0) {
|
|
131
|
+
sections.push("");
|
|
132
|
+
sections.push("## what i'm waiting on");
|
|
133
|
+
for (const a of awaiting) {
|
|
134
|
+
sections.push(`- ${a.name}: ${a.condition}`);
|
|
135
|
+
const obs = a.lastObservation && a.lastObservation.trim().length > 0
|
|
136
|
+
? `: "${a.lastObservation.trim()}"`
|
|
137
|
+
: "";
|
|
138
|
+
sections.push(` (checked ${a.checkedCount}x, last ${formatRelativeAge(a.lastCheckedAt, now)}${obs})`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
110
141
|
return sections.join("\n");
|
|
111
142
|
}
|
|
@@ -179,6 +179,7 @@ function parsePokeCommand(args) {
|
|
|
179
179
|
throw new Error(`Usage\n${usage()}`);
|
|
180
180
|
let taskId;
|
|
181
181
|
let habitName;
|
|
182
|
+
let awaitName;
|
|
182
183
|
for (let i = 1; i < args.length; i += 1) {
|
|
183
184
|
if (args[i] === "--task") {
|
|
184
185
|
taskId = args[i + 1];
|
|
@@ -188,8 +189,14 @@ function parsePokeCommand(args) {
|
|
|
188
189
|
habitName = args[i + 1];
|
|
189
190
|
i += 1;
|
|
190
191
|
}
|
|
192
|
+
if (args[i] === "--await") {
|
|
193
|
+
awaitName = args[i + 1];
|
|
194
|
+
i += 1;
|
|
195
|
+
}
|
|
191
196
|
}
|
|
192
|
-
// --
|
|
197
|
+
// Priority order: --await > --habit > --task
|
|
198
|
+
if (awaitName)
|
|
199
|
+
return { kind: "await.poke", agent, awaitName };
|
|
193
200
|
if (habitName)
|
|
194
201
|
return { kind: "habit.poke", agent, habitName };
|
|
195
202
|
if (!taskId)
|
|
@@ -52,6 +52,8 @@ const identity_1 = require("../identity");
|
|
|
52
52
|
const runtime_mode_1 = require("./runtime-mode");
|
|
53
53
|
const habit_scheduler_1 = require("../habits/habit-scheduler");
|
|
54
54
|
const habit_migration_1 = require("../habits/habit-migration");
|
|
55
|
+
const await_scheduler_1 = require("../awaiting/await-scheduler");
|
|
56
|
+
const await_expiry_1 = require("../awaiting/await-expiry");
|
|
55
57
|
const os_cron_deps_1 = require("./os-cron-deps");
|
|
56
58
|
const os_cron_1 = require("./os-cron");
|
|
57
59
|
const daemon_tombstone_1 = require("./daemon-tombstone");
|
|
@@ -173,6 +175,7 @@ const healthMonitor = new health_monitor_1.HealthMonitor({
|
|
|
173
175
|
},
|
|
174
176
|
});
|
|
175
177
|
const habitSchedulers = [];
|
|
178
|
+
const awaitSchedulers = [];
|
|
176
179
|
let entryRuntimeStopping = false;
|
|
177
180
|
let stopCommandExitScheduled = false;
|
|
178
181
|
function stopEntryRuntime() {
|
|
@@ -183,6 +186,10 @@ function stopEntryRuntime() {
|
|
|
183
186
|
s.stopWatching();
|
|
184
187
|
s.stop();
|
|
185
188
|
}
|
|
189
|
+
for (const s of awaitSchedulers) {
|
|
190
|
+
s.stopWatching();
|
|
191
|
+
s.stop();
|
|
192
|
+
}
|
|
186
193
|
healthMonitor.stopPeriodicChecks();
|
|
187
194
|
}
|
|
188
195
|
function scheduleCleanProcessExitAfterStopCommand() {
|
|
@@ -314,6 +321,16 @@ function emitHabitSetupError(agent, error) {
|
|
|
314
321
|
meta: { agent, error: normalized.message },
|
|
315
322
|
});
|
|
316
323
|
}
|
|
324
|
+
function emitAwaitSetupError(agent, error) {
|
|
325
|
+
const normalized = error instanceof Error ? error : new Error(String(error));
|
|
326
|
+
(0, runtime_1.emitNervesEvent)({
|
|
327
|
+
level: "error",
|
|
328
|
+
component: "daemon",
|
|
329
|
+
event: "daemon.await_setup_error",
|
|
330
|
+
message: `await setup failed for agent ${agent}`,
|
|
331
|
+
meta: { agent, error: normalized.message },
|
|
332
|
+
});
|
|
333
|
+
}
|
|
317
334
|
/* v8 ignore start — daemon health writer wiring, tested via daemon-health.test.ts @preserve */
|
|
318
335
|
const healthWriter = new daemon_health_1.DaemonHealthWriter((0, daemon_health_1.getDefaultHealthPath)());
|
|
319
336
|
const healthSink = (0, daemon_health_1.createHealthNervesSink)(healthWriter, buildDaemonHealthState);
|
|
@@ -396,6 +413,87 @@ void daemon.start().then(() => {
|
|
|
396
413
|
guidance: `fix ${agent} habits or cron setup and rerun ouro up to restore habit automation`,
|
|
397
414
|
});
|
|
398
415
|
}
|
|
416
|
+
// Parallel await-condition scheduler. Uses its own OS cron manager so
|
|
417
|
+
// habits and awaits don't share label namespace and stale removals can't
|
|
418
|
+
// collide.
|
|
419
|
+
const awaitsDir = path.join(bundleRoot, "awaiting");
|
|
420
|
+
const awaitDegradedComponent = `awaits:${agent}`;
|
|
421
|
+
try {
|
|
422
|
+
const awaitOsCronManager = new os_cron_1.LaunchdCronManager(osCronDeps);
|
|
423
|
+
const awaitScheduler = new await_scheduler_1.AwaitScheduler({
|
|
424
|
+
agent,
|
|
425
|
+
awaitsDir,
|
|
426
|
+
osCronManager: awaitOsCronManager,
|
|
427
|
+
onAwaitFire: (awaitName) => {
|
|
428
|
+
processManager.sendToAgent(agent, { type: "await", awaitName });
|
|
429
|
+
},
|
|
430
|
+
onAwaitExpire: (awaitName) => {
|
|
431
|
+
void (0, await_expiry_1.archiveAndAlertExpiredAwait)({
|
|
432
|
+
agentRoot: bundleRoot,
|
|
433
|
+
agentName: agent,
|
|
434
|
+
awaitName,
|
|
435
|
+
deliveryDeps: {
|
|
436
|
+
agentName: agent,
|
|
437
|
+
queuePending: () => {
|
|
438
|
+
// Best-effort: queue inner-dialog wake so the agent processes the alert path
|
|
439
|
+
(0, socket_client_1.sendDaemonCommand)(socketPath, { kind: "inner.wake", agent }).catch(() => { });
|
|
440
|
+
},
|
|
441
|
+
},
|
|
442
|
+
}).catch((err) => {
|
|
443
|
+
(0, runtime_1.emitNervesEvent)({
|
|
444
|
+
level: "error",
|
|
445
|
+
component: "daemon",
|
|
446
|
+
event: "daemon.await_expire_error",
|
|
447
|
+
message: "await expiry handler threw",
|
|
448
|
+
meta: { agent, awaitName, error: err instanceof Error ? err.message : String(err) },
|
|
449
|
+
});
|
|
450
|
+
});
|
|
451
|
+
},
|
|
452
|
+
deps: {
|
|
453
|
+
readdir: (dir) => fs.readdirSync(dir),
|
|
454
|
+
readFile: (p, enc) => fs.readFileSync(p, enc),
|
|
455
|
+
existsSync: (p) => fs.existsSync(p),
|
|
456
|
+
mkdir: (dir) => { fs.mkdirSync(dir, { recursive: true }); },
|
|
457
|
+
now: () => Date.now(),
|
|
458
|
+
ouroPath,
|
|
459
|
+
watch: (dir, cb) => fs.watch(dir, cb),
|
|
460
|
+
},
|
|
461
|
+
});
|
|
462
|
+
try {
|
|
463
|
+
awaitScheduler.start();
|
|
464
|
+
awaitScheduler.startPeriodicReconciliation();
|
|
465
|
+
awaitScheduler.watchForChanges();
|
|
466
|
+
awaitSchedulers.push(awaitScheduler);
|
|
467
|
+
}
|
|
468
|
+
catch (error) {
|
|
469
|
+
try {
|
|
470
|
+
awaitScheduler.stopWatching();
|
|
471
|
+
awaitScheduler.stop();
|
|
472
|
+
}
|
|
473
|
+
catch {
|
|
474
|
+
// best-effort cleanup
|
|
475
|
+
}
|
|
476
|
+
emitAwaitSetupError(agent, error);
|
|
477
|
+
recordRecoverableBootstrapFailure({
|
|
478
|
+
agent,
|
|
479
|
+
component: awaitDegradedComponent,
|
|
480
|
+
habitsDir: awaitsDir,
|
|
481
|
+
error,
|
|
482
|
+
guidance: `fix ${agent} awaits or cron setup and rerun ouro up to restore await automation`,
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
catch (err) {
|
|
487
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
488
|
+
emitAwaitSetupError(agent, error);
|
|
489
|
+
recordRecoverableBootstrapFailure({
|
|
490
|
+
agent,
|
|
491
|
+
component: awaitDegradedComponent,
|
|
492
|
+
habitsDir: awaitsDir,
|
|
493
|
+
error,
|
|
494
|
+
guidance: `fix ${agent} awaits or cron setup and rerun ouro up to restore await automation`,
|
|
495
|
+
});
|
|
496
|
+
}
|
|
399
497
|
}
|
|
400
498
|
healthMonitor.startPeriodicChecks(60_000);
|
|
401
499
|
/* v8 ignore start -- startup failure + signal handlers: call process.exit, untestable in vitest @preserve */
|
|
@@ -1199,6 +1199,13 @@ class OuroDaemon {
|
|
|
1199
1199
|
message: `poked habit ${command.habitName} for ${command.agent}`,
|
|
1200
1200
|
};
|
|
1201
1201
|
}
|
|
1202
|
+
case "await.poke": {
|
|
1203
|
+
this.processManager.sendToAgent?.(command.agent, { type: "await", awaitName: command.awaitName });
|
|
1204
|
+
return {
|
|
1205
|
+
ok: true,
|
|
1206
|
+
message: `poked await ${command.awaitName} for ${command.agent}`,
|
|
1207
|
+
};
|
|
1208
|
+
}
|
|
1202
1209
|
case "mcp.list": {
|
|
1203
1210
|
const mcpManager = await (0, mcp_manager_1.getSharedMcpManager)();
|
|
1204
1211
|
if (!mcpManager) {
|
package/dist/mind/prompt.js
CHANGED
|
@@ -80,6 +80,7 @@ const tasks_1 = require("../repertoire/tasks");
|
|
|
80
80
|
const session_activity_1 = require("../heart/session-activity");
|
|
81
81
|
const active_work_1 = require("../heart/active-work");
|
|
82
82
|
const commitments_1 = require("../heart/commitments");
|
|
83
|
+
const await_loader_1 = require("../heart/awaiting/await-loader");
|
|
83
84
|
const obligation_steering_1 = require("./obligation-steering");
|
|
84
85
|
const daemon_health_1 = require("../heart/daemon/daemon-health");
|
|
85
86
|
const scrutiny_1 = require("./scrutiny");
|
|
@@ -979,8 +980,18 @@ function commitmentsSection(options) {
|
|
|
979
980
|
const job = options.activeWorkFrame.inner?.job;
|
|
980
981
|
if (!job)
|
|
981
982
|
return "";
|
|
982
|
-
|
|
983
|
-
if (
|
|
983
|
+
let awaits = options.pendingAwaits ?? [];
|
|
984
|
+
if (!options.pendingAwaits) {
|
|
985
|
+
try {
|
|
986
|
+
awaits = (0, await_loader_1.loadPendingAwaitsForCommitments)((0, identity_1.getAgentRoot)());
|
|
987
|
+
}
|
|
988
|
+
catch {
|
|
989
|
+
// Identity not configured (test/eager-call contexts) — proceed without awaits
|
|
990
|
+
awaits = [];
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
const commitments = (0, commitments_1.deriveCommitments)(options.activeWorkFrame, job, options.activeWorkFrame.pendingObligations, awaits);
|
|
994
|
+
if (commitments.committedTo.length === 0 && awaits.length === 0)
|
|
984
995
|
return "";
|
|
985
996
|
return `## my commitments\n\n${(0, commitments_1.formatCommitments)(commitments)}`;
|
|
986
997
|
}
|