@aitne/daemon 0.1.9 → 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.d.ts +1 -0
- package/dist/api/env-writer.js +17 -7
- 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-schedule.js +5 -1
- 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/apple-calendar.js +4 -1
- 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/calendar.js +12 -2
- package/dist/api/routes/context/path-resolve.js +6 -1
- package/dist/api/routes/context/permissions.js +12 -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 +58 -12
- package/dist/api/routes/dashboard/cost-approvals.js +66 -0
- package/dist/api/routes/dashboard/notifications.js +9 -9
- package/dist/api/routes/dashboard/oauth-google.js +5 -3
- package/dist/api/routes/feedback.d.ts +3 -0
- package/dist/api/routes/feedback.js +349 -0
- package/dist/api/routes/git.js +10 -3
- package/dist/api/routes/github.js +5 -1
- package/dist/api/routes/integrations/crud-patch.js +5 -1
- package/dist/api/routes/integrations-reconcile.js +2 -2
- package/dist/api/routes/mcp.js +65 -13
- 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 +12 -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 +246 -8
- 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 +32 -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 +38 -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 +47 -18
- 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 +193 -5
- 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 +11 -1
- package/dist/core/context-paths.js +17 -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 +50 -1
- 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 +24 -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 +104 -1
- package/dist/core/dispatcher-scheduled-tasks.js +480 -8
- 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 +297 -60
- package/dist/core/dm-freshness-metrics.d.ts +1 -1
- package/dist/core/drift-effects.js +2 -2
- package/dist/core/feedback/consolidation-prep.d.ts +94 -0
- package/dist/core/feedback/consolidation-prep.js +254 -0
- package/dist/core/feedback/eviction-scorer.d.ts +81 -0
- package/dist/core/feedback/eviction-scorer.js +136 -0
- package/dist/core/feedback/lesson-format.d.ts +79 -0
- package/dist/core/feedback/lesson-format.js +199 -0
- package/dist/core/feedback/lesson-injection.d.ts +98 -0
- package/dist/core/feedback/lesson-injection.js +174 -0
- package/dist/core/feedback/lesson-merge.d.ts +51 -0
- package/dist/core/feedback/lesson-merge.js +88 -0
- package/dist/core/feedback/lesson-store-overview.d.ts +46 -0
- package/dist/core/feedback/lesson-store-overview.js +42 -0
- package/dist/core/feedback/promotion-gate.d.ts +69 -0
- package/dist/core/feedback/promotion-gate.js +117 -0
- package/dist/core/feedback/regeneralization-prep.d.ts +87 -0
- package/dist/core/feedback/regeneralization-prep.js +152 -0
- package/dist/core/feedback/scope-parser.d.ts +86 -0
- package/dist/core/feedback/scope-parser.js +141 -0
- 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 +83 -1
- package/dist/core/injection-policy.js +61 -3
- 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 +51 -1
- package/dist/core/signal-detector.js +321 -24
- 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 +60 -14
- package/dist/core/today-direct-writer.js +90 -13
- 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/wiki/wiki-fts.js +13 -6
- 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/feedback-signals-store.d.ts +77 -0
- package/dist/db/feedback-signals-store.js +144 -0
- package/dist/db/migrations.js +380 -0
- package/dist/db/observations.d.ts +2 -2
- package/dist/db/observations.js +3 -3
- package/dist/db/schema.js +260 -22
- 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/always-disallowed.d.ts +1 -1
- package/dist/safety/always-disallowed.js +39 -0
- 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 +97 -18
- 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 +34 -8
- package/dist/services/browser-history/lifecycle/platform.js +44 -2
- 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/mcp/probe.js +30 -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 +45 -12
- package/dist/settings/runtime-settings.js +215 -40
- package/dist/settings/settings-store.js +11 -3
- package/package.json +4 -4
|
@@ -45,6 +45,7 @@ function fromDbRow(row) {
|
|
|
45
45
|
createdAt: row.created_at,
|
|
46
46
|
startedAt: row.started_at,
|
|
47
47
|
finishedAt: row.finished_at,
|
|
48
|
+
deliveredAt: row.delivered_at,
|
|
48
49
|
};
|
|
49
50
|
}
|
|
50
51
|
/** Insert a fresh row in state=pending. The slot manager promotes it
|
|
@@ -55,8 +56,8 @@ export function createBrowserTask(db, input) {
|
|
|
55
56
|
originating_channel, schedule_row_id, require_final_confirm,
|
|
56
57
|
state, outcome_detail, report, effective_allowlist_regex,
|
|
57
58
|
blocked_requests_count, extract_chars_total,
|
|
58
|
-
created_at, started_at, finished_at)
|
|
59
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, 'pending', NULL, NULL, ?, 0, 0, ?, NULL, NULL)`).run(input.id, input.description, input.siteKey, JSON.stringify([...input.extraAllowedHosts]), input.originatingChannel, input.scheduleRowId, input.requireFinalConfirm ? 1 : 0, input.effectiveAllowlistRegex, input.createdAt);
|
|
59
|
+
created_at, started_at, finished_at, delivered_at)
|
|
60
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, 'pending', NULL, NULL, ?, 0, 0, ?, NULL, NULL, NULL)`).run(input.id, input.description, input.siteKey, JSON.stringify([...input.extraAllowedHosts]), input.originatingChannel, input.scheduleRowId, input.requireFinalConfirm ? 1 : 0, input.effectiveAllowlistRegex, input.createdAt);
|
|
60
61
|
const row = getBrowserTask(db, input.id);
|
|
61
62
|
if (!row) {
|
|
62
63
|
throw new Error(`createBrowserTask: post-insert row for ${input.id} missing`);
|
|
@@ -69,7 +70,7 @@ export function getBrowserTask(db, id) {
|
|
|
69
70
|
originating_channel, schedule_row_id, require_final_confirm,
|
|
70
71
|
state, outcome_detail, report, effective_allowlist_regex,
|
|
71
72
|
blocked_requests_count, extract_chars_total,
|
|
72
|
-
created_at, started_at, finished_at
|
|
73
|
+
created_at, started_at, finished_at, delivered_at
|
|
73
74
|
FROM browser_task
|
|
74
75
|
WHERE id = ?`)
|
|
75
76
|
.get(id);
|
|
@@ -96,7 +97,7 @@ export function listBrowserTasks(db, options = {}) {
|
|
|
96
97
|
originating_channel, schedule_row_id, require_final_confirm,
|
|
97
98
|
state, outcome_detail, report, effective_allowlist_regex,
|
|
98
99
|
blocked_requests_count, extract_chars_total,
|
|
99
|
-
created_at, started_at, finished_at
|
|
100
|
+
created_at, started_at, finished_at, delivered_at
|
|
100
101
|
FROM browser_task
|
|
101
102
|
${where.length ? `WHERE ${where.join(" AND ")}` : ""}
|
|
102
103
|
ORDER BY created_at DESC
|
|
@@ -183,6 +184,30 @@ export function markTerminal(db, input) {
|
|
|
183
184
|
.run(input.state, input.outcomeDetail, input.report, input.finishedAt, input.id);
|
|
184
185
|
return result.changes > 0 ? getBrowserTask(db, input.id) : null;
|
|
185
186
|
}
|
|
187
|
+
export function markBrowserTaskDelivered(db, id, deliveredAt) {
|
|
188
|
+
const result = db
|
|
189
|
+
.prepare(`UPDATE browser_task
|
|
190
|
+
SET delivered_at = COALESCE(delivered_at, ?)
|
|
191
|
+
WHERE id = ?`)
|
|
192
|
+
.run(deliveredAt, id);
|
|
193
|
+
return result.changes > 0 ? getBrowserTask(db, id) : null;
|
|
194
|
+
}
|
|
195
|
+
export function listUndeliveredBrowserTaskReports(db, limit = 20) {
|
|
196
|
+
const rows = db
|
|
197
|
+
.prepare(`SELECT id, description, site_key, extra_allowed_hosts_json,
|
|
198
|
+
originating_channel, schedule_row_id, require_final_confirm,
|
|
199
|
+
state, outcome_detail, report, effective_allowlist_regex,
|
|
200
|
+
blocked_requests_count, extract_chars_total,
|
|
201
|
+
created_at, started_at, finished_at, delivered_at
|
|
202
|
+
FROM browser_task
|
|
203
|
+
WHERE state = 'completed'
|
|
204
|
+
AND report IS NOT NULL
|
|
205
|
+
AND delivered_at IS NULL
|
|
206
|
+
ORDER BY finished_at ASC, created_at ASC
|
|
207
|
+
LIMIT ?`)
|
|
208
|
+
.all(limit);
|
|
209
|
+
return rows.map(fromDbRow);
|
|
210
|
+
}
|
|
186
211
|
/** Increment the per-task CDP-blocked counter. Atomic. */
|
|
187
212
|
export function incrementBlockedRequests(db, id, by) {
|
|
188
213
|
db.prepare(`UPDATE browser_task
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Quiet-hours DM deferral — QUIET_HOURS_HARDENING_PLAN.md Phase 1.
|
|
3
|
+
*
|
|
4
|
+
* When an autonomous outbound DM fires inside the quiet-hours window, the
|
|
5
|
+
* message is NOT sent and NOT dropped: it is persisted as a
|
|
6
|
+
* `task_type='dm'` `agent_schedule` row scheduled for the quiet-hours end
|
|
7
|
+
* boundary. That reuses the existing zero-cost pre-composed-DM machinery
|
|
8
|
+
* end to end — full message in `task_description` (no truncation),
|
|
9
|
+
* restart-safe, visible on the `/schedule` dashboard page, delivered by
|
|
10
|
+
* the scheduler's `handleDirectDm` (whose deliberate quiet-hours skip is
|
|
11
|
+
* correct here: the row fires *at* the quiet-hours edge).
|
|
12
|
+
*
|
|
13
|
+
* Pile-up guard: a second deferral from the same origin (same
|
|
14
|
+
* `deferred_from` + agent id / origin session id) appends to the existing
|
|
15
|
+
* pending row (two-blank-line join, the batch-flush convention) instead
|
|
16
|
+
* of inserting a sibling — an hourly Agent firing five times overnight
|
|
17
|
+
* yields one combined DM at the edge, not five.
|
|
18
|
+
*
|
|
19
|
+
* Shared by the `/api/notify` gate (Phase 1) and, later, the
|
|
20
|
+
* NotificationManager `scheduled.task` final-text branch (Phase 1b) —
|
|
21
|
+
* one implementation instead of bespoke `agent_schedule` SQL per caller.
|
|
22
|
+
*/
|
|
23
|
+
import type Database from "better-sqlite3";
|
|
24
|
+
import { type QuietHoursWindow } from "../core/quiet-hours.js";
|
|
25
|
+
export interface DeferDmParams {
|
|
26
|
+
/** Full message body — persisted untruncated in `task_description`. */
|
|
27
|
+
message: string;
|
|
28
|
+
/** Explicit platform targets; omitted → MessageHub default destinations. */
|
|
29
|
+
platforms?: string[] | undefined;
|
|
30
|
+
/** Origin marker for audit + coalescing, e.g. `"api.notify"`. */
|
|
31
|
+
deferredFrom: string;
|
|
32
|
+
/** Session that produced the message, when known. */
|
|
33
|
+
originSessionId?: number | undefined;
|
|
34
|
+
/** Owning user-Agent slug, when resolvable. */
|
|
35
|
+
agentId?: string | null | undefined;
|
|
36
|
+
}
|
|
37
|
+
export interface DeferredDmResult {
|
|
38
|
+
scheduleId: string;
|
|
39
|
+
/** SQLite-format UTC datetime the row fires at (quiet-hours end). */
|
|
40
|
+
deliverAfter: string;
|
|
41
|
+
/** True when appended to an existing pending deferred row. */
|
|
42
|
+
coalesced: boolean;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Gate decision + deferred-row insert. Returns `null` when `now` is NOT
|
|
46
|
+
* inside the quiet-hours window (caller proceeds with the immediate
|
|
47
|
+
* path); otherwise persists the message for the quiet-hours edge and
|
|
48
|
+
* returns the row handle.
|
|
49
|
+
*/
|
|
50
|
+
export declare function deferDmToQuietHoursEnd(db: Database.Database, window: QuietHoursWindow, params: DeferDmParams, now?: Date): DeferredDmResult | null;
|
|
51
|
+
/**
|
|
52
|
+
* Retime pending quiet-hours-deferred DM rows after a quiet-hours config
|
|
53
|
+
* change (the `syncDmSessionTimesToQuietHours` sibling for this module's
|
|
54
|
+
* rows). The deferral premise — "the row fires *at* the quiet-hours edge,
|
|
55
|
+
* so `handleDirectDm`'s quiet-hours skip is correct" — only holds while
|
|
56
|
+
* the window that produced `scheduled_for` is still the configured one:
|
|
57
|
+
*
|
|
58
|
+
* - window extended (end moved later) → without retiming the row fires
|
|
59
|
+
* *inside* the new quiet window;
|
|
60
|
+
* - window shortened (end moved earlier) → the row waits past the new
|
|
61
|
+
* edge for no reason.
|
|
62
|
+
*
|
|
63
|
+
* Rule: inside the new window every deferred row moves to the new edge;
|
|
64
|
+
* outside it, future-dated rows are pulled up to `now` (the next scheduler
|
|
65
|
+
* tick delivers them) and already-due rows are left for the tick to claim.
|
|
66
|
+
* User-scheduled `dm` rows (no `deferred_from` marker) are never touched.
|
|
67
|
+
* Returns the number of rows retimed.
|
|
68
|
+
*/
|
|
69
|
+
export declare function retimeDeferredDmRows(db: Database.Database, window: QuietHoursWindow, now?: Date): number;
|
|
70
|
+
/**
|
|
71
|
+
* Retime pending quiet-hours-deferred RUN rows (`agent.task` opt-in and
|
|
72
|
+
* `browser_task`) after a quiet-hours config change — the
|
|
73
|
+
* `retimeDeferredDmRows` sibling for rows the ScheduleWatcher's
|
|
74
|
+
* `deferClaimedRowForQuietHours` pushed to the old window's end. That helper
|
|
75
|
+
* stamps `task_context.quiet_hours_deferred` at deferral time for exactly
|
|
76
|
+
* this purpose: rows that merely *carry* the `defer_in_quiet_hours` opt-in on
|
|
77
|
+
* a future cron slot were never deferred and keep their cron-scheduled time.
|
|
78
|
+
*
|
|
79
|
+
* Unlike deferred DM rows (delivered by `handleDirectDm`, which skips the
|
|
80
|
+
* quiet-hours check by design), run rows re-check the window at claim time,
|
|
81
|
+
* so a *widened* window self-corrects — retiming it here merely skips the
|
|
82
|
+
* wasted claim/re-defer cycle and its duplicate audit row. The
|
|
83
|
+
* narrowed/disabled direction is the real fix: without retiming, the run
|
|
84
|
+
* waits at the old window's end for no reason.
|
|
85
|
+
*/
|
|
86
|
+
export declare function retimeDeferredRunRows(db: Database.Database, window: QuietHoursWindow, now?: Date): number;
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { formatSqliteDatetime } from "@aitne/shared";
|
|
2
|
+
import { nextQuietHoursEndMs, } from "../core/quiet-hours.js";
|
|
3
|
+
import { createLogger } from "../logging.js";
|
|
4
|
+
const logger = createLogger("deferred-dm");
|
|
5
|
+
/**
|
|
6
|
+
* Coalescing identity: prefer the Agent slug (stable across an Agent's
|
|
7
|
+
* overnight firings), fall back to the origin session, else an anonymous
|
|
8
|
+
* shared bucket. Unrelated anonymous system DMs deferring into one
|
|
9
|
+
* combined message is intended — that's the pile-up guard, not a bug.
|
|
10
|
+
*/
|
|
11
|
+
function coalesceKey(agentId, originSessionId) {
|
|
12
|
+
if (agentId)
|
|
13
|
+
return `agent:${agentId}`;
|
|
14
|
+
if (originSessionId !== null && originSessionId !== undefined) {
|
|
15
|
+
return `session:${originSessionId}`;
|
|
16
|
+
}
|
|
17
|
+
return "anonymous";
|
|
18
|
+
}
|
|
19
|
+
/** Union of explicit platform targets; either side defaulting (null /
|
|
20
|
+
* undefined = MessageHub default destinations) keeps the default. */
|
|
21
|
+
function mergePlatforms(existing, incoming) {
|
|
22
|
+
if (!Array.isArray(existing) || incoming === undefined)
|
|
23
|
+
return null;
|
|
24
|
+
return [...new Set([...existing, ...incoming])];
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Gate decision + deferred-row insert. Returns `null` when `now` is NOT
|
|
28
|
+
* inside the quiet-hours window (caller proceeds with the immediate
|
|
29
|
+
* path); otherwise persists the message for the quiet-hours edge and
|
|
30
|
+
* returns the row handle.
|
|
31
|
+
*/
|
|
32
|
+
export function deferDmToQuietHoursEnd(db, window, params, now = new Date()) {
|
|
33
|
+
const quietEndMs = nextQuietHoursEndMs(now, window);
|
|
34
|
+
if (quietEndMs === null)
|
|
35
|
+
return null;
|
|
36
|
+
const key = coalesceKey(params.agentId, params.originSessionId);
|
|
37
|
+
// `json_valid` guard first: a hand-edited row with broken JSON would
|
|
38
|
+
// otherwise make `json_extract` throw for the whole query. Such a row
|
|
39
|
+
// simply never coalesces; a fresh row carries this message.
|
|
40
|
+
const pending = db
|
|
41
|
+
.prepare(`SELECT id, task_description, task_context, scheduled_for
|
|
42
|
+
FROM agent_schedule
|
|
43
|
+
WHERE status = 'pending'
|
|
44
|
+
AND task_type = 'dm'
|
|
45
|
+
AND task_context IS NOT NULL
|
|
46
|
+
AND json_valid(task_context)
|
|
47
|
+
AND json_extract(task_context, '$.deferred_from') = ?
|
|
48
|
+
ORDER BY id ASC`)
|
|
49
|
+
.all(params.deferredFrom);
|
|
50
|
+
for (const row of pending) {
|
|
51
|
+
// json_valid in the WHERE clause guarantees parseability here.
|
|
52
|
+
const ctx = JSON.parse(row.task_context);
|
|
53
|
+
const rowKey = coalesceKey(typeof ctx.agent_id === "string" ? ctx.agent_id : null, typeof ctx.origin_session_id === "number" ? ctx.origin_session_id : null);
|
|
54
|
+
if (rowKey !== key)
|
|
55
|
+
continue;
|
|
56
|
+
ctx.platforms = mergePlatforms(ctx.platforms, params.platforms);
|
|
57
|
+
db.prepare(`UPDATE agent_schedule
|
|
58
|
+
SET task_description = ?, task_context = ?
|
|
59
|
+
WHERE id = ?`).run(`${row.task_description}\n\n${params.message}`, JSON.stringify(ctx), row.id);
|
|
60
|
+
const result = {
|
|
61
|
+
scheduleId: String(row.id),
|
|
62
|
+
deliverAfter: row.scheduled_for,
|
|
63
|
+
coalesced: true,
|
|
64
|
+
};
|
|
65
|
+
recordDeferralAudit(db, window, params, result);
|
|
66
|
+
return result;
|
|
67
|
+
}
|
|
68
|
+
const deliverAfter = formatSqliteDatetime(new Date(quietEndMs));
|
|
69
|
+
const taskContext = {
|
|
70
|
+
platforms: params.platforms ?? null,
|
|
71
|
+
// Matches the `/schedule/dm` default — deferred pings stay out of
|
|
72
|
+
// roadmap `Scheduled:` entries.
|
|
73
|
+
importance: "transient",
|
|
74
|
+
deferred_from: params.deferredFrom,
|
|
75
|
+
...(params.originSessionId !== undefined
|
|
76
|
+
? { origin_session_id: params.originSessionId }
|
|
77
|
+
: {}),
|
|
78
|
+
...(params.agentId ? { agent_id: params.agentId } : {}),
|
|
79
|
+
};
|
|
80
|
+
const inserted = db
|
|
81
|
+
.prepare(
|
|
82
|
+
// task_type='dm' is consumed directly by `handleDirectDm` — the LLM
|
|
83
|
+
// never runs, so `model` is NULL (same shape as POST /schedule/dm).
|
|
84
|
+
`INSERT INTO agent_schedule (scheduled_for, task_type, task_description, task_context, model, status)
|
|
85
|
+
VALUES (?, 'dm', ?, ?, NULL, 'pending')`)
|
|
86
|
+
.run(deliverAfter, params.message, JSON.stringify(taskContext));
|
|
87
|
+
const result = {
|
|
88
|
+
scheduleId: String(inserted.lastInsertRowid),
|
|
89
|
+
deliverAfter,
|
|
90
|
+
coalesced: false,
|
|
91
|
+
};
|
|
92
|
+
recordDeferralAudit(db, window, params, result);
|
|
93
|
+
return result;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Retime pending quiet-hours-deferred DM rows after a quiet-hours config
|
|
97
|
+
* change (the `syncDmSessionTimesToQuietHours` sibling for this module's
|
|
98
|
+
* rows). The deferral premise — "the row fires *at* the quiet-hours edge,
|
|
99
|
+
* so `handleDirectDm`'s quiet-hours skip is correct" — only holds while
|
|
100
|
+
* the window that produced `scheduled_for` is still the configured one:
|
|
101
|
+
*
|
|
102
|
+
* - window extended (end moved later) → without retiming the row fires
|
|
103
|
+
* *inside* the new quiet window;
|
|
104
|
+
* - window shortened (end moved earlier) → the row waits past the new
|
|
105
|
+
* edge for no reason.
|
|
106
|
+
*
|
|
107
|
+
* Rule: inside the new window every deferred row moves to the new edge;
|
|
108
|
+
* outside it, future-dated rows are pulled up to `now` (the next scheduler
|
|
109
|
+
* tick delivers them) and already-due rows are left for the tick to claim.
|
|
110
|
+
* User-scheduled `dm` rows (no `deferred_from` marker) are never touched.
|
|
111
|
+
* Returns the number of rows retimed.
|
|
112
|
+
*/
|
|
113
|
+
export function retimeDeferredDmRows(db, window, now = new Date()) {
|
|
114
|
+
const rows = db
|
|
115
|
+
.prepare(`SELECT id, scheduled_for
|
|
116
|
+
FROM agent_schedule
|
|
117
|
+
WHERE status = 'pending'
|
|
118
|
+
AND task_type = 'dm'
|
|
119
|
+
AND task_context IS NOT NULL
|
|
120
|
+
AND json_valid(task_context)
|
|
121
|
+
AND json_extract(task_context, '$.deferred_from') IS NOT NULL`)
|
|
122
|
+
.all();
|
|
123
|
+
return retimeRowsToWindowEdge(db, window, now, rows, "dm");
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Retime pending quiet-hours-deferred RUN rows (`agent.task` opt-in and
|
|
127
|
+
* `browser_task`) after a quiet-hours config change — the
|
|
128
|
+
* `retimeDeferredDmRows` sibling for rows the ScheduleWatcher's
|
|
129
|
+
* `deferClaimedRowForQuietHours` pushed to the old window's end. That helper
|
|
130
|
+
* stamps `task_context.quiet_hours_deferred` at deferral time for exactly
|
|
131
|
+
* this purpose: rows that merely *carry* the `defer_in_quiet_hours` opt-in on
|
|
132
|
+
* a future cron slot were never deferred and keep their cron-scheduled time.
|
|
133
|
+
*
|
|
134
|
+
* Unlike deferred DM rows (delivered by `handleDirectDm`, which skips the
|
|
135
|
+
* quiet-hours check by design), run rows re-check the window at claim time,
|
|
136
|
+
* so a *widened* window self-corrects — retiming it here merely skips the
|
|
137
|
+
* wasted claim/re-defer cycle and its duplicate audit row. The
|
|
138
|
+
* narrowed/disabled direction is the real fix: without retiming, the run
|
|
139
|
+
* waits at the old window's end for no reason.
|
|
140
|
+
*/
|
|
141
|
+
export function retimeDeferredRunRows(db, window, now = new Date()) {
|
|
142
|
+
const rows = db
|
|
143
|
+
.prepare(`SELECT id, scheduled_for
|
|
144
|
+
FROM agent_schedule
|
|
145
|
+
WHERE status = 'pending'
|
|
146
|
+
AND task_context IS NOT NULL
|
|
147
|
+
AND json_valid(task_context)
|
|
148
|
+
AND json_extract(task_context, '$.quiet_hours_deferred') = 1`)
|
|
149
|
+
.all();
|
|
150
|
+
return retimeRowsToWindowEdge(db, window, now, rows, "run");
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Shared retime rule: inside the new window every row moves to the new edge;
|
|
154
|
+
* outside it, future-dated rows are pulled up to `now` (the next scheduler
|
|
155
|
+
* tick handles them) and already-due rows are left for the tick to claim.
|
|
156
|
+
*/
|
|
157
|
+
function retimeRowsToWindowEdge(db, window, now, rows, kind) {
|
|
158
|
+
if (rows.length === 0)
|
|
159
|
+
return 0;
|
|
160
|
+
const quietEndMs = nextQuietHoursEndMs(now, window);
|
|
161
|
+
const target = formatSqliteDatetime(quietEndMs !== null ? new Date(quietEndMs) : now);
|
|
162
|
+
const update = db.prepare("UPDATE agent_schedule SET scheduled_for = ? WHERE id = ?");
|
|
163
|
+
let retimed = 0;
|
|
164
|
+
for (const row of rows) {
|
|
165
|
+
// Outside quiet hours only future rows move — an already-due row is
|
|
166
|
+
// the scheduler's to claim; rewriting it would just delay delivery.
|
|
167
|
+
if (quietEndMs === null && row.scheduled_for <= target)
|
|
168
|
+
continue;
|
|
169
|
+
if (row.scheduled_for === target)
|
|
170
|
+
continue;
|
|
171
|
+
update.run(target, row.id);
|
|
172
|
+
retimed++;
|
|
173
|
+
}
|
|
174
|
+
if (retimed > 0) {
|
|
175
|
+
logger.info({ retimed, target, insideQuietHours: quietEndMs !== null, kind }, "Retimed pending quiet-hours-deferred rows after quiet-hours change");
|
|
176
|
+
}
|
|
177
|
+
return retimed;
|
|
178
|
+
}
|
|
179
|
+
/** One `agent_actions` row per deferral so the user can see the delay —
|
|
180
|
+
* mirrors the scheduler's `browser_task.deferred_for_quiet_hours`. */
|
|
181
|
+
function recordDeferralAudit(db, window, params, result) {
|
|
182
|
+
try {
|
|
183
|
+
db.prepare(`INSERT INTO agent_actions
|
|
184
|
+
(action_type, detail, result, started_at, completed_at)
|
|
185
|
+
VALUES (?, ?, 'success', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)`).run("notify.deferred_for_quiet_hours", JSON.stringify({
|
|
186
|
+
scheduleId: result.scheduleId,
|
|
187
|
+
deferredFrom: params.deferredFrom,
|
|
188
|
+
originSessionId: params.originSessionId ?? null,
|
|
189
|
+
agentId: params.agentId ?? null,
|
|
190
|
+
deliverAfter: result.deliverAfter,
|
|
191
|
+
coalesced: result.coalesced,
|
|
192
|
+
quietHoursStart: window.start,
|
|
193
|
+
quietHoursEnd: window.end,
|
|
194
|
+
}));
|
|
195
|
+
}
|
|
196
|
+
catch (err) {
|
|
197
|
+
logger.warn({ err, scheduleId: result.scheduleId }, "Failed to record notify.deferred_for_quiet_hours audit — deferred row already persisted");
|
|
198
|
+
}
|
|
199
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type Database from "better-sqlite3";
|
|
2
|
+
export type FeedbackSignalSource = "behavioral" | "explicit" | "self_critique";
|
|
3
|
+
export type FeedbackSignalValence = "positive" | "negative" | "neutral" | "correction";
|
|
4
|
+
export type FeedbackScopeType = "user" | "agent" | "agent_slug" | "channel" | "task" | "integration";
|
|
5
|
+
export type FeedbackActionKind = "notification" | "agent_execution" | "vault_write" | "dm_reply";
|
|
6
|
+
export interface FeedbackSignalRow {
|
|
7
|
+
id: number;
|
|
8
|
+
created_at: string;
|
|
9
|
+
source: FeedbackSignalSource;
|
|
10
|
+
valence: FeedbackSignalValence | null;
|
|
11
|
+
scope_type: FeedbackScopeType;
|
|
12
|
+
scope_ref: string | null;
|
|
13
|
+
action_kind: FeedbackActionKind | null;
|
|
14
|
+
action_ref: string | null;
|
|
15
|
+
agent_id: string | null;
|
|
16
|
+
summary: string;
|
|
17
|
+
evidence_json: string | null;
|
|
18
|
+
consumed_at: string | null;
|
|
19
|
+
lesson_ref: string | null;
|
|
20
|
+
}
|
|
21
|
+
export interface RecordFeedbackSignalParams {
|
|
22
|
+
source: FeedbackSignalSource;
|
|
23
|
+
valence?: FeedbackSignalValence | null;
|
|
24
|
+
scopeType: FeedbackScopeType;
|
|
25
|
+
scopeRef?: string | null;
|
|
26
|
+
actionKind?: FeedbackActionKind | null;
|
|
27
|
+
actionRef?: string | null;
|
|
28
|
+
agentId?: string | null;
|
|
29
|
+
summary: string;
|
|
30
|
+
evidence?: unknown;
|
|
31
|
+
}
|
|
32
|
+
export interface RecentFeedbackSignalLookup {
|
|
33
|
+
scopeType: FeedbackScopeType;
|
|
34
|
+
scopeRef?: string | null;
|
|
35
|
+
summary: string;
|
|
36
|
+
withinSeconds: number;
|
|
37
|
+
}
|
|
38
|
+
export declare function recordFeedbackSignal(db: Database.Database, params: RecordFeedbackSignalParams): number;
|
|
39
|
+
export declare function findRecentFeedbackSignal(db: Database.Database, params: RecentFeedbackSignalLookup): FeedbackSignalRow | null;
|
|
40
|
+
export declare function hasFeedbackSignalForAction(db: Database.Database, params: {
|
|
41
|
+
source: FeedbackSignalSource;
|
|
42
|
+
actionKind: FeedbackActionKind;
|
|
43
|
+
actionRef: string;
|
|
44
|
+
valence?: FeedbackSignalValence | null;
|
|
45
|
+
userReaction?: string;
|
|
46
|
+
}): boolean;
|
|
47
|
+
export declare function getPendingFeedbackSignals(db: Database.Database, params?: {
|
|
48
|
+
scopeType?: FeedbackScopeType;
|
|
49
|
+
scopeRef?: string | null;
|
|
50
|
+
limit?: number;
|
|
51
|
+
offset?: number;
|
|
52
|
+
}): FeedbackSignalRow[];
|
|
53
|
+
/**
|
|
54
|
+
* Count unconsumed signals, optionally narrowed to one scope type. Drives the
|
|
55
|
+
* `GET /api/feedback/lessons` "N signals awaiting tonight's consolidation"
|
|
56
|
+
* health figure (FEEDBACK_LEARNING_LOOP_DESIGN.md §9 Phase 5) without loading
|
|
57
|
+
* the rows. Uses the same `consumed_at IS NULL` partial index as
|
|
58
|
+
* {@link getPendingFeedbackSignals}.
|
|
59
|
+
*/
|
|
60
|
+
export declare function countPendingFeedbackSignals(db: Database.Database, params?: {
|
|
61
|
+
scopeType?: FeedbackScopeType;
|
|
62
|
+
}): number;
|
|
63
|
+
export declare function consumeFeedbackSignals(db: Database.Database, ids: number[], lessonRef?: string | null): {
|
|
64
|
+
consumed: number;
|
|
65
|
+
notFound: number[];
|
|
66
|
+
};
|
|
67
|
+
export declare function sweepConsumedFeedbackSignals(db: Database.Database, cutoff: string): number;
|
|
68
|
+
/**
|
|
69
|
+
* Compute the retention cutoff ISO timestamp for
|
|
70
|
+
* {@link sweepConsumedFeedbackSignals} from the `feedbackSignalRetentionDays`
|
|
71
|
+
* knob. Returns `null` when the knob is missing or non-finite (NaN / Infinity),
|
|
72
|
+
* so the caller degrades to "skip the sweep" instead of throwing on
|
|
73
|
+
* `new Date(NaN).toISOString()` and abandoning the whole nightly consolidation
|
|
74
|
+
* (FEEDBACK_LEARNING_LOOP_DESIGN.md §11 v1.3 robustness). `nowMs` is injected so
|
|
75
|
+
* the math is deterministically testable.
|
|
76
|
+
*/
|
|
77
|
+
export declare function feedbackRetentionCutoff(retentionDays: number | undefined, nowMs: number): string | null;
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
export function recordFeedbackSignal(db, params) {
|
|
2
|
+
const evidenceJson = params.evidence === undefined ? "{}" : JSON.stringify(params.evidence);
|
|
3
|
+
const row = db
|
|
4
|
+
.prepare(`INSERT INTO feedback_signals (
|
|
5
|
+
source,
|
|
6
|
+
valence,
|
|
7
|
+
scope_type,
|
|
8
|
+
scope_ref,
|
|
9
|
+
action_kind,
|
|
10
|
+
action_ref,
|
|
11
|
+
agent_id,
|
|
12
|
+
summary,
|
|
13
|
+
evidence_json
|
|
14
|
+
)
|
|
15
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
16
|
+
RETURNING id`)
|
|
17
|
+
.get(params.source, params.valence ?? null, params.scopeType, params.scopeRef ?? null, params.actionKind ?? null, params.actionRef ?? null, params.agentId ?? null, params.summary, evidenceJson);
|
|
18
|
+
return row.id;
|
|
19
|
+
}
|
|
20
|
+
export function findRecentFeedbackSignal(db, params) {
|
|
21
|
+
const row = db
|
|
22
|
+
.prepare(`SELECT *
|
|
23
|
+
FROM feedback_signals
|
|
24
|
+
WHERE scope_type = ?
|
|
25
|
+
AND COALESCE(scope_ref, '') = COALESCE(?, '')
|
|
26
|
+
AND summary = ?
|
|
27
|
+
AND datetime(created_at) >= datetime('now', '-' || ? || ' seconds')
|
|
28
|
+
ORDER BY datetime(created_at) DESC, id DESC
|
|
29
|
+
LIMIT 1`)
|
|
30
|
+
.get(params.scopeType, params.scopeRef ?? null, params.summary, Math.max(0, Math.floor(params.withinSeconds)));
|
|
31
|
+
return row ?? null;
|
|
32
|
+
}
|
|
33
|
+
export function hasFeedbackSignalForAction(db, params) {
|
|
34
|
+
const where = [
|
|
35
|
+
"source = ?",
|
|
36
|
+
"action_kind = ?",
|
|
37
|
+
"action_ref = ?",
|
|
38
|
+
];
|
|
39
|
+
const values = [params.source, params.actionKind, params.actionRef];
|
|
40
|
+
if (params.valence !== undefined) {
|
|
41
|
+
where.push(params.valence === null ? "valence IS NULL" : "valence = ?");
|
|
42
|
+
if (params.valence !== null)
|
|
43
|
+
values.push(params.valence);
|
|
44
|
+
}
|
|
45
|
+
if (params.userReaction !== undefined) {
|
|
46
|
+
where.push("json_extract(evidence_json, '$.userReaction') = ?");
|
|
47
|
+
values.push(params.userReaction);
|
|
48
|
+
}
|
|
49
|
+
const row = db
|
|
50
|
+
.prepare(`SELECT 1 AS present
|
|
51
|
+
FROM feedback_signals
|
|
52
|
+
WHERE ${where.join(" AND ")}
|
|
53
|
+
LIMIT 1`)
|
|
54
|
+
.get(...values);
|
|
55
|
+
return row !== undefined;
|
|
56
|
+
}
|
|
57
|
+
export function getPendingFeedbackSignals(db, params = {}) {
|
|
58
|
+
const where = ["consumed_at IS NULL"];
|
|
59
|
+
const values = [];
|
|
60
|
+
if (params.scopeType !== undefined) {
|
|
61
|
+
where.push("scope_type = ?");
|
|
62
|
+
values.push(params.scopeType);
|
|
63
|
+
}
|
|
64
|
+
if (params.scopeRef !== undefined) {
|
|
65
|
+
where.push("COALESCE(scope_ref, '') = COALESCE(?, '')");
|
|
66
|
+
values.push(params.scopeRef);
|
|
67
|
+
}
|
|
68
|
+
const limit = Math.min(Math.max(params.limit ?? 100, 1), 500);
|
|
69
|
+
const offset = Math.max(params.offset ?? 0, 0);
|
|
70
|
+
values.push(limit, offset);
|
|
71
|
+
return db
|
|
72
|
+
.prepare(`SELECT *
|
|
73
|
+
FROM feedback_signals
|
|
74
|
+
WHERE ${where.join(" AND ")}
|
|
75
|
+
ORDER BY datetime(created_at) ASC, id ASC
|
|
76
|
+
LIMIT ? OFFSET ?`)
|
|
77
|
+
.all(...values);
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Count unconsumed signals, optionally narrowed to one scope type. Drives the
|
|
81
|
+
* `GET /api/feedback/lessons` "N signals awaiting tonight's consolidation"
|
|
82
|
+
* health figure (FEEDBACK_LEARNING_LOOP_DESIGN.md §9 Phase 5) without loading
|
|
83
|
+
* the rows. Uses the same `consumed_at IS NULL` partial index as
|
|
84
|
+
* {@link getPendingFeedbackSignals}.
|
|
85
|
+
*/
|
|
86
|
+
export function countPendingFeedbackSignals(db, params = {}) {
|
|
87
|
+
const where = ["consumed_at IS NULL"];
|
|
88
|
+
const values = [];
|
|
89
|
+
if (params.scopeType !== undefined) {
|
|
90
|
+
where.push("scope_type = ?");
|
|
91
|
+
values.push(params.scopeType);
|
|
92
|
+
}
|
|
93
|
+
const row = db
|
|
94
|
+
.prepare(`SELECT COUNT(*) AS n
|
|
95
|
+
FROM feedback_signals
|
|
96
|
+
WHERE ${where.join(" AND ")}`)
|
|
97
|
+
.get(...values);
|
|
98
|
+
return row.n;
|
|
99
|
+
}
|
|
100
|
+
export function consumeFeedbackSignals(db, ids, lessonRef) {
|
|
101
|
+
if (ids.length === 0)
|
|
102
|
+
return { consumed: 0, notFound: [] };
|
|
103
|
+
const placeholders = ids.map(() => "?").join(",");
|
|
104
|
+
const existing = db
|
|
105
|
+
.prepare(`SELECT id
|
|
106
|
+
FROM feedback_signals
|
|
107
|
+
WHERE id IN (${placeholders}) AND consumed_at IS NULL`)
|
|
108
|
+
.all(...ids);
|
|
109
|
+
const existingIds = new Set(existing.map((row) => row.id));
|
|
110
|
+
const notFound = ids.filter((id) => !existingIds.has(id));
|
|
111
|
+
if (existingIds.size === 0)
|
|
112
|
+
return { consumed: 0, notFound };
|
|
113
|
+
const updateIds = Array.from(existingIds);
|
|
114
|
+
const updatePlaceholders = updateIds.map(() => "?").join(",");
|
|
115
|
+
const consumed = db
|
|
116
|
+
.prepare(`UPDATE feedback_signals
|
|
117
|
+
SET consumed_at = CURRENT_TIMESTAMP, lesson_ref = COALESCE(?, lesson_ref)
|
|
118
|
+
WHERE id IN (${updatePlaceholders}) AND consumed_at IS NULL`)
|
|
119
|
+
.run(lessonRef ?? null, ...updateIds).changes;
|
|
120
|
+
return { consumed, notFound };
|
|
121
|
+
}
|
|
122
|
+
export function sweepConsumedFeedbackSignals(db, cutoff) {
|
|
123
|
+
return db
|
|
124
|
+
.prepare(`DELETE FROM feedback_signals
|
|
125
|
+
WHERE consumed_at IS NOT NULL
|
|
126
|
+
AND datetime(consumed_at) < datetime(?)`)
|
|
127
|
+
.run(cutoff).changes;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Compute the retention cutoff ISO timestamp for
|
|
131
|
+
* {@link sweepConsumedFeedbackSignals} from the `feedbackSignalRetentionDays`
|
|
132
|
+
* knob. Returns `null` when the knob is missing or non-finite (NaN / Infinity),
|
|
133
|
+
* so the caller degrades to "skip the sweep" instead of throwing on
|
|
134
|
+
* `new Date(NaN).toISOString()` and abandoning the whole nightly consolidation
|
|
135
|
+
* (FEEDBACK_LEARNING_LOOP_DESIGN.md §11 v1.3 robustness). `nowMs` is injected so
|
|
136
|
+
* the math is deterministically testable.
|
|
137
|
+
*/
|
|
138
|
+
export function feedbackRetentionCutoff(retentionDays, nowMs) {
|
|
139
|
+
if (typeof retentionDays !== "number" || !Number.isFinite(retentionDays)) {
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
const retentionMs = retentionDays * 24 * 60 * 60 * 1000;
|
|
143
|
+
return new Date(nowMs - retentionMs).toISOString();
|
|
144
|
+
}
|