@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,673 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Background-task runner — BACKGROUND_TASK_RUNNER_DESIGN.md §4.1.
|
|
3
|
+
*
|
|
4
|
+
* The browser-task runner's lifecycle skeleton, generalized: it owns the
|
|
5
|
+
* per-task lifecycle from POST → terminal state, REUSING the pure slot
|
|
6
|
+
* manager (`browser-task-slots.ts`) with a synthetic per-task slot key so
|
|
7
|
+
* background tasks contend ONLY on the global concurrency cap (never on a
|
|
8
|
+
* per-key queue). The Playwright plane is gone; the worker is a generic
|
|
9
|
+
* SDK session (`background-task-driver.ts`).
|
|
10
|
+
*
|
|
11
|
+
* What this owns beyond browser-task's runner:
|
|
12
|
+
* - DELIVERY ENQUEUE — the runner's `reconcileDriverOutcome` reads the
|
|
13
|
+
* finished artifact and decides delivery in ONE place (the design's
|
|
14
|
+
* "post-transition hook"): completed + notify=true ⇒ enqueue result;
|
|
15
|
+
* parked ⇒ enqueue the open clarification; failed/timeout/no_finish ⇒
|
|
16
|
+
* FAIL-LOUD synthesize a failure artifact (notify=true) + enqueue;
|
|
17
|
+
* cancelled ⇒ no delivery (the owner cancelled). notify=false ⇒ file
|
|
18
|
+
* only. The periodic recovery sweep re-enqueues any lost delivery.
|
|
19
|
+
*
|
|
20
|
+
* I/O-shaped. Excluded from the 100% coverage gate; the pure logic lives
|
|
21
|
+
* in the reused slot manager + the budget envelope.
|
|
22
|
+
*/
|
|
23
|
+
import { appendResolvedClarificationToBrief, getBackgroundTask, markRunning, markRunningFromParked, markTerminal, resetSingleForBootRedispatch, } from "../../db/background-task-store.js";
|
|
24
|
+
import { getClarification, getOpenClarificationForTask, } from "../../db/background-task-clarifications-store.js";
|
|
25
|
+
import { createLogger } from "../../logging.js";
|
|
26
|
+
import { prepareDriverHandle, releaseDriverHandle, resumeDriver, resumeFromBootDriver, runDriver, } from "./background-task-driver.js";
|
|
27
|
+
import { createInitialSlotState, decideAcquire, decideCancel, decidePark, decideRelease, decideUnpark, } from "../browser-task/browser-task-slots.js";
|
|
28
|
+
import { noopBackgroundTaskTransitionEmitter, } from "./background-task-transition-events.js";
|
|
29
|
+
const logger = createLogger("background-task-runner");
|
|
30
|
+
/** Synthetic per-task slot key — every background task gets a unique key
|
|
31
|
+
* so the reused per-siteKey slot manager only ever contends them against
|
|
32
|
+
* the global `maxConcurrent` cap, never against each other on a shared
|
|
33
|
+
* queue. `bg:` prefix is a grep-friendly debugging affordance. */
|
|
34
|
+
function slotKeyForTask(id) {
|
|
35
|
+
return `bg:${id}`;
|
|
36
|
+
}
|
|
37
|
+
export function createBackgroundTaskSlotStateRef(maxConcurrent) {
|
|
38
|
+
return { state: createInitialSlotState(maxConcurrent) };
|
|
39
|
+
}
|
|
40
|
+
export function createBackgroundTaskRunner(deps) {
|
|
41
|
+
const now = deps.nowFn ?? (() => Date.now());
|
|
42
|
+
const emitter = deps.transitionEmitter ?? noopBackgroundTaskTransitionEmitter;
|
|
43
|
+
const resumeAcrossRestart = deps.resumeAcrossRestart ?? false;
|
|
44
|
+
const parkedHandles = new Map();
|
|
45
|
+
const liveHandles = new Map();
|
|
46
|
+
const pendingAborts = new Map();
|
|
47
|
+
function tryAcquire(taskId) {
|
|
48
|
+
const row = getBackgroundTask(deps.db, taskId);
|
|
49
|
+
if (!row)
|
|
50
|
+
return { promoted: false, blocked: 0 };
|
|
51
|
+
const entry = {
|
|
52
|
+
taskId: row.id,
|
|
53
|
+
siteKey: slotKeyForTask(row.id),
|
|
54
|
+
enqueuedAt: row.createdAt,
|
|
55
|
+
};
|
|
56
|
+
try {
|
|
57
|
+
const { state, effect } = decideAcquire(deps.slotStateRef.state, entry, now());
|
|
58
|
+
deps.slotStateRef.state = state;
|
|
59
|
+
if (effect.kind === "promoted")
|
|
60
|
+
return { promoted: true, blocked: 0 };
|
|
61
|
+
return { promoted: false, blocked: effect.globalPos };
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
logger.warn({ err, taskId }, "background-task tryAcquire: slot manager already tracks this task — treating as no-op");
|
|
65
|
+
return { promoted: false, blocked: 0, alreadyTracked: true };
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function releaseAndPromote(taskId) {
|
|
69
|
+
const result = decideRelease(deps.slotStateRef.state, taskId, now());
|
|
70
|
+
deps.slotStateRef.state = result.state;
|
|
71
|
+
void emitReleaseEffects(result.effects);
|
|
72
|
+
}
|
|
73
|
+
function parkSlot(taskId) {
|
|
74
|
+
try {
|
|
75
|
+
deps.slotStateRef.state = decidePark(deps.slotStateRef.state, taskId);
|
|
76
|
+
}
|
|
77
|
+
catch (err) {
|
|
78
|
+
logger.warn({ err, taskId }, "decidePark failed (slot telemetry)");
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function unparkSlot(taskId) {
|
|
82
|
+
try {
|
|
83
|
+
deps.slotStateRef.state = decideUnpark(deps.slotStateRef.state, taskId);
|
|
84
|
+
}
|
|
85
|
+
catch (err) {
|
|
86
|
+
logger.warn({ err, taskId }, "decideUnpark failed (slot telemetry)");
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
async function emitReleaseEffects(effects) {
|
|
90
|
+
for (const e of effects) {
|
|
91
|
+
if (e.kind !== "promoted")
|
|
92
|
+
continue;
|
|
93
|
+
const startedAt = now();
|
|
94
|
+
const runningRow = markRunning(deps.db, e.taskId, startedAt);
|
|
95
|
+
if (!runningRow) {
|
|
96
|
+
pendingAborts.delete(e.taskId);
|
|
97
|
+
releaseAndPromote(e.taskId);
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
emitter.emitFromRow(runningRow, startedAt);
|
|
101
|
+
void runDriverFromPending(e.taskId).catch((err) => {
|
|
102
|
+
logger.error({ err, taskId: e.taskId }, "background-task promoted-drive failed");
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/** Synthesize a fail-loud artifact for a worker that died before
|
|
107
|
+
* `finish`. The owner asked for this task; silence on a requested task
|
|
108
|
+
* is the worse failure (§4.3). notify=true regardless of policy. */
|
|
109
|
+
function failLoudArtifact(row, outcomeDetail) {
|
|
110
|
+
const title = row.title ?? row.brief.slice(0, 80);
|
|
111
|
+
return {
|
|
112
|
+
draft: `That task ("${title}") couldn't finish: ${outcomeDetail}.`,
|
|
113
|
+
report: `Background task ${row.id} ("${title}") ended without a result.\n`
|
|
114
|
+
+ `Outcome: ${outcomeDetail}.\n`
|
|
115
|
+
+ `The worker did not call finish(), so no verbatim result was captured.`,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
async function reconcileDriverOutcome(input) {
|
|
119
|
+
const { taskId, handle, result } = input;
|
|
120
|
+
// PARK — keep the handle alive for /clarify. The ask_user tool
|
|
121
|
+
// already moved the row to `awaiting_user`; enqueue the clarification
|
|
122
|
+
// delivery so the owner sees the question.
|
|
123
|
+
if (result.outcome === "yielded_for_clarification") {
|
|
124
|
+
parkedHandles.set(taskId, handle);
|
|
125
|
+
liveHandles.delete(taskId);
|
|
126
|
+
parkSlot(taskId);
|
|
127
|
+
await enqueueClarificationDelivery(taskId);
|
|
128
|
+
logger.info({ taskId }, "background-task parked — awaiting clarification");
|
|
129
|
+
return { ok: true, reason: "parked_awaiting_user", state: "awaiting_user" };
|
|
130
|
+
}
|
|
131
|
+
// COMPLETED — the finish tool wrote the artifact + terminal. Read it
|
|
132
|
+
// and enqueue the result delivery iff the worker set notify=true.
|
|
133
|
+
if (result.outcome === "completed") {
|
|
134
|
+
cleanupHandle(taskId);
|
|
135
|
+
if (deps.driver) {
|
|
136
|
+
await releaseDriverHandle(deps.driver, handle).catch((err) => {
|
|
137
|
+
logger.warn({ err, taskId }, "release driver handle failed (continuing)");
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
releaseAndPromote(taskId);
|
|
141
|
+
const row = getBackgroundTask(deps.db, taskId);
|
|
142
|
+
emitter.emitFromRow(row, now());
|
|
143
|
+
if (row && row.notify === true) {
|
|
144
|
+
await enqueueResultDelivery(row);
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
logger.info({ taskId, notify: row?.notify }, "background-task completed with notify=false — filed, no push (§10.5)");
|
|
148
|
+
}
|
|
149
|
+
return { ok: true, reason: "completed", state: row?.state ?? "completed" };
|
|
150
|
+
}
|
|
151
|
+
// CANCELLED — the owner cancelled; no fail-loud delivery.
|
|
152
|
+
if (result.outcome === "cancelled") {
|
|
153
|
+
const finishedAt = now();
|
|
154
|
+
const terminal = markTerminal(deps.db, {
|
|
155
|
+
id: taskId,
|
|
156
|
+
state: "cancelled",
|
|
157
|
+
outcomeDetail: result.detail ?? "cancelled",
|
|
158
|
+
finishedAt,
|
|
159
|
+
});
|
|
160
|
+
emitter.emitFromRow(terminal, finishedAt);
|
|
161
|
+
cleanupHandle(taskId);
|
|
162
|
+
if (deps.driver) {
|
|
163
|
+
await releaseDriverHandle(deps.driver, handle).catch(() => { });
|
|
164
|
+
}
|
|
165
|
+
releaseAndPromote(taskId);
|
|
166
|
+
return { ok: false, reason: "cancelled", state: terminal?.state ?? "cancelled" };
|
|
167
|
+
}
|
|
168
|
+
// FAIL-LOUD terminals — worker died/timed out/exceeded budget without
|
|
169
|
+
// finish(). Synthesize the artifact (notify=true) so the owner always
|
|
170
|
+
// hears back on a requested task, then enqueue delivery.
|
|
171
|
+
const isTimeout = result.outcome === "timeout";
|
|
172
|
+
const terminalState = isTimeout ? "timeout" : "failed";
|
|
173
|
+
const outcomeDetail = result.detail ?? result.outcome;
|
|
174
|
+
const finishedAt = now();
|
|
175
|
+
const rowBefore = getBackgroundTask(deps.db, taskId);
|
|
176
|
+
const synthesized = rowBefore
|
|
177
|
+
? failLoudArtifact(rowBefore, outcomeDetail)
|
|
178
|
+
: { report: null, draft: null };
|
|
179
|
+
const terminal = markTerminal(deps.db, {
|
|
180
|
+
id: taskId,
|
|
181
|
+
state: terminalState,
|
|
182
|
+
outcomeDetail,
|
|
183
|
+
finishedAt,
|
|
184
|
+
report: synthesized.report,
|
|
185
|
+
draft: synthesized.draft,
|
|
186
|
+
notify: true,
|
|
187
|
+
significance: `task failed (${outcomeDetail})`,
|
|
188
|
+
});
|
|
189
|
+
emitter.emitFromRow(terminal, finishedAt);
|
|
190
|
+
cleanupHandle(taskId);
|
|
191
|
+
if (deps.driver) {
|
|
192
|
+
await releaseDriverHandle(deps.driver, handle).catch((err) => {
|
|
193
|
+
logger.warn({ err, taskId }, "release driver handle failed (continuing)");
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
releaseAndPromote(taskId);
|
|
197
|
+
if (terminal)
|
|
198
|
+
await enqueueResultDelivery(terminal);
|
|
199
|
+
return { ok: false, reason: terminalState, state: terminal?.state ?? terminalState };
|
|
200
|
+
}
|
|
201
|
+
async function enqueueResultDelivery(row) {
|
|
202
|
+
if (!deps.deliveryEnqueuer)
|
|
203
|
+
return;
|
|
204
|
+
if (!row.draft)
|
|
205
|
+
return;
|
|
206
|
+
try {
|
|
207
|
+
await deps.deliveryEnqueuer.enqueueResult({
|
|
208
|
+
taskId: row.id,
|
|
209
|
+
originatingChannel: row.originatingChannel,
|
|
210
|
+
title: row.title ?? row.brief.slice(0, 80),
|
|
211
|
+
draft: row.draft,
|
|
212
|
+
report: row.report ?? row.draft,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
catch (err) {
|
|
216
|
+
// Best-effort — the recovery sweep re-enqueues (notify=1 &
|
|
217
|
+
// delivered_at IS NULL) so a lost enqueue is not a lost result.
|
|
218
|
+
logger.warn({ err, taskId: row.id }, "background-task result delivery enqueue failed (recovery sweep will retry)");
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
async function enqueueClarificationDelivery(taskId) {
|
|
222
|
+
if (!deps.deliveryEnqueuer)
|
|
223
|
+
return;
|
|
224
|
+
const clar = getOpenClarificationForTask(deps.db, taskId);
|
|
225
|
+
if (!clar)
|
|
226
|
+
return;
|
|
227
|
+
const row = getBackgroundTask(deps.db, taskId);
|
|
228
|
+
try {
|
|
229
|
+
await deps.deliveryEnqueuer.enqueueClarification({
|
|
230
|
+
taskId,
|
|
231
|
+
originatingChannel: row?.originatingChannel ?? null,
|
|
232
|
+
title: row?.title ?? row?.brief.slice(0, 80) ?? `Task ${taskId}`,
|
|
233
|
+
clarificationId: clar.id,
|
|
234
|
+
question: clar.question,
|
|
235
|
+
contextSummary: clar.contextSummary,
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
catch (err) {
|
|
239
|
+
logger.warn({ err, taskId }, "background-task clarification delivery enqueue failed (recovery sweep will retry)");
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
function cleanupHandle(taskId) {
|
|
243
|
+
parkedHandles.delete(taskId);
|
|
244
|
+
liveHandles.delete(taskId);
|
|
245
|
+
pendingAborts.delete(taskId);
|
|
246
|
+
}
|
|
247
|
+
async function runDriverFromPending(taskId) {
|
|
248
|
+
if (!deps.driver) {
|
|
249
|
+
const finishedAt = now();
|
|
250
|
+
const terminal = markTerminal(deps.db, {
|
|
251
|
+
id: taskId,
|
|
252
|
+
state: "failed",
|
|
253
|
+
outcomeDetail: "runner_unavailable",
|
|
254
|
+
finishedAt,
|
|
255
|
+
report: "The background-task runner has no worker driver wired.",
|
|
256
|
+
draft: "That task couldn't start — the worker runtime is unavailable.",
|
|
257
|
+
notify: true,
|
|
258
|
+
});
|
|
259
|
+
emitter.emitFromRow(terminal, finishedAt);
|
|
260
|
+
pendingAborts.delete(taskId);
|
|
261
|
+
releaseAndPromote(taskId);
|
|
262
|
+
if (terminal)
|
|
263
|
+
await enqueueResultDelivery(terminal);
|
|
264
|
+
return { ok: false, reason: "no_driver", state: terminal?.state ?? "failed" };
|
|
265
|
+
}
|
|
266
|
+
const row = getBackgroundTask(deps.db, taskId);
|
|
267
|
+
if (!row) {
|
|
268
|
+
pendingAborts.delete(taskId);
|
|
269
|
+
releaseAndPromote(taskId);
|
|
270
|
+
return { ok: false, reason: "task_missing", state: null };
|
|
271
|
+
}
|
|
272
|
+
const prepared = await prepareDriverHandle({ deps: deps.driver, row });
|
|
273
|
+
if (!prepared.ok) {
|
|
274
|
+
const finishedAt = now();
|
|
275
|
+
const synthesized = failLoudArtifact(row, prepared.detail ?? prepared.reason);
|
|
276
|
+
const terminal = markTerminal(deps.db, {
|
|
277
|
+
id: taskId,
|
|
278
|
+
state: "failed",
|
|
279
|
+
outcomeDetail: prepared.detail ?? prepared.reason,
|
|
280
|
+
finishedAt,
|
|
281
|
+
report: synthesized.report,
|
|
282
|
+
draft: synthesized.draft,
|
|
283
|
+
notify: true,
|
|
284
|
+
});
|
|
285
|
+
emitter.emitFromRow(terminal, finishedAt);
|
|
286
|
+
pendingAborts.delete(taskId);
|
|
287
|
+
releaseAndPromote(taskId);
|
|
288
|
+
if (terminal)
|
|
289
|
+
await enqueueResultDelivery(terminal);
|
|
290
|
+
return { ok: false, reason: "failed", state: terminal?.state ?? "failed" };
|
|
291
|
+
}
|
|
292
|
+
const handle = prepared.handle;
|
|
293
|
+
liveHandles.set(taskId, handle);
|
|
294
|
+
const pendingAbort = pendingAborts.get(taskId);
|
|
295
|
+
if (pendingAbort !== undefined) {
|
|
296
|
+
pendingAborts.delete(taskId);
|
|
297
|
+
try {
|
|
298
|
+
handle.abortController.abort(new Error(pendingAbort));
|
|
299
|
+
}
|
|
300
|
+
catch (err) {
|
|
301
|
+
/* c8 ignore next 2 -- defensive */
|
|
302
|
+
logger.warn({ err, taskId, reason: pendingAbort }, "forwarding pending-cancel abort failed");
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
let result;
|
|
306
|
+
try {
|
|
307
|
+
result = await runDriver(deps.driver, row, handle);
|
|
308
|
+
}
|
|
309
|
+
catch (err) {
|
|
310
|
+
logger.error({ err, taskId }, "background-task driver threw");
|
|
311
|
+
result = {
|
|
312
|
+
outcome: "sdk_error",
|
|
313
|
+
sdkSessionId: handle.sdkSessionId,
|
|
314
|
+
detail: err instanceof Error ? err.message : String(err),
|
|
315
|
+
costUsd: 0,
|
|
316
|
+
numTurns: 0,
|
|
317
|
+
durationMs: 0,
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
return reconcileDriverOutcome({ taskId, handle, result });
|
|
321
|
+
}
|
|
322
|
+
async function runOnce(taskId) {
|
|
323
|
+
const row = getBackgroundTask(deps.db, taskId);
|
|
324
|
+
if (!row)
|
|
325
|
+
return { ok: false, reason: "task_missing", state: null };
|
|
326
|
+
if (row.state !== "pending") {
|
|
327
|
+
return { ok: false, reason: "already_terminal", state: row.state };
|
|
328
|
+
}
|
|
329
|
+
const { promoted, blocked, alreadyTracked } = tryAcquire(taskId);
|
|
330
|
+
if (alreadyTracked) {
|
|
331
|
+
return { ok: false, reason: "already_terminal", state: row.state };
|
|
332
|
+
}
|
|
333
|
+
if (!promoted) {
|
|
334
|
+
logger.info({ taskId, blocked }, "background-task queued — waiting for slot");
|
|
335
|
+
return { ok: true, reason: "queued", state: "pending" };
|
|
336
|
+
}
|
|
337
|
+
const startedAt = now();
|
|
338
|
+
const runningRow = markRunning(deps.db, taskId, startedAt);
|
|
339
|
+
if (!runningRow) {
|
|
340
|
+
pendingAborts.delete(taskId);
|
|
341
|
+
releaseAndPromote(taskId);
|
|
342
|
+
const afterRow = getBackgroundTask(deps.db, taskId);
|
|
343
|
+
return { ok: false, reason: "already_terminal", state: afterRow?.state ?? null };
|
|
344
|
+
}
|
|
345
|
+
emitter.emitFromRow(runningRow, startedAt);
|
|
346
|
+
return runDriverFromPending(taskId);
|
|
347
|
+
}
|
|
348
|
+
async function runFromPost(taskId) {
|
|
349
|
+
return runOnce(taskId);
|
|
350
|
+
}
|
|
351
|
+
async function runFromScheduleRow(taskId) {
|
|
352
|
+
return runOnce(taskId);
|
|
353
|
+
}
|
|
354
|
+
async function cancel(taskId, reason) {
|
|
355
|
+
const row = getBackgroundTask(deps.db, taskId);
|
|
356
|
+
if (!row)
|
|
357
|
+
return false;
|
|
358
|
+
const live = liveHandles.get(taskId);
|
|
359
|
+
const parked = parkedHandles.get(taskId);
|
|
360
|
+
const handle = live ?? parked;
|
|
361
|
+
if (handle) {
|
|
362
|
+
try {
|
|
363
|
+
handle.abortController.abort(new Error(reason || "cancel"));
|
|
364
|
+
}
|
|
365
|
+
catch (err) {
|
|
366
|
+
/* c8 ignore next 2 -- defensive */
|
|
367
|
+
logger.warn({ err, taskId }, "abort signal failed");
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
else if (row.state === "running") {
|
|
371
|
+
pendingAborts.set(taskId, reason || "cancel");
|
|
372
|
+
}
|
|
373
|
+
else if (row.state === "pending") {
|
|
374
|
+
// Queued behind the concurrency cap, not yet running — remove it
|
|
375
|
+
// from the slot FIFO and write the terminal directly. Without this
|
|
376
|
+
// the row stays `pending` forever and the FIFO entry leaks a slot
|
|
377
|
+
// reservation. `decideCancel` throws only if the task is the active
|
|
378
|
+
// occupant — by construction a `pending` DB row never is (markRunning
|
|
379
|
+
// runs synchronously after acquire), so the catch is defensive.
|
|
380
|
+
try {
|
|
381
|
+
deps.slotStateRef.state = decideCancel(deps.slotStateRef.state, taskId).state;
|
|
382
|
+
}
|
|
383
|
+
catch (err) {
|
|
384
|
+
logger.warn({ err, taskId }, "decideCancel on pending row failed (continuing)");
|
|
385
|
+
}
|
|
386
|
+
const finishedAt = now();
|
|
387
|
+
const terminal = markTerminal(deps.db, {
|
|
388
|
+
id: taskId,
|
|
389
|
+
state: "cancelled",
|
|
390
|
+
outcomeDetail: `cancelled_in_queue:${reason}`,
|
|
391
|
+
finishedAt,
|
|
392
|
+
});
|
|
393
|
+
emitter.emitFromRow(terminal, finishedAt);
|
|
394
|
+
logger.info({ taskId, reason }, "background-task cancel (pending → cancelled)");
|
|
395
|
+
return true;
|
|
396
|
+
}
|
|
397
|
+
// Parked tasks aren't iterating the SDK, so abort alone won't unwind —
|
|
398
|
+
// walk the terminal path manually (no fail-loud delivery on cancel).
|
|
399
|
+
if (parked && !live) {
|
|
400
|
+
parkedHandles.delete(taskId);
|
|
401
|
+
if (deps.driver) {
|
|
402
|
+
await releaseDriverHandle(deps.driver, parked).catch(() => { });
|
|
403
|
+
}
|
|
404
|
+
const finishedAt = now();
|
|
405
|
+
const terminal = markTerminal(deps.db, {
|
|
406
|
+
id: taskId,
|
|
407
|
+
state: "cancelled",
|
|
408
|
+
outcomeDetail: reason,
|
|
409
|
+
finishedAt,
|
|
410
|
+
});
|
|
411
|
+
emitter.emitFromRow(terminal, finishedAt);
|
|
412
|
+
pendingAborts.delete(taskId);
|
|
413
|
+
releaseAndPromote(taskId);
|
|
414
|
+
}
|
|
415
|
+
logger.info({ taskId, reason, currentState: row.state, hadLive: !!live, hadParked: !!parked }, "background-task cancel");
|
|
416
|
+
return true;
|
|
417
|
+
}
|
|
418
|
+
async function resumeAfterClarification(input) {
|
|
419
|
+
const row = getBackgroundTask(deps.db, input.taskId);
|
|
420
|
+
const parked = parkedHandles.get(input.taskId);
|
|
421
|
+
// The task vanished (retention prune / manual delete) — drop any stray
|
|
422
|
+
// handle and free its slot.
|
|
423
|
+
if (!row) {
|
|
424
|
+
if (parked) {
|
|
425
|
+
parkedHandles.delete(input.taskId);
|
|
426
|
+
if (deps.driver)
|
|
427
|
+
await releaseDriverHandle(deps.driver, parked).catch(() => { });
|
|
428
|
+
}
|
|
429
|
+
releaseAndPromote(input.taskId);
|
|
430
|
+
return { ok: false, reason: "task_missing", state: null };
|
|
431
|
+
}
|
|
432
|
+
// (1) Warm in-memory handle (no restart since the park) — resume the
|
|
433
|
+
// live SDK session. The slot was held across the park, so unpark it.
|
|
434
|
+
if (parked && deps.driver) {
|
|
435
|
+
parkedHandles.delete(input.taskId);
|
|
436
|
+
liveHandles.set(input.taskId, parked);
|
|
437
|
+
unparkSlot(input.taskId);
|
|
438
|
+
return driveResumeOrFallback(input, row, parked, deps.driver);
|
|
439
|
+
}
|
|
440
|
+
// A parked handle with no driver to drive it is pathological — drop it
|
|
441
|
+
// and fall through to the re-dispatch floor.
|
|
442
|
+
if (parked)
|
|
443
|
+
parkedHandles.delete(input.taskId);
|
|
444
|
+
// (2) Cross-restart: the in-memory handle was lost with the prior
|
|
445
|
+
// process. Reconstruct + resume the warm SDK session (§10.2) ONLY
|
|
446
|
+
// when resume is enabled, a session id was persisted, AND a slot is
|
|
447
|
+
// immediately free. Every other case — and any resume that can't
|
|
448
|
+
// load the session (§driveResumeOrFallback) — degrades to the
|
|
449
|
+
// zero-regression floor: re-dispatch from the brief with the owner's
|
|
450
|
+
// answer folded in (so the cold re-run doesn't re-ask). This mirrors
|
|
451
|
+
// `resumeFromBoot`; a clarify-after-restart therefore never
|
|
452
|
+
// fail-louds a recoverable task, over-commits a slot, or loses the
|
|
453
|
+
// owner's already-consumed answer.
|
|
454
|
+
if (resumeAcrossRestart && deps.driver && row.backendSessionId) {
|
|
455
|
+
const { promoted } = tryAcquire(input.taskId);
|
|
456
|
+
if (promoted) {
|
|
457
|
+
const prepared = await prepareDriverHandle({ deps: deps.driver, row });
|
|
458
|
+
if (prepared.ok) {
|
|
459
|
+
liveHandles.set(input.taskId, prepared.handle);
|
|
460
|
+
logger.info({ taskId: input.taskId }, "background-task clarify-after-restart — reconstructed handle from persisted session id");
|
|
461
|
+
return driveResumeOrFallback(input, row, prepared.handle, deps.driver);
|
|
462
|
+
}
|
|
463
|
+
// Reconstruction failed — release the slot we just took, then
|
|
464
|
+
// re-dispatch.
|
|
465
|
+
releaseAndPromote(input.taskId);
|
|
466
|
+
logger.warn({ taskId: input.taskId, reason: prepared.reason }, "background-task clarify-after-restart — handle reconstruction failed; re-dispatching from brief");
|
|
467
|
+
}
|
|
468
|
+
else {
|
|
469
|
+
// Concurrency cap full — tryAcquire queued the task; re-dispatch
|
|
470
|
+
// resets the row to pending so that queued FIFO entry drives it
|
|
471
|
+
// fresh (the same pattern resumeFromBoot uses).
|
|
472
|
+
logger.info({ taskId: input.taskId }, "background-task clarify-after-restart — concurrency cap full; re-dispatching from brief");
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
return redispatchAfterClarification(input);
|
|
476
|
+
}
|
|
477
|
+
/** Resume a parked/reconstructed worker with the owner's answer; if the
|
|
478
|
+
* warm SDK session can't load (`resume_unavailable`), degrade to a cold
|
|
479
|
+
* re-dispatch-with-answer rather than fail-louding (§10.2). */
|
|
480
|
+
async function driveResumeOrFallback(input, row, handle, driver) {
|
|
481
|
+
const resumedAt = now();
|
|
482
|
+
const resumed = markRunningFromParked(deps.db, input.taskId);
|
|
483
|
+
if (resumed)
|
|
484
|
+
emitter.emitFromRow(resumed, resumedAt);
|
|
485
|
+
let result;
|
|
486
|
+
try {
|
|
487
|
+
result = await resumeDriver(driver, row, handle, input.answer);
|
|
488
|
+
}
|
|
489
|
+
catch (err) {
|
|
490
|
+
logger.error({ err, taskId: input.taskId }, "background-task resume threw");
|
|
491
|
+
result = {
|
|
492
|
+
outcome: "sdk_error",
|
|
493
|
+
sdkSessionId: handle.sdkSessionId,
|
|
494
|
+
detail: err instanceof Error ? err.message : String(err),
|
|
495
|
+
costUsd: 0,
|
|
496
|
+
numTurns: 0,
|
|
497
|
+
durationMs: 0,
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
if (result.outcome === "resume_unavailable") {
|
|
501
|
+
logger.warn({ taskId: input.taskId, detail: result.detail }, "background-task clarify resume unavailable — re-dispatching from brief with the answer");
|
|
502
|
+
cleanupHandle(input.taskId);
|
|
503
|
+
await releaseDriverHandle(driver, handle).catch(() => { });
|
|
504
|
+
releaseAndPromote(input.taskId);
|
|
505
|
+
return redispatchAfterClarification(input);
|
|
506
|
+
}
|
|
507
|
+
return reconcileDriverOutcome({ taskId: input.taskId, handle, result });
|
|
508
|
+
}
|
|
509
|
+
/** Zero-regression fallback for a clarify that can't reuse the warm SDK
|
|
510
|
+
* session: fold the owner's just-answered clarification into the brief
|
|
511
|
+
* (the route already CAS-resolved it) and re-dispatch from the brief, so
|
|
512
|
+
* the cold re-run has the answer and doesn't re-ask. */
|
|
513
|
+
async function redispatchAfterClarification(input) {
|
|
514
|
+
const clar = getClarification(deps.db, input.clarificationId);
|
|
515
|
+
appendResolvedClarificationToBrief(deps.db, input.taskId, clar?.question ?? null, input.answer);
|
|
516
|
+
return redispatchFromBrief(input.taskId);
|
|
517
|
+
}
|
|
518
|
+
/**
|
|
519
|
+
* Phase 4 (§10.2) — boot recovery for ONE non-terminal task. A `running`
|
|
520
|
+
* task that captured an SDK session id is resumed via `query({resume})`
|
|
521
|
+
* (warm transcript + prompt cache survive the restart); everything else,
|
|
522
|
+
* and any resume that can't load the session, re-dispatches from the
|
|
523
|
+
* self-contained brief. Resume is a pure optimization — every failure
|
|
524
|
+
* path degrades to the proven v1 re-dispatch behaviour.
|
|
525
|
+
*/
|
|
526
|
+
async function resumeFromBoot(taskId) {
|
|
527
|
+
const row = getBackgroundTask(deps.db, taskId);
|
|
528
|
+
if (!row)
|
|
529
|
+
return { ok: false, reason: "task_missing", state: null };
|
|
530
|
+
if (!resumeAcrossRestart
|
|
531
|
+
|| !deps.driver
|
|
532
|
+
|| !row.backendSessionId
|
|
533
|
+
|| row.state !== "running") {
|
|
534
|
+
return redispatchFromBrief(taskId);
|
|
535
|
+
}
|
|
536
|
+
const { promoted, alreadyTracked } = tryAcquire(taskId);
|
|
537
|
+
if (alreadyTracked || !promoted) {
|
|
538
|
+
// Concurrency cap is full right now — re-dispatch (it queues, and the
|
|
539
|
+
// normal promotion path runs it fresh). Resume only when a slot is
|
|
540
|
+
// immediately free.
|
|
541
|
+
return redispatchFromBrief(taskId);
|
|
542
|
+
}
|
|
543
|
+
emitter.emitFromRow(row, now());
|
|
544
|
+
const prepared = await prepareDriverHandle({ deps: deps.driver, row });
|
|
545
|
+
if (!prepared.ok) {
|
|
546
|
+
const finishedAt = now();
|
|
547
|
+
const synthesized = failLoudArtifact(row, prepared.detail ?? prepared.reason);
|
|
548
|
+
const terminal = markTerminal(deps.db, {
|
|
549
|
+
id: taskId,
|
|
550
|
+
state: "failed",
|
|
551
|
+
outcomeDetail: prepared.detail ?? prepared.reason,
|
|
552
|
+
finishedAt,
|
|
553
|
+
report: synthesized.report,
|
|
554
|
+
draft: synthesized.draft,
|
|
555
|
+
notify: true,
|
|
556
|
+
});
|
|
557
|
+
emitter.emitFromRow(terminal, finishedAt);
|
|
558
|
+
releaseAndPromote(taskId);
|
|
559
|
+
if (terminal)
|
|
560
|
+
await enqueueResultDelivery(terminal);
|
|
561
|
+
return { ok: false, reason: "failed", state: terminal?.state ?? "failed" };
|
|
562
|
+
}
|
|
563
|
+
const handle = prepared.handle;
|
|
564
|
+
liveHandles.set(taskId, handle);
|
|
565
|
+
// A cancel that arrived during boot recovery (before this handle
|
|
566
|
+
// existed) parked its reason in `pendingAborts` — forward it now so the
|
|
567
|
+
// resume unwinds instead of dropping the cancel.
|
|
568
|
+
const pendingAbort = pendingAborts.get(taskId);
|
|
569
|
+
if (pendingAbort !== undefined) {
|
|
570
|
+
pendingAborts.delete(taskId);
|
|
571
|
+
try {
|
|
572
|
+
handle.abortController.abort(new Error(pendingAbort));
|
|
573
|
+
}
|
|
574
|
+
catch (err) {
|
|
575
|
+
/* c8 ignore next 2 -- defensive */
|
|
576
|
+
logger.warn({ err, taskId, reason: pendingAbort }, "forwarding pending-cancel abort failed (boot resume)");
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
let result;
|
|
580
|
+
try {
|
|
581
|
+
result = await resumeFromBootDriver(deps.driver, row, handle);
|
|
582
|
+
}
|
|
583
|
+
catch (err) {
|
|
584
|
+
result = {
|
|
585
|
+
outcome: "resume_unavailable",
|
|
586
|
+
sdkSessionId: handle.sdkSessionId,
|
|
587
|
+
detail: err instanceof Error ? err.message : String(err),
|
|
588
|
+
costUsd: 0,
|
|
589
|
+
numTurns: 0,
|
|
590
|
+
durationMs: 0,
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
if (result.outcome === "resume_unavailable") {
|
|
594
|
+
logger.warn({ taskId, detail: result.detail }, "background-task resume-across-restart unavailable — re-dispatching from brief");
|
|
595
|
+
cleanupHandle(taskId);
|
|
596
|
+
await releaseDriverHandle(deps.driver, handle).catch(() => { });
|
|
597
|
+
releaseAndPromote(taskId);
|
|
598
|
+
return redispatchFromBrief(taskId);
|
|
599
|
+
}
|
|
600
|
+
return reconcileDriverOutcome({ taskId, handle, result });
|
|
601
|
+
}
|
|
602
|
+
/** Reset a single non-terminal row to pending (clearing its lost session)
|
|
603
|
+
* and re-run its brief through the normal pending→running→drive path. */
|
|
604
|
+
async function redispatchFromBrief(taskId) {
|
|
605
|
+
resetSingleForBootRedispatch(deps.db, taskId, now());
|
|
606
|
+
return runOnce(taskId);
|
|
607
|
+
}
|
|
608
|
+
async function expireForDeadline(taskId, kind, waitedMs) {
|
|
609
|
+
const row = getBackgroundTask(deps.db, taskId);
|
|
610
|
+
if (!row)
|
|
611
|
+
return { ok: false, reason: "task_missing", state: null };
|
|
612
|
+
if (row.state === "completed"
|
|
613
|
+
|| row.state === "failed"
|
|
614
|
+
|| row.state === "timeout"
|
|
615
|
+
|| row.state === "cancelled") {
|
|
616
|
+
return { ok: false, reason: "already_terminal", state: row.state };
|
|
617
|
+
}
|
|
618
|
+
const outcomeDetail = kind === "clarification_deadline" ? "clarification_deadline" : "queue_timeout";
|
|
619
|
+
const parked = parkedHandles.get(taskId);
|
|
620
|
+
const live = liveHandles.get(taskId);
|
|
621
|
+
const handle = parked ?? live;
|
|
622
|
+
if (handle) {
|
|
623
|
+
try {
|
|
624
|
+
handle.abortController.abort(new Error(outcomeDetail));
|
|
625
|
+
}
|
|
626
|
+
catch (err) {
|
|
627
|
+
/* c8 ignore next 2 -- defensive */
|
|
628
|
+
logger.warn({ err, taskId, kind }, "expireForDeadline: abort failed");
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
if (parked) {
|
|
632
|
+
parkedHandles.delete(taskId);
|
|
633
|
+
if (deps.driver)
|
|
634
|
+
await releaseDriverHandle(deps.driver, parked).catch(() => { });
|
|
635
|
+
}
|
|
636
|
+
const finishedAt = now();
|
|
637
|
+
const synthesized = failLoudArtifact(row, outcomeDetail);
|
|
638
|
+
const terminal = markTerminal(deps.db, {
|
|
639
|
+
id: taskId,
|
|
640
|
+
state: "timeout",
|
|
641
|
+
outcomeDetail,
|
|
642
|
+
finishedAt,
|
|
643
|
+
report: synthesized.report,
|
|
644
|
+
draft: synthesized.draft,
|
|
645
|
+
notify: true,
|
|
646
|
+
significance: `task timed out (${outcomeDetail})`,
|
|
647
|
+
});
|
|
648
|
+
emitter.emitFromRow(terminal, finishedAt);
|
|
649
|
+
pendingAborts.delete(taskId);
|
|
650
|
+
// For a live-only handle (clarification deadline racing a still-live
|
|
651
|
+
// run), the abort unwinds the SDK into reconcileDriverOutcome which
|
|
652
|
+
// releases + promotes; releasing here too would double-promote.
|
|
653
|
+
if (parked || !live) {
|
|
654
|
+
releaseAndPromote(taskId);
|
|
655
|
+
}
|
|
656
|
+
if (terminal)
|
|
657
|
+
await enqueueResultDelivery(terminal);
|
|
658
|
+
logger.info({ taskId, kind, hadParked: !!parked, hadLive: !!live, waitedMs }, "background-task expired for deadline");
|
|
659
|
+
return { ok: false, reason: "timeout", state: terminal?.state ?? "timeout" };
|
|
660
|
+
}
|
|
661
|
+
function __peekParkedIds() {
|
|
662
|
+
return Array.from(parkedHandles.keys());
|
|
663
|
+
}
|
|
664
|
+
return {
|
|
665
|
+
runFromPost,
|
|
666
|
+
runFromScheduleRow,
|
|
667
|
+
cancel,
|
|
668
|
+
resumeAfterClarification,
|
|
669
|
+
resumeFromBoot,
|
|
670
|
+
expireForDeadline,
|
|
671
|
+
__peekParkedIds,
|
|
672
|
+
};
|
|
673
|
+
}
|