@ouro.bot/cli 0.1.0-alpha.724 → 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 +12 -0
- package/dist/heart/active-work.js +10 -3
- package/dist/heart/core.js +343 -77
- package/dist/heart/daemon/cli-exec.js +72 -14
- 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 +21 -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/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
|
@@ -41,6 +41,7 @@ const habit_runtime_state_1 = require("./habit-runtime-state");
|
|
|
41
41
|
const cadence_1 = require("../daemon/cadence");
|
|
42
42
|
const habit_policy_1 = require("../../rsvp/habit-policy");
|
|
43
43
|
const WATCH_DEBOUNCE_MS = 200;
|
|
44
|
+
const MAX_CRON_VERIFICATION_ATTEMPTS = 3;
|
|
44
45
|
class HabitScheduler {
|
|
45
46
|
agent;
|
|
46
47
|
habitsDir;
|
|
@@ -53,8 +54,10 @@ class HabitScheduler {
|
|
|
53
54
|
debounceTimer = null;
|
|
54
55
|
parseErrors = [];
|
|
55
56
|
timerFallbacks = new Map();
|
|
57
|
+
timerFallbackGenerations = new Map();
|
|
56
58
|
degradedHabitNames = new Map();
|
|
57
59
|
periodicTimer = null;
|
|
60
|
+
habitSnapshots = new Map();
|
|
58
61
|
constructor(options) {
|
|
59
62
|
this.agent = options.agent;
|
|
60
63
|
this.habitsDir = options.habitsDir;
|
|
@@ -71,11 +74,7 @@ class HabitScheduler {
|
|
|
71
74
|
message: "habit scheduler starting",
|
|
72
75
|
meta: { agent: this.agent, habitsDir: this.habitsDir },
|
|
73
76
|
});
|
|
74
|
-
|
|
75
|
-
const jobs = this.buildJobs(habits);
|
|
76
|
-
this.osCronManager.sync(jobs);
|
|
77
|
-
this.verifyCronAndCreateFallbacks(jobs);
|
|
78
|
-
this.fireOverdueHabits(habits);
|
|
77
|
+
this.runSchedulingCycle();
|
|
79
78
|
}
|
|
80
79
|
reconcile() {
|
|
81
80
|
(0, runtime_1.emitNervesEvent)({
|
|
@@ -86,21 +85,36 @@ class HabitScheduler {
|
|
|
86
85
|
});
|
|
87
86
|
// Clear ALL existing timers FIRST to prevent overlap window
|
|
88
87
|
this.clearAllTimerFallbacks();
|
|
89
|
-
|
|
90
|
-
const jobs = this.buildJobs(habits);
|
|
91
|
-
this.osCronManager.sync(jobs);
|
|
92
|
-
this.verifyCronAndCreateFallbacks(jobs);
|
|
93
|
-
this.fireOverdueHabits(habits);
|
|
88
|
+
this.runSchedulingCycle();
|
|
94
89
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
90
|
+
runSchedulingCycle() {
|
|
91
|
+
const scannedHabits = this.scanHabits();
|
|
92
|
+
let state = this.revalidateState({
|
|
93
|
+
habits: scannedHabits,
|
|
94
|
+
jobs: this.buildJobs(scannedHabits),
|
|
95
|
+
});
|
|
96
|
+
if (!this.syncJobsFailClosed(state.jobs))
|
|
97
|
+
return;
|
|
98
|
+
const afterSync = this.revalidateAndCorrectJobs(state);
|
|
99
|
+
if (afterSync === null)
|
|
100
|
+
return;
|
|
101
|
+
state = afterSync;
|
|
102
|
+
const afterVerification = this.verifyCronAndCreateFallbacks(state);
|
|
103
|
+
if (afterVerification === null)
|
|
104
|
+
return;
|
|
105
|
+
state = afterVerification;
|
|
106
|
+
const beforeDispatch = this.revalidateAndCorrectJobs(state);
|
|
107
|
+
if (beforeDispatch === null)
|
|
108
|
+
return;
|
|
109
|
+
this.fireOverdueHabits(beforeDispatch);
|
|
110
|
+
}
|
|
111
|
+
fireOverdueHabits(state) {
|
|
112
|
+
for (const expectedJob of state.jobs) {
|
|
113
|
+
const habit = this.revalidateHabit(expectedJob.taskId);
|
|
114
|
+
if (habit === null)
|
|
102
115
|
continue;
|
|
103
|
-
|
|
116
|
+
const currentJob = this.buildJobs([habit]).find((job) => job.id === expectedJob.id);
|
|
117
|
+
if (!currentJob || !this.jobEqual(expectedJob, currentJob))
|
|
104
118
|
continue;
|
|
105
119
|
const nowMs = this.deps.now();
|
|
106
120
|
const dueState = (0, cadence_1.evaluateCadenceDue)(habit.cadence, habit.lastRun, nowMs);
|
|
@@ -174,14 +188,21 @@ class HabitScheduler {
|
|
|
174
188
|
if (!job) {
|
|
175
189
|
return { ok: false, message: `unknown habit job: ${jobId}` };
|
|
176
190
|
}
|
|
177
|
-
this.
|
|
178
|
-
|
|
191
|
+
const currentHabit = this.revalidateHabit(job.taskId);
|
|
192
|
+
const currentJob = currentHabit === null
|
|
193
|
+
? undefined
|
|
194
|
+
: this.buildJobs([currentHabit]).find((candidate) => candidate.id === jobId);
|
|
195
|
+
if (!currentJob) {
|
|
196
|
+
return { ok: false, message: `unknown habit job: ${jobId}` };
|
|
197
|
+
}
|
|
198
|
+
this.onHabitFire(currentJob.taskId, trigger, {
|
|
199
|
+
occurrenceId: this.jobOccurrenceId(currentJob, trigger),
|
|
179
200
|
});
|
|
180
201
|
(0, runtime_1.emitNervesEvent)({
|
|
181
202
|
component: "daemon",
|
|
182
203
|
event: "daemon.habit_job_triggered",
|
|
183
204
|
message: "habit scheduler job triggered",
|
|
184
|
-
meta: { agent:
|
|
205
|
+
meta: { agent: currentJob.agent, habitName: currentJob.taskId, jobId, trigger },
|
|
185
206
|
});
|
|
186
207
|
return { ok: true, message: `triggered habit ${jobId}` };
|
|
187
208
|
}
|
|
@@ -246,31 +267,69 @@ class HabitScheduler {
|
|
|
246
267
|
this.scheduleNextReconciliation(intervalMs);
|
|
247
268
|
}, intervalMs);
|
|
248
269
|
}
|
|
249
|
-
verifyCronAndCreateFallbacks(
|
|
250
|
-
if (!this.execForVerify)
|
|
251
|
-
return;
|
|
252
|
-
|
|
253
|
-
for (
|
|
254
|
-
const
|
|
255
|
-
const
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
if (!
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
const
|
|
267
|
-
const
|
|
268
|
-
|
|
269
|
-
|
|
270
|
+
verifyCronAndCreateFallbacks(state) {
|
|
271
|
+
if (!this.execForVerify || state.jobs.length === 0)
|
|
272
|
+
return state;
|
|
273
|
+
let currentState = state;
|
|
274
|
+
for (let attempt = 1; attempt <= MAX_CRON_VERIFICATION_ATTEMPTS; attempt += 1) {
|
|
275
|
+
const verifiedLabels = this.verifyCronEntries();
|
|
276
|
+
const revalidatedState = this.revalidateAndCorrectJobs(currentState);
|
|
277
|
+
if (revalidatedState === null)
|
|
278
|
+
return null;
|
|
279
|
+
if (!this.jobsEqual(currentState.jobs, revalidatedState.jobs)) {
|
|
280
|
+
currentState = revalidatedState;
|
|
281
|
+
if (currentState.jobs.length === 0)
|
|
282
|
+
return currentState;
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
currentState = revalidatedState;
|
|
286
|
+
for (const job of currentState.jobs) {
|
|
287
|
+
const label = `bot.ouro.${job.agent}.${job.taskId}`;
|
|
288
|
+
const isVerified = this.platform === "darwin"
|
|
289
|
+
? verifiedLabels.has(label)
|
|
290
|
+
: verifiedLabels.has(job.taskId);
|
|
291
|
+
if (!isVerified) {
|
|
292
|
+
(0, runtime_1.emitNervesEvent)({
|
|
293
|
+
component: "daemon",
|
|
294
|
+
event: "daemon.habit_cron_verification_failed",
|
|
295
|
+
message: `cron verification failed for habit: ${job.taskId}`,
|
|
296
|
+
meta: { habitName: job.taskId, agent: job.agent, label },
|
|
297
|
+
});
|
|
298
|
+
const habitFile = currentState.habits.find((habit) => habit.name === job.taskId);
|
|
299
|
+
const cadence = habitFile.cadence;
|
|
300
|
+
const ms = (0, cadence_1.cadenceFallbackDelayMs)(cadence, this.deps.now());
|
|
301
|
+
if (ms !== null) {
|
|
302
|
+
this.createTimerFallback(job.taskId, cadence, ms);
|
|
303
|
+
this.degradedHabitNames.set(job.taskId, "cron registration failed — using timer fallback");
|
|
304
|
+
}
|
|
305
|
+
else {
|
|
306
|
+
this.degradedHabitNames.set(job.taskId, "cron registration failed — no timer fallback available");
|
|
307
|
+
}
|
|
270
308
|
}
|
|
271
|
-
this.degradedHabitNames.set(job.taskId, "cron registration failed — using timer fallback");
|
|
272
309
|
}
|
|
310
|
+
return currentState;
|
|
311
|
+
}
|
|
312
|
+
this.clearAllTimerFallbacks();
|
|
313
|
+
let cleanupError = null;
|
|
314
|
+
try {
|
|
315
|
+
this.osCronManager.removeAll();
|
|
316
|
+
}
|
|
317
|
+
catch (error) {
|
|
318
|
+
cleanupError = this.errorMessage(error);
|
|
273
319
|
}
|
|
320
|
+
(0, runtime_1.emitNervesEvent)({
|
|
321
|
+
level: "error",
|
|
322
|
+
component: "daemon",
|
|
323
|
+
event: "daemon.habit_cron_verification_unstable",
|
|
324
|
+
message: "habit scheduler cron verification did not stabilize; fail-closed cleanup attempted",
|
|
325
|
+
meta: {
|
|
326
|
+
agent: this.agent,
|
|
327
|
+
attempts: MAX_CRON_VERIFICATION_ATTEMPTS,
|
|
328
|
+
jobCount: currentState.jobs.length,
|
|
329
|
+
cleanupError,
|
|
330
|
+
},
|
|
331
|
+
});
|
|
332
|
+
return null;
|
|
274
333
|
}
|
|
275
334
|
verifyCronEntries() {
|
|
276
335
|
const verified = new Set();
|
|
@@ -302,25 +361,156 @@ class HabitScheduler {
|
|
|
302
361
|
return verified;
|
|
303
362
|
}
|
|
304
363
|
createTimerFallback(habitName, cadence, firstDelayMs) {
|
|
364
|
+
if (this.timerFallbacks.has(habitName))
|
|
365
|
+
this.clearTimerFallback(habitName);
|
|
366
|
+
const generation = this.nextTimerFallbackGeneration(habitName);
|
|
305
367
|
const schedule = () => {
|
|
306
368
|
const delayMs = (0, cadence_1.cadenceFallbackDelayMs)(cadence, this.deps.now()) ?? firstDelayMs;
|
|
307
369
|
const timer = setTimeout(() => {
|
|
370
|
+
const owner = this.timerFallbacks.get(habitName);
|
|
371
|
+
if (!owner || owner.handle !== timer || owner.generation !== generation)
|
|
372
|
+
return;
|
|
373
|
+
const currentHabit = this.revalidateHabit(habitName);
|
|
374
|
+
const currentJob = currentHabit === null
|
|
375
|
+
? undefined
|
|
376
|
+
: this.buildJobs([currentHabit]).find((job) => job.taskId === habitName);
|
|
377
|
+
if (!currentJob || currentHabit?.cadence !== cadence) {
|
|
378
|
+
this.clearTimerFallback(habitName);
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
308
381
|
this.onHabitFire(habitName, "overdue", {
|
|
309
382
|
occurrenceId: this.timerOccurrenceId(habitName, cadence),
|
|
310
383
|
});
|
|
384
|
+
const currentOwner = this.timerFallbacks.get(habitName);
|
|
385
|
+
if (!currentOwner || currentOwner.handle !== timer || currentOwner.generation !== generation)
|
|
386
|
+
return;
|
|
311
387
|
schedule();
|
|
312
388
|
}, delayMs);
|
|
313
|
-
this.timerFallbacks.set(habitName, timer);
|
|
389
|
+
this.timerFallbacks.set(habitName, { handle: timer, cadence, generation });
|
|
314
390
|
};
|
|
315
391
|
schedule();
|
|
316
392
|
}
|
|
317
393
|
clearAllTimerFallbacks() {
|
|
318
|
-
for (const
|
|
319
|
-
|
|
320
|
-
}
|
|
321
|
-
this.timerFallbacks.clear();
|
|
394
|
+
for (const name of [...this.timerFallbacks.keys()])
|
|
395
|
+
this.clearTimerFallback(name);
|
|
322
396
|
this.degradedHabitNames.clear();
|
|
323
397
|
}
|
|
398
|
+
clearTimerFallback(name) {
|
|
399
|
+
const owner = this.timerFallbacks.get(name);
|
|
400
|
+
clearTimeout(owner.handle);
|
|
401
|
+
this.timerFallbacks.delete(name);
|
|
402
|
+
this.degradedHabitNames.delete(name);
|
|
403
|
+
this.nextTimerFallbackGeneration(name);
|
|
404
|
+
}
|
|
405
|
+
nextTimerFallbackGeneration(name) {
|
|
406
|
+
const generation = (this.timerFallbackGenerations.get(name) ?? 0) + 1;
|
|
407
|
+
this.timerFallbackGenerations.set(name, generation);
|
|
408
|
+
return generation;
|
|
409
|
+
}
|
|
410
|
+
revalidateState(state) {
|
|
411
|
+
const habits = [];
|
|
412
|
+
for (const habit of state.habits) {
|
|
413
|
+
const currentHabit = this.revalidateHabit(habit.name);
|
|
414
|
+
if (currentHabit !== null)
|
|
415
|
+
habits.push(currentHabit);
|
|
416
|
+
}
|
|
417
|
+
return { habits, jobs: this.buildJobs(habits) };
|
|
418
|
+
}
|
|
419
|
+
revalidateAndCorrectJobs(state) {
|
|
420
|
+
const currentState = this.revalidateState(state);
|
|
421
|
+
this.pruneTimerFallbacks(state, currentState);
|
|
422
|
+
if (this.jobsEqual(state.jobs, currentState.jobs))
|
|
423
|
+
return currentState;
|
|
424
|
+
return this.syncJobsFailClosed(currentState.jobs) ? currentState : null;
|
|
425
|
+
}
|
|
426
|
+
revalidateHabit(name) {
|
|
427
|
+
const file = `${name}.md`;
|
|
428
|
+
const filePath = path.join(this.habitsDir, file);
|
|
429
|
+
let content;
|
|
430
|
+
try {
|
|
431
|
+
content = this.deps.readFile(filePath, "utf-8");
|
|
432
|
+
}
|
|
433
|
+
catch (error) {
|
|
434
|
+
this.habitSnapshots.delete(name);
|
|
435
|
+
this.recordHabitParseError(file, `habit definition unreadable during scheduler revalidation: ${this.errorMessage(error)}`);
|
|
436
|
+
return null;
|
|
437
|
+
}
|
|
438
|
+
try {
|
|
439
|
+
const cached = this.habitSnapshots.get(name);
|
|
440
|
+
const definition = cached?.content === content
|
|
441
|
+
? cached.definition
|
|
442
|
+
: (0, habit_parser_1.parseHabitFile)(content, filePath);
|
|
443
|
+
this.habitSnapshots.set(name, { content, definition });
|
|
444
|
+
const habit = (0, habit_runtime_state_1.applyHabitRuntimeState)(path.dirname(this.habitsDir), definition);
|
|
445
|
+
this.recordDegradedHabitError(file, habit);
|
|
446
|
+
return habit;
|
|
447
|
+
}
|
|
448
|
+
catch (error) {
|
|
449
|
+
this.habitSnapshots.delete(name);
|
|
450
|
+
this.recordHabitParseError(file, this.errorMessage(error));
|
|
451
|
+
return null;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
syncJobsFailClosed(jobs) {
|
|
455
|
+
try {
|
|
456
|
+
this.osCronManager.sync(jobs);
|
|
457
|
+
return true;
|
|
458
|
+
}
|
|
459
|
+
catch (error) {
|
|
460
|
+
this.clearAllTimerFallbacks();
|
|
461
|
+
let cleanupError = null;
|
|
462
|
+
try {
|
|
463
|
+
this.osCronManager.removeAll();
|
|
464
|
+
}
|
|
465
|
+
catch (cleanupFailure) {
|
|
466
|
+
cleanupError = this.errorMessage(cleanupFailure);
|
|
467
|
+
}
|
|
468
|
+
(0, runtime_1.emitNervesEvent)({
|
|
469
|
+
level: "error",
|
|
470
|
+
component: "daemon",
|
|
471
|
+
event: "daemon.habit_scheduler_sync_error",
|
|
472
|
+
message: "habit scheduler cron synchronization failed; fail-closed cleanup attempted",
|
|
473
|
+
meta: {
|
|
474
|
+
agent: this.agent,
|
|
475
|
+
error: this.errorMessage(error),
|
|
476
|
+
cleanupError,
|
|
477
|
+
jobCount: jobs.length,
|
|
478
|
+
},
|
|
479
|
+
});
|
|
480
|
+
return false;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
pruneTimerFallbacks(previousState, currentState) {
|
|
484
|
+
for (const [name, owner] of this.timerFallbacks) {
|
|
485
|
+
const previousJob = previousState.jobs.find((job) => job.taskId === name);
|
|
486
|
+
const currentJob = currentState.jobs.find((job) => job.taskId === name);
|
|
487
|
+
const currentHabit = currentState.habits.find((habit) => habit.name === name);
|
|
488
|
+
if (previousJob
|
|
489
|
+
&& currentJob
|
|
490
|
+
&& currentHabit?.cadence === owner.cadence
|
|
491
|
+
&& this.jobEqual(previousJob, currentJob))
|
|
492
|
+
continue;
|
|
493
|
+
this.clearTimerFallback(name);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
jobsEqual(left, right) {
|
|
497
|
+
if (left.length !== right.length)
|
|
498
|
+
return false;
|
|
499
|
+
return left.every((job, index) => this.jobEqual(job, right[index]));
|
|
500
|
+
}
|
|
501
|
+
jobEqual(left, right) {
|
|
502
|
+
return right !== undefined
|
|
503
|
+
&& left.id === right.id
|
|
504
|
+
&& left.agent === right.agent
|
|
505
|
+
&& left.taskId === right.taskId
|
|
506
|
+
&& left.schedule === right.schedule
|
|
507
|
+
&& left.lastRun === right.lastRun
|
|
508
|
+
&& left.command === right.command
|
|
509
|
+
&& left.taskPath === right.taskPath;
|
|
510
|
+
}
|
|
511
|
+
errorMessage(error) {
|
|
512
|
+
return error instanceof Error ? error.message : String(error);
|
|
513
|
+
}
|
|
324
514
|
scanHabits() {
|
|
325
515
|
let files;
|
|
326
516
|
try {
|
|
@@ -328,21 +518,36 @@ class HabitScheduler {
|
|
|
328
518
|
}
|
|
329
519
|
catch {
|
|
330
520
|
this.parseErrors = [];
|
|
521
|
+
this.habitSnapshots.clear();
|
|
331
522
|
return [];
|
|
332
523
|
}
|
|
333
524
|
const habits = [];
|
|
334
525
|
const errors = [];
|
|
526
|
+
const snapshots = new Map();
|
|
335
527
|
for (const file of files) {
|
|
336
528
|
if (!file.endsWith(".md"))
|
|
337
529
|
continue;
|
|
338
530
|
const filePath = path.join(this.habitsDir, file);
|
|
339
531
|
try {
|
|
340
532
|
const content = this.deps.readFile(filePath, "utf-8");
|
|
341
|
-
const
|
|
533
|
+
const definition = (0, habit_parser_1.parseHabitFile)(content, filePath);
|
|
534
|
+
const habit = (0, habit_runtime_state_1.applyHabitRuntimeState)(path.dirname(this.habitsDir), definition);
|
|
535
|
+
snapshots.set(habit.name, { content, definition });
|
|
342
536
|
habits.push(habit);
|
|
537
|
+
if (habit.status === "degraded") {
|
|
538
|
+
const error = `habit definition degraded: ${habit.degradedReason}`;
|
|
539
|
+
errors.push({ file, error });
|
|
540
|
+
(0, runtime_1.emitNervesEvent)({
|
|
541
|
+
level: "error",
|
|
542
|
+
component: "daemon",
|
|
543
|
+
event: "daemon.habit_parse_error",
|
|
544
|
+
message: "failed to parse habit file",
|
|
545
|
+
meta: { file, error, agent: this.agent },
|
|
546
|
+
});
|
|
547
|
+
}
|
|
343
548
|
}
|
|
344
549
|
catch (error) {
|
|
345
|
-
const errorMessage =
|
|
550
|
+
const errorMessage = this.errorMessage(error);
|
|
346
551
|
errors.push({ file, error: errorMessage });
|
|
347
552
|
(0, runtime_1.emitNervesEvent)({
|
|
348
553
|
level: "error",
|
|
@@ -358,6 +563,7 @@ class HabitScheduler {
|
|
|
358
563
|
}
|
|
359
564
|
}
|
|
360
565
|
this.parseErrors = errors;
|
|
566
|
+
this.habitSnapshots = snapshots;
|
|
361
567
|
return habits;
|
|
362
568
|
}
|
|
363
569
|
buildJobs(habits) {
|
|
@@ -398,6 +604,18 @@ class HabitScheduler {
|
|
|
398
604
|
this.recordHabitParseError(`${habit.name}.md`, "habit names cannot start with reserved cron namespace 'await.'");
|
|
399
605
|
return true;
|
|
400
606
|
}
|
|
607
|
+
recordDegradedHabitError(file, habit) {
|
|
608
|
+
const prefix = "habit definition degraded: ";
|
|
609
|
+
if (habit.status !== "degraded") {
|
|
610
|
+
this.parseErrors = this.parseErrors.filter((entry) => entry.file !== file || !entry.error.startsWith(prefix));
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
const error = `${prefix}${habit.degradedReason}`;
|
|
614
|
+
if (this.parseErrors.some((entry) => entry.file === file && entry.error === error))
|
|
615
|
+
return;
|
|
616
|
+
this.parseErrors = this.parseErrors.filter((entry) => entry.file !== file || !entry.error.startsWith(prefix));
|
|
617
|
+
this.recordHabitParseError(file, error);
|
|
618
|
+
}
|
|
401
619
|
recordHabitParseError(file, error) {
|
|
402
620
|
if (this.parseErrors.some((existing) => existing.file === file && existing.error === error))
|
|
403
621
|
return;
|
|
@@ -254,6 +254,22 @@ function parseExtraRouteSpec(spec) {
|
|
|
254
254
|
return { recipient: parts[0], channel: parts[1], key: parts[2] };
|
|
255
255
|
}
|
|
256
256
|
async function normalizeHabitPermissionEnvelope(habit, options) {
|
|
257
|
+
if (habit.status !== "active") {
|
|
258
|
+
const envelope = {
|
|
259
|
+
schemaVersion: 1,
|
|
260
|
+
canMessageOutward: false,
|
|
261
|
+
returnRoutes: [],
|
|
262
|
+
deniedTools: ["send_message", "surface"],
|
|
263
|
+
warnings: [`habit status ${habit.status} is non-executable`],
|
|
264
|
+
};
|
|
265
|
+
(0, runtime_1.emitNervesEvent)({
|
|
266
|
+
component: "daemon",
|
|
267
|
+
event: "daemon.habit_permission_envelope_normalized",
|
|
268
|
+
message: "habit permission envelope normalized",
|
|
269
|
+
meta: { agentRoot: options.agentRoot, habitName: habit.name, routes: 0, canMessageOutward: false },
|
|
270
|
+
});
|
|
271
|
+
return envelope;
|
|
272
|
+
}
|
|
257
273
|
const returnRoutes = [];
|
|
258
274
|
const warnings = [];
|
|
259
275
|
if (habit.surface.family) {
|
|
@@ -44,6 +44,7 @@ const runtime_1 = require("../../nerves/runtime");
|
|
|
44
44
|
const auth_flow_1 = require("../auth/auth-flow");
|
|
45
45
|
const provider_models_1 = require("../provider-models");
|
|
46
46
|
const habit_parser_1 = require("../habits/habit-parser");
|
|
47
|
+
const habit_lifecycle_1 = require("../habits/habit-lifecycle");
|
|
47
48
|
const bundle_manifest_1 = require("../../mind/bundle-manifest");
|
|
48
49
|
const record_paths_1 = require("../../mind/record-paths");
|
|
49
50
|
const hatch_specialist_1 = require("./hatch-specialist");
|
|
@@ -77,16 +78,17 @@ function writeReadme(dir, purpose) {
|
|
|
77
78
|
}
|
|
78
79
|
}
|
|
79
80
|
function writeHeartbeatHabit(bundleRoot, now) {
|
|
80
|
-
const habitsDir = path.join(bundleRoot, "habits");
|
|
81
|
-
fs.mkdirSync(habitsDir, { recursive: true });
|
|
82
|
-
const filePath = path.join(habitsDir, "heartbeat.md");
|
|
83
81
|
const content = (0, habit_parser_1.renderHabitFile)({
|
|
84
82
|
title: "Heartbeat check-in",
|
|
85
83
|
cadence: "30m",
|
|
86
84
|
status: "active",
|
|
87
85
|
created: now.toISOString(),
|
|
88
86
|
}, "Run a lightweight heartbeat cycle. Review task board and inbox.\nCheck on pending obligations. Write important durable outputs to Arc or Desk record.");
|
|
89
|
-
|
|
87
|
+
(0, habit_lifecycle_1.publishNewHabitDefinition)({
|
|
88
|
+
agentRoot: bundleRoot,
|
|
89
|
+
habitId: "heartbeat",
|
|
90
|
+
bytes: content,
|
|
91
|
+
});
|
|
90
92
|
}
|
|
91
93
|
function writeFriendImprint(bundleRoot, humanName, now) {
|
|
92
94
|
const friendsDir = path.join(bundleRoot, "friends");
|
|
@@ -568,38 +568,37 @@ function readHabitView(agentRoot, options = {}) {
|
|
|
568
568
|
for (const file of (0, shared_1.safeReaddir)(habitsDir)) {
|
|
569
569
|
if (!file.endsWith(".md"))
|
|
570
570
|
continue;
|
|
571
|
+
const filePath = path.join(habitsDir, file);
|
|
572
|
+
let habit;
|
|
571
573
|
try {
|
|
572
|
-
const filePath = path.join(habitsDir, file);
|
|
573
574
|
const raw = fs.readFileSync(filePath, "utf-8");
|
|
574
|
-
|
|
575
|
-
continue;
|
|
576
|
-
const habit = (0, habit_runtime_state_1.applyHabitRuntimeState)(agentRoot, (0, habit_parser_1.parseHabitFile)(raw, filePath));
|
|
577
|
-
const cadenceMs = parseCadenceMs(habit.cadence);
|
|
578
|
-
let isOverdue = false;
|
|
579
|
-
let overdueMs = null;
|
|
580
|
-
if (habit.status === "active" && habit.lastRun && cadenceMs) {
|
|
581
|
-
const elapsed = now.getTime() - Date.parse(habit.lastRun);
|
|
582
|
-
if (elapsed > cadenceMs) {
|
|
583
|
-
isOverdue = true;
|
|
584
|
-
overdueMs = elapsed - cadenceMs;
|
|
585
|
-
}
|
|
586
|
-
}
|
|
587
|
-
items.push({
|
|
588
|
-
name: habit.name,
|
|
589
|
-
title: habit.title,
|
|
590
|
-
cadence: habit.cadence,
|
|
591
|
-
status: habit.status,
|
|
592
|
-
lastRun: habit.lastRun,
|
|
593
|
-
bodyExcerpt: (0, shared_1.truncateExcerpt)(habit.body, 120),
|
|
594
|
-
isDegraded: false,
|
|
595
|
-
degradedReason: null,
|
|
596
|
-
isOverdue,
|
|
597
|
-
overdueMs,
|
|
598
|
-
});
|
|
575
|
+
habit = (0, habit_runtime_state_1.applyHabitRuntimeState)(agentRoot, (0, habit_parser_1.parseHabitFile)(raw, filePath));
|
|
599
576
|
}
|
|
600
577
|
catch {
|
|
601
|
-
|
|
578
|
+
habit = (0, habit_parser_1.createDegradedHabitFile)(filePath, "read_error");
|
|
602
579
|
}
|
|
580
|
+
const cadenceMs = parseCadenceMs(habit.cadence);
|
|
581
|
+
let isOverdue = false;
|
|
582
|
+
let overdueMs = null;
|
|
583
|
+
if (habit.status === "active" && habit.lastRun && cadenceMs) {
|
|
584
|
+
const elapsed = now.getTime() - Date.parse(habit.lastRun);
|
|
585
|
+
if (elapsed > cadenceMs) {
|
|
586
|
+
isOverdue = true;
|
|
587
|
+
overdueMs = elapsed - cadenceMs;
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
items.push({
|
|
591
|
+
name: habit.name,
|
|
592
|
+
title: habit.title,
|
|
593
|
+
cadence: habit.cadence,
|
|
594
|
+
status: habit.status,
|
|
595
|
+
lastRun: habit.lastRun,
|
|
596
|
+
bodyExcerpt: (0, shared_1.truncateExcerpt)(habit.body, 120),
|
|
597
|
+
isDegraded: habit.status === "degraded",
|
|
598
|
+
degradedReason: habit.status === "degraded" ? habit.degradedReason : null,
|
|
599
|
+
isOverdue,
|
|
600
|
+
overdueMs,
|
|
601
|
+
});
|
|
603
602
|
}
|
|
604
603
|
items.sort((a, b) => {
|
|
605
604
|
if (a.isOverdue && !b.isOverdue)
|
|
@@ -1,11 +1,43 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.EXPLICIT_PRIOR_WORK_RESUME_INSTRUCTION = exports.BACKGROUND_ONLY_INSTRUCTION = void 0;
|
|
4
|
+
exports.priorWorkInstruction = priorWorkInstruction;
|
|
5
|
+
exports.labelPriorWorkSurface = labelPriorWorkSurface;
|
|
3
6
|
exports.extractMessageText = extractMessageText;
|
|
4
7
|
exports.extractOrderedListReferents = extractOrderedListReferents;
|
|
5
8
|
exports.buildOrientationFrame = buildOrientationFrame;
|
|
6
9
|
exports.renderOrientationFrame = renderOrientationFrame;
|
|
7
10
|
const runtime_1 = require("../nerves/runtime");
|
|
8
11
|
const structured_output_1 = require("./structured-output");
|
|
12
|
+
exports.BACKGROUND_ONLY_INSTRUCTION = "Background only; do not execute.";
|
|
13
|
+
exports.EXPLICIT_PRIOR_WORK_RESUME_INSTRUCTION = "Prior work explicitly resumed by the current trigger.";
|
|
14
|
+
function priorWorkInstruction(resumePriorWork) {
|
|
15
|
+
return resumePriorWork
|
|
16
|
+
? exports.EXPLICIT_PRIOR_WORK_RESUME_INSTRUCTION
|
|
17
|
+
: exports.BACKGROUND_ONLY_INSTRUCTION;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Put the prior-work authority label directly under a section heading when one
|
|
21
|
+
* exists, otherwise before the surface. This keeps the label adjacent to the
|
|
22
|
+
* exact stale-work surface it governs instead of relying on a distant prompt
|
|
23
|
+
* preamble.
|
|
24
|
+
*/
|
|
25
|
+
function labelPriorWorkSurface(surface, resumePriorWork) {
|
|
26
|
+
if (!surface)
|
|
27
|
+
return "";
|
|
28
|
+
const instruction = priorWorkInstruction(resumePriorWork);
|
|
29
|
+
const inlineBoldHeading = /^(\*\*[^*]+:\*\*)\s+([\s\S]+)$/.exec(surface);
|
|
30
|
+
if (inlineBoldHeading) {
|
|
31
|
+
return `${inlineBoldHeading[1]}\n${instruction}\n${inlineBoldHeading[2]}`;
|
|
32
|
+
}
|
|
33
|
+
const newlineIndex = surface.indexOf("\n");
|
|
34
|
+
const firstLine = newlineIndex === -1 ? surface : surface.slice(0, newlineIndex);
|
|
35
|
+
if (firstLine.startsWith("## ") || /^\*\*[^*]+:\*\*$/.test(firstLine)) {
|
|
36
|
+
const remainder = newlineIndex === -1 ? "" : surface.slice(newlineIndex + 1);
|
|
37
|
+
return [firstLine, instruction, remainder].filter((part) => part.length > 0).join("\n");
|
|
38
|
+
}
|
|
39
|
+
return `${instruction}\n${surface}`;
|
|
40
|
+
}
|
|
9
41
|
function isRecord(value) {
|
|
10
42
|
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
11
43
|
}
|
|
@@ -221,7 +253,7 @@ function buildOrientationFrame(input) {
|
|
|
221
253
|
}
|
|
222
254
|
function renderOrientationFrame(frame) {
|
|
223
255
|
const lines = [
|
|
224
|
-
"##
|
|
256
|
+
"## Current trigger (authoritative)",
|
|
225
257
|
`channel: ${frame.channel}`,
|
|
226
258
|
`action policy: ${frame.actionPolicy.mode}`,
|
|
227
259
|
];
|
|
@@ -240,6 +272,31 @@ function renderOrientationFrame(frame) {
|
|
|
240
272
|
if (frame.source) {
|
|
241
273
|
lines.push("source:");
|
|
242
274
|
lines.push(`- kind: ${frame.source.kind}`);
|
|
275
|
+
if (frame.source.authority === "presentation_only") {
|
|
276
|
+
lines.push("- source authority: presentation only; never tool authority");
|
|
277
|
+
}
|
|
278
|
+
if (frame.source.conversationKind) {
|
|
279
|
+
lines.push(`- conversation kind: ${frame.source.conversationKind}`);
|
|
280
|
+
}
|
|
281
|
+
if (frame.source.event) {
|
|
282
|
+
lines.push(`- event: ${frame.source.event.kind} (${frame.source.event.sourceEventType}; from me: ${String(frame.source.event.fromMe)})`);
|
|
283
|
+
}
|
|
284
|
+
if (frame.source.actor) {
|
|
285
|
+
const actorLabel = frame.source.actor.displayName?.trim() || frame.source.actor.externalId;
|
|
286
|
+
lines.push(`- observed actor: ${actorLabel} [${frame.source.actor.provider}:${frame.source.actor.externalId}]`);
|
|
287
|
+
}
|
|
288
|
+
if (frame.source.participants) {
|
|
289
|
+
for (const participant of frame.source.participants) {
|
|
290
|
+
const participantLabel = participant.displayName?.trim() || participant.externalId;
|
|
291
|
+
lines.push(`- group participant only: ${participantLabel} [${participant.provider}:${participant.externalId}]`);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
if (frame.source.conversationKind === "group") {
|
|
295
|
+
lines.push("- participant membership is not evidence that someone spoke, read, requested, or authored a reaction target.");
|
|
296
|
+
}
|
|
297
|
+
if (frame.source.target) {
|
|
298
|
+
lines.push(`- target: ${frame.source.target.messageGuid} (authorship: ${frame.source.target.authorship})`);
|
|
299
|
+
}
|
|
243
300
|
if (frame.source.lane)
|
|
244
301
|
lines.push(`- lane: ${frame.source.lane}`);
|
|
245
302
|
if (frame.source.defaultReplyTarget)
|
|
@@ -44,6 +44,9 @@ function httpStatus(error) {
|
|
|
44
44
|
function delayForAttempt(policy, attempt) {
|
|
45
45
|
return policy.baseDelayMs * Math.pow(policy.backoffMultiplier, attempt - 1);
|
|
46
46
|
}
|
|
47
|
+
function isRetryable(error) {
|
|
48
|
+
return error.retryable !== false;
|
|
49
|
+
}
|
|
47
50
|
async function runProviderAttempt(input) {
|
|
48
51
|
const policy = normalizePolicy(input.policy);
|
|
49
52
|
const maxAttempts = Math.max(1, Math.floor(policy.maxAttempts));
|
|
@@ -74,7 +77,7 @@ async function runProviderAttempt(input) {
|
|
|
74
77
|
throw caught;
|
|
75
78
|
const error = toError(caught);
|
|
76
79
|
const classification = classify(caught, input.classifyError);
|
|
77
|
-
const willRetry = attempt < maxAttempts;
|
|
80
|
+
const willRetry = isRetryable(error) && attempt < maxAttempts;
|
|
78
81
|
const delayMs = willRetry ? delayForAttempt(policy, attempt) : undefined;
|
|
79
82
|
const record = {
|
|
80
83
|
attempt,
|