@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
|
@@ -127,7 +127,7 @@ export const EVENT_SKILL_SETS = {
|
|
|
127
127
|
"roadmap",
|
|
128
128
|
"management-policy",
|
|
129
129
|
],
|
|
130
|
-
"routine.
|
|
130
|
+
"routine.activity_scan": [
|
|
131
131
|
"context",
|
|
132
132
|
"today",
|
|
133
133
|
"observations",
|
|
@@ -260,6 +260,14 @@ export const EVENT_SKILL_SETS = {
|
|
|
260
260
|
// (no prior research offer to respond to) — that exclusion does
|
|
261
261
|
// not generalise.
|
|
262
262
|
"browser-task",
|
|
263
|
+
// BACKGROUND_TASK_RUNNER_DESIGN.md §5 / Phase 3 — DM-driven entry
|
|
264
|
+
// point to hand a long-running / open-ended task to the detached
|
|
265
|
+
// background runner. Loaded for the first DM too: the user's literal
|
|
266
|
+
// first message ("research X for me", "audit my repos") is exactly
|
|
267
|
+
// the spawn surface. The `background-task-reply` clarify skill is
|
|
268
|
+
// correctly absent here — no prior task can be parked on the very
|
|
269
|
+
// first DM of a session — exactly as `browser-history-respond` is.
|
|
270
|
+
"background-task",
|
|
263
271
|
],
|
|
264
272
|
"message.received.dm": [
|
|
265
273
|
"context",
|
|
@@ -305,6 +313,20 @@ export const EVENT_SKILL_SETS = {
|
|
|
305
313
|
// the agent simply does not call its endpoints when the user's
|
|
306
314
|
// request doesn't ask for a browser task.
|
|
307
315
|
"browser-task",
|
|
316
|
+
// BACKGROUND_TASK_RUNNER_DESIGN.md §5 / Phase 3 — the generic
|
|
317
|
+
// long-running-task surface. `background-task` teaches the DM agent
|
|
318
|
+
// to compose a self-contained brief, set the notification policy,
|
|
319
|
+
// POST + ack + end the turn, and read `GET /:id` for follow-up
|
|
320
|
+
// detail. `background-task-reply` is the narrow clarify-relay (mirror
|
|
321
|
+
// of `browser-history-respond`) that translates the owner's answer to
|
|
322
|
+
// a parked task's question into `POST /:id/clarify`. Loaded for
|
|
323
|
+
// `message.received.dm` (which `message.dm` + `dashboard.chat`
|
|
324
|
+
// inherit via PROCESS_TO_EVENT_TYPE). The reply skill is NOT on
|
|
325
|
+
// `message.received.dm_first` (no task can be parked on a session's
|
|
326
|
+
// first DM). No conditional gate: both skill bodies no-op when their
|
|
327
|
+
// endpoints have nothing to act on, and the in-context cost is small.
|
|
328
|
+
"background-task",
|
|
329
|
+
"background-task-reply",
|
|
308
330
|
],
|
|
309
331
|
// ── Task events ──
|
|
310
332
|
"schedule.approaching": [
|
|
@@ -564,6 +586,13 @@ export const ALL_SKILLS = [
|
|
|
564
586
|
// `browser-history-managed` skill that fronted the frozen workflow
|
|
565
587
|
// registry was retired alongside the route + routines in Phase 6.
|
|
566
588
|
"browser-task",
|
|
589
|
+
// BACKGROUND_TASK_RUNNER_DESIGN.md §5 / Phase 3 — generic detached
|
|
590
|
+
// long-task surface. `background-task` (spawn) loads on
|
|
591
|
+
// `message.received.dm` + `message.received.dm_first`;
|
|
592
|
+
// `background-task-reply` (clarify relay) loads on `message.received.dm`
|
|
593
|
+
// only. Both are DM-agent-exclusive, like browser-task.
|
|
594
|
+
"background-task",
|
|
595
|
+
"background-task-reply",
|
|
567
596
|
// Scheduling split — `/schedule` is one-shot only; recurring tasks are
|
|
568
597
|
// Agents. This skill teaches the DM agent to author a detailed recurring
|
|
569
598
|
// Agent (`POST /api/agents`) when the user asks for an ongoing cadence.
|
|
@@ -579,7 +608,7 @@ const PROCESS_TO_EVENT_TYPE = {
|
|
|
579
608
|
"routine.evening_review": "routine.evening_review",
|
|
580
609
|
"routine.weekly_review": "routine.weekly_review",
|
|
581
610
|
"routine.monthly_review": "routine.monthly_review",
|
|
582
|
-
"routine.
|
|
611
|
+
"routine.activity_scan": "routine.activity_scan",
|
|
583
612
|
"routine.roadmap_refresh": "routine.roadmap_refresh",
|
|
584
613
|
"routine.today_refresh": "routine.today_refresh",
|
|
585
614
|
"routine.user_profile_sweep": "routine.user_profile_sweep",
|
|
@@ -887,6 +916,48 @@ export function resolveSkillManifest(eventType, opts) {
|
|
|
887
916
|
export function resolveSkillManifestForProcess(processKey, opts) {
|
|
888
917
|
return resolveSkillManifest(PROCESS_TO_EVENT_TYPE[processKey] ?? processKey, opts);
|
|
889
918
|
}
|
|
919
|
+
/**
|
|
920
|
+
* Event-type / process-key families whose sessions do NOT receive the
|
|
921
|
+
* owner's user-authored skill library (`<contextDir>/policies/skills/`).
|
|
922
|
+
*
|
|
923
|
+
* User skills are general assistant extensions the owner writes for their
|
|
924
|
+
* conversational agent; `syncAllUserSkills` provisions them into a session
|
|
925
|
+
* workdir ON TOP of the process key's built-in manifest. Narrow-persona
|
|
926
|
+
* sessions run under dedicated profiles with tight, purpose-built manifests
|
|
927
|
+
* — dumping the full user-skill library into them is pure cold-start cost +
|
|
928
|
+
* attention dilution and never load-bearing (their work is fully covered by
|
|
929
|
+
* the built-in bundle, and the wiki/research skills are themselves
|
|
930
|
+
* built-ins, not user-authored).
|
|
931
|
+
*
|
|
932
|
+
* Excluded families:
|
|
933
|
+
* - `wiki.*` — the wiki-agent persona (ingest_url / compile /
|
|
934
|
+
* ask / lint / trace / connect).
|
|
935
|
+
* - `routine.research_*` — the browser-history research routines
|
|
936
|
+
* (cluster_update / offer_dm / dispatch /
|
|
937
|
+
* wiki_summary).
|
|
938
|
+
*
|
|
939
|
+
* Everything else (the conversational DM agent, DM-tone scheduled sessions,
|
|
940
|
+
* the daily/weekly/monthly routines, setup, knowledge import, git/github
|
|
941
|
+
* observers, scheduled tasks) keeps the prior "every user skill, every
|
|
942
|
+
* session" behaviour.
|
|
943
|
+
*/
|
|
944
|
+
export const USER_SKILL_EXCLUDED_PREFIXES = [
|
|
945
|
+
"wiki.",
|
|
946
|
+
"routine.research_",
|
|
947
|
+
];
|
|
948
|
+
/**
|
|
949
|
+
* Whether a session for `eventTypeOrProcessKey` should be provisioned with
|
|
950
|
+
* the owner's user-authored skills. Keyed on the SAME string the skill
|
|
951
|
+
* manifest resolves against (`processKey ?? eventType`) so the gate and the
|
|
952
|
+
* built-in manifest agree on what a session is.
|
|
953
|
+
*
|
|
954
|
+
* Gates the `syncAllUserSkills` call at every materialisation chokepoint
|
|
955
|
+
* (`createSessionWorkdir`, `ensureSessionWorkdir`, and the backend-fallback
|
|
956
|
+
* re-materialiser). See `USER_SKILL_EXCLUDED_PREFIXES`.
|
|
957
|
+
*/
|
|
958
|
+
export function eventTypeAcceptsUserSkills(eventTypeOrProcessKey) {
|
|
959
|
+
return !USER_SKILL_EXCLUDED_PREFIXES.some((prefix) => eventTypeOrProcessKey.startsWith(prefix));
|
|
960
|
+
}
|
|
890
961
|
export function getProfileForEvent(eventType) {
|
|
891
962
|
for (const rule of PROFILE_RULES) {
|
|
892
963
|
if (rule.exact ? eventType === rule.prefix : eventType.startsWith(rule.prefix)) {
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keep-awake posture while the daemon runs (`preventSleepMode` setting):
|
|
3
|
+
*
|
|
4
|
+
* - `"off"` — never inhibit sleep.
|
|
5
|
+
* - `"ac"` — inhibit system sleep only while on AC power
|
|
6
|
+
* (macOS `caffeinate -s`; battery drain stays impossible).
|
|
7
|
+
* - `"always"` — additionally inhibit idle sleep on battery
|
|
8
|
+
* (macOS `caffeinate -i -s`).
|
|
9
|
+
*
|
|
10
|
+
* Why this exists: every timer in the daemon (node-cron, WakeDetector,
|
|
11
|
+
* in-flight agent sessions) freezes while the host sleeps. A sleeping
|
|
12
|
+
* laptop turns the 04:00 day-boundary flow into hours of wake-catchup
|
|
13
|
+
* replays riding macOS maintenance DarkWakes — each replay re-fires
|
|
14
|
+
* day-boundary work in a 1–2 minute window before the machine re-sleeps,
|
|
15
|
+
* and cold prompt caches make those runs 10× the normal cost. Keeping
|
|
16
|
+
* the machine awake while plugged in removes that failure mode at the
|
|
17
|
+
* source; server installs (which never sleep) are unaffected.
|
|
18
|
+
*
|
|
19
|
+
* macOS only. `caffeinate` cannot override lid-close (clamshell) sleep —
|
|
20
|
+
* that needs root (`pmset disablesleep 1`), which the daemon must not
|
|
21
|
+
* touch. Windows/Linux hosts running this daemon are assumed to be
|
|
22
|
+
* servers or desktops with OS-level power management; the inhibitor is a
|
|
23
|
+
* no-op there and logs once at debug level.
|
|
24
|
+
*/
|
|
25
|
+
export declare const PREVENT_SLEEP_MODES: readonly ["off", "ac", "always"];
|
|
26
|
+
export type PreventSleepMode = (typeof PREVENT_SLEEP_MODES)[number];
|
|
27
|
+
/** Respawns allowed after an unexpected `caffeinate` exit before giving up. */
|
|
28
|
+
export declare const SLEEP_INHIBITOR_MAX_RESPAWNS = 3;
|
|
29
|
+
/** Pause before a respawn so a persistently-dying binary cannot tight-loop. */
|
|
30
|
+
export declare const SLEEP_INHIBITOR_RESPAWN_DELAY_MS = 5000;
|
|
31
|
+
export interface SleepInhibitCommand {
|
|
32
|
+
command: string;
|
|
33
|
+
args: string[];
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Pure command resolver — `null` means "do not inhibit" (mode off or
|
|
37
|
+
* unsupported platform). `-w <pid>` ties the power assertion to the
|
|
38
|
+
* daemon process itself, so the assertion is released even if the daemon
|
|
39
|
+
* is SIGKILLed and `stop()` never runs.
|
|
40
|
+
*/
|
|
41
|
+
export declare function resolveSleepInhibitCommand(platform: NodeJS.Platform, mode: PreventSleepMode, pid: number): SleepInhibitCommand | null;
|
|
42
|
+
export interface SleepInhibitorChild {
|
|
43
|
+
on(event: "error", listener: (err: Error) => void): unknown;
|
|
44
|
+
on(event: "exit", listener: (code: number | null, signal: NodeJS.Signals | null) => void): unknown;
|
|
45
|
+
kill(signal?: NodeJS.Signals): boolean;
|
|
46
|
+
unref(): void;
|
|
47
|
+
pid?: number;
|
|
48
|
+
}
|
|
49
|
+
export type SleepInhibitorSpawn = (command: string, args: readonly string[]) => SleepInhibitorChild;
|
|
50
|
+
export interface SleepInhibitorOptions {
|
|
51
|
+
mode: PreventSleepMode;
|
|
52
|
+
/** Injectable for tests; defaults to the host platform. */
|
|
53
|
+
platform?: NodeJS.Platform;
|
|
54
|
+
/** Injectable for tests; defaults to the daemon's own pid. */
|
|
55
|
+
pid?: number;
|
|
56
|
+
spawnFn?: SleepInhibitorSpawn;
|
|
57
|
+
respawnDelayMs?: number;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Holds a `caffeinate` child for the daemon's lifetime. Crash-safe by
|
|
61
|
+
* construction (`-w pid` above); `stop()` exists for symmetric shutdown
|
|
62
|
+
* and to suppress the respawn path during graceful exit.
|
|
63
|
+
*/
|
|
64
|
+
export declare class SleepInhibitor {
|
|
65
|
+
private readonly mode;
|
|
66
|
+
private readonly platform;
|
|
67
|
+
private readonly pid;
|
|
68
|
+
private readonly spawnFn;
|
|
69
|
+
private readonly respawnDelayMs;
|
|
70
|
+
private child;
|
|
71
|
+
private respawnTimer;
|
|
72
|
+
private respawns;
|
|
73
|
+
private started;
|
|
74
|
+
private stopped;
|
|
75
|
+
constructor(options: SleepInhibitorOptions);
|
|
76
|
+
start(): void;
|
|
77
|
+
stop(): void;
|
|
78
|
+
private spawnChild;
|
|
79
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { createLogger } from "../logging.js";
|
|
3
|
+
const logger = createLogger("sleep-inhibitor");
|
|
4
|
+
/**
|
|
5
|
+
* Keep-awake posture while the daemon runs (`preventSleepMode` setting):
|
|
6
|
+
*
|
|
7
|
+
* - `"off"` — never inhibit sleep.
|
|
8
|
+
* - `"ac"` — inhibit system sleep only while on AC power
|
|
9
|
+
* (macOS `caffeinate -s`; battery drain stays impossible).
|
|
10
|
+
* - `"always"` — additionally inhibit idle sleep on battery
|
|
11
|
+
* (macOS `caffeinate -i -s`).
|
|
12
|
+
*
|
|
13
|
+
* Why this exists: every timer in the daemon (node-cron, WakeDetector,
|
|
14
|
+
* in-flight agent sessions) freezes while the host sleeps. A sleeping
|
|
15
|
+
* laptop turns the 04:00 day-boundary flow into hours of wake-catchup
|
|
16
|
+
* replays riding macOS maintenance DarkWakes — each replay re-fires
|
|
17
|
+
* day-boundary work in a 1–2 minute window before the machine re-sleeps,
|
|
18
|
+
* and cold prompt caches make those runs 10× the normal cost. Keeping
|
|
19
|
+
* the machine awake while plugged in removes that failure mode at the
|
|
20
|
+
* source; server installs (which never sleep) are unaffected.
|
|
21
|
+
*
|
|
22
|
+
* macOS only. `caffeinate` cannot override lid-close (clamshell) sleep —
|
|
23
|
+
* that needs root (`pmset disablesleep 1`), which the daemon must not
|
|
24
|
+
* touch. Windows/Linux hosts running this daemon are assumed to be
|
|
25
|
+
* servers or desktops with OS-level power management; the inhibitor is a
|
|
26
|
+
* no-op there and logs once at debug level.
|
|
27
|
+
*/
|
|
28
|
+
export const PREVENT_SLEEP_MODES = ["off", "ac", "always"];
|
|
29
|
+
/** Respawns allowed after an unexpected `caffeinate` exit before giving up. */
|
|
30
|
+
export const SLEEP_INHIBITOR_MAX_RESPAWNS = 3;
|
|
31
|
+
/** Pause before a respawn so a persistently-dying binary cannot tight-loop. */
|
|
32
|
+
export const SLEEP_INHIBITOR_RESPAWN_DELAY_MS = 5_000;
|
|
33
|
+
/**
|
|
34
|
+
* Pure command resolver — `null` means "do not inhibit" (mode off or
|
|
35
|
+
* unsupported platform). `-w <pid>` ties the power assertion to the
|
|
36
|
+
* daemon process itself, so the assertion is released even if the daemon
|
|
37
|
+
* is SIGKILLed and `stop()` never runs.
|
|
38
|
+
*/
|
|
39
|
+
export function resolveSleepInhibitCommand(platform, mode, pid) {
|
|
40
|
+
if (mode === "off")
|
|
41
|
+
return null;
|
|
42
|
+
if (platform !== "darwin")
|
|
43
|
+
return null;
|
|
44
|
+
const flags = mode === "always" ? ["-i", "-s"] : ["-s"];
|
|
45
|
+
return { command: "caffeinate", args: [...flags, "-w", String(pid)] };
|
|
46
|
+
}
|
|
47
|
+
const defaultSpawn = (command, args) => spawn(command, args, { stdio: "ignore" });
|
|
48
|
+
/**
|
|
49
|
+
* Holds a `caffeinate` child for the daemon's lifetime. Crash-safe by
|
|
50
|
+
* construction (`-w pid` above); `stop()` exists for symmetric shutdown
|
|
51
|
+
* and to suppress the respawn path during graceful exit.
|
|
52
|
+
*/
|
|
53
|
+
export class SleepInhibitor {
|
|
54
|
+
mode;
|
|
55
|
+
platform;
|
|
56
|
+
pid;
|
|
57
|
+
spawnFn;
|
|
58
|
+
respawnDelayMs;
|
|
59
|
+
child = null;
|
|
60
|
+
respawnTimer = null;
|
|
61
|
+
respawns = 0;
|
|
62
|
+
started = false;
|
|
63
|
+
stopped = false;
|
|
64
|
+
constructor(options) {
|
|
65
|
+
this.mode = options.mode;
|
|
66
|
+
this.platform = options.platform ?? process.platform;
|
|
67
|
+
this.pid = options.pid ?? process.pid;
|
|
68
|
+
this.spawnFn = options.spawnFn ?? defaultSpawn;
|
|
69
|
+
this.respawnDelayMs =
|
|
70
|
+
options.respawnDelayMs ?? SLEEP_INHIBITOR_RESPAWN_DELAY_MS;
|
|
71
|
+
}
|
|
72
|
+
start() {
|
|
73
|
+
if (this.started)
|
|
74
|
+
return;
|
|
75
|
+
this.started = true;
|
|
76
|
+
const cmd = resolveSleepInhibitCommand(this.platform, this.mode, this.pid);
|
|
77
|
+
if (!cmd) {
|
|
78
|
+
logger.debug({ mode: this.mode, platform: this.platform }, "Sleep inhibitor inactive (mode off or unsupported platform)");
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
this.spawnChild(cmd);
|
|
82
|
+
}
|
|
83
|
+
stop() {
|
|
84
|
+
this.stopped = true;
|
|
85
|
+
if (this.respawnTimer) {
|
|
86
|
+
clearTimeout(this.respawnTimer);
|
|
87
|
+
this.respawnTimer = null;
|
|
88
|
+
}
|
|
89
|
+
// Kill but leave `this.child` set — the exit handler clears it and the
|
|
90
|
+
// `stopped` flag suppresses the respawn path.
|
|
91
|
+
this.child?.kill("SIGTERM");
|
|
92
|
+
}
|
|
93
|
+
spawnChild(cmd) {
|
|
94
|
+
let child;
|
|
95
|
+
try {
|
|
96
|
+
child = this.spawnFn(cmd.command, cmd.args);
|
|
97
|
+
}
|
|
98
|
+
catch (err) {
|
|
99
|
+
logger.warn({ err, command: cmd.command }, "Sleep inhibitor spawn failed — system sleep stays OS-managed");
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
this.child = child;
|
|
103
|
+
// The inhibitor must never keep the event loop alive on its own.
|
|
104
|
+
child.unref();
|
|
105
|
+
child.on("error", (err) => {
|
|
106
|
+
// ENOENT and friends — the binary is missing or unrunnable, which a
|
|
107
|
+
// respawn cannot fix. Warn once and fall back to OS-managed sleep.
|
|
108
|
+
this.child = null;
|
|
109
|
+
logger.warn({ err, command: cmd.command }, "Sleep inhibitor process errored — system sleep stays OS-managed");
|
|
110
|
+
});
|
|
111
|
+
child.on("exit", (code, signal) => {
|
|
112
|
+
if (this.child !== child)
|
|
113
|
+
return; // killed by stop() or replaced
|
|
114
|
+
this.child = null;
|
|
115
|
+
if (this.stopped)
|
|
116
|
+
return;
|
|
117
|
+
if (this.respawns >= SLEEP_INHIBITOR_MAX_RESPAWNS) {
|
|
118
|
+
logger.error({ code, signal, respawns: this.respawns }, "Sleep inhibitor exited repeatedly — giving up; system sleep stays OS-managed");
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
this.respawns += 1;
|
|
122
|
+
logger.warn({ code, signal, attempt: this.respawns }, "Sleep inhibitor exited unexpectedly — respawning");
|
|
123
|
+
this.respawnTimer = setTimeout(() => {
|
|
124
|
+
this.respawnTimer = null;
|
|
125
|
+
if (!this.stopped)
|
|
126
|
+
this.spawnChild(cmd);
|
|
127
|
+
}, this.respawnDelayMs);
|
|
128
|
+
this.respawnTimer.unref?.();
|
|
129
|
+
});
|
|
130
|
+
logger.info({ command: cmd.command, args: cmd.args, mode: this.mode }, "Sleep inhibitor active");
|
|
131
|
+
}
|
|
132
|
+
}
|
|
@@ -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
|
+
}
|