@aitne/daemon 0.1.10 → 0.1.11
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/dist/adapters/adapter-watchdog.d.ts +70 -0
- package/dist/adapters/adapter-watchdog.js +115 -0
- package/dist/adapters/discord.d.ts +17 -1
- package/dist/adapters/discord.js +33 -0
- package/dist/adapters/notification-manager.d.ts +27 -1
- package/dist/adapters/notification-manager.js +54 -39
- package/dist/adapters/slack-adapter.d.ts +26 -1
- package/dist/adapters/slack-adapter.js +41 -0
- package/dist/adapters/telegram-adapter.d.ts +18 -1
- package/dist/adapters/telegram-adapter.js +41 -2
- package/dist/adapters/types.d.ts +20 -0
- package/dist/adapters/whatsapp-adapter.d.ts +26 -7
- package/dist/adapters/whatsapp-adapter.js +74 -21
- package/dist/api/env-writer.js +8 -5
- package/dist/api/helpers/agent-errors-registry.d.ts +5 -5
- package/dist/api/helpers/agent-errors-registry.js +5 -5
- package/dist/api/routes/agent.js +33 -12
- package/dist/api/routes/agents/index.js +75 -16
- package/dist/api/routes/agents/views.d.ts +37 -2
- package/dist/api/routes/agents/views.js +64 -2
- package/dist/api/routes/background-task.d.ts +22 -0
- package/dist/api/routes/background-task.js +338 -0
- package/dist/api/routes/browser-history.js +9 -1
- package/dist/api/routes/context/permissions.js +3 -2
- package/dist/api/routes/context/snapshots.js +0 -3
- package/dist/api/routes/context/write.js +3 -17
- package/dist/api/routes/dashboard/config.js +48 -12
- package/dist/api/routes/dashboard/cost-approvals.js +66 -0
- package/dist/api/routes/dashboard/notifications.js +9 -9
- package/dist/api/routes/integrations/crud-patch.js +5 -1
- package/dist/api/routes/integrations-reconcile.js +2 -2
- package/dist/api/routes/notion.d.ts +1 -1
- package/dist/api/routes/observations.js +7 -7
- package/dist/api/routes/obsidian.d.ts +1 -1
- package/dist/api/routes/receipts.js +5 -1
- package/dist/api/routes/setup-migrate.js +1 -1
- package/dist/api/routes/setup.js +1 -1
- package/dist/api/routes/task-flows.d.ts +1 -1
- package/dist/api/routes/task-flows.js +1 -1
- package/dist/api/routes/tuning.d.ts +29 -0
- package/dist/api/routes/tuning.js +304 -0
- package/dist/api/server.d.ts +44 -16
- package/dist/api/server.js +9 -0
- package/dist/bootstrap/adapters.d.ts +19 -0
- package/dist/bootstrap/adapters.js +61 -0
- package/dist/bootstrap/api.d.ts +5 -3
- package/dist/bootstrap/api.js +45 -13
- package/dist/bootstrap/catchup.d.ts +1 -1
- package/dist/bootstrap/catchup.js +11 -11
- package/dist/bootstrap/event-pipeline.d.ts +11 -0
- package/dist/bootstrap/event-pipeline.js +245 -7
- package/dist/bootstrap/observers.js +9 -6
- package/dist/bootstrap/schedule-helpers.d.ts +104 -6
- package/dist/bootstrap/schedule-helpers.js +172 -19
- package/dist/config.js +26 -12
- package/dist/core/agent-core.d.ts +33 -1
- package/dist/core/agent-core.js +36 -1
- package/dist/core/agents/activity-scan-cadence.d.ts +103 -0
- package/dist/core/agents/activity-scan-cadence.js +127 -0
- package/dist/core/agents/agent-route-override.d.ts +53 -0
- package/dist/core/agents/agent-route-override.js +69 -0
- package/dist/core/agents/builtin-registry.d.ts +51 -14
- package/dist/core/agents/builtin-registry.js +92 -15
- package/dist/core/agents/config-gate-reconcile.d.ts +38 -0
- package/dist/core/agents/config-gate-reconcile.js +51 -0
- package/dist/core/agents/cron-substitute.d.ts +1 -1
- package/dist/core/agents/cron-substitute.js +1 -1
- package/dist/core/agents/custom-routine-migration.d.ts +60 -0
- package/dist/core/agents/custom-routine-migration.js +149 -0
- package/dist/core/agents/firing-blocked.d.ts +1 -1
- package/dist/core/agents/hourly-cadence.d.ts +102 -0
- package/dist/core/agents/hourly-cadence.js +126 -0
- package/dist/core/agents/loader-boot.js +23 -0
- package/dist/core/agents/loader.d.ts +19 -0
- package/dist/core/agents/loader.js +34 -2
- package/dist/core/agents/override-merge.d.ts +1 -1
- package/dist/core/agents/override-merge.js +9 -1
- package/dist/core/agents/recurrence-convert.d.ts +1 -1
- package/dist/core/agents/recurrence-convert.js +1 -1
- package/dist/core/agents/recurring-schedule-adapter.js +8 -0
- package/dist/core/alerts.js +6 -6
- package/dist/core/backends/auth-health-monitor.d.ts +2 -2
- package/dist/core/backends/auth-health-monitor.js +1 -1
- package/dist/core/backends/backend-router.d.ts +27 -1
- package/dist/core/backends/backend-router.js +165 -1
- package/dist/core/backends/claude-code-core.d.ts +71 -31
- package/dist/core/backends/claude-code-core.js +282 -54
- package/dist/core/backends/cli-quota-guards.d.ts +29 -1
- package/dist/core/backends/cli-quota-guards.js +40 -5
- package/dist/core/backends/codex-core.d.ts +6 -0
- package/dist/core/backends/codex-core.js +22 -6
- package/dist/core/backends/failure-spend.d.ts +58 -0
- package/dist/core/backends/failure-spend.js +137 -0
- package/dist/core/backends/gemini-cli-core.d.ts +6 -0
- package/dist/core/backends/gemini-cli-core.js +25 -6
- package/dist/core/backends/model-registry.d.ts +1 -1
- package/dist/core/backends/model-registry.js +4 -4
- package/dist/core/backends/opencode-core.d.ts +1 -1
- package/dist/core/backends/opencode-core.js +5 -5
- package/dist/core/backends/plan-presets.js +39 -15
- package/dist/core/bang-commands/commands-cost.js +3 -1
- package/dist/core/bang-commands/commands-report.js +4 -3
- package/dist/core/bang-commands/commands-research.js +4 -1
- package/dist/core/bang-commands/commands-revert-tuning.d.ts +18 -0
- package/dist/core/bang-commands/commands-revert-tuning.js +63 -0
- package/dist/core/bang-commands/commands-stop-start.js +3 -3
- package/dist/core/bang-commands/commands-task-control.d.ts +19 -0
- package/dist/core/bang-commands/commands-task-control.js +147 -0
- package/dist/core/bang-commands/commands-wiki.js +5 -5
- package/dist/core/bang-commands/index.d.ts +2 -0
- package/dist/core/bang-commands/index.js +12 -0
- package/dist/core/bang-commands/registry.d.ts +12 -0
- package/dist/core/browser-history/research-cluster-fanout.d.ts +28 -14
- package/dist/core/browser-history/research-cluster-fanout.js +39 -16
- package/dist/core/channel-timeline.d.ts +5 -1
- package/dist/core/channel-timeline.js +13 -0
- package/dist/core/context/index-reconciler.js +5 -2
- package/dist/core/context/policy-index-reconciler.d.ts +6 -4
- package/dist/core/context/policy-index-runner.js +25 -6
- package/dist/core/context-builder-calendar.js +10 -2
- package/dist/core/context-builder-conversation.d.ts +8 -1
- package/dist/core/context-builder-conversation.js +41 -7
- package/dist/core/context-builder-yesterday.js +4 -3
- package/dist/core/context-builder.d.ts +7 -2
- package/dist/core/context-builder.js +62 -20
- package/dist/core/context-file-serializer.d.ts +1 -1
- package/dist/core/context-file-serializer.js +1 -1
- package/dist/core/context-health.js +2 -2
- package/dist/core/context-paths.d.ts +1 -1
- package/dist/core/context-paths.js +1 -1
- package/dist/core/context-validation/prepare-write.js +1 -1
- package/dist/core/context-validation/routine-rulebook.d.ts +1 -1
- package/dist/core/context-vault-aliases.d.ts +0 -13
- package/dist/core/context-vault-aliases.js +37 -0
- package/dist/core/custom-routines.d.ts +99 -0
- package/dist/core/custom-routines.js +187 -0
- package/dist/core/daemon-api-cli.js +49 -0
- package/dist/core/day-boundary.d.ts +46 -0
- package/dist/core/day-boundary.js +40 -0
- package/dist/core/dispatcher-activity-scan.d.ts +221 -0
- package/dist/core/dispatcher-activity-scan.js +775 -0
- package/dist/core/dispatcher-error-handling.d.ts +6 -11
- package/dist/core/dispatcher-error-handling.js +38 -62
- package/dist/core/dispatcher-hourly-check.js +6 -1
- package/dist/core/dispatcher-message-handler.d.ts +10 -0
- package/dist/core/dispatcher-message-handler.js +17 -0
- package/dist/core/dispatcher-morning-routine.d.ts +6 -6
- package/dist/core/dispatcher-morning-routine.js +13 -13
- package/dist/core/dispatcher-result-processor.d.ts +33 -0
- package/dist/core/dispatcher-result-processor.js +167 -11
- package/dist/core/dispatcher-scheduled-background-task.d.ts +42 -0
- package/dist/core/dispatcher-scheduled-background-task.js +89 -0
- package/dist/core/dispatcher-scheduled-tasks.d.ts +63 -1
- package/dist/core/dispatcher-scheduled-tasks.js +213 -6
- package/dist/core/dispatcher-task-delivery.d.ts +105 -0
- package/dist/core/dispatcher-task-delivery.js +555 -0
- package/dist/core/dispatcher-types.d.ts +48 -9
- package/dist/core/dispatcher-types.js +3 -3
- package/dist/core/dispatcher.d.ts +112 -31
- package/dist/core/dispatcher.js +284 -59
- package/dist/core/dm-freshness-metrics.d.ts +1 -1
- package/dist/core/drift-effects.js +2 -2
- package/dist/core/feedback/consolidation-prep.js +17 -5
- package/dist/core/feedback/eviction-scorer.js +6 -2
- package/dist/core/feedback/lesson-format.js +9 -4
- package/dist/core/feedback/lesson-injection.d.ts +1 -1
- package/dist/core/feedback/lesson-injection.js +17 -2
- package/dist/core/feedback/lesson-store-overview.d.ts +8 -4
- package/dist/core/feedback/lesson-store-overview.js +8 -4
- package/dist/core/feedback/regeneralization-prep.js +29 -16
- package/dist/core/feedback/self-performance-prep.d.ts +186 -0
- package/dist/core/feedback/self-performance-prep.js +541 -0
- package/dist/core/feedback/tuning-actuator.d.ts +198 -0
- package/dist/core/feedback/tuning-actuator.js +432 -0
- package/dist/core/feedback/tuning-recommender.d.ts +247 -0
- package/dist/core/feedback/tuning-recommender.js +580 -0
- package/dist/core/feedback/tuning-revert-monitor.d.ts +90 -0
- package/dist/core/feedback/tuning-revert-monitor.js +213 -0
- package/dist/core/health-monitor.d.ts +6 -0
- package/dist/core/health-monitor.js +1 -1
- package/dist/core/injection-policy.d.ts +4 -4
- package/dist/core/injection-policy.js +4 -4
- package/dist/core/integration-main-backend.js +4 -0
- package/dist/core/management-md.d.ts +2 -2
- package/dist/core/management-md.js +51 -13
- package/dist/core/morning/orchestrator.d.ts +2 -2
- package/dist/core/morning/orchestrator.js +2 -2
- package/dist/core/notification-gate.d.ts +64 -0
- package/dist/core/notification-gate.js +51 -0
- package/dist/core/notification-rate-limit.d.ts +40 -0
- package/dist/core/notification-rate-limit.js +50 -0
- package/dist/core/policy-files.d.ts +1 -1
- package/dist/core/policy-files.js +2 -2
- package/dist/core/pre-pass-freshness.d.ts +4 -4
- package/dist/core/retention.d.ts +5 -0
- package/dist/core/retention.js +20 -4
- package/dist/core/review-context.d.ts +1 -1
- package/dist/core/review-context.js +10 -5
- package/dist/core/roadmap-write-lock.d.ts +2 -1
- package/dist/core/roadmap-write-lock.js +15 -10
- package/dist/core/routine-acquisition-plan.d.ts +47 -1
- package/dist/core/routine-acquisition-plan.js +78 -20
- package/dist/core/routine-fetch-window-retry.js +7 -4
- package/dist/core/routine-fetch-window-runner.d.ts +39 -3
- package/dist/core/routine-fetch-window-runner.js +264 -13
- package/dist/core/routine-windows.d.ts +2 -2
- package/dist/core/routine-windows.js +8 -5
- package/dist/core/scheduler.d.ts +175 -16
- package/dist/core/scheduler.js +559 -102
- package/dist/core/signal-detector.d.ts +12 -0
- package/dist/core/signal-detector.js +53 -9
- package/dist/core/skills-compiler-denied-tools.js +2 -2
- package/dist/core/skills-compiler-skill-index.d.ts +2 -2
- package/dist/core/skills-compiler-skill-index.js +2 -2
- package/dist/core/skills-compiler-variants.d.ts +1 -1
- package/dist/core/skills-compiler-variants.js +8 -0
- package/dist/core/skills-compiler.d.ts +29 -26
- package/dist/core/skills-compiler.js +117 -81
- package/dist/core/skills-manifest.d.ts +37 -0
- package/dist/core/skills-manifest.js +73 -2
- package/dist/core/sleep-inhibitor.d.ts +79 -0
- package/dist/core/sleep-inhibitor.js +132 -0
- package/dist/core/slim-system-prompt-loader.d.ts +77 -0
- package/dist/core/slim-system-prompt-loader.js +141 -0
- package/dist/core/spawn-gates.d.ts +126 -0
- package/dist/core/spawn-gates.js +180 -0
- package/dist/core/today-direct-writer.d.ts +2 -2
- package/dist/core/today-direct-writer.js +1 -1
- package/dist/core/today-write-lock.d.ts +4 -2
- package/dist/core/today-write-lock.js +30 -20
- package/dist/core/wake-detector.d.ts +55 -0
- package/dist/core/wake-detector.js +80 -0
- package/dist/core/wiki/compile-lock.d.ts +1 -1
- package/dist/core/wiki/compile-lock.js +1 -1
- package/dist/core/workdir.js +15 -6
- package/dist/db/activity-scan-signals.d.ts +77 -0
- package/dist/db/activity-scan-signals.js +378 -0
- package/dist/db/agents-store.d.ts +28 -0
- package/dist/db/agents-store.js +62 -0
- package/dist/db/background-task-clarifications-store.d.ts +81 -0
- package/dist/db/background-task-clarifications-store.js +152 -0
- package/dist/db/background-task-store.d.ts +207 -0
- package/dist/db/background-task-store.js +380 -0
- package/dist/db/browser-history-store.d.ts +39 -6
- package/dist/db/browser-history-store.js +51 -7
- package/dist/db/browser-task-clarifications-store.d.ts +12 -0
- package/dist/db/browser-task-clarifications-store.js +35 -5
- package/dist/db/browser-task-store.d.ts +3 -0
- package/dist/db/browser-task-store.js +29 -4
- package/dist/db/deferred-dm.d.ts +86 -0
- package/dist/db/deferred-dm.js +199 -0
- package/dist/db/migrations.js +330 -0
- package/dist/db/observations.d.ts +2 -2
- package/dist/db/observations.js +3 -3
- package/dist/db/schema.js +217 -16
- package/dist/db/voice-transcripts-store.d.ts +1 -1
- package/dist/index.js +86 -29
- package/dist/messaging/browser-task-mcp-notifier.d.ts +12 -70
- package/dist/messaging/browser-task-mcp-notifier.js +30 -151
- package/dist/messaging/browser-task-screenshot-attachment.d.ts +15 -0
- package/dist/messaging/browser-task-screenshot-attachment.js +63 -0
- package/dist/observers/delegated-sync-worker.d.ts +6 -6
- package/dist/observers/delegated-sync-worker.js +10 -10
- package/dist/observers/git-delegated-cron.d.ts +1 -1
- package/dist/observers/git-delegated-cron.js +2 -2
- package/dist/observers/github-poller-classifier.d.ts +3 -3
- package/dist/observers/github-poller-classifier.js +3 -3
- package/dist/observers/imminent-event-scheduler.d.ts +1 -1
- package/dist/observers/imminent-event-scheduler.js +1 -1
- package/dist/observers/mail-poller.d.ts +1 -0
- package/dist/observers/mail-poller.js +42 -3
- package/dist/observers/observation-summarizer/summarizer-client.d.ts +2 -2
- package/dist/observers/observation-summarizer/summarizer-client.js +2 -2
- package/dist/observers/observation-summarizer/worker.d.ts +2 -2
- package/dist/observers/observation-summarizer/worker.js +4 -4
- package/dist/observers/obsidian-watcher.d.ts +1 -1
- package/dist/observers/obsidian-watcher.js +1 -1
- package/dist/safety/agent-write-tracker.d.ts +4 -4
- package/dist/safety/agent-write-tracker.js +4 -4
- package/dist/safety/audit.d.ts +43 -5
- package/dist/safety/audit.js +86 -18
- package/dist/safety/risk-classifier.d.ts +6 -0
- package/dist/safety/risk-classifier.js +75 -11
- package/dist/scheduler/activity-scan-gate.d.ts +86 -0
- package/dist/scheduler/activity-scan-gate.js +132 -0
- package/dist/services/background-task/background-task-budget.d.ts +80 -0
- package/dist/services/background-task/background-task-budget.js +91 -0
- package/dist/services/background-task/background-task-driver.d.ts +105 -0
- package/dist/services/background-task/background-task-driver.js +416 -0
- package/dist/services/background-task/background-task-runner.d.ts +96 -0
- package/dist/services/background-task/background-task-runner.js +673 -0
- package/dist/services/background-task/background-task-tools.d.ts +84 -0
- package/dist/services/background-task/background-task-tools.js +247 -0
- package/dist/services/background-task/background-task-transition-events.d.ts +43 -0
- package/dist/services/background-task/background-task-transition-events.js +54 -0
- package/dist/services/browser-history/automation/egress-denylist.d.ts +1 -1
- package/dist/services/browser-history/automation/egress-denylist.js +16 -6
- package/dist/services/browser-history/managed-chromium/sandbox-launcher.js +0 -1
- package/dist/services/browser-task/browser-task-runner.js +53 -8
- package/dist/services/observations-batch.d.ts +1 -1
- package/dist/services/observations-batch.js +2 -2
- package/dist/settings/runtime-settings.d.ts +38 -11
- package/dist/settings/runtime-settings.js +203 -40
- package/dist/settings/settings-store.js +11 -3
- package/package.json +4 -4
package/dist/core/scheduler.js
CHANGED
|
@@ -7,7 +7,10 @@ import { discardStalePendingSchedules } from "./schedule-maintenance.js";
|
|
|
7
7
|
import { createLogger } from "../logging.js";
|
|
8
8
|
import { reconcileRecurringSchedules } from "../db/recurring-schedules.js";
|
|
9
9
|
import { recordAgentFiringBlocked } from "./agents/firing-blocked.js";
|
|
10
|
-
import {
|
|
10
|
+
import { resolveActivityScanCadence } from "./agents/activity-scan-cadence.js";
|
|
11
|
+
import { getRuntimeWindow } from "../db/agents-store.js";
|
|
12
|
+
import { getDueCatchupRoutines, getRecoverableStalledMorningWake, getStalledMorningRoutineWake, MORNING_MISSED_FIRE_GRACE_MINUTES, morningRoutineRanToday, readMorningRoutineStallThresholdMinutes, shouldCatchUpActivityScan, shouldQueueMissedMorningFire, } from "../bootstrap/schedule-helpers.js";
|
|
13
|
+
import { WakeDetector } from "./wake-detector.js";
|
|
11
14
|
import { readRuntimeState, writeRuntimeState } from "../db/runtime-state.js";
|
|
12
15
|
import { recordProactiveForwardDeliveries } from "./channel-timeline.js";
|
|
13
16
|
import { isInQuietHoursAt, nextQuietHoursEndMs } from "./quiet-hours.js";
|
|
@@ -18,6 +21,47 @@ import { isInQuietHoursAt, nextQuietHoursEndMs } from "./quiet-hours.js";
|
|
|
18
21
|
* agent-day even if the cron tick that owns it runs every minute.
|
|
19
22
|
*/
|
|
20
23
|
const MORNING_ROUTINE_STALL_ALERT_KEY = "morning_routine.stall_alert_day";
|
|
24
|
+
/**
|
|
25
|
+
* Cadence of the morning self-heal tick. The tick is cheap (three indexed
|
|
26
|
+
* SQLite reads in the common healthy case), and 10 minutes bounds the
|
|
27
|
+
* worst-case detection latency for a swallowed 04:00 cron fire at
|
|
28
|
+
* `MORNING_MISSED_FIRE_GRACE_MINUTES + 10`. Deliberately a dedicated
|
|
29
|
+
* interval rather than a rider on the activity-scan cron: the watchdog
|
|
30
|
+
* historically rode that cron and went silently dead the moment an
|
|
31
|
+
* operator set `activityScanEnabled=false` — exactly the configuration
|
|
32
|
+
* where the morning routine has no other safety net.
|
|
33
|
+
*/
|
|
34
|
+
export const MORNING_SELF_HEAL_INTERVAL_MS = 10 * 60_000;
|
|
35
|
+
/**
|
|
36
|
+
* Per-agent-day cap on hung-run recovery flips. Each flip re-runs the
|
|
37
|
+
* morning pipeline (pre-pass + Stage A — real backend spend), so a
|
|
38
|
+
* deterministically-wedging environment must not be retried every
|
|
39
|
+
* stall-threshold window all day. Past the cap the self-heal degrades to
|
|
40
|
+
* alert-only; the owner DM and a daemon restart are the escape hatches.
|
|
41
|
+
*/
|
|
42
|
+
export const MAX_SELFHEAL_REQUEUES_PER_AGENT_DAY = 2;
|
|
43
|
+
/**
|
|
44
|
+
* Missed-fire suppression window after process start. Boot catchup owns
|
|
45
|
+
* stale-today.md recovery at startup and runs the morning routine
|
|
46
|
+
* INLINE — no wake row exists for `shouldQueueMissedMorningFire` to
|
|
47
|
+
* dedup against, and its attempt audit row only appears once the
|
|
48
|
+
* fetch-window pre-pass hands over to Stage A. A pathologically slow
|
|
49
|
+
* pre-pass must not read as a missed fire, so the layer stays quiet
|
|
50
|
+
* until the boot path has long since either produced attempt rows or
|
|
51
|
+
* died (in which case the next tick after the window picks it up).
|
|
52
|
+
*/
|
|
53
|
+
export const MISSED_FIRE_BOOT_SUPPRESSION_MS = 30 * 60_000;
|
|
54
|
+
/**
|
|
55
|
+
* Routine name → built-in Agent slug, for the per-agent enabled gate on the
|
|
56
|
+
* wake catch-up path. Must match the slugs the corresponding cron callbacks
|
|
57
|
+
* pass to `isAgentEnabledForFiring` so a disabled Agent is suppressed
|
|
58
|
+
* identically whether its trigger arrives via cron or via wake catch-up.
|
|
59
|
+
*/
|
|
60
|
+
const WAKE_CATCHUP_AGENT_SLUGS = {
|
|
61
|
+
evening_review: "evening-review",
|
|
62
|
+
weekly_review: "weekly-review",
|
|
63
|
+
monthly_review: "monthly-review",
|
|
64
|
+
};
|
|
21
65
|
const logger = createLogger("scheduler");
|
|
22
66
|
/**
|
|
23
67
|
* True iff `intervalMinutes` cleanly fits inside an hour, so the firing
|
|
@@ -29,7 +73,7 @@ function isDivisorOfHour(intervalMinutes) {
|
|
|
29
73
|
return intervalMinutes >= 1 && intervalMinutes <= 60 && 60 % intervalMinutes === 0;
|
|
30
74
|
}
|
|
31
75
|
/**
|
|
32
|
-
* Build the cron expression that drives the
|
|
76
|
+
* Build the cron expression that drives the activity scan.
|
|
33
77
|
*
|
|
34
78
|
* Two regimes:
|
|
35
79
|
*
|
|
@@ -42,7 +86,7 @@ function isDivisorOfHour(intervalMinutes) {
|
|
|
42
86
|
* 2. **Arbitrary interval** (anything else, e.g. 7, 45, 90, 120, 720,
|
|
43
87
|
* 1440): we emit `"* <hourRange> * * *"` (every minute within active
|
|
44
88
|
* hours). The caller is expected to gate each tick with
|
|
45
|
-
* `
|
|
89
|
+
* `shouldFireActivityScanTickAt(...)`, which anchors the cadence to
|
|
46
90
|
* `activeStartHour` via `((h*60 + m) - activeStartHour*60) %
|
|
47
91
|
* intervalMinutes`. This anchor matters: a midnight-anchored modulo
|
|
48
92
|
* plus `activeStartHour > 0` would silently drop intervals where the
|
|
@@ -54,9 +98,9 @@ function isDivisorOfHour(intervalMinutes) {
|
|
|
54
98
|
*
|
|
55
99
|
* The minute-tick cron does fire 60× per hour even when most ticks are
|
|
56
100
|
* no-ops, but the callback's first action is the modulo check — overhead
|
|
57
|
-
* is negligible compared to the actual
|
|
101
|
+
* is negligible compared to the actual activity-scan work.
|
|
58
102
|
*/
|
|
59
|
-
export function
|
|
103
|
+
export function buildActivityScanCronExpr(intervalMinutes, startHour, endHourExclusive) {
|
|
60
104
|
const endHour = Math.max(startHour, endHourExclusive - 1);
|
|
61
105
|
const hourRange = startHour === endHour ? `${startHour}` : `${startHour}-${endHour}`;
|
|
62
106
|
if (isDivisorOfHour(intervalMinutes)) {
|
|
@@ -86,7 +130,7 @@ export function buildHourlyCronExpr(intervalMinutes, startHour, endHourExclusive
|
|
|
86
130
|
* so the divisor early-return doesn't change behavior — it's just
|
|
87
131
|
* explicit about which path the cron expression itself handles.
|
|
88
132
|
*/
|
|
89
|
-
export function
|
|
133
|
+
export function shouldFireActivityScanTickAt(localHour, localMinute, intervalMinutes, activeStartHour) {
|
|
90
134
|
if (isDivisorOfHour(intervalMinutes))
|
|
91
135
|
return true;
|
|
92
136
|
const minutesSinceMidnight = localHour * 60 + localMinute;
|
|
@@ -163,12 +207,12 @@ export class AgentScheduler {
|
|
|
163
207
|
noFutureTasksWarned = false;
|
|
164
208
|
onDayBoundary = null;
|
|
165
209
|
sendDm = null;
|
|
166
|
-
|
|
210
|
+
onActivityScan = null;
|
|
167
211
|
/**
|
|
168
212
|
* Phase 4 auth probe hook — fired on every hourly cron tick BEFORE
|
|
169
|
-
* `
|
|
213
|
+
* `onActivityScan` so the probe gets a chance to refresh DB cache +
|
|
170
214
|
* emit DMs even when the observation-threshold gate would skip the
|
|
171
|
-
*
|
|
215
|
+
* activity scan itself. The AuthHealthMonitor.checkAll() method owns
|
|
172
216
|
* its own kill-switch and morning-routine skip; the scheduler only
|
|
173
217
|
* applies the same `autonomousGate` short-circuit that protects the
|
|
174
218
|
* other cron callbacks.
|
|
@@ -176,10 +220,24 @@ export class AgentScheduler {
|
|
|
176
220
|
* See `docs/design/09-safety-cost.md` §9.5.4 for the gate
|
|
177
221
|
* ordering: morning-routine → hourly-already-running → auth probe
|
|
178
222
|
* → observation-threshold. Steps 1 + 2 are handled inside
|
|
179
|
-
* `
|
|
180
|
-
* threshold gate inside `
|
|
223
|
+
* `triggerActivityScan`; step 3 is this callback; step 4 is the
|
|
224
|
+
* threshold gate inside `triggerActivityScan`.
|
|
181
225
|
*/
|
|
182
226
|
onAuthProbe = null;
|
|
227
|
+
/**
|
|
228
|
+
* SELF_TUNING_REVIEW_CYCLE_DESIGN.md §3.4 Phase 3 — auto-revert monitor.
|
|
229
|
+
* Piggybacks the hourly cron tick (P2 — zero new scheduled sessions),
|
|
230
|
+
* fired AHEAD of the per-agent enabled gate and the autonomous setup
|
|
231
|
+
* gate so rollback safety survives the owner disabling the activity-scan
|
|
232
|
+
* Agent or a setup-gated daemon; the callback owns its own 1/day
|
|
233
|
+
* throttle, per-entry isolation, and DM emission. Remaining coupling:
|
|
234
|
+
* with `activityScanEnabled=false` this cron is never registered and
|
|
235
|
+
* applied changes stay unverified until the check is re-enabled —
|
|
236
|
+
* acceptable because R1/R3 govern the (now-idle) hourly pipeline
|
|
237
|
+
* itself; an applied R5 (`feedbackLessonMaxBytesGlobal`) change would
|
|
238
|
+
* sit unverified, with `!revert tuning` as the manual escape hatch.
|
|
239
|
+
*/
|
|
240
|
+
onSelfTuningRevertMonitor = null;
|
|
183
241
|
/**
|
|
184
242
|
* B-004 Phase 2a — nightly context-index reconciler callback (§4.1).
|
|
185
243
|
* Fires at 03:45 local (dayBoundaryHour - 15 min) via an internal cron
|
|
@@ -255,6 +313,27 @@ export class AgentScheduler {
|
|
|
255
313
|
nudgeSeq = 0;
|
|
256
314
|
observedSeq = 0;
|
|
257
315
|
sleepWaiter = null;
|
|
316
|
+
/**
|
|
317
|
+
* Detects machine sleep / forward clock jumps and replays the cron
|
|
318
|
+
* triggers the sleep swallowed (node-cron never fires missed ticks).
|
|
319
|
+
* See {@link runWakeCatchup}.
|
|
320
|
+
*/
|
|
321
|
+
wakeDetector = new WakeDetector({
|
|
322
|
+
onWake: (gapMs) => this.runWakeCatchup(gapMs),
|
|
323
|
+
});
|
|
324
|
+
/**
|
|
325
|
+
* Independent self-heal tick for the morning routine (alert + recover +
|
|
326
|
+
* missed-fire re-queue). See {@link runMorningSelfHeal}. Kept off the
|
|
327
|
+
* activity-scan cron on purpose — that cron is operator-disableable.
|
|
328
|
+
*/
|
|
329
|
+
morningSelfHealTimer = null;
|
|
330
|
+
/**
|
|
331
|
+
* Wall-clock instant this scheduler instance was constructed (one
|
|
332
|
+
* instance per daemon process). Drives the missed-fire boot
|
|
333
|
+
* suppression; tests override via cast to simulate a long-lived
|
|
334
|
+
* process.
|
|
335
|
+
*/
|
|
336
|
+
startedAtMs = Date.now();
|
|
258
337
|
constructor(eventBus, db, config) {
|
|
259
338
|
this.eventBus = eventBus;
|
|
260
339
|
this.db = db;
|
|
@@ -274,18 +353,27 @@ export class AgentScheduler {
|
|
|
274
353
|
setSendDmCallback(fn) {
|
|
275
354
|
this.sendDm = fn;
|
|
276
355
|
}
|
|
277
|
-
|
|
278
|
-
this.
|
|
356
|
+
setActivityScanCallback(fn) {
|
|
357
|
+
this.onActivityScan = fn;
|
|
279
358
|
}
|
|
280
359
|
/**
|
|
281
360
|
* Register the Phase 4 auth probe callback. Called on each hourly
|
|
282
|
-
* cron tick BEFORE the
|
|
283
|
-
* the probe continues to run even when the
|
|
361
|
+
* cron tick BEFORE the activity-scan observation threshold gate so
|
|
362
|
+
* the probe continues to run even when the activity scan itself
|
|
284
363
|
* would be skipped for lack of pending observations.
|
|
285
364
|
*/
|
|
286
365
|
setAuthProbeCallback(fn) {
|
|
287
366
|
this.onAuthProbe = fn;
|
|
288
367
|
}
|
|
368
|
+
/**
|
|
369
|
+
* Register the Phase 3 self-tuning auto-revert monitor. Called on each
|
|
370
|
+
* hourly cron tick alongside the auth probe; the monitor throttles
|
|
371
|
+
* itself to one pass per day and is a no-op until the actuator has
|
|
372
|
+
* written ledger entries.
|
|
373
|
+
*/
|
|
374
|
+
setSelfTuningRevertMonitorCallback(fn) {
|
|
375
|
+
this.onSelfTuningRevertMonitor = fn;
|
|
376
|
+
}
|
|
289
377
|
/**
|
|
290
378
|
* Register the context-index reconciler cron callback. Called every
|
|
291
379
|
* night at 03:45 local; the callback is expected to be fire-and-forget
|
|
@@ -380,9 +468,9 @@ export class AgentScheduler {
|
|
|
380
468
|
* (CLAUDE.md "morning_routine wake stall"). When the morning routine
|
|
381
469
|
* never writes an `agent_actions.result='success'` row, the dedup
|
|
382
470
|
* inside `queueMorningRoutineWake` keeps the stuck wake row pinned in
|
|
383
|
-
* `pending`/`running` and the
|
|
471
|
+
* `pending`/`running` and the activity-scan pre-routine gate silently
|
|
384
472
|
* skips every subsequent autonomous tick. The user gets no morning
|
|
385
|
-
* brief, no evening review, no
|
|
473
|
+
* brief, no evening review, no activity scan, and no error — the
|
|
386
474
|
* system is functionally dead until the wake row clears.
|
|
387
475
|
*
|
|
388
476
|
* Detection: oldest `task_type='wake'` row tied to
|
|
@@ -446,7 +534,7 @@ export class AgentScheduler {
|
|
|
446
534
|
return;
|
|
447
535
|
}
|
|
448
536
|
const message = `Aitne: morning routine stalled ${stalled.ageMinutes} min `
|
|
449
|
-
+ `(wake #${stalled.id}, status=${stalled.status}).
|
|
537
|
+
+ `(wake #${stalled.id}, status=${stalled.status}). Activity scan + `
|
|
450
538
|
+ `evening review blocked. Check logs or \`aitne restart\`.`;
|
|
451
539
|
try {
|
|
452
540
|
await this.sendDm(message);
|
|
@@ -470,13 +558,251 @@ export class AgentScheduler {
|
|
|
470
558
|
this.morningStallWatchdogRunning = false;
|
|
471
559
|
}
|
|
472
560
|
}
|
|
561
|
+
/**
|
|
562
|
+
* Self-heal tick for the morning routine. Three layers:
|
|
563
|
+
*
|
|
564
|
+
* 1. **Recover** — a wake row stuck in `running` whose claim
|
|
565
|
+
* (`task_context.claimedAt`) is ≥ stall-threshold minutes old with
|
|
566
|
+
* no success today (machine slept mid-run, the backend stream died)
|
|
567
|
+
* is flipped back to `pending` so the ScheduleWatcher re-claims it.
|
|
568
|
+
* Without this, `queueMorningRoutineWake` dedups into the corpse
|
|
569
|
+
* forever and the day stays frozen until a daemon restart. Runs
|
|
570
|
+
* BEFORE the alert so a stall the self-heal is about to fix doesn't
|
|
571
|
+
* burn the once-per-day DM budget on a misleading "restart the
|
|
572
|
+
* daemon" message; if the re-run wedges too, the next tick still
|
|
573
|
+
* alerts (the row's created_at age keeps growing). Capped at
|
|
574
|
+
* {@link MAX_SELFHEAL_REQUEUES_PER_AGENT_DAY} re-runs per agent-day
|
|
575
|
+
* so a deterministic hang cannot burn backend spend every
|
|
576
|
+
* threshold-window all day. Worst case if the original execution is
|
|
577
|
+
* alive after all: one duplicate morning run, serialized by the
|
|
578
|
+
* today-write-lock.
|
|
579
|
+
* 2. **Alert** — the stall watchdog ({@link checkMorningRoutineStall}).
|
|
580
|
+
* Previously this only ran on activity-scan cron ticks, so
|
|
581
|
+
* `activityScanEnabled=false` silently disabled it; this timer is the
|
|
582
|
+
* guaranteed host now (the cron-tick invocation remains, made safe
|
|
583
|
+
* by the watchdog's mutex + per-day DM dedup).
|
|
584
|
+
* 3. **Missed fire** — no attempt, no wake row, agent-day older than the
|
|
585
|
+
* grace window: the boundary cron tick was swallowed (sleep shorter
|
|
586
|
+
* than the WakeDetector's gap threshold straddling 04:00, or a
|
|
587
|
+
* detector failure) — open the day exactly the way the cron and the
|
|
588
|
+
* wake catch-up do (day-boundary callback → daily cleanup → wake row
|
|
589
|
+
* with due reviews riding the post-catchup context). Never resurrects
|
|
590
|
+
* an exhausted retry chain: failed attempts leave audit rows, which
|
|
591
|
+
* `shouldQueueMissedMorningFire` treats as "attempted". Suppressed
|
|
592
|
+
* during the first {@link MISSED_FIRE_BOOT_SUPPRESSION_MS} of process
|
|
593
|
+
* life: that window belongs to the boot catchup, whose INLINE morning
|
|
594
|
+
* run leaves no wake row for the predicate to dedup against (a slow
|
|
595
|
+
* pre-pass there must not look like a missed fire).
|
|
596
|
+
*
|
|
597
|
+
* Fire-and-forget; each layer owns its own error containment.
|
|
598
|
+
*/
|
|
599
|
+
async runMorningSelfHeal(now) {
|
|
600
|
+
const gateReason = this.autonomousGate();
|
|
601
|
+
if (gateReason !== null) {
|
|
602
|
+
this.logGateBlock(gateReason, { timer: "morning_self_heal" });
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
const agentDayConfig = {
|
|
606
|
+
timezone: this.config.timezone || undefined,
|
|
607
|
+
dayBoundaryHour: this.config.dayBoundaryHour,
|
|
608
|
+
};
|
|
609
|
+
// Layer 1 — hung-claim recovery.
|
|
610
|
+
try {
|
|
611
|
+
const thresholdMinutes = readMorningRoutineStallThresholdMinutes(this.db);
|
|
612
|
+
const recoverable = getRecoverableStalledMorningWake(this.db, agentDayConfig, thresholdMinutes, now);
|
|
613
|
+
if (recoverable
|
|
614
|
+
&& this.countSelfHealRequeuesToday(now) < MAX_SELFHEAL_REQUEUES_PER_AGENT_DAY) {
|
|
615
|
+
const flipped = this.db
|
|
616
|
+
.prepare(`UPDATE agent_schedule
|
|
617
|
+
SET status = 'pending', scheduled_for = ?
|
|
618
|
+
WHERE id = ? AND status = 'running'`)
|
|
619
|
+
.run(formatSqliteDatetime(now), recoverable.id);
|
|
620
|
+
if (flipped.changes > 0) {
|
|
621
|
+
logger.warn({
|
|
622
|
+
scheduleId: recoverable.id,
|
|
623
|
+
claimedAgeMinutes: recoverable.claimedAgeMinutes,
|
|
624
|
+
thresholdMinutes,
|
|
625
|
+
}, "Morning routine wake stuck in 'running' — flipped back to pending for re-claim");
|
|
626
|
+
try {
|
|
627
|
+
this.db
|
|
628
|
+
.prepare(`INSERT INTO agent_actions
|
|
629
|
+
(action_type, detail, result, started_at, completed_at)
|
|
630
|
+
VALUES (?, ?, 'success', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)`)
|
|
631
|
+
.run("morning_routine.selfheal_requeued", JSON.stringify({
|
|
632
|
+
scheduleId: recoverable.id,
|
|
633
|
+
claimedAgeMinutes: recoverable.claimedAgeMinutes,
|
|
634
|
+
thresholdMinutes,
|
|
635
|
+
}));
|
|
636
|
+
}
|
|
637
|
+
catch (auditErr) {
|
|
638
|
+
logger.warn({ err: auditErr, scheduleId: recoverable.id }, "Failed to record morning_routine.selfheal_requeued audit");
|
|
639
|
+
}
|
|
640
|
+
this.nudgeWatcher();
|
|
641
|
+
// Skip the alert for this tick — the recovery is the response.
|
|
642
|
+
// The missed-fire layer cannot apply (a wake row exists).
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
// Recoverable-but-capped (or lost the flip race) falls through to
|
|
647
|
+
// the alert so the operator hears about a hang the self-heal is no
|
|
648
|
+
// longer allowed to chase.
|
|
649
|
+
}
|
|
650
|
+
catch (err) {
|
|
651
|
+
logger.warn({ err }, "Morning self-heal recovery step failed");
|
|
652
|
+
}
|
|
653
|
+
// Layer 2 — alert-only watchdog.
|
|
654
|
+
await this.checkMorningRoutineStall(now).catch((err) => {
|
|
655
|
+
logger.warn({ err }, "Morning routine stall watchdog threw (self-heal tick)");
|
|
656
|
+
});
|
|
657
|
+
// Layer 3 — missed boundary fire.
|
|
658
|
+
try {
|
|
659
|
+
if (Date.now() - this.startedAtMs < MISSED_FIRE_BOOT_SUPPRESSION_MS) {
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
if (!shouldQueueMissedMorningFire(this.db, agentDayConfig, MORNING_MISSED_FIRE_GRACE_MINUTES, now)) {
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
if (!this.isAgentEnabledForFiring("morning-routine", "morning_self_heal")) {
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
// Mirror runWakeCatchup's morning branch: open the day properly and
|
|
669
|
+
// let any due reviews / activity scan ride the wake row's
|
|
670
|
+
// post-catchup context instead of being skipped by the dispatcher's
|
|
671
|
+
// morning-pending gate.
|
|
672
|
+
const tz = this.config.timezone || undefined;
|
|
673
|
+
const { start, end } = getAgentDayBoundsUtc(tz, this.config.dayBoundaryHour, now);
|
|
674
|
+
const dueRoutines = getDueCatchupRoutines(this.db, this.config, start, end, now).filter((routine) => this.isAgentEnabledForFiring(WAKE_CATCHUP_AGENT_SLUGS[routine] ?? routine, `${routine}_self_heal`));
|
|
675
|
+
const needsActivityScan = shouldCatchUpActivityScan(this.db, this.config, now)
|
|
676
|
+
&& this.isAgentEnabledForFiring("activity-scan", "activity_scan_self_heal");
|
|
677
|
+
try {
|
|
678
|
+
if (this.onDayBoundary) {
|
|
679
|
+
await this.onDayBoundary();
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
catch (err) {
|
|
683
|
+
logger.error({ err }, "Day boundary callback failed during morning self-heal");
|
|
684
|
+
}
|
|
685
|
+
this.dailyCleanup();
|
|
686
|
+
const queued = this.queueMorningRoutineWake("missed_cron_selfheal", {
|
|
687
|
+
postCatchupRoutines: dueRoutines,
|
|
688
|
+
postCatchupActivityScan: needsActivityScan,
|
|
689
|
+
});
|
|
690
|
+
this.nudgeWatcher();
|
|
691
|
+
logger.warn({ queued, dueRoutines, needsActivityScan }, "Morning routine fire was missed (sleep swallowed the boundary cron tick) — self-heal queued wake");
|
|
692
|
+
}
|
|
693
|
+
catch (err) {
|
|
694
|
+
logger.warn({ err }, "Morning self-heal missed-fire step failed");
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
/**
|
|
698
|
+
* Number of self-heal re-queues already performed this agent-day,
|
|
699
|
+
* counted from the `morning_routine.selfheal_requeued` audit rows the
|
|
700
|
+
* recovery layer writes. Throws propagate to the recovery layer's
|
|
701
|
+
* catch (fail-closed: an unreadable counter must not unlock unlimited
|
|
702
|
+
* re-runs).
|
|
703
|
+
*/
|
|
704
|
+
countSelfHealRequeuesToday(now) {
|
|
705
|
+
const { start } = getAgentDayBoundsUtc(this.config.timezone || undefined, this.config.dayBoundaryHour, now);
|
|
706
|
+
const row = this.db
|
|
707
|
+
.prepare(`SELECT COUNT(*) AS cnt
|
|
708
|
+
FROM agent_actions
|
|
709
|
+
WHERE action_type = 'morning_routine.selfheal_requeued'
|
|
710
|
+
AND started_at >= ?`)
|
|
711
|
+
.get(start);
|
|
712
|
+
return row.cnt;
|
|
713
|
+
}
|
|
714
|
+
/**
|
|
715
|
+
* Replay cron triggers swallowed by a machine sleep (or forward clock
|
|
716
|
+
* jump). node-cron does not fire ticks whose time passed while the
|
|
717
|
+
* process was suspended, so without this a daemon that sleeps through
|
|
718
|
+
* 04:00 / 18:00 / a Friday 19:00 recovers those routines only on the
|
|
719
|
+
* next daemon RESTART (`bootstrap/catchup.ts`) — possibly days later.
|
|
720
|
+
*
|
|
721
|
+
* Reuses the boot-time catchup's decision predicates so the two paths
|
|
722
|
+
* cannot drift: `getDueCatchupRoutines` dedups against `agent_actions`,
|
|
723
|
+
* `shouldCatchUpActivityScan` replays at most the current slot, and the
|
|
724
|
+
* morning routine goes through `queueMorningRoutineWake`'s DB-backed
|
|
725
|
+
* dedup. Downstream dispatch-time gates (autonomous setup gate,
|
|
726
|
+
* morning-pending review gate) still apply.
|
|
727
|
+
*
|
|
728
|
+
* Ordering: when the morning routine has not completed for the current
|
|
729
|
+
* agent-day, the review routines and the activity scan ride along on the
|
|
730
|
+
* wake row's `postCatchupRoutines` / `postCatchupActivityScan` context
|
|
731
|
+
* (same replay mechanism the boot catchup uses) so they run AFTER the
|
|
732
|
+
* day is opened instead of being skipped by the dispatcher's
|
|
733
|
+
* pre-routine gate.
|
|
734
|
+
*/
|
|
735
|
+
async runWakeCatchup(gapMs) {
|
|
736
|
+
const gapMinutes = Math.round(gapMs / 60_000);
|
|
737
|
+
const gateReason = this.autonomousGate();
|
|
738
|
+
if (gateReason !== null) {
|
|
739
|
+
this.logGateBlock(gateReason, { cron: "wake_catchup", gapMinutes });
|
|
740
|
+
return;
|
|
741
|
+
}
|
|
742
|
+
const now = new Date();
|
|
743
|
+
const tz = this.config.timezone || undefined;
|
|
744
|
+
const { start, end } = getAgentDayBoundsUtc(tz, this.config.dayBoundaryHour, now);
|
|
745
|
+
const dueRoutines = getDueCatchupRoutines(this.db, this.config, start, end, now).filter((routine) => this.isAgentEnabledForFiring(WAKE_CATCHUP_AGENT_SLUGS[routine] ?? routine, `${routine}_wake_catchup`));
|
|
746
|
+
const needsActivityScan = shouldCatchUpActivityScan(this.db, this.config, now)
|
|
747
|
+
&& this.isAgentEnabledForFiring("activity-scan", "activity_scan_wake_catchup");
|
|
748
|
+
if (!morningRoutineRanToday(this.db, { timezone: tz, dayBoundaryHour: this.config.dayBoundaryHour }, now)) {
|
|
749
|
+
// Slept across the day boundary (or the morning routine never
|
|
750
|
+
// succeeded today) — re-run the full 04:00 flow. The wake row
|
|
751
|
+
// dedups against an already-pending/running morning run, merging
|
|
752
|
+
// the post-catchup context instead of double-firing.
|
|
753
|
+
if (!this.isAgentEnabledForFiring("morning-routine", "morning_routine_wake_catchup")) {
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
try {
|
|
757
|
+
if (this.onDayBoundary) {
|
|
758
|
+
await this.onDayBoundary();
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
catch (err) {
|
|
762
|
+
logger.error({ err }, "Day boundary callback failed during wake catch-up");
|
|
763
|
+
}
|
|
764
|
+
this.dailyCleanup();
|
|
765
|
+
const queued = this.queueMorningRoutineWake("wake_catchup", {
|
|
766
|
+
postCatchupRoutines: dueRoutines,
|
|
767
|
+
postCatchupActivityScan: needsActivityScan,
|
|
768
|
+
});
|
|
769
|
+
this.nudgeWatcher();
|
|
770
|
+
logger.info({ gapMinutes, queued, dueRoutines, needsActivityScan }, "Wake catch-up queued morning routine");
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
for (const routine of dueRoutines) {
|
|
774
|
+
logger.info({ routine, gapMinutes }, "Wake catch-up replaying missed routine");
|
|
775
|
+
this.emitRoutine(routine);
|
|
776
|
+
}
|
|
777
|
+
if (needsActivityScan && this.onActivityScan) {
|
|
778
|
+
logger.info({ gapMinutes }, "Wake catch-up triggering missed activity scan");
|
|
779
|
+
void Promise.resolve(this.onActivityScan("wake_catchup")).catch((err) => {
|
|
780
|
+
logger.warn({ err }, "Wake catch-up activity scan failed");
|
|
781
|
+
});
|
|
782
|
+
}
|
|
783
|
+
if (dueRoutines.length === 0 && !needsActivityScan) {
|
|
784
|
+
logger.info({ gapMinutes }, "Wake catch-up: nothing missed");
|
|
785
|
+
}
|
|
786
|
+
}
|
|
473
787
|
start() {
|
|
474
788
|
this.setupRecurringJobs();
|
|
475
789
|
this.startScheduleWatcher();
|
|
790
|
+
this.wakeDetector.start();
|
|
791
|
+
this.morningSelfHealTimer = setInterval(() => {
|
|
792
|
+
void this.runMorningSelfHeal(new Date()).catch((err) => {
|
|
793
|
+
logger.warn({ err }, "Morning self-heal tick threw");
|
|
794
|
+
});
|
|
795
|
+
}, MORNING_SELF_HEAL_INTERVAL_MS);
|
|
796
|
+
this.morningSelfHealTimer.unref?.();
|
|
476
797
|
logger.info("Scheduler started");
|
|
477
798
|
}
|
|
478
799
|
stop() {
|
|
479
800
|
this.shutdown = true;
|
|
801
|
+
this.wakeDetector.stop();
|
|
802
|
+
if (this.morningSelfHealTimer) {
|
|
803
|
+
clearInterval(this.morningSelfHealTimer);
|
|
804
|
+
this.morningSelfHealTimer = null;
|
|
805
|
+
}
|
|
480
806
|
// Wake up the ScheduleWatcher's poll sleep so the loop returns
|
|
481
807
|
// immediately instead of waiting for the next interval tick. The
|
|
482
808
|
// sleepInterruptible body re-checks `shutdown` before re-entering
|
|
@@ -647,17 +973,16 @@ export class AgentScheduler {
|
|
|
647
973
|
// node-cron doesn't directly support "last day of month",
|
|
648
974
|
// so we run daily at 18:00 and check if tomorrow is the 1st.
|
|
649
975
|
//
|
|
650
|
-
// Default OFF pre-release
|
|
651
|
-
//
|
|
652
|
-
//
|
|
653
|
-
//
|
|
654
|
-
//
|
|
655
|
-
//
|
|
656
|
-
// (task-flow, context-builder branch, retention
|
|
657
|
-
// tree as a concept pending the Mirror+Prune
|
|
976
|
+
// Default OFF pre-release: the monthly-review AGENT row ships
|
|
977
|
+
// `enabled: false`, and `isAgentEnabledForFiring` below is the single
|
|
978
|
+
// fire-time switch (AGENTS_HUB_REDESIGN_PLAN.md §2 — the legacy
|
|
979
|
+
// `monthlyReviewEnabled` config gate was unified into it; a one-time
|
|
980
|
+
// boot reconcile carries an operator's old `true` forward). A toggle
|
|
981
|
+
// takes effect on the next month-end without restart or cron rebuild.
|
|
982
|
+
// The routine itself (task-flow, context-builder branch, retention
|
|
983
|
+
// coupling) stays in tree as a concept pending the Mirror+Prune
|
|
984
|
+
// redesign.
|
|
658
985
|
const monthlyJob = cron.schedule("0 18 * * *", () => {
|
|
659
|
-
if (!this.config.monthlyReviewEnabled)
|
|
660
|
-
return;
|
|
661
986
|
// Check if tomorrow (in configured timezone) is the 1st
|
|
662
987
|
const tomorrow = new Date();
|
|
663
988
|
tomorrow.setDate(tomorrow.getDate() + 1);
|
|
@@ -725,9 +1050,19 @@ export class AgentScheduler {
|
|
|
725
1050
|
}
|
|
726
1051
|
}, { timezone: tz });
|
|
727
1052
|
this.cronJobs.push(browserDigestJob);
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
1053
|
+
{
|
|
1054
|
+
// Cadence is owned by the activity-scan AGENT ROW (metadata_json.
|
|
1055
|
+
// runtime_window, edited via PATCH /api/agents/activity-scan) with the
|
|
1056
|
+
// legacy `activityScan*` config keys as per-field fallback —
|
|
1057
|
+
// AGENTS_HUB_REDESIGN_PLAN.md §2. Resolved once at registration; the
|
|
1058
|
+
// agents PATCH route triggers `reloadCrons()` on a cadence change, so
|
|
1059
|
+
// the closure below never goes stale. The job is registered
|
|
1060
|
+
// UNCONDITIONALLY: `agents.enabled` (fire-time `isAgentEnabledForFiring`
|
|
1061
|
+
// gate below) is the single on/off switch — the legacy
|
|
1062
|
+
// `activityScanEnabled` registration gate was unified into it.
|
|
1063
|
+
const activityScanCadence = resolveActivityScanCadence(getRuntimeWindow(this.db, "activity-scan"), this.config);
|
|
1064
|
+
const activityScanExpr = buildActivityScanCronExpr(activityScanCadence.intervalMinutes, activityScanCadence.activeStartHour, activityScanCadence.activeEndHour);
|
|
1065
|
+
const activityScanJob = cron.schedule(activityScanExpr, () => {
|
|
731
1066
|
const now = new Date();
|
|
732
1067
|
// Pull both hour and minute from the canonical timezone helper
|
|
733
1068
|
// so the day-boundary skip and the interval gate observe the
|
|
@@ -744,22 +1079,34 @@ export class AgentScheduler {
|
|
|
744
1079
|
// of each agent-day lands at the start of the active window —
|
|
745
1080
|
// critical for intervals near or equal to the window length.
|
|
746
1081
|
// Divisor-of-60 cases short-circuit inside the helper.
|
|
747
|
-
if (!
|
|
1082
|
+
if (!shouldFireActivityScanTickAt(local.hours, local.minutes, activityScanCadence.intervalMinutes, activityScanCadence.activeStartHour)) {
|
|
748
1083
|
return;
|
|
749
1084
|
}
|
|
1085
|
+
// Self-tuning auto-revert monitor — ahead of BOTH the per-agent
|
|
1086
|
+
// enabled gate and the autonomous setup gate below: rollback
|
|
1087
|
+
// safety must survive the owner disabling the activity-scan
|
|
1088
|
+
// Agent (a plausible cost-saving move while a tuned knob sits
|
|
1089
|
+
// unverified) and a degraded/setup-gated daemon. It is pure
|
|
1090
|
+
// daemon code (no LLM dispatch), owns its own 1/day throttle,
|
|
1091
|
+
// and is a no-op until the actuator has written ledger entries.
|
|
1092
|
+
if (this.onSelfTuningRevertMonitor) {
|
|
1093
|
+
void this.onSelfTuningRevertMonitor().catch((err) => {
|
|
1094
|
+
logger.warn({ err }, "Self-tuning revert monitor failed");
|
|
1095
|
+
});
|
|
1096
|
+
}
|
|
750
1097
|
// Per-built-in enabled gate, AFTER the interval gate so a
|
|
751
1098
|
// per-minute non-firing tick never inflates the suppressed count.
|
|
752
|
-
if (!this.isAgentEnabledForFiring("
|
|
1099
|
+
if (!this.isAgentEnabledForFiring("activity-scan", "activity_scan"))
|
|
753
1100
|
return;
|
|
754
|
-
//
|
|
1101
|
+
// triggerActivityScan has its own setup gate, but short-circuit
|
|
755
1102
|
// here to avoid the in-progress flag toggling for no reason.
|
|
756
1103
|
const gateReason = this.autonomousGate();
|
|
757
1104
|
if (gateReason !== null) {
|
|
758
|
-
this.logGateBlock(gateReason, { cron: "
|
|
1105
|
+
this.logGateBlock(gateReason, { cron: "activity_scan" });
|
|
759
1106
|
return;
|
|
760
1107
|
}
|
|
761
|
-
// Phase 4 auth probe runs BEFORE the
|
|
762
|
-
// observation-threshold gate (which can skip `
|
|
1108
|
+
// Phase 4 auth probe runs BEFORE the activity scan so that the
|
|
1109
|
+
// observation-threshold gate (which can skip `onActivityScan`
|
|
763
1110
|
// entirely when there's no pending user activity) does not
|
|
764
1111
|
// also stall auth health detection. The probe owns its own
|
|
765
1112
|
// morning-routine / probe-disabled gating; we only respect
|
|
@@ -771,16 +1118,16 @@ export class AgentScheduler {
|
|
|
771
1118
|
}
|
|
772
1119
|
// Morning-routine stall watchdog. Runs alongside the auth probe
|
|
773
1120
|
// because both are observability hooks that should fire even
|
|
774
|
-
// when the
|
|
1121
|
+
// when the activity scan itself gets gated (e.g., the gate
|
|
775
1122
|
// skip is the *symptom* the watchdog needs to catch).
|
|
776
1123
|
void this.checkMorningRoutineStall(now).catch((err) => {
|
|
777
1124
|
logger.warn({ err }, "Morning routine stall watchdog threw");
|
|
778
1125
|
});
|
|
779
|
-
if (this.
|
|
780
|
-
void this.
|
|
1126
|
+
if (this.onActivityScan) {
|
|
1127
|
+
void this.onActivityScan("cron");
|
|
781
1128
|
}
|
|
782
1129
|
}, { timezone: tz });
|
|
783
|
-
this.cronJobs.push(
|
|
1130
|
+
this.cronJobs.push(activityScanJob);
|
|
784
1131
|
}
|
|
785
1132
|
// P22 §6.3, §6.4 — skill curation. Registered only when the operator
|
|
786
1133
|
// has opted in via /settings/self-learning (`enabled=true`). Always
|
|
@@ -814,12 +1161,15 @@ export class AgentScheduler {
|
|
|
814
1161
|
}, { timezone: tz });
|
|
815
1162
|
this.cronJobs.push(skillCurationJob);
|
|
816
1163
|
}
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
1164
|
+
{
|
|
1165
|
+
const cadence = resolveActivityScanCadence(getRuntimeWindow(this.db, "activity-scan"), this.config);
|
|
1166
|
+
logger.info({
|
|
1167
|
+
morningHour: this.config.dayBoundaryHour,
|
|
1168
|
+
timezone: tz ?? "system",
|
|
1169
|
+
activityScanIntervalMinutes: cadence.intervalMinutes,
|
|
1170
|
+
activityScanActiveHours: `${cadence.activeStartHour}-${cadence.activeEndHour}`,
|
|
1171
|
+
}, "Recurring cron jobs configured");
|
|
1172
|
+
}
|
|
823
1173
|
}
|
|
824
1174
|
emitRoutine(routineName, data) {
|
|
825
1175
|
const event = {
|
|
@@ -855,7 +1205,7 @@ export class AgentScheduler {
|
|
|
855
1205
|
routine: "morning_routine",
|
|
856
1206
|
source,
|
|
857
1207
|
postCatchupRoutines: options?.postCatchupRoutines ?? [],
|
|
858
|
-
|
|
1208
|
+
postCatchupActivityScan: options?.postCatchupActivityScan ?? false,
|
|
859
1209
|
importance: "low",
|
|
860
1210
|
});
|
|
861
1211
|
const insertTxn = this.db.transaction(() => {
|
|
@@ -875,13 +1225,19 @@ export class AgentScheduler {
|
|
|
875
1225
|
: []),
|
|
876
1226
|
...(options?.postCatchupRoutines ?? []),
|
|
877
1227
|
]));
|
|
878
|
-
const
|
|
879
|
-
options?.
|
|
1228
|
+
const mergedActivityScan = existingContext.postCatchupActivityScan === true ||
|
|
1229
|
+
options?.postCatchupActivityScan === true;
|
|
1230
|
+
// Spread the existing context FIRST so keys this merge doesn't
|
|
1231
|
+
// know about survive — in particular the ScheduleWatcher's
|
|
1232
|
+
// `claimedAt` stamp on a running row: dropping it would blind
|
|
1233
|
+
// the self-heal recovery predicate exactly when the 04:00 cron
|
|
1234
|
+
// merges into a hung overnight run.
|
|
880
1235
|
const mergedContext = {
|
|
1236
|
+
...existingContext,
|
|
881
1237
|
routine: "morning_routine",
|
|
882
1238
|
source: existingContext.source ?? source,
|
|
883
1239
|
postCatchupRoutines: mergedRoutines,
|
|
884
|
-
|
|
1240
|
+
postCatchupActivityScan: mergedActivityScan,
|
|
885
1241
|
importance: "low",
|
|
886
1242
|
};
|
|
887
1243
|
// Bump `scheduled_for` forward when the new caller's NOW lies
|
|
@@ -990,6 +1346,25 @@ export class AgentScheduler {
|
|
|
990
1346
|
.run(row.id);
|
|
991
1347
|
if (result.changes === 0)
|
|
992
1348
|
continue;
|
|
1349
|
+
// Stamp the claim time on morning-routine wake rows. This is
|
|
1350
|
+
// the staleness signal the self-heal recovery predicate
|
|
1351
|
+
// (`getRecoverableStalledMorningWake`) measures from —
|
|
1352
|
+
// `created_at` and `scheduled_for` both lie after sleeps and
|
|
1353
|
+
// dedup merges. Best-effort and morning-scoped: a failure
|
|
1354
|
+
// here only demotes that row from auto-recovery to the
|
|
1355
|
+
// alert-only watchdog path.
|
|
1356
|
+
try {
|
|
1357
|
+
this.db
|
|
1358
|
+
.prepare(`UPDATE agent_schedule
|
|
1359
|
+
SET task_context = json_set(COALESCE(task_context, '{}'), '$.claimedAt', ?)
|
|
1360
|
+
WHERE id = ?
|
|
1361
|
+
AND json_valid(COALESCE(task_context, '{}'))
|
|
1362
|
+
AND json_extract(task_context, '$.routine') = 'morning_routine'`)
|
|
1363
|
+
.run(formatSqliteDatetime(new Date()), row.id);
|
|
1364
|
+
}
|
|
1365
|
+
catch (stampErr) {
|
|
1366
|
+
logger.warn({ err: stampErr, taskId: row.id }, "Failed to stamp claimedAt on claimed schedule row");
|
|
1367
|
+
}
|
|
993
1368
|
// Per-row try/catch: if the row body throws (e.g. malformed
|
|
994
1369
|
// task_context JSON), flip the claim to 'failed' so the row
|
|
995
1370
|
// doesn't stay 'running' forever and the watcher can move on.
|
|
@@ -1073,57 +1448,10 @@ export class AgentScheduler {
|
|
|
1073
1448
|
// delay; the row's status is reverted to `pending` so
|
|
1074
1449
|
// the next ScheduleWatcher tick re-evaluates.
|
|
1075
1450
|
if (row.task_type === "browser_task") {
|
|
1076
|
-
const fireAt = new Date();
|
|
1077
1451
|
const respectQuietHours = this.config.browserTaskRespectQuietHours !== false;
|
|
1078
|
-
if (respectQuietHours
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
end: this.config.quietHoursEnd,
|
|
1082
|
-
timezone: this.config.timezone || undefined,
|
|
1083
|
-
};
|
|
1084
|
-
if (isInQuietHoursAt(fireAt, quietHoursWindow)) {
|
|
1085
|
-
const deferUntilMs = nextQuietHoursEndMs(fireAt, quietHoursWindow);
|
|
1086
|
-
if (deferUntilMs !== null) {
|
|
1087
|
-
const deferredFor = formatSqliteDatetime(new Date(deferUntilMs));
|
|
1088
|
-
this.db
|
|
1089
|
-
.prepare(`UPDATE agent_schedule
|
|
1090
|
-
SET scheduled_for = ?, status = 'pending'
|
|
1091
|
-
WHERE id = ?`)
|
|
1092
|
-
.run(deferredFor, row.id);
|
|
1093
|
-
try {
|
|
1094
|
-
this.db
|
|
1095
|
-
.prepare(`INSERT INTO agent_actions
|
|
1096
|
-
(action_type, detail, result, started_at, completed_at)
|
|
1097
|
-
VALUES (?, ?, 'success', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)`)
|
|
1098
|
-
.run("browser_task.deferred_for_quiet_hours", JSON.stringify({
|
|
1099
|
-
scheduleId: row.id,
|
|
1100
|
-
originalScheduledFor: row.scheduled_for,
|
|
1101
|
-
deferredUntil: deferredFor,
|
|
1102
|
-
quietHoursStart: this.config.quietHoursStart,
|
|
1103
|
-
quietHoursEnd: this.config.quietHoursEnd,
|
|
1104
|
-
}));
|
|
1105
|
-
}
|
|
1106
|
-
catch (auditErr) {
|
|
1107
|
-
/* c8 ignore start -- defensive against schema partials */
|
|
1108
|
-
logger.warn({ err: auditErr, scheduleId: row.id }, "Failed to record browser_task.deferred_for_quiet_hours audit");
|
|
1109
|
-
/* c8 ignore stop */
|
|
1110
|
-
}
|
|
1111
|
-
logger.info({
|
|
1112
|
-
scheduleId: row.id,
|
|
1113
|
-
deferredUntil: deferredFor,
|
|
1114
|
-
quietHoursStart: this.config.quietHoursStart,
|
|
1115
|
-
quietHoursEnd: this.config.quietHoursEnd,
|
|
1116
|
-
}, "scheduled.browser_task deferred for quiet hours");
|
|
1117
|
-
continue;
|
|
1118
|
-
}
|
|
1119
|
-
// `nextQuietHoursEndMs` returning null inside a quiet-
|
|
1120
|
-
// hours predicate that just returned true would mean
|
|
1121
|
-
// a 24-hour window — the runtime-settings schema
|
|
1122
|
-
// disallows this (equal start/end short-circuits the
|
|
1123
|
-
// predicate), so it cannot occur in normal operation.
|
|
1124
|
-
// Fall through to dispatch rather than re-deferring
|
|
1125
|
-
// forever.
|
|
1126
|
-
}
|
|
1452
|
+
if (respectQuietHours &&
|
|
1453
|
+
this.deferClaimedRowForQuietHours(row, "browser_task.deferred_for_quiet_hours")) {
|
|
1454
|
+
continue;
|
|
1127
1455
|
}
|
|
1128
1456
|
const base = createEvent({
|
|
1129
1457
|
type: "scheduled.browser_task",
|
|
@@ -1151,12 +1479,64 @@ export class AgentScheduler {
|
|
|
1151
1479
|
logger.info({ scheduleId: row.id, taskType: row.task_type }, "Scheduled browser-task dispatched");
|
|
1152
1480
|
continue;
|
|
1153
1481
|
}
|
|
1482
|
+
// BACKGROUND_TASK_RUNNER_DESIGN.md §4.2 — generic background
|
|
1483
|
+
// task firing at its scheduled time. Body lives in
|
|
1484
|
+
// `task_context` (frozen at schedule time); the dispatcher's
|
|
1485
|
+
// `scheduled.background_task` handler creates the row at fire
|
|
1486
|
+
// time and hands off to the runner. No quiet-hours deferral
|
|
1487
|
+
// on dispatch — the worker may run at any hour; the DELIVERY
|
|
1488
|
+
// boundary quiet-hours-gates the owner-facing DM (§10.6).
|
|
1489
|
+
if (row.task_type === "background_task") {
|
|
1490
|
+
const base = createEvent({
|
|
1491
|
+
type: "scheduled.background_task",
|
|
1492
|
+
source: row.task_type,
|
|
1493
|
+
priority: EventPriority.NORMAL,
|
|
1494
|
+
});
|
|
1495
|
+
let parsedContext;
|
|
1496
|
+
try {
|
|
1497
|
+
parsedContext = JSON.parse(row.task_context ?? "{}");
|
|
1498
|
+
}
|
|
1499
|
+
catch (parseErr) {
|
|
1500
|
+
logger.error({ err: parseErr, scheduleId: row.id }, "scheduled.background_task: task_context JSON parse failed — marking row failed");
|
|
1501
|
+
this.db
|
|
1502
|
+
.prepare("UPDATE agent_schedule SET status = 'failed' WHERE id = ? AND status = 'running'")
|
|
1503
|
+
.run(row.id);
|
|
1504
|
+
continue;
|
|
1505
|
+
}
|
|
1506
|
+
const event = {
|
|
1507
|
+
...base,
|
|
1508
|
+
taskContext: parsedContext,
|
|
1509
|
+
correlationId: row.correlation_id ?? base.correlationId,
|
|
1510
|
+
scheduleId: row.id,
|
|
1511
|
+
};
|
|
1512
|
+
await this.eventBus.put(event);
|
|
1513
|
+
logger.info({ scheduleId: row.id, taskType: row.task_type }, "Scheduled background-task dispatched");
|
|
1514
|
+
continue;
|
|
1515
|
+
}
|
|
1516
|
+
const parsedTaskContext = JSON.parse(row.task_context ?? "{}");
|
|
1517
|
+
// QUIET_HOURS_HARDENING_PLAN.md §6 — per-row opt-in quiet-hours
|
|
1518
|
+
// deferral for user-Agent firings. The Agent loader copies the
|
|
1519
|
+
// definition's `schedule.defer_in_quiet_hours: true` into the
|
|
1520
|
+
// recurring row's task_context and `generateNextScheduleRow`
|
|
1521
|
+
// spreads it into every materialised row, so the check is
|
|
1522
|
+
// row-local (no `agents` join). The whole RUN moves past the
|
|
1523
|
+
// quiet window (fresh data at delivery time, no wasted 03:00
|
|
1524
|
+
// session), mirroring the browser_task deferral above. Built-ins
|
|
1525
|
+
// fire outside `recurring_schedules` and never carry the flag;
|
|
1526
|
+
// manual run-now rows omit it too (an explicit "run now" click
|
|
1527
|
+
// must fire immediately).
|
|
1528
|
+
if (row.task_type === "agent.task" &&
|
|
1529
|
+
parsedTaskContext.defer_in_quiet_hours === true &&
|
|
1530
|
+
this.deferClaimedRowForQuietHours(row, "agent.task.deferred_for_quiet_hours", typeof parsedTaskContext.agent_id === "string"
|
|
1531
|
+
? parsedTaskContext.agent_id
|
|
1532
|
+
: null)) {
|
|
1533
|
+
continue;
|
|
1534
|
+
}
|
|
1154
1535
|
const base = createEvent({
|
|
1155
1536
|
type: "scheduled.task",
|
|
1156
1537
|
source: row.task_type,
|
|
1157
1538
|
priority: EventPriority.NORMAL,
|
|
1158
1539
|
});
|
|
1159
|
-
const parsedTaskContext = JSON.parse(row.task_context ?? "{}");
|
|
1160
1540
|
// WIKI_BUILDER_DESIGN.md §3.4-bis — bang-spawned approval rows
|
|
1161
1541
|
// (today: wiki.compile via `!compile full` above threshold;
|
|
1162
1542
|
// generalisable to any future bang→approval path) carry a
|
|
@@ -1243,6 +1623,83 @@ export class AgentScheduler {
|
|
|
1243
1623
|
};
|
|
1244
1624
|
void loop();
|
|
1245
1625
|
}
|
|
1626
|
+
/**
|
|
1627
|
+
* Quiet-hours deferral for a claimed `agent_schedule` row (shared by the
|
|
1628
|
+
* `browser_task` always-on-by-config path and the `agent.task` per-row
|
|
1629
|
+
* opt-in, QUIET_HOURS_HARDENING_PLAN.md §6). When the current wall-clock
|
|
1630
|
+
* instant falls inside the configured quiet-hours window, the row is pushed
|
|
1631
|
+
* forward to the next quiet-hours-end boundary (status reverted to
|
|
1632
|
+
* `pending` so the next ScheduleWatcher tick re-evaluates), one
|
|
1633
|
+
* `agent_actions` audit row is written per deferral so the user can see the
|
|
1634
|
+
* delay, and `true` is returned. Returns `false` when outside the window —
|
|
1635
|
+
* or when `nextQuietHoursEndMs` cannot resolve a boundary, which inside a
|
|
1636
|
+
* quiet-hours predicate that just returned true would mean a 24-hour
|
|
1637
|
+
* window; the runtime-settings schema disallows this (equal start/end
|
|
1638
|
+
* short-circuits the predicate), so it cannot occur in normal operation.
|
|
1639
|
+
* Falling through to dispatch beats re-deferring forever.
|
|
1640
|
+
*
|
|
1641
|
+
* `agentId` (the owning user Agent's slug from `task_context.agent_id`)
|
|
1642
|
+
* stamps the audit row's `agent_id` column so the deferral is attributable
|
|
1643
|
+
* per Agent; the browser_task path has no owning Agent and passes none.
|
|
1644
|
+
*/
|
|
1645
|
+
deferClaimedRowForQuietHours(row, actionType, agentId = null) {
|
|
1646
|
+
const fireAt = new Date();
|
|
1647
|
+
const quietHoursWindow = {
|
|
1648
|
+
start: this.config.quietHoursStart,
|
|
1649
|
+
end: this.config.quietHoursEnd,
|
|
1650
|
+
timezone: this.config.timezone || undefined,
|
|
1651
|
+
};
|
|
1652
|
+
if (!isInQuietHoursAt(fireAt, quietHoursWindow))
|
|
1653
|
+
return false;
|
|
1654
|
+
const deferUntilMs = nextQuietHoursEndMs(fireAt, quietHoursWindow);
|
|
1655
|
+
if (deferUntilMs === null)
|
|
1656
|
+
return false;
|
|
1657
|
+
const deferredFor = formatSqliteDatetime(new Date(deferUntilMs));
|
|
1658
|
+
// `quiet_hours_deferred` marks the row as ACTUALLY deferred (vs merely
|
|
1659
|
+
// carrying the `defer_in_quiet_hours` opt-in on a future cron slot) so a
|
|
1660
|
+
// quiet-hours config change can retime exactly these rows
|
|
1661
|
+
// (`retimeDeferredRunRows` in db/deferred-dm.ts, the sibling of the
|
|
1662
|
+
// Phase-1 deferred-DM retime). Invalid task_context JSON is left
|
|
1663
|
+
// untouched — stamping must never destroy a browser_task's frozen body.
|
|
1664
|
+
this.db
|
|
1665
|
+
.prepare(`UPDATE agent_schedule
|
|
1666
|
+
SET scheduled_for = ?, status = 'pending',
|
|
1667
|
+
task_context = CASE
|
|
1668
|
+
WHEN task_context IS NULL
|
|
1669
|
+
THEN json_object('quiet_hours_deferred', json('true'))
|
|
1670
|
+
WHEN json_valid(task_context)
|
|
1671
|
+
THEN json_set(task_context, '$.quiet_hours_deferred', json('true'))
|
|
1672
|
+
ELSE task_context
|
|
1673
|
+
END
|
|
1674
|
+
WHERE id = ?`)
|
|
1675
|
+
.run(deferredFor, row.id);
|
|
1676
|
+
try {
|
|
1677
|
+
this.db
|
|
1678
|
+
.prepare(`INSERT INTO agent_actions
|
|
1679
|
+
(action_type, detail, result, agent_id, started_at, completed_at)
|
|
1680
|
+
VALUES (?, ?, 'success', ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)`)
|
|
1681
|
+
.run(actionType, JSON.stringify({
|
|
1682
|
+
scheduleId: row.id,
|
|
1683
|
+
originalScheduledFor: row.scheduled_for,
|
|
1684
|
+
deferredUntil: deferredFor,
|
|
1685
|
+
quietHoursStart: this.config.quietHoursStart,
|
|
1686
|
+
quietHoursEnd: this.config.quietHoursEnd,
|
|
1687
|
+
}), agentId);
|
|
1688
|
+
}
|
|
1689
|
+
catch (auditErr) {
|
|
1690
|
+
/* c8 ignore start -- defensive against schema partials */
|
|
1691
|
+
logger.warn({ err: auditErr, scheduleId: row.id, actionType }, "Failed to record quiet-hours deferral audit");
|
|
1692
|
+
/* c8 ignore stop */
|
|
1693
|
+
}
|
|
1694
|
+
logger.info({
|
|
1695
|
+
scheduleId: row.id,
|
|
1696
|
+
taskType: row.task_type,
|
|
1697
|
+
deferredUntil: deferredFor,
|
|
1698
|
+
quietHoursStart: this.config.quietHoursStart,
|
|
1699
|
+
quietHoursEnd: this.config.quietHoursEnd,
|
|
1700
|
+
}, "Scheduled row deferred for quiet hours");
|
|
1701
|
+
return true;
|
|
1702
|
+
}
|
|
1246
1703
|
/**
|
|
1247
1704
|
* Sleep for `ms` milliseconds, but resolve early when `stop()` or
|
|
1248
1705
|
* `nudgeWatcher()` fire. Used by the ScheduleWatcher between polls so
|