@aitne/daemon 0.1.10 → 0.1.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (305) hide show
  1. package/dist/adapters/adapter-watchdog.d.ts +70 -0
  2. package/dist/adapters/adapter-watchdog.js +115 -0
  3. package/dist/adapters/discord.d.ts +17 -1
  4. package/dist/adapters/discord.js +33 -0
  5. package/dist/adapters/notification-manager.d.ts +27 -1
  6. package/dist/adapters/notification-manager.js +54 -39
  7. package/dist/adapters/slack-adapter.d.ts +26 -1
  8. package/dist/adapters/slack-adapter.js +41 -0
  9. package/dist/adapters/telegram-adapter.d.ts +18 -1
  10. package/dist/adapters/telegram-adapter.js +41 -2
  11. package/dist/adapters/types.d.ts +20 -0
  12. package/dist/adapters/whatsapp-adapter.d.ts +26 -7
  13. package/dist/adapters/whatsapp-adapter.js +74 -21
  14. package/dist/api/env-writer.js +8 -5
  15. package/dist/api/helpers/agent-errors-registry.d.ts +5 -5
  16. package/dist/api/helpers/agent-errors-registry.js +5 -5
  17. package/dist/api/routes/agent.js +33 -12
  18. package/dist/api/routes/agents/index.js +75 -16
  19. package/dist/api/routes/agents/views.d.ts +37 -2
  20. package/dist/api/routes/agents/views.js +64 -2
  21. package/dist/api/routes/background-task.d.ts +22 -0
  22. package/dist/api/routes/background-task.js +338 -0
  23. package/dist/api/routes/browser-history.js +9 -1
  24. package/dist/api/routes/context/permissions.js +3 -2
  25. package/dist/api/routes/context/snapshots.js +0 -3
  26. package/dist/api/routes/context/write.js +3 -17
  27. package/dist/api/routes/dashboard/config.js +48 -12
  28. package/dist/api/routes/dashboard/cost-approvals.js +66 -0
  29. package/dist/api/routes/dashboard/notifications.js +9 -9
  30. package/dist/api/routes/integrations/crud-patch.js +5 -1
  31. package/dist/api/routes/integrations-reconcile.js +2 -2
  32. package/dist/api/routes/notion.d.ts +1 -1
  33. package/dist/api/routes/observations.js +7 -7
  34. package/dist/api/routes/obsidian.d.ts +1 -1
  35. package/dist/api/routes/receipts.js +5 -1
  36. package/dist/api/routes/setup-migrate.js +1 -1
  37. package/dist/api/routes/setup.js +1 -1
  38. package/dist/api/routes/task-flows.d.ts +1 -1
  39. package/dist/api/routes/task-flows.js +1 -1
  40. package/dist/api/routes/tuning.d.ts +29 -0
  41. package/dist/api/routes/tuning.js +304 -0
  42. package/dist/api/server.d.ts +44 -16
  43. package/dist/api/server.js +9 -0
  44. package/dist/bootstrap/adapters.d.ts +19 -0
  45. package/dist/bootstrap/adapters.js +61 -0
  46. package/dist/bootstrap/api.d.ts +5 -3
  47. package/dist/bootstrap/api.js +45 -13
  48. package/dist/bootstrap/catchup.d.ts +1 -1
  49. package/dist/bootstrap/catchup.js +11 -11
  50. package/dist/bootstrap/event-pipeline.d.ts +11 -0
  51. package/dist/bootstrap/event-pipeline.js +245 -7
  52. package/dist/bootstrap/observers.js +9 -6
  53. package/dist/bootstrap/schedule-helpers.d.ts +104 -6
  54. package/dist/bootstrap/schedule-helpers.js +172 -19
  55. package/dist/config.js +26 -12
  56. package/dist/core/agent-core.d.ts +33 -1
  57. package/dist/core/agent-core.js +36 -1
  58. package/dist/core/agents/activity-scan-cadence.d.ts +103 -0
  59. package/dist/core/agents/activity-scan-cadence.js +127 -0
  60. package/dist/core/agents/agent-route-override.d.ts +53 -0
  61. package/dist/core/agents/agent-route-override.js +69 -0
  62. package/dist/core/agents/builtin-registry.d.ts +51 -14
  63. package/dist/core/agents/builtin-registry.js +92 -15
  64. package/dist/core/agents/config-gate-reconcile.d.ts +38 -0
  65. package/dist/core/agents/config-gate-reconcile.js +51 -0
  66. package/dist/core/agents/cron-substitute.d.ts +1 -1
  67. package/dist/core/agents/cron-substitute.js +1 -1
  68. package/dist/core/agents/custom-routine-migration.d.ts +60 -0
  69. package/dist/core/agents/custom-routine-migration.js +149 -0
  70. package/dist/core/agents/firing-blocked.d.ts +1 -1
  71. package/dist/core/agents/hourly-cadence.d.ts +102 -0
  72. package/dist/core/agents/hourly-cadence.js +126 -0
  73. package/dist/core/agents/loader-boot.js +23 -0
  74. package/dist/core/agents/loader.d.ts +19 -0
  75. package/dist/core/agents/loader.js +34 -2
  76. package/dist/core/agents/override-merge.d.ts +1 -1
  77. package/dist/core/agents/override-merge.js +9 -1
  78. package/dist/core/agents/recurrence-convert.d.ts +1 -1
  79. package/dist/core/agents/recurrence-convert.js +1 -1
  80. package/dist/core/agents/recurring-schedule-adapter.js +8 -0
  81. package/dist/core/alerts.js +6 -6
  82. package/dist/core/backends/auth-health-monitor.d.ts +2 -2
  83. package/dist/core/backends/auth-health-monitor.js +1 -1
  84. package/dist/core/backends/backend-router.d.ts +27 -1
  85. package/dist/core/backends/backend-router.js +165 -1
  86. package/dist/core/backends/claude-code-core.d.ts +71 -31
  87. package/dist/core/backends/claude-code-core.js +282 -54
  88. package/dist/core/backends/cli-quota-guards.d.ts +29 -1
  89. package/dist/core/backends/cli-quota-guards.js +40 -5
  90. package/dist/core/backends/codex-core.d.ts +6 -0
  91. package/dist/core/backends/codex-core.js +22 -6
  92. package/dist/core/backends/failure-spend.d.ts +58 -0
  93. package/dist/core/backends/failure-spend.js +137 -0
  94. package/dist/core/backends/gemini-cli-core.d.ts +6 -0
  95. package/dist/core/backends/gemini-cli-core.js +25 -6
  96. package/dist/core/backends/model-registry.d.ts +1 -1
  97. package/dist/core/backends/model-registry.js +4 -4
  98. package/dist/core/backends/opencode-core.d.ts +1 -1
  99. package/dist/core/backends/opencode-core.js +5 -5
  100. package/dist/core/backends/plan-presets.js +39 -15
  101. package/dist/core/bang-commands/commands-cost.js +3 -1
  102. package/dist/core/bang-commands/commands-report.js +4 -3
  103. package/dist/core/bang-commands/commands-research.js +4 -1
  104. package/dist/core/bang-commands/commands-revert-tuning.d.ts +18 -0
  105. package/dist/core/bang-commands/commands-revert-tuning.js +63 -0
  106. package/dist/core/bang-commands/commands-stop-start.js +3 -3
  107. package/dist/core/bang-commands/commands-task-control.d.ts +19 -0
  108. package/dist/core/bang-commands/commands-task-control.js +147 -0
  109. package/dist/core/bang-commands/commands-wiki.js +5 -5
  110. package/dist/core/bang-commands/index.d.ts +2 -0
  111. package/dist/core/bang-commands/index.js +12 -0
  112. package/dist/core/bang-commands/registry.d.ts +12 -0
  113. package/dist/core/browser-history/research-cluster-fanout.d.ts +28 -14
  114. package/dist/core/browser-history/research-cluster-fanout.js +39 -16
  115. package/dist/core/channel-timeline.d.ts +5 -1
  116. package/dist/core/channel-timeline.js +13 -0
  117. package/dist/core/context/index-reconciler.js +5 -2
  118. package/dist/core/context/policy-index-reconciler.d.ts +6 -4
  119. package/dist/core/context/policy-index-runner.js +25 -6
  120. package/dist/core/context-builder-calendar.js +10 -2
  121. package/dist/core/context-builder-conversation.d.ts +8 -1
  122. package/dist/core/context-builder-conversation.js +41 -7
  123. package/dist/core/context-builder-yesterday.js +4 -3
  124. package/dist/core/context-builder.d.ts +7 -2
  125. package/dist/core/context-builder.js +62 -20
  126. package/dist/core/context-file-serializer.d.ts +1 -1
  127. package/dist/core/context-file-serializer.js +1 -1
  128. package/dist/core/context-health.js +2 -2
  129. package/dist/core/context-paths.d.ts +1 -1
  130. package/dist/core/context-paths.js +1 -1
  131. package/dist/core/context-validation/prepare-write.js +1 -1
  132. package/dist/core/context-validation/routine-rulebook.d.ts +1 -1
  133. package/dist/core/context-vault-aliases.d.ts +0 -13
  134. package/dist/core/context-vault-aliases.js +37 -0
  135. package/dist/core/custom-routines.d.ts +99 -0
  136. package/dist/core/custom-routines.js +187 -0
  137. package/dist/core/daemon-api-cli.js +49 -0
  138. package/dist/core/day-boundary.d.ts +46 -0
  139. package/dist/core/day-boundary.js +40 -0
  140. package/dist/core/dispatcher-activity-scan.d.ts +221 -0
  141. package/dist/core/dispatcher-activity-scan.js +775 -0
  142. package/dist/core/dispatcher-error-handling.d.ts +6 -11
  143. package/dist/core/dispatcher-error-handling.js +38 -62
  144. package/dist/core/dispatcher-hourly-check.js +6 -1
  145. package/dist/core/dispatcher-message-handler.d.ts +10 -0
  146. package/dist/core/dispatcher-message-handler.js +17 -0
  147. package/dist/core/dispatcher-morning-routine.d.ts +6 -6
  148. package/dist/core/dispatcher-morning-routine.js +13 -13
  149. package/dist/core/dispatcher-result-processor.d.ts +33 -0
  150. package/dist/core/dispatcher-result-processor.js +167 -11
  151. package/dist/core/dispatcher-scheduled-background-task.d.ts +42 -0
  152. package/dist/core/dispatcher-scheduled-background-task.js +89 -0
  153. package/dist/core/dispatcher-scheduled-tasks.d.ts +63 -1
  154. package/dist/core/dispatcher-scheduled-tasks.js +213 -6
  155. package/dist/core/dispatcher-task-delivery.d.ts +105 -0
  156. package/dist/core/dispatcher-task-delivery.js +555 -0
  157. package/dist/core/dispatcher-types.d.ts +48 -9
  158. package/dist/core/dispatcher-types.js +3 -3
  159. package/dist/core/dispatcher.d.ts +112 -31
  160. package/dist/core/dispatcher.js +284 -59
  161. package/dist/core/dm-freshness-metrics.d.ts +1 -1
  162. package/dist/core/drift-effects.js +2 -2
  163. package/dist/core/feedback/consolidation-prep.js +17 -5
  164. package/dist/core/feedback/eviction-scorer.js +6 -2
  165. package/dist/core/feedback/lesson-format.js +9 -4
  166. package/dist/core/feedback/lesson-injection.d.ts +1 -1
  167. package/dist/core/feedback/lesson-injection.js +17 -2
  168. package/dist/core/feedback/lesson-store-overview.d.ts +8 -4
  169. package/dist/core/feedback/lesson-store-overview.js +8 -4
  170. package/dist/core/feedback/regeneralization-prep.js +29 -16
  171. package/dist/core/feedback/self-performance-prep.d.ts +186 -0
  172. package/dist/core/feedback/self-performance-prep.js +541 -0
  173. package/dist/core/feedback/tuning-actuator.d.ts +198 -0
  174. package/dist/core/feedback/tuning-actuator.js +432 -0
  175. package/dist/core/feedback/tuning-recommender.d.ts +247 -0
  176. package/dist/core/feedback/tuning-recommender.js +580 -0
  177. package/dist/core/feedback/tuning-revert-monitor.d.ts +90 -0
  178. package/dist/core/feedback/tuning-revert-monitor.js +213 -0
  179. package/dist/core/health-monitor.d.ts +6 -0
  180. package/dist/core/health-monitor.js +1 -1
  181. package/dist/core/injection-policy.d.ts +4 -4
  182. package/dist/core/injection-policy.js +4 -4
  183. package/dist/core/integration-main-backend.js +4 -0
  184. package/dist/core/management-md.d.ts +2 -2
  185. package/dist/core/management-md.js +51 -13
  186. package/dist/core/morning/orchestrator.d.ts +2 -2
  187. package/dist/core/morning/orchestrator.js +2 -2
  188. package/dist/core/notification-gate.d.ts +64 -0
  189. package/dist/core/notification-gate.js +51 -0
  190. package/dist/core/notification-rate-limit.d.ts +40 -0
  191. package/dist/core/notification-rate-limit.js +50 -0
  192. package/dist/core/policy-files.d.ts +1 -1
  193. package/dist/core/policy-files.js +2 -2
  194. package/dist/core/pre-pass-freshness.d.ts +4 -4
  195. package/dist/core/retention.d.ts +5 -0
  196. package/dist/core/retention.js +20 -4
  197. package/dist/core/review-context.d.ts +1 -1
  198. package/dist/core/review-context.js +10 -5
  199. package/dist/core/roadmap-write-lock.d.ts +2 -1
  200. package/dist/core/roadmap-write-lock.js +15 -10
  201. package/dist/core/routine-acquisition-plan.d.ts +47 -1
  202. package/dist/core/routine-acquisition-plan.js +78 -20
  203. package/dist/core/routine-fetch-window-retry.js +7 -4
  204. package/dist/core/routine-fetch-window-runner.d.ts +39 -3
  205. package/dist/core/routine-fetch-window-runner.js +264 -13
  206. package/dist/core/routine-windows.d.ts +2 -2
  207. package/dist/core/routine-windows.js +8 -5
  208. package/dist/core/scheduler.d.ts +175 -16
  209. package/dist/core/scheduler.js +559 -102
  210. package/dist/core/signal-detector.d.ts +12 -0
  211. package/dist/core/signal-detector.js +53 -9
  212. package/dist/core/skills-compiler-denied-tools.js +2 -2
  213. package/dist/core/skills-compiler-skill-index.d.ts +2 -2
  214. package/dist/core/skills-compiler-skill-index.js +2 -2
  215. package/dist/core/skills-compiler-variants.d.ts +1 -1
  216. package/dist/core/skills-compiler-variants.js +8 -0
  217. package/dist/core/skills-compiler.d.ts +29 -26
  218. package/dist/core/skills-compiler.js +117 -81
  219. package/dist/core/skills-manifest.d.ts +37 -0
  220. package/dist/core/skills-manifest.js +73 -2
  221. package/dist/core/sleep-inhibitor.d.ts +79 -0
  222. package/dist/core/sleep-inhibitor.js +132 -0
  223. package/dist/core/slim-system-prompt-loader.d.ts +77 -0
  224. package/dist/core/slim-system-prompt-loader.js +141 -0
  225. package/dist/core/spawn-gates.d.ts +126 -0
  226. package/dist/core/spawn-gates.js +180 -0
  227. package/dist/core/today-direct-writer.d.ts +2 -2
  228. package/dist/core/today-direct-writer.js +1 -1
  229. package/dist/core/today-write-lock.d.ts +4 -2
  230. package/dist/core/today-write-lock.js +30 -20
  231. package/dist/core/wake-detector.d.ts +55 -0
  232. package/dist/core/wake-detector.js +80 -0
  233. package/dist/core/wiki/compile-lock.d.ts +1 -1
  234. package/dist/core/wiki/compile-lock.js +1 -1
  235. package/dist/core/workdir.js +15 -6
  236. package/dist/db/activity-scan-signals.d.ts +77 -0
  237. package/dist/db/activity-scan-signals.js +378 -0
  238. package/dist/db/agents-store.d.ts +28 -0
  239. package/dist/db/agents-store.js +62 -0
  240. package/dist/db/background-task-clarifications-store.d.ts +81 -0
  241. package/dist/db/background-task-clarifications-store.js +152 -0
  242. package/dist/db/background-task-store.d.ts +207 -0
  243. package/dist/db/background-task-store.js +380 -0
  244. package/dist/db/browser-history-store.d.ts +39 -6
  245. package/dist/db/browser-history-store.js +51 -7
  246. package/dist/db/browser-task-clarifications-store.d.ts +12 -0
  247. package/dist/db/browser-task-clarifications-store.js +35 -5
  248. package/dist/db/browser-task-store.d.ts +3 -0
  249. package/dist/db/browser-task-store.js +29 -4
  250. package/dist/db/deferred-dm.d.ts +86 -0
  251. package/dist/db/deferred-dm.js +199 -0
  252. package/dist/db/migrations.js +330 -0
  253. package/dist/db/observations.d.ts +2 -2
  254. package/dist/db/observations.js +3 -3
  255. package/dist/db/schema.js +217 -16
  256. package/dist/db/voice-transcripts-store.d.ts +1 -1
  257. package/dist/index.js +86 -29
  258. package/dist/messaging/browser-task-mcp-notifier.d.ts +12 -70
  259. package/dist/messaging/browser-task-mcp-notifier.js +30 -151
  260. package/dist/messaging/browser-task-screenshot-attachment.d.ts +15 -0
  261. package/dist/messaging/browser-task-screenshot-attachment.js +63 -0
  262. package/dist/observers/delegated-sync-worker.d.ts +6 -6
  263. package/dist/observers/delegated-sync-worker.js +10 -10
  264. package/dist/observers/git-delegated-cron.d.ts +1 -1
  265. package/dist/observers/git-delegated-cron.js +2 -2
  266. package/dist/observers/github-poller-classifier.d.ts +3 -3
  267. package/dist/observers/github-poller-classifier.js +3 -3
  268. package/dist/observers/imminent-event-scheduler.d.ts +1 -1
  269. package/dist/observers/imminent-event-scheduler.js +1 -1
  270. package/dist/observers/mail-poller.d.ts +1 -0
  271. package/dist/observers/mail-poller.js +42 -3
  272. package/dist/observers/observation-summarizer/summarizer-client.d.ts +2 -2
  273. package/dist/observers/observation-summarizer/summarizer-client.js +2 -2
  274. package/dist/observers/observation-summarizer/worker.d.ts +2 -2
  275. package/dist/observers/observation-summarizer/worker.js +4 -4
  276. package/dist/observers/obsidian-watcher.d.ts +1 -1
  277. package/dist/observers/obsidian-watcher.js +1 -1
  278. package/dist/safety/agent-write-tracker.d.ts +4 -4
  279. package/dist/safety/agent-write-tracker.js +4 -4
  280. package/dist/safety/audit.d.ts +43 -5
  281. package/dist/safety/audit.js +86 -18
  282. package/dist/safety/risk-classifier.d.ts +6 -0
  283. package/dist/safety/risk-classifier.js +75 -11
  284. package/dist/scheduler/activity-scan-gate.d.ts +86 -0
  285. package/dist/scheduler/activity-scan-gate.js +132 -0
  286. package/dist/services/background-task/background-task-budget.d.ts +80 -0
  287. package/dist/services/background-task/background-task-budget.js +91 -0
  288. package/dist/services/background-task/background-task-driver.d.ts +105 -0
  289. package/dist/services/background-task/background-task-driver.js +416 -0
  290. package/dist/services/background-task/background-task-runner.d.ts +96 -0
  291. package/dist/services/background-task/background-task-runner.js +673 -0
  292. package/dist/services/background-task/background-task-tools.d.ts +84 -0
  293. package/dist/services/background-task/background-task-tools.js +247 -0
  294. package/dist/services/background-task/background-task-transition-events.d.ts +43 -0
  295. package/dist/services/background-task/background-task-transition-events.js +54 -0
  296. package/dist/services/browser-history/automation/egress-denylist.d.ts +1 -1
  297. package/dist/services/browser-history/automation/egress-denylist.js +16 -6
  298. package/dist/services/browser-history/managed-chromium/sandbox-launcher.js +0 -1
  299. package/dist/services/browser-task/browser-task-runner.js +53 -8
  300. package/dist/services/observations-batch.d.ts +1 -1
  301. package/dist/services/observations-batch.js +2 -2
  302. package/dist/settings/runtime-settings.d.ts +38 -11
  303. package/dist/settings/runtime-settings.js +203 -40
  304. package/dist/settings/settings-store.js +11 -3
  305. package/package.json +4 -4
@@ -7,7 +7,25 @@ import { createLogger } from "../logging.js";
7
7
  import { processMailBatch } from "../services/mail-classifier.js";
8
8
  import { isIntegrationPollerless } from "../core/integration-lifecycle.js";
9
9
  import { emptyIngestionStats, processClassifiedMailBatch, } from "../services/mail-ingestion.js";
10
+ import { raceWithAbort } from "../core/abort-utils.js";
10
11
  const logger = createLogger("mail-poller");
12
+ /**
13
+ * Wall-clock cap on the per-account `pollSince` paging loop. Matches the
14
+ * NotionPoller tick cap. Without it, a half-dead connection (typical after
15
+ * machine sleep kills the TCP socket but the server never RSTs) hangs the
16
+ * account's poll forever — the tick-level `inFlight` flag then blocks every
17
+ * subsequent tick for ALL accounts until daemon restart. The aborted call
18
+ * leaks per the {@link raceWithAbort} contract; the transport's own timeout
19
+ * eventually reaps it.
20
+ */
21
+ const ACCOUNT_POLL_TIMEOUT_MS = 4 * 60 * 1000;
22
+ /**
23
+ * Wall-clock cap on `startIdle` (connect + mailboxOpen round-trips). Shorter
24
+ * than the poll cap — IDLE setup is two cheap round-trips; if they don't
25
+ * complete in a minute the socket is dead and the next tick should retry
26
+ * with a fresh connection.
27
+ */
28
+ const IDLE_START_TIMEOUT_MS = 60 * 1000;
11
29
  /**
12
30
  * Unified mail poller (§3.4). Iterates over `registry.listActiveAccounts()`
13
31
  * and calls `provider.pollSince()` per account, recording one aggregated
@@ -187,6 +205,19 @@ export class MailPoller {
187
205
  const provider = await this.registry.getProvider(account.id);
188
206
  if (!provider || !hasIdleSupport(provider))
189
207
  return;
208
+ const aborter = new AbortController();
209
+ const timer = setTimeout(() => {
210
+ aborter.abort(new Error(`mail_idle_start_timeout:${account.id}:${IDLE_START_TIMEOUT_MS}ms`));
211
+ }, IDLE_START_TIMEOUT_MS);
212
+ timer.unref?.();
213
+ try {
214
+ await raceWithAbort(this.startIdleForAccount(account, provider), aborter.signal);
215
+ }
216
+ finally {
217
+ clearTimeout(timer);
218
+ }
219
+ }
220
+ async startIdleForAccount(account, provider) {
190
221
  await provider.startIdle({
191
222
  onDirty: () => {
192
223
  // INTEGRATION_NATIVE_MODE_DESIGN.md §6.2 — the tick-level filter
@@ -335,9 +366,14 @@ export class MailPoller {
335
366
  // are immediately available; we walk them in this same tick (§3.4).
336
367
  let pages = 0;
337
368
  const maxPages = 5;
369
+ const aborter = new AbortController();
370
+ const timer = setTimeout(() => {
371
+ aborter.abort(new Error(`mail_poll_timeout:${account.id}:${ACCOUNT_POLL_TIMEOUT_MS}ms`));
372
+ }, ACCOUNT_POLL_TIMEOUT_MS);
373
+ timer.unref?.();
338
374
  try {
339
375
  while (!drained && pages < maxPages) {
340
- const result = await provider.pollSince(cursor, this.maxMessagesPerPoll);
376
+ const result = await raceWithAbort(provider.pollSince(cursor, this.maxMessagesPerPoll), aborter.signal);
341
377
  cursor = result.nextCursor;
342
378
  drained = result.drained;
343
379
  aggregated = aggregated.concat(result.messages);
@@ -353,6 +389,9 @@ export class MailPoller {
353
389
  });
354
390
  return;
355
391
  }
392
+ finally {
393
+ clearTimeout(timer);
394
+ }
356
395
  this.registry.recordPollTick(account.id, { success: true });
357
396
  // Forget prior re-consent DM cadence so a future failure can DM immediately
358
397
  // instead of silently waiting out `mailAuthFailureRetryHours` (§V3).
@@ -405,7 +444,7 @@ export class MailPoller {
405
444
  // Observe only what actually landed in local state. If upsert failed,
406
445
  // `mail_messages_index` has no row for the provider's reported
407
446
  // `userMessages` — emitting the observation anyway would tell the
408
- // hourly-check signal compute (and downstream skill flows) that N new
447
+ // activity-scan signal compute (and downstream skill flows) that N new
409
448
  // mails exist, and the agent would then query the index and find
410
449
  // zero. Worse: when the next tick retries the upsert successfully it
411
450
  // emits a second observation, double-counting the same batch. Gate
@@ -420,7 +459,7 @@ export class MailPoller {
420
459
  ref: `${account.id}-${Date.now()}`,
421
460
  changeType: "created",
422
461
  // External-sender mail is owner-relevant signal, not internal-system
423
- // bookkeeping. The hourly-check threshold gate, the silent-gate
462
+ // bookkeeping. The activity-scan threshold gate, the silent-gate
424
463
  // consumption path, and the `observations` skill all filter to
425
464
  // `actor='user'` (see dispatcher.ts:1540, 1832 + skills/observations
426
465
  // SKILL.md). Marking this `system` would invisibly excise mail from
@@ -3,10 +3,10 @@
3
3
  *
4
4
  * Today only Claude (Anthropic Messages API) is implemented. Codex and
5
5
  * Gemini fall back to `unsupported` — the worker translates that into a
6
- * `summary_status='skipped'` row so the hourly_check skill drops back to
6
+ * `summary_status='skipped'` row so the activity_scan skill drops back to
7
7
  * the legacy fetch-on-doubt pattern. This keeps the summarizer optional
8
8
  * — non-Claude operators don't pay for it but also don't get the
9
- * downstream hourly_check savings until per-backend support lands.
9
+ * downstream activity_scan savings until per-backend support lands.
10
10
  *
11
11
  * The client deliberately avoids the agent SDK's session machinery: a
12
12
  * one-shot summarizer with no tools doesn't need workdir + skills + MCP
@@ -3,10 +3,10 @@
3
3
  *
4
4
  * Today only Claude (Anthropic Messages API) is implemented. Codex and
5
5
  * Gemini fall back to `unsupported` — the worker translates that into a
6
- * `summary_status='skipped'` row so the hourly_check skill drops back to
6
+ * `summary_status='skipped'` row so the activity_scan skill drops back to
7
7
  * the legacy fetch-on-doubt pattern. This keeps the summarizer optional
8
8
  * — non-Claude operators don't pay for it but also don't get the
9
- * downstream hourly_check savings until per-backend support lands.
9
+ * downstream activity_scan savings until per-backend support lands.
10
10
  *
11
11
  * The client deliberately avoids the agent SDK's session machinery: a
12
12
  * one-shot summarizer with no tools doesn't need workdir + skills + MCP
@@ -18,7 +18,7 @@
18
18
  *
19
19
  * Backpressure: when the queue depth exceeds `queueDepthLimit` the
20
20
  * worker drops new arrivals to `summary_status='skipped'` directly,
21
- * skipping the LLM call. The hourly_check skill falls back to legacy
21
+ * skipping the LLM call. The activity_scan skill falls back to legacy
22
22
  * fetch-on-doubt for those rows.
23
23
  */
24
24
  import type Database from "better-sqlite3";
@@ -77,7 +77,7 @@ export declare class ObservationSummarizerWorker implements Observer {
77
77
  * Public entry — invoked by the recordObservation hook. Drops new
78
78
  * arrivals to `summary_status='skipped'` directly when the in-memory
79
79
  * queue is over capacity. The DB row's `summary_status` reflects the
80
- * actual state so hourly_check can route accordingly.
80
+ * actual state so activity_scan can route accordingly.
81
81
  *
82
82
  * `bypassBackpressure` is reserved for the startup reclaim sweep where
83
83
  * the rows are already in DB-pending state — applying backpressure
@@ -18,7 +18,7 @@
18
18
  *
19
19
  * Backpressure: when the queue depth exceeds `queueDepthLimit` the
20
20
  * worker drops new arrivals to `summary_status='skipped'` directly,
21
- * skipping the LLM call. The hourly_check skill falls back to legacy
21
+ * skipping the LLM call. The activity_scan skill falls back to legacy
22
22
  * fetch-on-doubt for those rows.
23
23
  */
24
24
  import { getObservationForSummarization, listObservationsAwaitingSummary, setObservationEnqueueHook, updateObservationSummary, } from "../../db/observations.js";
@@ -117,7 +117,7 @@ export class ObservationSummarizerWorker {
117
117
  * Public entry — invoked by the recordObservation hook. Drops new
118
118
  * arrivals to `summary_status='skipped'` directly when the in-memory
119
119
  * queue is over capacity. The DB row's `summary_status` reflects the
120
- * actual state so hourly_check can route accordingly.
120
+ * actual state so activity_scan can route accordingly.
121
121
  *
122
122
  * `bypassBackpressure` is reserved for the startup reclaim sweep where
123
123
  * the rows are already in DB-pending state — applying backpressure
@@ -229,7 +229,7 @@ export class ObservationSummarizerWorker {
229
229
  const result = await this.callLlm(prompt);
230
230
  if (!result.ok) {
231
231
  // auth_missing is a user-config issue, not a per-row failure. Mark
232
- // the row 'skipped' so the hourly_check fallback path picks it up
232
+ // the row 'skipped' so the activity_scan fallback path picks it up
233
233
  // (same posture as `unsupported_backend`) and warn at most once
234
234
  // per cooldown window so a missing ANTHROPIC_API_KEY does not spam
235
235
  // the log with one entry per pending observation.
@@ -241,7 +241,7 @@ export class ObservationSummarizerWorker {
241
241
  backend: this.client.backendId,
242
242
  model: this.client.modelId,
243
243
  cooldownMs: ObservationSummarizerWorker.AUTH_MISSING_WARN_COOLDOWN_MS,
244
- hint: "Set ANTHROPIC_API_KEY in env or store it via the dashboard. Pending rows are being marked 'skipped' and the hourly_check fallback path will read them directly.",
244
+ hint: "Set ANTHROPIC_API_KEY in env or store it via the dashboard. Pending rows are being marked 'skipped' and the activity_scan fallback path will read them directly.",
245
245
  }, "Summarizer LLM auth missing — falling back to skip, future warnings suppressed within cooldown");
246
246
  }
247
247
  this.persist(row.id, { status: "skipped", summaryText: null, novelty: null });
@@ -33,7 +33,7 @@ export interface ObsidianWatcherOptions {
33
33
  * **Agent-write suppression**: `AgentWriteTracker` pre-marks files the
34
34
  * agent writes via `/api/context/*`. When a watcher fires for one of
35
35
  * those, we silently drop the observation instead of recording it with
36
- * `actor='agent'`. Downstream consumers (hourly-check skill) already
36
+ * `actor='agent'`. Downstream consumers (activity-scan skill) already
37
37
  * filter by `actor='user'`, so an agent row has no consumer; leaving it
38
38
  * out of the table entirely prevents unbounded growth from the agent's
39
39
  * own write traffic.
@@ -21,7 +21,7 @@ const DIFF_PREVIEW_CAP = 2000;
21
21
  * **Agent-write suppression**: `AgentWriteTracker` pre-marks files the
22
22
  * agent writes via `/api/context/*`. When a watcher fires for one of
23
23
  * those, we silently drop the observation instead of recording it with
24
- * `actor='agent'`. Downstream consumers (hourly-check skill) already
24
+ * `actor='agent'`. Downstream consumers (activity-scan skill) already
25
25
  * filter by `actor='user'`, so an agent row has no consumer; leaving it
26
26
  * out of the table entirely prevents unbounded growth from the agent's
27
27
  * own write traffic.
@@ -2,7 +2,7 @@
2
2
  * AgentWriteTracker — short-lived, in-memory record of paths the agent is
3
3
  * currently writing via the daemon API. Observers consult it to classify
4
4
  * file-change events as `actor='agent'` vs `actor='user'`, which gives the
5
- * hourly-check dispatcher a way to ignore its own writes.
5
+ * activity-scan dispatcher a way to ignore its own writes.
6
6
  *
7
7
  * Two marking modes:
8
8
  * 1. **Content-hash mode** — caller passes the exact bytes they wrote.
@@ -26,7 +26,7 @@
26
26
  * polled sources (Notion, Calendar) whose observation lag is measured in
27
27
  * minutes, the caller MUST pass `opts.ttlMs` large enough to outlive the
28
28
  * poll cadence — otherwise every agent write is seen as a fresh user
29
- * edit and the hourly_check can loop on its own output.
29
+ * edit and the activity_scan can loop on its own output.
30
30
  */
31
31
  export declare class AgentWriteTracker {
32
32
  private readonly ttlMs;
@@ -34,7 +34,7 @@ export declare class AgentWriteTracker {
34
34
  /**
35
35
  * Parallel commit-tracking map keyed by `<repoPath>::<sha-lower>`. Used by
36
36
  * `GitWatcher` to flip observations of agent-originated commits from
37
- * `actor='user'` / `'unknown'` to `actor='agent'` so the hourly_check
37
+ * `actor='user'` / `'unknown'` to `actor='agent'` so the activity_scan
38
38
  * pending-floor does not count the daemon's own commits as user
39
39
  * activity (C1).
40
40
  */
@@ -64,7 +64,7 @@ export declare class AgentWriteTracker {
64
64
  * Register a git SHA the daemon just committed in `repoPath`. The next
65
65
  * `GitWatcher` observation of that SHA is flipped from `actor='user'` /
66
66
  * `'unknown'` to `actor='agent'` via `isAgentCommit`, which keeps the
67
- * hourly_check pending-floor from counting the daemon's own commits as
67
+ * activity_scan pending-floor from counting the daemon's own commits as
68
68
  * user activity (the loop bug described in C1).
69
69
  *
70
70
  * Production callers pass the full 40-char SHA from `git rev-parse HEAD`.
@@ -13,7 +13,7 @@ const DEFAULT_COMMIT_TTL_MS = 15 * 60_000;
13
13
  * AgentWriteTracker — short-lived, in-memory record of paths the agent is
14
14
  * currently writing via the daemon API. Observers consult it to classify
15
15
  * file-change events as `actor='agent'` vs `actor='user'`, which gives the
16
- * hourly-check dispatcher a way to ignore its own writes.
16
+ * activity-scan dispatcher a way to ignore its own writes.
17
17
  *
18
18
  * Two marking modes:
19
19
  * 1. **Content-hash mode** — caller passes the exact bytes they wrote.
@@ -37,7 +37,7 @@ const DEFAULT_COMMIT_TTL_MS = 15 * 60_000;
37
37
  * polled sources (Notion, Calendar) whose observation lag is measured in
38
38
  * minutes, the caller MUST pass `opts.ttlMs` large enough to outlive the
39
39
  * poll cadence — otherwise every agent write is seen as a fresh user
40
- * edit and the hourly_check can loop on its own output.
40
+ * edit and the activity_scan can loop on its own output.
41
41
  */
42
42
  export class AgentWriteTracker {
43
43
  ttlMs;
@@ -45,7 +45,7 @@ export class AgentWriteTracker {
45
45
  /**
46
46
  * Parallel commit-tracking map keyed by `<repoPath>::<sha-lower>`. Used by
47
47
  * `GitWatcher` to flip observations of agent-originated commits from
48
- * `actor='user'` / `'unknown'` to `actor='agent'` so the hourly_check
48
+ * `actor='user'` / `'unknown'` to `actor='agent'` so the activity_scan
49
49
  * pending-floor does not count the daemon's own commits as user
50
50
  * activity (C1).
51
51
  */
@@ -114,7 +114,7 @@ export class AgentWriteTracker {
114
114
  * Register a git SHA the daemon just committed in `repoPath`. The next
115
115
  * `GitWatcher` observation of that SHA is flipped from `actor='user'` /
116
116
  * `'unknown'` to `actor='agent'` via `isAgentCommit`, which keeps the
117
- * hourly_check pending-floor from counting the daemon's own commits as
117
+ * activity_scan pending-floor from counting the daemon's own commits as
118
118
  * user activity (the loop bug described in C1).
119
119
  *
120
120
  * Production callers pass the full 40-char SHA from `git rev-parse HEAD`.
@@ -58,6 +58,20 @@ export declare class AuditLogger implements IAuditLogger {
58
58
  trigger: "reactive" | "autonomous";
59
59
  backend?: AgentResult["backendId"];
60
60
  costSource?: AgentResult["costSource"];
61
+ /**
62
+ * Terminal result override (default `"success"`). `"partial"` records a
63
+ * session that ended cleanly but failed a post-run outcome check
64
+ * (RESEARCH_CLUSTER_COST_FIX_PLAN F5). `"partial"` is a legal
65
+ * `agent_actions.result` value (schema CHECK). Hard errors use
66
+ * `logError` instead.
67
+ */
68
+ result?: "success" | "partial";
69
+ /**
70
+ * Outcome-failure marker written to `agent_actions.error` when paired
71
+ * with `result:"partial"` (F5: `'journal_write_missing'`). Omitted on a
72
+ * plain success so the column stays NULL (legacy row shape).
73
+ */
74
+ error?: string;
61
75
  contextUpdated?: boolean;
62
76
  advisorCallCount?: number;
63
77
  /**
@@ -113,7 +127,15 @@ export declare class AuditLogger implements IAuditLogger {
113
127
  modelId?: string;
114
128
  }): number;
115
129
  private findInProgressRowId;
116
- logSkip(event: Event, reason: string, trigger: "reactive" | "autonomous"): void;
130
+ logSkip(event: Event, reason: string, trigger: "reactive" | "autonomous",
131
+ /**
132
+ * Optional structured context persisted to the `detail` JSON column.
133
+ * Used by the N2 spawn gates (`detail.spawnGate` — per-backend
134
+ * offline/auth verdicts) and the N3 pre-pass plan-assembly drop rows
135
+ * (`detail.prePass.skipReason`) so skip telemetry is queryable
136
+ * without parsing the `error` string. PREPASS_COST_REDUCTION_PLAN.md.
137
+ */
138
+ detail?: Record<string, unknown>): void;
117
139
  /**
118
140
  * Chat-attachments Phase 1 — log a successful upload (inbound or outbound)
119
141
  * to `agent_actions`. Shares the `agent_actions` table with agent-turn
@@ -143,10 +165,12 @@ export declare class AuditLogger implements IAuditLogger {
143
165
  * hit `max_budget_usd` because the row was written with no
144
166
  * timing/backend/model info.
145
167
  *
146
- * Tokens / cost / num_turns are intentionally NOT passed: an aborted
147
- * SDK stream doesn't surface a usable AgentResult, so any value here
148
- * would be a guess. duration_ms + backend + failure shape are
149
- * recoverable and that's what we record.
168
+ * Tokens / cost / num_turns are passed ONLY when the caller holds a
169
+ * real recovered spend figure (PREPASS_COST_REDUCTION_PLAN.md N1
170
+ * post-hoc budget kills, partial stream aborts). Callers without one
171
+ * must omit them: a fabricated value here would corrupt the cost
172
+ * dials. duration_ms + backend + failure shape are always
173
+ * recoverable and that's the baseline record.
150
174
  */
151
175
  context?: {
152
176
  durationMs?: number;
@@ -154,6 +178,20 @@ export declare class AuditLogger implements IAuditLogger {
154
178
  modelId?: string;
155
179
  failureKind?: string;
156
180
  failureCode?: string;
181
+ /**
182
+ * PREPASS_COST_REDUCTION_PLAN.md N1 — recovered spend for a failed
183
+ * turn the provider already billed. Pass ONLY a real recovered
184
+ * figure (BackendQuotaSpend / partial-usage snapshot), never a
185
+ * guess — the historical contract that failure rows carry no
186
+ * fabricated cost still holds for callers without one.
187
+ */
188
+ costUsd?: number;
189
+ costSource?: string;
190
+ tokensInput?: number;
191
+ tokensOutput?: number;
192
+ tokensCacheCreation?: number;
193
+ tokensCacheRead?: number;
194
+ numTurns?: number;
157
195
  /**
158
196
  * Pre-pass fan-out failure block. Mirrors the `prePass` payload on
159
197
  * `logAction` so `MetricsCollector.collectPrePassMetrics` can see
@@ -52,7 +52,7 @@ export class AuditLogger {
52
52
  this.agentIdResolver = resolver;
53
53
  }
54
54
  logAction(params) {
55
- const { event, model, costUsd, usage, modelUsage, durationMs, numTurns, trigger, backend, costSource, contextUpdated = false, advisorCallCount = 0, dmFreshness, prePass, dailyWrite, } = params;
55
+ const { event, model, costUsd, usage, modelUsage, durationMs, numTurns, trigger, backend, costSource, result = "success", error, contextUpdated = false, advisorCallCount = 0, dmFreshness, prePass, dailyWrite, } = params;
56
56
  try {
57
57
  const modelUsageJson = Object.keys(modelUsage).length > 0
58
58
  ? JSON.stringify(modelUsage)
@@ -81,7 +81,7 @@ export class AuditLogger {
81
81
  usage.outputTokens,
82
82
  durationMs,
83
83
  numTurns,
84
- "success",
84
+ result,
85
85
  ];
86
86
  if (this.hasCacheCreationTokensColumn) {
87
87
  columns.splice(7, 0, "cache_creation_tokens");
@@ -137,6 +137,16 @@ export class AuditLogger {
137
137
  columns.splice(columns.length - 2, 0, "agent_id");
138
138
  values.splice(values.length, 0, resolvedAgentId);
139
139
  }
140
+ // RESEARCH_CLUSTER_COST_FIX_PLAN F5 — persist the outcome-failure
141
+ // marker for a run downgraded to `result:"partial"`. Spliced like
142
+ // every other optional column (at `columns.length - 2`, appended to
143
+ // `values`) so the lockstep invariant the in_progress UPSERT loop
144
+ // relies on (`columns.length - 2 === values.length`) holds. A plain
145
+ // success omits it, leaving the column NULL (legacy row shape).
146
+ if (error !== undefined) {
147
+ columns.splice(columns.length - 2, 0, "error");
148
+ values.splice(values.length, 0, error);
149
+ }
140
150
  const detailPayload = {};
141
151
  // STAGE-C-DM-FRESHNESS-PLAN §Task 4 — persist DM freshness telemetry
142
152
  // into `detail`. Inserted only when supplied so non-DM rows stay
@@ -330,7 +340,15 @@ export class AuditLogger {
330
340
  return null;
331
341
  }
332
342
  }
333
- logSkip(event, reason, trigger) {
343
+ logSkip(event, reason, trigger,
344
+ /**
345
+ * Optional structured context persisted to the `detail` JSON column.
346
+ * Used by the N2 spawn gates (`detail.spawnGate` — per-backend
347
+ * offline/auth verdicts) and the N3 pre-pass plan-assembly drop rows
348
+ * (`detail.prePass.skipReason`) so skip telemetry is queryable
349
+ * without parsing the `error` string. PREPASS_COST_REDUCTION_PLAN.md.
350
+ */
351
+ detail) {
334
352
  try {
335
353
  // AGENT_DEFINITIONS_DESIGN.md §8.1 — stamp the owning Agent when the
336
354
  // in-flight firing resolved to one (e.g. a review routine skipped by the
@@ -338,17 +356,28 @@ export class AuditLogger {
338
356
  // skips (setup-mode / cost-cap) resolve to NULL — no execution context
339
357
  // exists yet — which is the legacy row shape.
340
358
  const resolvedAgentId = this.agentIdResolver?.(event) ?? null;
341
- const insertResult = resolvedAgentId !== null
342
- ? this.db
343
- .prepare(`INSERT INTO agent_actions
344
- (event_id, action_type, trigger, result, error, agent_id, started_at)
345
- VALUES (?, ?, ?, 'skipped', ?, ?, datetime('now'))`)
346
- .run(event.correlationId, event.type, trigger, reason, resolvedAgentId)
347
- : this.db
348
- .prepare(`INSERT INTO agent_actions
349
- (event_id, action_type, trigger, result, error, started_at)
350
- VALUES (?, ?, ?, 'skipped', ?, datetime('now'))`)
351
- .run(event.correlationId, event.type, trigger, reason);
359
+ const columns = ["event_id", "action_type", "trigger", "result", "error"];
360
+ const values = [
361
+ event.correlationId,
362
+ event.type,
363
+ trigger,
364
+ "skipped",
365
+ reason,
366
+ ];
367
+ if (resolvedAgentId !== null) {
368
+ columns.push("agent_id");
369
+ values.push(resolvedAgentId);
370
+ }
371
+ if (detail !== undefined) {
372
+ columns.push("detail");
373
+ values.push(JSON.stringify(detail));
374
+ }
375
+ const placeholders = columns.map(() => "?").join(", ");
376
+ const insertResult = this.db
377
+ .prepare(`INSERT INTO agent_actions
378
+ (${columns.join(", ")}, started_at)
379
+ VALUES (${placeholders}, datetime('now'))`)
380
+ .run(...values);
352
381
  this.emitInsertedRow(Number(insertResult.lastInsertRowid), event.type);
353
382
  }
354
383
  catch (err) {
@@ -416,10 +445,12 @@ export class AuditLogger {
416
445
  * hit `max_budget_usd` because the row was written with no
417
446
  * timing/backend/model info.
418
447
  *
419
- * Tokens / cost / num_turns are intentionally NOT passed: an aborted
420
- * SDK stream doesn't surface a usable AgentResult, so any value here
421
- * would be a guess. duration_ms + backend + failure shape are
422
- * recoverable and that's what we record.
448
+ * Tokens / cost / num_turns are passed ONLY when the caller holds a
449
+ * real recovered spend figure (PREPASS_COST_REDUCTION_PLAN.md N1
450
+ * post-hoc budget kills, partial stream aborts). Callers without one
451
+ * must omit them: a fabricated value here would corrupt the cost
452
+ * dials. duration_ms + backend + failure shape are always
453
+ * recoverable and that's the baseline record.
423
454
  */
424
455
  context) {
425
456
  try {
@@ -464,6 +495,43 @@ export class AuditLogger {
464
495
  columns.splice(columns.length - 2, 0, "backend");
465
496
  values.splice(values.length, 0, context.backendId);
466
497
  }
498
+ // PREPASS_COST_REDUCTION_PLAN.md N1 — recovered spend for a failed
499
+ // turn the provider already billed (post-hoc budget kill, partial
500
+ // stream abort, timeout-with-usage). Only callers that hold a real
501
+ // recovered figure pass these; the historical "no guessed values"
502
+ // contract still applies to everyone else.
503
+ if (typeof context?.costUsd === "number" && context.costUsd >= 0) {
504
+ columns.splice(columns.length - 2, 0, "cost_usd");
505
+ values.splice(values.length, 0, context.costUsd);
506
+ }
507
+ if (this.hasCostSourceColumn && context?.costSource) {
508
+ columns.splice(columns.length - 2, 0, "cost_source");
509
+ values.splice(values.length, 0, context.costSource);
510
+ }
511
+ if (typeof context?.tokensInput === "number" && context.tokensInput >= 0) {
512
+ columns.splice(columns.length - 2, 0, "tokens_input");
513
+ values.splice(values.length, 0, context.tokensInput);
514
+ }
515
+ if (typeof context?.tokensOutput === "number" && context.tokensOutput >= 0) {
516
+ columns.splice(columns.length - 2, 0, "tokens_output");
517
+ values.splice(values.length, 0, context.tokensOutput);
518
+ }
519
+ if (this.hasCacheCreationTokensColumn
520
+ && typeof context?.tokensCacheCreation === "number"
521
+ && context.tokensCacheCreation >= 0) {
522
+ columns.splice(columns.length - 2, 0, "cache_creation_tokens");
523
+ values.splice(values.length, 0, context.tokensCacheCreation);
524
+ }
525
+ if (this.hasCacheReadTokensColumn
526
+ && typeof context?.tokensCacheRead === "number"
527
+ && context.tokensCacheRead >= 0) {
528
+ columns.splice(columns.length - 2, 0, "cache_read_tokens");
529
+ values.splice(values.length, 0, context.tokensCacheRead);
530
+ }
531
+ if (typeof context?.numTurns === "number" && context.numTurns > 0) {
532
+ columns.splice(columns.length - 2, 0, "num_turns");
533
+ values.splice(values.length, 0, context.numTurns);
534
+ }
467
535
  // AGENT_DEFINITIONS_DESIGN.md §8.1 — stamp the owning Agent when the
468
536
  // in-flight firing resolved to one, so a FAILED routine's audit row is
469
537
  // attributable to its Agent (the same resolver `logAction` uses). The
@@ -62,6 +62,12 @@ export interface AuditableRoute {
62
62
  path: string;
63
63
  }
64
64
  export declare function auditRiskClassifications(routes: ReadonlyArray<AuditableRoute>): AuditableRoute[];
65
+ /** Extract the path portion of an `API_RISK` key: `"METHOD /path"` → `/path`,
66
+ * or a bare `"/path"` path-only key unchanged. Ranking must compare the path
67
+ * segment alone — the leading `"METHOD "` token (3–6 chars) would otherwise
68
+ * inflate a shorter, less-specific method-keyed prefix above a longer
69
+ * path-only one. */
70
+ export declare function keyPathOf(key: string): string;
65
71
  /**
66
72
  * Return every `/api/*` path that `API_RISK` classifies as
67
73
  * `RiskTier.ReadSensitive` for GET requests, with `{*}` placeholders
@@ -285,6 +285,30 @@ const API_RISK = {
285
285
  "GET /api/browser-task/{*}/screenshots/{*}": RiskTier.ReadSensitive,
286
286
  "POST /api/browser-task/{*}/clarify": RiskTier.Autonomous,
287
287
  "POST /api/browser-task/{*}/cancel": RiskTier.Autonomous,
288
+ // ── Background Task (BACKGROUND_TASK_RUNNER_DESIGN.md §7) ──
289
+ // Generic detached long-task surface, cloned from browser-task and
290
+ // classified to the same posture. The only production callers are the
291
+ // DM-agent `background-task` / `background-task-reply` skills and the
292
+ // morning-briefing session, whose curl shim carries `x-read-token`
293
+ // (sufficient for ReadSensitive, never enough for Approve). The same
294
+ // loopback / sec-fetch / channel-attestation defenses as browser-task
295
+ // apply, and every dispatched task surfaces to the user via DM — no
296
+ // covert dispatch path.
297
+ // - POST (spawn) + clarify + cancel are Autonomous, matching the
298
+ // sibling agent-driven write paths (design §7: "same posture as
299
+ // /api/browser-task").
300
+ // - The list + detail reads stay ReadSensitive (NOT Autonomous):
301
+ // the artifact (`report` / `brief` / `draft` / `significance`)
302
+ // carries personal research / audit content, exactly the reason
303
+ // browser-task's reads are ReadSensitive. Reachable from the agent
304
+ // sessions that need them because their curl carries the read
305
+ // token (cf. GET /api/observations, also ReadSensitive, which the
306
+ // briefing already calls).
307
+ "POST /api/background-task": RiskTier.Autonomous,
308
+ "GET /api/background-task": RiskTier.ReadSensitive,
309
+ "GET /api/background-task/{*}": RiskTier.ReadSensitive,
310
+ "POST /api/background-task/{*}/clarify": RiskTier.Autonomous,
311
+ "POST /api/background-task/{*}/cancel": RiskTier.Autonomous,
288
312
  "/api/setup": RiskTier.Approve,
289
313
  "POST /api/setup/redetect-browsers": RiskTier.Approve,
290
314
  // Management Mode Phase 2 — migration endpoint. Redundant with the
@@ -457,6 +481,16 @@ const API_RISK = {
457
481
  // (FEEDBACK_LEARNING_LOOP_DESIGN.md §9 Phase 5). Summarises cap utilisation
458
482
  // only — lesson prose was redaction-scrubbed at capture — so Autonomous.
459
483
  "GET /api/feedback/lessons": RiskTier.Autonomous,
484
+ // Self-tuning verdict endpoint (SELF_TUNING_REVIEW_CYCLE_DESIGN.md §3.4).
485
+ // Autonomous + (Phase 3) mandatory owner DM on apply — the exact pattern
486
+ // that replaced the abolished Notify tier; requiring the Approve bearer
487
+ // would put a human back in every loop iteration and defeat the design.
488
+ // Safety is carried by code, not tier: verdicts may only reference
489
+ // daemon-generated single-use recommendation ids from the current cycle,
490
+ // the handler is idempotent per id, and Phase 2 never actuates (shadow).
491
+ "POST /api/tuning/verdicts": RiskTier.Autonomous,
492
+ // Pending-cycle read — knob names + telemetry counts only, no user prose.
493
+ "GET /api/tuning/pending": RiskTier.Autonomous,
460
494
  // ── Notification ──
461
495
  "/api/notify": RiskTier.Autonomous,
462
496
  // ── External Service Proxy ──
@@ -668,7 +702,7 @@ const API_RISK = {
668
702
  "PATCH /api/delegated-sync/active-hours": RiskTier.Approve,
669
703
  "POST /api/delegated-sync/cadences/": RiskTier.Approve,
670
704
  // INTEGRATION-DRIFT-DETECTION-PLAN.md §6.0 — drift-detection chokepoint.
671
- // Autonomous: the agent's hourly_check delegated variant POSTs the
705
+ // Autonomous: the agent's activity_scan delegated variant POSTs the
672
706
  // result of its connector fetch to compute a structural diff. Defense
673
707
  // layers (window-key allowlist + per-call audit row) live inside the
674
708
  // handler. Daemon-internal callers (CalendarPoller, DelegatedSyncWorker)
@@ -893,9 +927,14 @@ export function findExplicitRiskClassification(method, path) {
893
927
  /* c8 ignore stop */
894
928
  if (keyMethod && keyMethod !== method)
895
929
  return false;
896
- return path.startsWith(keyPath);
930
+ return pathPrefixMatches(keyPath, path);
897
931
  })
898
- .sort((a, b) => b[0].length - a[0].length);
932
+ // Rank by matched path-prefix length so the most-specific prefix wins.
933
+ // Must compare the path segment, NOT the raw key: the `"METHOD "` token
934
+ // would otherwise lift a shorter, less-specific method-keyed prefix
935
+ // (e.g. `DELETE /api/git`) above a longer path-only one (`/api/github`),
936
+ // silently downgrading the tier. Mirrors the step-3 pattern tiebreaker.
937
+ .sort((a, b) => keyPathOf(b[0]).length - keyPathOf(a[0]).length);
899
938
  if (candidates.length > 0)
900
939
  return candidates[0][1];
901
940
  return null;
@@ -975,16 +1014,41 @@ function matchesPattern(keyPath, actualPath) {
975
1014
  }
976
1015
  return true;
977
1016
  }
978
- /** Count characters before the first `{*}` (or whole length if none) —
979
- * used to rank pattern candidates by specificity. Defensive against future
980
- * overlapping `{*}` keys: today no two `{*}` entries in `API_RISK` match
981
- * the same `(method, path)` pair, so the sort comparator never invokes
982
- * this helper. The function exists so the first overlapping pair added
983
- * picks the more specific entry instead of relying on iteration order. */
1017
+ /** Extract the path portion of an `API_RISK` key: `"METHOD /path"` `/path`,
1018
+ * or a bare `"/path"` path-only key unchanged. Ranking must compare the path
1019
+ * segment alone the leading `"METHOD "` token (3–6 chars) would otherwise
1020
+ * inflate a shorter, less-specific method-keyed prefix above a longer
1021
+ * path-only one. */
1022
+ export function keyPathOf(key) {
1023
+ return key.includes(" ") ? key.split(" ")[1] : key;
1024
+ }
1025
+ /** Segment-aware prefix test for step-4 (non-`{*}`) keys. A raw
1026
+ * `path.startsWith(keyPath)` matches a *string* prefix, so `/api/git`
1027
+ * would spuriously match the unrelated sibling `/api/git-accounts` (and,
1028
+ * worse, an unclassified future `/api/git-webhook` — silently inheriting
1029
+ * the sibling's tier instead of failing closed to Approve). A key that
1030
+ * ends in `/` is an explicit subtree catch-all, so a raw `startsWith` is
1031
+ * already the boundary; otherwise the match must be a strict
1032
+ * `/`-delimited descendant. Exact `path === keyPath` is pre-empted by the
1033
+ * step-1/step-2 exact lookups, so step 4 only ever matches descendants.
1034
+ * Mirrors the segment discipline of `matchesPattern` (step 3). */
1035
+ function pathPrefixMatches(keyPath, path) {
1036
+ if (keyPath.endsWith("/"))
1037
+ return path.startsWith(keyPath);
1038
+ return path.startsWith(keyPath + "/");
1039
+ }
1040
+ /** Count characters of the path prefix before the first `{*}` (or the whole
1041
+ * path length if none) — used to rank pattern candidates by specificity.
1042
+ * Defensive against future overlapping `{*}` keys: today no two `{*}` entries
1043
+ * in `API_RISK` match the same `(method, path)` pair, so the sort comparator
1044
+ * never invokes this helper. The function exists so the first overlapping
1045
+ * pair added picks the more specific entry instead of relying on iteration
1046
+ * order. */
984
1047
  /* c8 ignore start */
985
1048
  function literalPrefixLength(key) {
986
- const idx = key.indexOf("{*}");
987
- return idx < 0 ? key.length : idx;
1049
+ const keyPath = keyPathOf(key);
1050
+ const idx = keyPath.indexOf("{*}");
1051
+ return idx < 0 ? keyPath.length : idx;
988
1052
  }
989
1053
  /* c8 ignore stop */
990
1054
  /** Strip a trailing `{*}` placeholder from `path` so callers can