@aitne/daemon 0.1.2 → 0.1.4
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/LICENSE +21 -0
- package/dist/adapters/whatsapp-adapter.d.ts.map +1 -1
- package/dist/adapters/whatsapp-adapter.js +0 -1
- package/dist/adapters/whatsapp-adapter.js.map +1 -1
- package/dist/api/integration-route-gate.d.ts +15 -11
- package/dist/api/integration-route-gate.d.ts.map +1 -1
- package/dist/api/integration-route-gate.js +60 -23
- package/dist/api/integration-route-gate.js.map +1 -1
- package/dist/api/json-body.d.ts +22 -7
- package/dist/api/json-body.d.ts.map +1 -1
- package/dist/api/json-body.js +27 -8
- package/dist/api/json-body.js.map +1 -1
- package/dist/api/routes/agent.d.ts.map +1 -1
- package/dist/api/routes/agent.js +18 -0
- package/dist/api/routes/agent.js.map +1 -1
- package/dist/api/routes/backends.d.ts.map +1 -1
- package/dist/api/routes/backends.js +96 -1
- package/dist/api/routes/backends.js.map +1 -1
- package/dist/api/routes/books.js +1 -1
- package/dist/api/routes/books.js.map +1 -1
- package/dist/api/routes/context.d.ts.map +1 -1
- package/dist/api/routes/context.js +13 -1
- package/dist/api/routes/context.js.map +1 -1
- package/dist/api/routes/dashboard.d.ts.map +1 -1
- package/dist/api/routes/dashboard.js +75 -5
- package/dist/api/routes/dashboard.js.map +1 -1
- package/dist/api/routes/github.d.ts.map +1 -1
- package/dist/api/routes/github.js +38 -5
- package/dist/api/routes/github.js.map +1 -1
- package/dist/api/routes/integrations.d.ts +35 -6
- package/dist/api/routes/integrations.d.ts.map +1 -1
- package/dist/api/routes/integrations.js +191 -16
- package/dist/api/routes/integrations.js.map +1 -1
- package/dist/api/routes/mail.d.ts.map +1 -1
- package/dist/api/routes/mail.js +112 -46
- package/dist/api/routes/mail.js.map +1 -1
- package/dist/api/routes/observations.d.ts.map +1 -1
- package/dist/api/routes/observations.js +161 -8
- package/dist/api/routes/observations.js.map +1 -1
- package/dist/api/routes/setup-migrate.d.ts +9 -1
- package/dist/api/routes/setup-migrate.d.ts.map +1 -1
- package/dist/api/routes/setup-migrate.js +4 -2
- package/dist/api/routes/setup-migrate.js.map +1 -1
- package/dist/api/routes/skills.d.ts.map +1 -1
- package/dist/api/routes/skills.js +39 -1
- package/dist/api/routes/skills.js.map +1 -1
- package/dist/api/routes/voice.d.ts.map +1 -1
- package/dist/api/routes/voice.js +154 -14
- package/dist/api/routes/voice.js.map +1 -1
- package/dist/bootstrap/adapters.d.ts +109 -0
- package/dist/bootstrap/adapters.d.ts.map +1 -0
- package/dist/bootstrap/adapters.js +237 -0
- package/dist/bootstrap/adapters.js.map +1 -0
- package/dist/bootstrap/catchup.d.ts +23 -0
- package/dist/bootstrap/catchup.d.ts.map +1 -0
- package/dist/bootstrap/catchup.js +124 -0
- package/dist/bootstrap/catchup.js.map +1 -0
- package/dist/bootstrap/schedule-helpers.d.ts +18 -0
- package/dist/bootstrap/schedule-helpers.d.ts.map +1 -0
- package/dist/bootstrap/schedule-helpers.js +96 -0
- package/dist/bootstrap/schedule-helpers.js.map +1 -0
- package/dist/bootstrap/services.d.ts +60 -0
- package/dist/bootstrap/services.d.ts.map +1 -0
- package/dist/bootstrap/services.js +209 -0
- package/dist/bootstrap/services.js.map +1 -0
- package/dist/core/backends/backend-router.d.ts +23 -0
- package/dist/core/backends/backend-router.d.ts.map +1 -1
- package/dist/core/backends/backend-router.js +48 -3
- package/dist/core/backends/backend-router.js.map +1 -1
- package/dist/core/backends/claude-auth.d.ts +70 -0
- package/dist/core/backends/claude-auth.d.ts.map +1 -0
- package/dist/core/backends/claude-auth.js +198 -0
- package/dist/core/backends/claude-auth.js.map +1 -0
- package/dist/core/backends/claude-code-core.d.ts +47 -119
- package/dist/core/backends/claude-code-core.d.ts.map +1 -1
- package/dist/core/backends/claude-code-core.js +112 -1565
- package/dist/core/backends/claude-code-core.js.map +1 -1
- package/dist/core/backends/claude-delegated.d.ts +86 -0
- package/dist/core/backends/claude-delegated.d.ts.map +1 -0
- package/dist/core/backends/claude-delegated.js +801 -0
- package/dist/core/backends/claude-delegated.js.map +1 -0
- package/dist/core/backends/claude-errors.d.ts +39 -0
- package/dist/core/backends/claude-errors.d.ts.map +1 -0
- package/dist/core/backends/claude-errors.js +71 -0
- package/dist/core/backends/claude-errors.js.map +1 -0
- package/dist/core/backends/claude-probe.d.ts +103 -0
- package/dist/core/backends/claude-probe.d.ts.map +1 -0
- package/dist/core/backends/claude-probe.js +336 -0
- package/dist/core/backends/claude-probe.js.map +1 -0
- package/dist/core/backends/claude-tool-collection.d.ts +135 -0
- package/dist/core/backends/claude-tool-collection.d.ts.map +1 -0
- package/dist/core/backends/claude-tool-collection.js +831 -0
- package/dist/core/backends/claude-tool-collection.js.map +1 -0
- package/dist/core/backends/gemini-cli-core.d.ts +21 -0
- package/dist/core/backends/gemini-cli-core.d.ts.map +1 -1
- package/dist/core/backends/gemini-cli-core.js +84 -6
- package/dist/core/backends/gemini-cli-core.js.map +1 -1
- package/dist/core/backends/prompt-utils.d.ts +1 -0
- package/dist/core/backends/prompt-utils.d.ts.map +1 -1
- package/dist/core/backends/prompt-utils.js +60 -3
- package/dist/core/backends/prompt-utils.js.map +1 -1
- package/dist/core/context-builder.d.ts +36 -12
- package/dist/core/context-builder.d.ts.map +1 -1
- package/dist/core/context-builder.js +179 -89
- package/dist/core/context-builder.js.map +1 -1
- package/dist/core/dispatcher-date-utils.d.ts +49 -0
- package/dist/core/dispatcher-date-utils.d.ts.map +1 -0
- package/dist/core/dispatcher-date-utils.js +132 -0
- package/dist/core/dispatcher-date-utils.js.map +1 -0
- package/dist/core/dispatcher-error-handling.d.ts +159 -0
- package/dist/core/dispatcher-error-handling.d.ts.map +1 -0
- package/dist/core/dispatcher-error-handling.js +393 -0
- package/dist/core/dispatcher-error-handling.js.map +1 -0
- package/dist/core/dispatcher-hourly-check.d.ts +150 -0
- package/dist/core/dispatcher-hourly-check.d.ts.map +1 -0
- package/dist/core/dispatcher-hourly-check.js +665 -0
- package/dist/core/dispatcher-hourly-check.js.map +1 -0
- package/dist/core/dispatcher-message-handler.d.ts +170 -0
- package/dist/core/dispatcher-message-handler.d.ts.map +1 -0
- package/dist/core/dispatcher-message-handler.js +1054 -0
- package/dist/core/dispatcher-message-handler.js.map +1 -0
- package/dist/core/dispatcher-morning-routine.d.ts +169 -0
- package/dist/core/dispatcher-morning-routine.d.ts.map +1 -0
- package/dist/core/dispatcher-morning-routine.js +434 -0
- package/dist/core/dispatcher-morning-routine.js.map +1 -0
- package/dist/core/dispatcher-prompt.d.ts +107 -0
- package/dist/core/dispatcher-prompt.d.ts.map +1 -0
- package/dist/core/dispatcher-prompt.js +227 -0
- package/dist/core/dispatcher-prompt.js.map +1 -0
- package/dist/core/dispatcher-repository-helpers.d.ts +39 -0
- package/dist/core/dispatcher-repository-helpers.d.ts.map +1 -0
- package/dist/core/dispatcher-repository-helpers.js +86 -0
- package/dist/core/dispatcher-repository-helpers.js.map +1 -0
- package/dist/core/dispatcher-result-processor.d.ts +145 -0
- package/dist/core/dispatcher-result-processor.d.ts.map +1 -0
- package/dist/core/dispatcher-result-processor.js +414 -0
- package/dist/core/dispatcher-result-processor.js.map +1 -0
- package/dist/core/dispatcher-scheduled-tasks.d.ts +406 -0
- package/dist/core/dispatcher-scheduled-tasks.d.ts.map +1 -0
- package/dist/core/dispatcher-scheduled-tasks.js +998 -0
- package/dist/core/dispatcher-scheduled-tasks.js.map +1 -0
- package/dist/core/dispatcher-types.d.ts +296 -0
- package/dist/core/dispatcher-types.d.ts.map +1 -0
- package/dist/core/dispatcher-types.js +106 -0
- package/dist/core/dispatcher-types.js.map +1 -0
- package/dist/core/dispatcher.d.ts +86 -610
- package/dist/core/dispatcher.d.ts.map +1 -1
- package/dist/core/dispatcher.js +293 -3542
- package/dist/core/dispatcher.js.map +1 -1
- package/dist/core/integration-health.d.ts +18 -10
- package/dist/core/integration-health.d.ts.map +1 -1
- package/dist/core/integration-health.js +31 -1
- package/dist/core/integration-health.js.map +1 -1
- package/dist/core/integration-lifecycle.d.ts +65 -0
- package/dist/core/integration-lifecycle.d.ts.map +1 -1
- package/dist/core/integration-lifecycle.js +167 -16
- package/dist/core/integration-lifecycle.js.map +1 -1
- package/dist/core/integration-main-backend.d.ts +40 -0
- package/dist/core/integration-main-backend.d.ts.map +1 -1
- package/dist/core/integration-main-backend.js +89 -2
- package/dist/core/integration-main-backend.js.map +1 -1
- package/dist/core/management-md.d.ts +51 -17
- package/dist/core/management-md.d.ts.map +1 -1
- package/dist/core/management-md.js +233 -56
- package/dist/core/management-md.js.map +1 -1
- package/dist/core/output-language-policy.d.ts +74 -0
- package/dist/core/output-language-policy.d.ts.map +1 -0
- package/dist/core/output-language-policy.js +194 -0
- package/dist/core/output-language-policy.js.map +1 -0
- package/dist/core/prompts.d.ts +1 -0
- package/dist/core/prompts.d.ts.map +1 -1
- package/dist/core/prompts.js +121 -3
- package/dist/core/prompts.js.map +1 -1
- package/dist/core/repository-management-docs.d.ts +24 -0
- package/dist/core/repository-management-docs.d.ts.map +1 -1
- package/dist/core/repository-management-docs.js +210 -26
- package/dist/core/repository-management-docs.js.map +1 -1
- package/dist/core/routine-acquisition-plan.d.ts +131 -0
- package/dist/core/routine-acquisition-plan.d.ts.map +1 -0
- package/dist/core/routine-acquisition-plan.js +268 -0
- package/dist/core/routine-acquisition-plan.js.map +1 -0
- package/dist/core/routine-fetch-window-runner.d.ts +201 -0
- package/dist/core/routine-fetch-window-runner.d.ts.map +1 -0
- package/dist/core/routine-fetch-window-runner.js +661 -0
- package/dist/core/routine-fetch-window-runner.js.map +1 -0
- package/dist/core/routine-windows.d.ts +156 -0
- package/dist/core/routine-windows.d.ts.map +1 -0
- package/dist/core/routine-windows.js +330 -0
- package/dist/core/routine-windows.js.map +1 -0
- package/dist/core/skills-compiler.d.ts +11 -0
- package/dist/core/skills-compiler.d.ts.map +1 -1
- package/dist/core/skills-compiler.js +102 -13
- package/dist/core/skills-compiler.js.map +1 -1
- package/dist/core/skills-manifest.d.ts.map +1 -1
- package/dist/core/skills-manifest.js +26 -0
- package/dist/core/skills-manifest.js.map +1 -1
- package/dist/core/system-reset.d.ts.map +1 -1
- package/dist/core/system-reset.js +25 -2
- package/dist/core/system-reset.js.map +1 -1
- package/dist/db/observations.d.ts +45 -2
- package/dist/db/observations.d.ts.map +1 -1
- package/dist/db/observations.js +112 -14
- package/dist/db/observations.js.map +1 -1
- package/dist/db/schema.d.ts.map +1 -1
- package/dist/db/schema.js +13 -25
- package/dist/db/schema.js.map +1 -1
- package/dist/index.js +83 -610
- package/dist/index.js.map +1 -1
- package/dist/observers/delegated-sync-worker.d.ts +45 -2
- package/dist/observers/delegated-sync-worker.d.ts.map +1 -1
- package/dist/observers/delegated-sync-worker.js +71 -21
- package/dist/observers/delegated-sync-worker.js.map +1 -1
- package/dist/observers/mail-poller.d.ts +12 -5
- package/dist/observers/mail-poller.d.ts.map +1 -1
- package/dist/observers/mail-poller.js +36 -14
- package/dist/observers/mail-poller.js.map +1 -1
- package/dist/observers/manager.d.ts +37 -5
- package/dist/observers/manager.d.ts.map +1 -1
- package/dist/observers/manager.js +28 -10
- package/dist/observers/manager.js.map +1 -1
- package/dist/safety/risk-classifier.d.ts.map +1 -1
- package/dist/safety/risk-classifier.js +5 -0
- package/dist/safety/risk-classifier.js.map +1 -1
- package/dist/services/delegated-backend-invoker.d.ts +1 -51
- package/dist/services/delegated-backend-invoker.d.ts.map +1 -1
- package/dist/services/delegated-backend-invoker.js +41 -480
- package/dist/services/delegated-backend-invoker.js.map +1 -1
- package/dist/services/delegated-invoker-audit.d.ts +94 -0
- package/dist/services/delegated-invoker-audit.d.ts.map +1 -0
- package/dist/services/delegated-invoker-audit.js +238 -0
- package/dist/services/delegated-invoker-audit.js.map +1 -0
- package/dist/services/delegated-invoker-cache-hits.d.ts +34 -0
- package/dist/services/delegated-invoker-cache-hits.d.ts.map +1 -0
- package/dist/services/delegated-invoker-cache-hits.js +104 -0
- package/dist/services/delegated-invoker-cache-hits.js.map +1 -0
- package/dist/services/delegated-invoker-janitors.d.ts +28 -0
- package/dist/services/delegated-invoker-janitors.d.ts.map +1 -0
- package/dist/services/delegated-invoker-janitors.js +104 -0
- package/dist/services/delegated-invoker-janitors.js.map +1 -0
- package/dist/services/delegated-invoker-utils.d.ts +42 -0
- package/dist/services/delegated-invoker-utils.d.ts.map +1 -0
- package/dist/services/delegated-invoker-utils.js +100 -0
- package/dist/services/delegated-invoker-utils.js.map +1 -0
- package/dist/services/delegated-task-runtime.d.ts +1 -1
- package/dist/services/delegated-task-runtime.js +1 -1
- package/dist/services/integrations/snapshot-partitions.d.ts +5 -0
- package/dist/services/integrations/snapshot-partitions.d.ts.map +1 -1
- package/dist/services/integrations/snapshot-partitions.js +12 -0
- package/dist/services/integrations/snapshot-partitions.js.map +1 -1
- package/dist/services/voice/transcriber-impl.d.ts.map +1 -1
- package/dist/services/voice/transcriber-impl.js +46 -0
- package/dist/services/voice/transcriber-impl.js.map +1 -1
- package/package.json +12 -12
|
@@ -0,0 +1,665 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `HourlyCheckCoordinator` — owns the dispatcher's
|
|
3
|
+
* `triggerHourlyCheck` entry point and the cost-reduction-structural §B
|
|
4
|
+
* three-stage gate that fronts it. The coordinator decides whether a
|
|
5
|
+
* given hourly tick:
|
|
6
|
+
* - skips (autonomous gate / morning routine active / already running
|
|
7
|
+
* / below threshold);
|
|
8
|
+
* - silently consumes observations + records an Agent Log line
|
|
9
|
+
* (Stage 0 deterministic gate or Stage 2 lite-tier `log_only`);
|
|
10
|
+
* - escalates to the existing Stage 3 enqueue (Stage 2 `escalate`
|
|
11
|
+
* verdict or `failed` cautious-escalate path).
|
|
12
|
+
*
|
|
13
|
+
* Extracted from `core/dispatcher.ts` as part of phase D-2 of
|
|
14
|
+
* `docs/design/appendices/file-split-plan.md`. Pattern B (stateful
|
|
15
|
+
* coordinator): the coordinator owns the gate logic but borrows live
|
|
16
|
+
* accessors for state the dispatcher continues to own — the
|
|
17
|
+
* `hourlyCheckInProgress` flag (atomic check-and-set inside the
|
|
18
|
+
* trigger), the `morningRoutineInProgress` flag (read-only), and the
|
|
19
|
+
* lazily-injected delegated-sync refresh callback.
|
|
20
|
+
*
|
|
21
|
+
* Dispatcher entry points served:
|
|
22
|
+
* - `EventDispatcher.triggerHourlyCheck(source, options)` is now a
|
|
23
|
+
* thin one-liner that delegates to `trigger(source, options)`.
|
|
24
|
+
*
|
|
25
|
+
* Invariants preserved bit-for-bit from
|
|
26
|
+
* `docs/design/02-event-pipeline.md` §2:
|
|
27
|
+
* - skip-if-morning-routine-in-progress;
|
|
28
|
+
* - skip-if-hourly-already-running (atomic flag flip BEFORE any
|
|
29
|
+
* await boundary — the C1 race fix from before the split);
|
|
30
|
+
* - skip-if-pending-observations-below-threshold (legacy
|
|
31
|
+
* min-observations floor honoured only when the gate would have
|
|
32
|
+
* proceeded to Stage 3 anyway);
|
|
33
|
+
* - skip-if-setup-incomplete / vault-degraded / user-paused via
|
|
34
|
+
* `isAutonomousAllowed`.
|
|
35
|
+
*
|
|
36
|
+
* Shared-state references held:
|
|
37
|
+
* - `setHourlyCheckInProgress` / `isHourlyCheckInProgress` —
|
|
38
|
+
* getter/setter pair around the dispatcher's flag. The flag is
|
|
39
|
+
* left `true` when an enqueue actually happens (the EventBus
|
|
40
|
+
* consumer's `dispatchSafe` finally clears it on routine
|
|
41
|
+
* completion); it is reset inline when the coordinator owns the
|
|
42
|
+
* turn (silent gate paths) or when the trigger is skipping.
|
|
43
|
+
* - `isMorningRoutineActive` — read-only mirror of the dispatcher
|
|
44
|
+
* method so the gate stays single-sourced.
|
|
45
|
+
* - `isAutonomousAllowed` — same; returns the
|
|
46
|
+
* `TriggerHourlyCheckSkipReason` the gate should surface.
|
|
47
|
+
* - `getDelegatedSyncRefresh` — accessor; null when no delegated
|
|
48
|
+
* integration is wired, in which case the gate proceeds without
|
|
49
|
+
* a refresh, matching pre-injection behaviour.
|
|
50
|
+
*/
|
|
51
|
+
import { EventPriority, createEvent, nativeIntegrationsContributingObservationsForProcessKey, } from "@aitne/shared";
|
|
52
|
+
import { readIntegrations } from "../db/integrations-store.js";
|
|
53
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
54
|
+
import { join } from "node:path";
|
|
55
|
+
import { CONTEXT_RELATIVE_PATHS } from "./context-paths.js";
|
|
56
|
+
import { getContextDir } from "../config.js";
|
|
57
|
+
import { consumeObservations, getPendingCount, getPendingObservations, } from "../db/observations.js";
|
|
58
|
+
import { computeHourlyCheckSignals } from "../db/hourly-check-signals.js";
|
|
59
|
+
import { buildGateAuditDetail, decideStage, renderGateDecisionBlock, } from "../scheduler/hourly-check-gate.js";
|
|
60
|
+
import { appendAgentLogLine } from "./today-direct-writer.js";
|
|
61
|
+
import { parseStage2Verdict } from "./dispatcher-types.js";
|
|
62
|
+
import { createLogger } from "../logging.js";
|
|
63
|
+
const logger = createLogger("dispatcher-hourly-check");
|
|
64
|
+
export class HourlyCheckCoordinator {
|
|
65
|
+
db;
|
|
66
|
+
config;
|
|
67
|
+
eventBus;
|
|
68
|
+
contextBuilder;
|
|
69
|
+
agentRouter;
|
|
70
|
+
audit;
|
|
71
|
+
todayWriteLock;
|
|
72
|
+
prompt;
|
|
73
|
+
fetchWindowRunner;
|
|
74
|
+
getDelegatedSyncRefresh;
|
|
75
|
+
setHourlyCheckInProgress;
|
|
76
|
+
isHourlyCheckInProgress;
|
|
77
|
+
isMorningRoutineActive;
|
|
78
|
+
isAutonomousAllowed;
|
|
79
|
+
constructor(deps) {
|
|
80
|
+
this.db = deps.db;
|
|
81
|
+
this.config = deps.config;
|
|
82
|
+
this.eventBus = deps.eventBus;
|
|
83
|
+
this.contextBuilder = deps.contextBuilder;
|
|
84
|
+
this.agentRouter = deps.agentRouter;
|
|
85
|
+
this.audit = deps.audit;
|
|
86
|
+
this.todayWriteLock = deps.todayWriteLock;
|
|
87
|
+
this.prompt = deps.prompt;
|
|
88
|
+
this.fetchWindowRunner = deps.fetchWindowRunner;
|
|
89
|
+
this.getDelegatedSyncRefresh = deps.getDelegatedSyncRefresh;
|
|
90
|
+
this.setHourlyCheckInProgress = deps.setHourlyCheckInProgress;
|
|
91
|
+
this.isHourlyCheckInProgress = deps.isHourlyCheckInProgress;
|
|
92
|
+
this.isMorningRoutineActive = deps.isMorningRoutineActive;
|
|
93
|
+
this.isAutonomousAllowed = deps.isAutonomousAllowed;
|
|
94
|
+
}
|
|
95
|
+
async trigger(source, options = {}) {
|
|
96
|
+
const forced = options.force === true;
|
|
97
|
+
const minObservations = this.config.hourlyCheckMinObservations;
|
|
98
|
+
// C1 fix: atomic check-and-set on hourlyCheckInProgress BEFORE any await
|
|
99
|
+
// boundary. Previously `await this.isMorningRoutineActive()` yielded to
|
|
100
|
+
// the microtask queue, allowing cron + /api/agent/run-now arriving in
|
|
101
|
+
// the same tick to both observe `hourlyCheckInProgress === false` and
|
|
102
|
+
// both enqueue. Because Node is single-threaded and better-sqlite3 is
|
|
103
|
+
// synchronous, doing set-first + sync checks + rollback-on-skip is now
|
|
104
|
+
// race-free.
|
|
105
|
+
if (this.isHourlyCheckInProgress()) {
|
|
106
|
+
logger.info({ source }, "Hourly check skipped — previous hourly check is still running");
|
|
107
|
+
return {
|
|
108
|
+
status: "skipped",
|
|
109
|
+
reason: "hourly_check_in_progress",
|
|
110
|
+
minObservations,
|
|
111
|
+
forced,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
this.setHourlyCheckInProgress(true);
|
|
115
|
+
// Rollback flag unless we actually enqueue the event or land on a
|
|
116
|
+
// silent path that owns its own reset.
|
|
117
|
+
let enqueued = false;
|
|
118
|
+
let silentPathOwnsReset = false;
|
|
119
|
+
try {
|
|
120
|
+
const setupBlock = this.isAutonomousAllowed();
|
|
121
|
+
if (setupBlock !== null) {
|
|
122
|
+
logger.info({ source, reason: setupBlock }, "Hourly check skipped — autonomous work paused for setup");
|
|
123
|
+
return {
|
|
124
|
+
status: "skipped",
|
|
125
|
+
reason: setupBlock,
|
|
126
|
+
minObservations,
|
|
127
|
+
forced,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
if (this.isMorningRoutineActive()) {
|
|
131
|
+
logger.info({ source }, "Hourly check skipped — morning routine is active");
|
|
132
|
+
return {
|
|
133
|
+
status: "skipped",
|
|
134
|
+
reason: "morning_routine_active",
|
|
135
|
+
minObservations,
|
|
136
|
+
forced,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
// Refresh delegated-sync snapshots for any cadence the operator
|
|
140
|
+
// left opted-OUT (the post-Phase-9 default). Without this, Gmail /
|
|
141
|
+
// Notion observations would dry up entirely in delegated mode and
|
|
142
|
+
// the routine.hourly_check.delegated.* task flow's Step 0a / 0c
|
|
143
|
+
// would have nothing to consume — Step 1's `/api/observations`
|
|
144
|
+
// call would return only Obsidian / Git rows. Calendar's Step 0b
|
|
145
|
+
// already fetches actively via `/reconcile`, so the gap is
|
|
146
|
+
// specific to gmail / notion. See `docs/design/appendices/
|
|
147
|
+
// delegated-sync-opt-in.md` and the worker's
|
|
148
|
+
// `runDisabledCadencesForHourlyCheck` doc-comment for the full
|
|
149
|
+
// reasoning. Failures are logged but do NOT block the check —
|
|
150
|
+
// a stuck cadence cannot starve the entire hourly loop.
|
|
151
|
+
const delegatedSyncRefresh = this.getDelegatedSyncRefresh();
|
|
152
|
+
if (delegatedSyncRefresh) {
|
|
153
|
+
try {
|
|
154
|
+
await delegatedSyncRefresh();
|
|
155
|
+
}
|
|
156
|
+
catch (err) {
|
|
157
|
+
logger.warn({ err, source }, "Pre-hourly-check delegated sync refresh failed; proceeding with stale snapshot");
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
const pendingCount = getPendingCount(this.db, { actorFilter: "user" });
|
|
161
|
+
// INTEGRATION_NATIVE_MODE_DESIGN.md §6.5.1 — threshold bypass when
|
|
162
|
+
// at least one native integration contributes observations during
|
|
163
|
+
// hourly_check execution. "Contributes observations" is the union
|
|
164
|
+
// of `taskFlowsTouched` (in-turn variant POSTs) and
|
|
165
|
+
// `taskFlowsReferenced` (partial-include POSTs from the
|
|
166
|
+
// `routine.fetch_window` pre-pass — RDAD §10 R3). Without the
|
|
167
|
+
// referenced arm, an Outlook-only-native deployment
|
|
168
|
+
// (taskFlowsTouched is `[]` for user-managed connectors) would
|
|
169
|
+
// never trip the bypass: pre-pass would never run, no observations
|
|
170
|
+
// would land, and the gate would lock the routine into permanent
|
|
171
|
+
// skip. When the bypass fires the legacy `below_threshold` floor
|
|
172
|
+
// is suppressed; the in-turn POSTs populate the table by end of
|
|
173
|
+
// turn.
|
|
174
|
+
const nativeHourlyCheckIntegrations = nativeIntegrationsContributingObservationsForProcessKey("routine.hourly_check", readIntegrations(this.db));
|
|
175
|
+
const hasNativeForHourlyCheck = nativeHourlyCheckIntegrations.length > 0;
|
|
176
|
+
if (hasNativeForHourlyCheck) {
|
|
177
|
+
logger.debug({
|
|
178
|
+
source,
|
|
179
|
+
pendingCount,
|
|
180
|
+
minObservations,
|
|
181
|
+
nativeIntegrations: nativeHourlyCheckIntegrations,
|
|
182
|
+
}, "Hourly check: native integrations active — pre-run threshold bypass per §6.5.1");
|
|
183
|
+
}
|
|
184
|
+
// cost-reduction-structural §B — three-stage gate.
|
|
185
|
+
// Mode `off` falls through to the legacy min-observations gate +
|
|
186
|
+
// straight enqueue (rollback path for the gate); `shadow`/`live`
|
|
187
|
+
// compute the gate verdict before any other branch fires.
|
|
188
|
+
const gateMode = (this.config.hourlyCheckGateMode
|
|
189
|
+
?? "shadow");
|
|
190
|
+
if (gateMode === "off") {
|
|
191
|
+
if (!forced && !hasNativeForHourlyCheck && pendingCount < minObservations) {
|
|
192
|
+
logger.debug({ source, pendingCount, minObservations }, "Hourly check skipped — not enough pending observations");
|
|
193
|
+
return {
|
|
194
|
+
status: "skipped",
|
|
195
|
+
reason: "below_threshold",
|
|
196
|
+
pendingCount,
|
|
197
|
+
minObservations,
|
|
198
|
+
forced,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
await this.eventBus.put({
|
|
202
|
+
...createEvent({
|
|
203
|
+
type: "routine.hourly_check",
|
|
204
|
+
source,
|
|
205
|
+
priority: EventPriority.NORMAL,
|
|
206
|
+
}),
|
|
207
|
+
routine: "hourly_check",
|
|
208
|
+
data: { pendingCount, forced },
|
|
209
|
+
...(options.requestedModel ? { requestedModel: options.requestedModel } : {}),
|
|
210
|
+
});
|
|
211
|
+
enqueued = true;
|
|
212
|
+
return {
|
|
213
|
+
status: "queued",
|
|
214
|
+
pendingCount,
|
|
215
|
+
minObservations,
|
|
216
|
+
forced,
|
|
217
|
+
gateMode: "off",
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
const decision = this.computeHourlyCheckGateDecision();
|
|
221
|
+
if (gateMode === "shadow") {
|
|
222
|
+
// Shadow mode: log the gate verdict, then proceed to Stage 3
|
|
223
|
+
// exactly as before so the existing pipeline is uncovered.
|
|
224
|
+
this.logGateAuditRow(decision, {
|
|
225
|
+
mode: "shadow",
|
|
226
|
+
appliedDecision: "stage3_shadow",
|
|
227
|
+
forced,
|
|
228
|
+
});
|
|
229
|
+
if (!forced && !hasNativeForHourlyCheck && pendingCount < minObservations) {
|
|
230
|
+
logger.debug({ source, pendingCount, minObservations, gateStage: decision.stage }, "Hourly check skipped (shadow) — not enough pending observations");
|
|
231
|
+
return {
|
|
232
|
+
status: "skipped",
|
|
233
|
+
reason: "below_threshold",
|
|
234
|
+
pendingCount,
|
|
235
|
+
minObservations,
|
|
236
|
+
forced,
|
|
237
|
+
gateMode: "shadow",
|
|
238
|
+
gateStage: decision.stage,
|
|
239
|
+
gateReason: decision.reason,
|
|
240
|
+
appliedStage: "stage3_shadow",
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
await this.enqueueStage3HourlyCheck(source, { ...decision }, { mode: "shadow", forced, pendingCount, requestedModel: options.requestedModel });
|
|
244
|
+
enqueued = true;
|
|
245
|
+
return {
|
|
246
|
+
status: "queued",
|
|
247
|
+
pendingCount,
|
|
248
|
+
minObservations,
|
|
249
|
+
forced,
|
|
250
|
+
gateMode: "shadow",
|
|
251
|
+
gateStage: decision.stage,
|
|
252
|
+
gateReason: decision.reason,
|
|
253
|
+
appliedStage: "stage3_shadow",
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
// gateMode === 'live'
|
|
257
|
+
// Honour the legacy min-observations floor only when the gate
|
|
258
|
+
// would have proceeded to Stage 3 anyway. The silent gate path
|
|
259
|
+
// already short-circuits the noisy "1 obs, no signal" case below
|
|
260
|
+
// it, so keeping the floor active there would just suppress the
|
|
261
|
+
// gate's telemetry.
|
|
262
|
+
if (!forced
|
|
263
|
+
&& !hasNativeForHourlyCheck
|
|
264
|
+
&& decision.stage === "stage3"
|
|
265
|
+
&& pendingCount < minObservations) {
|
|
266
|
+
this.logGateAuditRow(decision, {
|
|
267
|
+
mode: "live",
|
|
268
|
+
appliedDecision: "stage3",
|
|
269
|
+
forced,
|
|
270
|
+
// Mark the row as a skip even though the gate wanted Stage 3 —
|
|
271
|
+
// the legacy min-observations floor short-circuited it. Without
|
|
272
|
+
// this, every `below_threshold` skip would persist as a phantom
|
|
273
|
+
// `result='success'` row in the audit feed.
|
|
274
|
+
resultOverride: "skipped",
|
|
275
|
+
extra: { skipped: "below_threshold" },
|
|
276
|
+
});
|
|
277
|
+
return {
|
|
278
|
+
status: "skipped",
|
|
279
|
+
reason: "below_threshold",
|
|
280
|
+
pendingCount,
|
|
281
|
+
minObservations,
|
|
282
|
+
forced,
|
|
283
|
+
gateMode: "live",
|
|
284
|
+
gateStage: decision.stage,
|
|
285
|
+
gateReason: decision.reason,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
if (decision.stage === "stage0_silent") {
|
|
289
|
+
const silentResult = this.runSilentHourlyCheckPath(decision, "stage0_silent", {
|
|
290
|
+
source,
|
|
291
|
+
forced,
|
|
292
|
+
});
|
|
293
|
+
silentPathOwnsReset = true;
|
|
294
|
+
return {
|
|
295
|
+
...silentResult,
|
|
296
|
+
minObservations,
|
|
297
|
+
gateMode: "live",
|
|
298
|
+
gateStage: decision.stage,
|
|
299
|
+
gateReason: decision.reason,
|
|
300
|
+
appliedStage: "stage0_silent",
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
if (decision.stage === "stage2") {
|
|
304
|
+
const verdict = await this.runStage2Triage(decision, source);
|
|
305
|
+
if (verdict === "log_only") {
|
|
306
|
+
const silentResult = this.runSilentHourlyCheckPath(decision, "stage2_log_only", { source, forced });
|
|
307
|
+
silentPathOwnsReset = true;
|
|
308
|
+
return {
|
|
309
|
+
...silentResult,
|
|
310
|
+
minObservations,
|
|
311
|
+
gateMode: "live",
|
|
312
|
+
gateStage: decision.stage,
|
|
313
|
+
gateReason: decision.reason,
|
|
314
|
+
appliedStage: "stage2_log_only",
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
// verdict === 'escalate' OR 'failed' (failed → cautious escalate
|
|
318
|
+
// since a malformed JSON should not silently skip a hour's worth
|
|
319
|
+
// of signals; matches the prompt contract's stated default).
|
|
320
|
+
await this.enqueueStage3HourlyCheck(source, decision, {
|
|
321
|
+
mode: "live",
|
|
322
|
+
forced,
|
|
323
|
+
pendingCount,
|
|
324
|
+
requestedModel: options.requestedModel,
|
|
325
|
+
stage2Verdict: verdict,
|
|
326
|
+
});
|
|
327
|
+
enqueued = true;
|
|
328
|
+
return {
|
|
329
|
+
status: "queued",
|
|
330
|
+
pendingCount,
|
|
331
|
+
minObservations,
|
|
332
|
+
forced,
|
|
333
|
+
gateMode: "live",
|
|
334
|
+
gateStage: decision.stage,
|
|
335
|
+
gateReason: decision.reason,
|
|
336
|
+
appliedStage: "stage3",
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
// decision.stage === 'stage3'
|
|
340
|
+
await this.enqueueStage3HourlyCheck(source, decision, { mode: "live", forced, pendingCount, requestedModel: options.requestedModel });
|
|
341
|
+
enqueued = true;
|
|
342
|
+
return {
|
|
343
|
+
status: "queued",
|
|
344
|
+
pendingCount,
|
|
345
|
+
minObservations,
|
|
346
|
+
forced,
|
|
347
|
+
gateMode: "live",
|
|
348
|
+
gateStage: decision.stage,
|
|
349
|
+
gateReason: decision.reason,
|
|
350
|
+
appliedStage: "stage3",
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
finally {
|
|
354
|
+
// Flag is only left true when we successfully enqueued OR the
|
|
355
|
+
// silent path explicitly opted out of resetting (it resets at
|
|
356
|
+
// the end of its own helper). The event loop's dispatchSafe()
|
|
357
|
+
// finally block clears the flag when an enqueued routine event
|
|
358
|
+
// finishes processing.
|
|
359
|
+
if (!enqueued && !silentPathOwnsReset) {
|
|
360
|
+
this.setHourlyCheckInProgress(false);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* cost-reduction-structural §B — pull a fresh signal snapshot and run
|
|
366
|
+
* the deterministic gate. Helper so the dispatcher's call site stays
|
|
367
|
+
* compact and tests can spy on the boundary.
|
|
368
|
+
*/
|
|
369
|
+
computeHourlyCheckGateDecision() {
|
|
370
|
+
const todayMd = this.readTodayMdSafe();
|
|
371
|
+
const signals = computeHourlyCheckSignals(this.db, {
|
|
372
|
+
vipMailSenders: this.config.vipMailSenders ?? [],
|
|
373
|
+
todayMd,
|
|
374
|
+
// Pass the configured agent timezone so `agentPlanOverdueCount`
|
|
375
|
+
// compares HH:MM rows in the right zone. Falls back to the
|
|
376
|
+
// engine's local TZ inside `computeHourlyCheckSignals` when this
|
|
377
|
+
// config field is empty (the common single-user case).
|
|
378
|
+
...(this.config.timezone
|
|
379
|
+
? { agentTimezone: this.config.timezone }
|
|
380
|
+
: {}),
|
|
381
|
+
});
|
|
382
|
+
return decideStage(signals, {
|
|
383
|
+
heartbeatHours: this.config.hourlyCheckHeartbeatHours ?? 4,
|
|
384
|
+
stage2Enabled: this.config.hourlyCheckStage2Enabled ?? false,
|
|
385
|
+
pendingObsLowSignalCeiling: this.config.hourlyCheckLowSignalPendingCeiling ?? 0,
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
readTodayMdSafe() {
|
|
389
|
+
try {
|
|
390
|
+
const path = join(getContextDir(this.config, this.db), CONTEXT_RELATIVE_PATHS.today);
|
|
391
|
+
if (!existsSync(path))
|
|
392
|
+
return null;
|
|
393
|
+
return readFileSync(path, "utf-8");
|
|
394
|
+
}
|
|
395
|
+
catch (err) {
|
|
396
|
+
logger.warn({ err }, "Failed to read today.md for hourly_check signals");
|
|
397
|
+
return null;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
401
|
+
* cost-reduction-structural §B — daemon-direct silent path. Used by
|
|
402
|
+
* Stage 0 and Stage 2 log-only verdicts. Consumes pending user
|
|
403
|
+
* observations + appends a single Agent Log line + records the gate
|
|
404
|
+
* verdict to `agent_actions`. The flag is reset before return.
|
|
405
|
+
*/
|
|
406
|
+
runSilentHourlyCheckPath(decision, appliedDecision, ctx) {
|
|
407
|
+
const reason = appliedDecision === "stage0_silent"
|
|
408
|
+
? "gate_stage0_silent"
|
|
409
|
+
: "gate_stage2_log_only";
|
|
410
|
+
let pendingCount = 0;
|
|
411
|
+
try {
|
|
412
|
+
pendingCount = decision.signals.pendingObsCount;
|
|
413
|
+
// Append a single bullet to today.md ## Agent Log. Best-effort —
|
|
414
|
+
// when today.md is missing or the lock is held, we still consume
|
|
415
|
+
// the observations so the queue doesn't grow indefinitely.
|
|
416
|
+
const message = appliedDecision === "stage0_silent"
|
|
417
|
+
? `[hourly_check] Quiet (${decision.reason}) — ${pendingCount} obs consumed silently`
|
|
418
|
+
: `[hourly_check] Stage 2 log-only (${decision.reason}) — ${pendingCount} obs consumed silently`;
|
|
419
|
+
if (this.todayWriteLock) {
|
|
420
|
+
appendAgentLogLine({
|
|
421
|
+
contextDir: getContextDir(this.config, this.db),
|
|
422
|
+
message,
|
|
423
|
+
todayWriteLock: this.todayWriteLock,
|
|
424
|
+
timezone: this.config.timezone || undefined,
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
// Consume the observations under the gate's correlation id so
|
|
428
|
+
// dashboards can attribute "consumed by gate" rows separately
|
|
429
|
+
// from agent-driven consumption.
|
|
430
|
+
try {
|
|
431
|
+
const pending = getPendingObservations(this.db, {
|
|
432
|
+
actorFilter: "user",
|
|
433
|
+
limit: 100,
|
|
434
|
+
});
|
|
435
|
+
if (pending.length > 0) {
|
|
436
|
+
consumeObservations(this.db, pending.map((row) => row.id), `hourly_check_gate:${appliedDecision}`);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
catch (err) {
|
|
440
|
+
logger.warn({ err }, "Failed to consume observations on silent gate path");
|
|
441
|
+
}
|
|
442
|
+
this.logGateAuditRow(decision, {
|
|
443
|
+
mode: "live",
|
|
444
|
+
appliedDecision,
|
|
445
|
+
forced: ctx.forced,
|
|
446
|
+
});
|
|
447
|
+
logger.info({
|
|
448
|
+
source: ctx.source,
|
|
449
|
+
gateStage: decision.stage,
|
|
450
|
+
gateReason: decision.reason,
|
|
451
|
+
appliedDecision,
|
|
452
|
+
pendingCount,
|
|
453
|
+
}, "Hourly check silenced by Stage-1 gate");
|
|
454
|
+
}
|
|
455
|
+
finally {
|
|
456
|
+
this.setHourlyCheckInProgress(false);
|
|
457
|
+
}
|
|
458
|
+
return {
|
|
459
|
+
status: "skipped",
|
|
460
|
+
reason,
|
|
461
|
+
pendingCount,
|
|
462
|
+
forced: ctx.forced,
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
async enqueueStage3HourlyCheck(source, decision, extra) {
|
|
466
|
+
const gateBlock = renderGateDecisionBlock(decision, {
|
|
467
|
+
mode: extra.mode,
|
|
468
|
+
forced: extra.forced,
|
|
469
|
+
});
|
|
470
|
+
if (extra.mode === "live") {
|
|
471
|
+
this.logGateAuditRow(decision, {
|
|
472
|
+
mode: extra.mode,
|
|
473
|
+
appliedDecision: "stage3",
|
|
474
|
+
forced: extra.forced,
|
|
475
|
+
...(extra.stage2Verdict ? { stage2Verdict: extra.stage2Verdict } : {}),
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
const stage3Event = {
|
|
479
|
+
...createEvent({
|
|
480
|
+
type: "routine.hourly_check",
|
|
481
|
+
source,
|
|
482
|
+
priority: EventPriority.NORMAL,
|
|
483
|
+
}),
|
|
484
|
+
routine: "hourly_check",
|
|
485
|
+
data: {
|
|
486
|
+
pendingCount: extra.pendingCount,
|
|
487
|
+
forced: extra.forced,
|
|
488
|
+
gateDecision: {
|
|
489
|
+
mode: extra.mode,
|
|
490
|
+
stage: decision.stage,
|
|
491
|
+
reason: decision.reason,
|
|
492
|
+
forced: extra.forced,
|
|
493
|
+
...(extra.stage2Verdict ? { stage2Verdict: extra.stage2Verdict } : {}),
|
|
494
|
+
block: gateBlock,
|
|
495
|
+
},
|
|
496
|
+
},
|
|
497
|
+
...(extra.requestedModel ? { requestedModel: extra.requestedModel } : {}),
|
|
498
|
+
};
|
|
499
|
+
// ROUTINE_DATA_ACQUISITION_DESIGN.md Phase 4 / D3 — synchronous
|
|
500
|
+
// pre-pass between the gate's escalate verdict and the Stage 3
|
|
501
|
+
// enqueue. The block lives on `event.data.fetchReportBlock` so
|
|
502
|
+
// ContextBuilder folds it into the Stage 3 prompt when the EventBus
|
|
503
|
+
// consumer dispatches the routine. Pre-pass is skipped when the
|
|
504
|
+
// gate's escalate reason is `forced` (operator-pushed runs already
|
|
505
|
+
// know they want a fresh look) or `shadow` (shadow mode is
|
|
506
|
+
// explicitly running the existing pipeline unchanged, so the
|
|
507
|
+
// pre-pass would change the comparison fixture). Failures inside
|
|
508
|
+
// the runner surface as `<fetch_report status="failed">` and never
|
|
509
|
+
// throw — design §11 R5.
|
|
510
|
+
if (extra.mode === "live" && !extra.forced) {
|
|
511
|
+
const prepass = await this.fetchWindowRunner.run(stage3Event, "routine.hourly_check");
|
|
512
|
+
stage3Event.data = {
|
|
513
|
+
...stage3Event.data,
|
|
514
|
+
fetchReportBlock: prepass.block,
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
await this.eventBus.put(stage3Event);
|
|
518
|
+
}
|
|
519
|
+
logGateAuditRow(decision, params) {
|
|
520
|
+
try {
|
|
521
|
+
// The gate-audit helper only knows about the canonical stages
|
|
522
|
+
// (gate output) plus the shadow-mode marker. Map the silent-path
|
|
523
|
+
// alias `stage2_log_only` onto its canonical sibling so the
|
|
524
|
+
// helper's typing stays narrow; the verdict is preserved verbatim
|
|
525
|
+
// alongside `stage_reached` in the merged detail.
|
|
526
|
+
const auditAppliedDecision = params.appliedDecision === "stage2_log_only"
|
|
527
|
+
? "stage0_silent"
|
|
528
|
+
: params.appliedDecision;
|
|
529
|
+
const detail = {
|
|
530
|
+
...buildGateAuditDetail(decision, {
|
|
531
|
+
mode: params.mode,
|
|
532
|
+
appliedDecision: auditAppliedDecision,
|
|
533
|
+
forced: params.forced,
|
|
534
|
+
...(params.stage2Verdict ? { stage2Verdict: params.stage2Verdict } : {}),
|
|
535
|
+
}),
|
|
536
|
+
// Always reflect the *real* applied stage in the row regardless
|
|
537
|
+
// of the alias mapping above.
|
|
538
|
+
stage_reached: params.appliedDecision,
|
|
539
|
+
...(params.extra ?? {}),
|
|
540
|
+
};
|
|
541
|
+
const isShadow = params.appliedDecision === "stage3_shadow";
|
|
542
|
+
const isSilentPath = params.appliedDecision === "stage0_silent"
|
|
543
|
+
|| params.appliedDecision === "stage2_log_only";
|
|
544
|
+
const result = params.resultOverride
|
|
545
|
+
?? (isShadow ? "success" : isSilentPath ? "skipped" : "success");
|
|
546
|
+
this.db
|
|
547
|
+
.prepare(`INSERT INTO agent_actions
|
|
548
|
+
(action_type, trigger, result, detail, started_at, completed_at)
|
|
549
|
+
VALUES ('hourly_check.gate', 'autonomous', ?, json(?), datetime('now'), datetime('now'))`)
|
|
550
|
+
.run(result, JSON.stringify(detail));
|
|
551
|
+
}
|
|
552
|
+
catch (err) {
|
|
553
|
+
logger.warn({ err }, "Failed to record hourly_check.gate audit row");
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
/**
|
|
557
|
+
* cost-reduction-structural §B Stage 2 — synchronous lite-tier triage.
|
|
558
|
+
* Builds a `routine.hourly_check.triage` RoutineEvent and runs it
|
|
559
|
+
* inline through the agent router (NOT the EventBus, so the result
|
|
560
|
+
* is available before we decide whether to silence or escalate).
|
|
561
|
+
*
|
|
562
|
+
* The agent contract is JSON-only output (`{ "action": "log_only" |
|
|
563
|
+
* "escalate", "reason": "..." }`); on parse failure we return
|
|
564
|
+
* `'failed'` and the caller treats that as cautious escalate.
|
|
565
|
+
*
|
|
566
|
+
* Tool/turn clamp (defense-in-depth):
|
|
567
|
+
* - `allowedToolsOverride: []` removes every tool from the SDK's
|
|
568
|
+
* allowlist for the spawn. Stage 2 has nothing to do but emit a
|
|
569
|
+
* JSON line; the design's "no write tools" rule is enforced here
|
|
570
|
+
* instead of relying on the prompt alone.
|
|
571
|
+
* - `maxTurns: 1` caps the spawn at a single assistant turn. Even
|
|
572
|
+
* if a future prompt change accidentally invites tool use, the
|
|
573
|
+
* spawn cannot loop. Codex/Gemini have no per-spawn `allowedTools`
|
|
574
|
+
* surface today (acknowledged gap in `agent-core.ts`); the
|
|
575
|
+
* `maxTurns` cap and process_backend_config envelope are the
|
|
576
|
+
* remaining safety floor on those backends.
|
|
577
|
+
*/
|
|
578
|
+
async runStage2Triage(decision, source) {
|
|
579
|
+
const triageEvent = {
|
|
580
|
+
...createEvent({
|
|
581
|
+
type: "routine.hourly_check.triage",
|
|
582
|
+
source,
|
|
583
|
+
priority: EventPriority.NORMAL,
|
|
584
|
+
}),
|
|
585
|
+
routine: "hourly_check.triage",
|
|
586
|
+
data: {
|
|
587
|
+
forced: false,
|
|
588
|
+
gateDecision: {
|
|
589
|
+
mode: "live",
|
|
590
|
+
stage: decision.stage,
|
|
591
|
+
reason: decision.reason,
|
|
592
|
+
forced: false,
|
|
593
|
+
block: renderGateDecisionBlock(decision, { mode: "live", forced: false }),
|
|
594
|
+
},
|
|
595
|
+
},
|
|
596
|
+
};
|
|
597
|
+
let context;
|
|
598
|
+
try {
|
|
599
|
+
context = await this.contextBuilder.build(triageEvent);
|
|
600
|
+
}
|
|
601
|
+
catch (err) {
|
|
602
|
+
logger.error({ err }, "Stage 2 triage context build failed");
|
|
603
|
+
return "failed";
|
|
604
|
+
}
|
|
605
|
+
const processKey = "routine.hourly_check.triage";
|
|
606
|
+
const reassemblePrompt = (bid) => this.prompt.assemble(triageEvent.type, processKey, bid);
|
|
607
|
+
let binding;
|
|
608
|
+
try {
|
|
609
|
+
binding = this.agentRouter.resolveBinding(triageEvent, { processKey });
|
|
610
|
+
}
|
|
611
|
+
catch (err) {
|
|
612
|
+
logger.error({ err }, "Stage 2 triage binding resolve failed");
|
|
613
|
+
return "failed";
|
|
614
|
+
}
|
|
615
|
+
const prompt = reassemblePrompt(binding.main.backendId);
|
|
616
|
+
let result;
|
|
617
|
+
try {
|
|
618
|
+
result = await this.agentRouter.execute({
|
|
619
|
+
prompt,
|
|
620
|
+
context,
|
|
621
|
+
event: triageEvent,
|
|
622
|
+
processKey,
|
|
623
|
+
preResolvedBinding: binding,
|
|
624
|
+
reassemblePrompt,
|
|
625
|
+
// Defense-in-depth: Stage 2 must not call any tool. Empty
|
|
626
|
+
// `allowedToolsOverride` REPLACES the default allowlist on
|
|
627
|
+
// Claude (Codex/Gemini have no per-spawn `allowedTools` surface
|
|
628
|
+
// — acknowledged gap in `agent-core.ts`). The `max_turns=1` cap
|
|
629
|
+
// for the spawn comes from the seeded `process_backend_config`
|
|
630
|
+
// row for `routine.hourly_check.triage` (see `db/schema.ts`),
|
|
631
|
+
// which the router reads via `binding.main.maxTurns`. Together
|
|
632
|
+
// these mean: zero tools on Claude, one assistant turn on every
|
|
633
|
+
// backend.
|
|
634
|
+
allowedToolsOverride: [],
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
catch (err) {
|
|
638
|
+
logger.error({ err }, "Stage 2 triage agent execution failed");
|
|
639
|
+
return "failed";
|
|
640
|
+
}
|
|
641
|
+
// Audit row for the lite-tier session itself, distinct from the gate
|
|
642
|
+
// audit row written by `logGateAuditRow`.
|
|
643
|
+
try {
|
|
644
|
+
this.audit.logAction({
|
|
645
|
+
event: triageEvent,
|
|
646
|
+
model: result.model,
|
|
647
|
+
costUsd: result.costUsd,
|
|
648
|
+
usage: result.usage,
|
|
649
|
+
modelUsage: result.modelUsage,
|
|
650
|
+
durationMs: result.durationMs,
|
|
651
|
+
numTurns: result.numTurns,
|
|
652
|
+
trigger: "autonomous",
|
|
653
|
+
backend: result.backendId,
|
|
654
|
+
costSource: result.costSource,
|
|
655
|
+
contextUpdated: result.contextUpdated,
|
|
656
|
+
advisorCallCount: result.advisorCallCount,
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
catch (err) {
|
|
660
|
+
logger.warn({ err }, "Failed to log Stage 2 triage agent_actions row");
|
|
661
|
+
}
|
|
662
|
+
return parseStage2Verdict(result.output);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
//# sourceMappingURL=dispatcher-hourly-check.js.map
|