@aitne/daemon 0.1.9 → 0.1.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/adapter-watchdog.d.ts +70 -0
- package/dist/adapters/adapter-watchdog.js +115 -0
- package/dist/adapters/discord.d.ts +17 -1
- package/dist/adapters/discord.js +33 -0
- package/dist/adapters/notification-manager.d.ts +27 -1
- package/dist/adapters/notification-manager.js +54 -39
- package/dist/adapters/slack-adapter.d.ts +26 -1
- package/dist/adapters/slack-adapter.js +41 -0
- package/dist/adapters/telegram-adapter.d.ts +18 -1
- package/dist/adapters/telegram-adapter.js +41 -2
- package/dist/adapters/types.d.ts +20 -0
- package/dist/adapters/whatsapp-adapter.d.ts +26 -7
- package/dist/adapters/whatsapp-adapter.js +74 -21
- package/dist/api/env-writer.d.ts +1 -0
- package/dist/api/env-writer.js +17 -7
- package/dist/api/helpers/agent-errors-registry.d.ts +5 -5
- package/dist/api/helpers/agent-errors-registry.js +5 -5
- package/dist/api/routes/agent-schedule.js +5 -1
- package/dist/api/routes/agent.js +33 -12
- package/dist/api/routes/agents/index.js +75 -16
- package/dist/api/routes/agents/views.d.ts +37 -2
- package/dist/api/routes/agents/views.js +64 -2
- package/dist/api/routes/apple-calendar.js +4 -1
- package/dist/api/routes/background-task.d.ts +22 -0
- package/dist/api/routes/background-task.js +338 -0
- package/dist/api/routes/browser-history.js +9 -1
- package/dist/api/routes/calendar.js +12 -2
- package/dist/api/routes/context/path-resolve.js +6 -1
- package/dist/api/routes/context/permissions.js +12 -2
- package/dist/api/routes/context/snapshots.js +0 -3
- package/dist/api/routes/context/write.js +3 -17
- package/dist/api/routes/dashboard/config.js +58 -12
- package/dist/api/routes/dashboard/cost-approvals.js +66 -0
- package/dist/api/routes/dashboard/notifications.js +9 -9
- package/dist/api/routes/dashboard/oauth-google.js +5 -3
- package/dist/api/routes/feedback.d.ts +3 -0
- package/dist/api/routes/feedback.js +349 -0
- package/dist/api/routes/git.js +10 -3
- package/dist/api/routes/github.js +5 -1
- package/dist/api/routes/integrations/crud-patch.js +5 -1
- package/dist/api/routes/integrations-reconcile.js +2 -2
- package/dist/api/routes/mcp.js +65 -13
- package/dist/api/routes/notion.d.ts +1 -1
- package/dist/api/routes/observations.js +7 -7
- package/dist/api/routes/obsidian.d.ts +1 -1
- package/dist/api/routes/receipts.js +5 -1
- package/dist/api/routes/setup-migrate.js +1 -1
- package/dist/api/routes/setup.js +1 -1
- package/dist/api/routes/task-flows.d.ts +1 -1
- package/dist/api/routes/task-flows.js +1 -1
- package/dist/api/routes/tuning.d.ts +29 -0
- package/dist/api/routes/tuning.js +304 -0
- package/dist/api/server.d.ts +44 -16
- package/dist/api/server.js +12 -0
- package/dist/bootstrap/adapters.d.ts +19 -0
- package/dist/bootstrap/adapters.js +61 -0
- package/dist/bootstrap/api.d.ts +5 -3
- package/dist/bootstrap/api.js +45 -13
- package/dist/bootstrap/catchup.d.ts +1 -1
- package/dist/bootstrap/catchup.js +11 -11
- package/dist/bootstrap/event-pipeline.d.ts +11 -0
- package/dist/bootstrap/event-pipeline.js +246 -8
- package/dist/bootstrap/observers.js +9 -6
- package/dist/bootstrap/schedule-helpers.d.ts +104 -6
- package/dist/bootstrap/schedule-helpers.js +172 -19
- package/dist/config.js +32 -12
- package/dist/core/agent-core.d.ts +33 -1
- package/dist/core/agent-core.js +36 -1
- package/dist/core/agents/activity-scan-cadence.d.ts +103 -0
- package/dist/core/agents/activity-scan-cadence.js +127 -0
- package/dist/core/agents/agent-route-override.d.ts +53 -0
- package/dist/core/agents/agent-route-override.js +69 -0
- package/dist/core/agents/builtin-registry.d.ts +51 -14
- package/dist/core/agents/builtin-registry.js +92 -15
- package/dist/core/agents/config-gate-reconcile.d.ts +38 -0
- package/dist/core/agents/config-gate-reconcile.js +51 -0
- package/dist/core/agents/cron-substitute.d.ts +1 -1
- package/dist/core/agents/cron-substitute.js +1 -1
- package/dist/core/agents/custom-routine-migration.d.ts +60 -0
- package/dist/core/agents/custom-routine-migration.js +149 -0
- package/dist/core/agents/firing-blocked.d.ts +1 -1
- package/dist/core/agents/hourly-cadence.d.ts +102 -0
- package/dist/core/agents/hourly-cadence.js +126 -0
- package/dist/core/agents/loader-boot.js +23 -0
- package/dist/core/agents/loader.d.ts +19 -0
- package/dist/core/agents/loader.js +34 -2
- package/dist/core/agents/override-merge.d.ts +1 -1
- package/dist/core/agents/override-merge.js +9 -1
- package/dist/core/agents/recurrence-convert.d.ts +1 -1
- package/dist/core/agents/recurrence-convert.js +1 -1
- package/dist/core/agents/recurring-schedule-adapter.js +8 -0
- package/dist/core/alerts.js +6 -6
- package/dist/core/backends/auth-health-monitor.d.ts +2 -2
- package/dist/core/backends/auth-health-monitor.js +1 -1
- package/dist/core/backends/backend-router.d.ts +27 -1
- package/dist/core/backends/backend-router.js +165 -1
- package/dist/core/backends/claude-code-core.d.ts +71 -31
- package/dist/core/backends/claude-code-core.js +282 -54
- package/dist/core/backends/cli-quota-guards.d.ts +29 -1
- package/dist/core/backends/cli-quota-guards.js +40 -5
- package/dist/core/backends/codex-core.d.ts +6 -0
- package/dist/core/backends/codex-core.js +22 -6
- package/dist/core/backends/failure-spend.d.ts +58 -0
- package/dist/core/backends/failure-spend.js +137 -0
- package/dist/core/backends/gemini-cli-core.d.ts +6 -0
- package/dist/core/backends/gemini-cli-core.js +38 -6
- package/dist/core/backends/model-registry.d.ts +1 -1
- package/dist/core/backends/model-registry.js +4 -4
- package/dist/core/backends/opencode-core.d.ts +1 -1
- package/dist/core/backends/opencode-core.js +5 -5
- package/dist/core/backends/plan-presets.js +47 -18
- package/dist/core/bang-commands/commands-cost.js +3 -1
- package/dist/core/bang-commands/commands-report.js +4 -3
- package/dist/core/bang-commands/commands-research.js +4 -1
- package/dist/core/bang-commands/commands-revert-tuning.d.ts +18 -0
- package/dist/core/bang-commands/commands-revert-tuning.js +63 -0
- package/dist/core/bang-commands/commands-stop-start.js +3 -3
- package/dist/core/bang-commands/commands-task-control.d.ts +19 -0
- package/dist/core/bang-commands/commands-task-control.js +147 -0
- package/dist/core/bang-commands/commands-wiki.js +5 -5
- package/dist/core/bang-commands/index.d.ts +2 -0
- package/dist/core/bang-commands/index.js +12 -0
- package/dist/core/bang-commands/registry.d.ts +12 -0
- package/dist/core/browser-history/research-cluster-fanout.d.ts +28 -14
- package/dist/core/browser-history/research-cluster-fanout.js +39 -16
- package/dist/core/channel-timeline.d.ts +5 -1
- package/dist/core/channel-timeline.js +13 -0
- package/dist/core/context/index-reconciler.js +5 -2
- package/dist/core/context/policy-index-reconciler.d.ts +6 -4
- package/dist/core/context/policy-index-runner.js +25 -6
- package/dist/core/context-builder-calendar.js +10 -2
- package/dist/core/context-builder-conversation.d.ts +8 -1
- package/dist/core/context-builder-conversation.js +41 -7
- package/dist/core/context-builder-yesterday.js +4 -3
- package/dist/core/context-builder.d.ts +7 -2
- package/dist/core/context-builder.js +193 -5
- package/dist/core/context-file-serializer.d.ts +1 -1
- package/dist/core/context-file-serializer.js +1 -1
- package/dist/core/context-health.js +2 -2
- package/dist/core/context-paths.d.ts +11 -1
- package/dist/core/context-paths.js +17 -1
- package/dist/core/context-validation/prepare-write.js +1 -1
- package/dist/core/context-validation/routine-rulebook.d.ts +1 -1
- package/dist/core/context-vault-aliases.d.ts +0 -13
- package/dist/core/context-vault-aliases.js +37 -0
- package/dist/core/custom-routines.d.ts +99 -0
- package/dist/core/custom-routines.js +187 -0
- package/dist/core/daemon-api-cli.js +50 -1
- package/dist/core/day-boundary.d.ts +46 -0
- package/dist/core/day-boundary.js +40 -0
- package/dist/core/dispatcher-activity-scan.d.ts +221 -0
- package/dist/core/dispatcher-activity-scan.js +775 -0
- package/dist/core/dispatcher-error-handling.d.ts +6 -11
- package/dist/core/dispatcher-error-handling.js +38 -62
- package/dist/core/dispatcher-hourly-check.js +6 -1
- package/dist/core/dispatcher-message-handler.d.ts +10 -0
- package/dist/core/dispatcher-message-handler.js +24 -0
- package/dist/core/dispatcher-morning-routine.d.ts +6 -6
- package/dist/core/dispatcher-morning-routine.js +13 -13
- package/dist/core/dispatcher-result-processor.d.ts +33 -0
- package/dist/core/dispatcher-result-processor.js +167 -11
- package/dist/core/dispatcher-scheduled-background-task.d.ts +42 -0
- package/dist/core/dispatcher-scheduled-background-task.js +89 -0
- package/dist/core/dispatcher-scheduled-tasks.d.ts +104 -1
- package/dist/core/dispatcher-scheduled-tasks.js +480 -8
- package/dist/core/dispatcher-task-delivery.d.ts +105 -0
- package/dist/core/dispatcher-task-delivery.js +555 -0
- package/dist/core/dispatcher-types.d.ts +48 -9
- package/dist/core/dispatcher-types.js +3 -3
- package/dist/core/dispatcher.d.ts +112 -31
- package/dist/core/dispatcher.js +297 -60
- package/dist/core/dm-freshness-metrics.d.ts +1 -1
- package/dist/core/drift-effects.js +2 -2
- package/dist/core/feedback/consolidation-prep.d.ts +94 -0
- package/dist/core/feedback/consolidation-prep.js +254 -0
- package/dist/core/feedback/eviction-scorer.d.ts +81 -0
- package/dist/core/feedback/eviction-scorer.js +136 -0
- package/dist/core/feedback/lesson-format.d.ts +79 -0
- package/dist/core/feedback/lesson-format.js +199 -0
- package/dist/core/feedback/lesson-injection.d.ts +98 -0
- package/dist/core/feedback/lesson-injection.js +174 -0
- package/dist/core/feedback/lesson-merge.d.ts +51 -0
- package/dist/core/feedback/lesson-merge.js +88 -0
- package/dist/core/feedback/lesson-store-overview.d.ts +46 -0
- package/dist/core/feedback/lesson-store-overview.js +42 -0
- package/dist/core/feedback/promotion-gate.d.ts +69 -0
- package/dist/core/feedback/promotion-gate.js +117 -0
- package/dist/core/feedback/regeneralization-prep.d.ts +87 -0
- package/dist/core/feedback/regeneralization-prep.js +152 -0
- package/dist/core/feedback/scope-parser.d.ts +86 -0
- package/dist/core/feedback/scope-parser.js +141 -0
- package/dist/core/feedback/self-performance-prep.d.ts +186 -0
- package/dist/core/feedback/self-performance-prep.js +541 -0
- package/dist/core/feedback/tuning-actuator.d.ts +198 -0
- package/dist/core/feedback/tuning-actuator.js +432 -0
- package/dist/core/feedback/tuning-recommender.d.ts +247 -0
- package/dist/core/feedback/tuning-recommender.js +580 -0
- package/dist/core/feedback/tuning-revert-monitor.d.ts +90 -0
- package/dist/core/feedback/tuning-revert-monitor.js +213 -0
- package/dist/core/health-monitor.d.ts +6 -0
- package/dist/core/health-monitor.js +1 -1
- package/dist/core/injection-policy.d.ts +83 -1
- package/dist/core/injection-policy.js +61 -3
- package/dist/core/integration-main-backend.js +4 -0
- package/dist/core/management-md.d.ts +2 -2
- package/dist/core/management-md.js +51 -13
- package/dist/core/morning/orchestrator.d.ts +2 -2
- package/dist/core/morning/orchestrator.js +2 -2
- package/dist/core/notification-gate.d.ts +64 -0
- package/dist/core/notification-gate.js +51 -0
- package/dist/core/notification-rate-limit.d.ts +40 -0
- package/dist/core/notification-rate-limit.js +50 -0
- package/dist/core/policy-files.d.ts +1 -1
- package/dist/core/policy-files.js +2 -2
- package/dist/core/pre-pass-freshness.d.ts +4 -4
- package/dist/core/retention.d.ts +5 -0
- package/dist/core/retention.js +20 -4
- package/dist/core/review-context.d.ts +1 -1
- package/dist/core/review-context.js +10 -5
- package/dist/core/roadmap-write-lock.d.ts +2 -1
- package/dist/core/roadmap-write-lock.js +15 -10
- package/dist/core/routine-acquisition-plan.d.ts +47 -1
- package/dist/core/routine-acquisition-plan.js +78 -20
- package/dist/core/routine-fetch-window-retry.js +7 -4
- package/dist/core/routine-fetch-window-runner.d.ts +39 -3
- package/dist/core/routine-fetch-window-runner.js +264 -13
- package/dist/core/routine-windows.d.ts +2 -2
- package/dist/core/routine-windows.js +8 -5
- package/dist/core/scheduler.d.ts +175 -16
- package/dist/core/scheduler.js +559 -102
- package/dist/core/signal-detector.d.ts +51 -1
- package/dist/core/signal-detector.js +321 -24
- package/dist/core/skills-compiler-denied-tools.js +2 -2
- package/dist/core/skills-compiler-skill-index.d.ts +2 -2
- package/dist/core/skills-compiler-skill-index.js +2 -2
- package/dist/core/skills-compiler-variants.d.ts +1 -1
- package/dist/core/skills-compiler-variants.js +8 -0
- package/dist/core/skills-compiler.d.ts +29 -26
- package/dist/core/skills-compiler.js +117 -81
- package/dist/core/skills-manifest.d.ts +37 -0
- package/dist/core/skills-manifest.js +73 -2
- package/dist/core/sleep-inhibitor.d.ts +79 -0
- package/dist/core/sleep-inhibitor.js +132 -0
- package/dist/core/slim-system-prompt-loader.d.ts +77 -0
- package/dist/core/slim-system-prompt-loader.js +141 -0
- package/dist/core/spawn-gates.d.ts +126 -0
- package/dist/core/spawn-gates.js +180 -0
- package/dist/core/today-direct-writer.d.ts +60 -14
- package/dist/core/today-direct-writer.js +90 -13
- package/dist/core/today-write-lock.d.ts +4 -2
- package/dist/core/today-write-lock.js +30 -20
- package/dist/core/wake-detector.d.ts +55 -0
- package/dist/core/wake-detector.js +80 -0
- package/dist/core/wiki/compile-lock.d.ts +1 -1
- package/dist/core/wiki/compile-lock.js +1 -1
- package/dist/core/wiki/wiki-fts.js +13 -6
- package/dist/core/workdir.js +15 -6
- package/dist/db/activity-scan-signals.d.ts +77 -0
- package/dist/db/activity-scan-signals.js +378 -0
- package/dist/db/agents-store.d.ts +28 -0
- package/dist/db/agents-store.js +62 -0
- package/dist/db/background-task-clarifications-store.d.ts +81 -0
- package/dist/db/background-task-clarifications-store.js +152 -0
- package/dist/db/background-task-store.d.ts +207 -0
- package/dist/db/background-task-store.js +380 -0
- package/dist/db/browser-history-store.d.ts +39 -6
- package/dist/db/browser-history-store.js +51 -7
- package/dist/db/browser-task-clarifications-store.d.ts +12 -0
- package/dist/db/browser-task-clarifications-store.js +35 -5
- package/dist/db/browser-task-store.d.ts +3 -0
- package/dist/db/browser-task-store.js +29 -4
- package/dist/db/deferred-dm.d.ts +86 -0
- package/dist/db/deferred-dm.js +199 -0
- package/dist/db/feedback-signals-store.d.ts +77 -0
- package/dist/db/feedback-signals-store.js +144 -0
- package/dist/db/migrations.js +380 -0
- package/dist/db/observations.d.ts +2 -2
- package/dist/db/observations.js +3 -3
- package/dist/db/schema.js +260 -22
- package/dist/db/voice-transcripts-store.d.ts +1 -1
- package/dist/index.js +86 -29
- package/dist/messaging/browser-task-mcp-notifier.d.ts +12 -70
- package/dist/messaging/browser-task-mcp-notifier.js +30 -151
- package/dist/messaging/browser-task-screenshot-attachment.d.ts +15 -0
- package/dist/messaging/browser-task-screenshot-attachment.js +63 -0
- package/dist/observers/delegated-sync-worker.d.ts +6 -6
- package/dist/observers/delegated-sync-worker.js +10 -10
- package/dist/observers/git-delegated-cron.d.ts +1 -1
- package/dist/observers/git-delegated-cron.js +2 -2
- package/dist/observers/github-poller-classifier.d.ts +3 -3
- package/dist/observers/github-poller-classifier.js +3 -3
- package/dist/observers/imminent-event-scheduler.d.ts +1 -1
- package/dist/observers/imminent-event-scheduler.js +1 -1
- package/dist/observers/mail-poller.d.ts +1 -0
- package/dist/observers/mail-poller.js +42 -3
- package/dist/observers/observation-summarizer/summarizer-client.d.ts +2 -2
- package/dist/observers/observation-summarizer/summarizer-client.js +2 -2
- package/dist/observers/observation-summarizer/worker.d.ts +2 -2
- package/dist/observers/observation-summarizer/worker.js +4 -4
- package/dist/observers/obsidian-watcher.d.ts +1 -1
- package/dist/observers/obsidian-watcher.js +1 -1
- package/dist/safety/agent-write-tracker.d.ts +4 -4
- package/dist/safety/agent-write-tracker.js +4 -4
- package/dist/safety/always-disallowed.d.ts +1 -1
- package/dist/safety/always-disallowed.js +39 -0
- package/dist/safety/audit.d.ts +43 -5
- package/dist/safety/audit.js +86 -18
- package/dist/safety/risk-classifier.d.ts +6 -0
- package/dist/safety/risk-classifier.js +97 -18
- package/dist/scheduler/activity-scan-gate.d.ts +86 -0
- package/dist/scheduler/activity-scan-gate.js +132 -0
- package/dist/services/background-task/background-task-budget.d.ts +80 -0
- package/dist/services/background-task/background-task-budget.js +91 -0
- package/dist/services/background-task/background-task-driver.d.ts +105 -0
- package/dist/services/background-task/background-task-driver.js +416 -0
- package/dist/services/background-task/background-task-runner.d.ts +96 -0
- package/dist/services/background-task/background-task-runner.js +673 -0
- package/dist/services/background-task/background-task-tools.d.ts +84 -0
- package/dist/services/background-task/background-task-tools.js +247 -0
- package/dist/services/background-task/background-task-transition-events.d.ts +43 -0
- package/dist/services/background-task/background-task-transition-events.js +54 -0
- package/dist/services/browser-history/automation/egress-denylist.d.ts +1 -1
- package/dist/services/browser-history/automation/egress-denylist.js +34 -8
- package/dist/services/browser-history/lifecycle/platform.js +44 -2
- package/dist/services/browser-history/managed-chromium/sandbox-launcher.js +0 -1
- package/dist/services/browser-task/browser-task-runner.js +53 -8
- package/dist/services/mcp/probe.js +30 -8
- package/dist/services/observations-batch.d.ts +1 -1
- package/dist/services/observations-batch.js +2 -2
- package/dist/settings/runtime-settings.d.ts +45 -12
- package/dist/settings/runtime-settings.js +215 -40
- package/dist/settings/settings-store.js +11 -3
- package/package.json +4 -4
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single source of truth for the daemon's per-process **slim** system-prompt
|
|
3
|
+
* templates. A slim prompt replaces the verbose `preset: "claude_code"` system
|
|
4
|
+
* prompt (Claude SDK) / the wide profile + skill-index instruction file
|
|
5
|
+
* (Codex / Gemini CLI) with a tight, self-contained operational contract for
|
|
6
|
+
* one short, mechanical, lite-tier process key — shedding ~30 K tokens of
|
|
7
|
+
* built-in tool descriptions, the skills index, the memory-system docs, and
|
|
8
|
+
* tone/style guidance the key never uses.
|
|
9
|
+
*
|
|
10
|
+
* Two cost-reduction efforts share this registry:
|
|
11
|
+
* - `routine.fetch_window` — docs/design/appendices/fetch-window-cost-reduction.md
|
|
12
|
+
* Phase 1 / 1.5 (the original slim prompt).
|
|
13
|
+
* - `routine.research_cluster_update` — RESEARCH_CLUSTER_COST_FIX_PLAN.md F4
|
|
14
|
+
* (Phase 2): the nightly per-cluster journal-append session.
|
|
15
|
+
*
|
|
16
|
+
* Adding a slim key is a one-line entry in `SLIM_SYSTEM_PROMPT_LOADERS` below
|
|
17
|
+
* (plus the asset file under agent-assets/system-prompts/ and, for CLI parity,
|
|
18
|
+
* the skill set in skills-compiler.ts:`SLIM_CLI_SKILL_SETS`).
|
|
19
|
+
*
|
|
20
|
+
* The same template is consumed by two backend paths so the body stays
|
|
21
|
+
* byte-identical across backends:
|
|
22
|
+
* - Claude SDK — passed as `query()`'s `systemPrompt` string by
|
|
23
|
+
* `claude-code-core.ts:buildSystemPrompt`.
|
|
24
|
+
* - Codex / Gemini CLI — written verbatim as `AGENTS.md` / `GEMINI.md` by
|
|
25
|
+
* `skills-compiler.ts:materializeSlimCliSession`.
|
|
26
|
+
*
|
|
27
|
+
* Hoisting the loaders out of `claude-code-core.ts` lets the skills compiler
|
|
28
|
+
* import them without a cross-backend dependency, and keeps the disk read +
|
|
29
|
+
* cache amortised across both code paths (one read per template per daemon
|
|
30
|
+
* boot regardless of how many sessions of either backend run).
|
|
31
|
+
*
|
|
32
|
+
* Each template is immutable for the daemon's lifetime, so a single
|
|
33
|
+
* per-template module-level cache is sufficient. The test reset helpers let
|
|
34
|
+
* unit tests simulate a fresh boot without reaching into module internals.
|
|
35
|
+
*/
|
|
36
|
+
import { type ProcessKey } from "@aitne/shared";
|
|
37
|
+
/**
|
|
38
|
+
* Load the slim `routine.fetch_window` system-prompt template, caching the
|
|
39
|
+
* result for the daemon's lifetime.
|
|
40
|
+
*/
|
|
41
|
+
export declare function loadFetchWindowSystemPrompt(): string;
|
|
42
|
+
/**
|
|
43
|
+
* Load the slim `routine.research_cluster_update` system-prompt template
|
|
44
|
+
* (RESEARCH_CLUSTER_COST_FIX_PLAN.md F4), caching for the daemon's lifetime.
|
|
45
|
+
*/
|
|
46
|
+
export declare function loadResearchClusterUpdateSystemPrompt(): string;
|
|
47
|
+
/**
|
|
48
|
+
* Registry of process keys whose session uses a slim system prompt instead of
|
|
49
|
+
* the wide preset / profile. Typed as `Partial<Record<ProcessKey, …>>` (not an
|
|
50
|
+
* inferred string-literal map) so a rename in `@aitne/shared/process-key.ts`
|
|
51
|
+
* lights up here rather than silently dead-branching every consumer.
|
|
52
|
+
*
|
|
53
|
+
* Both call sites (Claude SDK `buildSystemPrompt`, CLI `materializeSlimCliSession`)
|
|
54
|
+
* resolve membership through `loadSlimSystemPrompt` / `isSlimSystemPromptKey`,
|
|
55
|
+
* so adding a key here is the single edit that wires both backends.
|
|
56
|
+
*/
|
|
57
|
+
export declare const SLIM_SYSTEM_PROMPT_LOADERS: Partial<Record<ProcessKey, () => string>>;
|
|
58
|
+
/** True when `processKey` has a slim system-prompt template registered. */
|
|
59
|
+
export declare function isSlimSystemPromptKey(processKey: ProcessKey | undefined): boolean;
|
|
60
|
+
/**
|
|
61
|
+
* Load the slim system-prompt body for `processKey`, or `null` when the key
|
|
62
|
+
* has no slim template (callers fall through to the wide preset / profile).
|
|
63
|
+
* The disk read is amortised by the per-template cache.
|
|
64
|
+
*/
|
|
65
|
+
export declare function loadSlimSystemPrompt(processKey: ProcessKey | undefined): string | null;
|
|
66
|
+
/**
|
|
67
|
+
* Test-only — drop every cached slim prompt so a subsequent load re-reads from
|
|
68
|
+
* disk. Production never needs this; the templates are immutable per boot.
|
|
69
|
+
*/
|
|
70
|
+
export declare function resetSlimSystemPromptsForTest(): void;
|
|
71
|
+
/**
|
|
72
|
+
* Back-compat alias retained so the existing fetch_window tests / call sites
|
|
73
|
+
* keep working after the Phase 2 generalization. Resets every slim cache (a
|
|
74
|
+
* superset of the prior fetch_window-only reset — harmless for tests). Prefer
|
|
75
|
+
* `resetSlimSystemPromptsForTest` in new code.
|
|
76
|
+
*/
|
|
77
|
+
export declare function resetFetchWindowSystemPromptForTest(): void;
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single source of truth for the daemon's per-process **slim** system-prompt
|
|
3
|
+
* templates. A slim prompt replaces the verbose `preset: "claude_code"` system
|
|
4
|
+
* prompt (Claude SDK) / the wide profile + skill-index instruction file
|
|
5
|
+
* (Codex / Gemini CLI) with a tight, self-contained operational contract for
|
|
6
|
+
* one short, mechanical, lite-tier process key — shedding ~30 K tokens of
|
|
7
|
+
* built-in tool descriptions, the skills index, the memory-system docs, and
|
|
8
|
+
* tone/style guidance the key never uses.
|
|
9
|
+
*
|
|
10
|
+
* Two cost-reduction efforts share this registry:
|
|
11
|
+
* - `routine.fetch_window` — docs/design/appendices/fetch-window-cost-reduction.md
|
|
12
|
+
* Phase 1 / 1.5 (the original slim prompt).
|
|
13
|
+
* - `routine.research_cluster_update` — RESEARCH_CLUSTER_COST_FIX_PLAN.md F4
|
|
14
|
+
* (Phase 2): the nightly per-cluster journal-append session.
|
|
15
|
+
*
|
|
16
|
+
* Adding a slim key is a one-line entry in `SLIM_SYSTEM_PROMPT_LOADERS` below
|
|
17
|
+
* (plus the asset file under agent-assets/system-prompts/ and, for CLI parity,
|
|
18
|
+
* the skill set in skills-compiler.ts:`SLIM_CLI_SKILL_SETS`).
|
|
19
|
+
*
|
|
20
|
+
* The same template is consumed by two backend paths so the body stays
|
|
21
|
+
* byte-identical across backends:
|
|
22
|
+
* - Claude SDK — passed as `query()`'s `systemPrompt` string by
|
|
23
|
+
* `claude-code-core.ts:buildSystemPrompt`.
|
|
24
|
+
* - Codex / Gemini CLI — written verbatim as `AGENTS.md` / `GEMINI.md` by
|
|
25
|
+
* `skills-compiler.ts:materializeSlimCliSession`.
|
|
26
|
+
*
|
|
27
|
+
* Hoisting the loaders out of `claude-code-core.ts` lets the skills compiler
|
|
28
|
+
* import them without a cross-backend dependency, and keeps the disk read +
|
|
29
|
+
* cache amortised across both code paths (one read per template per daemon
|
|
30
|
+
* boot regardless of how many sessions of either backend run).
|
|
31
|
+
*
|
|
32
|
+
* Each template is immutable for the daemon's lifetime, so a single
|
|
33
|
+
* per-template module-level cache is sufficient. The test reset helpers let
|
|
34
|
+
* unit tests simulate a fresh boot without reaching into module internals.
|
|
35
|
+
*/
|
|
36
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
37
|
+
import { dirname, join } from "node:path";
|
|
38
|
+
import { fileURLToPath } from "node:url";
|
|
39
|
+
import { substituteBrandTokens } from "@aitne/shared";
|
|
40
|
+
/**
|
|
41
|
+
* Resolve + read a slim-prompt asset by file basename (without the `.md`
|
|
42
|
+
* extension), applying brand-token substitution.
|
|
43
|
+
*
|
|
44
|
+
* Path resolution follows the same shape as `prompts.ts:resolveTaskFlowsDir`:
|
|
45
|
+
* `__dirname` lives at `packages/daemon/src/core/` (or the matching
|
|
46
|
+
* `dist/core/` after build), so the repo's `agent-assets/` directory is four
|
|
47
|
+
* levels up. The cwd fallback only fires in unusual harness layouts.
|
|
48
|
+
*
|
|
49
|
+
* Brand tokens (`{APP_NAME}`) are substituted here so the slim template stays
|
|
50
|
+
* aligned with the wide path's substitution contract: the Claude SDK
|
|
51
|
+
* systemPrompt and the CLI AGENTS.md / GEMINI.md both see the substituted
|
|
52
|
+
* product name instead of a literal `{APP_NAME}` leaking through. APP_NAME is
|
|
53
|
+
* a compile-time constant, so the per-boot cache stays byte-stable.
|
|
54
|
+
*/
|
|
55
|
+
function readSlimSystemPromptAsset(basename) {
|
|
56
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
57
|
+
const candidates = [
|
|
58
|
+
join(here, "..", "..", "..", "..", "agent-assets", "system-prompts", `${basename}.md`),
|
|
59
|
+
// Defensive fallback — if the module is invoked from an unexpected layout
|
|
60
|
+
// we still find the asset by walking up from cwd. Last-resort only; the
|
|
61
|
+
// relative path above is the canonical resolution.
|
|
62
|
+
join(process.cwd(), "agent-assets", "system-prompts", `${basename}.md`),
|
|
63
|
+
];
|
|
64
|
+
for (const path of candidates) {
|
|
65
|
+
if (existsSync(path)) {
|
|
66
|
+
return substituteBrandTokens(readFileSync(path, "utf-8"));
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
throw new Error(`slim system prompt "${basename}" not found. Looked in: ${candidates.join(", ")}`);
|
|
70
|
+
}
|
|
71
|
+
// Per-template module-level caches. Each template is byte-stable per boot, so
|
|
72
|
+
// a single cached string is sufficient; the reset helper drops them for tests.
|
|
73
|
+
let cachedFetchWindowSystemPrompt = null;
|
|
74
|
+
let cachedResearchClusterUpdateSystemPrompt = null;
|
|
75
|
+
/**
|
|
76
|
+
* Load the slim `routine.fetch_window` system-prompt template, caching the
|
|
77
|
+
* result for the daemon's lifetime.
|
|
78
|
+
*/
|
|
79
|
+
export function loadFetchWindowSystemPrompt() {
|
|
80
|
+
if (cachedFetchWindowSystemPrompt !== null)
|
|
81
|
+
return cachedFetchWindowSystemPrompt;
|
|
82
|
+
cachedFetchWindowSystemPrompt = readSlimSystemPromptAsset("routine-fetch-window");
|
|
83
|
+
return cachedFetchWindowSystemPrompt;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Load the slim `routine.research_cluster_update` system-prompt template
|
|
87
|
+
* (RESEARCH_CLUSTER_COST_FIX_PLAN.md F4), caching for the daemon's lifetime.
|
|
88
|
+
*/
|
|
89
|
+
export function loadResearchClusterUpdateSystemPrompt() {
|
|
90
|
+
if (cachedResearchClusterUpdateSystemPrompt !== null) {
|
|
91
|
+
return cachedResearchClusterUpdateSystemPrompt;
|
|
92
|
+
}
|
|
93
|
+
cachedResearchClusterUpdateSystemPrompt = readSlimSystemPromptAsset("routine-research-cluster-update");
|
|
94
|
+
return cachedResearchClusterUpdateSystemPrompt;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Registry of process keys whose session uses a slim system prompt instead of
|
|
98
|
+
* the wide preset / profile. Typed as `Partial<Record<ProcessKey, …>>` (not an
|
|
99
|
+
* inferred string-literal map) so a rename in `@aitne/shared/process-key.ts`
|
|
100
|
+
* lights up here rather than silently dead-branching every consumer.
|
|
101
|
+
*
|
|
102
|
+
* Both call sites (Claude SDK `buildSystemPrompt`, CLI `materializeSlimCliSession`)
|
|
103
|
+
* resolve membership through `loadSlimSystemPrompt` / `isSlimSystemPromptKey`,
|
|
104
|
+
* so adding a key here is the single edit that wires both backends.
|
|
105
|
+
*/
|
|
106
|
+
export const SLIM_SYSTEM_PROMPT_LOADERS = {
|
|
107
|
+
"routine.fetch_window": loadFetchWindowSystemPrompt,
|
|
108
|
+
"routine.research_cluster_update": loadResearchClusterUpdateSystemPrompt,
|
|
109
|
+
};
|
|
110
|
+
/** True when `processKey` has a slim system-prompt template registered. */
|
|
111
|
+
export function isSlimSystemPromptKey(processKey) {
|
|
112
|
+
return processKey !== undefined && processKey in SLIM_SYSTEM_PROMPT_LOADERS;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Load the slim system-prompt body for `processKey`, or `null` when the key
|
|
116
|
+
* has no slim template (callers fall through to the wide preset / profile).
|
|
117
|
+
* The disk read is amortised by the per-template cache.
|
|
118
|
+
*/
|
|
119
|
+
export function loadSlimSystemPrompt(processKey) {
|
|
120
|
+
if (processKey === undefined)
|
|
121
|
+
return null;
|
|
122
|
+
const loader = SLIM_SYSTEM_PROMPT_LOADERS[processKey];
|
|
123
|
+
return loader ? loader() : null;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Test-only — drop every cached slim prompt so a subsequent load re-reads from
|
|
127
|
+
* disk. Production never needs this; the templates are immutable per boot.
|
|
128
|
+
*/
|
|
129
|
+
export function resetSlimSystemPromptsForTest() {
|
|
130
|
+
cachedFetchWindowSystemPrompt = null;
|
|
131
|
+
cachedResearchClusterUpdateSystemPrompt = null;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Back-compat alias retained so the existing fetch_window tests / call sites
|
|
135
|
+
* keep working after the Phase 2 generalization. Resets every slim cache (a
|
|
136
|
+
* superset of the prior fetch_window-only reset — harmless for tests). Prefer
|
|
137
|
+
* `resetSlimSystemPromptsForTest` in new code.
|
|
138
|
+
*/
|
|
139
|
+
export function resetFetchWindowSystemPromptForTest() {
|
|
140
|
+
resetSlimSystemPromptsForTest();
|
|
141
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pre-spawn gates for autonomous routine sessions —
|
|
3
|
+
* PREPASS_COST_REDUCTION_PLAN.md N2.
|
|
4
|
+
*
|
|
5
|
+
* Two cheap checks run before the daemon spawns an autonomous backend
|
|
6
|
+
* session (routine dispatch + pre-pass fan-out sub-sessions), so a
|
|
7
|
+
* session that would deterministically fail is skipped instead of
|
|
8
|
+
* billed/spawned:
|
|
9
|
+
*
|
|
10
|
+
* 1. **Offline gate** — DNS-resolve the *backend API host* (the daemon
|
|
11
|
+
* never talks to integration APIs in delegated/native mode, so the
|
|
12
|
+
* backend host is the only one that matters). Uses `dns.lookup`
|
|
13
|
+
* (getaddrinfo — the same resolver path that produces the observed
|
|
14
|
+
* ENOTFOUND failures), NOT `dns.resolve` (c-ares bypasses the OS
|
|
15
|
+
* resolver / hosts file and can disagree with what the session would
|
|
16
|
+
* see). Results are cached ~60s per host so a fan-out of N
|
|
17
|
+
* integrations costs one lookup.
|
|
18
|
+
* 2. **Auth gate** — consult the cached auth-health row
|
|
19
|
+
* (`readCachedAuthStatus`) and treat the backend as non-viable only
|
|
20
|
+
* when `shouldSkip` is true (expired/missing with a ≤10-min-fresh
|
|
21
|
+
* cache, or a recovery subprocess owning the row). The hourly
|
|
22
|
+
* `checkAll()` probe and the reactive `recordReactiveAuth*` writers
|
|
23
|
+
* refresh the cache independently of routine sessions, so a
|
|
24
|
+
* recovered backend un-skips within minutes.
|
|
25
|
+
*
|
|
26
|
+
* **A spawn is skipped only when EVERY candidate backend (main +
|
|
27
|
+
* fallback) is non-viable.** The BackendRouter already skips an
|
|
28
|
+
* auth-unhealthy main straight to its fallback, so gating on the main
|
|
29
|
+
* alone would suppress sessions the router could have completed —
|
|
30
|
+
* exactly the accuracy degradation the now-scope forbids.
|
|
31
|
+
*
|
|
32
|
+
* Skips never touch pre-pass freshness state, so the next tick retries.
|
|
33
|
+
* Every decision is fail-open: an unknown backend host, a gate-internal
|
|
34
|
+
* error, or a DB failure lets the spawn proceed — the gate exists to
|
|
35
|
+
* save doomed sessions, never to block live ones.
|
|
36
|
+
*/
|
|
37
|
+
import type Database from "better-sqlite3";
|
|
38
|
+
import type { BackendId } from "@aitne/shared";
|
|
39
|
+
/**
|
|
40
|
+
* Backend → API host the SDK/CLI must reach for a session to be viable.
|
|
41
|
+
* Hosts chosen per the default auth path of each backend's runtime:
|
|
42
|
+
* Claude Code → Anthropic API; Codex CLI (ChatGPT-plan auth) →
|
|
43
|
+
* chatgpt.com; Gemini CLI (OAuth code-assist) → cloudcode-pa. The gate
|
|
44
|
+
* only needs a DNS answer for outage detection, so an API-key install
|
|
45
|
+
* resolving a sibling host of the same provider is equally conclusive.
|
|
46
|
+
* Backends without an entry (e.g. opencode, which routes to arbitrary
|
|
47
|
+
* providers) are fail-open: the offline gate passes them.
|
|
48
|
+
*/
|
|
49
|
+
export declare const BACKEND_API_HOSTS: Partial<Record<BackendId, string>>;
|
|
50
|
+
export type SpawnGateSkipReason = "offline" | "auth_unhealthy";
|
|
51
|
+
/** Per-candidate diagnostics carried into the skip audit row. */
|
|
52
|
+
export interface SpawnGateBackendVerdict {
|
|
53
|
+
backendId: BackendId;
|
|
54
|
+
/** Host probed by the offline gate; null = no mapping (gate passed). */
|
|
55
|
+
host: string | null;
|
|
56
|
+
/** True when the host failed to resolve (within the cache TTL). */
|
|
57
|
+
offline: boolean;
|
|
58
|
+
/** Cached auth status string (`ok` / `expired` / `missing` / …). */
|
|
59
|
+
authStatus: string;
|
|
60
|
+
/** True when the cached auth row says a spawn is doomed. */
|
|
61
|
+
authShouldSkip: boolean;
|
|
62
|
+
/** Net verdict: this backend could run the session. */
|
|
63
|
+
viable: boolean;
|
|
64
|
+
}
|
|
65
|
+
export interface SpawnGateDecision {
|
|
66
|
+
skip: boolean;
|
|
67
|
+
/**
|
|
68
|
+
* Present iff `skip`. `offline` when every candidate was blocked by
|
|
69
|
+
* DNS; `auth_unhealthy` when at least one candidate resolved but had
|
|
70
|
+
* a confirmed-bad auth cache.
|
|
71
|
+
*/
|
|
72
|
+
reason?: SpawnGateSkipReason;
|
|
73
|
+
backends: SpawnGateBackendVerdict[];
|
|
74
|
+
}
|
|
75
|
+
export interface AutonomousSpawnGateOptions {
|
|
76
|
+
/** Injected for tests; defaults to `node:dns/promises.lookup`. */
|
|
77
|
+
lookup?: (host: string) => Promise<unknown>;
|
|
78
|
+
/** Injected clock for tests; defaults to `Date.now`. */
|
|
79
|
+
now?: () => number;
|
|
80
|
+
/** DNS verdict cache TTL; defaults to 60s. */
|
|
81
|
+
dnsCacheTtlMs?: number;
|
|
82
|
+
/** Per-lookup deadline (fail-open past it); defaults to 2.5s. */
|
|
83
|
+
dnsLookupTimeoutMs?: number;
|
|
84
|
+
/**
|
|
85
|
+
* Freshness window forwarded to `readCachedAuthStatus`. Omit to use
|
|
86
|
+
* that module's 10-minute default; callers with a configured
|
|
87
|
+
* `authPreflightFreshnessMs` should thread it through so the gate and
|
|
88
|
+
* the router agree on what "recently confirmed bad" means.
|
|
89
|
+
*/
|
|
90
|
+
authFreshnessMs?: number;
|
|
91
|
+
/** Host mapping override for tests. */
|
|
92
|
+
backendApiHosts?: Partial<Record<BackendId, string>>;
|
|
93
|
+
}
|
|
94
|
+
export declare class AutonomousSpawnGate {
|
|
95
|
+
private readonly db;
|
|
96
|
+
private readonly lookup;
|
|
97
|
+
private readonly now;
|
|
98
|
+
private readonly dnsCacheTtlMs;
|
|
99
|
+
private readonly dnsLookupTimeoutMs;
|
|
100
|
+
private readonly authFreshnessMs;
|
|
101
|
+
private readonly hosts;
|
|
102
|
+
private readonly dnsCache;
|
|
103
|
+
constructor(db: Database.Database, options?: AutonomousSpawnGateOptions);
|
|
104
|
+
/**
|
|
105
|
+
* Evaluate the gates for the candidate backends that could run the
|
|
106
|
+
* session (binding main first, then fallback). Returns `skip: false`
|
|
107
|
+
* for an empty candidate list (nothing to assert) and on any internal
|
|
108
|
+
* error (fail-open).
|
|
109
|
+
*/
|
|
110
|
+
evaluate(candidates: readonly BackendId[]): Promise<SpawnGateDecision>;
|
|
111
|
+
private evaluateBackend;
|
|
112
|
+
private hostResolves;
|
|
113
|
+
/**
|
|
114
|
+
* One bounded lookup attempt. Three fail-OPEN (`true`) outcomes that
|
|
115
|
+
* deliberately do not count as "offline":
|
|
116
|
+
* - the resolver answered (any address);
|
|
117
|
+
* - `EAI_AGAIN` — the resolver said "try again", which is a transient
|
|
118
|
+
* resolver condition, not an outage verdict;
|
|
119
|
+
* - the deadline elapsed — no answer is not a negative answer.
|
|
120
|
+
* Only a definitive resolution failure (ENOTFOUND et al.) returns
|
|
121
|
+
* `false`. The verdict — including a fail-open one — is cached by the
|
|
122
|
+
* caller for the TTL so a hung resolver costs at most one deadline
|
|
123
|
+
* per host per minute.
|
|
124
|
+
*/
|
|
125
|
+
private lookupWithDeadline;
|
|
126
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pre-spawn gates for autonomous routine sessions —
|
|
3
|
+
* PREPASS_COST_REDUCTION_PLAN.md N2.
|
|
4
|
+
*
|
|
5
|
+
* Two cheap checks run before the daemon spawns an autonomous backend
|
|
6
|
+
* session (routine dispatch + pre-pass fan-out sub-sessions), so a
|
|
7
|
+
* session that would deterministically fail is skipped instead of
|
|
8
|
+
* billed/spawned:
|
|
9
|
+
*
|
|
10
|
+
* 1. **Offline gate** — DNS-resolve the *backend API host* (the daemon
|
|
11
|
+
* never talks to integration APIs in delegated/native mode, so the
|
|
12
|
+
* backend host is the only one that matters). Uses `dns.lookup`
|
|
13
|
+
* (getaddrinfo — the same resolver path that produces the observed
|
|
14
|
+
* ENOTFOUND failures), NOT `dns.resolve` (c-ares bypasses the OS
|
|
15
|
+
* resolver / hosts file and can disagree with what the session would
|
|
16
|
+
* see). Results are cached ~60s per host so a fan-out of N
|
|
17
|
+
* integrations costs one lookup.
|
|
18
|
+
* 2. **Auth gate** — consult the cached auth-health row
|
|
19
|
+
* (`readCachedAuthStatus`) and treat the backend as non-viable only
|
|
20
|
+
* when `shouldSkip` is true (expired/missing with a ≤10-min-fresh
|
|
21
|
+
* cache, or a recovery subprocess owning the row). The hourly
|
|
22
|
+
* `checkAll()` probe and the reactive `recordReactiveAuth*` writers
|
|
23
|
+
* refresh the cache independently of routine sessions, so a
|
|
24
|
+
* recovered backend un-skips within minutes.
|
|
25
|
+
*
|
|
26
|
+
* **A spawn is skipped only when EVERY candidate backend (main +
|
|
27
|
+
* fallback) is non-viable.** The BackendRouter already skips an
|
|
28
|
+
* auth-unhealthy main straight to its fallback, so gating on the main
|
|
29
|
+
* alone would suppress sessions the router could have completed —
|
|
30
|
+
* exactly the accuracy degradation the now-scope forbids.
|
|
31
|
+
*
|
|
32
|
+
* Skips never touch pre-pass freshness state, so the next tick retries.
|
|
33
|
+
* Every decision is fail-open: an unknown backend host, a gate-internal
|
|
34
|
+
* error, or a DB failure lets the spawn proceed — the gate exists to
|
|
35
|
+
* save doomed sessions, never to block live ones.
|
|
36
|
+
*/
|
|
37
|
+
import { lookup as dnsLookup } from "node:dns/promises";
|
|
38
|
+
import { readCachedAuthStatus } from "./backends/auth-health-monitor.js";
|
|
39
|
+
import { createLogger } from "../logging.js";
|
|
40
|
+
const logger = createLogger("spawn-gates");
|
|
41
|
+
/**
|
|
42
|
+
* Backend → API host the SDK/CLI must reach for a session to be viable.
|
|
43
|
+
* Hosts chosen per the default auth path of each backend's runtime:
|
|
44
|
+
* Claude Code → Anthropic API; Codex CLI (ChatGPT-plan auth) →
|
|
45
|
+
* chatgpt.com; Gemini CLI (OAuth code-assist) → cloudcode-pa. The gate
|
|
46
|
+
* only needs a DNS answer for outage detection, so an API-key install
|
|
47
|
+
* resolving a sibling host of the same provider is equally conclusive.
|
|
48
|
+
* Backends without an entry (e.g. opencode, which routes to arbitrary
|
|
49
|
+
* providers) are fail-open: the offline gate passes them.
|
|
50
|
+
*/
|
|
51
|
+
export const BACKEND_API_HOSTS = {
|
|
52
|
+
claude: "api.anthropic.com",
|
|
53
|
+
codex: "chatgpt.com",
|
|
54
|
+
gemini: "cloudcode-pa.googleapis.com",
|
|
55
|
+
};
|
|
56
|
+
/** Default TTL for cached per-host DNS verdicts (~60s per the N2 spec). */
|
|
57
|
+
const DEFAULT_DNS_CACHE_TTL_MS = 60 * 1000;
|
|
58
|
+
/**
|
|
59
|
+
* Deadline for a single `dns.lookup` call. getaddrinfo has no timeout
|
|
60
|
+
* of its own — a degraded resolver can block for the OS resolver
|
|
61
|
+
* timeout (5-30s), serially per candidate host, stalling the
|
|
62
|
+
* autonomous lane and every fan-out sub-session start. Past the
|
|
63
|
+
* deadline the gate fails OPEN (treats the host as resolvable): an
|
|
64
|
+
* answer we don't have is not an outage signal.
|
|
65
|
+
*/
|
|
66
|
+
const DEFAULT_DNS_LOOKUP_TIMEOUT_MS = 2_500;
|
|
67
|
+
export class AutonomousSpawnGate {
|
|
68
|
+
db;
|
|
69
|
+
lookup;
|
|
70
|
+
now;
|
|
71
|
+
dnsCacheTtlMs;
|
|
72
|
+
dnsLookupTimeoutMs;
|
|
73
|
+
authFreshnessMs;
|
|
74
|
+
hosts;
|
|
75
|
+
dnsCache = new Map();
|
|
76
|
+
constructor(db, options = {}) {
|
|
77
|
+
this.db = db;
|
|
78
|
+
// `dnsLookup` is referenced directly (no wrapper arrow) so the
|
|
79
|
+
// default arm carries no never-invoked closure — tests cover the
|
|
80
|
+
// `??` branch by constructing without options, without doing real
|
|
81
|
+
// DNS.
|
|
82
|
+
this.lookup = options.lookup ?? dnsLookup;
|
|
83
|
+
this.now = options.now ?? (() => Date.now());
|
|
84
|
+
this.dnsCacheTtlMs = options.dnsCacheTtlMs ?? DEFAULT_DNS_CACHE_TTL_MS;
|
|
85
|
+
this.dnsLookupTimeoutMs =
|
|
86
|
+
options.dnsLookupTimeoutMs ?? DEFAULT_DNS_LOOKUP_TIMEOUT_MS;
|
|
87
|
+
this.authFreshnessMs = options.authFreshnessMs;
|
|
88
|
+
this.hosts = options.backendApiHosts ?? BACKEND_API_HOSTS;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Evaluate the gates for the candidate backends that could run the
|
|
92
|
+
* session (binding main first, then fallback). Returns `skip: false`
|
|
93
|
+
* for an empty candidate list (nothing to assert) and on any internal
|
|
94
|
+
* error (fail-open).
|
|
95
|
+
*/
|
|
96
|
+
async evaluate(candidates) {
|
|
97
|
+
try {
|
|
98
|
+
if (candidates.length === 0) {
|
|
99
|
+
return { skip: false, backends: [] };
|
|
100
|
+
}
|
|
101
|
+
const backends = [];
|
|
102
|
+
for (const backendId of candidates) {
|
|
103
|
+
backends.push(await this.evaluateBackend(backendId));
|
|
104
|
+
}
|
|
105
|
+
if (backends.some((b) => b.viable)) {
|
|
106
|
+
return { skip: false, backends };
|
|
107
|
+
}
|
|
108
|
+
const reason = backends.every((b) => b.offline)
|
|
109
|
+
? "offline"
|
|
110
|
+
: "auth_unhealthy";
|
|
111
|
+
return { skip: true, reason, backends };
|
|
112
|
+
}
|
|
113
|
+
catch (err) {
|
|
114
|
+
logger.warn({ err, candidates }, "Spawn-gate evaluation failed — failing open");
|
|
115
|
+
return { skip: false, backends: [] };
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
async evaluateBackend(backendId) {
|
|
119
|
+
const host = this.hosts[backendId] ?? null;
|
|
120
|
+
const offline = host === null ? false : !(await this.hostResolves(host));
|
|
121
|
+
// readCachedAuthStatus is fail-open by contract (returns
|
|
122
|
+
// `{status:"unknown", shouldSkip:false}` on any DB error), and
|
|
123
|
+
// `evaluate()`'s outer catch fails the whole gate open as the last
|
|
124
|
+
// line of defense — no per-call try/catch needed here.
|
|
125
|
+
const cached = this.authFreshnessMs === undefined
|
|
126
|
+
? readCachedAuthStatus(this.db, backendId)
|
|
127
|
+
: readCachedAuthStatus(this.db, backendId, this.authFreshnessMs);
|
|
128
|
+
return {
|
|
129
|
+
backendId,
|
|
130
|
+
host,
|
|
131
|
+
offline,
|
|
132
|
+
authStatus: cached.status,
|
|
133
|
+
authShouldSkip: cached.shouldSkip,
|
|
134
|
+
viable: !offline && !cached.shouldSkip,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
async hostResolves(host) {
|
|
138
|
+
const nowMs = this.now();
|
|
139
|
+
const cached = this.dnsCache.get(host);
|
|
140
|
+
if (cached && cached.expiresAtMs > nowMs) {
|
|
141
|
+
return cached.ok;
|
|
142
|
+
}
|
|
143
|
+
const ok = await this.lookupWithDeadline(host);
|
|
144
|
+
this.dnsCache.set(host, {
|
|
145
|
+
ok,
|
|
146
|
+
expiresAtMs: this.now() + this.dnsCacheTtlMs,
|
|
147
|
+
});
|
|
148
|
+
return ok;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* One bounded lookup attempt. Three fail-OPEN (`true`) outcomes that
|
|
152
|
+
* deliberately do not count as "offline":
|
|
153
|
+
* - the resolver answered (any address);
|
|
154
|
+
* - `EAI_AGAIN` — the resolver said "try again", which is a transient
|
|
155
|
+
* resolver condition, not an outage verdict;
|
|
156
|
+
* - the deadline elapsed — no answer is not a negative answer.
|
|
157
|
+
* Only a definitive resolution failure (ENOTFOUND et al.) returns
|
|
158
|
+
* `false`. The verdict — including a fail-open one — is cached by the
|
|
159
|
+
* caller for the TTL so a hung resolver costs at most one deadline
|
|
160
|
+
* per host per minute.
|
|
161
|
+
*/
|
|
162
|
+
async lookupWithDeadline(host) {
|
|
163
|
+
let timer;
|
|
164
|
+
const attempt = this.lookup(host).then(() => true, (err) => {
|
|
165
|
+
const code = typeof err === "object" && err !== null && "code" in err
|
|
166
|
+
? err.code
|
|
167
|
+
: undefined;
|
|
168
|
+
return code === "EAI_AGAIN";
|
|
169
|
+
});
|
|
170
|
+
const deadline = new Promise((resolve) => {
|
|
171
|
+
timer = setTimeout(() => resolve(true), this.dnsLookupTimeoutMs);
|
|
172
|
+
});
|
|
173
|
+
try {
|
|
174
|
+
return await Promise.race([attempt, deadline]);
|
|
175
|
+
}
|
|
176
|
+
finally {
|
|
177
|
+
clearTimeout(timer);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Daemon-direct
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
2
|
+
* Daemon-direct, lock-aware writes to today.md that run *before* (or
|
|
3
|
+
* instead of) an agent session — bypassing `/api/context/*` because there
|
|
4
|
+
* is no subprocess to issue the curl call from. Two operations live here:
|
|
5
|
+
*
|
|
6
|
+
* 1. {@link appendAgentLogLine} — append a single `## Agent Log` bullet.
|
|
7
|
+
* Used by the three-stage activity_scan gate (cost-reduction-structural
|
|
8
|
+
* §B) on the stage0_silent / stage2_log_only paths so a "no-op" cron
|
|
9
|
+
* tick still leaves an audit trail without paying for an LLM session.
|
|
10
|
+
* 2. {@link ensureTodaySkeleton} — seed the canonical empty skeleton when
|
|
11
|
+
* today.md is **absent**, so a section-only refresh routine has a valid
|
|
12
|
+
* PATCH target instead of 404-ing and budget-burning on full-file PUTs.
|
|
7
13
|
*
|
|
8
14
|
* Why this lives in the daemon (not /api/context/* via curl):
|
|
9
15
|
* - These paths run *before* the agent is spawned. There is no
|
|
@@ -12,15 +18,16 @@
|
|
|
12
18
|
* `context-staleness.ts`) — the same tier the PATCH route already
|
|
13
19
|
* classifies for `## Agent Log` appends. Bypassing the route is
|
|
14
20
|
* fine because we never touch the prompt-context-changed hook here.
|
|
15
|
-
* - The today-write-lock invariant is preserved:
|
|
16
|
-
* mutating the file, so morning_routine and direct writes
|
|
17
|
-
* interleave.
|
|
21
|
+
* - The today-write-lock invariant is preserved: both functions acquire
|
|
22
|
+
* it before mutating the file, so morning_routine and direct writes
|
|
23
|
+
* never interleave.
|
|
18
24
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
25
|
+
* Synthesis boundary: `appendAgentLogLine` NEVER synthesizes structure —
|
|
26
|
+
* a missing / malformed / heading-less file returns false and the gate
|
|
27
|
+
* caller proceeds. `ensureTodaySkeleton` synthesizes ONLY the empty
|
|
28
|
+
* skeleton, ONLY when the file is entirely absent, and never touches a
|
|
29
|
+
* present file. Neither populates today.md — full creation and repair stay
|
|
30
|
+
* the morning routine's job.
|
|
24
31
|
*/
|
|
25
32
|
import type { TodayWriteLockManager } from "./today-write-lock.js";
|
|
26
33
|
export interface AppendAgentLogLineInput {
|
|
@@ -28,7 +35,7 @@ export interface AppendAgentLogLineInput {
|
|
|
28
35
|
/**
|
|
29
36
|
* Bullet text to append, **without** the leading `- ` prefix and
|
|
30
37
|
* without a trailing newline. The writer normalizes both.
|
|
31
|
-
* Example: `"12:00 [
|
|
38
|
+
* Example: `"12:00 [activity_scan] Quiet — 0 obs"`.
|
|
32
39
|
*/
|
|
33
40
|
message: string;
|
|
34
41
|
todayWriteLock: TodayWriteLockManager;
|
|
@@ -61,6 +68,45 @@ export interface AppendAgentLogLineResult {
|
|
|
61
68
|
* writers cleanly.
|
|
62
69
|
*/
|
|
63
70
|
export declare function appendAgentLogLine(input: AppendAgentLogLineInput): Promise<AppendAgentLogLineResult>;
|
|
71
|
+
export interface EnsureTodaySkeletonInput {
|
|
72
|
+
contextDir: string;
|
|
73
|
+
todayWriteLock: TodayWriteLockManager;
|
|
74
|
+
}
|
|
75
|
+
export interface EnsureTodaySkeletonResult {
|
|
76
|
+
seeded: boolean;
|
|
77
|
+
reason?: "already_present" | "lock_unavailable" | "io_error";
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Guarantee a `today.md` working surface exists before a section-only
|
|
81
|
+
* refresh routine (`routine.today_refresh`) assumes it.
|
|
82
|
+
*
|
|
83
|
+
* `rotateDayFiles()` intentionally renames `today.md` → `yesterday.md` at
|
|
84
|
+
* the day boundary and relies on the morning routine to recreate the
|
|
85
|
+
* dated file. When the morning routine has not run yet — or failed (e.g.
|
|
86
|
+
* a quota/budget death with no fallback backend) — `today.md` is absent
|
|
87
|
+
* and the refresh task flow's `PATCH section=user_schedule` 404s. The
|
|
88
|
+
* agent then improvises full-file `PUT`s, which the strict
|
|
89
|
+
* `validateTodayContent` schema rejects line-by-line; on a single-backend
|
|
90
|
+
* binding with a tight per-turn budget that loop tips into
|
|
91
|
+
* `BackendQuotaError(max_budget_usd)` and the refresh dies without ever
|
|
92
|
+
* writing the file — the "Refresh Today does nothing" symptom.
|
|
93
|
+
*
|
|
94
|
+
* This deterministic pre-step removes that whole failure mode: when the
|
|
95
|
+
* file is **entirely absent** we seed the canonical empty skeleton so the
|
|
96
|
+
* agent's section PATCH always has a valid target. A file that already
|
|
97
|
+
* exists is left byte-untouched — a valid dated file OR the legacy
|
|
98
|
+
* `# Today` bridge stub both accept the section PATCH (the route's
|
|
99
|
+
* `allowLegacyToday` branch). We never repair a malformed-but-present
|
|
100
|
+
* file and never overwrite user content; full creation/repair stays the
|
|
101
|
+
* morning routine's job. The seeded skeleton is dateless (`# Today`), so
|
|
102
|
+
* it does NOT satisfy `hasCurrentAgentDayTodayMd()` and the pending
|
|
103
|
+
* morning-routine retry still fires and upgrades it.
|
|
104
|
+
*
|
|
105
|
+
* Lock-aware exactly like {@link appendAgentLogLine}: if the morning
|
|
106
|
+
* routine holds the today-write-lock (mid-creation) we skip and let it
|
|
107
|
+
* win — the refresh session then 409-defers on its own PATCH.
|
|
108
|
+
*/
|
|
109
|
+
export declare function ensureTodaySkeleton(input: EnsureTodaySkeletonInput): Promise<EnsureTodaySkeletonResult>;
|
|
64
110
|
/**
|
|
65
111
|
* Splice a new bullet line into the `## Agent Log` section, immediately
|
|
66
112
|
* before the next `## ` heading or end-of-file. Returns null when the
|