mixdog 1.3.4 → 1.4.0

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 (472) hide show
  1. package/LICENSES/oh-my-pi-MIT.txt +23 -0
  2. package/NOTICE.md +17 -2
  3. package/README.md +11 -0
  4. package/package.json +3 -11
  5. package/scripts/daemon-stub.mjs +1 -1
  6. package/scripts/embed-google-oauth-client.mjs +49 -0
  7. package/scripts/gate-local.mjs +4 -2
  8. package/scripts/lib/test-spawn-ledger.mjs +46 -1
  9. package/scripts/native-release-status.mjs +2 -2
  10. package/scripts/recall-history-cases.json +1 -1
  11. package/scripts/suite-health.mjs +10 -1
  12. package/scripts/test.mjs +12 -11
  13. package/src/agents/reviewer/AGENT.md +7 -11
  14. package/src/agents/reviewer/agent.json +1 -1
  15. package/src/defaults/shipped-history.json +25 -1
  16. package/src/defaults/skills/browser-use/SKILL.md +77 -17
  17. package/src/defaults/skills/code-tidy/SKILL.md +142 -270
  18. package/src/defaults/skills/code-tidy/references/agent-cleanup.md +4 -4
  19. package/src/defaults/skills/code-tidy/references/cleanup-report.md +31 -76
  20. package/src/defaults/skills/code-tidy/references/dead-code.md +6 -12
  21. package/src/defaults/skills/computer-use/SKILL.md +33 -9
  22. package/src/defaults/skills/setup/SKILL.md +29 -2
  23. package/src/defaults/skills/setup/references/actions.md +117 -28
  24. package/src/defaults/skills/setup/references/surfaces.md +77 -35
  25. package/src/defaults/skills/setup-guide/SKILL.md +3 -3
  26. package/src/defaults/skills/visualize/SKILL.md +2 -1
  27. package/src/lib/keychain-cjs.cjs +359 -29
  28. package/src/lib/rules-builder.cjs +10 -8
  29. package/src/rules/agent/AGENT.md +6 -6
  30. package/src/rules/agent-mode/AGENT_MODE.md +10 -2
  31. package/src/rules/agent-mode/MIXY.md +12 -0
  32. package/src/rules/lead/LEAD.md +18 -20
  33. package/src/rules/shared/05-tool-calls.md +5 -6
  34. package/src/rules/shared/10-tool-workflow.md +2 -0
  35. package/src/rules/shared/60-verification.md +7 -5
  36. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +2 -3
  37. package/src/runtime/agent/orchestrator/agent-runtime/heartbeat-completion.mjs +193 -0
  38. package/src/runtime/agent/orchestrator/agent-runtime/title-completion.mjs +3 -1
  39. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +3 -1
  40. package/src/runtime/agent/orchestrator/config.mjs +34 -0
  41. package/src/runtime/agent/orchestrator/context/collect.mjs +3 -0
  42. package/src/runtime/agent/orchestrator/context/deferred-tools.mjs +72 -67
  43. package/src/runtime/agent/orchestrator/context/skill-catalog.mjs +54 -1
  44. package/src/runtime/agent/orchestrator/mcp/agent-connectors.mjs +3 -3
  45. package/src/runtime/agent/orchestrator/mcp/client-transport.mjs +177 -73
  46. package/src/runtime/agent/orchestrator/mcp/client.mjs +25 -2
  47. package/src/runtime/agent/orchestrator/mcp/oauth-callback.mjs +31 -2
  48. package/src/runtime/agent/orchestrator/mcp/oauth-client.mjs +16 -3
  49. package/src/runtime/agent/orchestrator/mcp/oauth-login.mjs +42 -7
  50. package/src/runtime/agent/orchestrator/mcp/oauth-provider.mjs +21 -2
  51. package/src/runtime/agent/orchestrator/mcp/oauth-shared.mjs +23 -0
  52. package/src/runtime/agent/orchestrator/mcp/oauth.mjs +11 -1
  53. package/src/runtime/agent/orchestrator/providers/account-pool.mjs +230 -137
  54. package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +64 -7
  55. package/src/runtime/agent/orchestrator/providers/anthropic-api-request.mjs +18 -5
  56. package/src/runtime/agent/orchestrator/providers/anthropic-cache-warm.mjs +165 -0
  57. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
  58. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +4 -1
  59. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-request/gzip-post.mjs +14 -14
  60. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +160 -69
  61. package/src/runtime/agent/orchestrator/providers/anthropic-overload-fallback.mjs +25 -3
  62. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +64 -33
  63. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +135 -87
  64. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-catalog.mjs +52 -8
  65. package/src/runtime/agent/orchestrator/providers/antigravity-oauth.mjs +10 -1
  66. package/src/runtime/agent/orchestrator/providers/antigravity-request.mjs +22 -6
  67. package/src/runtime/agent/orchestrator/providers/antigravity-response.mjs +15 -4
  68. package/src/runtime/agent/orchestrator/providers/antigravity-stream.mjs +17 -2
  69. package/src/runtime/agent/orchestrator/providers/antigravity-transport.mjs +142 -104
  70. package/src/runtime/agent/orchestrator/providers/cache-warm-admission.mjs +90 -0
  71. package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +1 -1
  72. package/src/runtime/agent/orchestrator/providers/cursor-wire-normalization.mjs +10 -4
  73. package/src/runtime/agent/orchestrator/providers/cursor-wire-protobuf.mjs +10 -217
  74. package/src/runtime/agent/orchestrator/providers/cursor-wire-transport.mjs +25 -9
  75. package/src/runtime/agent/orchestrator/providers/cursor.mjs +22 -6
  76. package/src/runtime/agent/orchestrator/providers/devin-auth.mjs +409 -0
  77. package/src/runtime/agent/orchestrator/providers/devin-wire-families.mjs +187 -0
  78. package/src/runtime/agent/orchestrator/providers/devin-wire-identity.mjs +342 -0
  79. package/src/runtime/agent/orchestrator/providers/devin-wire-models.mjs +541 -0
  80. package/src/runtime/agent/orchestrator/providers/devin-wire-protobuf.mjs +305 -0
  81. package/src/runtime/agent/orchestrator/providers/devin-wire-request.mjs +558 -0
  82. package/src/runtime/agent/orchestrator/providers/devin-wire-schema.mjs +880 -0
  83. package/src/runtime/agent/orchestrator/providers/devin-wire-session.mjs +190 -0
  84. package/src/runtime/agent/orchestrator/providers/devin-wire-tool-args.mjs +351 -0
  85. package/src/runtime/agent/orchestrator/providers/devin-wire-transport.mjs +560 -0
  86. package/src/runtime/agent/orchestrator/providers/devin.mjs +491 -0
  87. package/src/runtime/agent/orchestrator/providers/effort-configuration.mjs +3 -0
  88. package/src/runtime/agent/orchestrator/providers/gemini-output-limit.mjs +46 -0
  89. package/src/runtime/agent/orchestrator/providers/gemini-reasoning-relay.mjs +30 -0
  90. package/src/runtime/agent/orchestrator/providers/gemini-request-body.mjs +7 -11
  91. package/src/runtime/agent/orchestrator/providers/gemini-retry-policy.mjs +61 -22
  92. package/src/runtime/agent/orchestrator/providers/gemini-sdk-request.mjs +1 -0
  93. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +71 -4
  94. package/src/runtime/agent/orchestrator/providers/gemini.mjs +25 -2
  95. package/src/runtime/agent/orchestrator/providers/grok-client-version.mjs +1 -1
  96. package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +96 -5
  97. package/src/runtime/agent/orchestrator/providers/lib/oauth-token-utils.mjs +17 -3
  98. package/src/runtime/agent/orchestrator/providers/lib/tail-reminder-split.mjs +23 -5
  99. package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +52 -10
  100. package/src/runtime/agent/orchestrator/providers/model-proxy-channel.d.mts +8 -1
  101. package/src/runtime/agent/orchestrator/providers/model-proxy-channel.mjs +1 -0
  102. package/src/runtime/agent/orchestrator/providers/npm-cli-version.mjs +11 -3
  103. package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +19 -1
  104. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +39 -3
  105. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream/stream-state.mjs +3 -1
  106. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream.mjs +2 -0
  107. package/src/runtime/agent/orchestrator/providers/openai-error-contract.mjs +6 -1
  108. package/src/runtime/agent/orchestrator/providers/openai-oauth-tokens.mjs +10 -9
  109. package/src/runtime/agent/orchestrator/providers/openai-ws-send/send-context.mjs +22 -3
  110. package/src/runtime/agent/orchestrator/providers/openai-ws-send-attempts/attempt-context.mjs +43 -5
  111. package/src/runtime/agent/orchestrator/providers/openai-ws-send-attempts/handshake-failure.mjs +3 -1
  112. package/src/runtime/agent/orchestrator/providers/openai-ws-send-attempts/stream-failure.mjs +6 -3
  113. package/src/runtime/agent/orchestrator/providers/opencode-go-gemini.mjs +1 -0
  114. package/src/runtime/agent/orchestrator/providers/provider-availability.mjs +19 -0
  115. package/src/runtime/agent/orchestrator/providers/provider-error-kind.mjs +65 -1
  116. package/src/runtime/agent/orchestrator/providers/quota-reading.mjs +38 -12
  117. package/src/runtime/agent/orchestrator/providers/registry.mjs +43 -3
  118. package/src/runtime/agent/orchestrator/providers/retry-executor.mjs +39 -7
  119. package/src/runtime/agent/orchestrator/providers/statusline-route-meta.mjs +6 -0
  120. package/src/runtime/agent/orchestrator/providers/wire-protobuf-codec.mjs +368 -0
  121. package/src/runtime/agent/orchestrator/runtime-core/ask-user-tool-defs.mjs +12 -7
  122. package/src/runtime/agent/orchestrator/runtime-core/builtin-features.mjs +2 -0
  123. package/src/runtime/agent/orchestrator/runtime-core/compaction-config-keys.mjs +62 -0
  124. package/src/runtime/agent/orchestrator/runtime-core/config-helpers.mjs +37 -16
  125. package/src/runtime/agent/orchestrator/runtime-core/deferred-tool-delta.mjs +100 -23
  126. package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +1 -1
  127. package/src/runtime/agent/orchestrator/runtime-core/reaction-tool-defs.mjs +133 -0
  128. package/src/runtime/agent/orchestrator/runtime-core/session-text.mjs +2 -0
  129. package/src/runtime/agent/orchestrator/runtime-core/tool-bridge.mjs +19 -4
  130. package/src/runtime/agent/orchestrator/runtime-core/tool-catalog-data.mjs +3 -0
  131. package/src/runtime/agent/orchestrator/runtime-core/tool-catalog.mjs +125 -30
  132. package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +23 -5
  133. package/src/runtime/agent/orchestrator/runtime-core/tool-profile.mjs +35 -10
  134. package/src/runtime/agent/orchestrator/runtime-core/workflow/agents.mjs +57 -10
  135. package/src/runtime/agent/orchestrator/runtime-core/workflow/context.mjs +9 -6
  136. package/src/runtime/agent/orchestrator/runtime-core/workflow-ids.mjs +1 -1
  137. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +18 -0
  138. package/src/runtime/agent/orchestrator/session/agent-room.d.mts +0 -3
  139. package/src/runtime/agent/orchestrator/session/agent-room.mjs +10 -4
  140. package/src/runtime/agent/orchestrator/session/cache-warm.mjs +792 -0
  141. package/src/runtime/agent/orchestrator/session/compact/runner.mjs +148 -25
  142. package/src/runtime/agent/orchestrator/session/compaction-archive-capture.mjs +475 -58
  143. package/src/runtime/agent/orchestrator/session/compaction-archive.mjs +42 -17
  144. package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +8 -0
  145. package/src/runtime/agent/orchestrator/session/human-gate/gate.mjs +47 -9
  146. package/src/runtime/agent/orchestrator/session/human-gate/outbound-policy.mjs +6 -2
  147. package/src/runtime/agent/orchestrator/session/human-gate/requests.mjs +36 -4
  148. package/src/runtime/agent/orchestrator/session/human-gate/store.mjs +14 -2
  149. package/src/runtime/agent/orchestrator/session/loop/assistant-commit.mjs +8 -0
  150. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +18 -22
  151. package/src/runtime/agent/orchestrator/session/loop/fresh-context.mjs +32 -3
  152. package/src/runtime/agent/orchestrator/session/loop/loop-state.mjs +11 -2
  153. package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/segments.mjs +4 -1
  154. package/src/runtime/agent/orchestrator/session/loop/no-tool-turn.mjs +51 -3
  155. package/src/runtime/agent/orchestrator/session/loop/route-fallback.mjs +17 -3
  156. package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +10 -2
  157. package/src/runtime/agent/orchestrator/session/loop/tool-exec/after-hook.mjs +2 -1
  158. package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +14 -4
  159. package/src/runtime/agent/orchestrator/session/loop/tool-exec/dispatch.mjs +16 -2
  160. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +16 -1
  161. package/src/runtime/agent/orchestrator/session/loop/tool-exposure-migration.mjs +10 -0
  162. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +3 -10
  163. package/src/runtime/agent/orchestrator/session/loop/tool-phase.mjs +23 -5
  164. package/src/runtime/agent/orchestrator/session/loop/turn-limits.mjs +60 -0
  165. package/src/runtime/agent/orchestrator/session/manager/ask-queue.mjs +12 -2
  166. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +19 -1
  167. package/src/runtime/agent/orchestrator/session/manager/ask-turn-commit.mjs +2 -0
  168. package/src/runtime/agent/orchestrator/session/manager/ask-turn-execute.mjs +133 -90
  169. package/src/runtime/agent/orchestrator/session/manager/ask-turn-loop-options.mjs +14 -0
  170. package/src/runtime/agent/orchestrator/session/manager/ask-turn-settle.mjs +2 -1
  171. package/src/runtime/agent/orchestrator/session/manager/ask-turn-state.mjs +31 -4
  172. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +30 -18
  173. package/src/runtime/agent/orchestrator/session/manager/owner-route-fallbacks.mjs +23 -13
  174. package/src/runtime/agent/orchestrator/session/manager/pending-message-entry.mjs +11 -2
  175. package/src/runtime/agent/orchestrator/session/manager/provider-replay.mjs +13 -0
  176. package/src/runtime/agent/orchestrator/session/manager/route-fallback-chain.mjs +129 -0
  177. package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +20 -2
  178. package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +2 -0
  179. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +8 -0
  180. package/src/runtime/agent/orchestrator/session/manager/session-record.mjs +1 -1
  181. package/src/runtime/agent/orchestrator/session/manager/session-tool-surface.mjs +13 -0
  182. package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +7 -2
  183. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint-journal.mjs +102 -15
  184. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint.mjs +18 -5
  185. package/src/runtime/agent/orchestrator/session/message-ids.mjs +5 -3
  186. package/src/runtime/agent/orchestrator/session/message-reactions.mjs +89 -0
  187. package/src/runtime/agent/orchestrator/session/message-reactions.test-support.mjs +75 -0
  188. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +19 -8
  189. package/src/runtime/agent/orchestrator/session/prompt-cache-idle-gaps.mjs +113 -0
  190. package/src/runtime/agent/orchestrator/session/prompt-cache-touch-store.mjs +127 -0
  191. package/src/runtime/agent/orchestrator/session/routine-result.d.mts +0 -2
  192. package/src/runtime/agent/orchestrator/session/routine-result.mjs +47 -11
  193. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +20 -2
  194. package/src/runtime/agent/orchestrator/session/sensitive-projection.mjs +100 -0
  195. package/src/runtime/agent/orchestrator/session/sensitive-values.mjs +257 -0
  196. package/src/runtime/agent/orchestrator/session/side-question-store.mjs +20 -7
  197. package/src/runtime/agent/orchestrator/session/side-question.mjs +18 -0
  198. package/src/runtime/agent/orchestrator/session/skill-update-notice.mjs +60 -0
  199. package/src/runtime/agent/orchestrator/session/store/live-state.mjs +6 -3
  200. package/src/runtime/agent/orchestrator/session/store/save-delta.mjs +9 -5
  201. package/src/runtime/agent/orchestrator/session/store/serialize.mjs +9 -8
  202. package/src/runtime/agent/orchestrator/session/store/session-persistence-events.mjs +29 -0
  203. package/src/runtime/agent/orchestrator/session/store-agent-worker-header-cache.mjs +85 -0
  204. package/src/runtime/agent/orchestrator/session/store-agent-worker-rows.mjs +133 -18
  205. package/src/runtime/agent/orchestrator/session/store-summary-catalog.mjs +12 -2
  206. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +44 -14
  207. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +84 -3
  208. package/src/runtime/agent/orchestrator/session/stored-attachment-history.mjs +32 -0
  209. package/src/runtime/agent/orchestrator/session/tail-reminder-rules.mjs +11 -5
  210. package/src/runtime/agent/orchestrator/session/thinking-binding.mjs +108 -0
  211. package/src/runtime/agent/orchestrator/session/tool-batch/pre-dispatch.mjs +2 -2
  212. package/src/runtime/agent/orchestrator/session/tool-batch/record-outcome.mjs +2 -1
  213. package/src/runtime/agent/orchestrator/session/tool-batch/state.mjs +4 -0
  214. package/src/runtime/agent/orchestrator/session/tool-batch.mjs +5 -0
  215. package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +25 -2
  216. package/src/runtime/agent/orchestrator/session/transcript-restore/tool-result-status.mjs +9 -7
  217. package/src/runtime/agent/orchestrator/session/transcript-restore/tool-result-text.mjs +2 -2
  218. package/src/runtime/agent/orchestrator/session/turn-failure.mjs +5 -1
  219. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +5 -2
  220. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/backgrounded-result.mjs +12 -1
  221. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/misuse-guard.mjs +240 -0
  222. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/result-format.mjs +1 -1
  223. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/shell-cwd.mjs +38 -0
  224. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +25 -5
  225. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +65 -7
  226. package/src/runtime/agent/orchestrator/tools/builtin/shell-runtime.mjs +7 -1
  227. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +36 -13
  228. package/src/runtime/browser-bridge/action-schema.mjs +13 -2
  229. package/src/runtime/browser-bridge/client.mjs +73 -8
  230. package/src/runtime/browser-bridge/input-fields.mjs +8 -9
  231. package/src/runtime/browser-bridge/mail-links.mjs +107 -0
  232. package/src/runtime/browser-bridge/tool-defs.mjs +1 -1
  233. package/src/runtime/channels/lib/config.mjs +6 -61
  234. package/src/runtime/channels/lib/crash-log.mjs +1 -1
  235. package/src/runtime/channels/lib/event-pipeline.mjs +4 -19
  236. package/src/runtime/channels/lib/event-queue.mjs +3 -18
  237. package/src/runtime/channels/lib/owned-runtime/automation-services.mjs +2 -2
  238. package/src/runtime/channels/lib/owned-runtime/automation-start.mjs +3 -4
  239. package/src/runtime/channels/lib/owned-runtime/config-reload.mjs +4 -59
  240. package/src/runtime/channels/lib/owned-runtime/owned-start.mjs +33 -115
  241. package/src/runtime/channels/lib/owned-runtime/owned-stop.mjs +5 -22
  242. package/src/runtime/channels/lib/owned-runtime/state.mjs +2 -8
  243. package/src/runtime/channels/lib/owned-runtime.mjs +6 -23
  244. package/src/runtime/channels/lib/parent-bridge.mjs +2 -2
  245. package/src/runtime/channels/lib/runtime-paths.mjs +4 -43
  246. package/src/runtime/channels/lib/schedule-errors.mjs +51 -0
  247. package/src/runtime/channels/lib/scheduler.mjs +135 -154
  248. package/src/runtime/channels/lib/schedules-db.mjs +83 -11
  249. package/src/runtime/channels/lib/status-snapshot.mjs +1 -1
  250. package/src/runtime/channels/lib/tool-dispatch.mjs +14 -2
  251. package/src/runtime/channels/lib/webhooks-db.mjs +5 -9
  252. package/src/runtime/channels/lib/worker-bootstrap.mjs +0 -1
  253. package/src/runtime/channels/lib/worker-ipc.mjs +0 -1
  254. package/src/runtime/channels/lib/worker-main.mjs +19 -34
  255. package/src/runtime/computer-bridge/action-schema.mjs +42 -1
  256. package/src/runtime/computer-bridge/actions.mjs +9 -1
  257. package/src/runtime/computer-bridge/client.mjs +180 -8
  258. package/src/runtime/computer-bridge/core-actions.mjs +2 -2
  259. package/src/runtime/computer-bridge/error-recovery.mjs +27 -2
  260. package/src/runtime/computer-bridge/linux-readiness.mjs +65 -23
  261. package/src/runtime/computer-bridge/pending-continuation.mjs +1 -1
  262. package/src/runtime/connectors/connector-accounts-mcp.mjs +230 -0
  263. package/src/runtime/connectors/gmail-app-password.mjs +172 -39
  264. package/src/runtime/connectors/google-mcp.mjs +10 -12
  265. package/src/runtime/memory/lib/pg/adapter.mjs +77 -70
  266. package/src/runtime/memory/lib/pg/supervisor.mjs +24 -12
  267. package/src/runtime/memory/lib/runtime-fetcher.mjs +23 -5
  268. package/src/runtime/office/com/com-adapter.mjs +17 -1
  269. package/src/runtime/office/com/office-com-host.ps1 +51 -1
  270. package/src/runtime/office/pdf/cfb-fixture.mjs +328 -0
  271. package/src/runtime/office/pdf/cfb-protection.mjs +311 -0
  272. package/src/runtime/office/pdf/document-preview.mjs +326 -8
  273. package/src/runtime/office/pdf/ooxml-crypto.mjs +469 -0
  274. package/src/runtime/office/pdf/ooxml-key-worker.mjs +14 -0
  275. package/src/runtime/office/portable/portable-soffice.mjs +45 -5
  276. package/src/runtime/session-search/session-search-db.mjs +73 -9
  277. package/src/runtime/session-search/session-search-ingest.mjs +110 -20
  278. package/src/runtime/shared/abort-race.mjs +89 -0
  279. package/src/runtime/shared/agent-avatar.d.mts +0 -1
  280. package/src/runtime/shared/agent-kind.mjs +4 -3
  281. package/src/runtime/shared/bot-hops.mjs +62 -2
  282. package/src/runtime/shared/bounded-download.mjs +8 -3
  283. package/src/runtime/shared/config.mjs +51 -35
  284. package/src/runtime/shared/connector-accounts.mjs +20 -10
  285. package/src/runtime/shared/connector-catalog.d.mts +33 -2
  286. package/src/runtime/shared/connector-catalog.mjs +166 -8
  287. package/src/runtime/shared/connector-triggers.mjs +22 -19
  288. package/src/runtime/shared/defaults-separation.mjs +2 -14
  289. package/src/runtime/shared/delegated-approval-host.mjs +103 -11
  290. package/src/runtime/shared/developer-options.mjs +32 -1
  291. package/src/runtime/shared/err-text.mjs +24 -15
  292. package/src/runtime/shared/file-permissions.d.mts +0 -8
  293. package/src/runtime/shared/file-permissions.mjs +0 -11
  294. package/src/runtime/shared/git-direct.test-support.mjs +19 -0
  295. package/src/runtime/shared/google-oauth-client.generated.json +4 -0
  296. package/src/runtime/shared/google-oauth-client.mjs +34 -9
  297. package/src/runtime/shared/heartbeat-interval.d.mts +4 -0
  298. package/src/runtime/shared/heartbeat-interval.mjs +19 -0
  299. package/src/runtime/shared/idle-auto-clear-gate.mjs +104 -0
  300. package/src/runtime/shared/llm/anthropic-betas.mjs +7 -0
  301. package/src/runtime/shared/llm/anthropic-thinking-contract.mjs +12 -0
  302. package/src/runtime/shared/llm/provider-catalog-registry.mjs +1 -0
  303. package/src/runtime/shared/llm/usage-rollup.mjs +1 -1
  304. package/src/runtime/shared/llm/well-formed.mjs +66 -0
  305. package/src/runtime/shared/mixy-presence.mjs +22 -0
  306. package/src/runtime/shared/orchestration.d.mts +2 -0
  307. package/src/runtime/shared/pg-schema-db.mjs +27 -16
  308. package/src/runtime/shared/profile-config.cjs +1 -1
  309. package/src/runtime/shared/prompt-origin.d.mts +4 -0
  310. package/src/runtime/shared/prompt-origin.mjs +21 -0
  311. package/src/runtime/shared/provider-accounts.mjs +231 -59
  312. package/src/runtime/shared/provider-api-key.mjs +19 -8
  313. package/src/runtime/shared/provider-display-name.mjs +29 -0
  314. package/src/runtime/shared/provider-timeout-phase.mjs +2 -0
  315. package/src/runtime/shared/routine-trigger.d.mts +2 -0
  316. package/src/runtime/shared/routine-trigger.mjs +52 -0
  317. package/src/runtime/shared/sensitive-path-rules.d.mts +2 -10
  318. package/src/runtime/shared/sensitive-path-rules.mjs +19 -41
  319. package/src/runtime/shared/session-activity-signal.mjs +34 -1
  320. package/src/runtime/shared/session-sidebar-host.d.mts +1 -1
  321. package/src/runtime/shared/session-sidebar-host.mjs +7 -3
  322. package/src/runtime/shared/session-turn-prelude.mjs +18 -0
  323. package/src/runtime/shared/tool-execution-contract.mjs +2 -0
  324. package/src/runtime/shared/tool-result-summary.mjs +1 -14
  325. package/src/runtime/shared/transcript-metadata.mjs +38 -0
  326. package/src/runtime/shared/user-data-guard.mjs +30 -2
  327. package/src/runtime/shared/worktrees/activity-gate.mjs +12 -5
  328. package/src/runtime/shared/worktrees/test-support.mjs +9 -4
  329. package/src/runtime/terminal-bridge/client.mjs +10 -1
  330. package/src/runtime/visualize-bridge/client.mjs +27 -8
  331. package/src/runtime/visualize-bridge/tool-defs.mjs +22 -8
  332. package/src/runtime/visualize-bridge/tool-meta.d.mts +5 -0
  333. package/src/runtime/visualize-bridge/tool-meta.mjs +22 -0
  334. package/src/runtime/web-search/lib/fetch-pipeline.mjs +2 -2
  335. package/src/session-runtime/agent-home-delivery.mjs +486 -179
  336. package/src/session-runtime/agent-room-delivery.mjs +28 -1
  337. package/src/session-runtime/agent-task-sessions.mjs +561 -199
  338. package/src/session-runtime/boot/apis.mjs +9 -0
  339. package/src/session-runtime/boot/begin.mjs +3 -2
  340. package/src/session-runtime/boot/infrastructure.mjs +4 -0
  341. package/src/session-runtime/boot/providers.mjs +3 -0
  342. package/src/session-runtime/boot/tools.mjs +8 -1
  343. package/src/session-runtime/bot-chain.mjs +32 -0
  344. package/src/session-runtime/bot-status.mjs +7 -4
  345. package/src/session-runtime/channel-config-api.mjs +63 -17
  346. package/src/session-runtime/config-lifecycle.mjs +6 -0
  347. package/src/session-runtime/connector-merge-migration.mjs +305 -0
  348. package/src/session-runtime/internal-tool-executor/ask-user-tool.mjs +132 -28
  349. package/src/session-runtime/internal-tool-executor/bridge-tools.mjs +2 -0
  350. package/src/session-runtime/internal-tool-executor/reaction-tool.mjs +45 -0
  351. package/src/session-runtime/internal-tool-executor/workspace-tools.mjs +72 -5
  352. package/src/session-runtime/internal-tool-executor.mjs +4 -0
  353. package/src/session-runtime/lifecycle/inheritance.mjs +16 -3
  354. package/src/session-runtime/lifecycle/session-catalog.mjs +3 -1
  355. package/src/session-runtime/lifecycle/teardown/runtime-stops.mjs +50 -4
  356. package/src/session-runtime/lifecycle/teardown.mjs +23 -5
  357. package/src/session-runtime/mcp-glue/server-catalog.mjs +61 -3
  358. package/src/session-runtime/mcp-oauth-login.mjs +5 -3
  359. package/src/session-runtime/mixy-heartbeat.mjs +927 -0
  360. package/src/session-runtime/model-route/route-tuning.mjs +10 -6
  361. package/src/session-runtime/notification-bus.mjs +31 -117
  362. package/src/session-runtime/notification-completion-queue.mjs +54 -0
  363. package/src/session-runtime/notification-listeners.mjs +76 -0
  364. package/src/session-runtime/owned-automation-run.mjs +35 -13
  365. package/src/session-runtime/prewarm/channel-start.mjs +3 -7
  366. package/src/session-runtime/provider-auth/login.mjs +7 -0
  367. package/src/session-runtime/provider-models/row-hydration.mjs +13 -2
  368. package/src/session-runtime/resource-mcp-api.mjs +143 -224
  369. package/src/session-runtime/resource-mcp-connector-accounts.mjs +163 -0
  370. package/src/session-runtime/resource-mcp-oauth-api.mjs +217 -0
  371. package/src/session-runtime/resource-mcp-secrets.mjs +82 -0
  372. package/src/session-runtime/resource-mcp-servers.mjs +78 -0
  373. package/src/session-runtime/resource-mcp-store.mjs +49 -0
  374. package/src/session-runtime/routine-home-turn.mjs +33 -6
  375. package/src/session-runtime/routine-owner.mjs +40 -9
  376. package/src/session-runtime/schedule-session-run.mjs +7 -3
  377. package/src/session-runtime/services/agent-tool/close-flow.mjs +3 -0
  378. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-plan.mjs +8 -12
  379. package/src/session-runtime/services/agent-tool/spawn-preset.mjs +7 -2
  380. package/src/session-runtime/services/agent-tool/tag-registry.mjs +1 -0
  381. package/src/session-runtime/services/agent-tool/worker-index/awaiting-user-sync.mjs +51 -0
  382. package/src/session-runtime/services/agent-tool/worker-index/disposal.mjs +55 -0
  383. package/src/session-runtime/services/agent-tool/worker-index/released-store.mjs +123 -0
  384. package/src/session-runtime/services/agent-tool/worker-index/restart-settlement.mjs +112 -0
  385. package/src/session-runtime/services/agent-tool/worker-index/row-reads.mjs +36 -0
  386. package/src/session-runtime/services/agent-tool/worker-index/row-shape.mjs +2 -0
  387. package/src/session-runtime/services/agent-tool/worker-index/row-store.mjs +17 -4
  388. package/src/session-runtime/services/agent-tool/worker-index/row-writes.mjs +58 -0
  389. package/src/session-runtime/services/agent-tool/worker-index.mjs +41 -241
  390. package/src/session-runtime/services/agent-tool/worker-rows.mjs +3 -0
  391. package/src/session-runtime/services/channel-admin.mjs +73 -31
  392. package/src/session-runtime/services/channel-session-router.mjs +4 -2
  393. package/src/session-runtime/services/channel-worker.mjs +1 -1
  394. package/src/session-runtime/services/provider-admin.mjs +128 -25
  395. package/src/session-runtime/services/schedule-access.mjs +14 -0
  396. package/src/session-runtime/services/seeds.mjs +0 -28
  397. package/src/session-runtime/services/usage-dashboard-model.mjs +1 -0
  398. package/src/session-runtime/session-compaction-sync.mjs +27 -0
  399. package/src/session-runtime/session-create/session-options.mjs +8 -4
  400. package/src/session-runtime/settings-builtin-tools-api.mjs +11 -0
  401. package/src/session-runtime/settings-compaction-api.mjs +2 -40
  402. package/src/session-runtime/settings-system-api.mjs +28 -1
  403. package/src/session-runtime/setup-tool/agent-settings-actions.mjs +65 -0
  404. package/src/session-runtime/setup-tool/executor.mjs +187 -29
  405. package/src/session-runtime/setup-tool/extended-actions.mjs +33 -2
  406. package/src/session-runtime/setup-tool/session-route-actions.mjs +152 -0
  407. package/src/session-runtime/setup-tool/settings-contract.mjs +68 -6
  408. package/src/session-runtime/setup-tool/tool-defs.mjs +92 -6
  409. package/src/session-runtime/task-session-route.mjs +99 -0
  410. package/src/session-runtime/tool-policy/agent-profile-fold.mjs +17 -13
  411. package/src/session-runtime/tool-policy/policy-stage.mjs +21 -4
  412. package/src/session-runtime/tool-policy/surface-helpers.mjs +2 -3
  413. package/src/session-runtime/tool-surface.mjs +4 -0
  414. package/src/session-runtime/turn/turn-run/ask-callbacks.mjs +1 -0
  415. package/src/session-runtime/workflow-agents-api/agent-editor.mjs +98 -21
  416. package/src/session-runtime/workflow-agents-api/style-and-mode.mjs +4 -0
  417. package/src/session-runtime/workflow-agents-api/workflow-packs.mjs +6 -1
  418. package/src/session-runtime/workspace/transitions.mjs +10 -2
  419. package/src/standalone/channel-transport/client-registry/client-records.mjs +65 -0
  420. package/src/standalone/channel-transport/client-registry/client-registration.mjs +118 -0
  421. package/src/standalone/channel-transport/client-registry.mjs +24 -163
  422. package/src/standalone/channel-transport.mjs +1 -1
  423. package/src/standalone/daemon-channels-loader.mjs +10 -8
  424. package/src/standalone/daemon-session-control/list-page.mjs +72 -0
  425. package/src/standalone/daemon-session-control/request.mjs +51 -0
  426. package/src/standalone/daemon-session-control/room-host.mjs +57 -0
  427. package/src/standalone/daemon-session-control/route-control.mjs +48 -0
  428. package/src/standalone/daemon-session-control/send-turn.mjs +60 -0
  429. package/src/standalone/daemon-session-control/worktree-control.mjs +40 -0
  430. package/src/standalone/daemon-session-control.mjs +161 -157
  431. package/src/standalone/daemon.mjs +33 -2
  432. package/src/standalone/session-client.mjs +7 -3
  433. package/src/standalone/session-protocol.mjs +10 -0
  434. package/src/standalone/session-service/agent-tree/agent-cancel.mjs +6 -2
  435. package/src/standalone/session-service/agent-tree/agent-child.mjs +1 -0
  436. package/src/standalone/session-service/quota-resume.mjs +3 -0
  437. package/src/standalone/session-service/retention/busy.mjs +65 -1
  438. package/src/standalone/session-service/session-calls/session-action.mjs +23 -2
  439. package/src/standalone/session-service/session-calls/session-turns.mjs +6 -0
  440. package/src/standalone/session-service/session-calls/session-views.mjs +8 -0
  441. package/src/standalone/session-service.mjs +13 -0
  442. package/src/standalone/session-tool-output.mjs +21 -10
  443. package/src/tui/app/model-options.mjs +4 -27
  444. package/src/tui/dist/index.mjs +81 -65
  445. package/src/tui/session/agent-job-feed/model-notification.mjs +3 -0
  446. package/src/tui/session/initial-state.mjs +3 -0
  447. package/src/tui/session/notice-surface.mjs +23 -1
  448. package/src/tui/session/queue-helpers.mjs +3 -42
  449. package/src/tui/session/session-api/integrations.mjs +1 -0
  450. package/src/tui/session/session-api/lifecycle.mjs +8 -0
  451. package/src/tui/session/session-api/routes-agent-definitions.mjs +14 -2
  452. package/src/tui/session/session-api/routes-write.mjs +5 -1
  453. package/src/tui/session/session-api/routes.mjs +1 -0
  454. package/src/tui/session/session-api/settings.mjs +30 -2
  455. package/src/tui/session/session-flow/auto-clear.mjs +4 -33
  456. package/src/tui/session/session-flow/drain-closed-queue.mjs +38 -0
  457. package/src/tui/session/session-flow/drain-enqueue.mjs +86 -0
  458. package/src/tui/session/session-flow/drain-retry.mjs +35 -0
  459. package/src/tui/session/session-flow/drain.mjs +118 -149
  460. package/src/tui/session/session-flow/queue/entry-shape.mjs +5 -0
  461. package/src/tui/session/session-flow/queue/take-entries.mjs +5 -2
  462. package/src/tui/session/tool-approval-delegation.mjs +64 -0
  463. package/src/tui/session/tool-approval-request.mjs +39 -0
  464. package/src/tui/session/tool-approval-settle.mjs +62 -0
  465. package/src/tui/session/tool-approval.mjs +69 -115
  466. package/src/tui/session/tui-steering-persist.mjs +8 -1
  467. package/src/tui/session/turn-stream.mjs +36 -3
  468. package/src/tui/session/turn.mjs +23 -2
  469. package/src/vendor/statusline/bin/statusline-route.mjs +1 -219
  470. package/src/vendor/statusline/src/gateway/route-meta.mjs +35 -13
  471. package/scripts/code-graph-description-contract.mjs +0 -188
  472. package/src/runtime/channels/lib/owned-runtime/ownership-refresh.mjs +0 -36
@@ -0,0 +1,792 @@
1
+ // cache-warm.mjs — keeps a session's prompt cache readable for its next turn
2
+ // — every session, Lead, bot or sub-agent, while warming is on
3
+ // (`promptCacheWarming`, default on) — one interval at a time while the
4
+ // session idles, as far as the session's own history of returns says it
5
+ // pays. A warm re-sends the session's last real request
6
+ // (side-question-store.mjs) through the provider's cache-warm mode
7
+ // (providers/anthropic-cache-warm.mjs: the provider's own request builder,
8
+ // max_tokens 1, one POST with no retry, fallback or account rotation, cut at
9
+ // the first content block), so the provider reads and refreshes the cached
10
+ // prefix and writes nothing. The warm never reaches the transcript, the UI,
11
+ // the admission queue or the session record (lastProviderSendAt included);
12
+ // its usage-ledger row carries the `cache-warm` source, written on success,
13
+ // failure and cancellation.
14
+ //
15
+ // Scope: explicit-breakpoint providers (cache-strategy.mjs) with a warm mode —
16
+ // a miss costs a write premium there; automatic-caching providers are left
17
+ // alone. A request whose thinking budget derives from max_tokens is never
18
+ // replayed (a different budget changes the message cache key). A session
19
+ // that never idles long enough to return within a refresh interval (a
20
+ // sub-agent, closed when its task ends) learns no return and gets no warm.
21
+ //
22
+ // Rule (planCacheWarm), one for every warm, at each refresh point (one
23
+ // interval I = T − 60 s after the last confirmed cache use; T is
24
+ // the tier the provider reported writing), e the session's idle time since
25
+ // its last real call: warm iff, for some j ≥ 1
26
+ // further warms, P(e, j) · missCost > j · warmCost — missCost a write of the
27
+ // prompt at that tier minus the read the turn makes anyway, warmCost a read
28
+ // of it plus the one output token, and P(e, j) the lower bound of the 95%
29
+ // Wilson interval of this session's own recorded idle gaps (idle-gap store:
30
+ // the last 7 days) that ended in a return within (e, e + j·I] among those
31
+ // still idle at e. j = 1 is the per-interval break-even; a larger j is the
32
+ // same break-even over the warms a return further out needs (a 3-hourly
33
+ // routine is idle through two refresh points before it returns). No history
34
+ // → no warm → plain auto-clear. Returns count up to the idle window
35
+ // (IDLE_RETURN_MEASURED_WINDOW_MS after the last real call). A successful
36
+ // warm counts as cache use for the idle auto-clear (sessionActivityTimestamp
37
+ // in idle-auto-clear-gate.mjs), so its deadline moves with each one.
38
+ // Any fact the rule needs that is not known (written tier, prices, prompt
39
+ // size, the account holding the entry) means no warm. The chain stops on a
40
+ // miss, a failure, a late timer, a turn in flight, a new real request
41
+ // (side-question-store.mjs cancels), a compaction, a clear, a close, or a
42
+ // delete; the auto-clear then runs from the last real or successful warm
43
+ // use.
44
+ //
45
+ // Idle gaps, across turn boundaries only and from real activity only: every
46
+ // settled main-loop request of a real turn (any provider) moves the session's
47
+ // idle anchor to that request's send; a returning turn (the user's, a
48
+ // scheduled fire's, a webhook's, a heartbeat's, a sub-agent dispatch) closes
49
+ // the gap at its start once its first request goes out (noteCacheWarmTurn,
50
+ // noteCacheWarmActivity). Requests inside a running turn and a retried
51
+ // (continued) turn are no return. System-caused requests are neither return
52
+ // nor activity: turns the runtime starts itself (cacheWarmTurnKind), and
53
+ // every call outside the main loop's capture (compaction summaries, titles,
54
+ // /btw side questions, memory recap, prewarms, warms). The planner counts
55
+ // idle time from the same anchor. Each session learns from its own gaps. A gap still open at the idle window is recorded as
56
+ // censored there.
57
+ import { ACCOUNT_PROVIDERS } from '../../../shared/provider-accounts.mjs';
58
+ import { setCacheWarmUseSource } from '../../../shared/idle-auto-clear-gate.mjs';
59
+ import {
60
+ ANTHROPIC_TAIL_LIFETIME_MS,
61
+ cacheCapabilityForProvider,
62
+ providerTailCacheLifetimeMs,
63
+ } from '../agent-runtime/cache-strategy.mjs';
64
+ import { anthropicEffortLevelsFor } from '../../../shared/llm/anthropic-effort.mjs';
65
+ import { isTurnInFlight, runtimeState } from './manager/runtime-state.mjs';
66
+ import { withTailReminder } from './tail-reminder-rules.mjs';
67
+ import { promptCacheTouchStore } from './prompt-cache-touch-store.mjs';
68
+ import { createIdleGapStore } from './prompt-cache-idle-gaps.mjs';
69
+ import { registerSessionPurgeHook } from './store/session-observers.mjs';
70
+ import { PROMPT_ORIGIN_AGENT_REPORT, PROMPT_ORIGIN_CHANNEL, promptOriginOf } from '../../../shared/prompt-origin.mjs';
71
+
72
+ const CACHE_WARM_SOURCE = 'cache-warm';
73
+
74
+ // The refresh goes out this long before the entry's expiry, for both tiers
75
+ // (5m → 240 s after the last cache use, 1h → 3540 s): in a 7-day sample of
76
+ // hourly routine fires, cache misses inside the 1h lifetime appeared only at
77
+ // 3588–3597 s after the previous call, so 60 s ahead stays clear of them
78
+ // (git 795bb9e36's message; the sample itself is not kept in the repo). An
79
+ // entry with less than this left is not worth arming either: its refresh
80
+ // point has passed.
81
+ export const CACHE_WARM_LEAD_MS = 60_000;
82
+ // The idle stretch returns are learned over: 12 h after the session's last
83
+ // real call. Origin: the 7-day ledger measurement of idle-return chances
84
+ // (git 39efd623e, which first stated it) covered returns within 12 h of a
85
+ // session's last real call, "with the auto-clear window lifted to 12 h"; the
86
+ // per-session learner (git d284ef970) kept that window and attributed it to a
87
+ // "maintainer audit" that no commit or doc records beyond that measurement.
88
+ // A longer gap is recorded as censored there, and no warm serves a return
89
+ // beyond it.
90
+ export const IDLE_RETURN_MEASURED_WINDOW_MS = 12 * 60 * 60 * 1000;
91
+ // The period a session's idle gaps are kept and learned from: the last 7
92
+ // days — the length of the same ledger measurement (git 39efd623e,
93
+ // fe0271047: "a 7-day ledger sample"); no further derivation is recorded.
94
+ export const IDLE_RETURN_LOOKBACK_MS = 7 * 24 * 60 * 60 * 1000;
95
+ // z of a two-sided 95% normal interval (the 0.975 standard normal quantile),
96
+ // for the Wilson score bound.
97
+ const WILSON_Z_95 = 1.959963984540054;
98
+
99
+ /** Whether prompt-cache warming is on (config `promptCacheWarming`, default
100
+ * on; config.mjs migrates the earlier target list). */
101
+ export function cacheWarmingEnabled(config) {
102
+ return config?.promptCacheWarming !== false;
103
+ }
104
+
105
+ /** The lower bound of the 95% Wilson score interval of `hits` in `n`
106
+ * (0 for no trials): small samples get no invented confidence. */
107
+ export function wilsonLowerBound(hits, n, z = WILSON_Z_95) {
108
+ // No trials or no hits: the bound is exactly 0 (the formula's float
109
+ // rounding would leave ~1e-17).
110
+ if (!(n > 0) || !(hits > 0)) return 0;
111
+ const p = hits / n;
112
+ const z2 = z * z;
113
+ const centre = p + z2 / (2 * n);
114
+ const spread = z * Math.sqrt((p * (1 - p)) / n + z2 / (4 * n * n));
115
+ return Math.max(0, (centre - spread) / (1 + z2 / n));
116
+ }
117
+
118
+ /**
119
+ * The chance, learned from a session's own idle `gaps` (`{ ms, censored? }`),
120
+ * that a session idle for `elapsedMs` returns within `withinMs` more: the
121
+ * Wilson lower bound of the gaps that ended in a return in
122
+ * (elapsedMs, elapsedMs + withinMs] among the gaps still idle at elapsedMs
123
+ * (a censored gap is idle up to its cut and never a return).
124
+ * `{ idle, returns, p }`.
125
+ */
126
+ export function sessionReturnChance(gaps, elapsedMs, withinMs) {
127
+ let idle = 0;
128
+ let returns = 0;
129
+ for (const gap of gaps || []) {
130
+ if (!(gap.ms > elapsedMs)) continue;
131
+ idle += 1;
132
+ if (!gap.censored && gap.ms <= elapsedMs + withinMs) returns += 1;
133
+ }
134
+ return { idle, returns, p: wilsonLowerBound(returns, idle) };
135
+ }
136
+
137
+ /** When a warm goes out after the last cache use of an entry living `ttlMs`. */
138
+ export function cacheWarmDelayMs(ttlMs) {
139
+ return Math.max(1, ttlMs - CACHE_WARM_LEAD_MS);
140
+ }
141
+
142
+ const TIERS = Object.freeze(['5m', '1h']);
143
+
144
+ /**
145
+ * The lifetime of the message-tail entry a response wrote, or 0 when the
146
+ * usage does not prove it. Any write reaches the tail (a prefix write
147
+ * rewrites everything after it), so a single written tier is the tail's.
148
+ * With both tiers written, a tier is the tail's when no other breakpoint of
149
+ * `cacheStrategy` asked for it. A response that wrote nothing proves nothing.
150
+ */
151
+ export function writtenCacheTtlMs(usage, cacheStrategy) {
152
+ const written = Number(usage?.cacheWriteTokens) || 0;
153
+ if (!(written > 0)) return 0;
154
+ const written1h = Math.min(Number(usage?.cacheWrite1hTokens) || 0, written);
155
+ const written5m = written - written1h;
156
+ if (written5m <= 0) return ANTHROPIC_TAIL_LIFETIME_MS['1h'];
157
+ if (written1h <= 0) return ANTHROPIC_TAIL_LIFETIME_MS['5m'];
158
+ const tail = cacheStrategy?.messages;
159
+ if (!TIERS.includes(tail)) return 0;
160
+ const others = [cacheStrategy.tools, cacheStrategy.system, cacheStrategy.tier3];
161
+ return others.includes(tail) ? 0 : ANTHROPIC_TAIL_LIFETIME_MS[tail];
162
+ }
163
+
164
+ /** Why `request` (a side-question-store snapshot) may not be warmed, or null. */
165
+ export function cacheWarmIneligibility(request) {
166
+ const name = request?.provider?.name;
167
+ if (cacheCapabilityForProvider(name) !== 'explicit-breakpoint') return 'provider caches without breakpoints';
168
+ if (typeof request.provider.sendCacheWarm !== 'function') return 'provider has no cache-warm mode';
169
+ const opts = request.opts || {};
170
+ if (!opts.session?.id) return 'no session';
171
+ if (Number(opts.thinkingBudgetTokens) > 0) return 'thinking budget derives from max_tokens';
172
+ if (opts.effort && anthropicEffortLevelsFor(request.model).length === 0) {
173
+ return 'thinking budget derives from max_tokens';
174
+ }
175
+ if (!providerTailCacheLifetimeMs(name, opts.cacheStrategy)) return 'no expiring message-tail cache';
176
+ return null;
177
+ }
178
+
179
+ /** The send a warm makes for `request`: the same messages (with the same
180
+ * ephemeral tail reminder the real send added), model, tools and options,
181
+ * with max_tokens 1 and the warm's own signal. */
182
+ export function cacheWarmSend(request, { signal } = {}) {
183
+ const session = request.opts?.session || null;
184
+ return {
185
+ messages: withTailReminder(request.messages, { session, provider: request.provider, model: request.model }),
186
+ model: request.model,
187
+ tools: request.tools.length ? request.tools : undefined,
188
+ opts: { ...request.opts, maxOutputTokensOverride: 1, signal },
189
+ };
190
+ }
191
+
192
+ /** `{ warmCost, missCost }` in dollars for a prompt of `promptTokens` at
193
+ * per-token `prices` ({ read, write, output } of the entry's tier), or null
194
+ * when the prompt size or any price is unknown. */
195
+ export function cacheWarmEconomics(promptTokens, prices) {
196
+ const tokens = Number(promptTokens);
197
+ const read = prices?.read;
198
+ const write = prices?.write;
199
+ const output = prices?.output;
200
+ if (!(tokens > 0) || ![read, write, output].every((value) => Number.isFinite(value) && value >= 0)) return null;
201
+ return { warmCost: read * tokens + output, missCost: Math.max(0, (write - read) * tokens) };
202
+ }
203
+
204
+ /**
205
+ * Whether a warm sent at `now` pays off for the session: the entry was last
206
+ * used at `touchAt` and lives `ttlMs`; the session's last real call went out
207
+ * at `lastCallAt`; `gaps` are its own recorded idle gaps; `latencyMs` the
208
+ * measured time a warm of this chain took to reach the provider (null: not
209
+ * measured yet). With e = now − lastCallAt and I the refresh interval, warm
210
+ * iff some j ≥ 1 has P(e, j·I) · missCost > j · warmCost (P:
211
+ * sessionReturnChance), returns counted inside the idle window. The idle
212
+ * auto-clear is no bound: a successful warm is cache use, so its deadline
213
+ * moves with each one. `{ warm: true, warms: j, p }` or `{ stop }`.
214
+ */
215
+ export function planCacheWarm({ now, touchAt, ttlMs, lastCallAt, gaps, promptTokens, prices, latencyMs = null }) {
216
+ if (!(lastCallAt > 0)) return { stop: 'last real call unknown' };
217
+ // The warm serves only a return after the current entry's expiry, inside
218
+ // the idle window returns are learned over.
219
+ if (!(touchAt + ttlMs < lastCallAt + IDLE_RETURN_MEASURED_WINDOW_MS)) {
220
+ return { stop: 'idle time reaches the learned window' };
221
+ }
222
+ // The warm must reach the provider while the entry still lives: a timer
223
+ // fires at or after the refresh point, TTL − lead, so the lead itself cannot
224
+ // be the floor; the chain's measured warm latency (none before the first
225
+ // warm) is.
226
+ if (touchAt + ttlMs - now <= (Number(latencyMs) || 0)) return { stop: 'late timer' };
227
+ const economics = cacheWarmEconomics(promptTokens, prices);
228
+ if (!economics) return { stop: 'prompt size or cache prices unknown' };
229
+ const interval = cacheWarmDelayMs(ttlMs);
230
+ const elapsed = now - lastCallAt;
231
+ if (!sessionReturnChance(gaps, elapsed, 0).idle) return { stop: 'no idle history this long' };
232
+ for (let j = 1; elapsed + j * interval <= IDLE_RETURN_MEASURED_WINDOW_MS; j += 1) {
233
+ const { p } = sessionReturnChance(gaps, elapsed, j * interval);
234
+ if (p * economics.missCost > j * economics.warmCost) return { warm: true, warms: j, p };
235
+ }
236
+ return { stop: 'warms cost more than the expected miss' };
237
+ }
238
+
239
+ // Defaults of the injectable dependencies.
240
+
241
+ async function defaultEnabled() {
242
+ const { loadConfig } = await import('../config.mjs');
243
+ return cacheWarmingEnabled(loadConfig({ secrets: false }));
244
+ }
245
+
246
+ // Per-token dollars of a cache read, a cache write of `ttlMs`'s tier and an
247
+ // output token for the request's model, from the pricing catalog (null when
248
+ // unpriced), at the rates of a prompt of `promptTokens`: a model priced by
249
+ // prompt length charges cache reads and writes at the tier that length falls
250
+ // in (Anthropic prompt-caching docs, Pricing: "On a model priced by prompt
251
+ // length, cache reads and cache writes count toward a request's prompt
252
+ // length"), and the warm's one output token at that tier too. The request's
253
+ // own service tier prices it as the real send was (fast mode: the provider's
254
+ // builder sends `speed: 'fast'` under exactly these conditions,
255
+ // providers/anthropic-api-request.mjs, anthropic-oauth.mjs).
256
+ export async function defaultPrices(request, ttlMs, promptTokens) {
257
+ const [{ priceUsage }, { supportsAnthropicFastMode }, { fastModeAvailable }] = await Promise.all([
258
+ import('../../../shared/llm/cost.mjs'),
259
+ import('../../../shared/llm/anthropic-betas.mjs'),
260
+ import('../providers/anthropic-fast-mode.mjs'),
261
+ ]);
262
+ const MILLION = 1_000_000;
263
+ const fast = request.opts?.fast === true && supportsAnthropicFastMode(request.model) && fastModeAvailable();
264
+ const base = {
265
+ model: request.model,
266
+ provider: request.provider.name,
267
+ inputTokensInclusive: false,
268
+ ...(fast ? { requestServiceTier: 'fast' } : {}),
269
+ };
270
+ // The per-million rates of a request using exactly `tokens` (null when any
271
+ // used slot is unpriced); a request of promptTokens prompt tokens in all.
272
+ const ratesOf = (tokens) => {
273
+ const priced = priceUsage({ ...base, ...tokens });
274
+ return priced.costUsd === null || priced.costUsd === undefined ? null : priced.rates;
275
+ };
276
+ const perToken = (rates, key) => (rates ? rates[key] / MILLION : null);
277
+ const oneHour = ttlMs >= ANTHROPIC_TAIL_LIFETIME_MS['1h'];
278
+ const readRates = ratesOf({ cacheReadTokens: promptTokens, outputTokens: 1 });
279
+ const writeRates = ratesOf({
280
+ cacheWriteTokens: promptTokens,
281
+ ...(oneHour ? { cacheWrite1hTokens: promptTokens } : {}),
282
+ });
283
+ return {
284
+ read: perToken(readRates, 'cacheReadCostPerM'),
285
+ write: perToken(writeRates, oneHour ? 'cacheWrite1hCostPerM' : 'cacheWriteCostPerM'),
286
+ output: perToken(readRates, 'outputCostPerM'),
287
+ };
288
+ }
289
+
290
+ // The registered providers a re-armed request needs: waits for the owning
291
+ // runtime's own provider initialization; never initializes them itself.
292
+ async function defaultProvidersReady() {
293
+ const { providersInitialized } = await import('../providers/registry.mjs');
294
+ await providersInitialized();
295
+ }
296
+
297
+ async function defaultRegisteredProvider(name) {
298
+ const { registeredProvider } = await import('../providers/registry.mjs');
299
+ return registeredProvider(name);
300
+ }
301
+
302
+ async function defaultSend(request, chain, target, onUsage) {
303
+ const controller = new AbortController();
304
+ chain.controller = controller;
305
+ try {
306
+ const send = cacheWarmSend(request, { signal: controller.signal });
307
+ return await target.provider.sendCacheWarm(send.messages, send.model, send.tools, send.opts, {
308
+ onUsage,
309
+ accountId: target.accountId,
310
+ });
311
+ } finally {
312
+ if (chain.controller === controller) chain.controller = null;
313
+ }
314
+ }
315
+
316
+ // The warm's usage-ledger row, in the shape the provider boundary writes
317
+ // (usage-accounting.mjs), under the `cache-warm` source; the ledger prices
318
+ // every reported token with the catalog.
319
+ async function defaultRecordUsage(request, usage, startedAt, accountId) {
320
+ const { getUsageLedger, makeUsageRecord } = await import('../../../shared/llm/usage-ledger.mjs');
321
+ const ledger = getUsageLedger();
322
+ if (!ledger) return;
323
+ const session = request.opts.session;
324
+ await ledger.recordQueued(
325
+ makeUsageRecord({
326
+ ts: Date.now(),
327
+ provider: request.provider.name,
328
+ model: request.model,
329
+ requestedModel: request.model,
330
+ sessionId: session.id,
331
+ ownerSessionId: session.ownerSessionId || session.parentSessionId,
332
+ sourceType: CACHE_WARM_SOURCE,
333
+ inputTokens: usage.inputTokens,
334
+ inputTokensInclusive: false,
335
+ outputTokens: usage.outputTokens,
336
+ cacheReadTokens: usage.cachedTokens,
337
+ cacheWriteTokens: usage.cacheWriteTokens,
338
+ cacheWrite1hTokens: usage.cacheWrite1hTokens,
339
+ account: accountId || '',
340
+ durationMs: Date.now() - startedAt,
341
+ })
342
+ );
343
+ }
344
+
345
+ async function defaultLastSentRequest(sessionId) {
346
+ const { lastSentMainRequest } = await import('./side-question.mjs');
347
+ return lastSentMainRequest(sessionId);
348
+ }
349
+
350
+ const DEFAULT_DEPS = Object.freeze({
351
+ now: () => Date.now(),
352
+ timers: globalThis,
353
+ enabled: defaultEnabled,
354
+ turnInFlight: (sessionId) => isTurnInFlight(runtimeState.get(sessionId)),
355
+ lastSentRequest: defaultLastSentRequest,
356
+ providersReady: defaultProvidersReady,
357
+ registeredProvider: defaultRegisteredProvider,
358
+ prices: defaultPrices,
359
+ send: defaultSend,
360
+ recordUsage: defaultRecordUsage,
361
+ touches: () => promptCacheTouchStore(),
362
+ gaps: () => promptCacheIdleGapStore(),
363
+ log: (line) => process.stderr.write(`[cache-warm] ${line}\n`),
364
+ });
365
+ let deps = DEFAULT_DEPS;
366
+
367
+ // The idle auto-clear measures idleness from the last cache use, a
368
+ // successful warm included (durable in the touch store).
369
+ setCacheWarmUseSource((sessionId) => (sessionId ? deps.touches().lastWarmUse(sessionId) : 0));
370
+
371
+ // A session deleted for good drops everything it learned and touched.
372
+ registerSessionPurgeHook((sessionId) => {
373
+ cancelCacheWarm(sessionId);
374
+ turns.delete(sessionId);
375
+ deps.touches().forget(sessionId);
376
+ deps.touches().forgetWarmUse(sessionId);
377
+ deps.gaps().forget(sessionId);
378
+ });
379
+
380
+ let defaultGapStore = null;
381
+ /** The idle-gap store the warmer learns from (prompt-cache-idle-gaps.mjs). */
382
+ function promptCacheIdleGapStore() {
383
+ defaultGapStore ??= createIdleGapStore({
384
+ lookbackMs: IDLE_RETURN_LOOKBACK_MS,
385
+ windowMs: IDLE_RETURN_MEASURED_WINDOW_MS,
386
+ // No refresh point lies inside a gap shorter than the shortest interval.
387
+ minMs: cacheWarmDelayMs(ANTHROPIC_TAIL_LIFETIME_MS['5m']),
388
+ });
389
+ return defaultGapStore;
390
+ }
391
+
392
+ // The plan of `chain` at its refresh point `at` (planCacheWarm), from the
393
+ // session's own idle history. Unknown facts fail closed.
394
+ async function planFor(sessionId, chain, at) {
395
+ if (!(await deps.enabled())) return { stop: 'cache warming off' };
396
+ return planCacheWarm({
397
+ now: at,
398
+ touchAt: chain.touchAt,
399
+ ttlMs: chain.ttlMs,
400
+ // Idle time counts from the last real activity (the gap store's anchor),
401
+ // the clock the gaps were learned on; a system-caused request since then
402
+ // refreshed the cache (touchAt) but not the idle clock.
403
+ lastCallAt: deps.gaps().openAt(sessionId),
404
+ gaps: deps.gaps().gaps(sessionId),
405
+ promptTokens: chain.promptTokens,
406
+ prices: await deps.prices(chain.request, chain.ttlMs, chain.promptTokens),
407
+ latencyMs: chain.latencyMs,
408
+ });
409
+ }
410
+
411
+ /** Test seam: replace some dependencies; call with no argument to restore. */
412
+ export function _setCacheWarmDepsForTest(overrides = null) {
413
+ for (const sessionId of [...chains.keys()]) cancelCacheWarm(sessionId);
414
+ deps = overrides ? { ...DEFAULT_DEPS, ...overrides } : DEFAULT_DEPS;
415
+ }
416
+
417
+ // sessionId -> the armed chain: { request, ttlMs, touchAt, promptTokens,
418
+ // accountId, latencyMs, refreshAt, timer, controller }.
419
+ const chains = new Map();
420
+
421
+ /** The armed chain of `sessionId` (tests and diagnostics). */
422
+ export function cacheWarmChain(sessionId) {
423
+ return chains.get(sessionId) || null;
424
+ }
425
+
426
+ // Turns the runtime starts itself, by their prompt provenance
427
+ // (manager/ask-queue.mjs `promptSource`): a Goal's automatic continuation and
428
+ // close-out (runtime-synthetic), and a task notification (a finished
429
+ // background task or a pending resume reported back). Neither the user nor a
430
+ // routine caused them — unless the notification is an inbound channel
431
+ // message (`origin` PROMPT_ORIGIN_CHANNEL), which is presented the same way.
432
+ // The other way round, a prompt whose origin is PROMPT_ORIGIN_AGENT_REPORT (a
433
+ // bot-home wake, a room member's relay) is the runtime's though presented as
434
+ // a prompt.
435
+ const SYSTEM_TURN_SOURCES = new Set(['goal-continuation', 'goal-closeout', 'task-notification']);
436
+
437
+ // What a turn of the provenance `source` / `origin` is: 'system' or 'return'.
438
+ function provenanceKind(source, origin) {
439
+ if (origin === PROMPT_ORIGIN_CHANNEL) return 'return';
440
+ if (origin === PROMPT_ORIGIN_AGENT_REPORT) return 'system';
441
+ return SYSTEM_TURN_SOURCES.has(source) ? 'system' : 'return';
442
+ }
443
+
444
+ // The input message of the failed turn a retry reruns, by the turn's durable
445
+ // identity. The failure's completion sits on the session's last message
446
+ // (ask-turn-settle.mjs attachFailedTurnCompletion: the prompt itself, or the
447
+ // turn's last reply / tool result); its owner is the user message of the same
448
+ // attempt (`attemptId`, else the turn start `at` the settle path matches on),
449
+ // searched back to the previous turn's recorded end. A retained retry
450
+ // (`retryTurnId`) names the failed prompt itself (retainedFailedTurnInput).
451
+ function failedTurnMessage(messages, input) {
452
+ const list = Array.isArray(messages) ? messages : [];
453
+ const endIndex = list.findLastIndex((item) => item?.role !== 'system');
454
+ const end = list[endIndex];
455
+ const completion = end?.meta?.transcript?.completion;
456
+ if (completion?.status !== 'failed') return null;
457
+ if (input.retryTurnId && end.meta.transcript.messageId !== input.retryTurnId) return null;
458
+ if (end.role === 'user') return end;
459
+ const attemptId = typeof completion.attemptId === 'string' ? completion.attemptId : '';
460
+ const at = Number(completion.at) || 0;
461
+ for (let index = endIndex - 1; index >= 0; index -= 1) {
462
+ const transcript = list[index]?.meta?.transcript;
463
+ if (transcript?.completion) return null;
464
+ if (list[index].role !== 'user' || !transcript) continue;
465
+ if (transcript.attemptId ? transcript.attemptId === attemptId : at > 0 && Number(transcript.at) === at) {
466
+ return list[index];
467
+ }
468
+ }
469
+ return null;
470
+ }
471
+
472
+ /** The provenance `{ source, origin }` of the failed turn a retry input
473
+ * reruns (failedTurnMessage), or null when it cannot be resolved. A rebuilt
474
+ * retry message records it (`meta.provenance`, ask-turn-execute.mjs), so a
475
+ * turn retried again keeps it. */
476
+ export function failedTurnProvenance(messages, input) {
477
+ if (input?.retryFailedTurn !== true && !input?.retryTurnId) return null;
478
+ const meta = failedTurnMessage(messages, input)?.meta;
479
+ if (!meta) return null;
480
+ if (meta.provenance && typeof meta.provenance === 'object') return { ...meta.provenance };
481
+ const origin = promptOriginOf(meta.origin);
482
+ return {
483
+ ...(typeof meta.source === 'string' && meta.source ? { source: meta.source } : {}),
484
+ ...(origin ? { origin } : {}),
485
+ };
486
+ }
487
+
488
+ /**
489
+ * What a turn of askSession is for the idle learning, from its input (and,
490
+ * for a retry, the session's `messages` as the turn found them):
491
+ * - a retry of a failed turn (`retryFailedTurn` / `retryTurnId`: the failed
492
+ * turn's own user message runs again — a quota auto-resume, the Retry
493
+ * action) continues that turn: 'system' when the failed turn was system-
494
+ * caused (its stored meta.source / meta.origin / meta.provenance), else
495
+ * 'continuation';
496
+ * - 'system' for a SYSTEM_TURN_SOURCES prompt not from a channel, and for an
497
+ * agent report (a bot-home wake, a room relay);
498
+ * - 'return' for everything else (the user, a scheduled fire, a webhook, a
499
+ * heartbeat, a channel message, a sub-agent dispatch).
500
+ */
501
+ export function cacheWarmTurnKind(input, messages = null) {
502
+ if (input?.retryFailedTurn === true || input?.retryTurnId) {
503
+ const failed = failedTurnProvenance(messages, input);
504
+ return failed && provenanceKind(failed.source, failed.origin) === 'system' ? 'system' : 'continuation';
505
+ }
506
+ const source = typeof input?.promptSource === 'string' ? input.promptSource : input?.promptSource?.source;
507
+ return provenanceKind(source, input?.promptOrigin);
508
+ }
509
+
510
+ // sessionId -> its running turn: { kind, startedAt, returning }.
511
+ const turns = new Map();
512
+
513
+ /** A turn of the session opened (manager/ask-session.mjs), of `kind`
514
+ * (cacheWarmTurnKind). A 'return' closes the session's idle gap at its
515
+ * start, once its first request goes out. Returns the turn's entry, which
516
+ * endCacheWarmTurn takes. */
517
+ export function noteCacheWarmTurn(sessionId, kind) {
518
+ if (!sessionId) return null;
519
+ const turn = { kind, startedAt: deps.now(), returning: kind === 'return', suspended: null };
520
+ turns.set(sessionId, turn);
521
+ // A turn entering the session owns its cache from here: a warm pending in
522
+ // its planning lookups or on the wire must not overlap the real request.
523
+ // The chain is stopped (the abort is not a miss) and its captured request
524
+ // and confirmed touch are kept on the turn: the turn's own settled request
525
+ // arms the next chain, and a turn that ends without any request gets this
526
+ // one back (endCacheWarmTurn).
527
+ const chain = chains.get(sessionId);
528
+ if (chain) {
529
+ const { request, ttlMs, touchAt, promptTokens, accountId, latencyMs } = chain;
530
+ turn.suspended = { request, ttlMs, touchAt, promptTokens, accountId, latencyMs };
531
+ stopChain(sessionId, chain, 'turn entered', { quiet: true });
532
+ }
533
+ return turn;
534
+ }
535
+
536
+ /** The turn noteCacheWarmTurn returned ended (completed, failed or
537
+ * cancelled): its entry goes unless a later turn already replaced it. The
538
+ * durable idle anchor stays. When the turn sent no request (any request,
539
+ * invalidation or close dropped `suspended`), no chain replaced the one it
540
+ * stopped and the cache entry has not expired, that chain comes back with
541
+ * its original touch and schedule; the touch and idle-gap stores are left
542
+ * alone. */
543
+ export function endCacheWarmTurn(sessionId, turn) {
544
+ if (!turn || turns.get(sessionId) !== turn) return;
545
+ turns.delete(sessionId);
546
+ const chain = turn.suspended;
547
+ turn.suspended = null;
548
+ if (!chain || chains.has(sessionId)) return;
549
+ if (deps.now() < chain.touchAt + chain.ttlMs - CACHE_WARM_LEAD_MS) {
550
+ const restored = startChain(sessionId, chain);
551
+ restored.latencyMs = chain.latencyMs;
552
+ }
553
+ }
554
+
555
+ /** The running turn the warmer holds for `sessionId`, or null (tests). */
556
+ export function cacheWarmTurn(sessionId) {
557
+ return turns.get(sessionId) || null;
558
+ }
559
+
560
+ // Whether a captured request is the session's real activity: one of a turn
561
+ // that is not system-caused, sent by the main loop itself (no `requestKind`,
562
+ // or the wire's 'turn'; a compaction summary or a prewarm names its own).
563
+ function realActivity(sessionId, request) {
564
+ if (turns.get(sessionId)?.kind === 'system') return false;
565
+ const kind = request?.opts?.requestKind;
566
+ return !kind || kind === 'turn';
567
+ }
568
+
569
+ /** A request of the main loop is going out: the chain stops; when it is the
570
+ * first request of a returning turn, the open idle gap closes at the turn's
571
+ * start. Requests inside a running turn, of a continued turn or of a
572
+ * system-caused turn are no return. Warms never come through here. */
573
+ export function noteCacheWarmActivity(sessionId, request = null) {
574
+ if (!sessionId) return;
575
+ const turn = turns.get(sessionId);
576
+ if (turn?.returning && realActivity(sessionId, request)) {
577
+ turn.returning = false;
578
+ deps.gaps().close(sessionId, turn.startedAt);
579
+ }
580
+ cancelCacheWarm(sessionId);
581
+ }
582
+
583
+ /** Drop the session's chain: clear its timer and abort a warm in flight
584
+ * (its usage is still accounted). */
585
+ export function cancelCacheWarm(sessionId) {
586
+ // Whatever cancels (a request, an invalidation, a close) also voids the
587
+ // chain the running turn would restore.
588
+ const turn = sessionId ? turns.get(sessionId) : null;
589
+ if (turn) turn.suspended = null;
590
+ const chain = sessionId ? chains.get(sessionId) : null;
591
+ if (chain) stopChain(sessionId, chain, 'canceled', { quiet: true });
592
+ }
593
+
594
+ /** The session's cached prefix is gone (compaction, clear, rewind): drop
595
+ * the chain, its durable touch and its warm use. */
596
+ export function forgetCacheWarm(sessionId) {
597
+ cancelCacheWarm(sessionId);
598
+ if (!sessionId) return;
599
+ deps.touches().forget(sessionId);
600
+ deps.touches().forgetWarmUse(sessionId);
601
+ }
602
+
603
+ function recordTouch(sessionId, chain) {
604
+ deps.touches().record(sessionId, {
605
+ touchAt: chain.touchAt,
606
+ ttlMs: chain.ttlMs,
607
+ promptTokens: chain.promptTokens,
608
+ accountId: chain.accountId,
609
+ });
610
+ }
611
+
612
+ function stopChain(sessionId, chain, reason, { quiet = false } = {}) {
613
+ if (chains.get(sessionId) !== chain) return;
614
+ chains.delete(sessionId);
615
+ if (chain.timer) deps.timers.clearTimeout(chain.timer);
616
+ chain.timer = null;
617
+ chain.controller?.abort(new Error(`cache warm stopped: ${reason}`));
618
+ if (!quiet) deps.log(`${sessionId}: stopped (${reason})`);
619
+ }
620
+
621
+ function startChain(sessionId, { request, ttlMs, touchAt, promptTokens, accountId }) {
622
+ const chain = {
623
+ request,
624
+ ttlMs,
625
+ touchAt,
626
+ promptTokens,
627
+ accountId: accountId || '',
628
+ latencyMs: null,
629
+ refreshAt: 0,
630
+ timer: null,
631
+ controller: null,
632
+ };
633
+ chains.set(sessionId, chain);
634
+ scheduleWarm(sessionId, chain);
635
+ return chain;
636
+ }
637
+
638
+ // The provider and account a warm of `chain` goes to: the roster account
639
+ // that holds the entry, or the provider itself when it has no roster. null
640
+ // when that account is not known.
641
+ function warmTarget(chain) {
642
+ const provider = chain.request.provider;
643
+ if (!ACCOUNT_PROVIDERS.includes(provider.name)) return { provider, accountId: '' };
644
+ if (!chain.accountId || typeof provider.forAccount !== 'function') return null;
645
+ return { provider: provider.forAccount(chain.accountId), accountId: chain.accountId };
646
+ }
647
+
648
+ /**
649
+ * Arm the session's warm chain from a real request that settled with
650
+ * `response`, and record the confirmed touch durably; replaces any chain.
651
+ * Nothing is armed — and the durable touch is dropped — for an ineligible
652
+ * request, one without a send time, or a response that does not prove the
653
+ * tier it wrote or the prompt size.
654
+ */
655
+ export function armCacheWarm(sessionId, request, response) {
656
+ cancelCacheWarm(sessionId);
657
+ // A real request settled: the session's own activity is the latest use.
658
+ if (sessionId) deps.touches().forgetWarmUse(sessionId);
659
+ // Whatever its provider: after real activity the session's idle clock runs
660
+ // from this request's send (the clock the planner reads). A later request
661
+ // of the same turn only moves it; the next returning turn closes it. A
662
+ // system-caused request leaves it alone.
663
+ if (sessionId && request && realActivity(sessionId, request)) {
664
+ deps.gaps().open(sessionId, Number(request.sentAt));
665
+ }
666
+ if (!sessionId || !request || cacheWarmIneligibility(request)) return;
667
+ const usage = response?.usage || {};
668
+ const touchAt = Number(request.sentAt);
669
+ const ttlMs = writtenCacheTtlMs(usage, request.opts.cacheStrategy);
670
+ const promptTokens =
671
+ typeof usage.promptTokens === 'number'
672
+ ? usage.promptTokens
673
+ : (Number(usage.inputTokens) || 0) + (Number(usage.cachedTokens) || 0) + (Number(usage.cacheWriteTokens) || 0);
674
+ if (!(touchAt > 0) || !ttlMs || !(promptTokens > 0)) {
675
+ deps.touches().forget(sessionId);
676
+ return;
677
+ }
678
+ const chain = startChain(sessionId, {
679
+ request,
680
+ ttlMs,
681
+ touchAt,
682
+ promptTokens,
683
+ accountId: response?.providerMetadata?.accountRouting?.accountId,
684
+ });
685
+ recordTouch(sessionId, chain);
686
+ }
687
+
688
+ /**
689
+ * Daemon start: the in-memory chains died with the previous process. Re-arm
690
+ * every touched session while warming is on from its durable touch (prompt-cache-touch-store.mjs: the last confirmed cache
691
+ * use, the tier the provider wrote, the account and prompt size) while the
692
+ * entry still has more than the lead left, replaying the request its
693
+ * next turn rebuilds without the last reply. A candidate's expired touch is
694
+ * dropped; each chain's first refresh point applies its rule. Resolves how
695
+ * many chains were armed.
696
+ */
697
+ export async function rearmCacheWarmsAfterRestart() {
698
+ if (!(await deps.enabled())) return 0;
699
+ const candidates = [];
700
+ for (const sessionId of deps.touches().ids()) {
701
+ if (chains.has(sessionId)) continue;
702
+ const touch = deps.touches().get(sessionId);
703
+ if (!touch) continue;
704
+ if (!(deps.now() < touch.touchAt + touch.ttlMs - CACHE_WARM_LEAD_MS)) {
705
+ deps.touches().forget(sessionId);
706
+ continue;
707
+ }
708
+ candidates.push([sessionId, touch]);
709
+ }
710
+ if (!candidates.length) return 0;
711
+ await deps.providersReady();
712
+ let armed = 0;
713
+ for (const [sessionId, touch] of candidates) {
714
+ let request = null;
715
+ try {
716
+ request = await deps.lastSentRequest(sessionId);
717
+ } catch (error) {
718
+ deps.log(`${sessionId}: restart re-arm skipped (${error?.message || error})`);
719
+ }
720
+ if (!request || cacheWarmIneligibility(request) || chains.has(sessionId)) continue;
721
+ startChain(sessionId, {
722
+ request,
723
+ ttlMs: touch.ttlMs,
724
+ touchAt: touch.touchAt,
725
+ promptTokens: touch.promptTokens,
726
+ accountId: touch.accountId,
727
+ });
728
+ armed += 1;
729
+ }
730
+ return armed;
731
+ }
732
+
733
+ function scheduleWarm(sessionId, chain) {
734
+ chain.refreshAt = chain.touchAt + cacheWarmDelayMs(chain.ttlMs);
735
+ chain.timer = deps.timers.setTimeout(
736
+ () => void fireWarm(sessionId, chain).catch((error) => stopChain(sessionId, chain, error?.message || error)),
737
+ Math.max(0, chain.refreshAt - deps.now())
738
+ );
739
+ chain.timer?.unref?.();
740
+ }
741
+
742
+ async function fireWarm(sessionId, chain) {
743
+ if (chains.get(sessionId) !== chain) return;
744
+ chain.timer = null;
745
+ if (deps.turnInFlight(sessionId)) return stopChain(sessionId, chain, 'turn in flight');
746
+ const plan = await planFor(sessionId, chain, deps.now());
747
+ if (chains.get(sessionId) !== chain) return;
748
+ if (plan.stop) return stopChain(sessionId, chain, plan.stop);
749
+ // A registry re-initialization replaced the instance the request was
750
+ // captured on: its replay would run on a retired instance, and the request
751
+ // is not rebuilt onto the new one.
752
+ if ((await deps.registeredProvider(chain.request.provider.name)) !== chain.request.provider) {
753
+ return stopChain(sessionId, chain, 'provider instance replaced');
754
+ }
755
+ if (chains.get(sessionId) !== chain) return;
756
+ const target = warmTarget(chain);
757
+ if (!target) return stopChain(sessionId, chain, 'account holding the cache unknown');
758
+ // Last check before dispatch, after the final await: a turn that began
759
+ // during the lookups has the cache to itself.
760
+ if (deps.turnInFlight(sessionId)) return stopChain(sessionId, chain, 'turn in flight');
761
+ const sentAt = deps.now();
762
+ let usage = null;
763
+ let reportedAt = null;
764
+ let failure = null;
765
+ try {
766
+ usage = await deps.send(chain.request, chain, target, (reported) => {
767
+ usage = reported;
768
+ reportedAt ??= deps.now();
769
+ });
770
+ } catch (error) {
771
+ failure = error;
772
+ usage = error?.usage || usage;
773
+ }
774
+ // Billed whatever happened next: success, failure or cancellation.
775
+ if (usage) {
776
+ void Promise.resolve()
777
+ .then(() => deps.recordUsage(chain.request, usage, sentAt, target.accountId))
778
+ .catch((error) => deps.log(`${sessionId}: warm usage not recorded (${error?.message || error})`));
779
+ }
780
+ if (chains.get(sessionId) !== chain) return;
781
+ if (failure) return stopChain(sessionId, chain, `warm failed: ${failure?.message || failure}`);
782
+ if (!(Number(usage?.cachedTokens) > 0) || Number(usage?.cacheWriteTokens) > 0) {
783
+ deps.touches().forget(sessionId);
784
+ return stopChain(sessionId, chain, 'warm missed the cache');
785
+ }
786
+ chain.touchAt = sentAt;
787
+ chain.latencyMs = (reportedAt ?? deps.now()) - sentAt;
788
+ recordTouch(sessionId, chain);
789
+ // A read without a write is cache use: the idle auto-clear counts from it.
790
+ deps.touches().recordWarmUse(sessionId, sentAt);
791
+ scheduleWarm(sessionId, chain);
792
+ }