@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,380 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Background-task row store — BACKGROUND_TASK_RUNNER_DESIGN.md §6.
|
|
3
|
+
*
|
|
4
|
+
* I/O-bound CRUD over `background_task`. The state machine is enforced
|
|
5
|
+
* at this layer via the CHECK constraint on the `state` column (closed
|
|
6
|
+
* set) plus the per-transition CAS helpers below — `markRunning`,
|
|
7
|
+
* `markAwaitingUser`, `markRunningFromParked`, `markTerminal` — each
|
|
8
|
+
* one's WHERE clause refuses an out-of-order write so a race between two
|
|
9
|
+
* writers cannot flip a row backwards.
|
|
10
|
+
*
|
|
11
|
+
* The genuinely new shape vs `browser_task` is the ARTIFACT: `report`
|
|
12
|
+
* (verbatim result — the fidelity anchor), `draft` (worker-authored
|
|
13
|
+
* summary), `notify` (the worker's disposition vs the spawn-time policy),
|
|
14
|
+
* `significance`, and `artifact_path`. `markTerminal` writes them in the
|
|
15
|
+
* same transition as the terminal state so a worker's `finish` (or the
|
|
16
|
+
* runner's fail-loud synthesis) is atomic.
|
|
17
|
+
*
|
|
18
|
+
* Pure decision logic (slot arithmetic) is reused from
|
|
19
|
+
* `services/browser-task/browser-task-slots.ts`; this module is the SQL
|
|
20
|
+
* wrapper and is excluded from the coverage gate (same posture as
|
|
21
|
+
* `browser-task-store.ts`).
|
|
22
|
+
*/
|
|
23
|
+
export const BACKGROUND_TASK_TERMINAL_STATES = new Set(["completed", "failed", "timeout", "cancelled"]);
|
|
24
|
+
export const BACKGROUND_TASK_NON_TERMINAL_STATES = new Set(["pending", "running", "awaiting_user"]);
|
|
25
|
+
const SELECT_COLUMNS = `id, brief, title, state, notification_policy,
|
|
26
|
+
significance_criteria, report, draft, notify, significance,
|
|
27
|
+
artifact_path, outcome_detail, originating_channel, correlation_id,
|
|
28
|
+
schedule_row_id, tier, max_budget_usd, backend_session_id,
|
|
29
|
+
created_at, started_at, finished_at, delivered_at`;
|
|
30
|
+
/** Parse the persisted `significance_criteria` JSON. Tolerant: a malformed
|
|
31
|
+
* or non-array value degrades to null rather than throwing, so a row hand-
|
|
32
|
+
* written by a migration / test can never crash a read. */
|
|
33
|
+
function parseSignificanceCriteria(raw) {
|
|
34
|
+
if (!raw)
|
|
35
|
+
return null;
|
|
36
|
+
try {
|
|
37
|
+
const parsed = JSON.parse(raw);
|
|
38
|
+
if (!Array.isArray(parsed))
|
|
39
|
+
return null;
|
|
40
|
+
const items = parsed.filter((x) => typeof x === "string");
|
|
41
|
+
return items.length > 0 ? items : null;
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function fromDbRow(row) {
|
|
48
|
+
return {
|
|
49
|
+
id: row.id,
|
|
50
|
+
brief: row.brief,
|
|
51
|
+
title: row.title,
|
|
52
|
+
state: row.state,
|
|
53
|
+
notificationPolicy: row.notification_policy,
|
|
54
|
+
significanceCriteria: parseSignificanceCriteria(row.significance_criteria),
|
|
55
|
+
report: row.report,
|
|
56
|
+
draft: row.draft,
|
|
57
|
+
notify: row.notify === null ? null : row.notify === 1,
|
|
58
|
+
significance: row.significance,
|
|
59
|
+
artifactPath: row.artifact_path,
|
|
60
|
+
outcomeDetail: row.outcome_detail,
|
|
61
|
+
originatingChannel: row.originating_channel,
|
|
62
|
+
correlationId: row.correlation_id,
|
|
63
|
+
scheduleRowId: row.schedule_row_id,
|
|
64
|
+
tier: row.tier,
|
|
65
|
+
maxBudgetUsd: row.max_budget_usd,
|
|
66
|
+
backendSessionId: row.backend_session_id,
|
|
67
|
+
createdAt: row.created_at,
|
|
68
|
+
startedAt: row.started_at,
|
|
69
|
+
finishedAt: row.finished_at,
|
|
70
|
+
deliveredAt: row.delivered_at,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
/** Insert a fresh row in state=pending. The slot manager promotes it to
|
|
74
|
+
* `running` once a slot frees. */
|
|
75
|
+
export function createBackgroundTask(db, input) {
|
|
76
|
+
const criteria = input.significanceCriteria && input.significanceCriteria.length > 0
|
|
77
|
+
? JSON.stringify([...input.significanceCriteria])
|
|
78
|
+
: null;
|
|
79
|
+
db.prepare(`INSERT INTO background_task
|
|
80
|
+
(id, brief, title, state, notification_policy, significance_criteria,
|
|
81
|
+
report, draft, notify, significance, artifact_path,
|
|
82
|
+
outcome_detail, originating_channel, correlation_id, schedule_row_id,
|
|
83
|
+
tier, max_budget_usd, backend_session_id,
|
|
84
|
+
created_at, started_at, finished_at, delivered_at)
|
|
85
|
+
VALUES (?, ?, ?, 'pending', ?, ?,
|
|
86
|
+
NULL, NULL, NULL, NULL, NULL,
|
|
87
|
+
NULL, ?, ?, ?,
|
|
88
|
+
?, ?, NULL,
|
|
89
|
+
?, NULL, NULL, NULL)`).run(input.id, input.brief, input.title, input.notificationPolicy, criteria, input.originatingChannel, input.correlationId, input.scheduleRowId, input.tier, input.maxBudgetUsd, input.createdAt);
|
|
90
|
+
const row = getBackgroundTask(db, input.id);
|
|
91
|
+
if (!row) {
|
|
92
|
+
throw new Error(`createBackgroundTask: post-insert row for ${input.id} missing`);
|
|
93
|
+
}
|
|
94
|
+
return row;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* §10.3 brief-dedup — find a still-relevant task with an IDENTICAL brief
|
|
98
|
+
* spawned inside the dedup window, so a runaway fan-out (the
|
|
99
|
+
* RESEARCH_CLUSTER_COST_FIX_PLAN class: a replayed trigger POSTing the
|
|
100
|
+
* same brief many times in minutes) collapses onto the first task instead
|
|
101
|
+
* of spawning N workers. Matches on `brief` + `tier` (the two inputs that
|
|
102
|
+
* define "the same work" — `notificationPolicy` only affects delivery, not
|
|
103
|
+
* the work done) within `sinceMs`, and excludes the FAIL terminals
|
|
104
|
+
* (`failed`/`timeout`/`cancelled`) so a prior failure is retryable rather
|
|
105
|
+
* than sticky. A `completed` duplicate inside the window IS returned — the
|
|
106
|
+
* answer already exists, re-running would just re-spend. Newest first.
|
|
107
|
+
*/
|
|
108
|
+
export function findRecentDuplicateBackgroundTask(db, input) {
|
|
109
|
+
const row = db
|
|
110
|
+
.prepare(`SELECT ${SELECT_COLUMNS}
|
|
111
|
+
FROM background_task
|
|
112
|
+
WHERE brief = ?
|
|
113
|
+
AND tier IS ?
|
|
114
|
+
AND created_at >= ?
|
|
115
|
+
AND state NOT IN ('failed', 'timeout', 'cancelled')
|
|
116
|
+
ORDER BY created_at DESC
|
|
117
|
+
LIMIT 1`)
|
|
118
|
+
.get(input.brief, input.tier, input.sinceMs);
|
|
119
|
+
return row ? fromDbRow(row) : null;
|
|
120
|
+
}
|
|
121
|
+
export function getBackgroundTask(db, id) {
|
|
122
|
+
const row = db
|
|
123
|
+
.prepare(`SELECT ${SELECT_COLUMNS} FROM background_task WHERE id = ?`)
|
|
124
|
+
.get(id);
|
|
125
|
+
return row ? fromDbRow(row) : null;
|
|
126
|
+
}
|
|
127
|
+
/** Shared WHERE builder for list + count so the two never diverge. */
|
|
128
|
+
function buildListWhere(options) {
|
|
129
|
+
const { states, notify, finishedSinceMs } = options;
|
|
130
|
+
const where = [];
|
|
131
|
+
const params = [];
|
|
132
|
+
if (states && states.length > 0) {
|
|
133
|
+
where.push(`state IN (${states.map(() => "?").join(", ")})`);
|
|
134
|
+
params.push(...states);
|
|
135
|
+
}
|
|
136
|
+
if (notify !== undefined) {
|
|
137
|
+
where.push(`notify = ?`);
|
|
138
|
+
params.push(notify ? 1 : 0);
|
|
139
|
+
}
|
|
140
|
+
if (finishedSinceMs !== undefined) {
|
|
141
|
+
where.push(`finished_at >= ?`);
|
|
142
|
+
params.push(finishedSinceMs);
|
|
143
|
+
}
|
|
144
|
+
return {
|
|
145
|
+
clause: where.length ? `WHERE ${where.join(" AND ")}` : "",
|
|
146
|
+
params,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
export function listBackgroundTasks(db, options = {}) {
|
|
150
|
+
const { limit = 50, offset = 0 } = options;
|
|
151
|
+
const { clause, params } = buildListWhere(options);
|
|
152
|
+
const sql = `SELECT ${SELECT_COLUMNS}
|
|
153
|
+
FROM background_task
|
|
154
|
+
${clause}
|
|
155
|
+
ORDER BY created_at DESC
|
|
156
|
+
LIMIT ? OFFSET ?`;
|
|
157
|
+
const rows = db
|
|
158
|
+
.prepare(sql)
|
|
159
|
+
.all(...params, limit, offset);
|
|
160
|
+
return rows.map(fromDbRow);
|
|
161
|
+
}
|
|
162
|
+
export function countBackgroundTasks(db, options = {}) {
|
|
163
|
+
const { clause, params } = buildListWhere(options);
|
|
164
|
+
const sql = `SELECT COUNT(*) AS c FROM background_task
|
|
165
|
+
${clause}`;
|
|
166
|
+
const row = db.prepare(sql).get(...params);
|
|
167
|
+
return row?.c ?? 0;
|
|
168
|
+
}
|
|
169
|
+
/** Pending → running. CAS on prior state so a concurrent terminal
|
|
170
|
+
* transition (cancel-while-pending) does not get clobbered. */
|
|
171
|
+
export function markRunning(db, id, startedAt) {
|
|
172
|
+
const result = db
|
|
173
|
+
.prepare(`UPDATE background_task
|
|
174
|
+
SET state = 'running', started_at = COALESCE(started_at, ?)
|
|
175
|
+
WHERE id = ? AND state = 'pending'`)
|
|
176
|
+
.run(startedAt, id);
|
|
177
|
+
return result.changes > 0 ? getBackgroundTask(db, id) : null;
|
|
178
|
+
}
|
|
179
|
+
/** Running → awaiting_user. Slot stays held; resume via /clarify. */
|
|
180
|
+
export function markAwaitingUser(db, id) {
|
|
181
|
+
const result = db
|
|
182
|
+
.prepare(`UPDATE background_task
|
|
183
|
+
SET state = 'awaiting_user'
|
|
184
|
+
WHERE id = ? AND state = 'running'`)
|
|
185
|
+
.run(id);
|
|
186
|
+
return result.changes > 0 ? getBackgroundTask(db, id) : null;
|
|
187
|
+
}
|
|
188
|
+
/** awaiting_user → running. Used by /clarify resume. */
|
|
189
|
+
export function markRunningFromParked(db, id) {
|
|
190
|
+
const result = db
|
|
191
|
+
.prepare(`UPDATE background_task
|
|
192
|
+
SET state = 'running'
|
|
193
|
+
WHERE id = ? AND state = 'awaiting_user'`)
|
|
194
|
+
.run(id);
|
|
195
|
+
return result.changes > 0 ? getBackgroundTask(db, id) : null;
|
|
196
|
+
}
|
|
197
|
+
/** Any non-terminal state → terminal, writing the artifact atomically.
|
|
198
|
+
* Idempotent — re-running on an already-terminal row CAS-misses and
|
|
199
|
+
* returns null. */
|
|
200
|
+
export function markTerminal(db, input) {
|
|
201
|
+
const result = db
|
|
202
|
+
.prepare(`UPDATE background_task
|
|
203
|
+
SET state = ?,
|
|
204
|
+
outcome_detail = ?,
|
|
205
|
+
report = COALESCE(?, report),
|
|
206
|
+
draft = COALESCE(?, draft),
|
|
207
|
+
notify = COALESCE(?, notify),
|
|
208
|
+
significance = COALESCE(?, significance),
|
|
209
|
+
artifact_path = COALESCE(?, artifact_path),
|
|
210
|
+
finished_at = ?
|
|
211
|
+
WHERE id = ?
|
|
212
|
+
AND state IN ('pending', 'running', 'awaiting_user')`)
|
|
213
|
+
.run(input.state, input.outcomeDetail, input.report ?? null, input.draft ?? null, input.notify === undefined ? null : input.notify ? 1 : 0, input.significance ?? null, input.artifactPath ?? null, input.finishedAt, input.id);
|
|
214
|
+
return result.changes > 0 ? getBackgroundTask(db, input.id) : null;
|
|
215
|
+
}
|
|
216
|
+
/** Capture the SDK session id once the first turn streams it, so a
|
|
217
|
+
* /clarify resume can `query({resume})` the warm session. */
|
|
218
|
+
export function setBackendSessionId(db, id, sessionId) {
|
|
219
|
+
db.prepare(`UPDATE background_task SET backend_session_id = ? WHERE id = ?`).run(sessionId, id);
|
|
220
|
+
}
|
|
221
|
+
export function markBackgroundTaskDelivered(db, id, deliveredAt) {
|
|
222
|
+
const result = db
|
|
223
|
+
.prepare(`UPDATE background_task
|
|
224
|
+
SET delivered_at = COALESCE(delivered_at, ?)
|
|
225
|
+
WHERE id = ?`)
|
|
226
|
+
.run(deliveredAt, id);
|
|
227
|
+
return result.changes > 0 ? getBackgroundTask(db, id) : null;
|
|
228
|
+
}
|
|
229
|
+
/** Delivery recovery target — completed rows whose worker stored a
|
|
230
|
+
* notify=true artifact but whose DM was never sent/recorded (§10.2). */
|
|
231
|
+
export function listUndeliveredBackgroundTaskReports(db, limit = 20) {
|
|
232
|
+
const rows = db
|
|
233
|
+
.prepare(`SELECT ${SELECT_COLUMNS}
|
|
234
|
+
FROM background_task
|
|
235
|
+
WHERE state = 'completed'
|
|
236
|
+
AND notify = 1
|
|
237
|
+
AND delivered_at IS NULL
|
|
238
|
+
AND draft IS NOT NULL
|
|
239
|
+
ORDER BY finished_at ASC, created_at ASC
|
|
240
|
+
LIMIT ?`)
|
|
241
|
+
.all(limit);
|
|
242
|
+
return rows.map(fromDbRow);
|
|
243
|
+
}
|
|
244
|
+
/** §10.5 — filed (notify=false) results, for the periodic digest +
|
|
245
|
+
* owner pull ("did that monitor ever run?"). */
|
|
246
|
+
export function listFiledBackgroundTaskResults(db, sinceMs, limit = 50) {
|
|
247
|
+
const rows = db
|
|
248
|
+
.prepare(`SELECT ${SELECT_COLUMNS}
|
|
249
|
+
FROM background_task
|
|
250
|
+
WHERE state = 'completed'
|
|
251
|
+
AND notify = 0
|
|
252
|
+
AND finished_at >= ?
|
|
253
|
+
ORDER BY finished_at DESC
|
|
254
|
+
LIMIT ?`)
|
|
255
|
+
.all(sinceMs, limit);
|
|
256
|
+
return rows.map(fromDbRow);
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* §10.2 boot re-dispatch-from-brief — reset every non-terminal row to
|
|
260
|
+
* `pending` (clearing the lost in-memory session) so the event-pipeline
|
|
261
|
+
* boot hook can re-run each one's brief through the runner. Returns the
|
|
262
|
+
* ids reset so the caller can fan out the re-dispatch.
|
|
263
|
+
*
|
|
264
|
+
* Unlike browser_task's `sweepNonTerminalRowsForBootRecovery` (which
|
|
265
|
+
* force-fails), background tasks are re-dispatchable because the brief is
|
|
266
|
+
* self-contained. `backend_session_id` is cleared since the prior SDK
|
|
267
|
+
* session is unreachable after a restart.
|
|
268
|
+
*
|
|
269
|
+
* Open clarifications belonging to the reset tasks are resolved in the
|
|
270
|
+
* SAME transaction. The pre-restart run that raised an `ask_user` is
|
|
271
|
+
* gone, so its clarification row is orphaned: a surviving `resolved = 0`
|
|
272
|
+
* row would later trip the deadline scanner (`listOverdueClarifications`
|
|
273
|
+
* → `expireForDeadline`) into transitioning the FRESH re-dispatched run
|
|
274
|
+
* to `timeout` — and because re-dispatch makes the task ACTIVE again
|
|
275
|
+
* (pending→running), that `expireForDeadline` is NOT a no-op the way it
|
|
276
|
+
* is for a terminal row. Clearing them here closes that window.
|
|
277
|
+
*/
|
|
278
|
+
export function resetNonTerminalForBootRedispatch(db, nowMs = Date.now()) {
|
|
279
|
+
const txn = db.transaction(() => {
|
|
280
|
+
const rows = db
|
|
281
|
+
.prepare(`SELECT id FROM background_task
|
|
282
|
+
WHERE state IN ('pending', 'running', 'awaiting_user')`)
|
|
283
|
+
.all();
|
|
284
|
+
if (rows.length === 0)
|
|
285
|
+
return [];
|
|
286
|
+
// Abandon orphaned clarifications BEFORE the state reset (so the
|
|
287
|
+
// `task_id IN (non-terminal)` subquery still matches them).
|
|
288
|
+
db.prepare(`UPDATE background_task_clarifications
|
|
289
|
+
SET resolved = 1, answered_at = COALESCE(answered_at, ?)
|
|
290
|
+
WHERE resolved = 0
|
|
291
|
+
AND task_id IN (
|
|
292
|
+
SELECT id FROM background_task
|
|
293
|
+
WHERE state IN ('pending', 'running', 'awaiting_user')
|
|
294
|
+
)`).run(nowMs);
|
|
295
|
+
db.prepare(`UPDATE background_task
|
|
296
|
+
SET state = 'pending',
|
|
297
|
+
started_at = NULL,
|
|
298
|
+
backend_session_id = NULL
|
|
299
|
+
WHERE state IN ('pending', 'running', 'awaiting_user')`).run();
|
|
300
|
+
return rows.map((r) => ({ id: r.id }));
|
|
301
|
+
});
|
|
302
|
+
return txn();
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Phase 4 resume-across-restart (§10.2) — the non-terminal rows the boot
|
|
306
|
+
* recovery path partitions into "resume the SDK session" vs "re-dispatch
|
|
307
|
+
* from brief". Returns just the discriminators (id, state, session id) so
|
|
308
|
+
* the caller can decide without loading the full artifact.
|
|
309
|
+
*/
|
|
310
|
+
export function listNonTerminalBackgroundTasks(db) {
|
|
311
|
+
return db
|
|
312
|
+
.prepare(`SELECT id, state, backend_session_id
|
|
313
|
+
FROM background_task
|
|
314
|
+
WHERE state IN ('pending', 'running', 'awaiting_user')
|
|
315
|
+
ORDER BY created_at ASC`)
|
|
316
|
+
.all()
|
|
317
|
+
.map((r) => ({ id: r.id, state: r.state, backendSessionId: r.backend_session_id }));
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Phase 4 resume-across-restart (§10.2) — reset ONE non-terminal row back
|
|
321
|
+
* to `pending` for re-dispatch-from-brief, clearing its (now unreachable)
|
|
322
|
+
* SDK session id and resolving its orphaned open clarifications in the same
|
|
323
|
+
* transaction (same rationale as the bulk
|
|
324
|
+
* `resetNonTerminalForBootRedispatch`: a surviving `resolved = 0` row would
|
|
325
|
+
* trip the deadline scanner into timing out the FRESH re-dispatched run).
|
|
326
|
+
* Used by the boot path for the rows it re-dispatches and by the runner's
|
|
327
|
+
* resume-failure fallback. Returns the row id when it was non-terminal,
|
|
328
|
+
* else null (idempotent on an already-terminal / missing row).
|
|
329
|
+
*/
|
|
330
|
+
export function resetSingleForBootRedispatch(db, id, nowMs = Date.now()) {
|
|
331
|
+
const txn = db.transaction(() => {
|
|
332
|
+
const row = db
|
|
333
|
+
.prepare(`SELECT state FROM background_task WHERE id = ?`)
|
|
334
|
+
.get(id);
|
|
335
|
+
if (!row || !BACKGROUND_TASK_NON_TERMINAL_STATES.has(row.state)) {
|
|
336
|
+
return null;
|
|
337
|
+
}
|
|
338
|
+
db.prepare(`UPDATE background_task_clarifications
|
|
339
|
+
SET resolved = 1, answered_at = COALESCE(answered_at, ?)
|
|
340
|
+
WHERE resolved = 0 AND task_id = ?`).run(nowMs, id);
|
|
341
|
+
db.prepare(`UPDATE background_task
|
|
342
|
+
SET state = 'pending',
|
|
343
|
+
started_at = NULL,
|
|
344
|
+
backend_session_id = NULL
|
|
345
|
+
WHERE id = ?`).run(id);
|
|
346
|
+
return id;
|
|
347
|
+
});
|
|
348
|
+
return txn();
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* Fold a just-answered clarification into the task's brief so a COLD
|
|
352
|
+
* re-dispatch (when the warm SDK session can't be resumed across a restart,
|
|
353
|
+
* §10.2) still carries the owner's answer and doesn't re-ask the same
|
|
354
|
+
* question. The worker only ever sees the brief, so appending the resolved
|
|
355
|
+
* Q&A is the only way to thread the answer into a fresh run. Idempotency is
|
|
356
|
+
* not required — this runs at most once per clarification per re-dispatch,
|
|
357
|
+
* and the clarify route has already CAS-resolved the row.
|
|
358
|
+
*/
|
|
359
|
+
export function appendResolvedClarificationToBrief(db, id, question, answer) {
|
|
360
|
+
const block = `\n\n<resolved_clarification>\n`
|
|
361
|
+
+ (question ? `You previously asked: ${question}\n` : "")
|
|
362
|
+
+ `The owner answered: ${answer}\n`
|
|
363
|
+
+ `</resolved_clarification>`;
|
|
364
|
+
db.prepare(`UPDATE background_task SET brief = brief || ? WHERE id = ?`).run(block, id);
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* Retention prune for terminal rows older than `cutoffMs`. Children in
|
|
368
|
+
* `background_task_clarifications` go with the parent via ON DELETE
|
|
369
|
+
* CASCADE. Non-terminal rows are never deleted (the boot re-dispatch
|
|
370
|
+
* sweep owns them). `finished_at` is the lifetime anchor with a
|
|
371
|
+
* `created_at` fallback for the rare unset-finished_at terminal.
|
|
372
|
+
*/
|
|
373
|
+
export function deleteTerminalBackgroundTasksOlderThan(db, cutoffMs) {
|
|
374
|
+
const result = db
|
|
375
|
+
.prepare(`DELETE FROM background_task
|
|
376
|
+
WHERE state IN ('completed', 'failed', 'timeout', 'cancelled')
|
|
377
|
+
AND COALESCE(finished_at, created_at) < ?`)
|
|
378
|
+
.run(cutoffMs);
|
|
379
|
+
return result.changes;
|
|
380
|
+
}
|
|
@@ -184,16 +184,49 @@ export declare function renameResearchCluster(db: Database.Database, slug: strin
|
|
|
184
184
|
*/
|
|
185
185
|
export declare function bumpClusterAgentSummaryRevision(db: Database.Database, slug: string): number;
|
|
186
186
|
/**
|
|
187
|
-
* Clusters
|
|
188
|
-
*
|
|
189
|
-
*
|
|
190
|
-
*
|
|
191
|
-
*
|
|
187
|
+
* Clusters eligible for a `routine.research_cluster_update` enqueue this
|
|
188
|
+
* agent-day: `active` rows with meaningful activity inside the lookback
|
|
189
|
+
* window that have NOT already been enqueued for `todayAgentDay`
|
|
190
|
+
* (`journal_update_enqueued_on` is the stamp `claimClusterJournalEnqueue`
|
|
191
|
+
* writes at fan-out time; ISO 'YYYY-MM-DD' strings compare lexically =
|
|
192
|
+
* chronologically, so `< todayAgentDay` means "last enqueued on an
|
|
193
|
+
* earlier agent day"). This filter is the efficiency gate for SEQUENTIAL
|
|
194
|
+
* replays (a wake catch-up / morning self-heal re-running after a prior
|
|
195
|
+
* fire completed); the atomic per-row claim in `claimClusterJournalEnqueue`
|
|
196
|
+
* is what additionally protects against two CONCURRENTLY in-flight
|
|
197
|
+
* callbacks — RESEARCH_CLUSTER_COST_FIX_PLAN.md RC1. Muted / concluded / dormant clusters do not get nightly journal
|
|
198
|
+
* appends. Capped by `limit` so a backlog never floods the fan-out.
|
|
192
199
|
*/
|
|
193
|
-
export declare function listClustersNeedingUpdate(db: Database.Database, lookbackMs: number, nowMs
|
|
200
|
+
export declare function listClustersNeedingUpdate(db: Database.Database, lookbackMs: number, nowMs: number, limit: number, todayAgentDay: string): Array<{
|
|
194
201
|
slug: string;
|
|
195
202
|
displayName: string;
|
|
196
203
|
}>;
|
|
204
|
+
/**
|
|
205
|
+
* Atomically CLAIM `agentDay`'s `routine.research_cluster_update` enqueue
|
|
206
|
+
* slot for this cluster. Returns `true` iff THIS call wrote the stamp —
|
|
207
|
+
* the prior value was NULL or an earlier agent-day; `false` if the slot
|
|
208
|
+
* was already claimed for `agentDay` (or a newer one) or the slug is
|
|
209
|
+
* unknown. Called BEFORE `eventBus.put` (claim-before-enqueue — same
|
|
210
|
+
* rationale as `fireDecision`'s `stampClusterDmFields` call in
|
|
211
|
+
* browser-history-poller.ts).
|
|
212
|
+
*
|
|
213
|
+
* The conditional UPDATE is a single atomic SQLite statement, so two
|
|
214
|
+
* day-boundary callbacks racing on the same cluster cannot both claim
|
|
215
|
+
* it: the 04:00 cron fires `onDayBoundary` fire-and-forget (scheduler.ts)
|
|
216
|
+
* and can overlap a wake catch-up, and each fan-out iterates its own
|
|
217
|
+
* `listClustersNeedingUpdate` snapshot — so the per-row claim, not the
|
|
218
|
+
* snapshot filter, is what guarantees exactly one enqueue per cluster per
|
|
219
|
+
* agent day. Exactly one caller sees `changes === 1` and enqueues; the
|
|
220
|
+
* loser skips. The `< ?` guard doubles as monotonicity — a stale replay
|
|
221
|
+
* carrying an older agent-day matches no row and cannot regress a newer
|
|
222
|
+
* stamp (ISO 'YYYY-MM-DD' strings compare lexically = chronologically).
|
|
223
|
+
*
|
|
224
|
+
* Failure semantics are intentional: the claim persists even if the
|
|
225
|
+
* subsequent `eventBus.put` throws, so a cluster whose enqueue failed
|
|
226
|
+
* retries on the NEXT agent day — bounded, no same-day loop (the F6
|
|
227
|
+
* backfill covers the missed day).
|
|
228
|
+
*/
|
|
229
|
+
export declare function claimClusterJournalEnqueue(db: Database.Database, slug: string, agentDay: string): boolean;
|
|
197
230
|
export declare const OFFER_DEFAULT_TTL_MS: number;
|
|
198
231
|
export interface PendingOfferInput {
|
|
199
232
|
slug: string;
|
|
@@ -498,22 +498,66 @@ export function bumpClusterAgentSummaryRevision(db, slug) {
|
|
|
498
498
|
return row.rev;
|
|
499
499
|
}
|
|
500
500
|
/**
|
|
501
|
-
* Clusters
|
|
502
|
-
*
|
|
503
|
-
*
|
|
504
|
-
*
|
|
505
|
-
*
|
|
501
|
+
* Clusters eligible for a `routine.research_cluster_update` enqueue this
|
|
502
|
+
* agent-day: `active` rows with meaningful activity inside the lookback
|
|
503
|
+
* window that have NOT already been enqueued for `todayAgentDay`
|
|
504
|
+
* (`journal_update_enqueued_on` is the stamp `claimClusterJournalEnqueue`
|
|
505
|
+
* writes at fan-out time; ISO 'YYYY-MM-DD' strings compare lexically =
|
|
506
|
+
* chronologically, so `< todayAgentDay` means "last enqueued on an
|
|
507
|
+
* earlier agent day"). This filter is the efficiency gate for SEQUENTIAL
|
|
508
|
+
* replays (a wake catch-up / morning self-heal re-running after a prior
|
|
509
|
+
* fire completed); the atomic per-row claim in `claimClusterJournalEnqueue`
|
|
510
|
+
* is what additionally protects against two CONCURRENTLY in-flight
|
|
511
|
+
* callbacks — RESEARCH_CLUSTER_COST_FIX_PLAN.md RC1. Muted / concluded / dormant clusters do not get nightly journal
|
|
512
|
+
* appends. Capped by `limit` so a backlog never floods the fan-out.
|
|
506
513
|
*/
|
|
507
|
-
export function listClustersNeedingUpdate(db, lookbackMs, nowMs
|
|
514
|
+
export function listClustersNeedingUpdate(db, lookbackMs, nowMs, limit, todayAgentDay) {
|
|
508
515
|
const since = nowMs - lookbackMs;
|
|
509
516
|
return db
|
|
510
517
|
.prepare(`SELECT slug, display_name AS displayName
|
|
511
518
|
FROM browser_research_clusters
|
|
512
519
|
WHERE status = 'active'
|
|
513
520
|
AND last_activity_at >= ?
|
|
521
|
+
AND (journal_update_enqueued_on IS NULL
|
|
522
|
+
OR journal_update_enqueued_on < ?)
|
|
514
523
|
ORDER BY last_activity_at DESC
|
|
515
524
|
LIMIT ?`)
|
|
516
|
-
.all(since, limit);
|
|
525
|
+
.all(since, todayAgentDay, limit);
|
|
526
|
+
}
|
|
527
|
+
/**
|
|
528
|
+
* Atomically CLAIM `agentDay`'s `routine.research_cluster_update` enqueue
|
|
529
|
+
* slot for this cluster. Returns `true` iff THIS call wrote the stamp —
|
|
530
|
+
* the prior value was NULL or an earlier agent-day; `false` if the slot
|
|
531
|
+
* was already claimed for `agentDay` (or a newer one) or the slug is
|
|
532
|
+
* unknown. Called BEFORE `eventBus.put` (claim-before-enqueue — same
|
|
533
|
+
* rationale as `fireDecision`'s `stampClusterDmFields` call in
|
|
534
|
+
* browser-history-poller.ts).
|
|
535
|
+
*
|
|
536
|
+
* The conditional UPDATE is a single atomic SQLite statement, so two
|
|
537
|
+
* day-boundary callbacks racing on the same cluster cannot both claim
|
|
538
|
+
* it: the 04:00 cron fires `onDayBoundary` fire-and-forget (scheduler.ts)
|
|
539
|
+
* and can overlap a wake catch-up, and each fan-out iterates its own
|
|
540
|
+
* `listClustersNeedingUpdate` snapshot — so the per-row claim, not the
|
|
541
|
+
* snapshot filter, is what guarantees exactly one enqueue per cluster per
|
|
542
|
+
* agent day. Exactly one caller sees `changes === 1` and enqueues; the
|
|
543
|
+
* loser skips. The `< ?` guard doubles as monotonicity — a stale replay
|
|
544
|
+
* carrying an older agent-day matches no row and cannot regress a newer
|
|
545
|
+
* stamp (ISO 'YYYY-MM-DD' strings compare lexically = chronologically).
|
|
546
|
+
*
|
|
547
|
+
* Failure semantics are intentional: the claim persists even if the
|
|
548
|
+
* subsequent `eventBus.put` throws, so a cluster whose enqueue failed
|
|
549
|
+
* retries on the NEXT agent day — bounded, no same-day loop (the F6
|
|
550
|
+
* backfill covers the missed day).
|
|
551
|
+
*/
|
|
552
|
+
export function claimClusterJournalEnqueue(db, slug, agentDay) {
|
|
553
|
+
const info = db
|
|
554
|
+
.prepare(`UPDATE browser_research_clusters
|
|
555
|
+
SET journal_update_enqueued_on = ?
|
|
556
|
+
WHERE slug = ?
|
|
557
|
+
AND (journal_update_enqueued_on IS NULL
|
|
558
|
+
OR journal_update_enqueued_on < ?)`)
|
|
559
|
+
.run(agentDay, slug, agentDay);
|
|
560
|
+
return info.changes > 0;
|
|
517
561
|
}
|
|
518
562
|
// ── browser_pending_offers — materialised view of open offer state ──
|
|
519
563
|
export const OFFER_DEFAULT_TTL_MS = 14 * 24 * 60 * 60 * 1000;
|
|
@@ -26,6 +26,7 @@ export interface BrowserTaskClarificationRow {
|
|
|
26
26
|
screenshotKey: string | null;
|
|
27
27
|
askedAt: number;
|
|
28
28
|
deadlineAt: number;
|
|
29
|
+
deliveredAt: number | null;
|
|
29
30
|
answer: string | null;
|
|
30
31
|
answeredAt: number | null;
|
|
31
32
|
resolved: boolean;
|
|
@@ -65,3 +66,14 @@ export declare function listOverdueClarifications(db: Database.Database, nowMs:
|
|
|
65
66
|
* Differs from `resolveClarification` in that it skips the deadline
|
|
66
67
|
* check — the scanner is calling this BECAUSE the deadline passed. */
|
|
67
68
|
export declare function expireClarification(db: Database.Database, id: string, nowMs: number): BrowserTaskClarificationRow | null;
|
|
69
|
+
export declare function markClarificationDelivered(db: Database.Database, id: string, deliveredAt: number): BrowserTaskClarificationRow | null;
|
|
70
|
+
/** A recovery-sweep clarification row enriched with the parent task's
|
|
71
|
+
* delivery fields. The list query already INNER JOINs `browser_task`
|
|
72
|
+
* (on `state='awaiting_user'`), so the originating channel + description
|
|
73
|
+
* are folded in here — the sweep needs no second `getBrowserTask` fetch
|
|
74
|
+
* and carries no unreachable "task missing" guard. */
|
|
75
|
+
export interface UndeliveredClarificationRow extends BrowserTaskClarificationRow {
|
|
76
|
+
taskOriginatingChannel: string | null;
|
|
77
|
+
taskDescription: string;
|
|
78
|
+
}
|
|
79
|
+
export declare function listUndeliveredClarifications(db: Database.Database, nowMs: number, limit?: number): readonly UndeliveredClarificationRow[];
|
|
@@ -26,6 +26,7 @@ function fromDbRow(row) {
|
|
|
26
26
|
screenshotKey: row.screenshot_key,
|
|
27
27
|
askedAt: row.asked_at,
|
|
28
28
|
deadlineAt: row.deadline_at,
|
|
29
|
+
deliveredAt: row.delivered_at,
|
|
29
30
|
answer: row.answer,
|
|
30
31
|
answeredAt: row.answered_at,
|
|
31
32
|
resolved: row.resolved === 1,
|
|
@@ -38,8 +39,8 @@ export function createClarification(db, input) {
|
|
|
38
39
|
const deadline = input.askedAt + CLARIFICATION_TTL_MS;
|
|
39
40
|
db.prepare(`INSERT INTO browser_task_clarifications
|
|
40
41
|
(id, task_id, question, context_summary, screenshot_key,
|
|
41
|
-
asked_at, deadline_at, answer, answered_at, resolved)
|
|
42
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, NULL, NULL, 0)`).run(input.id, input.taskId, input.question, input.contextSummary, input.screenshotKey, input.askedAt, deadline);
|
|
42
|
+
asked_at, deadline_at, delivered_at, answer, answered_at, resolved)
|
|
43
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, NULL, NULL, NULL, 0)`).run(input.id, input.taskId, input.question, input.contextSummary, input.screenshotKey, input.askedAt, deadline);
|
|
43
44
|
const row = getClarification(db, input.id);
|
|
44
45
|
if (!row) {
|
|
45
46
|
throw new Error(`createClarification: post-insert row for ${input.id} missing`);
|
|
@@ -49,7 +50,7 @@ export function createClarification(db, input) {
|
|
|
49
50
|
export function getClarification(db, id) {
|
|
50
51
|
const row = db
|
|
51
52
|
.prepare(`SELECT id, task_id, question, context_summary, screenshot_key,
|
|
52
|
-
asked_at, deadline_at, answer, answered_at, resolved
|
|
53
|
+
asked_at, deadline_at, delivered_at, answer, answered_at, resolved
|
|
53
54
|
FROM browser_task_clarifications
|
|
54
55
|
WHERE id = ?`)
|
|
55
56
|
.get(id);
|
|
@@ -58,7 +59,7 @@ export function getClarification(db, id) {
|
|
|
58
59
|
export function listClarificationsForTask(db, taskId) {
|
|
59
60
|
const rows = db
|
|
60
61
|
.prepare(`SELECT id, task_id, question, context_summary, screenshot_key,
|
|
61
|
-
asked_at, deadline_at, answer, answered_at, resolved
|
|
62
|
+
asked_at, deadline_at, delivered_at, answer, answered_at, resolved
|
|
62
63
|
FROM browser_task_clarifications
|
|
63
64
|
WHERE task_id = ?
|
|
64
65
|
ORDER BY asked_at ASC`)
|
|
@@ -96,7 +97,7 @@ export function resolveClarification(db, input) {
|
|
|
96
97
|
export function listOverdueClarifications(db, nowMs) {
|
|
97
98
|
const rows = db
|
|
98
99
|
.prepare(`SELECT id, task_id, question, context_summary, screenshot_key,
|
|
99
|
-
asked_at, deadline_at, answer, answered_at, resolved
|
|
100
|
+
asked_at, deadline_at, delivered_at, answer, answered_at, resolved
|
|
100
101
|
FROM browser_task_clarifications
|
|
101
102
|
WHERE resolved = 0 AND deadline_at < ?
|
|
102
103
|
ORDER BY deadline_at ASC`)
|
|
@@ -116,3 +117,32 @@ export function expireClarification(db, id, nowMs) {
|
|
|
116
117
|
return null;
|
|
117
118
|
return getClarification(db, id);
|
|
118
119
|
}
|
|
120
|
+
export function markClarificationDelivered(db, id, deliveredAt) {
|
|
121
|
+
const result = db
|
|
122
|
+
.prepare(`UPDATE browser_task_clarifications
|
|
123
|
+
SET delivered_at = COALESCE(delivered_at, ?)
|
|
124
|
+
WHERE id = ?`)
|
|
125
|
+
.run(deliveredAt, id);
|
|
126
|
+
return result.changes > 0 ? getClarification(db, id) : null;
|
|
127
|
+
}
|
|
128
|
+
export function listUndeliveredClarifications(db, nowMs, limit = 20) {
|
|
129
|
+
const rows = db
|
|
130
|
+
.prepare(`SELECT c.id, c.task_id, c.question, c.context_summary, c.screenshot_key,
|
|
131
|
+
c.asked_at, c.deadline_at, c.delivered_at,
|
|
132
|
+
c.answer, c.answered_at, c.resolved,
|
|
133
|
+
t.originating_channel, t.description
|
|
134
|
+
FROM browser_task_clarifications c
|
|
135
|
+
JOIN browser_task t ON t.id = c.task_id
|
|
136
|
+
WHERE c.resolved = 0
|
|
137
|
+
AND c.delivered_at IS NULL
|
|
138
|
+
AND c.deadline_at >= ?
|
|
139
|
+
AND t.state = 'awaiting_user'
|
|
140
|
+
ORDER BY c.asked_at ASC
|
|
141
|
+
LIMIT ?`)
|
|
142
|
+
.all(nowMs, limit);
|
|
143
|
+
return rows.map((row) => ({
|
|
144
|
+
...fromDbRow(row),
|
|
145
|
+
taskOriginatingChannel: row.originating_channel,
|
|
146
|
+
taskDescription: row.description,
|
|
147
|
+
}));
|
|
148
|
+
}
|
|
@@ -34,6 +34,7 @@ export interface BrowserTaskRow {
|
|
|
34
34
|
createdAt: number;
|
|
35
35
|
startedAt: number | null;
|
|
36
36
|
finishedAt: number | null;
|
|
37
|
+
deliveredAt: number | null;
|
|
37
38
|
}
|
|
38
39
|
export interface CreateBrowserTaskInput {
|
|
39
40
|
id: string;
|
|
@@ -79,6 +80,8 @@ export interface TerminalTransitionInput {
|
|
|
79
80
|
/** Any non-terminal state → terminal. Idempotent — re-running on an
|
|
80
81
|
* already-terminal row CAS-misses and returns null. */
|
|
81
82
|
export declare function markTerminal(db: Database.Database, input: TerminalTransitionInput): BrowserTaskRow | null;
|
|
83
|
+
export declare function markBrowserTaskDelivered(db: Database.Database, id: string, deliveredAt: number): BrowserTaskRow | null;
|
|
84
|
+
export declare function listUndeliveredBrowserTaskReports(db: Database.Database, limit?: number): readonly BrowserTaskRow[];
|
|
82
85
|
/** Increment the per-task CDP-blocked counter. Atomic. */
|
|
83
86
|
export declare function incrementBlockedRequests(db: Database.Database, id: string, by: number): void;
|
|
84
87
|
/** Increment the per-task cumulative untrusted-content counter. */
|