@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,661 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `RoutineFetchWindowRunner` — Phase 4 / D1 pre-pass orchestration.
|
|
3
|
+
*
|
|
4
|
+
* ROUTINE_DATA_ACQUISITION_DESIGN.md §6.1.1 / Phase 4 D1 — every routine
|
|
5
|
+
* dispatcher (morning_routine, today_refresh, hourly_check, evening /
|
|
6
|
+
* weekly / monthly review) calls this runner immediately before
|
|
7
|
+
* dispatching the parent session. The runner:
|
|
8
|
+
*
|
|
9
|
+
* 1. Reads the per-routine plan from `ROUTINE_WINDOWS` and the current
|
|
10
|
+
* integration state, fans out per-account where applicable, resolves
|
|
11
|
+
* each row's predicate (`direct` / `delegated-same` / `delegated-cross`
|
|
12
|
+
* / `native` / skip) via `buildAcquisitionPlan`, and renders an
|
|
13
|
+
* `<acquisition-plan>` block.
|
|
14
|
+
* 2. Synthesises a `routine.fetch_window` RoutineEvent, embeds the plan
|
|
15
|
+
* block in `event.data.acquisitionPlanBlock` so ContextBuilder folds
|
|
16
|
+
* it into the fetcher's prompt context, and routes the event
|
|
17
|
+
* synchronously through the standard `IAgentRouter.execute` /
|
|
18
|
+
* `PromptAssembler.assemble` pipeline. The session inherits the
|
|
19
|
+
* `routine.fetch_window` ProcessKey binding (lite tier per F1 / §6.9)
|
|
20
|
+
* and the dedicated `routine-fetch-window` agent profile.
|
|
21
|
+
* 3. Parses the agent's single-JSON-line output into a structured
|
|
22
|
+
* `FetchReport`, renders a `<fetch_report>` block, and logs one
|
|
23
|
+
* `agent_actions` row via the dispatcher's audit logger.
|
|
24
|
+
* 4. Returns the rendered block + parsed report so the caller can graft
|
|
25
|
+
* the block into the **parent** routine event's
|
|
26
|
+
* `event.data.fetchReportBlock`. ContextBuilder injects that block
|
|
27
|
+
* verbatim into the parent session's prompt (mirrors the
|
|
28
|
+
* `<gate_decision>` pattern used by hourly_check Stage 3).
|
|
29
|
+
*
|
|
30
|
+
* Failure-mode contract (Phase 4 D1 + design §11 R5 / OQ4):
|
|
31
|
+
*
|
|
32
|
+
* - **No applicable rows** (routine has no windows in `ROUTINE_WINDOWS`,
|
|
33
|
+
* every integration is disabled, every account list is empty) — the
|
|
34
|
+
* runner returns an empty `<fetch_report>` with `status="skipped"` and
|
|
35
|
+
* `fetched=posted=duplicates=0`. The parent routine still runs; the
|
|
36
|
+
* block is informational only.
|
|
37
|
+
* - **Pre-pass session errors** (binding resolve fails, agent throws,
|
|
38
|
+
* JSON parse fails) — the runner logs the failure and returns a
|
|
39
|
+
* `<fetch_report status="failed">` with one `pre-pass-failed` error.
|
|
40
|
+
* Pre-pass cost gains are forfeit for this tick; the parent routine
|
|
41
|
+
* continues with whatever observations the rest of the plan produced.
|
|
42
|
+
* Throwing here would otherwise propagate up and abort the parent
|
|
43
|
+
* routine — the opposite of P3 ("Lite for Fetch, Medium for Decide").
|
|
44
|
+
* - **Partial success** — the report's `errors` array carries per-row
|
|
45
|
+
* failures (`no-surface`, `fetch-failed`, `budget-exhausted`). The
|
|
46
|
+
* block surfaces them so the parent prompt can decide whether to
|
|
47
|
+
* treat its observations view as complete.
|
|
48
|
+
*/
|
|
49
|
+
import { EventPriority, INTEGRATION_KEYS, createEvent, getAgentDayDateStr, getIntegrationDescriptor, isRoutineEvent, } from "@aitne/shared";
|
|
50
|
+
import { readIntegrations } from "../db/integrations-store.js";
|
|
51
|
+
import { ROUTINE_WINDOWS, routineHasWindows, } from "./routine-windows.js";
|
|
52
|
+
import { buildAcquisitionPlan, buildAcquisitionTimestamps, } from "./routine-acquisition-plan.js";
|
|
53
|
+
import { createLogger } from "../logging.js";
|
|
54
|
+
const logger = createLogger("routine-fetch-window-runner");
|
|
55
|
+
// ── Module helpers ────────────────────────────────────────────────────────
|
|
56
|
+
/** The ProcessKey + event type the pre-pass session always runs under. */
|
|
57
|
+
const FETCH_WINDOW_PROCESS_KEY = "routine.fetch_window";
|
|
58
|
+
const FETCH_WINDOW_EVENT_TYPE = "routine.fetch_window";
|
|
59
|
+
/**
|
|
60
|
+
* Daemon REST surfaces the pre-pass partials may target. Curl prefixes
|
|
61
|
+
* are constructed with the configured `apiPort` at dispatch time so a
|
|
62
|
+
* non-default port survives the clamp. Everything OTHER than these
|
|
63
|
+
* prefixes is denied — the pre-pass cannot reach `/api/notify`,
|
|
64
|
+
* `/api/context/*`, `/api/agent/*`, etc., even though Bash(curl *) is
|
|
65
|
+
* the project default. This is the daemon-side enforcement that backs
|
|
66
|
+
* the agent profile's "no notify, no context writes" guardrails (P3:
|
|
67
|
+
* Lite for Fetch — the pre-pass has zero business making decisions).
|
|
68
|
+
*
|
|
69
|
+
* `jq *` stays allowed because direct-mode partials pipe curl output
|
|
70
|
+
* through jq for compact projection before posting to /api/observations.
|
|
71
|
+
*
|
|
72
|
+
* The clamp is Claude-only — Codex/Gemini have no per-spawn allowedTools
|
|
73
|
+
* surface today (CLAUDE.md acknowledges the gap). The
|
|
74
|
+
* `process_backend_config` envelope (`max_turns=20`, `max_budget_usd=0.20`)
|
|
75
|
+
* remains the floor on those backends.
|
|
76
|
+
*/
|
|
77
|
+
function buildPrePassDaemonRestPatterns(apiPort) {
|
|
78
|
+
const root = `http://localhost:${apiPort}/api`;
|
|
79
|
+
return [
|
|
80
|
+
// Observations — the only write surface the pre-pass touches.
|
|
81
|
+
// Catches both POST /api/observations and GET /api/observations*.
|
|
82
|
+
`Bash(curl ${root}/observations*)`,
|
|
83
|
+
// Direct-mode mail / calendar / notion reads.
|
|
84
|
+
`Bash(curl ${root}/mail/*)`,
|
|
85
|
+
`Bash(curl ${root}/calendar/*)`,
|
|
86
|
+
`Bash(curl ${root}/notion/*)`,
|
|
87
|
+
// delegated-cross proxy. Only Gmail / Google Calendar / Notion
|
|
88
|
+
// expose this; user-managed Outlook has no proxy and the runner
|
|
89
|
+
// collapses cross-backend bindings to delegated-same per
|
|
90
|
+
// routine-acquisition-plan.ts:resolveFetchMode.
|
|
91
|
+
`Bash(curl ${root}/integrations/*)`,
|
|
92
|
+
// Compact-projection helper used by the partials.
|
|
93
|
+
"Bash(jq *)",
|
|
94
|
+
];
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Project the active integrations into the per-backend MCP tool names
|
|
98
|
+
* the pre-pass needs. Includes both delegated-same bindings (where the
|
|
99
|
+
* connector is registered through the daemon's Claude SDK) AND native
|
|
100
|
+
* bindings (where the connector is loaded by the user but the same
|
|
101
|
+
* descriptor declares which capability tool names exist). User-managed
|
|
102
|
+
* descriptors (`backendConnectors[backend]` undefined for Outlook) are
|
|
103
|
+
* skipped — those rows surface as `no-surface` errors per the partial
|
|
104
|
+
* contract, which is the documented behaviour.
|
|
105
|
+
*
|
|
106
|
+
* Cross-backend delegated bindings contribute zero MCP tools because
|
|
107
|
+
* the partial reaches them through `/api/integrations/<key>/exec`, not
|
|
108
|
+
* via the session backend's MCP namespace.
|
|
109
|
+
*/
|
|
110
|
+
function collectIntegrationToolsForBackend(integrations, backend) {
|
|
111
|
+
const out = new Set();
|
|
112
|
+
for (const key of INTEGRATION_KEYS) {
|
|
113
|
+
const state = integrations[key];
|
|
114
|
+
if (!state)
|
|
115
|
+
continue;
|
|
116
|
+
let active = false;
|
|
117
|
+
if (state.mode === "delegated" && state.delegatedBackend === backend) {
|
|
118
|
+
active = true;
|
|
119
|
+
}
|
|
120
|
+
else if (state.mode === "native" && state.nativeBackend === backend) {
|
|
121
|
+
active = true;
|
|
122
|
+
}
|
|
123
|
+
if (!active)
|
|
124
|
+
continue;
|
|
125
|
+
const connector = getIntegrationDescriptor(key).backendConnectors[backend];
|
|
126
|
+
if (!connector)
|
|
127
|
+
continue; // user-managed (no descriptor connector for this backend)
|
|
128
|
+
for (const toolNames of Object.values(connector.capabilityTools)) {
|
|
129
|
+
for (const toolName of toolNames) {
|
|
130
|
+
out.add(connector.toolNamespace + toolName);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return Array.from(out);
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Compose the per-execute `allowedToolsOverride` for the pre-pass. The
|
|
138
|
+
* override REPLACES the SDK's default allowlist (no union per
|
|
139
|
+
* claude-code-core.ts:437) so the list must be exhaustive for every
|
|
140
|
+
* surface the partials use under any (integration, mode) cell. Mode
|
|
141
|
+
* coverage:
|
|
142
|
+
*
|
|
143
|
+
* - `direct`: daemon REST → curl prefix.
|
|
144
|
+
* - `delegated-same`: session backend MCP → integration tool name.
|
|
145
|
+
* - `delegated-cross`: daemon delegation proxy → curl prefix
|
|
146
|
+
* (`/api/integrations/<key>/exec`).
|
|
147
|
+
* - `native` (descriptor-bound): session backend MCP → integration
|
|
148
|
+
* tool name.
|
|
149
|
+
* - `native` (user-managed) / no-surface: nothing in the override —
|
|
150
|
+
* the partial records `no-surface` and the runner's report carries
|
|
151
|
+
* the gap forward to the parent routine.
|
|
152
|
+
*
|
|
153
|
+
* Exported for unit testing — the runner consumes it via
|
|
154
|
+
* `composePrePassAllowedTools` at dispatch time.
|
|
155
|
+
*/
|
|
156
|
+
export function composePrePassAllowedTools(apiPort, integrations, sessionBackend) {
|
|
157
|
+
return [
|
|
158
|
+
...buildPrePassDaemonRestPatterns(apiPort),
|
|
159
|
+
...collectIntegrationToolsForBackend(integrations, sessionBackend),
|
|
160
|
+
];
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Map a `MailAccount.kind` onto the integration key the registry uses
|
|
164
|
+
* for routing. Today: `gmail` → `gmail`, `outlook` → `outlook_mail`.
|
|
165
|
+
* Yahoo / iCloud / IMAP accounts are not tied to a routed integration
|
|
166
|
+
* and therefore do not participate in the pre-pass fan-out today.
|
|
167
|
+
*/
|
|
168
|
+
function mailAccountIntegrationKey(account) {
|
|
169
|
+
switch (account.kind) {
|
|
170
|
+
case "gmail":
|
|
171
|
+
return "gmail";
|
|
172
|
+
case "outlook":
|
|
173
|
+
return "outlook_mail";
|
|
174
|
+
default:
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Derive the canonical `RoutineWindowKey` from a routine event. The
|
|
180
|
+
* caller's intent is conveyed by `event.type` (always `routine.<name>`),
|
|
181
|
+
* with `RoutineEvent.routine` carrying the same suffix without the
|
|
182
|
+
* `routine.` prefix. Returns null for routines outside the catalog so
|
|
183
|
+
* the caller can short-circuit before touching plan assembly.
|
|
184
|
+
*/
|
|
185
|
+
export function routineWindowKeyFromEvent(event) {
|
|
186
|
+
if (!isRoutineEvent(event))
|
|
187
|
+
return null;
|
|
188
|
+
const candidate = `routine.${event.routine}`;
|
|
189
|
+
return (ROUTINE_WINDOWS[candidate] !== undefined
|
|
190
|
+
? candidate
|
|
191
|
+
: null);
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Walk `text` and return every balanced `{...}` slice (top-level objects
|
|
195
|
+
* only; nested braces are honoured). Strings are tracked so brace
|
|
196
|
+
* characters inside `"..."` literals don't unbalance the scan. Used by
|
|
197
|
+
* `parseFetchWindowOutput` to pick the LAST top-level object on stdout —
|
|
198
|
+
* agents occasionally emit a think-aloud line carrying a JSON snippet
|
|
199
|
+
* before the verdict, and the fetcher's contract is "the last
|
|
200
|
+
* top-level JSON object wins."
|
|
201
|
+
*
|
|
202
|
+
* Exported for direct unit testing; the runner consumes it via
|
|
203
|
+
* `parseFetchWindowOutput`.
|
|
204
|
+
*/
|
|
205
|
+
export function extractBalancedJsonObjects(text) {
|
|
206
|
+
const out = [];
|
|
207
|
+
let depth = 0;
|
|
208
|
+
let start = -1;
|
|
209
|
+
let inString = false;
|
|
210
|
+
let escape = false;
|
|
211
|
+
for (let i = 0; i < text.length; i++) {
|
|
212
|
+
const ch = text[i];
|
|
213
|
+
if (inString) {
|
|
214
|
+
if (escape) {
|
|
215
|
+
escape = false;
|
|
216
|
+
}
|
|
217
|
+
else if (ch === "\\") {
|
|
218
|
+
escape = true;
|
|
219
|
+
}
|
|
220
|
+
else if (ch === '"') {
|
|
221
|
+
inString = false;
|
|
222
|
+
}
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
if (ch === '"') {
|
|
226
|
+
inString = true;
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
if (ch === "{") {
|
|
230
|
+
if (depth === 0)
|
|
231
|
+
start = i;
|
|
232
|
+
depth++;
|
|
233
|
+
}
|
|
234
|
+
else if (ch === "}") {
|
|
235
|
+
if (depth > 0) {
|
|
236
|
+
depth--;
|
|
237
|
+
if (depth === 0 && start >= 0) {
|
|
238
|
+
out.push(text.slice(start, i + 1));
|
|
239
|
+
start = -1;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return out;
|
|
245
|
+
}
|
|
246
|
+
/** Strict-JSON parse of the fetcher's single-line output. */
|
|
247
|
+
export function parseFetchWindowOutput(output) {
|
|
248
|
+
const trimmed = (output ?? "").trim();
|
|
249
|
+
if (!trimmed)
|
|
250
|
+
return { parseError: "empty-output" };
|
|
251
|
+
// Tolerate code fences without making them mandatory — mirrors
|
|
252
|
+
// `parseStage2Verdict` in dispatcher-types.ts.
|
|
253
|
+
const stripped = trimmed
|
|
254
|
+
.replace(/^```(?:json)?\s*/i, "")
|
|
255
|
+
.replace(/```\s*$/i, "")
|
|
256
|
+
.trim();
|
|
257
|
+
const candidates = extractBalancedJsonObjects(stripped);
|
|
258
|
+
if (candidates.length === 0)
|
|
259
|
+
return { parseError: "no-json-object" };
|
|
260
|
+
const objText = candidates[candidates.length - 1];
|
|
261
|
+
let parsed;
|
|
262
|
+
try {
|
|
263
|
+
parsed = JSON.parse(objText);
|
|
264
|
+
}
|
|
265
|
+
catch (err) {
|
|
266
|
+
return { parseError: `invalid-json: ${err.message}` };
|
|
267
|
+
}
|
|
268
|
+
if (!parsed || typeof parsed !== "object") {
|
|
269
|
+
return { parseError: "not-an-object" };
|
|
270
|
+
}
|
|
271
|
+
const obj = parsed;
|
|
272
|
+
const fetched = typeof obj.fetched === "number" ? obj.fetched : 0;
|
|
273
|
+
const posted = typeof obj.posted === "number" ? obj.posted : 0;
|
|
274
|
+
const duplicates = typeof obj.duplicates === "number" ? obj.duplicates : 0;
|
|
275
|
+
const errors = Array.isArray(obj.errors)
|
|
276
|
+
? obj.errors
|
|
277
|
+
.filter((row) => typeof row === "object" && row !== null)
|
|
278
|
+
.map((row) => ({ ...row }))
|
|
279
|
+
: [];
|
|
280
|
+
const status = errors.length > 0 ? "partial" : "success";
|
|
281
|
+
return {
|
|
282
|
+
status,
|
|
283
|
+
fetched,
|
|
284
|
+
posted,
|
|
285
|
+
duplicates,
|
|
286
|
+
errors,
|
|
287
|
+
skipped: false,
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
function xmlEscape(value) {
|
|
291
|
+
return value
|
|
292
|
+
.replace(/&/g, "&")
|
|
293
|
+
.replace(/"/g, """)
|
|
294
|
+
.replace(/</g, "<")
|
|
295
|
+
.replace(/>/g, ">");
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Render the `<fetch_report>` XML block injected into the parent
|
|
299
|
+
* routine's prompt. Keep the schema narrow — every additional attribute
|
|
300
|
+
* costs prompt tokens on the cache-warm parent session.
|
|
301
|
+
*
|
|
302
|
+
* `meta.routine` accepts any string so the no-routine-key skip path can
|
|
303
|
+
* render the parent event's actual type (e.g. `routine.skill_curation`)
|
|
304
|
+
* instead of borrowing a catalog entry as a placeholder. The renderer
|
|
305
|
+
* strips the `routine.` prefix verbatim — callers may pass either the
|
|
306
|
+
* fully-qualified ProcessKey or a bare suffix.
|
|
307
|
+
*/
|
|
308
|
+
export function renderFetchReportBlock(report, meta) {
|
|
309
|
+
const routineAttr = meta.routine.replace(/^routine\./, "");
|
|
310
|
+
const lines = [
|
|
311
|
+
`<fetch_report routine="${xmlEscape(routineAttr)}" agent_day="${xmlEscape(meta.agentDay)}" status="${xmlEscape(report.status)}" fetched="${report.fetched}" posted="${report.posted}" duplicates="${report.duplicates}">`,
|
|
312
|
+
];
|
|
313
|
+
if (report.failureReason) {
|
|
314
|
+
lines.push(` <failure>${xmlEscape(report.failureReason)}</failure>`);
|
|
315
|
+
}
|
|
316
|
+
for (const err of report.errors) {
|
|
317
|
+
const type = typeof err.type === "string" ? err.type : "unknown";
|
|
318
|
+
// Compact, attribute-shaped serialisation: every string-typed key
|
|
319
|
+
// becomes an XML attribute; nested objects are collapsed to JSON
|
|
320
|
+
// text content so the block stays parseable both as XML and as a
|
|
321
|
+
// line-by-line scan target.
|
|
322
|
+
const attrEntries = Object.entries(err).filter(([k, v]) => k !== "type" && (typeof v === "string" || typeof v === "number"));
|
|
323
|
+
const attrs = attrEntries
|
|
324
|
+
.map(([k, v]) => `${xmlEscape(k)}="${xmlEscape(String(v))}"`)
|
|
325
|
+
.join(" ");
|
|
326
|
+
const nested = Object.entries(err).filter(([k, v]) => k !== "type"
|
|
327
|
+
&& typeof v !== "string"
|
|
328
|
+
&& typeof v !== "number");
|
|
329
|
+
if (nested.length > 0) {
|
|
330
|
+
lines.push(` <error type="${xmlEscape(type)}"${attrs ? " " + attrs : ""}>${xmlEscape(JSON.stringify(Object.fromEntries(nested)))}</error>`);
|
|
331
|
+
}
|
|
332
|
+
else {
|
|
333
|
+
lines.push(` <error type="${xmlEscape(type)}"${attrs ? " " + attrs : ""} />`);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
lines.push("</fetch_report>");
|
|
337
|
+
return lines.join("\n");
|
|
338
|
+
}
|
|
339
|
+
// ── Runner ────────────────────────────────────────────────────────────────
|
|
340
|
+
export class RoutineFetchWindowRunner {
|
|
341
|
+
db;
|
|
342
|
+
config;
|
|
343
|
+
contextBuilder;
|
|
344
|
+
agentRouter;
|
|
345
|
+
audit;
|
|
346
|
+
prompt;
|
|
347
|
+
getActiveMailAccounts;
|
|
348
|
+
constructor(deps) {
|
|
349
|
+
this.db = deps.db;
|
|
350
|
+
this.config = deps.config;
|
|
351
|
+
this.contextBuilder = deps.contextBuilder;
|
|
352
|
+
this.agentRouter = deps.agentRouter;
|
|
353
|
+
this.audit = deps.audit;
|
|
354
|
+
this.prompt = deps.prompt;
|
|
355
|
+
this.getActiveMailAccounts = deps.getActiveMailAccounts;
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Execute the pre-pass for `parentEvent`. Returns the fetch report
|
|
359
|
+
* and rendered `<fetch_report>` block; callers graft the block into
|
|
360
|
+
* the parent event's `event.data.fetchReportBlock` so ContextBuilder
|
|
361
|
+
* injects it into the parent prompt.
|
|
362
|
+
*
|
|
363
|
+
* `routineKey` overrides the auto-derived window key — used by
|
|
364
|
+
* morning_routine to opt into `routine.morning_routine_initial`'s
|
|
365
|
+
* plan when `yesterday.md` is absent (their plans currently coincide,
|
|
366
|
+
* but the seam exists for future divergence).
|
|
367
|
+
*/
|
|
368
|
+
async run(parentEvent, routineKey) {
|
|
369
|
+
const key = routineKey ?? routineWindowKeyFromEvent(parentEvent);
|
|
370
|
+
const agentDay = getAgentDayDateStr(this.config.timezone || undefined, this.config.dayBoundaryHour);
|
|
371
|
+
if (!key) {
|
|
372
|
+
const report = {
|
|
373
|
+
status: "skipped",
|
|
374
|
+
fetched: 0,
|
|
375
|
+
posted: 0,
|
|
376
|
+
duplicates: 0,
|
|
377
|
+
errors: [],
|
|
378
|
+
skipped: true,
|
|
379
|
+
failureReason: "no-routine-window-key",
|
|
380
|
+
};
|
|
381
|
+
// Surface the parent's actual event type rather than borrowing a
|
|
382
|
+
// catalog entry — the report attribution would otherwise lie about
|
|
383
|
+
// which routine the pre-pass was attempted for, hiding the
|
|
384
|
+
// misroute behind a plausible-looking placeholder.
|
|
385
|
+
const block = renderFetchReportBlock(report, {
|
|
386
|
+
routine: parentEvent.type,
|
|
387
|
+
agentDay,
|
|
388
|
+
});
|
|
389
|
+
return { report, block };
|
|
390
|
+
}
|
|
391
|
+
if (!routineHasWindows(key)) {
|
|
392
|
+
const report = {
|
|
393
|
+
status: "skipped",
|
|
394
|
+
fetched: 0,
|
|
395
|
+
posted: 0,
|
|
396
|
+
duplicates: 0,
|
|
397
|
+
errors: [],
|
|
398
|
+
skipped: true,
|
|
399
|
+
};
|
|
400
|
+
const block = renderFetchReportBlock(report, { routine: key, agentDay });
|
|
401
|
+
return { report, block };
|
|
402
|
+
}
|
|
403
|
+
// Build the acquisition plan from the current integration state +
|
|
404
|
+
// active accounts. The dispatcher's session backend is the resolved
|
|
405
|
+
// main backend for the routine.fetch_window ProcessKey — resolve via
|
|
406
|
+
// the agent router so the per-(integration, mode) predicate
|
|
407
|
+
// (`delegated-same` vs `delegated-cross`) is correct for the actual
|
|
408
|
+
// run.
|
|
409
|
+
//
|
|
410
|
+
// `integrationsSnapshot` and `sessionBackend` are hoisted above the
|
|
411
|
+
// try block so the `allowedToolsOverride` composer below can re-use
|
|
412
|
+
// the same snapshot the plan was built from. Recomputing the
|
|
413
|
+
// snapshot at execute time would risk a TOCTOU drift if the
|
|
414
|
+
// operator flips an integration mid-pre-pass — the plan would have
|
|
415
|
+
// a row the override couldn't reach, manifesting as a silent MCP
|
|
416
|
+
// permission denial.
|
|
417
|
+
let fetcherEvent;
|
|
418
|
+
let sessionBackend;
|
|
419
|
+
let integrationsSnapshot;
|
|
420
|
+
try {
|
|
421
|
+
// Resolve binding off a placeholder event so we know the session
|
|
422
|
+
// backend before we synthesise the real one (the plan attribute
|
|
423
|
+
// depends on it). The placeholder borrows correlationId so audit
|
|
424
|
+
// rows correlate back to the parent.
|
|
425
|
+
const placeholder = {
|
|
426
|
+
...createEvent({
|
|
427
|
+
type: FETCH_WINDOW_EVENT_TYPE,
|
|
428
|
+
source: parentEvent.source,
|
|
429
|
+
priority: EventPriority.NORMAL,
|
|
430
|
+
correlationId: parentEvent.correlationId,
|
|
431
|
+
}),
|
|
432
|
+
routine: "fetch_window",
|
|
433
|
+
};
|
|
434
|
+
const preBinding = this.agentRouter.resolveBinding(placeholder, {
|
|
435
|
+
processKey: FETCH_WINDOW_PROCESS_KEY,
|
|
436
|
+
});
|
|
437
|
+
sessionBackend = preBinding.main.backendId;
|
|
438
|
+
integrationsSnapshot = readIntegrations(this.db);
|
|
439
|
+
const accounts = this.collectAccounts(integrationsSnapshot);
|
|
440
|
+
const timestamps = buildAcquisitionTimestamps(new Date(), this.config.timezone || undefined, this.config.dayBoundaryHour);
|
|
441
|
+
const planBlock = buildAcquisitionPlan({
|
|
442
|
+
routine: key,
|
|
443
|
+
agentDay,
|
|
444
|
+
integrations: integrationsSnapshot,
|
|
445
|
+
sessionBackend,
|
|
446
|
+
accounts,
|
|
447
|
+
timestamps,
|
|
448
|
+
});
|
|
449
|
+
fetcherEvent = {
|
|
450
|
+
...placeholder,
|
|
451
|
+
data: {
|
|
452
|
+
...placeholder.data,
|
|
453
|
+
acquisitionPlanBlock: planBlock,
|
|
454
|
+
parentRoutine: key,
|
|
455
|
+
parentCorrelationId: parentEvent.correlationId,
|
|
456
|
+
},
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
catch (err) {
|
|
460
|
+
return this.fail(key, agentDay, parentEvent, "plan-assembly-failed", err);
|
|
461
|
+
}
|
|
462
|
+
// The acquisition plan can resolve to zero `<fetch>` rows when every
|
|
463
|
+
// integration the routine touches is disabled / cross-backend-bound
|
|
464
|
+
// on a connector-less integration / etc. Treat that as a skip — we
|
|
465
|
+
// don't pay the Haiku cold-start to confirm the agent has nothing
|
|
466
|
+
// to do.
|
|
467
|
+
if (!fetcherPlanHasFetches(fetcherEvent.data.acquisitionPlanBlock)) {
|
|
468
|
+
const report = {
|
|
469
|
+
status: "skipped",
|
|
470
|
+
fetched: 0,
|
|
471
|
+
posted: 0,
|
|
472
|
+
duplicates: 0,
|
|
473
|
+
errors: [],
|
|
474
|
+
skipped: true,
|
|
475
|
+
fetcherCorrelationId: fetcherEvent.correlationId,
|
|
476
|
+
};
|
|
477
|
+
const block = renderFetchReportBlock(report, { routine: key, agentDay });
|
|
478
|
+
logger.debug({
|
|
479
|
+
routine: key,
|
|
480
|
+
correlationId: fetcherEvent.correlationId,
|
|
481
|
+
parentCorrelationId: parentEvent.correlationId,
|
|
482
|
+
}, "Routine fetch-window pre-pass skipped — acquisition plan empty");
|
|
483
|
+
return { report, block };
|
|
484
|
+
}
|
|
485
|
+
// Execute the fetcher session through the standard router pipeline.
|
|
486
|
+
let context;
|
|
487
|
+
try {
|
|
488
|
+
context = await this.contextBuilder.build(fetcherEvent);
|
|
489
|
+
}
|
|
490
|
+
catch (err) {
|
|
491
|
+
return this.fail(key, agentDay, parentEvent, "context-build-failed", err, {
|
|
492
|
+
fetcherCorrelationId: fetcherEvent.correlationId,
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
let binding;
|
|
496
|
+
try {
|
|
497
|
+
binding = this.agentRouter.resolveBinding(fetcherEvent, {
|
|
498
|
+
processKey: FETCH_WINDOW_PROCESS_KEY,
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
catch (err) {
|
|
502
|
+
return this.fail(key, agentDay, parentEvent, "binding-resolve-failed", err, {
|
|
503
|
+
fetcherCorrelationId: fetcherEvent.correlationId,
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
const reassemblePrompt = (bid) => this.prompt.assemble(FETCH_WINDOW_EVENT_TYPE, FETCH_WINDOW_PROCESS_KEY, bid);
|
|
507
|
+
const prompt = reassemblePrompt(binding.main.backendId);
|
|
508
|
+
// Defense-in-depth tool clamp. Replaces the SDK's default
|
|
509
|
+
// allowlist with a narrow set covering only the surfaces the
|
|
510
|
+
// partials use. Backs the agent profile's "no notify, no context
|
|
511
|
+
// writes" guardrails with a daemon-enforced floor so a misbehaving
|
|
512
|
+
// Haiku turn cannot reach `/api/notify`, `/api/context/*`, or any
|
|
513
|
+
// tool outside the documented partial contract. Codex / Gemini
|
|
514
|
+
// ignore per-spawn `allowedToolsOverride` (CLAUDE.md §"Per-spawn
|
|
515
|
+
// tools surface gap"), but the `process_backend_config` envelope
|
|
516
|
+
// (max_turns / max_budget_usd) still applies.
|
|
517
|
+
const allowedToolsOverride = composePrePassAllowedTools(this.config.apiPort, integrationsSnapshot, binding.main.backendId);
|
|
518
|
+
let result;
|
|
519
|
+
try {
|
|
520
|
+
result = await this.agentRouter.execute({
|
|
521
|
+
prompt,
|
|
522
|
+
context,
|
|
523
|
+
event: fetcherEvent,
|
|
524
|
+
processKey: FETCH_WINDOW_PROCESS_KEY,
|
|
525
|
+
preResolvedBinding: binding,
|
|
526
|
+
reassemblePrompt,
|
|
527
|
+
allowedToolsOverride,
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
catch (err) {
|
|
531
|
+
return this.fail(key, agentDay, parentEvent, "agent-execute-failed", err, {
|
|
532
|
+
fetcherCorrelationId: fetcherEvent.correlationId,
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
// Audit row for the fetcher session itself, distinct from the
|
|
536
|
+
// parent routine's audit row written by the result processor.
|
|
537
|
+
try {
|
|
538
|
+
this.audit.logAction({
|
|
539
|
+
event: fetcherEvent,
|
|
540
|
+
model: result.model,
|
|
541
|
+
costUsd: result.costUsd,
|
|
542
|
+
usage: result.usage,
|
|
543
|
+
modelUsage: result.modelUsage,
|
|
544
|
+
durationMs: result.durationMs,
|
|
545
|
+
numTurns: result.numTurns,
|
|
546
|
+
trigger: "autonomous",
|
|
547
|
+
...(result.backendId ? { backend: result.backendId } : {}),
|
|
548
|
+
...(result.costSource ? { costSource: result.costSource } : {}),
|
|
549
|
+
contextUpdated: result.contextUpdated,
|
|
550
|
+
...(typeof result.advisorCallCount === "number"
|
|
551
|
+
? { advisorCallCount: result.advisorCallCount }
|
|
552
|
+
: {}),
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
catch (err) {
|
|
556
|
+
logger.warn({ err, routine: key, correlationId: fetcherEvent.correlationId }, "Failed to log routine.fetch_window agent_actions row");
|
|
557
|
+
}
|
|
558
|
+
const parsed = parseFetchWindowOutput(result.output);
|
|
559
|
+
if ("parseError" in parsed) {
|
|
560
|
+
const report = {
|
|
561
|
+
status: "failed",
|
|
562
|
+
fetched: 0,
|
|
563
|
+
posted: 0,
|
|
564
|
+
duplicates: 0,
|
|
565
|
+
errors: [{ type: "pre-pass-parse-failed", reason: parsed.parseError }],
|
|
566
|
+
skipped: false,
|
|
567
|
+
failureReason: parsed.parseError,
|
|
568
|
+
fetcherCorrelationId: fetcherEvent.correlationId,
|
|
569
|
+
};
|
|
570
|
+
const block = renderFetchReportBlock(report, { routine: key, agentDay });
|
|
571
|
+
logger.warn({
|
|
572
|
+
routine: key,
|
|
573
|
+
reason: parsed.parseError,
|
|
574
|
+
correlationId: fetcherEvent.correlationId,
|
|
575
|
+
parentCorrelationId: parentEvent.correlationId,
|
|
576
|
+
}, "Routine fetch-window pre-pass output unparsable — parent routine will see <fetch_report status='failed'>");
|
|
577
|
+
return { report, block };
|
|
578
|
+
}
|
|
579
|
+
const report = {
|
|
580
|
+
...parsed,
|
|
581
|
+
fetcherCorrelationId: fetcherEvent.correlationId,
|
|
582
|
+
};
|
|
583
|
+
const block = renderFetchReportBlock(report, { routine: key, agentDay });
|
|
584
|
+
logger.info({
|
|
585
|
+
routine: key,
|
|
586
|
+
status: report.status,
|
|
587
|
+
fetched: report.fetched,
|
|
588
|
+
posted: report.posted,
|
|
589
|
+
duplicates: report.duplicates,
|
|
590
|
+
errorCount: report.errors.length,
|
|
591
|
+
correlationId: fetcherEvent.correlationId,
|
|
592
|
+
parentCorrelationId: parentEvent.correlationId,
|
|
593
|
+
}, "Routine fetch-window pre-pass completed");
|
|
594
|
+
return { report, block };
|
|
595
|
+
}
|
|
596
|
+
/**
|
|
597
|
+
* Helper for the failure paths — renders a `<fetch_report status="failed">`
|
|
598
|
+
* block and logs the underlying error. Never throws so the caller can
|
|
599
|
+
* always continue with the parent routine dispatch.
|
|
600
|
+
*/
|
|
601
|
+
fail(routine, agentDay, parentEvent, kind, err, extra = {}) {
|
|
602
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
603
|
+
const report = {
|
|
604
|
+
status: "failed",
|
|
605
|
+
fetched: 0,
|
|
606
|
+
posted: 0,
|
|
607
|
+
duplicates: 0,
|
|
608
|
+
errors: [{ type: "pre-pass-failed", kind, message }],
|
|
609
|
+
skipped: false,
|
|
610
|
+
failureReason: `${kind}: ${message}`,
|
|
611
|
+
...(extra.fetcherCorrelationId
|
|
612
|
+
? { fetcherCorrelationId: extra.fetcherCorrelationId }
|
|
613
|
+
: {}),
|
|
614
|
+
};
|
|
615
|
+
const block = renderFetchReportBlock(report, { routine, agentDay });
|
|
616
|
+
logger.warn({
|
|
617
|
+
routine,
|
|
618
|
+
kind,
|
|
619
|
+
err,
|
|
620
|
+
parentCorrelationId: parentEvent.correlationId,
|
|
621
|
+
}, "Routine fetch-window pre-pass failed — parent routine will see <fetch_report status='failed'>");
|
|
622
|
+
return { report, block };
|
|
623
|
+
}
|
|
624
|
+
/**
|
|
625
|
+
* Translate the mail registry's active-account list into the
|
|
626
|
+
* `AcquisitionAccount[]` shape `buildAcquisitionPlan` expects. Only
|
|
627
|
+
* accounts whose integration is currently non-disabled survive — a
|
|
628
|
+
* disabled gmail integration with five accounts produces zero rows,
|
|
629
|
+
* matching the partial's `<!-- mode:disabled:gmail -->` no-op.
|
|
630
|
+
*/
|
|
631
|
+
collectAccounts(integrations) {
|
|
632
|
+
const rows = [];
|
|
633
|
+
for (const account of this.getActiveMailAccounts()) {
|
|
634
|
+
const integrationKey = mailAccountIntegrationKey(account);
|
|
635
|
+
if (integrationKey === null)
|
|
636
|
+
continue;
|
|
637
|
+
const state = integrations[integrationKey];
|
|
638
|
+
if (!state || state.mode === "disabled")
|
|
639
|
+
continue;
|
|
640
|
+
rows.push({
|
|
641
|
+
integration: integrationKey,
|
|
642
|
+
accountId: account.id,
|
|
643
|
+
label: account.email,
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
return rows;
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
// ── Internal helpers ──────────────────────────────────────────────────────
|
|
650
|
+
/**
|
|
651
|
+
* Return true when the rendered plan carries at least one `<fetch ...>`
|
|
652
|
+
* element. The plan is well-formed even when empty (the dispatcher
|
|
653
|
+
* always emits the wrapper) — we still want to short-circuit so the
|
|
654
|
+
* pre-pass cold-start never fires for a routine with nothing to do.
|
|
655
|
+
*/
|
|
656
|
+
function fetcherPlanHasFetches(plan) {
|
|
657
|
+
if (typeof plan !== "string")
|
|
658
|
+
return false;
|
|
659
|
+
return /<fetch\s/i.test(plan);
|
|
660
|
+
}
|
|
661
|
+
//# sourceMappingURL=routine-fetch-window-runner.js.map
|