@aitne/daemon 0.1.10 → 0.1.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/adapter-watchdog.d.ts +70 -0
- package/dist/adapters/adapter-watchdog.js +115 -0
- package/dist/adapters/discord.d.ts +17 -1
- package/dist/adapters/discord.js +33 -0
- package/dist/adapters/notification-manager.d.ts +27 -1
- package/dist/adapters/notification-manager.js +54 -39
- package/dist/adapters/slack-adapter.d.ts +26 -1
- package/dist/adapters/slack-adapter.js +41 -0
- package/dist/adapters/telegram-adapter.d.ts +18 -1
- package/dist/adapters/telegram-adapter.js +41 -2
- package/dist/adapters/types.d.ts +20 -0
- package/dist/adapters/whatsapp-adapter.d.ts +26 -7
- package/dist/adapters/whatsapp-adapter.js +74 -21
- package/dist/api/env-writer.js +8 -5
- package/dist/api/helpers/agent-errors-registry.d.ts +5 -5
- package/dist/api/helpers/agent-errors-registry.js +5 -5
- package/dist/api/routes/agent.js +33 -12
- package/dist/api/routes/agents/index.js +75 -16
- package/dist/api/routes/agents/views.d.ts +37 -2
- package/dist/api/routes/agents/views.js +64 -2
- package/dist/api/routes/background-task.d.ts +22 -0
- package/dist/api/routes/background-task.js +338 -0
- package/dist/api/routes/browser-history.js +9 -1
- package/dist/api/routes/context/permissions.js +3 -2
- package/dist/api/routes/context/snapshots.js +0 -3
- package/dist/api/routes/context/write.js +3 -17
- package/dist/api/routes/dashboard/config.js +48 -12
- package/dist/api/routes/dashboard/cost-approvals.js +66 -0
- package/dist/api/routes/dashboard/notifications.js +9 -9
- package/dist/api/routes/integrations/crud-patch.js +5 -1
- package/dist/api/routes/integrations-reconcile.js +2 -2
- package/dist/api/routes/notion.d.ts +1 -1
- package/dist/api/routes/observations.js +7 -7
- package/dist/api/routes/obsidian.d.ts +1 -1
- package/dist/api/routes/receipts.js +5 -1
- package/dist/api/routes/setup-migrate.js +1 -1
- package/dist/api/routes/setup.js +1 -1
- package/dist/api/routes/task-flows.d.ts +1 -1
- package/dist/api/routes/task-flows.js +1 -1
- package/dist/api/routes/tuning.d.ts +29 -0
- package/dist/api/routes/tuning.js +304 -0
- package/dist/api/server.d.ts +44 -16
- package/dist/api/server.js +9 -0
- package/dist/bootstrap/adapters.d.ts +19 -0
- package/dist/bootstrap/adapters.js +61 -0
- package/dist/bootstrap/api.d.ts +5 -3
- package/dist/bootstrap/api.js +45 -13
- package/dist/bootstrap/catchup.d.ts +1 -1
- package/dist/bootstrap/catchup.js +11 -11
- package/dist/bootstrap/event-pipeline.d.ts +11 -0
- package/dist/bootstrap/event-pipeline.js +245 -7
- package/dist/bootstrap/observers.js +9 -6
- package/dist/bootstrap/schedule-helpers.d.ts +104 -6
- package/dist/bootstrap/schedule-helpers.js +172 -19
- package/dist/config.js +26 -12
- package/dist/core/agent-core.d.ts +33 -1
- package/dist/core/agent-core.js +36 -1
- package/dist/core/agents/activity-scan-cadence.d.ts +103 -0
- package/dist/core/agents/activity-scan-cadence.js +127 -0
- package/dist/core/agents/agent-route-override.d.ts +53 -0
- package/dist/core/agents/agent-route-override.js +69 -0
- package/dist/core/agents/builtin-registry.d.ts +51 -14
- package/dist/core/agents/builtin-registry.js +92 -15
- package/dist/core/agents/config-gate-reconcile.d.ts +38 -0
- package/dist/core/agents/config-gate-reconcile.js +51 -0
- package/dist/core/agents/cron-substitute.d.ts +1 -1
- package/dist/core/agents/cron-substitute.js +1 -1
- package/dist/core/agents/custom-routine-migration.d.ts +60 -0
- package/dist/core/agents/custom-routine-migration.js +149 -0
- package/dist/core/agents/firing-blocked.d.ts +1 -1
- package/dist/core/agents/hourly-cadence.d.ts +102 -0
- package/dist/core/agents/hourly-cadence.js +126 -0
- package/dist/core/agents/loader-boot.js +23 -0
- package/dist/core/agents/loader.d.ts +19 -0
- package/dist/core/agents/loader.js +34 -2
- package/dist/core/agents/override-merge.d.ts +1 -1
- package/dist/core/agents/override-merge.js +9 -1
- package/dist/core/agents/recurrence-convert.d.ts +1 -1
- package/dist/core/agents/recurrence-convert.js +1 -1
- package/dist/core/agents/recurring-schedule-adapter.js +8 -0
- package/dist/core/alerts.js +6 -6
- package/dist/core/backends/auth-health-monitor.d.ts +2 -2
- package/dist/core/backends/auth-health-monitor.js +1 -1
- package/dist/core/backends/backend-router.d.ts +27 -1
- package/dist/core/backends/backend-router.js +165 -1
- package/dist/core/backends/claude-code-core.d.ts +71 -31
- package/dist/core/backends/claude-code-core.js +282 -54
- package/dist/core/backends/cli-quota-guards.d.ts +29 -1
- package/dist/core/backends/cli-quota-guards.js +40 -5
- package/dist/core/backends/codex-core.d.ts +6 -0
- package/dist/core/backends/codex-core.js +22 -6
- package/dist/core/backends/failure-spend.d.ts +58 -0
- package/dist/core/backends/failure-spend.js +137 -0
- package/dist/core/backends/gemini-cli-core.d.ts +6 -0
- package/dist/core/backends/gemini-cli-core.js +25 -6
- package/dist/core/backends/model-registry.d.ts +1 -1
- package/dist/core/backends/model-registry.js +4 -4
- package/dist/core/backends/opencode-core.d.ts +1 -1
- package/dist/core/backends/opencode-core.js +5 -5
- package/dist/core/backends/plan-presets.js +39 -15
- package/dist/core/bang-commands/commands-cost.js +3 -1
- package/dist/core/bang-commands/commands-report.js +4 -3
- package/dist/core/bang-commands/commands-research.js +4 -1
- package/dist/core/bang-commands/commands-revert-tuning.d.ts +18 -0
- package/dist/core/bang-commands/commands-revert-tuning.js +63 -0
- package/dist/core/bang-commands/commands-stop-start.js +3 -3
- package/dist/core/bang-commands/commands-task-control.d.ts +19 -0
- package/dist/core/bang-commands/commands-task-control.js +147 -0
- package/dist/core/bang-commands/commands-wiki.js +5 -5
- package/dist/core/bang-commands/index.d.ts +2 -0
- package/dist/core/bang-commands/index.js +12 -0
- package/dist/core/bang-commands/registry.d.ts +12 -0
- package/dist/core/browser-history/research-cluster-fanout.d.ts +28 -14
- package/dist/core/browser-history/research-cluster-fanout.js +39 -16
- package/dist/core/channel-timeline.d.ts +5 -1
- package/dist/core/channel-timeline.js +13 -0
- package/dist/core/context/index-reconciler.js +5 -2
- package/dist/core/context/policy-index-reconciler.d.ts +6 -4
- package/dist/core/context/policy-index-runner.js +25 -6
- package/dist/core/context-builder-calendar.js +10 -2
- package/dist/core/context-builder-conversation.d.ts +8 -1
- package/dist/core/context-builder-conversation.js +41 -7
- package/dist/core/context-builder-yesterday.js +4 -3
- package/dist/core/context-builder.d.ts +7 -2
- package/dist/core/context-builder.js +62 -20
- package/dist/core/context-file-serializer.d.ts +1 -1
- package/dist/core/context-file-serializer.js +1 -1
- package/dist/core/context-health.js +2 -2
- package/dist/core/context-paths.d.ts +1 -1
- package/dist/core/context-paths.js +1 -1
- package/dist/core/context-validation/prepare-write.js +1 -1
- package/dist/core/context-validation/routine-rulebook.d.ts +1 -1
- package/dist/core/context-vault-aliases.d.ts +0 -13
- package/dist/core/context-vault-aliases.js +37 -0
- package/dist/core/custom-routines.d.ts +99 -0
- package/dist/core/custom-routines.js +187 -0
- package/dist/core/daemon-api-cli.js +49 -0
- package/dist/core/day-boundary.d.ts +46 -0
- package/dist/core/day-boundary.js +40 -0
- package/dist/core/dispatcher-activity-scan.d.ts +221 -0
- package/dist/core/dispatcher-activity-scan.js +775 -0
- package/dist/core/dispatcher-error-handling.d.ts +6 -11
- package/dist/core/dispatcher-error-handling.js +38 -62
- package/dist/core/dispatcher-hourly-check.js +6 -1
- package/dist/core/dispatcher-message-handler.d.ts +10 -0
- package/dist/core/dispatcher-message-handler.js +17 -0
- package/dist/core/dispatcher-morning-routine.d.ts +6 -6
- package/dist/core/dispatcher-morning-routine.js +13 -13
- package/dist/core/dispatcher-result-processor.d.ts +33 -0
- package/dist/core/dispatcher-result-processor.js +167 -11
- package/dist/core/dispatcher-scheduled-background-task.d.ts +42 -0
- package/dist/core/dispatcher-scheduled-background-task.js +89 -0
- package/dist/core/dispatcher-scheduled-tasks.d.ts +63 -1
- package/dist/core/dispatcher-scheduled-tasks.js +213 -6
- package/dist/core/dispatcher-task-delivery.d.ts +105 -0
- package/dist/core/dispatcher-task-delivery.js +555 -0
- package/dist/core/dispatcher-types.d.ts +48 -9
- package/dist/core/dispatcher-types.js +3 -3
- package/dist/core/dispatcher.d.ts +112 -31
- package/dist/core/dispatcher.js +284 -59
- package/dist/core/dm-freshness-metrics.d.ts +1 -1
- package/dist/core/drift-effects.js +2 -2
- package/dist/core/feedback/consolidation-prep.js +17 -5
- package/dist/core/feedback/eviction-scorer.js +6 -2
- package/dist/core/feedback/lesson-format.js +9 -4
- package/dist/core/feedback/lesson-injection.d.ts +1 -1
- package/dist/core/feedback/lesson-injection.js +17 -2
- package/dist/core/feedback/lesson-store-overview.d.ts +8 -4
- package/dist/core/feedback/lesson-store-overview.js +8 -4
- package/dist/core/feedback/regeneralization-prep.js +29 -16
- package/dist/core/feedback/self-performance-prep.d.ts +186 -0
- package/dist/core/feedback/self-performance-prep.js +541 -0
- package/dist/core/feedback/tuning-actuator.d.ts +198 -0
- package/dist/core/feedback/tuning-actuator.js +432 -0
- package/dist/core/feedback/tuning-recommender.d.ts +247 -0
- package/dist/core/feedback/tuning-recommender.js +580 -0
- package/dist/core/feedback/tuning-revert-monitor.d.ts +90 -0
- package/dist/core/feedback/tuning-revert-monitor.js +213 -0
- package/dist/core/health-monitor.d.ts +6 -0
- package/dist/core/health-monitor.js +1 -1
- package/dist/core/injection-policy.d.ts +4 -4
- package/dist/core/injection-policy.js +4 -4
- package/dist/core/integration-main-backend.js +4 -0
- package/dist/core/management-md.d.ts +2 -2
- package/dist/core/management-md.js +51 -13
- package/dist/core/morning/orchestrator.d.ts +2 -2
- package/dist/core/morning/orchestrator.js +2 -2
- package/dist/core/notification-gate.d.ts +64 -0
- package/dist/core/notification-gate.js +51 -0
- package/dist/core/notification-rate-limit.d.ts +40 -0
- package/dist/core/notification-rate-limit.js +50 -0
- package/dist/core/policy-files.d.ts +1 -1
- package/dist/core/policy-files.js +2 -2
- package/dist/core/pre-pass-freshness.d.ts +4 -4
- package/dist/core/retention.d.ts +5 -0
- package/dist/core/retention.js +20 -4
- package/dist/core/review-context.d.ts +1 -1
- package/dist/core/review-context.js +10 -5
- package/dist/core/roadmap-write-lock.d.ts +2 -1
- package/dist/core/roadmap-write-lock.js +15 -10
- package/dist/core/routine-acquisition-plan.d.ts +47 -1
- package/dist/core/routine-acquisition-plan.js +78 -20
- package/dist/core/routine-fetch-window-retry.js +7 -4
- package/dist/core/routine-fetch-window-runner.d.ts +39 -3
- package/dist/core/routine-fetch-window-runner.js +264 -13
- package/dist/core/routine-windows.d.ts +2 -2
- package/dist/core/routine-windows.js +8 -5
- package/dist/core/scheduler.d.ts +175 -16
- package/dist/core/scheduler.js +559 -102
- package/dist/core/signal-detector.d.ts +12 -0
- package/dist/core/signal-detector.js +53 -9
- package/dist/core/skills-compiler-denied-tools.js +2 -2
- package/dist/core/skills-compiler-skill-index.d.ts +2 -2
- package/dist/core/skills-compiler-skill-index.js +2 -2
- package/dist/core/skills-compiler-variants.d.ts +1 -1
- package/dist/core/skills-compiler-variants.js +8 -0
- package/dist/core/skills-compiler.d.ts +29 -26
- package/dist/core/skills-compiler.js +117 -81
- package/dist/core/skills-manifest.d.ts +37 -0
- package/dist/core/skills-manifest.js +73 -2
- package/dist/core/sleep-inhibitor.d.ts +79 -0
- package/dist/core/sleep-inhibitor.js +132 -0
- package/dist/core/slim-system-prompt-loader.d.ts +77 -0
- package/dist/core/slim-system-prompt-loader.js +141 -0
- package/dist/core/spawn-gates.d.ts +126 -0
- package/dist/core/spawn-gates.js +180 -0
- package/dist/core/today-direct-writer.d.ts +2 -2
- package/dist/core/today-direct-writer.js +1 -1
- package/dist/core/today-write-lock.d.ts +4 -2
- package/dist/core/today-write-lock.js +30 -20
- package/dist/core/wake-detector.d.ts +55 -0
- package/dist/core/wake-detector.js +80 -0
- package/dist/core/wiki/compile-lock.d.ts +1 -1
- package/dist/core/wiki/compile-lock.js +1 -1
- package/dist/core/workdir.js +15 -6
- package/dist/db/activity-scan-signals.d.ts +77 -0
- package/dist/db/activity-scan-signals.js +378 -0
- package/dist/db/agents-store.d.ts +28 -0
- package/dist/db/agents-store.js +62 -0
- package/dist/db/background-task-clarifications-store.d.ts +81 -0
- package/dist/db/background-task-clarifications-store.js +152 -0
- package/dist/db/background-task-store.d.ts +207 -0
- package/dist/db/background-task-store.js +380 -0
- package/dist/db/browser-history-store.d.ts +39 -6
- package/dist/db/browser-history-store.js +51 -7
- package/dist/db/browser-task-clarifications-store.d.ts +12 -0
- package/dist/db/browser-task-clarifications-store.js +35 -5
- package/dist/db/browser-task-store.d.ts +3 -0
- package/dist/db/browser-task-store.js +29 -4
- package/dist/db/deferred-dm.d.ts +86 -0
- package/dist/db/deferred-dm.js +199 -0
- package/dist/db/migrations.js +330 -0
- package/dist/db/observations.d.ts +2 -2
- package/dist/db/observations.js +3 -3
- package/dist/db/schema.js +217 -16
- package/dist/db/voice-transcripts-store.d.ts +1 -1
- package/dist/index.js +86 -29
- package/dist/messaging/browser-task-mcp-notifier.d.ts +12 -70
- package/dist/messaging/browser-task-mcp-notifier.js +30 -151
- package/dist/messaging/browser-task-screenshot-attachment.d.ts +15 -0
- package/dist/messaging/browser-task-screenshot-attachment.js +63 -0
- package/dist/observers/delegated-sync-worker.d.ts +6 -6
- package/dist/observers/delegated-sync-worker.js +10 -10
- package/dist/observers/git-delegated-cron.d.ts +1 -1
- package/dist/observers/git-delegated-cron.js +2 -2
- package/dist/observers/github-poller-classifier.d.ts +3 -3
- package/dist/observers/github-poller-classifier.js +3 -3
- package/dist/observers/imminent-event-scheduler.d.ts +1 -1
- package/dist/observers/imminent-event-scheduler.js +1 -1
- package/dist/observers/mail-poller.d.ts +1 -0
- package/dist/observers/mail-poller.js +42 -3
- package/dist/observers/observation-summarizer/summarizer-client.d.ts +2 -2
- package/dist/observers/observation-summarizer/summarizer-client.js +2 -2
- package/dist/observers/observation-summarizer/worker.d.ts +2 -2
- package/dist/observers/observation-summarizer/worker.js +4 -4
- package/dist/observers/obsidian-watcher.d.ts +1 -1
- package/dist/observers/obsidian-watcher.js +1 -1
- package/dist/safety/agent-write-tracker.d.ts +4 -4
- package/dist/safety/agent-write-tracker.js +4 -4
- package/dist/safety/audit.d.ts +43 -5
- package/dist/safety/audit.js +86 -18
- package/dist/safety/risk-classifier.d.ts +6 -0
- package/dist/safety/risk-classifier.js +75 -11
- package/dist/scheduler/activity-scan-gate.d.ts +86 -0
- package/dist/scheduler/activity-scan-gate.js +132 -0
- package/dist/services/background-task/background-task-budget.d.ts +80 -0
- package/dist/services/background-task/background-task-budget.js +91 -0
- package/dist/services/background-task/background-task-driver.d.ts +105 -0
- package/dist/services/background-task/background-task-driver.js +416 -0
- package/dist/services/background-task/background-task-runner.d.ts +96 -0
- package/dist/services/background-task/background-task-runner.js +673 -0
- package/dist/services/background-task/background-task-tools.d.ts +84 -0
- package/dist/services/background-task/background-task-tools.js +247 -0
- package/dist/services/background-task/background-task-transition-events.d.ts +43 -0
- package/dist/services/background-task/background-task-transition-events.js +54 -0
- package/dist/services/browser-history/automation/egress-denylist.d.ts +1 -1
- package/dist/services/browser-history/automation/egress-denylist.js +16 -6
- package/dist/services/browser-history/managed-chromium/sandbox-launcher.js +0 -1
- package/dist/services/browser-task/browser-task-runner.js +53 -8
- package/dist/services/observations-batch.d.ts +1 -1
- package/dist/services/observations-batch.js +2 -2
- package/dist/settings/runtime-settings.d.ts +38 -11
- package/dist/settings/runtime-settings.js +203 -40
- package/dist/settings/settings-store.js +11 -3
- package/package.json +4 -4
|
@@ -0,0 +1,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
|
+
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* is no subprocess to issue the curl call from. Two operations live here:
|
|
5
5
|
*
|
|
6
6
|
* 1. {@link appendAgentLogLine} — append a single `## Agent Log` bullet.
|
|
7
|
-
* Used by the three-stage
|
|
7
|
+
* Used by the three-stage activity_scan gate (cost-reduction-structural
|
|
8
8
|
* §B) on the stage0_silent / stage2_log_only paths so a "no-op" cron
|
|
9
9
|
* tick still leaves an audit trail without paying for an LLM session.
|
|
10
10
|
* 2. {@link ensureTodaySkeleton} — seed the canonical empty skeleton when
|
|
@@ -35,7 +35,7 @@ export interface AppendAgentLogLineInput {
|
|
|
35
35
|
/**
|
|
36
36
|
* Bullet text to append, **without** the leading `- ` prefix and
|
|
37
37
|
* without a trailing newline. The writer normalizes both.
|
|
38
|
-
* Example: `"12:00 [
|
|
38
|
+
* Example: `"12:00 [activity_scan] Quiet — 0 obs"`.
|
|
39
39
|
*/
|
|
40
40
|
message: string;
|
|
41
41
|
todayWriteLock: TodayWriteLockManager;
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* is no subprocess to issue the curl call from. Two operations live here:
|
|
5
5
|
*
|
|
6
6
|
* 1. {@link appendAgentLogLine} — append a single `## Agent Log` bullet.
|
|
7
|
-
* Used by the three-stage
|
|
7
|
+
* Used by the three-stage activity_scan gate (cost-reduction-structural
|
|
8
8
|
* §B) on the stage0_silent / stage2_log_only paths so a "no-op" cron
|
|
9
9
|
* tick still leaves an audit trail without paying for an LLM session.
|
|
10
10
|
* 2. {@link ensureTodaySkeleton} — seed the canonical empty skeleton when
|
|
@@ -13,8 +13,9 @@ export interface TodayWriteLockManager {
|
|
|
13
13
|
export declare class InMemoryTodayWriteLockManager implements TodayWriteLockManager {
|
|
14
14
|
private readonly timeoutMs;
|
|
15
15
|
private holder;
|
|
16
|
-
private
|
|
16
|
+
private expiresAtMs;
|
|
17
17
|
constructor(timeoutMs: number);
|
|
18
|
+
private expireIfStale;
|
|
18
19
|
acquire(): {
|
|
19
20
|
ok: true;
|
|
20
21
|
lockId: string;
|
|
@@ -38,8 +39,9 @@ export declare function getTodayWriteLockTimeoutMs(executeTimeoutMinutes: number
|
|
|
38
39
|
export declare class MigrationLock {
|
|
39
40
|
private readonly timeoutMs;
|
|
40
41
|
private holder;
|
|
41
|
-
private
|
|
42
|
+
private expiresAtMs;
|
|
42
43
|
constructor(timeoutMs: number);
|
|
44
|
+
private expireIfStale;
|
|
43
45
|
acquire(): {
|
|
44
46
|
ok: true;
|
|
45
47
|
lockId: string;
|
|
@@ -4,44 +4,50 @@ const logger = createLogger("today-write-lock");
|
|
|
4
4
|
export class InMemoryTodayWriteLockManager {
|
|
5
5
|
timeoutMs;
|
|
6
6
|
holder = null;
|
|
7
|
-
|
|
7
|
+
expiresAtMs = 0;
|
|
8
8
|
constructor(timeoutMs) {
|
|
9
9
|
this.timeoutMs = timeoutMs;
|
|
10
10
|
}
|
|
11
|
+
// Expiry is wall-clock (Date.now) checked lazily on access, not a
|
|
12
|
+
// setTimeout: Node timers run on the monotonic clock and don't advance
|
|
13
|
+
// while the machine sleeps, so a timer armed before sleep would hold the
|
|
14
|
+
// lock up to the whole sleep duration past its intended TTL.
|
|
15
|
+
expireIfStale() {
|
|
16
|
+
if (this.holder && Date.now() >= this.expiresAtMs) {
|
|
17
|
+
logger.warn({ lockId: this.holder }, "Today write lock expired by timeout");
|
|
18
|
+
this.holder = null;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
11
21
|
acquire() {
|
|
22
|
+
this.expireIfStale();
|
|
12
23
|
if (this.holder) {
|
|
13
24
|
logger.debug({ existingHolder: this.holder }, "Lock acquire rejected — already held");
|
|
14
25
|
return { ok: false, holder: this.holder };
|
|
15
26
|
}
|
|
16
27
|
const lockId = randomUUID();
|
|
17
28
|
this.holder = lockId;
|
|
18
|
-
this.
|
|
19
|
-
logger.warn({ lockId: this.holder }, "Today write lock expired by timeout");
|
|
20
|
-
this.holder = null;
|
|
21
|
-
this.timer = null;
|
|
22
|
-
}, this.timeoutMs);
|
|
29
|
+
this.expiresAtMs = Date.now() + this.timeoutMs;
|
|
23
30
|
logger.debug({ lockId }, "Today write lock acquired");
|
|
24
31
|
return { ok: true, lockId };
|
|
25
32
|
}
|
|
26
33
|
release(lockId) {
|
|
34
|
+
this.expireIfStale();
|
|
27
35
|
if (!this.holder || this.holder !== lockId) {
|
|
28
36
|
return false;
|
|
29
37
|
}
|
|
30
38
|
this.holder = null;
|
|
31
|
-
if (this.timer) {
|
|
32
|
-
clearTimeout(this.timer);
|
|
33
|
-
this.timer = null;
|
|
34
|
-
}
|
|
35
39
|
logger.debug({ lockId }, "Today write lock released");
|
|
36
40
|
return true;
|
|
37
41
|
}
|
|
38
42
|
isHeldBy(lockId) {
|
|
43
|
+
this.expireIfStale();
|
|
39
44
|
if (!this.holder) {
|
|
40
45
|
return false;
|
|
41
46
|
}
|
|
42
47
|
return this.holder === lockId;
|
|
43
48
|
}
|
|
44
49
|
getHolder() {
|
|
50
|
+
this.expireIfStale();
|
|
45
51
|
return this.holder;
|
|
46
52
|
}
|
|
47
53
|
}
|
|
@@ -62,41 +68,45 @@ export function getTodayWriteLockTimeoutMs(executeTimeoutMinutes) {
|
|
|
62
68
|
export class MigrationLock {
|
|
63
69
|
timeoutMs;
|
|
64
70
|
holder = null;
|
|
65
|
-
|
|
71
|
+
expiresAtMs = 0;
|
|
66
72
|
constructor(timeoutMs) {
|
|
67
73
|
this.timeoutMs = timeoutMs;
|
|
68
74
|
}
|
|
75
|
+
// Wall-clock lazy expiry — see InMemoryTodayWriteLockManager for why
|
|
76
|
+
// this is not a setTimeout.
|
|
77
|
+
expireIfStale() {
|
|
78
|
+
if (this.holder && Date.now() >= this.expiresAtMs) {
|
|
79
|
+
logger.warn({ lockId: this.holder }, "Migration lock expired by timeout");
|
|
80
|
+
this.holder = null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
69
83
|
acquire() {
|
|
84
|
+
this.expireIfStale();
|
|
70
85
|
if (this.holder) {
|
|
71
86
|
logger.debug({ existingHolder: this.holder }, "Migration lock rejected — already held");
|
|
72
87
|
return { ok: false, holder: this.holder };
|
|
73
88
|
}
|
|
74
89
|
const lockId = randomUUID();
|
|
75
90
|
this.holder = lockId;
|
|
76
|
-
this.
|
|
77
|
-
logger.warn({ lockId: this.holder }, "Migration lock expired by timeout");
|
|
78
|
-
this.holder = null;
|
|
79
|
-
this.timer = null;
|
|
80
|
-
}, this.timeoutMs);
|
|
91
|
+
this.expiresAtMs = Date.now() + this.timeoutMs;
|
|
81
92
|
logger.debug({ lockId }, "Migration lock acquired");
|
|
82
93
|
return { ok: true, lockId };
|
|
83
94
|
}
|
|
84
95
|
release(lockId) {
|
|
96
|
+
this.expireIfStale();
|
|
85
97
|
if (!this.holder || this.holder !== lockId) {
|
|
86
98
|
return false;
|
|
87
99
|
}
|
|
88
100
|
this.holder = null;
|
|
89
|
-
if (this.timer) {
|
|
90
|
-
clearTimeout(this.timer);
|
|
91
|
-
this.timer = null;
|
|
92
|
-
}
|
|
93
101
|
logger.debug({ lockId }, "Migration lock released");
|
|
94
102
|
return true;
|
|
95
103
|
}
|
|
96
104
|
isHeld() {
|
|
105
|
+
this.expireIfStale();
|
|
97
106
|
return this.holder !== null;
|
|
98
107
|
}
|
|
99
108
|
getHolder() {
|
|
109
|
+
this.expireIfStale();
|
|
100
110
|
return this.holder;
|
|
101
111
|
}
|
|
102
112
|
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default tick cadence. One minute keeps detection latency low while the
|
|
3
|
+
* per-tick work (one Date.now() subtraction) is negligible.
|
|
4
|
+
*/
|
|
5
|
+
export declare const WAKE_DETECTOR_INTERVAL_MS = 60000;
|
|
6
|
+
/**
|
|
7
|
+
* Minimum unexplained gap between ticks that counts as a sleep / suspend /
|
|
8
|
+
* forward clock jump. Five minutes is far above any plausible event-loop
|
|
9
|
+
* stall on a healthy daemon, and far below the shortest sleep that can
|
|
10
|
+
* swallow a cron tick worth catching up (the activity scan's default
|
|
11
|
+
* 60-minute cadence).
|
|
12
|
+
*/
|
|
13
|
+
export declare const WAKE_GAP_THRESHOLD_MS: number;
|
|
14
|
+
export interface WakeDetectorOptions {
|
|
15
|
+
/**
|
|
16
|
+
* Invoked once per detected wake with the gap length. Errors (sync or
|
|
17
|
+
* async) are caught and logged — the detector keeps ticking.
|
|
18
|
+
*/
|
|
19
|
+
onWake: (gapMs: number) => void | Promise<void>;
|
|
20
|
+
intervalMs?: number;
|
|
21
|
+
gapThresholdMs?: number;
|
|
22
|
+
/** Injectable wall clock for tests. */
|
|
23
|
+
now?: () => number;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Detects machine sleep / suspend / forward clock jumps from inside the
|
|
27
|
+
* process.
|
|
28
|
+
*
|
|
29
|
+
* node-cron (and every other timer in the daemon) runs on Node's timer
|
|
30
|
+
* wheel, which does not fire while the host is suspended — a cron tick
|
|
31
|
+
* scheduled inside a sleep window is silently lost, not replayed on wake.
|
|
32
|
+
* The boot-time catchup in `bootstrap/catchup.ts` covers daemon *restarts*,
|
|
33
|
+
* but a long sleep with the process still alive had no equivalent until
|
|
34
|
+
* this detector.
|
|
35
|
+
*
|
|
36
|
+
* Mechanism: a short `setInterval` notes the wall-clock time of each tick.
|
|
37
|
+
* Timers freeze during sleep, so the first tick after wake observes a
|
|
38
|
+
* wall-clock gap of roughly the sleep duration; anything above
|
|
39
|
+
* `gapThresholdMs` beyond the expected interval fires `onWake`. Backward
|
|
40
|
+
* clock jumps are ignored — there is nothing to catch up when time moves
|
|
41
|
+
* backward, and the next tick re-baselines automatically.
|
|
42
|
+
*/
|
|
43
|
+
export declare class WakeDetector {
|
|
44
|
+
private readonly onWake;
|
|
45
|
+
private readonly intervalMs;
|
|
46
|
+
private readonly gapThresholdMs;
|
|
47
|
+
private readonly now;
|
|
48
|
+
private timer;
|
|
49
|
+
private lastTickMs;
|
|
50
|
+
constructor(options: WakeDetectorOptions);
|
|
51
|
+
start(): void;
|
|
52
|
+
stop(): void;
|
|
53
|
+
/** Test seam — exercises one tick without waiting on real timers. */
|
|
54
|
+
tick(): void;
|
|
55
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { createLogger } from "../logging.js";
|
|
2
|
+
const logger = createLogger("wake-detector");
|
|
3
|
+
/**
|
|
4
|
+
* Default tick cadence. One minute keeps detection latency low while the
|
|
5
|
+
* per-tick work (one Date.now() subtraction) is negligible.
|
|
6
|
+
*/
|
|
7
|
+
export const WAKE_DETECTOR_INTERVAL_MS = 60_000;
|
|
8
|
+
/**
|
|
9
|
+
* Minimum unexplained gap between ticks that counts as a sleep / suspend /
|
|
10
|
+
* forward clock jump. Five minutes is far above any plausible event-loop
|
|
11
|
+
* stall on a healthy daemon, and far below the shortest sleep that can
|
|
12
|
+
* swallow a cron tick worth catching up (the activity scan's default
|
|
13
|
+
* 60-minute cadence).
|
|
14
|
+
*/
|
|
15
|
+
export const WAKE_GAP_THRESHOLD_MS = 5 * 60_000;
|
|
16
|
+
/**
|
|
17
|
+
* Detects machine sleep / suspend / forward clock jumps from inside the
|
|
18
|
+
* process.
|
|
19
|
+
*
|
|
20
|
+
* node-cron (and every other timer in the daemon) runs on Node's timer
|
|
21
|
+
* wheel, which does not fire while the host is suspended — a cron tick
|
|
22
|
+
* scheduled inside a sleep window is silently lost, not replayed on wake.
|
|
23
|
+
* The boot-time catchup in `bootstrap/catchup.ts` covers daemon *restarts*,
|
|
24
|
+
* but a long sleep with the process still alive had no equivalent until
|
|
25
|
+
* this detector.
|
|
26
|
+
*
|
|
27
|
+
* Mechanism: a short `setInterval` notes the wall-clock time of each tick.
|
|
28
|
+
* Timers freeze during sleep, so the first tick after wake observes a
|
|
29
|
+
* wall-clock gap of roughly the sleep duration; anything above
|
|
30
|
+
* `gapThresholdMs` beyond the expected interval fires `onWake`. Backward
|
|
31
|
+
* clock jumps are ignored — there is nothing to catch up when time moves
|
|
32
|
+
* backward, and the next tick re-baselines automatically.
|
|
33
|
+
*/
|
|
34
|
+
export class WakeDetector {
|
|
35
|
+
onWake;
|
|
36
|
+
intervalMs;
|
|
37
|
+
gapThresholdMs;
|
|
38
|
+
now;
|
|
39
|
+
timer = null;
|
|
40
|
+
lastTickMs = 0;
|
|
41
|
+
constructor(options) {
|
|
42
|
+
this.onWake = options.onWake;
|
|
43
|
+
this.intervalMs = options.intervalMs ?? WAKE_DETECTOR_INTERVAL_MS;
|
|
44
|
+
this.gapThresholdMs = options.gapThresholdMs ?? WAKE_GAP_THRESHOLD_MS;
|
|
45
|
+
this.now = options.now ?? Date.now;
|
|
46
|
+
}
|
|
47
|
+
start() {
|
|
48
|
+
if (this.timer)
|
|
49
|
+
return;
|
|
50
|
+
this.lastTickMs = this.now();
|
|
51
|
+
this.timer = setInterval(() => this.tick(), this.intervalMs);
|
|
52
|
+
this.timer.unref?.();
|
|
53
|
+
}
|
|
54
|
+
stop() {
|
|
55
|
+
if (!this.timer)
|
|
56
|
+
return;
|
|
57
|
+
clearInterval(this.timer);
|
|
58
|
+
this.timer = null;
|
|
59
|
+
}
|
|
60
|
+
/** Test seam — exercises one tick without waiting on real timers. */
|
|
61
|
+
tick() {
|
|
62
|
+
const current = this.now();
|
|
63
|
+
const gapMs = current - this.lastTickMs - this.intervalMs;
|
|
64
|
+
this.lastTickMs = current;
|
|
65
|
+
if (gapMs < this.gapThresholdMs)
|
|
66
|
+
return;
|
|
67
|
+
logger.warn({ gapMinutes: Math.round(gapMs / 60_000) }, "Wall-clock gap detected (machine sleep or clock jump) — running wake catch-up");
|
|
68
|
+
try {
|
|
69
|
+
const result = this.onWake(gapMs);
|
|
70
|
+
if (result && typeof result.then === "function") {
|
|
71
|
+
result.then(undefined, (err) => {
|
|
72
|
+
logger.error({ err }, "Wake catch-up handler failed");
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
catch (err) {
|
|
77
|
+
logger.error({ err }, "Wake catch-up handler threw");
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* time. The lock is held until the dispatcher releases it via
|
|
13
13
|
* `releaseWikiCompileLock` in `executeDefault`'s `finally` block.
|
|
14
14
|
* - The lock is purely in-process; that matches the dispatcher's
|
|
15
|
-
* existing concurrency invariants (`
|
|
15
|
+
* existing concurrency invariants (`activityScanInProgress`,
|
|
16
16
|
* `morningRoutineActive` are also in-memory flags). A second
|
|
17
17
|
* daemon process would not see the lock — but the daemon is
|
|
18
18
|
* single-process by design.
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* time. The lock is held until the dispatcher releases it via
|
|
13
13
|
* `releaseWikiCompileLock` in `executeDefault`'s `finally` block.
|
|
14
14
|
* - The lock is purely in-process; that matches the dispatcher's
|
|
15
|
-
* existing concurrency invariants (`
|
|
15
|
+
* existing concurrency invariants (`activityScanInProgress`,
|
|
16
16
|
* `morningRoutineActive` are also in-memory flags). A second
|
|
17
17
|
* daemon process would not see the lock — but the daemon is
|
|
18
18
|
* single-process by design.
|
package/dist/core/workdir.js
CHANGED
|
@@ -25,7 +25,7 @@ import { createLogger } from "../logging.js";
|
|
|
25
25
|
import { SkillsCompiler } from "./skills-compiler.js";
|
|
26
26
|
import { EMPTY_MAIL_ACCOUNTS_MD, renderMailAccountsMd, } from "./skills-compiler-tree.js";
|
|
27
27
|
import { refreshSkillIndexBlock } from "./skills-compiler-skill-index.js";
|
|
28
|
-
import { getProfileForEvent, resolveSkillManifest, resolveSkillManifestForProcess, } from "./skills-manifest.js";
|
|
28
|
+
import { eventTypeAcceptsUserSkills, getProfileForEvent, resolveSkillManifest, resolveSkillManifestForProcess, } from "./skills-manifest.js";
|
|
29
29
|
import { ensureDaemonApiCli } from "./daemon-api-cli.js";
|
|
30
30
|
import { computeInstructionAssetStatus, readInstructionStampManifest, sessionInstructionAssetsStale, writeInstructionAssetStamp, } from "./release-assets.js";
|
|
31
31
|
const logger = createLogger("workdir");
|
|
@@ -252,11 +252,15 @@ export function createSessionWorkdir(projectRoot, eventType, userSkillsDir, opti
|
|
|
252
252
|
// turns, manifest now resolves to a different skill set).
|
|
253
253
|
{ processKey: options?.processKey ?? eventType, skillSlugs: deployed.skills });
|
|
254
254
|
ensureDaemonApiCli(sessionDir);
|
|
255
|
-
// User skills: every skill the user has authored,
|
|
256
|
-
//
|
|
257
|
-
//
|
|
255
|
+
// User skills: every skill the user has authored, EXCEPT for
|
|
256
|
+
// narrow-persona keys (wiki.* / routine.research_*) which run a tight
|
|
257
|
+
// built-in manifest and would only be diluted by the owner's general
|
|
258
|
+
// skill library — see `eventTypeAcceptsUserSkills`. Uses the
|
|
259
|
+
// manifest-backed sync so the initial population is consistent with the
|
|
260
|
+
// per-message sync the dispatcher runs on Opus events.
|
|
258
261
|
let userSync = null;
|
|
259
|
-
if (userSkillsDir
|
|
262
|
+
if (userSkillsDir &&
|
|
263
|
+
eventTypeAcceptsUserSkills(options?.processKey ?? eventType)) {
|
|
260
264
|
userSync = syncAllUserSkills(sessionDir, userSkillsDir);
|
|
261
265
|
// docs/design/appendices/skills-unification.md Phase 1 §R4 — splice user-authored
|
|
262
266
|
// slugs into the `<skill-index>` block AFTER they land on disk.
|
|
@@ -419,7 +423,12 @@ export function ensureSessionWorkdir(projectRoot, dataDir, dbSessionId, eventTyp
|
|
|
419
423
|
// CONTEXT_VAULT_REDESIGN — falling back there would resurrect the
|
|
420
424
|
// Obsidian-mode divergence bug v4 V11 fixed.
|
|
421
425
|
const userSkillsDir = join(options?.contextDir ?? join(dataDir, "context"), "policies", "skills");
|
|
422
|
-
|
|
426
|
+
// Narrow-persona keys (wiki.* / routine.research_*) skip the owner's
|
|
427
|
+
// user-skill library; their tight built-in manifest is the whole
|
|
428
|
+
// surface. See `eventTypeAcceptsUserSkills`.
|
|
429
|
+
const userSync = eventTypeAcceptsUserSkills(options?.processKey ?? eventType)
|
|
430
|
+
? syncAllUserSkills(sessionDir, userSkillsDir)
|
|
431
|
+
: null;
|
|
423
432
|
// docs/design/appendices/skills-unification.md Phase 1 §R4 — fold user-authored slugs
|
|
424
433
|
// into the `<skill-index>` block now that they're on disk. Idempotent
|
|
425
434
|
// and inexpensive (single instruction-file rewrite).
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stage-0 signal compute for the three-stage activity_scan gate
|
|
3
|
+
* (cost-reduction-structural §B). Pure DB-read shape: every value is
|
|
4
|
+
* derived from existing tables (`observations`, `mail_messages_index`,
|
|
5
|
+
* `agent_schedule`, `agent_actions`) plus the optional today.md content
|
|
6
|
+
* passed in by the caller. No LLM call, no filesystem I/O of its own —
|
|
7
|
+
* the dispatcher injects the today.md snapshot (when available) so this
|
|
8
|
+
* module stays unit-testable from a synthetic Database handle.
|
|
9
|
+
*
|
|
10
|
+
* The shape mirrors the design doc 1:1 so the gate (Stage 1) can be
|
|
11
|
+
* a pure function over `ActivityScanSignals` + a config block.
|
|
12
|
+
*/
|
|
13
|
+
import type Database from "better-sqlite3";
|
|
14
|
+
export interface ActivityScanSignals {
|
|
15
|
+
/** Post dedup-against-today user-actor pending observation count. */
|
|
16
|
+
pendingObsCount: number;
|
|
17
|
+
/**
|
|
18
|
+
* Maximum `novelty_score` across pending user observations whose
|
|
19
|
+
* `summary_status='done'`. `null` when no observation has a done
|
|
20
|
+
* summary yet — the gate treats null as a cautious mid-novelty default
|
|
21
|
+
* (see decideStage).
|
|
22
|
+
*/
|
|
23
|
+
maxNoveltyScore: number | null;
|
|
24
|
+
/** Histogram of novelty levels across pending+done observations. */
|
|
25
|
+
noveltyDistribution: {
|
|
26
|
+
low: number;
|
|
27
|
+
mid: number;
|
|
28
|
+
high: number;
|
|
29
|
+
};
|
|
30
|
+
/** Count of unread mail rows whose sender is in the VIP list. */
|
|
31
|
+
vipMailUnreadCount: number;
|
|
32
|
+
/** True when at least one pending calendar observation exists. */
|
|
33
|
+
calendarHas24hChange: boolean;
|
|
34
|
+
/**
|
|
35
|
+
* True when at least two pending calendar observations carry
|
|
36
|
+
* overlapping `start`/`end` ISO timestamps in their JSON payload
|
|
37
|
+
* within the next 24 hours. Tolerant of missing fields — when the
|
|
38
|
+
* payload shape isn't recognizable, returns false (the LLM still
|
|
39
|
+
* sees the underlying observation in Stage 3 if escalated).
|
|
40
|
+
*/
|
|
41
|
+
calendarHasConflict: boolean;
|
|
42
|
+
/**
|
|
43
|
+
* Number of `## Agent Plan` rows in today.md whose HH:MM is before
|
|
44
|
+
* `now` in the agent timezone. Best-effort regex parse — when
|
|
45
|
+
* today.md is missing or unparseable, returns 0.
|
|
46
|
+
*/
|
|
47
|
+
agentPlanOverdueCount: number;
|
|
48
|
+
/** Count of `agent_schedule` rows due in the next 6 hours. */
|
|
49
|
+
scheduleApproachingCount: number;
|
|
50
|
+
/**
|
|
51
|
+
* Hours since the last activity_scan that escalated to Stage 3 (or
|
|
52
|
+
* before the gate was deployed, since the last activity_scan run at
|
|
53
|
+
* all). `Infinity` when no row has been recorded yet.
|
|
54
|
+
*/
|
|
55
|
+
hoursSinceLastStage3Run: number;
|
|
56
|
+
}
|
|
57
|
+
export interface ComputeActivityScanSignalsOptions {
|
|
58
|
+
/** Owner-VIP mail addresses, lowercased exact-match. */
|
|
59
|
+
vipMailSenders?: readonly string[];
|
|
60
|
+
/** Look-ahead window for `scheduleApproachingCount`. Default 6h. */
|
|
61
|
+
scheduleHorizonHours?: number;
|
|
62
|
+
/** Look-ahead window for calendar conflict detection. Default 24h. */
|
|
63
|
+
calendarHorizonHours?: number;
|
|
64
|
+
/** today.md content snapshot (or null when missing/unreadable). */
|
|
65
|
+
todayMd?: string | null;
|
|
66
|
+
/** Wall-clock anchor — injectable for tests. Defaults to `new Date()`. */
|
|
67
|
+
now?: Date;
|
|
68
|
+
/**
|
|
69
|
+
* Agent timezone (IANA name, e.g. `Asia/Tokyo`) used to compare
|
|
70
|
+
* `## Agent Plan` HH:MM rows against `now`. When omitted, falls back
|
|
71
|
+
* to the JS engine's local timezone — fine in the common single-user
|
|
72
|
+
* deployment but wrong if the daemon runs in UTC and the operator
|
|
73
|
+
* pinned a different `config.timezone`.
|
|
74
|
+
*/
|
|
75
|
+
agentTimezone?: string;
|
|
76
|
+
}
|
|
77
|
+
export declare function computeActivityScanSignals(db: Database.Database, options?: ComputeActivityScanSignalsOptions): ActivityScanSignals;
|