@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,378 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stage-0 signal compute for the three-stage activity_scan gate
|
|
3
|
+
* (cost-reduction-structural §B). Pure DB-read shape: every value is
|
|
4
|
+
* derived from existing tables (`observations`, `mail_messages_index`,
|
|
5
|
+
* `agent_schedule`, `agent_actions`) plus the optional today.md content
|
|
6
|
+
* passed in by the caller. No LLM call, no filesystem I/O of its own —
|
|
7
|
+
* the dispatcher injects the today.md snapshot (when available) so this
|
|
8
|
+
* module stays unit-testable from a synthetic Database handle.
|
|
9
|
+
*
|
|
10
|
+
* The shape mirrors the design doc 1:1 so the gate (Stage 1) can be
|
|
11
|
+
* a pure function over `ActivityScanSignals` + a config block.
|
|
12
|
+
*/
|
|
13
|
+
import { buildSourcePrefixFilter, } from "@aitne/shared";
|
|
14
|
+
/**
|
|
15
|
+
* Build a `(source LIKE 'a:%' OR source LIKE 'b:%' ...)` clause covering
|
|
16
|
+
* every direct-poller and pre-pass-partial prefix for the given kinds.
|
|
17
|
+
* Sourced from `INTEGRATION_DESCRIPTORS` so a new integration shipping
|
|
18
|
+
* with `prePassPartial: "<kind>-acquire.<key>.md"` auto-extends the
|
|
19
|
+
* gate's view of the world. Empty `kinds` yields `(1=0)` (defensive).
|
|
20
|
+
*
|
|
21
|
+
* CLAUDE.md: "Never hardcode an integration reference outside the
|
|
22
|
+
* registry."
|
|
23
|
+
*/
|
|
24
|
+
function sourceMatchClause(kinds) {
|
|
25
|
+
return buildSourcePrefixFilter(kinds);
|
|
26
|
+
}
|
|
27
|
+
/** All-kinds union — every observation source the gate cares about. */
|
|
28
|
+
const ALL_KINDS = [
|
|
29
|
+
"mail",
|
|
30
|
+
"calendar",
|
|
31
|
+
"notion",
|
|
32
|
+
"vault",
|
|
33
|
+
"repo",
|
|
34
|
+
];
|
|
35
|
+
export function computeActivityScanSignals(db, options = {}) {
|
|
36
|
+
const now = options.now ?? new Date();
|
|
37
|
+
const vipSenders = (options.vipMailSenders ?? []).map((s) => s.toLowerCase());
|
|
38
|
+
const calendarHorizonMs = (options.calendarHorizonHours ?? 24) * 60 * 60 * 1000;
|
|
39
|
+
const scheduleHorizonHours = options.scheduleHorizonHours ?? 6;
|
|
40
|
+
// Gate filters by `source` (mail/calendar/notion/vault/repo), NEVER by
|
|
41
|
+
// `actor`. Delegated-sync-worker and the routine.fetch_window pre-pass
|
|
42
|
+
// both POST `actor='agent'` rows; the gate must see them as real
|
|
43
|
+
// activity. The 30-min `pre_pass_last_run:<key>` freshness window in
|
|
44
|
+
// `ActivityScanCoordinator.harvestForGate` structurally prevents a
|
|
45
|
+
// single pre-pass row from being re-counted across ticks.
|
|
46
|
+
const allFilter = sourceMatchClause(ALL_KINDS);
|
|
47
|
+
const pending = db
|
|
48
|
+
.prepare(`SELECT COUNT(*) AS count
|
|
49
|
+
FROM observations
|
|
50
|
+
WHERE consumed_at IS NULL
|
|
51
|
+
AND ${allFilter.clause}`)
|
|
52
|
+
.get(...allFilter.values);
|
|
53
|
+
const noveltyMaxRow = db
|
|
54
|
+
.prepare(`SELECT MAX(novelty_score) AS max_score
|
|
55
|
+
FROM observations
|
|
56
|
+
WHERE consumed_at IS NULL
|
|
57
|
+
AND summary_status = 'done'
|
|
58
|
+
AND novelty_score IS NOT NULL
|
|
59
|
+
AND ${allFilter.clause}`)
|
|
60
|
+
.get(...allFilter.values);
|
|
61
|
+
const distributionRows = db
|
|
62
|
+
.prepare(`SELECT novelty_score AS score, COUNT(*) AS count
|
|
63
|
+
FROM observations
|
|
64
|
+
WHERE consumed_at IS NULL
|
|
65
|
+
AND summary_status = 'done'
|
|
66
|
+
AND novelty_score IS NOT NULL
|
|
67
|
+
AND ${allFilter.clause}
|
|
68
|
+
GROUP BY novelty_score`)
|
|
69
|
+
.all(...allFilter.values);
|
|
70
|
+
const noveltyDistribution = { low: 0, mid: 0, high: 0 };
|
|
71
|
+
for (const row of distributionRows) {
|
|
72
|
+
if (row.score <= 1)
|
|
73
|
+
noveltyDistribution.low += row.count;
|
|
74
|
+
else if (row.score === 2)
|
|
75
|
+
noveltyDistribution.mid += row.count;
|
|
76
|
+
else if (row.score >= 3)
|
|
77
|
+
noveltyDistribution.high += row.count;
|
|
78
|
+
}
|
|
79
|
+
const vipMailUnreadCount = countVipUnreadMail(db, vipSenders);
|
|
80
|
+
const calendarFilter = sourceMatchClause(["calendar"]);
|
|
81
|
+
const calendarRow = db
|
|
82
|
+
.prepare(`SELECT COUNT(*) AS count
|
|
83
|
+
FROM observations
|
|
84
|
+
WHERE consumed_at IS NULL
|
|
85
|
+
AND ${calendarFilter.clause}`)
|
|
86
|
+
.get(...calendarFilter.values);
|
|
87
|
+
const calendarHas24hChange = calendarRow.count > 0;
|
|
88
|
+
const calendarHasConflict = detectCalendarConflict(db, now, calendarHorizonMs);
|
|
89
|
+
const agentPlanOverdueCount = countOverdueAgentPlanRows(options.todayMd ?? null, now, options.agentTimezone);
|
|
90
|
+
const horizon = new Date(now.getTime() + scheduleHorizonHours * 60 * 60 * 1000);
|
|
91
|
+
const scheduleRow = db
|
|
92
|
+
.prepare(`SELECT COUNT(*) AS count
|
|
93
|
+
FROM agent_schedule
|
|
94
|
+
WHERE status = 'pending'
|
|
95
|
+
AND scheduled_for >= ?
|
|
96
|
+
AND scheduled_for < ?`)
|
|
97
|
+
.get(toSqliteUtc(now), toSqliteUtc(horizon));
|
|
98
|
+
const hoursSinceLastStage3Run = computeHoursSinceLastStage3(db, now);
|
|
99
|
+
return {
|
|
100
|
+
pendingObsCount: pending.count,
|
|
101
|
+
maxNoveltyScore: noveltyMaxRow.max_score,
|
|
102
|
+
noveltyDistribution,
|
|
103
|
+
vipMailUnreadCount,
|
|
104
|
+
calendarHas24hChange,
|
|
105
|
+
calendarHasConflict,
|
|
106
|
+
agentPlanOverdueCount,
|
|
107
|
+
scheduleApproachingCount: scheduleRow.count,
|
|
108
|
+
hoursSinceLastStage3Run,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
function countVipUnreadMail(db, vipSenders) {
|
|
112
|
+
if (vipSenders.length === 0)
|
|
113
|
+
return 0;
|
|
114
|
+
// Primary path (delegated / native / direct-with-pre-pass):
|
|
115
|
+
// unread VIP mail rides on `observations` with source prefixes
|
|
116
|
+
// `gmail:%` / `outlook_mail:%` (pre-pass) or `mail:%` (direct
|
|
117
|
+
// aggregate; only carries lifecycle metadata). The pre-pass payload
|
|
118
|
+
// is normalized at the `/api/observations` POST chokepoint to surface
|
|
119
|
+
// `is_read=0` + `from_email=<lowercased>` so this query can read a
|
|
120
|
+
// single canonical shape regardless of provider. We treat a row as
|
|
121
|
+
// VIP-unread when EITHER:
|
|
122
|
+
// (a) the normalized `is_read=0` + `from_email` keys are present
|
|
123
|
+
// and match, OR
|
|
124
|
+
// (b) `is_read` is absent (pre-normalization payload or non-mail
|
|
125
|
+
// lifecycle row) but `payload.raw.from` is a substring match
|
|
126
|
+
// for one of the VIP addresses — captures the legacy case
|
|
127
|
+
// where the partial doesn't emit the normalized keys.
|
|
128
|
+
const mailFilter = sourceMatchClause(["mail"]);
|
|
129
|
+
const senderPlaceholders = vipSenders.map(() => "?").join(",");
|
|
130
|
+
const observationsRow = db
|
|
131
|
+
.prepare(`SELECT COUNT(*) AS count
|
|
132
|
+
FROM observations
|
|
133
|
+
WHERE consumed_at IS NULL
|
|
134
|
+
AND ${mailFilter.clause}
|
|
135
|
+
AND COALESCE(json_extract(payload, '$.is_read'), 0) = 0
|
|
136
|
+
AND LOWER(
|
|
137
|
+
COALESCE(
|
|
138
|
+
json_extract(payload, '$.from_email'),
|
|
139
|
+
json_extract(payload, '$.raw.from'),
|
|
140
|
+
json_extract(payload, '$.from'),
|
|
141
|
+
''
|
|
142
|
+
)
|
|
143
|
+
) IN (${senderPlaceholders})`)
|
|
144
|
+
.get(...mailFilter.values, ...vipSenders);
|
|
145
|
+
let observationsCount = observationsRow.count;
|
|
146
|
+
// Substring fallback for `payload.raw.from` shapes like
|
|
147
|
+
// "Foo Bar <foo@bar.com>" — the IN-match above only catches exact
|
|
148
|
+
// address fields. Defensive cap (50 rows) keeps the per-tick cost
|
|
149
|
+
// bounded; tens of unread VIP mails in one window is already a
|
|
150
|
+
// hard-escalate signal regardless of exact count.
|
|
151
|
+
if (observationsCount === 0) {
|
|
152
|
+
const candidateRows = db
|
|
153
|
+
.prepare(`SELECT json_extract(payload, '$.raw.from') AS rawFrom
|
|
154
|
+
FROM observations
|
|
155
|
+
WHERE consumed_at IS NULL
|
|
156
|
+
AND ${mailFilter.clause}
|
|
157
|
+
AND COALESCE(json_extract(payload, '$.is_read'), 0) = 0
|
|
158
|
+
LIMIT 50`)
|
|
159
|
+
.all(...mailFilter.values);
|
|
160
|
+
for (const row of candidateRows) {
|
|
161
|
+
if (!row.rawFrom)
|
|
162
|
+
continue;
|
|
163
|
+
const haystack = row.rawFrom.toLowerCase();
|
|
164
|
+
if (vipSenders.some((s) => haystack.includes(s))) {
|
|
165
|
+
observationsCount += 1;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
// Fallback path (direct mode pre-pre-pass installs whose observations
|
|
170
|
+
// are aggregate `mail:lifecycle` rows without per-message detail).
|
|
171
|
+
// The `mail_messages_index` table carries the canonical `is_read` +
|
|
172
|
+
// `from_email` columns and the MailPoller writes to it on every poll.
|
|
173
|
+
const tableExists = db
|
|
174
|
+
.prepare(`SELECT 1 AS present
|
|
175
|
+
FROM sqlite_master
|
|
176
|
+
WHERE type = 'table' AND name = 'mail_messages_index'`)
|
|
177
|
+
.get();
|
|
178
|
+
let tableCount = 0;
|
|
179
|
+
if (tableExists) {
|
|
180
|
+
const row = db
|
|
181
|
+
.prepare(`SELECT COUNT(*) AS count
|
|
182
|
+
FROM mail_messages_index
|
|
183
|
+
WHERE is_read = 0
|
|
184
|
+
AND deleted_at_utc IS NULL
|
|
185
|
+
AND LOWER(COALESCE(from_email, '')) IN (${senderPlaceholders})`)
|
|
186
|
+
.get(...vipSenders);
|
|
187
|
+
tableCount = row.count;
|
|
188
|
+
}
|
|
189
|
+
return Math.max(observationsCount, tableCount);
|
|
190
|
+
}
|
|
191
|
+
function detectCalendarConflict(db, now, horizonMs) {
|
|
192
|
+
// Pull pending calendar observation payloads. We tolerate the common
|
|
193
|
+
// shapes (`start`, `end`, `start.dateTime`, `end.dateTime`) and bail
|
|
194
|
+
// gracefully when neither is recognizable. A "conflict" is two events
|
|
195
|
+
// whose [start, end) ranges intersect within the lookahead window.
|
|
196
|
+
const calendarFilter = sourceMatchClause(["calendar"]);
|
|
197
|
+
const rows = db
|
|
198
|
+
.prepare(`SELECT payload
|
|
199
|
+
FROM observations
|
|
200
|
+
WHERE consumed_at IS NULL
|
|
201
|
+
AND payload IS NOT NULL
|
|
202
|
+
AND ${calendarFilter.clause}`)
|
|
203
|
+
.all(...calendarFilter.values);
|
|
204
|
+
const ranges = [];
|
|
205
|
+
const horizon = now.getTime() + horizonMs;
|
|
206
|
+
for (const row of rows) {
|
|
207
|
+
if (!row.payload)
|
|
208
|
+
continue;
|
|
209
|
+
let parsed;
|
|
210
|
+
try {
|
|
211
|
+
parsed = JSON.parse(row.payload);
|
|
212
|
+
}
|
|
213
|
+
catch {
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
const range = extractRange(parsed);
|
|
217
|
+
if (!range)
|
|
218
|
+
continue;
|
|
219
|
+
if (range.end <= now.getTime())
|
|
220
|
+
continue;
|
|
221
|
+
if (range.start >= horizon)
|
|
222
|
+
continue;
|
|
223
|
+
ranges.push(range);
|
|
224
|
+
}
|
|
225
|
+
if (ranges.length < 2)
|
|
226
|
+
return false;
|
|
227
|
+
ranges.sort((a, b) => a.start - b.start);
|
|
228
|
+
for (let i = 1; i < ranges.length; i++) {
|
|
229
|
+
if (ranges[i].start < ranges[i - 1].end) {
|
|
230
|
+
return true;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
235
|
+
function extractRange(payload) {
|
|
236
|
+
if (!payload || typeof payload !== "object")
|
|
237
|
+
return null;
|
|
238
|
+
const record = payload;
|
|
239
|
+
const start = parseTimestamp(record["start"] ??
|
|
240
|
+
(typeof record["startTime"] === "string" ? record["startTime"] : undefined) ??
|
|
241
|
+
readNested(record, "start", "dateTime") ??
|
|
242
|
+
readNested(record, "start", "date"));
|
|
243
|
+
const end = parseTimestamp(record["end"] ??
|
|
244
|
+
(typeof record["endTime"] === "string" ? record["endTime"] : undefined) ??
|
|
245
|
+
readNested(record, "end", "dateTime") ??
|
|
246
|
+
readNested(record, "end", "date"));
|
|
247
|
+
if (start === null || end === null || end <= start)
|
|
248
|
+
return null;
|
|
249
|
+
return { start, end };
|
|
250
|
+
}
|
|
251
|
+
function readNested(record, key, inner) {
|
|
252
|
+
const child = record[key];
|
|
253
|
+
if (!child || typeof child !== "object")
|
|
254
|
+
return undefined;
|
|
255
|
+
return child[inner];
|
|
256
|
+
}
|
|
257
|
+
function parseTimestamp(value) {
|
|
258
|
+
if (typeof value !== "string" || value.length === 0)
|
|
259
|
+
return null;
|
|
260
|
+
const ts = Date.parse(value);
|
|
261
|
+
return Number.isFinite(ts) ? ts : null;
|
|
262
|
+
}
|
|
263
|
+
const AGENT_PLAN_HEADING = /^##\s+Agent Plan\b/i;
|
|
264
|
+
const NEXT_HEADING = /^##\s+/;
|
|
265
|
+
const PLAN_ROW_TIME = /^[\s>*-]*?(\d{1,2}):(\d{2})\b/;
|
|
266
|
+
function countOverdueAgentPlanRows(todayMd, now, agentTimezone) {
|
|
267
|
+
if (!todayMd)
|
|
268
|
+
return 0;
|
|
269
|
+
const lines = todayMd.split(/\r?\n/);
|
|
270
|
+
let inPlan = false;
|
|
271
|
+
let count = 0;
|
|
272
|
+
const nowMin = minutesOfDayInTimezone(now, agentTimezone);
|
|
273
|
+
for (const line of lines) {
|
|
274
|
+
if (AGENT_PLAN_HEADING.test(line)) {
|
|
275
|
+
inPlan = true;
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
if (inPlan && NEXT_HEADING.test(line)) {
|
|
279
|
+
break;
|
|
280
|
+
}
|
|
281
|
+
if (!inPlan)
|
|
282
|
+
continue;
|
|
283
|
+
const match = PLAN_ROW_TIME.exec(line);
|
|
284
|
+
if (!match)
|
|
285
|
+
continue;
|
|
286
|
+
const hh = Number(match[1]);
|
|
287
|
+
const mm = Number(match[2]);
|
|
288
|
+
if (!Number.isFinite(hh) || !Number.isFinite(mm))
|
|
289
|
+
continue;
|
|
290
|
+
if (hh > 23 || mm > 59)
|
|
291
|
+
continue;
|
|
292
|
+
const planMin = hh * 60 + mm;
|
|
293
|
+
if (planMin < nowMin)
|
|
294
|
+
count += 1;
|
|
295
|
+
}
|
|
296
|
+
return count;
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Convert a wall-clock instant into "minutes-of-day in the given IANA
|
|
300
|
+
* timezone". Falls back to the JS engine's local timezone when no zone
|
|
301
|
+
* is supplied or the zone string is invalid — the latter avoids the
|
|
302
|
+
* gate going dark on an operator typo.
|
|
303
|
+
*/
|
|
304
|
+
function minutesOfDayInTimezone(now, timezone) {
|
|
305
|
+
if (!timezone) {
|
|
306
|
+
return now.getHours() * 60 + now.getMinutes();
|
|
307
|
+
}
|
|
308
|
+
try {
|
|
309
|
+
const formatter = new Intl.DateTimeFormat("en-GB", {
|
|
310
|
+
hour: "2-digit",
|
|
311
|
+
minute: "2-digit",
|
|
312
|
+
hour12: false,
|
|
313
|
+
timeZone: timezone,
|
|
314
|
+
});
|
|
315
|
+
const parts = formatter.formatToParts(now);
|
|
316
|
+
const hh = Number(parts.find((p) => p.type === "hour")?.value ?? "0");
|
|
317
|
+
const mm = Number(parts.find((p) => p.type === "minute")?.value ?? "0");
|
|
318
|
+
if (!Number.isFinite(hh) || !Number.isFinite(mm)) {
|
|
319
|
+
return now.getHours() * 60 + now.getMinutes();
|
|
320
|
+
}
|
|
321
|
+
return hh * 60 + mm;
|
|
322
|
+
}
|
|
323
|
+
catch {
|
|
324
|
+
return now.getHours() * 60 + now.getMinutes();
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
function computeHoursSinceLastStage3(db, now) {
|
|
328
|
+
// Prefer the gate-emitted row (`activity_scan.gate` with
|
|
329
|
+
// stage_reached='stage3') — that matches the design doc's "since the
|
|
330
|
+
// last Stage 3 run" semantics. Fall back to the legacy
|
|
331
|
+
// `routine.activity_scan` action-type for installs deployed before
|
|
332
|
+
// the gate was wired. (The `stage3_shadow` marker was removed when
|
|
333
|
+
// the gateMode enum collapsed in HOURLY_CHECK_GATE_REDESIGN_PLAN.md
|
|
334
|
+
// Phase 4 — only the canonical `stage3` is ever written now.)
|
|
335
|
+
// The `hourly_check.*` legacy action types are pre-v0.1.11 rename rows —
|
|
336
|
+
// keep them in the union so the heartbeat window doesn't falsely report
|
|
337
|
+
// "no recent Stage 3" on the first post-upgrade ticks. Safe to drop once
|
|
338
|
+
// the heartbeat lookback (≤48 h) has fully aged past an upgrade.
|
|
339
|
+
const gateRow = db
|
|
340
|
+
.prepare(`SELECT started_at
|
|
341
|
+
FROM agent_actions
|
|
342
|
+
WHERE action_type IN ('activity_scan.gate', 'hourly_check.gate')
|
|
343
|
+
AND json_extract(detail, '$.stage_reached') = 'stage3'
|
|
344
|
+
ORDER BY started_at DESC
|
|
345
|
+
LIMIT 1`)
|
|
346
|
+
.get();
|
|
347
|
+
let lastStarted = gateRow?.started_at ?? null;
|
|
348
|
+
if (!lastStarted) {
|
|
349
|
+
const fallbackRow = db
|
|
350
|
+
.prepare(`SELECT started_at
|
|
351
|
+
FROM agent_actions
|
|
352
|
+
WHERE action_type IN ('routine.activity_scan', 'routine.hourly_check')
|
|
353
|
+
AND result IN ('success', 'partial', 'failed')
|
|
354
|
+
ORDER BY started_at DESC
|
|
355
|
+
LIMIT 1`)
|
|
356
|
+
.get();
|
|
357
|
+
lastStarted = fallbackRow?.started_at ?? null;
|
|
358
|
+
}
|
|
359
|
+
if (!lastStarted)
|
|
360
|
+
return Number.POSITIVE_INFINITY;
|
|
361
|
+
const lastUtc = parseSqliteUtc(lastStarted);
|
|
362
|
+
if (lastUtc === null)
|
|
363
|
+
return Number.POSITIVE_INFINITY;
|
|
364
|
+
const diffMs = now.getTime() - lastUtc;
|
|
365
|
+
if (diffMs <= 0)
|
|
366
|
+
return 0;
|
|
367
|
+
return diffMs / (60 * 60 * 1000);
|
|
368
|
+
}
|
|
369
|
+
function toSqliteUtc(date) {
|
|
370
|
+
// "YYYY-MM-DD HH:MM:SS" — matches CURRENT_TIMESTAMP / datetime('now').
|
|
371
|
+
return date.toISOString().replace("T", " ").slice(0, 19);
|
|
372
|
+
}
|
|
373
|
+
function parseSqliteUtc(value) {
|
|
374
|
+
// Tolerate both "YYYY-MM-DD HH:MM:SS" and ISO 8601.
|
|
375
|
+
const iso = value.includes("T") ? value : value.replace(" ", "T") + "Z";
|
|
376
|
+
const ts = Date.parse(iso);
|
|
377
|
+
return Number.isFinite(ts) ? ts : null;
|
|
378
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type Database from "better-sqlite3";
|
|
2
2
|
import type { AgentKind, ScheduleKind, StopWarning } from "@aitne/shared";
|
|
3
|
+
import { type RuntimeWindowOverride } from "../core/agents/activity-scan-cadence.js";
|
|
3
4
|
/**
|
|
4
5
|
* Agents store — durable identity layer for the Agent Definitions feature
|
|
5
6
|
* (AGENT_DEFINITIONS_DESIGN.md §5.1). One `agents` row per built-in routine
|
|
@@ -37,6 +38,13 @@ export interface AgentMetadata {
|
|
|
37
38
|
last_error?: string;
|
|
38
39
|
/** Built-in field-level edits that must survive `npm i -g` (§6.4.1). */
|
|
39
40
|
override_snapshot?: Record<string, unknown>;
|
|
41
|
+
/**
|
|
42
|
+
* Runtime-window cadence overrides for `activity-scan` (interval / active
|
|
43
|
+
* hours / observation threshold) — AGENTS_HUB_REDESIGN_PLAN.md §2. Written
|
|
44
|
+
* by `PATCH /api/agents/:slug` (`schedule_window`), preserved by the loader
|
|
45
|
+
* like `override_snapshot`, resolved by `core/agents/activity-scan-cadence.ts`.
|
|
46
|
+
*/
|
|
47
|
+
runtime_window?: Record<string, unknown>;
|
|
40
48
|
[key: string]: unknown;
|
|
41
49
|
}
|
|
42
50
|
/** Raw row as stored in SQLite (snake_case, JSON columns unparsed). */
|
|
@@ -170,6 +178,26 @@ export declare function deleteAgent(db: Database.Database, slug: string): boolea
|
|
|
170
178
|
* or carries no snapshot, so callers can treat the result uniformly.
|
|
171
179
|
*/
|
|
172
180
|
export declare function getOverrideSnapshot(db: Database.Database, slug: string): Record<string, unknown>;
|
|
181
|
+
/**
|
|
182
|
+
* Read just the `enabled` flag for a slug, with a caller-supplied fallback for
|
|
183
|
+
* a missing row (pass the registry's `defaultEnabled` for built-ins). Cheap
|
|
184
|
+
* single-column read for fire-time / catch-up gates outside the scheduler's
|
|
185
|
+
* cached `isAgentEnabledForFiring` path.
|
|
186
|
+
*/
|
|
187
|
+
export declare function getAgentEnabled(db: Database.Database, slug: string, fallback: boolean): boolean;
|
|
188
|
+
/**
|
|
189
|
+
* Read the sanitized runtime-window override (`metadata_json.runtime_window`)
|
|
190
|
+
* for a slug. Returns `{}` when the Agent is missing or carries no override,
|
|
191
|
+
* so callers can hand the result straight to `resolveActivityScanCadence`.
|
|
192
|
+
*/
|
|
193
|
+
export declare function getRuntimeWindow(db: Database.Database, slug: string): RuntimeWindowOverride;
|
|
194
|
+
/**
|
|
195
|
+
* Replace the runtime-window override, preserving every other `metadata_json`
|
|
196
|
+
* key. An empty override removes the `runtime_window` key entirely (all fields
|
|
197
|
+
* back on the config fallback). Returns the updated DTO, or `null` when no row
|
|
198
|
+
* matches.
|
|
199
|
+
*/
|
|
200
|
+
export declare function setRuntimeWindow(db: Database.Database, slug: string, window: RuntimeWindowOverride, now?: number): AgentDTO | null;
|
|
173
201
|
/**
|
|
174
202
|
* Replace the override snapshot, preserving every other `metadata_json` key.
|
|
175
203
|
* An empty snapshot removes the `override_snapshot` key entirely (so a fully
|
package/dist/db/agents-store.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { parseRuntimeWindowOverride, } from "../core/agents/activity-scan-cadence.js";
|
|
1
2
|
// ── JSON parsing (defensive — store is the writer, but never trust bytes) ──
|
|
2
3
|
function parseTags(json) {
|
|
3
4
|
try {
|
|
@@ -240,6 +241,67 @@ export function getOverrideSnapshot(db, slug) {
|
|
|
240
241
|
return {};
|
|
241
242
|
return parseMetadata(row.metadata_json).override_snapshot ?? {};
|
|
242
243
|
}
|
|
244
|
+
/**
|
|
245
|
+
* Read just the `enabled` flag for a slug, with a caller-supplied fallback for
|
|
246
|
+
* a missing row (pass the registry's `defaultEnabled` for built-ins). Cheap
|
|
247
|
+
* single-column read for fire-time / catch-up gates outside the scheduler's
|
|
248
|
+
* cached `isAgentEnabledForFiring` path.
|
|
249
|
+
*/
|
|
250
|
+
export function getAgentEnabled(db, slug, fallback) {
|
|
251
|
+
try {
|
|
252
|
+
const row = db
|
|
253
|
+
.prepare("SELECT enabled FROM agents WHERE id = ?")
|
|
254
|
+
.get(slug);
|
|
255
|
+
return row ? row.enabled === 1 : fallback;
|
|
256
|
+
}
|
|
257
|
+
catch {
|
|
258
|
+
// Defensive: a DB without the agents table (minimal test fixtures,
|
|
259
|
+
// pre-applySchema boot edge) behaves like a missing row.
|
|
260
|
+
return fallback;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Read the sanitized runtime-window override (`metadata_json.runtime_window`)
|
|
265
|
+
* for a slug. Returns `{}` when the Agent is missing or carries no override,
|
|
266
|
+
* so callers can hand the result straight to `resolveActivityScanCadence`.
|
|
267
|
+
*/
|
|
268
|
+
export function getRuntimeWindow(db, slug) {
|
|
269
|
+
try {
|
|
270
|
+
const row = db
|
|
271
|
+
.prepare("SELECT metadata_json FROM agents WHERE id = ?")
|
|
272
|
+
.get(slug);
|
|
273
|
+
if (!row)
|
|
274
|
+
return {};
|
|
275
|
+
return parseRuntimeWindowOverride(parseMetadata(row.metadata_json).runtime_window);
|
|
276
|
+
}
|
|
277
|
+
catch {
|
|
278
|
+
// Defensive: a DB without the agents table (minimal test fixtures,
|
|
279
|
+
// pre-applySchema boot edge) behaves like a missing row.
|
|
280
|
+
return {};
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Replace the runtime-window override, preserving every other `metadata_json`
|
|
285
|
+
* key. An empty override removes the `runtime_window` key entirely (all fields
|
|
286
|
+
* back on the config fallback). Returns the updated DTO, or `null` when no row
|
|
287
|
+
* matches.
|
|
288
|
+
*/
|
|
289
|
+
export function setRuntimeWindow(db, slug, window, now = Date.now()) {
|
|
290
|
+
const row = db
|
|
291
|
+
.prepare("SELECT metadata_json FROM agents WHERE id = ?")
|
|
292
|
+
.get(slug);
|
|
293
|
+
if (!row)
|
|
294
|
+
return null;
|
|
295
|
+
const metadata = parseMetadata(row.metadata_json);
|
|
296
|
+
if (Object.keys(window).length === 0) {
|
|
297
|
+
delete metadata.runtime_window;
|
|
298
|
+
}
|
|
299
|
+
else {
|
|
300
|
+
metadata.runtime_window = window;
|
|
301
|
+
}
|
|
302
|
+
db.prepare("UPDATE agents SET metadata_json = ?, updated_at = ? WHERE id = ?").run(JSON.stringify(metadata), now, slug);
|
|
303
|
+
return getAgent(db, slug);
|
|
304
|
+
}
|
|
243
305
|
/**
|
|
244
306
|
* Replace the override snapshot, preserving every other `metadata_json` key.
|
|
245
307
|
* An empty snapshot removes the `override_snapshot` key entirely (so a fully
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Background-task clarifications — BACKGROUND_TASK_RUNNER_DESIGN.md §6.
|
|
3
|
+
*
|
|
4
|
+
* One row per `ask_user` round-trip. The worker writes the row when it
|
|
5
|
+
* calls `ask_user`, the runner transitions the task to `awaiting_user`,
|
|
6
|
+
* and the delivery boundary surfaces the question to the owner (active
|
|
7
|
+
* delivery turn or idle draft send). The owner replies via DM; the DM
|
|
8
|
+
* agent forwards the answer through `POST /api/background-task/:id/clarify`
|
|
9
|
+
* which calls `resolveClarification` here.
|
|
10
|
+
*
|
|
11
|
+
* Deadline enforcement: a daemon housekeeping tick sweeps
|
|
12
|
+
* `WHERE resolved = 0 AND deadline_at < now()` via
|
|
13
|
+
* `listOverdueClarifications` and transitions the parent task to
|
|
14
|
+
* `timeout` (releasing the slot). The TTL is CONFIGURABLE
|
|
15
|
+
* (`backgroundTaskClarificationTtlMinutes`) and longer than
|
|
16
|
+
* browser-task's fixed 5 min — no browser resource is held while parked.
|
|
17
|
+
*
|
|
18
|
+
* I/O-bound. Excluded from the coverage gate.
|
|
19
|
+
*/
|
|
20
|
+
import type Database from "better-sqlite3";
|
|
21
|
+
export interface BackgroundTaskClarificationRow {
|
|
22
|
+
id: string;
|
|
23
|
+
taskId: string;
|
|
24
|
+
question: string;
|
|
25
|
+
contextSummary: string | null;
|
|
26
|
+
askedAt: number;
|
|
27
|
+
deadlineAt: number;
|
|
28
|
+
deliveredAt: number | null;
|
|
29
|
+
answer: string | null;
|
|
30
|
+
answeredAt: number | null;
|
|
31
|
+
resolved: boolean;
|
|
32
|
+
}
|
|
33
|
+
export interface CreateClarificationInput {
|
|
34
|
+
id: string;
|
|
35
|
+
taskId: string;
|
|
36
|
+
question: string;
|
|
37
|
+
contextSummary: string | null;
|
|
38
|
+
askedAt: number;
|
|
39
|
+
/** TTL in ms — the deadline is computed here (asked_at + ttlMs) so the
|
|
40
|
+
* runner and the scanner cannot disagree. The caller reads
|
|
41
|
+
* `backgroundTaskClarificationTtlMinutes` and passes ms. */
|
|
42
|
+
ttlMs: number;
|
|
43
|
+
}
|
|
44
|
+
export declare function createClarification(db: Database.Database, input: CreateClarificationInput): BackgroundTaskClarificationRow;
|
|
45
|
+
export declare function getClarification(db: Database.Database, id: string): BackgroundTaskClarificationRow | null;
|
|
46
|
+
export declare function listClarificationsForTask(db: Database.Database, taskId: string): readonly BackgroundTaskClarificationRow[];
|
|
47
|
+
/** The most-recent unresolved clarification for a task — the one a
|
|
48
|
+
* /clarify reply without an explicit id resolves against. */
|
|
49
|
+
export declare function getOpenClarificationForTask(db: Database.Database, taskId: string): BackgroundTaskClarificationRow | null;
|
|
50
|
+
export interface ResolveClarificationResult {
|
|
51
|
+
ok: boolean;
|
|
52
|
+
row: BackgroundTaskClarificationRow | null;
|
|
53
|
+
reason?: "not_found" | "already_resolved" | "expired";
|
|
54
|
+
}
|
|
55
|
+
/** CAS-resolve a clarification with the owner's answer. Refuses if
|
|
56
|
+
* already resolved (idempotent forwarding) or past deadline (the
|
|
57
|
+
* deadline scanner owns the timeout transition). */
|
|
58
|
+
export declare function resolveClarification(db: Database.Database, input: {
|
|
59
|
+
id: string;
|
|
60
|
+
answer: string;
|
|
61
|
+
answeredAt: number;
|
|
62
|
+
}): ResolveClarificationResult;
|
|
63
|
+
/** Sweep target — every unresolved clarification whose deadline has
|
|
64
|
+
* passed. The scanner transitions each parent task to `timeout`. */
|
|
65
|
+
export declare function listOverdueClarifications(db: Database.Database, nowMs: number): readonly BackgroundTaskClarificationRow[];
|
|
66
|
+
/** Mark a clarification resolved without an answer (deadline path). */
|
|
67
|
+
export declare function expireClarification(db: Database.Database, id: string, nowMs: number): BackgroundTaskClarificationRow | null;
|
|
68
|
+
export declare function markClarificationDelivered(db: Database.Database, id: string, deliveredAt: number): BackgroundTaskClarificationRow | null;
|
|
69
|
+
/** A recovery-sweep clarification row enriched with the parent task's
|
|
70
|
+
* delivery fields. The list query already INNER JOINs `background_task`
|
|
71
|
+
* (on `state='awaiting_user'`), so these are folded in here — the sweep
|
|
72
|
+
* needs no second `getBackgroundTask` fetch and carries no unreachable
|
|
73
|
+
* "task missing" guard. */
|
|
74
|
+
export interface UndeliveredClarificationRow extends BackgroundTaskClarificationRow {
|
|
75
|
+
taskOriginatingChannel: string | null;
|
|
76
|
+
taskTitle: string | null;
|
|
77
|
+
taskBrief: string;
|
|
78
|
+
}
|
|
79
|
+
/** Delivery recovery target — undelivered, still-open clarifications
|
|
80
|
+
* whose parent task is parked, joined with that task's delivery fields. */
|
|
81
|
+
export declare function listUndeliveredClarifications(db: Database.Database, nowMs: number, limit?: number): readonly UndeliveredClarificationRow[];
|