@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
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Self-Tuning Review Cycle — Verify stage / auto-revert monitor
|
|
3
|
+
* (SELF_TUNING_REVIEW_CYCLE_DESIGN.md §3.4, Phase 3).
|
|
4
|
+
*
|
|
5
|
+
* Piggybacks the existing hourly cron tick (scheduler.ts — same
|
|
6
|
+
* fire-and-forget slot as the auth probe; no new scheduled session, P2) and
|
|
7
|
+
* throttles itself to one pass per UTC day via
|
|
8
|
+
* {@link REVERT_MONITOR_STATE_KEY}. Seven days after an applied config
|
|
9
|
+
* change, it recomputes the rule's target metric over the verify window
|
|
10
|
+
* `[applied_at, applied_at + 7d)` and:
|
|
11
|
+
*
|
|
12
|
+
* - **regression past the rule's margin** → revert through the shared
|
|
13
|
+
* {@link revertAppliedTuningChange} (config restored via the
|
|
14
|
+
* `applyConfigUpdates` chokepoint, ledger stamped `reverted_at` — which
|
|
15
|
+
* triggers the 28-day re-proposal cool-down — audit
|
|
16
|
+
* `self_tuning.reverted`, `self_critique` signal so the failure becomes
|
|
17
|
+
* a lesson) and DM the owner;
|
|
18
|
+
* - **no regression** → stamp `verified_at` + audit
|
|
19
|
+
* `self_tuning.verified` so the entry is never re-examined.
|
|
20
|
+
*
|
|
21
|
+
* Per-rule margins (D3/D4 — named constants, deliberately not settings
|
|
22
|
+
* keys):
|
|
23
|
+
* - R1 reverts if daily novelty≥2 observation arrivals fall >30% below
|
|
24
|
+
* the pre-change baseline (stale pre-pass suppressing signal) OR the
|
|
25
|
+
* cautious-escalate tick share rises >10 pt.
|
|
26
|
+
* - R3 reverts if >10% of `stage0_silent` ticks in the window carried
|
|
27
|
+
* `maxNoveltyScore ≥ 2` in their audited snapshot — harm only the
|
|
28
|
+
* raised ceiling can introduce (today's gate never silences novelty≥2).
|
|
29
|
+
* - R5 reverts on the explicit-correction proxy: any negative explicit /
|
|
30
|
+
* self_critique signal citing a lesson within the window.
|
|
31
|
+
*
|
|
32
|
+
* The monitor runs regardless of `selfTuningEnabled`: entries only exist
|
|
33
|
+
* once actuation has run, and a safety rollback must keep working even if
|
|
34
|
+
* the owner turns the loop off afterwards. Only `config`-actuator entries
|
|
35
|
+
* are verified — lesson/schedule entries carry no machine state.
|
|
36
|
+
*/
|
|
37
|
+
import { TUNING_METRIC_WINDOW_DAYS, auditSelfTuning, computeR1Metric, computeR3Metric, countLessonRegressionSignals, ledgerStateKey, listLedgerEntries, revertAppliedTuningChange, } from "./tuning-actuator.js";
|
|
38
|
+
import { readRuntimeState, writeRuntimeState } from "../../db/runtime-state.js";
|
|
39
|
+
import { createLogger } from "../../logging.js";
|
|
40
|
+
const logger = createLogger("tuning-revert-monitor");
|
|
41
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
42
|
+
/**
|
|
43
|
+
* Daily-throttle state key. Dot-separated namespace on purpose — the
|
|
44
|
+
* Measure stage's `gatherLedger` scans `self_tuning:%` and must never pick
|
|
45
|
+
* monitor state up as a phantom ledger entry (same rule as the pending
|
|
46
|
+
* cycle key).
|
|
47
|
+
*/
|
|
48
|
+
export const REVERT_MONITOR_STATE_KEY = "self_tuning.revert_monitor";
|
|
49
|
+
/** §3.4 — days between apply and the verify pass. */
|
|
50
|
+
export const TUNING_VERIFY_WINDOW_DAYS = TUNING_METRIC_WINDOW_DAYS;
|
|
51
|
+
/** D4 — R1 reverts when novelty≥2 arrivals fall >30% below baseline. */
|
|
52
|
+
export const R1_NOVELTY_ARRIVALS_MAX_DROP = 0.3;
|
|
53
|
+
/** D4 — R1 reverts when the cautious-escalate share rises >10 pt. */
|
|
54
|
+
export const R1_CAUTIOUS_ESCALATE_MAX_RISE = 0.1;
|
|
55
|
+
/** D3 — R3 reverts when >10% of silent ticks carried novelty≥2 snapshots. */
|
|
56
|
+
export const R3_SILENT_NOVELTY_GE2_MAX_SHARE = 0.1;
|
|
57
|
+
function isR1Metric(value) {
|
|
58
|
+
return (typeof value === "object" &&
|
|
59
|
+
value !== null &&
|
|
60
|
+
typeof value.noveltyGe2PerDay === "number" &&
|
|
61
|
+
typeof value.cautiousEscalateShare === "number");
|
|
62
|
+
}
|
|
63
|
+
function pct(value) {
|
|
64
|
+
return `${Math.round(value * 100)}%`;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Decide one applied entry's fate. Pure given the DB rows: every margin is
|
|
68
|
+
* compared against telemetry that already exists (D3 — no recomputation of
|
|
69
|
+
* live signals). An entry whose `applied_at` cannot be parsed, or whose
|
|
70
|
+
* rule has no metric, settles as verified with an explanatory result — the
|
|
71
|
+
* conservative direction is "leave the change in place", never "revert
|
|
72
|
+
* without evidence".
|
|
73
|
+
*/
|
|
74
|
+
export function evaluateAppliedEntry(db, entry, now) {
|
|
75
|
+
const appliedMs = Date.parse(entry.blob.applied_at);
|
|
76
|
+
if (Number.isNaN(appliedMs)) {
|
|
77
|
+
return { action: "verify", result: "invalid_applied_at" };
|
|
78
|
+
}
|
|
79
|
+
const windowEndMs = appliedMs + TUNING_VERIFY_WINDOW_DAYS * DAY_MS;
|
|
80
|
+
if (now.getTime() < windowEndMs)
|
|
81
|
+
return { action: "wait" };
|
|
82
|
+
const from = new Date(appliedMs);
|
|
83
|
+
const to = new Date(windowEndMs);
|
|
84
|
+
if (entry.blob.rule === "R1") {
|
|
85
|
+
if (!isR1Metric(entry.blob.baselineMetric)) {
|
|
86
|
+
return { action: "verify", result: "no_baseline" };
|
|
87
|
+
}
|
|
88
|
+
const baseline = entry.blob.baselineMetric;
|
|
89
|
+
const current = computeR1Metric(db, from, to);
|
|
90
|
+
if (baseline.noveltyGe2PerDay > 0 &&
|
|
91
|
+
current.noveltyGe2PerDay <
|
|
92
|
+
baseline.noveltyGe2PerDay * (1 - R1_NOVELTY_ARRIVALS_MAX_DROP)) {
|
|
93
|
+
return {
|
|
94
|
+
action: "revert",
|
|
95
|
+
reason: `novelty>=2 observation arrivals fell to ` +
|
|
96
|
+
`${current.noveltyGe2PerDay.toFixed(2)}/day vs baseline ` +
|
|
97
|
+
`${baseline.noveltyGe2PerDay.toFixed(2)}/day (>30% drop)`,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
if (current.cautiousEscalateShare >
|
|
101
|
+
baseline.cautiousEscalateShare + R1_CAUTIOUS_ESCALATE_MAX_RISE) {
|
|
102
|
+
return {
|
|
103
|
+
action: "revert",
|
|
104
|
+
reason: `cautious-escalate tick share rose to ` +
|
|
105
|
+
`${pct(current.cautiousEscalateShare)} vs baseline ` +
|
|
106
|
+
`${pct(baseline.cautiousEscalateShare)} (>10 pt rise)`,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
return { action: "verify", result: "pass" };
|
|
110
|
+
}
|
|
111
|
+
if (entry.blob.rule === "R3") {
|
|
112
|
+
const metric = computeR3Metric(db, from, to);
|
|
113
|
+
if (metric.stage0Ticks > 0 &&
|
|
114
|
+
metric.noveltyGe2 / metric.stage0Ticks > R3_SILENT_NOVELTY_GE2_MAX_SHARE) {
|
|
115
|
+
return {
|
|
116
|
+
action: "revert",
|
|
117
|
+
reason: `${metric.noveltyGe2}/${metric.stage0Ticks} silent ticks carried ` +
|
|
118
|
+
`maxNoveltyScore>=2 (>10% — harm from the raised ceiling)`,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
return { action: "verify", result: "pass" };
|
|
122
|
+
}
|
|
123
|
+
if (entry.blob.rule === "R5") {
|
|
124
|
+
const signals = countLessonRegressionSignals(db, from, to);
|
|
125
|
+
if (signals > 0) {
|
|
126
|
+
return {
|
|
127
|
+
action: "revert",
|
|
128
|
+
reason: `${signals} explicit-correction signal(s) cited a lesson within ` +
|
|
129
|
+
"the verify window (forgotten-lesson proxy)",
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
return { action: "verify", result: "pass" };
|
|
133
|
+
}
|
|
134
|
+
return { action: "verify", result: "no_metric" };
|
|
135
|
+
}
|
|
136
|
+
/** §3.4 — the one-line owner DM for an auto-revert. */
|
|
137
|
+
export function buildAutoRevertDmMessage(entry, reason) {
|
|
138
|
+
return (`Self-tuning auto-revert: restored ${entry.key} to ` +
|
|
139
|
+
`${String(entry.blob.prev)} — ${reason}. The key is now in a 28-day ` +
|
|
140
|
+
"re-proposal cool-down.");
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* The cron-tick entry point. Throttled to one pass per UTC day; the state
|
|
144
|
+
* write happens before the scan so a mid-pass failure waits for tomorrow
|
|
145
|
+
* instead of retrying every tick. Each entry is processed in isolation —
|
|
146
|
+
* one broken entry never blocks the rest.
|
|
147
|
+
*/
|
|
148
|
+
export async function runSelfTuningRevertMonitor(deps, now = new Date()) {
|
|
149
|
+
const today = now.toISOString().slice(0, 10);
|
|
150
|
+
const state = readRuntimeState(deps.db, REVERT_MONITOR_STATE_KEY);
|
|
151
|
+
if (state?.lastRunDay === today) {
|
|
152
|
+
return { ran: false, reverted: [], verified: [] };
|
|
153
|
+
}
|
|
154
|
+
writeRuntimeState(deps.db, REVERT_MONITOR_STATE_KEY, { lastRunDay: today });
|
|
155
|
+
const run = { ran: true, reverted: [], verified: [] };
|
|
156
|
+
const due = listLedgerEntries(deps.db).filter((entry) => entry.blob.actuator === "config" &&
|
|
157
|
+
entry.blob.reverted_at === undefined &&
|
|
158
|
+
entry.blob.verified_at === undefined);
|
|
159
|
+
for (const entry of due) {
|
|
160
|
+
try {
|
|
161
|
+
const decision = evaluateAppliedEntry(deps.db, entry, now);
|
|
162
|
+
if (decision.action === "wait")
|
|
163
|
+
continue;
|
|
164
|
+
if (decision.action === "revert") {
|
|
165
|
+
const result = await revertAppliedTuningChange(deps, entry, {
|
|
166
|
+
trigger: "auto",
|
|
167
|
+
reason: decision.reason,
|
|
168
|
+
now,
|
|
169
|
+
});
|
|
170
|
+
if (!result.ok) {
|
|
171
|
+
logger.warn({ key: entry.key, error: result.error }, "Auto-revert failed at the config chokepoint");
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
run.reverted.push(entry.key);
|
|
175
|
+
if (deps.sendDm) {
|
|
176
|
+
try {
|
|
177
|
+
await deps.sendDm(buildAutoRevertDmMessage(entry, decision.reason));
|
|
178
|
+
}
|
|
179
|
+
catch (err) {
|
|
180
|
+
logger.warn({ err, key: entry.key }, "Auto-revert DM failed");
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
logger.warn({ key: entry.key }, "Auto-revert applied without DM path — owner not notified");
|
|
185
|
+
}
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
// decision.action === "verify" — clean window (or no metric): stamp
|
|
189
|
+
// so the entry is never re-examined; revertability via
|
|
190
|
+
// `!revert tuning` is unaffected. Re-read before writing (same
|
|
191
|
+
// discipline as revertAppliedTuningChange): an `!revert tuning`
|
|
192
|
+
// landing between this pass's scan and this stamp must not have its
|
|
193
|
+
// `reverted_at` clobbered by the stale scanned blob — that would
|
|
194
|
+
// both resurrect the key as revertable and drop its 28d cool-down.
|
|
195
|
+
const current = readRuntimeState(deps.db, ledgerStateKey(entry.key)) ?? entry.blob;
|
|
196
|
+
writeRuntimeState(deps.db, ledgerStateKey(entry.key), {
|
|
197
|
+
...current,
|
|
198
|
+
verified_at: now.toISOString(),
|
|
199
|
+
verify_result: decision.result,
|
|
200
|
+
});
|
|
201
|
+
auditSelfTuning(deps.db, "self_tuning.verified", "autonomous", "success", {
|
|
202
|
+
key: entry.key,
|
|
203
|
+
rule: entry.blob.rule,
|
|
204
|
+
verifyResult: decision.result,
|
|
205
|
+
});
|
|
206
|
+
run.verified.push(entry.key);
|
|
207
|
+
}
|
|
208
|
+
catch (err) {
|
|
209
|
+
logger.warn({ err, key: entry.key }, "Revert-monitor entry failed");
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return run;
|
|
213
|
+
}
|
|
@@ -4,6 +4,12 @@ import type { EventBus } from "./event-bus.js";
|
|
|
4
4
|
import type { MessageHub } from "../adapters/message-hub.js";
|
|
5
5
|
import type { ObserverManager } from "../observers/manager.js";
|
|
6
6
|
export interface HealthStatus {
|
|
7
|
+
/**
|
|
8
|
+
* Seconds since daemon start. Every consumer of the `/health` `uptime`
|
|
9
|
+
* field (`bin/aitne.mjs formatUptime`, dashboard `formatUptime`)
|
|
10
|
+
* formats seconds — this was milliseconds until 2026-06-10, which made
|
|
11
|
+
* `aitne status` report a minutes-old daemon as days of uptime.
|
|
12
|
+
*/
|
|
7
13
|
daemonUptime: number;
|
|
8
14
|
eventBusSize: number;
|
|
9
15
|
activeSessions: number;
|
|
@@ -89,7 +89,7 @@ export class HealthMonitor {
|
|
|
89
89
|
dbConnected = false;
|
|
90
90
|
}
|
|
91
91
|
return {
|
|
92
|
-
daemonUptime: Date.now() - this.startedAt.getTime(),
|
|
92
|
+
daemonUptime: Math.floor((Date.now() - this.startedAt.getTime()) / 1000),
|
|
93
93
|
eventBusSize: this.eventBus.size,
|
|
94
94
|
activeSessions,
|
|
95
95
|
dbConnected,
|
|
@@ -83,7 +83,7 @@ export interface InjectionPolicy {
|
|
|
83
83
|
* redaction-aware wikilinks. Also drops the `*` policy-file merge
|
|
84
84
|
* because the lite-tier skill bundle never invokes MCP; the redaction
|
|
85
85
|
* policy is re-declared inline.
|
|
86
|
-
* - **
|
|
86
|
+
* - **Activity scan** (`routine.activity_scan`) — task-flow §"Execution
|
|
87
87
|
* budget" explicitly tells the agent NOT to read roadmap / projects /
|
|
88
88
|
* user files unless an observation warrants it.
|
|
89
89
|
* - **Today refresh** (`routine.today_refresh`) — dashboard-triggered
|
|
@@ -125,13 +125,13 @@ export declare function getInjectionPolicy(eventOrProcessKey: string): Injection
|
|
|
125
125
|
* global agent-operating behaviour — notification discipline, filter
|
|
126
126
|
* quality). Phase 3 consumer: `ContextBuilder`.
|
|
127
127
|
* - `slim` — use the hard-2048-byte, top-N-by-score variant on the hourly
|
|
128
|
-
* notify turn (§6). Only `routine.
|
|
128
|
+
* notify turn (§6). Only `routine.activity_scan` sets it. Implies `global`.
|
|
129
129
|
* - `self` — eligible for the per-agent `policies/agents/<slug>/lessons.md`
|
|
130
130
|
* block (scope `agent:<slug>`). **Phase 4 consumer.** The builder reads it
|
|
131
131
|
* next to `<agent_identity>` and gates it on a resolved, path-safe slug
|
|
132
132
|
* stamped onto `event.data.agentId` at the dispatch site — `self === true`
|
|
133
133
|
* here means "this surface *may* carry self lessons"; an actual injection
|
|
134
|
-
* additionally requires the run to be bound to an Agent. `
|
|
134
|
+
* additionally requires the run to be bound to an Agent. `activity_scan`
|
|
135
135
|
* keeps `self: false` so the slim notify turn never carries a second block.
|
|
136
136
|
*
|
|
137
137
|
* **Surface keying is grounded in the real event-type strings build() sees,
|
|
@@ -146,7 +146,7 @@ export declare function getInjectionPolicy(eventOrProcessKey: string): Injection
|
|
|
146
146
|
* no notifications — injecting lessons there would be wasted bytes against
|
|
147
147
|
* the §0 cost constraint. So Stage A is keyed, the umbrella and Stage B are
|
|
148
148
|
* not.
|
|
149
|
-
* - `routine.
|
|
149
|
+
* - `routine.activity_scan` is the escalated Stage-3 LLM/notify turn (gate
|
|
150
150
|
* Layers 1–3 are code and build no prompt), so the slim block bites exactly
|
|
151
151
|
* where the notify decision is made. The `.triage` lite classification is
|
|
152
152
|
* intentionally excluded.
|
|
@@ -59,7 +59,7 @@ const DEFAULT_POLICY = {
|
|
|
59
59
|
* redaction-aware wikilinks. Also drops the `*` policy-file merge
|
|
60
60
|
* because the lite-tier skill bundle never invokes MCP; the redaction
|
|
61
61
|
* policy is re-declared inline.
|
|
62
|
-
* - **
|
|
62
|
+
* - **Activity scan** (`routine.activity_scan`) — task-flow §"Execution
|
|
63
63
|
* budget" explicitly tells the agent NOT to read roadmap / projects /
|
|
64
64
|
* user files unless an observation warrants it.
|
|
65
65
|
* - **Today refresh** (`routine.today_refresh`) — dashboard-triggered
|
|
@@ -88,9 +88,9 @@ export function getInjectionPolicy(eventOrProcessKey) {
|
|
|
88
88
|
policyFileGlobalMerge: false,
|
|
89
89
|
};
|
|
90
90
|
}
|
|
91
|
-
// Narrow routines (
|
|
91
|
+
// Narrow routines (activity scan, today refresh) — drop both heavy
|
|
92
92
|
// blocks. `*` policy merge is preserved (redaction.md is non-negotiable).
|
|
93
|
-
if (eventOrProcessKey === "routine.
|
|
93
|
+
if (eventOrProcessKey === "routine.activity_scan" ||
|
|
94
94
|
eventOrProcessKey === "routine.today_refresh") {
|
|
95
95
|
return {
|
|
96
96
|
alwaysBlocks: NO_BLOCKS,
|
|
@@ -162,7 +162,7 @@ export function getAgentLessonsInjection(eventOrProcessKey, opts) {
|
|
|
162
162
|
case "routine.monthly_review":
|
|
163
163
|
return LESSONS_DM_REVIEW;
|
|
164
164
|
// Hourly notify turn — slim, hard-capped notification-discipline variant.
|
|
165
|
-
case "routine.
|
|
165
|
+
case "routine.activity_scan":
|
|
166
166
|
return LESSONS_HOURLY;
|
|
167
167
|
// Defined-agent task execution (§5 "Defined-agent execution"). A bare
|
|
168
168
|
// scheduled.task stays NONE (the §5 opt-out); one that resolves to an Agent
|
|
@@ -131,6 +131,10 @@ export function cascadeNativeBindingsOnMainSwitch(db, newMainBackendId) {
|
|
|
131
131
|
// against the same drift covered by the c8-ignored branch above.
|
|
132
132
|
/* c8 ignore next */
|
|
133
133
|
deniedTools: state.deniedTools ?? [],
|
|
134
|
+
// User configuration that must survive the disable/re-enable cycle —
|
|
135
|
+
// PATCH re-enables with `previous.fetchTargets`, so dropping it here
|
|
136
|
+
// would silently wipe the allowlist on a main-backend change.
|
|
137
|
+
fetchTargets: state.fetchTargets ?? [],
|
|
134
138
|
lastChangedAt: now,
|
|
135
139
|
});
|
|
136
140
|
flipped.push({
|
|
@@ -58,7 +58,7 @@ export declare function renderNoteSourcesSection(integrations: IntegrationsRecor
|
|
|
58
58
|
/**
|
|
59
59
|
* INTEGRATION_NATIVE_MODE_DESIGN.md §7.3 — render the full per-session
|
|
60
60
|
* routing table that the per-backend instruction file (`CLAUDE.md` /
|
|
61
|
-
* `AGENTS.md` / `GEMINI.md`) and the
|
|
61
|
+
* `AGENTS.md` / `GEMINI.md`) and the activity_scan / DM task-flow files
|
|
62
62
|
* substitute in for the `<integration-routing-table>` placeholder.
|
|
63
63
|
*
|
|
64
64
|
* Always renders every registered integration, even when all rows are
|
|
@@ -77,7 +77,7 @@ export declare function renderIntegrationRoutingTable(integrations: Integrations
|
|
|
77
77
|
* `native` rows; `disabled` rows are filtered out entirely so the
|
|
78
78
|
* task-flow's "for each integration" loop has zero iterations for them.
|
|
79
79
|
*
|
|
80
|
-
* This is what the
|
|
80
|
+
* This is what the activity_scan and DM task-flow files iterate over;
|
|
81
81
|
* the full {@link renderIntegrationRoutingTable} is for the instruction
|
|
82
82
|
* file's read-only audit summary.
|
|
83
83
|
*/
|
|
@@ -63,24 +63,53 @@ function consumeSelfWrite(absPath) {
|
|
|
63
63
|
return pendingSelfWrites.delete(absPath);
|
|
64
64
|
}
|
|
65
65
|
// ── Render ─────────────────────────────────────────────────────────────────
|
|
66
|
-
//
|
|
67
|
-
//
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
//
|
|
66
|
+
// Frontmatter contract. `policies/integrations.md` lives inside the vault
|
|
67
|
+
// under the `policies/` authority class, so it MUST satisfy the vault
|
|
68
|
+
// frontmatter validator (`context-frontmatter.ts`): `type: rule`,
|
|
69
|
+
// `owner ∈ {agent, shared, user}`, and an ISO `updated` date. Before the
|
|
70
|
+
// CONTEXT_VAULT_REDESIGN restructure this file lived at the un-validated
|
|
71
|
+
// `~/.personal-agent/integrations.md`, so it shipped a bespoke
|
|
72
|
+
// daemon-snapshot frontmatter (`owner: daemon`, no `type`/`updated`). The
|
|
73
|
+
// restructure moved it under `policies/` and added the generic `policies/`
|
|
74
|
+
// validation, but this renderer was never reconciled — leaving every
|
|
75
|
+
// install's file flagged "frontmatter requires `type`" by Vault Health.
|
|
76
|
+
//
|
|
77
|
+
// `owner` is `shared` because the file is a daemon-rendered snapshot of
|
|
78
|
+
// `settings.integrations_json` that the user may also hand-edit (chokidar
|
|
79
|
+
// reconciles edits back into the DB) — the same mixed authority as
|
|
80
|
+
// `policies/management.md`. The Dashboard (Settings → Connections) remains
|
|
81
|
+
// the canonical edit surface. See §14.3 of
|
|
71
82
|
// docs/design/14-integration-delegation.md.
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
83
|
+
//
|
|
84
|
+
// `updated` is derived from the most recent `lastChangedAt` across all
|
|
85
|
+
// integration rows (truncated to a calendar date) so the render stays a
|
|
86
|
+
// pure function of DB state: booting re-renders byte-identical output until
|
|
87
|
+
// a mode actually changes, preserving the idempotency contract above.
|
|
88
|
+
const FRONTMATTER_FALLBACK_UPDATED = "2026-04-17";
|
|
89
|
+
function renderFrontmatter(integrations) {
|
|
90
|
+
let latest = "";
|
|
91
|
+
for (const key of INTEGRATION_KEYS) {
|
|
92
|
+
const ts = integrations[key].lastChangedAt;
|
|
93
|
+
if (ts > latest)
|
|
94
|
+
latest = ts;
|
|
95
|
+
}
|
|
96
|
+
const updated = /^\d{4}-\d{2}-\d{2}/.test(latest)
|
|
97
|
+
? latest.slice(0, 10)
|
|
98
|
+
: FRONTMATTER_FALLBACK_UPDATED;
|
|
99
|
+
return `---
|
|
100
|
+
type: rule
|
|
101
|
+
slug: integrations
|
|
102
|
+
owner: shared
|
|
103
|
+
updated: ${updated}
|
|
76
104
|
schema_version: 1
|
|
77
105
|
---
|
|
78
106
|
`;
|
|
107
|
+
}
|
|
79
108
|
const MODES_SECTION = `## Modes
|
|
80
109
|
|
|
81
110
|
- **direct** — daemon holds credentials and polls; full feature set; setup required.
|
|
82
111
|
- **delegated** — daemon proxies a separate backend connector on a cadence; reduced features; zero setup.
|
|
83
|
-
- **native** — main backend's own native MCP / connector reaches the integration on-demand within the same DM /
|
|
112
|
+
- **native** — main backend's own native MCP / connector reaches the integration on-demand within the same DM / activity_scan turn; no daemon polling and no daemon-side proxy.
|
|
84
113
|
- **disabled** — integration off.
|
|
85
114
|
`;
|
|
86
115
|
function renderCurrentStateTable(integrations) {
|
|
@@ -132,15 +161,23 @@ export function renderNoteSourcesSection(integrations, notes) {
|
|
|
132
161
|
else if (notion.mode === "delegated" && notion.delegatedBackend) {
|
|
133
162
|
notionLine = `enabled (delegated via ${notion.delegatedBackend})`;
|
|
134
163
|
}
|
|
164
|
+
else if (notion.mode === "native" && notion.nativeBackend) {
|
|
165
|
+
notionLine = `enabled (native via ${notion.nativeBackend})`;
|
|
166
|
+
}
|
|
135
167
|
else {
|
|
136
168
|
notionLine = "enabled (direct)";
|
|
137
169
|
}
|
|
170
|
+
const notionTargets = (notion.fetchTargets ?? []).map((target) => target.label);
|
|
171
|
+
const notionTargetsLine = notionTargets.length > 0
|
|
172
|
+
? notionTargets.join(", ")
|
|
173
|
+
: "—";
|
|
138
174
|
return [
|
|
139
175
|
"## Note Sources",
|
|
140
176
|
"",
|
|
141
177
|
"<!-- Auto-generated. Edit settings via Dashboard → Settings → Note. Hand-edits are overwritten on next render. -->",
|
|
142
178
|
`- Obsidian vault (personal): ${obsidianLine}`,
|
|
143
179
|
`- Notion: ${notionLine}`,
|
|
180
|
+
`- Notion routine fetch targets: ${notionTargetsLine}`,
|
|
144
181
|
"",
|
|
145
182
|
].join("\n");
|
|
146
183
|
}
|
|
@@ -218,7 +255,7 @@ function renderToolDenySection(integrations) {
|
|
|
218
255
|
/**
|
|
219
256
|
* INTEGRATION_NATIVE_MODE_DESIGN.md §7.3 — render the full per-session
|
|
220
257
|
* routing table that the per-backend instruction file (`CLAUDE.md` /
|
|
221
|
-
* `AGENTS.md` / `GEMINI.md`) and the
|
|
258
|
+
* `AGENTS.md` / `GEMINI.md`) and the activity_scan / DM task-flow files
|
|
222
259
|
* substitute in for the `<integration-routing-table>` placeholder.
|
|
223
260
|
*
|
|
224
261
|
* Always renders every registered integration, even when all rows are
|
|
@@ -247,7 +284,7 @@ export function renderIntegrationRoutingTable(integrations) {
|
|
|
247
284
|
* `native` rows; `disabled` rows are filtered out entirely so the
|
|
248
285
|
* task-flow's "for each integration" loop has zero iterations for them.
|
|
249
286
|
*
|
|
250
|
-
* This is what the
|
|
287
|
+
* This is what the activity_scan and DM task-flow files iterate over;
|
|
251
288
|
* the full {@link renderIntegrationRoutingTable} is for the instruction
|
|
252
289
|
* file's read-only audit summary.
|
|
253
290
|
*/
|
|
@@ -381,7 +418,7 @@ export function renderManagementMd(integrations, notes = {
|
|
|
381
418
|
externalObsidianWatch: true,
|
|
382
419
|
}) {
|
|
383
420
|
return [
|
|
384
|
-
|
|
421
|
+
renderFrontmatter(integrations),
|
|
385
422
|
"# Integration Management\n",
|
|
386
423
|
MODES_SECTION,
|
|
387
424
|
renderCurrentStateTable(integrations),
|
|
@@ -666,6 +703,7 @@ function mergeParsedIntoDb(dbState, parsed) {
|
|
|
666
703
|
if (semanticChange) {
|
|
667
704
|
merged[key] = {
|
|
668
705
|
...next,
|
|
706
|
+
fetchTargets: prev.fetchTargets ?? [],
|
|
669
707
|
lastChangedAt: new Date().toISOString(),
|
|
670
708
|
};
|
|
671
709
|
}
|
|
@@ -110,7 +110,7 @@ export interface MorningPipelineOrchestratorDeps {
|
|
|
110
110
|
* morning-routine-optimization.md Phase 6 — ⑥ AgentJournalAppender
|
|
111
111
|
* needs the safety write-tracker so the journal's atomic write does
|
|
112
112
|
* not get tagged as a user-actor change by the obsidian / git
|
|
113
|
-
* observers (which would re-trigger the
|
|
113
|
+
* observers (which would re-trigger the activity scan on the agent's
|
|
114
114
|
* own output). The context-index reconciler is intentionally NOT
|
|
115
115
|
* threaded here: `journal/agent.md` is not in the indexable set, so
|
|
116
116
|
* the chokidar fallback path covers it without an explicit hint.
|
|
@@ -254,7 +254,7 @@ export declare class MorningRoutinePipelineOrchestrator {
|
|
|
254
254
|
* (audit's internal try/catch swallowed a real SQLite error AND
|
|
255
255
|
* `processResult`'s notification path threw too), the parent-audit
|
|
256
256
|
* emitter will return `stage_a_row_missing` and the pre-routine gate
|
|
257
|
-
* stays unfired for the day — that day's
|
|
257
|
+
* stays unfired for the day — that day's activity_scan / evening_review
|
|
258
258
|
* are skipped with `morning_routine_pending_for_today`, but
|
|
259
259
|
* `MAX_RETRIES`-bounded `scheduleMorningRetry` does NOT loop on this
|
|
260
260
|
* shape because today.md health is independent. The day's automation
|
|
@@ -358,7 +358,7 @@ export class MorningRoutinePipelineOrchestrator {
|
|
|
358
358
|
* (audit's internal try/catch swallowed a real SQLite error AND
|
|
359
359
|
* `processResult`'s notification path threw too), the parent-audit
|
|
360
360
|
* emitter will return `stage_a_row_missing` and the pre-routine gate
|
|
361
|
-
* stays unfired for the day — that day's
|
|
361
|
+
* stays unfired for the day — that day's activity_scan / evening_review
|
|
362
362
|
* are skipped with `morning_routine_pending_for_today`, but
|
|
363
363
|
* `MAX_RETRIES`-bounded `scheduleMorningRetry` does NOT loop on this
|
|
364
364
|
* shape because today.md health is independent. The day's automation
|
|
@@ -993,7 +993,7 @@ export class MorningRoutinePipelineOrchestrator {
|
|
|
993
993
|
// could still call `Write` on `daily/<date>.md` directly —
|
|
994
994
|
// bypassing the daemon-side `DailyJournalComposer` chokepoint
|
|
995
995
|
// and racing it. Mirrors the precedent at
|
|
996
|
-
// `dispatcher-
|
|
996
|
+
// `dispatcher-activity-scan.ts:1003` (`routine.activity_scan.triage`).
|
|
997
997
|
//
|
|
998
998
|
// Activation requires the clamp gate in `claude-code-core.ts`
|
|
999
999
|
// to honour an empty array as "no tools" — fixed in the same
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Outbound-notification gate — QUIET_HOURS_HARDENING_PLAN.md Phase 1.
|
|
3
|
+
*
|
|
4
|
+
* Single decision function for the API-deps `sendNotification` chokepoint
|
|
5
|
+
* (`bootstrap/api.ts`), closing finding F1: `POST /api/notify` used to
|
|
6
|
+
* bypass quiet hours AND rate limits without the explicit-user-intent
|
|
7
|
+
* justification the other delivery paths encode. The intent rule —
|
|
8
|
+
* "explicit user-chosen time → deliver regardless of quiet hours; ambient
|
|
9
|
+
* autonomous output → suppress/defer" — now holds here too:
|
|
10
|
+
*
|
|
11
|
+
* 1. safety / critical → send immediately (mirrors NotificationManager);
|
|
12
|
+
* 2. inside quiet hours → defer to a `task_type='dm'` agent_schedule row
|
|
13
|
+
* at the quiet-hours edge (durable, coalesced per origin — see
|
|
14
|
+
* `db/deferred-dm.ts`), never silently dropped;
|
|
15
|
+
* 3. outside quiet hours → enforce the same hourly/daily rate limits the
|
|
16
|
+
* proactive path enforces; the live session gets a `rate_limit`
|
|
17
|
+
* verdict it can adapt to (write to today.md instead) rather than a
|
|
18
|
+
* silent queue.
|
|
19
|
+
*
|
|
20
|
+
* Pure composition over covered helpers — keep glue out of bootstrap.
|
|
21
|
+
*/
|
|
22
|
+
import type Database from "better-sqlite3";
|
|
23
|
+
/** Safety categories bypass quiet hours and user preferences. Owned here
|
|
24
|
+
* so the NotificationManager and this gate share one list. */
|
|
25
|
+
export declare const SAFETY_CATEGORIES: readonly ["security", "deadline", "error", "critical"];
|
|
26
|
+
export interface OutboundGateConfig {
|
|
27
|
+
quietHoursStart: string;
|
|
28
|
+
quietHoursEnd: string;
|
|
29
|
+
/** IANA tz; empty string falls back to system timezone. */
|
|
30
|
+
timezone: string;
|
|
31
|
+
maxNotificationsPerHour: number;
|
|
32
|
+
maxNotificationsPerDay: number;
|
|
33
|
+
dayBoundaryHour: number;
|
|
34
|
+
}
|
|
35
|
+
export interface OutboundGateParams {
|
|
36
|
+
message: string;
|
|
37
|
+
platforms?: string[] | undefined;
|
|
38
|
+
priority?: string | undefined;
|
|
39
|
+
notificationType?: string | undefined;
|
|
40
|
+
originSessionId?: number | undefined;
|
|
41
|
+
agentId?: string | null | undefined;
|
|
42
|
+
/** Origin marker stamped into the deferred row, e.g. `"api.notify"`. */
|
|
43
|
+
deferredFrom: string;
|
|
44
|
+
}
|
|
45
|
+
export type OutboundGateResult = {
|
|
46
|
+
action: "send";
|
|
47
|
+
} | {
|
|
48
|
+
action: "defer";
|
|
49
|
+
scheduleId: string;
|
|
50
|
+
/** SQLite-format UTC datetime the deferred DM fires at. */
|
|
51
|
+
deliverAfter: string;
|
|
52
|
+
coalesced: boolean;
|
|
53
|
+
} | {
|
|
54
|
+
action: "rate_limit";
|
|
55
|
+
retryAfter: string | null;
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* Critical priority and safety-tagged notification types deliver
|
|
59
|
+
* immediately — same bypass set as `NotificationManager.isSafetyCategory`
|
|
60
|
+
* ("urgent" accepted defensively; the notify schema only emits
|
|
61
|
+
* critical/high/normal/low).
|
|
62
|
+
*/
|
|
63
|
+
export declare function bypassesOutboundGate(priority: string | undefined, notificationType: string | undefined): boolean;
|
|
64
|
+
export declare function gateOutboundNotification(db: Database.Database, config: OutboundGateConfig, params: OutboundGateParams, now?: Date): OutboundGateResult;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { deferDmToQuietHoursEnd } from "../db/deferred-dm.js";
|
|
2
|
+
import { evaluateNotificationRateLimit, } from "./notification-rate-limit.js";
|
|
3
|
+
/** Safety categories bypass quiet hours and user preferences. Owned here
|
|
4
|
+
* so the NotificationManager and this gate share one list. */
|
|
5
|
+
export const SAFETY_CATEGORIES = [
|
|
6
|
+
"security",
|
|
7
|
+
"deadline",
|
|
8
|
+
"error",
|
|
9
|
+
"critical",
|
|
10
|
+
];
|
|
11
|
+
/**
|
|
12
|
+
* Critical priority and safety-tagged notification types deliver
|
|
13
|
+
* immediately — same bypass set as `NotificationManager.isSafetyCategory`
|
|
14
|
+
* ("urgent" accepted defensively; the notify schema only emits
|
|
15
|
+
* critical/high/normal/low).
|
|
16
|
+
*/
|
|
17
|
+
export function bypassesOutboundGate(priority, notificationType) {
|
|
18
|
+
if (priority === "critical" || priority === "urgent")
|
|
19
|
+
return true;
|
|
20
|
+
return (notificationType !== undefined &&
|
|
21
|
+
SAFETY_CATEGORIES.includes(notificationType));
|
|
22
|
+
}
|
|
23
|
+
export function gateOutboundNotification(db, config, params, now = new Date()) {
|
|
24
|
+
if (bypassesOutboundGate(params.priority, params.notificationType)) {
|
|
25
|
+
return { action: "send" };
|
|
26
|
+
}
|
|
27
|
+
const deferred = deferDmToQuietHoursEnd(db, {
|
|
28
|
+
start: config.quietHoursStart,
|
|
29
|
+
end: config.quietHoursEnd,
|
|
30
|
+
timezone: config.timezone || undefined,
|
|
31
|
+
}, {
|
|
32
|
+
message: params.message,
|
|
33
|
+
platforms: params.platforms,
|
|
34
|
+
deferredFrom: params.deferredFrom,
|
|
35
|
+
originSessionId: params.originSessionId,
|
|
36
|
+
agentId: params.agentId,
|
|
37
|
+
}, now);
|
|
38
|
+
if (deferred !== null) {
|
|
39
|
+
return { action: "defer", ...deferred };
|
|
40
|
+
}
|
|
41
|
+
const rateLimit = evaluateNotificationRateLimit(db, {
|
|
42
|
+
maxNotificationsPerHour: config.maxNotificationsPerHour,
|
|
43
|
+
maxNotificationsPerDay: config.maxNotificationsPerDay,
|
|
44
|
+
timezone: config.timezone,
|
|
45
|
+
dayBoundaryHour: config.dayBoundaryHour,
|
|
46
|
+
}, now);
|
|
47
|
+
if (rateLimit.limited) {
|
|
48
|
+
return { action: "rate_limit", retryAfter: rateLimit.retryAfter };
|
|
49
|
+
}
|
|
50
|
+
return { action: "send" };
|
|
51
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Outbound-notification rate-limit evaluation — pure(ish) helper shared by
|
|
3
|
+
* NotificationManager (proactive suppression) and the `/api/notify` gate
|
|
4
|
+
* (QUIET_HOURS_HARDENING_PLAN.md Phase 1). Extracted so the two call sites
|
|
5
|
+
* cannot drift on the counting semantics: distinct dispatches, delivered
|
|
6
|
+
* only, `message.received` replies excluded, hourly window + agent-day
|
|
7
|
+
* window both enforced.
|
|
8
|
+
*
|
|
9
|
+
* 100% covered. NotificationManager itself stays excluded from the
|
|
10
|
+
* coverage gate as I/O-heavy; this helper is the pure leg it shares with
|
|
11
|
+
* the notify-route gate.
|
|
12
|
+
*/
|
|
13
|
+
import type Database from "better-sqlite3";
|
|
14
|
+
export interface NotificationRateLimitOptions {
|
|
15
|
+
maxNotificationsPerHour: number;
|
|
16
|
+
maxNotificationsPerDay: number;
|
|
17
|
+
/** IANA tz; empty/undefined falls back to system timezone. */
|
|
18
|
+
timezone?: string | undefined;
|
|
19
|
+
/** Agent day boundary hour (default config: 4). */
|
|
20
|
+
dayBoundaryHour: number;
|
|
21
|
+
}
|
|
22
|
+
export interface NotificationRateLimitState {
|
|
23
|
+
limited: boolean;
|
|
24
|
+
/**
|
|
25
|
+
* SQLite-format UTC datetime (`YYYY-MM-DD HH:MM:SS`) when a retry could
|
|
26
|
+
* succeed, or `null` when not limited. Hourly limit → the moment the
|
|
27
|
+
* oldest delivery in the trailing hour ages out of the window; daily
|
|
28
|
+
* limit → the agent-day end boundary. Advisory — the caller's retry can
|
|
29
|
+
* still lose to a concurrent delivery.
|
|
30
|
+
*/
|
|
31
|
+
retryAfter: string | null;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Count semantics mirror the pre-extraction `NotificationManager`
|
|
35
|
+
* implementation byte-for-byte: a multi-channel dispatch counts once
|
|
36
|
+
* (DISTINCT on dispatch_id, falling back to the row id for legacy rows
|
|
37
|
+
* with an empty dispatch_id), only `delivered` rows count, and
|
|
38
|
+
* `message.received` reply forwards never count against proactive budget.
|
|
39
|
+
*/
|
|
40
|
+
export declare function evaluateNotificationRateLimit(db: Database.Database, opts: NotificationRateLimitOptions, now?: Date): NotificationRateLimitState;
|