@aitne/daemon 0.1.3 → 0.1.6
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/notification-manager.d.ts +12 -0
- package/dist/adapters/notification-manager.d.ts.map +1 -1
- package/dist/adapters/notification-manager.js +39 -1
- package/dist/adapters/notification-manager.js.map +1 -1
- 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 +25 -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/commands.d.ts.map +1 -1
- package/dist/api/routes/commands.js +16 -13
- package/dist/api/routes/commands.js.map +1 -1
- package/dist/api/routes/context.d.ts.map +1 -1
- package/dist/api/routes/context.js +26 -3
- 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 +103 -5
- package/dist/api/routes/dashboard.js.map +1 -1
- package/dist/api/routes/fs.d.ts +23 -0
- package/dist/api/routes/fs.d.ts.map +1 -0
- package/dist/api/routes/fs.js +156 -0
- package/dist/api/routes/fs.js.map +1 -0
- package/dist/api/routes/fs.logic.d.ts +62 -0
- package/dist/api/routes/fs.logic.d.ts.map +1 -0
- package/dist/api/routes/fs.logic.js +137 -0
- package/dist/api/routes/fs.logic.js.map +1 -0
- 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/health.d.ts.map +1 -1
- package/dist/api/routes/health.js +4 -2
- package/dist/api/routes/health.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 +192 -15
- 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/metrics.d.ts +1 -0
- package/dist/api/routes/metrics.d.ts.map +1 -1
- package/dist/api/routes/metrics.js +24 -0
- package/dist/api/routes/metrics.js.map +1 -1
- package/dist/api/routes/observations.d.ts.map +1 -1
- package/dist/api/routes/observations.js +696 -30
- 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 +9 -1
- package/dist/api/routes/skills.d.ts.map +1 -1
- package/dist/api/routes/skills.js +77 -17
- 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 +62 -4
- package/dist/api/routes/voice.js.map +1 -1
- package/dist/api/routes/wiki.d.ts +4 -0
- package/dist/api/routes/wiki.d.ts.map +1 -0
- package/dist/api/routes/wiki.js +1075 -0
- package/dist/api/routes/wiki.js.map +1 -0
- package/dist/api/server.d.ts +13 -0
- package/dist/api/server.d.ts.map +1 -1
- package/dist/api/server.js +27 -1
- package/dist/api/server.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/config.d.ts.map +1 -1
- package/dist/config.js +26 -0
- package/dist/config.js.map +1 -1
- package/dist/core/agent-core.d.ts +25 -0
- package/dist/core/agent-core.d.ts.map +1 -1
- package/dist/core/agent-core.js.map +1 -1
- package/dist/core/backends/backend-router.d.ts +28 -1
- package/dist/core/backends/backend-router.d.ts.map +1 -1
- package/dist/core/backends/backend-router.js +58 -4
- 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 +166 -1561
- 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 +1093 -0
- package/dist/core/backends/claude-tool-collection.js.map +1 -0
- package/dist/core/backends/codex-core.d.ts.map +1 -1
- package/dist/core/backends/codex-core.js +36 -0
- package/dist/core/backends/codex-core.js.map +1 -1
- package/dist/core/backends/gemini-cli-core.d.ts +45 -5
- package/dist/core/backends/gemini-cli-core.d.ts.map +1 -1
- package/dist/core/backends/gemini-cli-core.js +146 -36
- package/dist/core/backends/gemini-cli-core.js.map +1 -1
- package/dist/core/backends/plan-presets.d.ts +3 -1
- package/dist/core/backends/plan-presets.d.ts.map +1 -1
- package/dist/core/backends/plan-presets.js +42 -2
- package/dist/core/backends/plan-presets.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/bang-commands/commands-help.d.ts +5 -0
- package/dist/core/bang-commands/commands-help.d.ts.map +1 -0
- package/dist/core/bang-commands/commands-help.js +69 -0
- package/dist/core/bang-commands/commands-help.js.map +1 -0
- package/dist/core/bang-commands/commands-wiki.d.ts +75 -0
- package/dist/core/bang-commands/commands-wiki.d.ts.map +1 -0
- package/dist/core/bang-commands/commands-wiki.js +574 -0
- package/dist/core/bang-commands/commands-wiki.js.map +1 -0
- package/dist/core/bang-commands/index.d.ts +4 -2
- package/dist/core/bang-commands/index.d.ts.map +1 -1
- package/dist/core/bang-commands/index.js +15 -1
- package/dist/core/bang-commands/index.js.map +1 -1
- package/dist/core/bang-commands/registry.d.ts +47 -4
- package/dist/core/bang-commands/registry.d.ts.map +1 -1
- package/dist/core/bang-commands/registry.js +85 -15
- package/dist/core/bang-commands/registry.js.map +1 -1
- package/dist/core/context-builder.d.ts +53 -12
- package/dist/core/context-builder.d.ts.map +1 -1
- package/dist/core/context-builder.js +240 -92
- package/dist/core/context-builder.js.map +1 -1
- package/dist/core/daemon-api-cli.d.ts.map +1 -1
- package/dist/core/daemon-api-cli.js +50 -2
- package/dist/core/daemon-api-cli.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 +1064 -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 +449 -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 +168 -0
- package/dist/core/dispatcher-result-processor.d.ts.map +1 -0
- package/dist/core/dispatcher-result-processor.js +533 -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 +1032 -0
- package/dist/core/dispatcher-scheduled-tasks.js.map +1 -0
- package/dist/core/dispatcher-types.d.ts +411 -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 +122 -610
- package/dist/core/dispatcher.d.ts.map +1 -1
- package/dist/core/dispatcher.js +365 -3521
- 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 +163 -14
- 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/metrics.d.ts +127 -0
- package/dist/core/metrics.d.ts.map +1 -1
- package/dist/core/metrics.js +256 -1
- package/dist/core/metrics.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 +3 -1
- package/dist/core/prompts.d.ts.map +1 -1
- package/dist/core/prompts.js +161 -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/roadmap-validate.js +13 -1
- package/dist/core/roadmap-validate.js.map +1 -1
- package/dist/core/routine-acquisition-plan.d.ts +182 -0
- package/dist/core/routine-acquisition-plan.d.ts.map +1 -0
- package/dist/core/routine-acquisition-plan.js +367 -0
- package/dist/core/routine-acquisition-plan.js.map +1 -0
- package/dist/core/routine-fetch-window-retry.d.ts +109 -0
- package/dist/core/routine-fetch-window-retry.d.ts.map +1 -0
- package/dist/core/routine-fetch-window-retry.js +210 -0
- package/dist/core/routine-fetch-window-retry.js.map +1 -0
- package/dist/core/routine-fetch-window-runner.d.ts +427 -0
- package/dist/core/routine-fetch-window-runner.d.ts.map +1 -0
- package/dist/core/routine-fetch-window-runner.js +1591 -0
- package/dist/core/routine-fetch-window-runner.js.map +1 -0
- package/dist/core/routine-windows.d.ts +171 -0
- package/dist/core/routine-windows.d.ts.map +1 -0
- package/dist/core/routine-windows.js +377 -0
- package/dist/core/routine-windows.js.map +1 -0
- package/dist/core/scheduler.d.ts +50 -2
- package/dist/core/scheduler.d.ts.map +1 -1
- package/dist/core/scheduler.js +88 -7
- package/dist/core/scheduler.js.map +1 -1
- package/dist/core/skill-curation/declarations.d.ts.map +1 -1
- package/dist/core/skill-curation/declarations.js +11 -12
- package/dist/core/skill-curation/declarations.js.map +1 -1
- package/dist/core/skill-source-paths.d.ts +14 -0
- package/dist/core/skill-source-paths.d.ts.map +1 -0
- package/dist/core/skill-source-paths.js +82 -0
- package/dist/core/skill-source-paths.js.map +1 -0
- package/dist/core/skills-compiler.d.ts +29 -0
- package/dist/core/skills-compiler.d.ts.map +1 -1
- package/dist/core/skills-compiler.js +166 -30
- 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 +72 -0
- package/dist/core/skills-manifest.js.map +1 -1
- package/dist/core/system-reset.d.ts +25 -0
- package/dist/core/system-reset.d.ts.map +1 -1
- package/dist/core/system-reset.js +72 -2
- package/dist/core/system-reset.js.map +1 -1
- package/dist/core/wiki/approval-queue.d.ts +31 -0
- package/dist/core/wiki/approval-queue.d.ts.map +1 -0
- package/dist/core/wiki/approval-queue.js +44 -0
- package/dist/core/wiki/approval-queue.js.map +1 -0
- package/dist/core/wiki/bridge.d.ts +74 -0
- package/dist/core/wiki/bridge.d.ts.map +1 -0
- package/dist/core/wiki/bridge.js +405 -0
- package/dist/core/wiki/bridge.js.map +1 -0
- package/dist/core/wiki/compile-lock.d.ts +42 -0
- package/dist/core/wiki/compile-lock.d.ts.map +1 -0
- package/dist/core/wiki/compile-lock.js +55 -0
- package/dist/core/wiki/compile-lock.js.map +1 -0
- package/dist/core/wiki/compile-preview.d.ts +8 -0
- package/dist/core/wiki/compile-preview.d.ts.map +1 -0
- package/dist/core/wiki/compile-preview.js +200 -0
- package/dist/core/wiki/compile-preview.js.map +1 -0
- package/dist/core/wiki/cost-estimate.d.ts +30 -0
- package/dist/core/wiki/cost-estimate.d.ts.map +1 -0
- package/dist/core/wiki/cost-estimate.js +243 -0
- package/dist/core/wiki/cost-estimate.js.map +1 -0
- package/dist/core/wiki/dispatcher.d.ts +48 -0
- package/dist/core/wiki/dispatcher.d.ts.map +1 -0
- package/dist/core/wiki/dispatcher.js +92 -0
- package/dist/core/wiki/dispatcher.js.map +1 -0
- package/dist/core/wiki/git-precompile.d.ts +86 -0
- package/dist/core/wiki/git-precompile.d.ts.map +1 -0
- package/dist/core/wiki/git-precompile.js +96 -0
- package/dist/core/wiki/git-precompile.js.map +1 -0
- package/dist/core/wiki/import-migrate.d.ts +38 -0
- package/dist/core/wiki/import-migrate.d.ts.map +1 -0
- package/dist/core/wiki/import-migrate.js +310 -0
- package/dist/core/wiki/import-migrate.js.map +1 -0
- package/dist/core/wiki/import-probe.d.ts +76 -0
- package/dist/core/wiki/import-probe.d.ts.map +1 -0
- package/dist/core/wiki/import-probe.js +245 -0
- package/dist/core/wiki/import-probe.js.map +1 -0
- package/dist/core/wiki/index-cache.d.ts +39 -0
- package/dist/core/wiki/index-cache.d.ts.map +1 -0
- package/dist/core/wiki/index-cache.js +152 -0
- package/dist/core/wiki/index-cache.js.map +1 -0
- package/dist/core/wiki/multi-url-dispatch.d.ts +52 -0
- package/dist/core/wiki/multi-url-dispatch.d.ts.map +1 -0
- package/dist/core/wiki/multi-url-dispatch.js +72 -0
- package/dist/core/wiki/multi-url-dispatch.js.map +1 -0
- package/dist/core/wiki/wiki-fts.d.ts +75 -0
- package/dist/core/wiki/wiki-fts.d.ts.map +1 -0
- package/dist/core/wiki/wiki-fts.js +265 -0
- package/dist/core/wiki/wiki-fts.js.map +1 -0
- package/dist/core/wiki/workspaces.d.ts +101 -0
- package/dist/core/wiki/workspaces.d.ts.map +1 -0
- package/dist/core/wiki/workspaces.js +352 -0
- package/dist/core/wiki/workspaces.js.map +1 -0
- package/dist/core/wiki/write-strategy.d.ts +70 -0
- package/dist/core/wiki/write-strategy.d.ts.map +1 -0
- package/dist/core/wiki/write-strategy.js +112 -0
- package/dist/core/wiki/write-strategy.js.map +1 -0
- package/dist/core/workdir.d.ts +8 -1
- package/dist/core/workdir.d.ts.map +1 -1
- package/dist/core/workdir.js +4 -1
- package/dist/core/workdir.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 +135 -25
- package/dist/db/schema.js.map +1 -1
- package/dist/db/wiki-store.d.ts +3 -0
- package/dist/db/wiki-store.d.ts.map +1 -0
- package/dist/db/wiki-store.js +7 -0
- package/dist/db/wiki-store.js.map +1 -0
- package/dist/index.js +159 -610
- package/dist/index.js.map +1 -1
- package/dist/messaging/url-extract.d.ts +8 -0
- package/dist/messaging/url-extract.d.ts.map +1 -0
- package/dist/messaging/url-extract.js +41 -0
- package/dist/messaging/url-extract.js.map +1 -0
- package/dist/observers/delegated-sync-worker.d.ts +52 -1
- package/dist/observers/delegated-sync-worker.d.ts.map +1 -1
- package/dist/observers/delegated-sync-worker.js +75 -18
- package/dist/observers/delegated-sync-worker.js.map +1 -1
- package/dist/observers/imminent-event-scheduler.d.ts +20 -7
- package/dist/observers/imminent-event-scheduler.d.ts.map +1 -1
- package/dist/observers/imminent-event-scheduler.js +134 -29
- package/dist/observers/imminent-event-scheduler.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/always-disallowed.d.ts +65 -0
- package/dist/safety/always-disallowed.d.ts.map +1 -1
- package/dist/safety/always-disallowed.js +106 -10
- package/dist/safety/always-disallowed.js.map +1 -1
- package/dist/safety/audit.d.ts +46 -1
- package/dist/safety/audit.d.ts.map +1 -1
- package/dist/safety/audit.js +79 -16
- package/dist/safety/audit.js.map +1 -1
- package/dist/safety/risk-classifier.d.ts.map +1 -1
- package/dist/safety/risk-classifier.js +29 -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 +7 -8
- package/dist/services/voice/transcriber-impl.js.map +1 -1
- package/dist/settings/runtime-settings.d.ts +12 -1
- package/dist/settings/runtime-settings.d.ts.map +1 -1
- package/dist/settings/runtime-settings.js +59 -1
- package/dist/settings/runtime-settings.js.map +1 -1
- package/package.json +2 -2
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { Hono } from "hono";
|
|
2
|
+
import { INTEGRATION_KEYS, isIntegrationKey, } from "@aitne/shared";
|
|
2
3
|
import { consumeObservations, getNoveltyDistribution, getObservationStats, getPendingObservations, getSummaryStatusCounts, recordObservation, } from "../../db/observations.js";
|
|
4
|
+
import { readIntegrationFlipLock } from "../../core/integration-lifecycle.js";
|
|
3
5
|
import { createLogger } from "../../logging.js";
|
|
4
|
-
import { readJsonBody } from "../json-body.js";
|
|
6
|
+
import { DEFAULT_JSON_BODY_MAX_BYTES, readJsonBody } from "../json-body.js";
|
|
5
7
|
const logger = createLogger("observations-api");
|
|
6
8
|
function parseBoolean(value, defaultValue) {
|
|
7
9
|
if (value === undefined)
|
|
@@ -22,6 +24,80 @@ function parsePayload(payload) {
|
|
|
22
24
|
return payload;
|
|
23
25
|
}
|
|
24
26
|
}
|
|
27
|
+
/**
|
|
28
|
+
* ROUTINE_DATA_ACQUISITION_DESIGN.md §6.7 — turn the comma-separated
|
|
29
|
+
* `?source_prefix=gmail:,outlook_mail:` query string into a list the
|
|
30
|
+
* db helper can OR via per-prefix `LIKE` predicates. Returns
|
|
31
|
+
* `undefined` (not `[]`) when the param is absent so the db helper can
|
|
32
|
+
* cleanly distinguish "no multi-prefix filter requested" from "empty
|
|
33
|
+
* prefix list".
|
|
34
|
+
*
|
|
35
|
+
* Empty / whitespace-only segments are dropped here (the db helper
|
|
36
|
+
* trims again as a defense-in-depth measure) — a caller passing
|
|
37
|
+
* `?source_prefix=,gmail:,` shouldn't widen the query to every row.
|
|
38
|
+
*/
|
|
39
|
+
function parseSourcePrefixes(raw) {
|
|
40
|
+
if (raw === undefined)
|
|
41
|
+
return undefined;
|
|
42
|
+
const parts = raw
|
|
43
|
+
.split(",")
|
|
44
|
+
.map((p) => p.trim())
|
|
45
|
+
.filter((p) => p.length > 0);
|
|
46
|
+
return parts.length > 0 ? parts : undefined;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* ROUTINE_DATA_ACQUISITION_DESIGN.md CR2 — resolve `since=` /
|
|
50
|
+
* `observed_at_after=` from the request, validate the format, and
|
|
51
|
+
* coerce empty strings to `undefined` so the db filter is skipped
|
|
52
|
+
* rather than emitted as `datetime(observed_at) >= datetime("")`
|
|
53
|
+
* (which evaluates to NULL and silently drops every row).
|
|
54
|
+
*
|
|
55
|
+
* Returns:
|
|
56
|
+
* - `{ ok: true, value: undefined }` — neither param present, or
|
|
57
|
+
* both were empty / whitespace-only.
|
|
58
|
+
* - `{ ok: true, value: "<iso>" }` — a non-empty, parseable timestamp
|
|
59
|
+
* (canonical `since` wins over the `observed_at_after` alias when
|
|
60
|
+
* both are supplied).
|
|
61
|
+
* - `{ ok: false, raw, param }` — a non-empty value was provided but
|
|
62
|
+
* `Date.parse` rejected it. The route turns this into 400.
|
|
63
|
+
*/
|
|
64
|
+
function resolveSinceParam(rawSince, rawAlias) {
|
|
65
|
+
const sinceClean = rawSince !== undefined ? rawSince.trim() : "";
|
|
66
|
+
const aliasClean = rawAlias !== undefined ? rawAlias.trim() : "";
|
|
67
|
+
if (sinceClean.length > 0) {
|
|
68
|
+
return Number.isFinite(Date.parse(sinceClean))
|
|
69
|
+
? { ok: true, value: sinceClean }
|
|
70
|
+
: { ok: false, raw: sinceClean, param: "since" };
|
|
71
|
+
}
|
|
72
|
+
if (aliasClean.length > 0) {
|
|
73
|
+
return Number.isFinite(Date.parse(aliasClean))
|
|
74
|
+
? { ok: true, value: aliasClean }
|
|
75
|
+
: { ok: false, raw: aliasClean, param: "observed_at_after" };
|
|
76
|
+
}
|
|
77
|
+
return { ok: true, value: undefined };
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* INTEGRATION_NATIVE_MODE_DESIGN.md §11.3.1 — map an observation `source`
|
|
81
|
+
* string to the integration key whose flip lock would gate writes against
|
|
82
|
+
* it. The agent's hourly_check / native-mode skill always uses one of the
|
|
83
|
+
* registry's integration keys verbatim as the `source` value (e.g.
|
|
84
|
+
* `"gmail"`, `"google_calendar"`, `"notion"`). Sources outside the
|
|
85
|
+
* registry (Obsidian, Git, messaging adapter, system) are never locked
|
|
86
|
+
* and return null.
|
|
87
|
+
*
|
|
88
|
+
* Matches the exact key first, then falls back to a colon prefix (e.g.
|
|
89
|
+
* `"gmail:account-1"`) so per-account / per-database source values still
|
|
90
|
+
* resolve. Returns null for anything else.
|
|
91
|
+
*/
|
|
92
|
+
function inferIntegrationKeyFromSource(source) {
|
|
93
|
+
if (isIntegrationKey(source))
|
|
94
|
+
return source;
|
|
95
|
+
for (const key of INTEGRATION_KEYS) {
|
|
96
|
+
if (source.startsWith(`${key}:`))
|
|
97
|
+
return key;
|
|
98
|
+
}
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
25
101
|
/**
|
|
26
102
|
* cost-reduction-structural §A "Failure modes" — the summary may be
|
|
27
103
|
* outdated when the worker lags far behind the observation moment (e.g.
|
|
@@ -63,18 +139,36 @@ export function createObservationRoutes(deps) {
|
|
|
63
139
|
const limit = Math.min(Math.max(parseNumber(c.req.query("limit"), 20), 1), 100);
|
|
64
140
|
const offset = Math.max(parseNumber(c.req.query("offset"), 0), 0);
|
|
65
141
|
const source = c.req.query("source");
|
|
142
|
+
const sourcePrefixRaw = c.req.query("source_prefix");
|
|
66
143
|
const actor = c.req.query("actor");
|
|
67
|
-
|
|
144
|
+
// ROUTINE_DATA_ACQUISITION_DESIGN.md §6.7 / CR2 — `since=` is the
|
|
145
|
+
// canonical name; `observed_at_after=` is the routine-side alias
|
|
146
|
+
// (`since` wins when both are supplied). Empty / whitespace-only
|
|
147
|
+
// values are coerced to "no filter" rather than `datetime("")
|
|
148
|
+
// → NULL → drop all rows", and an unparseable string returns 400
|
|
149
|
+
// so misconfigured callers fail loud instead of silently filtering
|
|
150
|
+
// everything out.
|
|
151
|
+
const sinceResolution = resolveSinceParam(c.req.query("since"), c.req.query("observed_at_after"));
|
|
152
|
+
if (!sinceResolution.ok) {
|
|
153
|
+
return c.json({
|
|
154
|
+
error: "invalid_since",
|
|
155
|
+
param: sinceResolution.param,
|
|
156
|
+
value: sinceResolution.raw,
|
|
157
|
+
message: `'${sinceResolution.param}' must be an ISO 8601 timestamp or SQL UTC datetime`,
|
|
158
|
+
}, 400);
|
|
159
|
+
}
|
|
68
160
|
if (actor && !["user", "agent", "system", "unknown"].includes(actor)) {
|
|
69
161
|
return c.json({ error: "invalid_actor" }, 400);
|
|
70
162
|
}
|
|
163
|
+
const sourceFilterPrefixes = parseSourcePrefixes(sourcePrefixRaw);
|
|
71
164
|
const observations = getPendingObservations(db, {
|
|
72
165
|
pending,
|
|
73
166
|
limit,
|
|
74
167
|
offset,
|
|
75
168
|
sourceFilter: source,
|
|
169
|
+
sourceFilterPrefixes,
|
|
76
170
|
actorFilter: actor,
|
|
77
|
-
since,
|
|
171
|
+
since: sinceResolution.value,
|
|
78
172
|
}).map((row) => ({
|
|
79
173
|
id: row.id,
|
|
80
174
|
source: row.source,
|
|
@@ -104,61 +198,633 @@ export function createObservationRoutes(deps) {
|
|
|
104
198
|
*
|
|
105
199
|
* Used by `routine.hourly_check` to queue `roadmap_candidate` signals
|
|
106
200
|
* (long-horizon intents too weak to write to roadmap.md directly;
|
|
107
|
-
* ROADMAP-REDESIGN §3.4 RFC-C)
|
|
108
|
-
*
|
|
109
|
-
*
|
|
201
|
+
* ROADMAP-REDESIGN §3.4 RFC-C) AND by INTEGRATION_NATIVE_MODE_DESIGN.md
|
|
202
|
+
* §8.3 native-mode hourly_check turns to persist the materialised mail
|
|
203
|
+
* thread / calendar event list the agent just fetched via the main
|
|
204
|
+
* backend's MCP. The DB layer UPSERTs on `(source, ref)` where
|
|
205
|
+
* `consumed_at IS NULL`, so re-posting the same candidate across hourly
|
|
206
|
+
* ticks coalesces instead of duplicating.
|
|
207
|
+
*
|
|
208
|
+
* §8.3 server-side hash: the daemon computes `contentHash` from the
|
|
209
|
+
* canonical payload via the shared util in `@aitne/shared/observations-hash`
|
|
210
|
+
* and returns it in the response. Pollers and the delegated-sync-worker
|
|
211
|
+
* route through the same util so hashes are comparable across modes — a
|
|
212
|
+
* `delegated → native` flip dedups against pre-flip observations.
|
|
110
213
|
*
|
|
111
214
|
* Actor defaults to `agent` here — the user and system channels have
|
|
112
215
|
* their own writers (vault watchers, mail poller, etc.). Permitting
|
|
113
216
|
* only `agent` / `system` guards against prompt-injection attempts to
|
|
114
217
|
* forge user-authored observations.
|
|
218
|
+
*
|
|
219
|
+
* §11.3.1 defensive lock-window check: if a mode-flip is currently in
|
|
220
|
+
* progress for the integration owning this `source`, reject the write
|
|
221
|
+
* with 409 to prevent straggler observations from landing under the
|
|
222
|
+
* old mode label. Sources outside the integration registry (Obsidian,
|
|
223
|
+
* Git, messaging adapter) are never rejected.
|
|
115
224
|
*/
|
|
225
|
+
const RECORD_EXPECTED_SHAPE = '{"source": string, "ref": string, "changeType"?: "created"|"modified"|"deleted", "actor"?: "agent"|"system", "payload"?: unknown}';
|
|
226
|
+
const RECORD_EXAMPLE = '{"source":"roadmap_candidate:travel","ref":"trip-portland-2026-summer","changeType":"created","actor":"agent","payload":{"note":"DM mentioned Portland trip"}}';
|
|
116
227
|
app.post("/observations", async (c) => {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
228
|
+
// Peek at the raw body BEFORE delegating to `readJsonBody` so we can
|
|
229
|
+
// turn a query-string-shaped body ("limit=30", "actor=user&limit=20")
|
|
230
|
+
// into a method-confusion hint. Production telemetry showed the
|
|
231
|
+
// hourly_check agent sending `POST /api/observations` with body
|
|
232
|
+
// `limit=30`, expecting it to fetch. Forwarding readJsonBody's
|
|
233
|
+
// generic "Unexpected token 'l'" message gave the agent no signal
|
|
234
|
+
// that the right call was `GET /api/observations?limit=30`.
|
|
235
|
+
//
|
|
236
|
+
// Size cap mirrors `readJsonBody`'s defense-in-depth: declared
|
|
237
|
+
// Content-Length AND post-read byteLength are both checked against
|
|
238
|
+
// `DEFAULT_JSON_BODY_MAX_BYTES` (1 MiB). Without this an inline
|
|
239
|
+
// reader would happily buffer a malicious 10 MB body and pin RAM.
|
|
240
|
+
const declared = c.req.header("content-length");
|
|
241
|
+
if (declared !== undefined) {
|
|
242
|
+
const declaredN = Number.parseInt(declared, 10);
|
|
243
|
+
if (Number.isFinite(declaredN) && declaredN > DEFAULT_JSON_BODY_MAX_BYTES) {
|
|
244
|
+
return c.json({
|
|
245
|
+
error: "body_too_large",
|
|
246
|
+
maxBytes: DEFAULT_JSON_BODY_MAX_BYTES,
|
|
247
|
+
actualBytes: declaredN,
|
|
248
|
+
}, 413);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
let raw;
|
|
252
|
+
try {
|
|
253
|
+
raw = await c.req.text();
|
|
254
|
+
}
|
|
255
|
+
catch (err) {
|
|
256
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
257
|
+
return c.json({ error: "invalid_json_body", message: detail }, 400);
|
|
258
|
+
}
|
|
259
|
+
const actualBytes = Buffer.byteLength(raw, "utf-8");
|
|
260
|
+
if (actualBytes > DEFAULT_JSON_BODY_MAX_BYTES) {
|
|
261
|
+
return c.json({
|
|
262
|
+
error: "body_too_large",
|
|
263
|
+
maxBytes: DEFAULT_JSON_BODY_MAX_BYTES,
|
|
264
|
+
actualBytes,
|
|
265
|
+
}, 413);
|
|
266
|
+
}
|
|
267
|
+
const trimmed = raw.trim();
|
|
268
|
+
if (trimmed.length > 0 && trimmed[0] !== "{" && trimmed[0] !== "[") {
|
|
269
|
+
// A bare `key=value(&key=value)+` body shape is unambiguous:
|
|
270
|
+
// there is no JSON document that starts with a bare identifier
|
|
271
|
+
// followed by `=`. Suggest the GET form verbatim so the agent
|
|
272
|
+
// can copy-paste it.
|
|
273
|
+
if (/^[A-Za-z_][A-Za-z0-9_]*=/.test(trimmed)) {
|
|
274
|
+
return c.json({
|
|
275
|
+
error: "method_confusion",
|
|
276
|
+
message: "POST /api/observations records a new observation (JSON body). Your body looks like a query string — did you mean to GET?",
|
|
277
|
+
hint: `Use GET /api/observations?${trimmed} to fetch, or POST with a JSON body matching expectedShape to record.`,
|
|
278
|
+
expectedShape: RECORD_EXPECTED_SHAPE,
|
|
279
|
+
example: RECORD_EXAMPLE,
|
|
280
|
+
}, 400);
|
|
281
|
+
}
|
|
282
|
+
return c.json({
|
|
283
|
+
error: "invalid_json_body",
|
|
284
|
+
message: `Body must be a JSON object starting with '{' — received '${trimmed.slice(0, 32)}…'`,
|
|
285
|
+
expectedShape: RECORD_EXPECTED_SHAPE,
|
|
286
|
+
example: RECORD_EXAMPLE,
|
|
287
|
+
}, 400);
|
|
288
|
+
}
|
|
289
|
+
let body;
|
|
290
|
+
try {
|
|
291
|
+
body = trimmed.length === 0 ? null : JSON.parse(trimmed);
|
|
292
|
+
}
|
|
293
|
+
catch (err) {
|
|
294
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
295
|
+
return c.json({
|
|
296
|
+
error: "invalid_json_body",
|
|
297
|
+
message: detail,
|
|
298
|
+
expectedShape: RECORD_EXPECTED_SHAPE,
|
|
299
|
+
example: RECORD_EXAMPLE,
|
|
300
|
+
}, 400);
|
|
301
|
+
}
|
|
302
|
+
const issues = [];
|
|
303
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) {
|
|
304
|
+
return c.json({
|
|
305
|
+
error: "validation_error",
|
|
306
|
+
message: "Body must be a JSON object",
|
|
307
|
+
expectedShape: RECORD_EXPECTED_SHAPE,
|
|
308
|
+
example: RECORD_EXAMPLE,
|
|
309
|
+
}, 400);
|
|
310
|
+
}
|
|
311
|
+
if (typeof body.source !== "string" || body.source.length === 0) {
|
|
312
|
+
issues.push({
|
|
313
|
+
field: "source",
|
|
314
|
+
expected: "non-empty string",
|
|
315
|
+
got: body.source === undefined ? "missing" : typeof body.source,
|
|
316
|
+
hint: "Use a registry-aware prefix like 'roadmap_candidate:<subkind>' or an integration key like 'gmail:<account>'",
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
if (typeof body.ref !== "string" || body.ref.length === 0) {
|
|
320
|
+
issues.push({
|
|
321
|
+
field: "ref",
|
|
322
|
+
expected: "non-empty string",
|
|
323
|
+
got: body.ref === undefined ? "missing" : typeof body.ref,
|
|
324
|
+
hint: "A stable identifier within the source — e.g. message id, file path, candidate slug",
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
if (typeof body.changeType !== "string" &&
|
|
328
|
+
body.changeType !== undefined) {
|
|
329
|
+
issues.push({
|
|
330
|
+
field: "changeType",
|
|
331
|
+
expected: "'created' | 'modified' | 'deleted' (optional, defaults to 'created')",
|
|
332
|
+
got: typeof body.changeType,
|
|
333
|
+
});
|
|
127
334
|
}
|
|
335
|
+
if (issues.length > 0) {
|
|
336
|
+
return c.json({
|
|
337
|
+
error: "validation_error",
|
|
338
|
+
message: "Request body failed schema validation",
|
|
339
|
+
expectedShape: RECORD_EXPECTED_SHAPE,
|
|
340
|
+
example: RECORD_EXAMPLE,
|
|
341
|
+
issues,
|
|
342
|
+
}, 400);
|
|
343
|
+
}
|
|
344
|
+
// The `issues` array above guarantees `body.source` and `body.ref`
|
|
345
|
+
// are non-empty strings; cast for the rest of the handler.
|
|
346
|
+
const source = body.source;
|
|
347
|
+
const ref = body.ref;
|
|
128
348
|
const changeType = typeof body.changeType === "string" ? body.changeType : "created";
|
|
129
349
|
if (!["created", "modified", "deleted"].includes(changeType)) {
|
|
130
|
-
return c.json({
|
|
350
|
+
return c.json({
|
|
351
|
+
error: "invalid_change_type",
|
|
352
|
+
message: `'changeType' must be one of 'created', 'modified', 'deleted' — received '${changeType}'`,
|
|
353
|
+
hint: "Omit the field to default to 'created'",
|
|
354
|
+
}, 400);
|
|
131
355
|
}
|
|
132
356
|
const actor = typeof body.actor === "string" ? body.actor : "agent";
|
|
133
357
|
if (!["agent", "system"].includes(actor)) {
|
|
134
|
-
return c.json({
|
|
358
|
+
return c.json({
|
|
359
|
+
error: "invalid_actor",
|
|
360
|
+
message: `'actor' must be 'agent' or 'system' — received '${actor}'`,
|
|
361
|
+
hint: "User-originated observations come in through the vault / mail watchers; this endpoint only accepts agent/system writes",
|
|
362
|
+
}, 400);
|
|
363
|
+
}
|
|
364
|
+
// §11.3.1 — defensive flip-lock check. The integration key inferred
|
|
365
|
+
// from the source is the same key the PATCH route would have locked.
|
|
366
|
+
// If a flip is mid-flight, return 409 so the agent retries after the
|
|
367
|
+
// drain completes. Sources that don't map to a registered integration
|
|
368
|
+
// are pass-through (Obsidian / Git / messaging never lock).
|
|
369
|
+
const lockedKey = inferIntegrationKeyFromSource(source);
|
|
370
|
+
if (lockedKey) {
|
|
371
|
+
const lock = readIntegrationFlipLock(db, lockedKey);
|
|
372
|
+
if (lock) {
|
|
373
|
+
logger.warn({ source: body.source, ref: body.ref, lockedKey, lock }, "Observation write rejected — integration flip lock held");
|
|
374
|
+
return c.json({
|
|
375
|
+
error: "integration_flip_in_progress",
|
|
376
|
+
integration: lockedKey,
|
|
377
|
+
heldBy: lock,
|
|
378
|
+
message: `A mode flip for '${lockedKey}' is in progress; retry shortly.`,
|
|
379
|
+
}, 409);
|
|
380
|
+
}
|
|
135
381
|
}
|
|
136
|
-
recordObservation(db, {
|
|
137
|
-
source
|
|
138
|
-
ref
|
|
382
|
+
const result = recordObservation(db, {
|
|
383
|
+
source,
|
|
384
|
+
ref,
|
|
139
385
|
changeType: changeType,
|
|
140
386
|
actor: actor,
|
|
141
387
|
payload: body.payload,
|
|
142
388
|
});
|
|
143
|
-
logger.info({
|
|
144
|
-
|
|
389
|
+
logger.info({
|
|
390
|
+
source,
|
|
391
|
+
ref,
|
|
392
|
+
actor,
|
|
393
|
+
contentHash: result.contentHash,
|
|
394
|
+
action: result.action,
|
|
395
|
+
}, "Observation recorded via API");
|
|
396
|
+
// ROUTINE_DATA_ACQUISITION_DESIGN.md CR1 — surface payload-identical
|
|
397
|
+
// re-posts as 409 so the routine pre-pass fetcher's JSON return
|
|
398
|
+
// shape (`{"fetched":N,"posted":M,"duplicates":K,"errors":[…]}`)
|
|
399
|
+
// can count them. The 409 body's `error` field distinguishes this
|
|
400
|
+
// case from the §11.3.1 `integration_flip_in_progress` 409 above;
|
|
401
|
+
// callers that don't care about the distinction still read `error`
|
|
402
|
+
// before counting.
|
|
403
|
+
if (result.action === "duplicate") {
|
|
404
|
+
return c.json({
|
|
405
|
+
error: "duplicate",
|
|
406
|
+
contentHash: result.contentHash,
|
|
407
|
+
id: result.id,
|
|
408
|
+
message: "Same (source, ref) pending row already stores this payload",
|
|
409
|
+
}, 409);
|
|
410
|
+
}
|
|
411
|
+
return c.json({
|
|
412
|
+
ok: true,
|
|
413
|
+
contentHash: result.contentHash,
|
|
414
|
+
id: result.id,
|
|
415
|
+
action: result.action,
|
|
416
|
+
});
|
|
417
|
+
});
|
|
418
|
+
/**
|
|
419
|
+
* POST /observations/batch — record many agent-originated observations in a
|
|
420
|
+
* single transaction.
|
|
421
|
+
*
|
|
422
|
+
* Reason this exists: the routine.fetch_window pre-pass on Haiku posts
|
|
423
|
+
* many observations per integration window (~20 mail messages, ~6 calendar
|
|
424
|
+
* events). Calling `POST /observations` once per item collided with two
|
|
425
|
+
* orthogonal safety layers in `claude-tool-collection.ts:bashCurlHook`:
|
|
426
|
+
*
|
|
427
|
+
* 1. The "one curl per Bash invocation" cap blocks `cat | bash`, chained
|
|
428
|
+
* `curl … ; curl …`, and `for` loops containing curl.
|
|
429
|
+
* 2. URL extraction strips heredoc bodies before validation, so a
|
|
430
|
+
* `cat > /tmp/script.sh << 'EOF' … curl http://localhost:8321/… EOF`
|
|
431
|
+
* batching shape blocks with "curl command must contain an explicit
|
|
432
|
+
* localhost URL" — the URL lives in the stdin payload, not argv.
|
|
433
|
+
*
|
|
434
|
+
* Production telemetry on 2026-05-13 morning routine showed Haiku
|
|
435
|
+
* burning four budget cycles and posting zero observations because every
|
|
436
|
+
* batching shape it tried was blocked, leaving `today.md` empty. The
|
|
437
|
+
* single-curl-with-array endpoint resolves the cardinality mismatch
|
|
438
|
+
* without weakening either hook.
|
|
439
|
+
*
|
|
440
|
+
* Body: `{ "observations": [...] }` with up to 200 entries per call.
|
|
441
|
+
* Per-item validation mirrors POST /observations exactly. The whole
|
|
442
|
+
* batch executes inside one `db.transaction()`; any per-item failure is
|
|
443
|
+
* recorded in the response and the rest of the batch proceeds.
|
|
444
|
+
*
|
|
445
|
+
* Response is always 200 (or 400 for a malformed envelope). Per-item
|
|
446
|
+
* outcomes live in `results[*].status` so the agent doesn't retry the
|
|
447
|
+
* whole batch on a partial failure.
|
|
448
|
+
*/
|
|
449
|
+
const BATCH_MAX_OBSERVATIONS = 200;
|
|
450
|
+
const BATCH_EXPECTED_SHAPE = '{"observations": [{"source": string, "ref": string, "changeType"?: "created"|"modified"|"deleted", "actor"?: "agent"|"system", "payload"?: unknown}, ...]}';
|
|
451
|
+
const BATCH_EXAMPLE = '{"observations":[{"source":"google_calendar:primary","ref":"evt-1","payload":{"kind":"calendar","providerId":"primary","raw":{"title":"…"}}},{"source":"google_calendar:primary","ref":"evt-2","payload":{"kind":"calendar","providerId":"primary","raw":{"title":"…"}}}]}';
|
|
452
|
+
function validateBatchItem(item, index) {
|
|
453
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) {
|
|
454
|
+
return {
|
|
455
|
+
ok: false,
|
|
456
|
+
result: {
|
|
457
|
+
index,
|
|
458
|
+
status: "validation_error",
|
|
459
|
+
error: "item must be a JSON object",
|
|
460
|
+
hint: BATCH_EXPECTED_SHAPE,
|
|
461
|
+
},
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
const obj = item;
|
|
465
|
+
if (typeof obj.source !== "string" || obj.source.length === 0) {
|
|
466
|
+
return {
|
|
467
|
+
ok: false,
|
|
468
|
+
result: {
|
|
469
|
+
index,
|
|
470
|
+
status: "validation_error",
|
|
471
|
+
error: "'source' must be a non-empty string",
|
|
472
|
+
hint: "Use 'gmail:<account>', 'google_calendar:<calendarId>', 'notion:<dbId>', etc.",
|
|
473
|
+
},
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
if (typeof obj.ref !== "string" || obj.ref.length === 0) {
|
|
477
|
+
return {
|
|
478
|
+
ok: false,
|
|
479
|
+
result: {
|
|
480
|
+
index,
|
|
481
|
+
status: "validation_error",
|
|
482
|
+
source: obj.source,
|
|
483
|
+
error: "'ref' must be a non-empty string",
|
|
484
|
+
hint: "Stable id within the source — e.g. message id, event id",
|
|
485
|
+
},
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
const changeType = typeof obj.changeType === "string" ? obj.changeType : "created";
|
|
489
|
+
if (!["created", "modified", "deleted"].includes(changeType)) {
|
|
490
|
+
return {
|
|
491
|
+
ok: false,
|
|
492
|
+
result: {
|
|
493
|
+
index,
|
|
494
|
+
status: "validation_error",
|
|
495
|
+
source: obj.source,
|
|
496
|
+
ref: obj.ref,
|
|
497
|
+
error: `'changeType' must be 'created'|'modified'|'deleted' — received '${changeType}'`,
|
|
498
|
+
},
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
const actor = typeof obj.actor === "string" ? obj.actor : "agent";
|
|
502
|
+
if (!["agent", "system"].includes(actor)) {
|
|
503
|
+
return {
|
|
504
|
+
ok: false,
|
|
505
|
+
result: {
|
|
506
|
+
index,
|
|
507
|
+
status: "validation_error",
|
|
508
|
+
source: obj.source,
|
|
509
|
+
ref: obj.ref,
|
|
510
|
+
error: `'actor' must be 'agent' or 'system' — received '${actor}'`,
|
|
511
|
+
},
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
return {
|
|
515
|
+
ok: true,
|
|
516
|
+
source: obj.source,
|
|
517
|
+
ref: obj.ref,
|
|
518
|
+
changeType: changeType,
|
|
519
|
+
actor: actor,
|
|
520
|
+
payload: obj.payload,
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
app.post("/observations/batch", async (c) => {
|
|
524
|
+
// Size-cap defense mirrors POST /observations.
|
|
525
|
+
const declared = c.req.header("content-length");
|
|
526
|
+
if (declared !== undefined) {
|
|
527
|
+
const declaredN = Number.parseInt(declared, 10);
|
|
528
|
+
if (Number.isFinite(declaredN) && declaredN > DEFAULT_JSON_BODY_MAX_BYTES) {
|
|
529
|
+
return c.json({
|
|
530
|
+
error: "body_too_large",
|
|
531
|
+
maxBytes: DEFAULT_JSON_BODY_MAX_BYTES,
|
|
532
|
+
actualBytes: declaredN,
|
|
533
|
+
}, 413);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
let raw;
|
|
537
|
+
try {
|
|
538
|
+
raw = await c.req.text();
|
|
539
|
+
}
|
|
540
|
+
catch (err) {
|
|
541
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
542
|
+
return c.json({ error: "invalid_json_body", message: detail }, 400);
|
|
543
|
+
}
|
|
544
|
+
const actualBytes = Buffer.byteLength(raw, "utf-8");
|
|
545
|
+
if (actualBytes > DEFAULT_JSON_BODY_MAX_BYTES) {
|
|
546
|
+
return c.json({
|
|
547
|
+
error: "body_too_large",
|
|
548
|
+
maxBytes: DEFAULT_JSON_BODY_MAX_BYTES,
|
|
549
|
+
actualBytes,
|
|
550
|
+
}, 413);
|
|
551
|
+
}
|
|
552
|
+
let envelope;
|
|
553
|
+
try {
|
|
554
|
+
envelope = raw.trim().length === 0 ? null : JSON.parse(raw);
|
|
555
|
+
}
|
|
556
|
+
catch (err) {
|
|
557
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
558
|
+
return c.json({
|
|
559
|
+
error: "invalid_json_body",
|
|
560
|
+
message: detail,
|
|
561
|
+
expectedShape: BATCH_EXPECTED_SHAPE,
|
|
562
|
+
example: BATCH_EXAMPLE,
|
|
563
|
+
}, 400);
|
|
564
|
+
}
|
|
565
|
+
if (!envelope || typeof envelope !== "object" || Array.isArray(envelope)) {
|
|
566
|
+
return c.json({
|
|
567
|
+
error: "validation_error",
|
|
568
|
+
message: "Body must be a JSON object with an 'observations' array",
|
|
569
|
+
expectedShape: BATCH_EXPECTED_SHAPE,
|
|
570
|
+
example: BATCH_EXAMPLE,
|
|
571
|
+
}, 400);
|
|
572
|
+
}
|
|
573
|
+
if (!Array.isArray(envelope.observations)) {
|
|
574
|
+
return c.json({
|
|
575
|
+
error: "validation_error",
|
|
576
|
+
message: "'observations' must be an array",
|
|
577
|
+
expectedShape: BATCH_EXPECTED_SHAPE,
|
|
578
|
+
example: BATCH_EXAMPLE,
|
|
579
|
+
hint: "Wrap your observation objects in an 'observations' array — POST {\"observations\":[…]}",
|
|
580
|
+
}, 400);
|
|
581
|
+
}
|
|
582
|
+
if (envelope.observations.length === 0) {
|
|
583
|
+
// Empty batch is a documented no-op so the pre-pass can emit a
|
|
584
|
+
// zero-event window without a 400 stutter.
|
|
585
|
+
return c.json({
|
|
586
|
+
results: [],
|
|
587
|
+
fetched: 0,
|
|
588
|
+
posted: 0,
|
|
589
|
+
duplicates: 0,
|
|
590
|
+
errors: 0,
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
if (envelope.observations.length > BATCH_MAX_OBSERVATIONS) {
|
|
594
|
+
return c.json({
|
|
595
|
+
error: "batch_too_large",
|
|
596
|
+
message: `Batch size ${envelope.observations.length} exceeds maximum ${BATCH_MAX_OBSERVATIONS}`,
|
|
597
|
+
maxItems: BATCH_MAX_OBSERVATIONS,
|
|
598
|
+
hint: "Split the batch into chunks of at most 200 items.",
|
|
599
|
+
}, 400);
|
|
600
|
+
}
|
|
601
|
+
const results = [];
|
|
602
|
+
let posted = 0;
|
|
603
|
+
let duplicates = 0;
|
|
604
|
+
let errorCount = 0;
|
|
605
|
+
// Single explicit transaction wraps the whole batch — better-sqlite3
|
|
606
|
+
// transactions amortise the per-statement overhead to ~30-100x for
|
|
607
|
+
// bulk inserts, which is the primary perf win this endpoint offers
|
|
608
|
+
// beyond bypassing the bashCurlHook one-curl cap. The flip-lock check
|
|
609
|
+
// stays per-item so a mixed-integration batch (defensive — the
|
|
610
|
+
// pre-pass scopes each sub-session to one integration, but the
|
|
611
|
+
// endpoint must hold under any caller) does not block on the first
|
|
612
|
+
// unrelated lock.
|
|
613
|
+
const writeBatch = db.transaction((items) => {
|
|
614
|
+
for (let i = 0; i < items.length; i++) {
|
|
615
|
+
const validated = validateBatchItem(items[i], i);
|
|
616
|
+
if (!validated.ok) {
|
|
617
|
+
results.push(validated.result);
|
|
618
|
+
errorCount += 1;
|
|
619
|
+
continue;
|
|
620
|
+
}
|
|
621
|
+
const lockedKey = inferIntegrationKeyFromSource(validated.source);
|
|
622
|
+
if (lockedKey) {
|
|
623
|
+
const lock = readIntegrationFlipLock(db, lockedKey);
|
|
624
|
+
if (lock) {
|
|
625
|
+
logger.warn({ source: validated.source, ref: validated.ref, lockedKey, lock }, "Batch observation write rejected — integration flip lock held");
|
|
626
|
+
results.push({
|
|
627
|
+
index: i,
|
|
628
|
+
status: "flip_locked",
|
|
629
|
+
source: validated.source,
|
|
630
|
+
ref: validated.ref,
|
|
631
|
+
error: `Integration '${lockedKey}' flip in progress`,
|
|
632
|
+
});
|
|
633
|
+
errorCount += 1;
|
|
634
|
+
continue;
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
const result = recordObservation(db, {
|
|
638
|
+
source: validated.source,
|
|
639
|
+
ref: validated.ref,
|
|
640
|
+
changeType: validated.changeType,
|
|
641
|
+
actor: validated.actor,
|
|
642
|
+
payload: validated.payload,
|
|
643
|
+
});
|
|
644
|
+
if (result.action === "duplicate") {
|
|
645
|
+
duplicates += 1;
|
|
646
|
+
results.push({
|
|
647
|
+
index: i,
|
|
648
|
+
status: "duplicate",
|
|
649
|
+
source: validated.source,
|
|
650
|
+
ref: validated.ref,
|
|
651
|
+
contentHash: result.contentHash,
|
|
652
|
+
id: result.id,
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
else {
|
|
656
|
+
posted += 1;
|
|
657
|
+
results.push({
|
|
658
|
+
index: i,
|
|
659
|
+
status: result.action,
|
|
660
|
+
source: validated.source,
|
|
661
|
+
ref: validated.ref,
|
|
662
|
+
contentHash: result.contentHash,
|
|
663
|
+
id: result.id,
|
|
664
|
+
});
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
});
|
|
668
|
+
writeBatch(envelope.observations);
|
|
669
|
+
logger.info({
|
|
670
|
+
count: envelope.observations.length,
|
|
671
|
+
posted,
|
|
672
|
+
duplicates,
|
|
673
|
+
errors: errorCount,
|
|
674
|
+
}, "Observations batch recorded via API");
|
|
675
|
+
return c.json({
|
|
676
|
+
results,
|
|
677
|
+
fetched: envelope.observations.length,
|
|
678
|
+
posted,
|
|
679
|
+
duplicates,
|
|
680
|
+
errors: errorCount,
|
|
681
|
+
});
|
|
145
682
|
});
|
|
683
|
+
/**
|
|
684
|
+
* Field-level validation contract for `POST /observations/consume`.
|
|
685
|
+
*
|
|
686
|
+
* Production telemetry (2026-05) showed a single Stage-3 hourly_check
|
|
687
|
+
* burning $0.58 / 25 turns retrying this endpoint with shape variants
|
|
688
|
+
* (`correlation_id` snake_case, stringified ids, the angle-bracket
|
|
689
|
+
* placeholder copied verbatim, per-id paths, etc.). The legacy
|
|
690
|
+
* `{ error: "validation_error" }` response gave the agent zero signal
|
|
691
|
+
* about which field was wrong, so it would mutate a random field and
|
|
692
|
+
* retry. Returning the full schema + the specific issue + a one-line
|
|
693
|
+
* hint lets the agent self-correct on the next turn instead of the
|
|
694
|
+
* eighth.
|
|
695
|
+
*/
|
|
696
|
+
const CONSUME_EXPECTED_SHAPE = '{"ids": number[], "correlationId": string}';
|
|
697
|
+
const CONSUME_EXAMPLE = '{"ids":[14,17],"correlationId":"hourly-2026-04-23T15:00:00Z-7af3"}';
|
|
146
698
|
app.post("/observations/consume", async (c) => {
|
|
147
699
|
const parsedBody = await readJsonBody(c);
|
|
148
700
|
if (!parsedBody.ok)
|
|
149
701
|
return parsedBody.response;
|
|
150
702
|
const body = parsedBody.body;
|
|
151
|
-
if (!body ||
|
|
152
|
-
return c.json({
|
|
703
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) {
|
|
704
|
+
return c.json({
|
|
705
|
+
error: "validation_error",
|
|
706
|
+
message: "Body must be a JSON object",
|
|
707
|
+
expectedShape: CONSUME_EXPECTED_SHAPE,
|
|
708
|
+
example: CONSUME_EXAMPLE,
|
|
709
|
+
}, 400);
|
|
710
|
+
}
|
|
711
|
+
const issues = [];
|
|
712
|
+
if (body.correlation_id !== undefined &&
|
|
713
|
+
body.correlationId === undefined) {
|
|
714
|
+
issues.push({
|
|
715
|
+
field: "correlationId",
|
|
716
|
+
expected: "string (camelCase)",
|
|
717
|
+
got: "received 'correlation_id' (snake_case) instead",
|
|
718
|
+
hint: "Rename the field to camelCase 'correlationId' — value is the verbatim id from the <event_correlation_id> tag in your prompt context",
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
else if (typeof body.correlationId !== "string") {
|
|
722
|
+
issues.push({
|
|
723
|
+
field: "correlationId",
|
|
724
|
+
expected: "string",
|
|
725
|
+
got: body.correlationId === undefined
|
|
726
|
+
? "missing"
|
|
727
|
+
: typeof body.correlationId,
|
|
728
|
+
hint: "Copy verbatim from <event_correlation_id>…</event_correlation_id> in your prompt context",
|
|
729
|
+
});
|
|
153
730
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
731
|
+
else if (body.correlationId.trim().length === 0) {
|
|
732
|
+
issues.push({
|
|
733
|
+
field: "correlationId",
|
|
734
|
+
expected: "non-empty string",
|
|
735
|
+
got: "empty string",
|
|
736
|
+
hint: "Copy verbatim from <event_correlation_id>…</event_correlation_id> in your prompt context",
|
|
737
|
+
});
|
|
157
738
|
}
|
|
158
|
-
|
|
159
|
-
|
|
739
|
+
else if (body.correlationId.startsWith("<") &&
|
|
740
|
+
body.correlationId.endsWith(">")) {
|
|
741
|
+
issues.push({
|
|
742
|
+
field: "correlationId",
|
|
743
|
+
expected: "verbatim correlation id",
|
|
744
|
+
got: `placeholder text '${body.correlationId}'`,
|
|
745
|
+
hint: "Use the real id from <event_correlation_id>…</event_correlation_id>, not the angle-bracket placeholder",
|
|
746
|
+
});
|
|
747
|
+
}
|
|
748
|
+
if (!Array.isArray(body.ids)) {
|
|
749
|
+
issues.push({
|
|
750
|
+
field: "ids",
|
|
751
|
+
expected: "number[]",
|
|
752
|
+
got: body.ids === undefined ? "missing" : typeof body.ids,
|
|
753
|
+
hint: "Array of integer observation row ids — e.g. [14, 17]",
|
|
754
|
+
});
|
|
755
|
+
}
|
|
756
|
+
else if (body.ids.length > 0) {
|
|
757
|
+
// Empty array is a documented no-op (preserves the legacy
|
|
758
|
+
// contract: `consumeObservations` returns `{consumed:0,notFound:[]}`
|
|
759
|
+
// for an empty list). Only validate element shape when the array
|
|
760
|
+
// actually has content.
|
|
761
|
+
const stringIds = body.ids.filter((id) => typeof id === "string");
|
|
762
|
+
if (stringIds.length > 0) {
|
|
763
|
+
issues.push({
|
|
764
|
+
field: "ids",
|
|
765
|
+
expected: "number[]",
|
|
766
|
+
got: `array contains strings (e.g. ${JSON.stringify(stringIds.slice(0, 3))})`,
|
|
767
|
+
hint: 'Use integers, not strings — [14, 17] not ["14", "17"]',
|
|
768
|
+
});
|
|
769
|
+
}
|
|
770
|
+
else {
|
|
771
|
+
const nonInt = body.ids.find((id) => typeof id !== "number" || !Number.isInteger(id));
|
|
772
|
+
if (nonInt !== undefined) {
|
|
773
|
+
issues.push({
|
|
774
|
+
field: "ids",
|
|
775
|
+
expected: "number[] (integers)",
|
|
776
|
+
got: `array contains non-integer value ${JSON.stringify(nonInt)}`,
|
|
777
|
+
hint: "ids must be integer observation row ids returned by GET /api/observations",
|
|
778
|
+
});
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
if (issues.length > 0) {
|
|
783
|
+
return c.json({
|
|
784
|
+
error: "validation_error",
|
|
785
|
+
message: "Request body failed schema validation",
|
|
786
|
+
expectedShape: CONSUME_EXPECTED_SHAPE,
|
|
787
|
+
example: CONSUME_EXAMPLE,
|
|
788
|
+
issues,
|
|
789
|
+
}, 400);
|
|
790
|
+
}
|
|
791
|
+
const ids = body.ids;
|
|
792
|
+
const correlationId = body.correlationId;
|
|
793
|
+
const result = consumeObservations(db, ids, correlationId);
|
|
794
|
+
logger.info({ consumed: result.consumed, correlationId }, "Observations consumed");
|
|
160
795
|
return c.json(result);
|
|
161
796
|
});
|
|
797
|
+
/**
|
|
798
|
+
* Helpful 405 for the per-id consume shape the agent has reached for in
|
|
799
|
+
* production (`POST /api/observations/:id/consume`). Without an explicit
|
|
800
|
+
* handler this path falls through to Hono's 404 with body
|
|
801
|
+
* `"404 Not Found"`, which gives the agent nothing to act on. Returning
|
|
802
|
+
* a 405 with the canonical bulk-endpoint hint pulls the agent back onto
|
|
803
|
+
* the correct shape on the next turn.
|
|
804
|
+
*/
|
|
805
|
+
app.all("/observations/:id/consume", (c) => {
|
|
806
|
+
const id = c.req.param("id");
|
|
807
|
+
return c.json({
|
|
808
|
+
error: "use_bulk_endpoint",
|
|
809
|
+
message: "Per-id consume is not supported. Use the bulk endpoint with a single-element ids array.",
|
|
810
|
+
expectedShape: CONSUME_EXPECTED_SHAPE,
|
|
811
|
+
example: CONSUME_EXAMPLE,
|
|
812
|
+
hint: `POST /api/observations/consume with body {"ids":[${id}],"correlationId":"<copy from <event_correlation_id>>"}`,
|
|
813
|
+
}, 405);
|
|
814
|
+
});
|
|
815
|
+
/**
|
|
816
|
+
* Helpful 405 for `GET /api/observations/consume`. The bulk consume is
|
|
817
|
+
* POST-only — without this handler the request 404s with no actionable
|
|
818
|
+
* detail, and the agent's recovery loop produced 8x retries in one
|
|
819
|
+
* routine.hourly_check session.
|
|
820
|
+
*/
|
|
821
|
+
app.get("/observations/consume", (c) => c.json({
|
|
822
|
+
error: "method_not_allowed",
|
|
823
|
+
message: "GET is not supported on /api/observations/consume — use POST.",
|
|
824
|
+
expectedShape: CONSUME_EXPECTED_SHAPE,
|
|
825
|
+
example: CONSUME_EXAMPLE,
|
|
826
|
+
hint: `POST /api/observations/consume with body ${CONSUME_EXAMPLE}`,
|
|
827
|
+
}, 405, { Allow: "POST" }));
|
|
162
828
|
app.get("/observations/stats", (c) => {
|
|
163
829
|
const stats = getObservationStats(db);
|
|
164
830
|
// cost-reduction-structural §A telemetry — surface summarizer health
|