talon-agent 4.6.1 → 5.0.1

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 (310) hide show
  1. package/README.md +3 -3
  2. package/package.json +2 -1
  3. package/prompts/README.md +2 -2
  4. package/prompts/system/memory-recall.md +16 -0
  5. package/src/app.ts +22 -7
  6. package/src/backend/claude-sdk/factory.ts +21 -41
  7. package/src/backend/claude-sdk/handler.ts +28 -7
  8. package/src/backend/claude-sdk/host/in-process.ts +147 -0
  9. package/src/backend/claude-sdk/one-shot.ts +1 -1
  10. package/src/backend/claude-sdk/options.ts +7 -6
  11. package/src/backend/claude-sdk/stream.ts +30 -3
  12. package/src/backend/claude-sdk/warm.ts +1 -1
  13. package/src/backend/codex/constants.ts +1 -1
  14. package/src/backend/codex/factory.ts +2 -2
  15. package/src/backend/codex/handler/events.ts +1 -1
  16. package/src/backend/codex/handler/message.ts +26 -12
  17. package/src/backend/codex/handler/rollout-accounting.ts +1 -1
  18. package/src/backend/codex/init.ts +1 -1
  19. package/src/backend/codex/mcp-config.ts +2 -2
  20. package/src/backend/codex/one-shot.ts +1 -1
  21. package/src/backend/kilo/handler/message.ts +4 -1
  22. package/src/backend/openai-agents/constants.ts +1 -1
  23. package/src/backend/openai-agents/factory.ts +2 -2
  24. package/src/backend/openai-agents/handler/events.ts +1 -1
  25. package/src/backend/openai-agents/handler/message.ts +8 -4
  26. package/src/backend/openai-agents/init.ts +1 -1
  27. package/src/backend/openai-agents/mcp-pool.ts +2 -2
  28. package/src/backend/opencode/handler/message.ts +4 -1
  29. package/src/backend/remote-server/chat-turn.ts +31 -23
  30. package/src/backend/remote-server/events.ts +3 -3
  31. package/src/backend/remote-server/factory.ts +6 -3
  32. package/src/backend/remote-server/index.ts +1 -1
  33. package/src/backend/remote-server/mcp.ts +1 -1
  34. package/src/backend/remote-server/one-shot.ts +1 -1
  35. package/src/backend/remote-server/server-bindings.ts +1 -1
  36. package/src/backend/remote-server/turn.ts +1 -1
  37. package/src/backend/runtime/cache/cache-metrics.ts +126 -0
  38. package/src/backend/{shared → runtime/cache}/cache-telemetry.ts +22 -2
  39. package/src/backend/{shared → runtime}/index.ts +34 -25
  40. package/src/backend/{shared → runtime/prompt}/delivery-contract.ts +1 -1
  41. package/src/backend/{shared → runtime/prompt}/prompt-format.ts +46 -2
  42. package/src/backend/{shared → runtime/prompt}/system-prompt.ts +4 -4
  43. package/src/backend/{shared → runtime/turn}/delivered-text.ts +1 -1
  44. package/src/backend/{shared → runtime/turn}/delivery.ts +3 -3
  45. package/src/backend/{shared → runtime/turn}/handle-retry.ts +5 -5
  46. package/src/backend/{shared → runtime/turn}/handler-to-events.ts +24 -10
  47. package/src/backend/{shared → runtime/turn}/handler-types.ts +7 -1
  48. package/src/backend/{shared → runtime/turn}/model-retry.ts +2 -2
  49. package/src/backend/{shared → runtime/turn}/result-events.ts +2 -2
  50. package/src/backend/{shared → runtime/turn}/stream-state.ts +2 -2
  51. package/src/backend/{shared → runtime/turn}/turn-interrupt.ts +2 -2
  52. package/src/backend/{shared → runtime/turn}/turn-phases.ts +6 -6
  53. package/src/bootstrap.ts +6 -21
  54. package/src/cli.ts +6 -3
  55. package/src/core/agent-runtime/README.md +15 -0
  56. package/src/core/agent-runtime/agent-host.ts +387 -0
  57. package/src/core/agent-runtime/capabilities.ts +11 -0
  58. package/src/core/agent-runtime/contract-tests.ts +92 -1
  59. package/src/core/agent-runtime/events.ts +1 -1
  60. package/src/core/background/{isolated-agent.ts → cron/isolated-agent.ts} +3 -3
  61. package/src/core/background/{job-health.ts → cron/job-health.ts} +1 -1
  62. package/src/core/background/{job-oneshot.ts → cron/job-oneshot.ts} +5 -5
  63. package/src/core/background/{job-prompt.ts → cron/job-prompt.ts} +1 -1
  64. package/src/core/background/{cron.ts → cron/scheduler.ts} +6 -6
  65. package/src/core/background/{cron-spec.ts → cron/spec.ts} +1 -1
  66. package/src/core/background/{dream.ts → dream/index.ts} +10 -15
  67. package/src/core/background/{plan-alerts.ts → pulse/plan-alerts.ts} +3 -3
  68. package/src/core/background/{pulse.ts → pulse/pulse.ts} +8 -5
  69. package/src/core/background/triggers/command.ts +2 -2
  70. package/src/{util → core/config}/harden.ts +1 -1
  71. package/src/core/config/index.ts +16 -12
  72. package/src/{util → core/daemon}/boot-timer.ts +15 -1
  73. package/src/core/daemon/pidfile.ts +1 -1
  74. package/src/core/daemon/resource-sampler.ts +121 -0
  75. package/src/{util → core/daemon}/respawn.ts +1 -1
  76. package/src/core/doctor/index.ts +1 -1
  77. package/src/core/engine/dispatcher.ts +16 -0
  78. package/src/core/engine/gateway-actions/cron.ts +2 -2
  79. package/src/core/engine/gateway-actions/fetch-url.ts +1 -1
  80. package/src/core/engine/gateway-actions/index.ts +4 -1
  81. package/src/core/engine/gateway-actions/memory.ts +335 -0
  82. package/src/core/engine/gateway-actions/native/exec-background.ts +129 -0
  83. package/src/core/engine/gateway-actions/native/exec-remote.ts +85 -0
  84. package/src/core/engine/gateway-actions/native/exec.ts +182 -0
  85. package/src/core/engine/gateway-actions/native/index.ts +46 -0
  86. package/src/core/engine/gateway-actions/native/params.ts +46 -0
  87. package/src/core/engine/gateway-actions/native/read.ts +182 -0
  88. package/src/core/engine/gateway-actions/native/results.ts +34 -0
  89. package/src/core/engine/gateway-actions/native/search.ts +212 -0
  90. package/src/core/engine/gateway-actions/native/shell.ts +39 -0
  91. package/src/core/engine/gateway-actions/native/teleport.ts +55 -0
  92. package/src/core/engine/gateway-actions/native/write.ts +170 -0
  93. package/src/core/engine/gateway.ts +0 -6
  94. package/src/core/frontend-runtime/builtins.ts +2 -2
  95. package/src/{util → core/frontend-runtime}/chat-id.ts +30 -0
  96. package/src/core/mcp-hub/index.ts +1 -1
  97. package/src/core/mcp-hub/talon-server.ts +1 -1
  98. package/src/core/memory/import.ts +3 -2
  99. package/src/core/memory/taps.ts +199 -0
  100. package/src/core/memory/turn-retrieval.ts +222 -0
  101. package/src/core/plugin/mcp.ts +1 -1
  102. package/src/core/prompt/assemble.ts +2 -12
  103. package/src/core/prompt/index.ts +2 -2
  104. package/src/core/prompt/invalidation.ts +1 -1
  105. package/src/core/{scripting/lua-runner.ts → scripts/lua.ts} +1 -1
  106. package/src/core/tasks/index.ts +1 -1
  107. package/src/core/tools/{chat.ts → chat/chat.ts} +1 -1
  108. package/src/core/tools/{cross-send.ts → chat/cross-send.ts} +1 -1
  109. package/src/core/tools/{history.ts → chat/history.ts} +2 -2
  110. package/src/core/tools/{media.ts → chat/media.ts} +1 -1
  111. package/src/core/tools/{members.ts → chat/members.ts} +2 -2
  112. package/src/core/tools/{messaging.ts → chat/messaging.ts} +2 -2
  113. package/src/core/tools/{moderation.ts → chat/moderation.ts} +2 -2
  114. package/src/core/tools/{stickers.ts → chat/stickers.ts} +2 -2
  115. package/src/core/tools/{whatsapp.ts → chat/whatsapp.ts} +1 -1
  116. package/src/core/tools/content/memory.ts +128 -0
  117. package/src/core/tools/{web.ts → content/web.ts} +1 -1
  118. package/src/core/tools/index.ts +21 -19
  119. package/src/core/tools/{admin.ts → ops/admin.ts} +1 -1
  120. package/src/core/tools/{bridge.ts → ops/bridge.ts} +2 -2
  121. package/src/core/tools/{goals.ts → ops/goals.ts} +2 -2
  122. package/src/core/tools/{mesh.ts → ops/mesh.ts} +1 -1
  123. package/src/core/tools/{models.ts → ops/models.ts} +1 -1
  124. package/src/core/tools/{native.ts → ops/native.ts} +1 -1
  125. package/src/core/tools/{scheduling.ts → ops/scheduling.ts} +1 -1
  126. package/src/core/tools/{scripts.ts → ops/scripts.ts} +1 -1
  127. package/src/core/tools/{skills.ts → ops/skills.ts} +1 -1
  128. package/src/core/tools/{triggers.ts → ops/triggers.ts} +1 -1
  129. package/src/core/tools/types.ts +1 -0
  130. package/src/{util → core/vfs}/workspace.ts +2 -2
  131. package/src/core/weaver/turn-cpu.ts +32 -0
  132. package/src/core/weaver/weaver.ts +36 -0
  133. package/src/frontend/discord/admin.ts +1 -1
  134. package/src/frontend/discord/callbacks/components/backend-select.ts +1 -1
  135. package/src/frontend/discord/callbacks/components/effort.ts +1 -1
  136. package/src/frontend/discord/callbacks/components/index.ts +1 -1
  137. package/src/frontend/discord/callbacks/components/pulse.ts +1 -1
  138. package/src/frontend/discord/callbacks/components/settings.ts +2 -2
  139. package/src/frontend/discord/callbacks/modals.ts +4 -1
  140. package/src/frontend/discord/commands/admin.ts +2 -36
  141. package/src/frontend/discord/commands/definitions.ts +0 -11
  142. package/src/frontend/discord/commands/info.ts +1 -1
  143. package/src/frontend/discord/commands/interaction.ts +1 -1
  144. package/src/frontend/discord/commands/router.ts +0 -3
  145. package/src/frontend/discord/commands/session.ts +8 -2
  146. package/src/frontend/discord/commands/settings.ts +2 -2
  147. package/src/frontend/discord/handlers/messages.ts +1 -1
  148. package/src/frontend/discord/handlers/state.ts +1 -1
  149. package/src/frontend/discord/middleware.ts +2 -26
  150. package/src/frontend/discord/ready.ts +1 -1
  151. package/src/frontend/discord/render.ts +41 -259
  152. package/src/frontend/discord/runtime.ts +1 -2
  153. package/src/frontend/native/{auth.ts → bridge/auth.ts} +2 -2
  154. package/src/frontend/native/{discovery.ts → bridge/discovery.ts} +3 -3
  155. package/src/frontend/native/{routes → bridge/routes}/chats.ts +1 -1
  156. package/src/frontend/native/{routes → bridge/routes}/daemon.ts +1 -1
  157. package/src/frontend/native/{routes → bridge/routes}/host.ts +6 -3
  158. package/src/frontend/native/{routes → bridge/routes}/pre-auth.ts +1 -1
  159. package/src/frontend/native/{server.ts → bridge/server.ts} +3 -3
  160. package/src/frontend/native/{tls.ts → bridge/tls.ts} +2 -2
  161. package/src/frontend/native/{chat-lifecycle.ts → chats/chat-lifecycle.ts} +3 -3
  162. package/src/frontend/native/{chat-wire.ts → chats/chat-wire.ts} +4 -4
  163. package/src/frontend/native/{chats.ts → chats/chats.ts} +4 -4
  164. package/src/frontend/native/{empty-chat-sweep.ts → chats/empty-chat-sweep.ts} +4 -4
  165. package/src/frontend/native/{history.ts → chats/history.ts} +7 -7
  166. package/src/frontend/native/{reset.ts → chats/reset.ts} +7 -7
  167. package/src/frontend/native/index.ts +13 -10
  168. package/src/frontend/native/{media.ts → media/media.ts} +3 -3
  169. package/src/frontend/native/runtime.ts +1 -1
  170. package/src/frontend/native/{control.ts → surface/control.ts} +4 -4
  171. package/src/frontend/native/{extensions.ts → surface/extensions.ts} +12 -9
  172. package/src/frontend/native/{handlers.ts → surface/handlers.ts} +19 -14
  173. package/src/frontend/native/{logs.ts → surface/logs.ts} +2 -2
  174. package/src/frontend/native/{memory.ts → surface/memory.ts} +2 -2
  175. package/src/frontend/native/{models.ts → surface/models.ts} +17 -10
  176. package/src/frontend/native/{settings.ts → surface/settings.ts} +9 -9
  177. package/src/frontend/native/{status.ts → surface/status.ts} +3 -3
  178. package/src/frontend/native/{actions.ts → turn/actions.ts} +7 -4
  179. package/src/frontend/native/{context.ts → turn/context.ts} +8 -8
  180. package/src/frontend/native/{emit.ts → turn/emit.ts} +7 -7
  181. package/src/frontend/native/{queue.ts → turn/queue.ts} +3 -3
  182. package/src/frontend/native/{turn-meta.ts → turn/turn-meta.ts} +2 -2
  183. package/src/frontend/native/{turn.ts → turn/turn.ts} +9 -9
  184. package/src/frontend/{shared → presentation}/format.ts +4 -3
  185. package/src/frontend/{shared → presentation}/model-commands.ts +1 -1
  186. package/src/frontend/{shared → presentation}/reasoning-levels.ts +12 -0
  187. package/src/frontend/presentation/reports.ts +493 -0
  188. package/src/frontend/{shared → presentation}/session-status.ts +36 -9
  189. package/src/frontend/{shared → presentation}/status-context.ts +101 -2
  190. package/src/frontend/teams/commands.ts +2 -2
  191. package/src/frontend/teams/turn.ts +1 -1
  192. package/src/frontend/telegram/admin/background.ts +1 -1
  193. package/src/frontend/telegram/admin/health.ts +1 -1
  194. package/src/frontend/telegram/admin/sessions.ts +1 -1
  195. package/src/frontend/telegram/callbacks/effort.ts +2 -2
  196. package/src/frontend/telegram/callbacks/metrics.ts +1 -1
  197. package/src/frontend/telegram/callbacks/model/backend.ts +1 -1
  198. package/src/frontend/telegram/callbacks/model/views.ts +1 -1
  199. package/src/frontend/telegram/callbacks/pulse.ts +1 -1
  200. package/src/frontend/telegram/callbacks/query.ts +1 -1
  201. package/src/frontend/telegram/callbacks/settings.ts +3 -3
  202. package/src/frontend/telegram/commands/admin.ts +6 -41
  203. package/src/frontend/telegram/commands/index.ts +1 -1
  204. package/src/frontend/telegram/commands/info.ts +2 -2
  205. package/src/frontend/telegram/commands/session.ts +9 -3
  206. package/src/frontend/telegram/commands/settings.ts +5 -3
  207. package/src/frontend/telegram/handlers/messages.ts +0 -10
  208. package/src/frontend/telegram/handlers/state.ts +1 -1
  209. package/src/frontend/telegram/index.ts +1 -5
  210. package/src/frontend/telegram/middleware.ts +1 -15
  211. package/src/frontend/telegram/model-menu.ts +1 -1
  212. package/src/frontend/telegram/render/html.ts +30 -0
  213. package/src/frontend/telegram/{helpers → render}/menu.ts +46 -19
  214. package/src/frontend/telegram/render/reports.ts +182 -0
  215. package/src/frontend/terminal/builtins/context.ts +2 -2
  216. package/src/frontend/terminal/builtins/session.ts +1 -1
  217. package/src/frontend/terminal/builtins/status.ts +2 -2
  218. package/src/frontend/terminal/index.ts +2 -2
  219. package/src/frontend/terminal/renderer.ts +1 -1
  220. package/src/frontend/whatsapp/access.ts +2 -2
  221. package/src/frontend/whatsapp/actions/history.ts +2 -2
  222. package/src/frontend/whatsapp/actions/messaging.ts +2 -2
  223. package/src/frontend/whatsapp/actions/moderation.ts +1 -1
  224. package/src/frontend/whatsapp/actions/send.ts +1 -1
  225. package/src/frontend/whatsapp/commands.ts +12 -6
  226. package/src/frontend/whatsapp/{connection.ts → connection/connection.ts} +5 -5
  227. package/src/frontend/whatsapp/{pairing-service.ts → connection/pairing-service.ts} +3 -3
  228. package/src/frontend/whatsapp/{wa-logger.ts → connection/wa-logger.ts} +2 -2
  229. package/src/frontend/whatsapp/index.ts +4 -4
  230. package/src/frontend/whatsapp/{inbound.ts → messages/inbound.ts} +15 -15
  231. package/src/frontend/whatsapp/{media-store.ts → messages/media-store.ts} +4 -4
  232. package/src/frontend/whatsapp/{message-store.ts → messages/message-store.ts} +1 -1
  233. package/src/frontend/whatsapp/{turn-recovery.ts → messages/turn-recovery.ts} +4 -4
  234. package/src/frontend/whatsapp/registry.ts +2 -2
  235. package/src/frontend/whatsapp/runtime.ts +1 -1
  236. package/src/index.ts +6 -3
  237. package/src/storage/db.ts +6 -1
  238. package/src/storage/memory.ts +59 -8
  239. package/src/storage/metrics.ts +38 -0
  240. package/src/storage/repositories/sessions-repo.ts +6 -0
  241. package/src/storage/session-record.ts +13 -2
  242. package/src/storage/sessions.ts +12 -0
  243. package/src/storage/sql/db.sql +5 -0
  244. package/src/storage/sql/schema.sql +4 -0
  245. package/src/storage/sql/sessions.sql +3 -3
  246. package/src/storage/sql/statements.generated.ts +10 -3
  247. package/src/storage/sql/turn-meta.sql +1 -1
  248. package/src/util/log.ts +1 -1
  249. package/src/util/paths.ts +0 -2
  250. package/src/util/runtime.ts +1 -1
  251. package/src/core/engine/gateway-actions/native.ts +0 -1035
  252. package/src/core/soul/README.md +0 -110
  253. package/src/core/soul/RESEARCH.md +0 -98
  254. package/src/core/soul/associative.ts +0 -98
  255. package/src/core/soul/centrality.ts +0 -98
  256. package/src/core/soul/cluster.ts +0 -83
  257. package/src/core/soul/compiler.ts +0 -207
  258. package/src/core/soul/consolidate.ts +0 -179
  259. package/src/core/soul/critic.ts +0 -162
  260. package/src/core/soul/dag.ts +0 -265
  261. package/src/core/soul/delta.ts +0 -123
  262. package/src/core/soul/drift.ts +0 -99
  263. package/src/core/soul/embedder.ts +0 -129
  264. package/src/core/soul/emergent-critic.ts +0 -96
  265. package/src/core/soul/forgetting.ts +0 -131
  266. package/src/core/soul/governance.ts +0 -93
  267. package/src/core/soul/hash.ts +0 -97
  268. package/src/core/soul/hdc.ts +0 -154
  269. package/src/core/soul/kernel.ts +0 -540
  270. package/src/core/soul/lattice.ts +0 -103
  271. package/src/core/soul/lens.ts +0 -110
  272. package/src/core/soul/projector.ts +0 -240
  273. package/src/core/soul/reflect.ts +0 -170
  274. package/src/core/soul/reflex.ts +0 -164
  275. package/src/core/soul/retrieve.ts +0 -146
  276. package/src/core/soul/salience.ts +0 -146
  277. package/src/core/soul/service.ts +0 -204
  278. package/src/core/soul/settings.ts +0 -47
  279. package/src/core/soul/signals.ts +0 -117
  280. package/src/core/soul/talon-embedder.ts +0 -80
  281. package/src/core/soul/taps.ts +0 -199
  282. package/src/core/soul/types.ts +0 -298
  283. package/src/core/soul/valence.ts +0 -83
  284. package/src/frontend/telegram/helpers/diagnostics.ts +0 -428
  285. package/src/frontend/telegram/helpers/format.ts +0 -42
  286. package/src/frontend/telegram/helpers/index.ts +0 -13
  287. package/src/util/cleanup-registry.ts +0 -36
  288. /package/src/backend/{shared → runtime}/frontends.ts +0 -0
  289. /package/src/backend/{shared → runtime}/metrics.ts +0 -0
  290. /package/src/backend/{shared → runtime}/sleep.ts +0 -0
  291. /package/src/backend/{shared → runtime/turn}/flow-violation.ts +0 -0
  292. /package/src/backend/{shared → runtime}/usage.ts +0 -0
  293. /package/src/{util/mcp-launcher.ts → core/mcp-hub/launcher.ts} +0 -0
  294. /package/src/{util → core/tools/content}/web-content.ts +0 -0
  295. /package/src/core/tools/{mcp-env.ts → ops/mcp-env.ts} +0 -0
  296. /package/src/{util → core/weaver}/session-name.ts +0 -0
  297. /package/src/frontend/native/{routes → bridge/routes}/index.ts +0 -0
  298. /package/src/frontend/native/{routes → bridge/routes}/memory.ts +0 -0
  299. /package/src/frontend/native/{routes → bridge/routes}/mesh.ts +0 -0
  300. /package/src/frontend/native/{routes → bridge/routes}/models.ts +0 -0
  301. /package/src/frontend/native/{routes → bridge/routes}/params.ts +0 -0
  302. /package/src/frontend/native/{routes → bridge/routes}/table.ts +0 -0
  303. /package/src/frontend/native/{tool-result.ts → turn/tool-result.ts} +0 -0
  304. /package/src/frontend/{shared → presentation}/access.ts +0 -0
  305. /package/src/frontend/{shared → presentation}/plan-usage-report.ts +0 -0
  306. /package/src/frontend/whatsapp/{auth-state.ts → connection/auth-state.ts} +0 -0
  307. /package/src/frontend/whatsapp/{identity.ts → connection/identity.ts} +0 -0
  308. /package/src/frontend/whatsapp/{pairing-lock.ts → connection/pairing-lock.ts} +0 -0
  309. /package/src/frontend/whatsapp/{pairing.ts → connection/pairing.ts} +0 -0
  310. /package/src/frontend/whatsapp/{pins.ts → messages/pins.ts} +0 -0
@@ -7,11 +7,14 @@
7
7
  * `codex.resumeThread(id)`, the rollout-JSONL live/settle usage accounting
8
8
  * (`rollout-accounting.ts`), and the ChatGPT-OAuth model-mismatch recovery
9
9
  * ladder. The post-stream phases are the shared ones in
10
- * `backend/shared/turn-phases.ts`.
10
+ * `backend/runtime/turn/turn-phases.ts`.
11
11
  */
12
12
 
13
13
  import type { Thread, Usage } from "@openai/codex-sdk";
14
- import type { QueryParams, QueryResult } from "../../shared/handler-types.js";
14
+ import type {
15
+ QueryParams,
16
+ QueryResult,
17
+ } from "../../runtime/turn/handler-types.js";
15
18
  import {
16
19
  getSession,
17
20
  incrementTurns,
@@ -37,7 +40,7 @@ import {
37
40
  nameSessionFromFirstMessage,
38
41
  finishCallbackTurn,
39
42
  type StreamState,
40
- } from "../../shared/index.js";
43
+ } from "../../runtime/index.js";
41
44
 
42
45
  import {
43
46
  codexSystemPromptSuffix,
@@ -48,7 +51,7 @@ import {
48
51
  import {
49
52
  frontendsForChat,
50
53
  nonTerminalFrontends,
51
- } from "../../shared/frontends.js";
54
+ } from "../../runtime/frontends.js";
52
55
  import { getState } from "../state.js";
53
56
  import { ensureCodex, getCodexAuthInfo } from "../init.js";
54
57
  import {
@@ -213,6 +216,23 @@ async function maybeFallbackForChatGptMismatch(
213
216
  * ChatGPT-OAuth account is swapped pre-emptively rather than letting the
214
217
  * first turn fail.
215
218
  */
219
+ /**
220
+ * The turn's user prompt: the shared framing every backend emits, plus
221
+ * whatever this turn's memory retrieval produced. `formatUserPrompt` is
222
+ * the one place `retrievedMemory` is rendered — see
223
+ * `backend/runtime/prompt/prompt-format.ts`.
224
+ */
225
+ function buildTurnPrompt(params: QueryParams): string {
226
+ return formatUserPrompt({
227
+ text: params.text,
228
+ senderName: params.senderName ?? "user",
229
+ senderHandle: params.senderHandle,
230
+ isGroup: params.isGroup,
231
+ messageId: params.messageId,
232
+ retrievedMemory: params.retrievedMemory,
233
+ });
234
+ }
235
+
216
236
  function resolveCodexModel(chatId: string, requested: string | undefined) {
217
237
  const authInfo = getCodexAuthInfo();
218
238
  const authAwareDefault =
@@ -409,7 +429,7 @@ export async function handleMessage(
409
429
  }
410
430
  const codex = ensureCodex(params.chatId);
411
431
 
412
- const { chatId, text, senderName, senderHandle, isGroup, messageId } = params;
432
+ const { chatId, text, senderName, isGroup } = params;
413
433
  const t0 = Date.now();
414
434
  const session = getSession(chatId);
415
435
  const previousTurns = session.turns;
@@ -432,13 +452,7 @@ export async function handleMessage(
432
452
  sessionEpoch: session.createdAt,
433
453
  });
434
454
 
435
- const prompt = formatUserPrompt({
436
- text,
437
- senderName: senderName ?? "user",
438
- senderHandle,
439
- isGroup,
440
- messageId,
441
- });
455
+ const prompt = buildTurnPrompt(params);
442
456
 
443
457
  log("agent", `[${chatId}] <- (${text.length} chars)`);
444
458
  traceMessage(chatId, "in", text, { senderName, isGroup });
@@ -19,7 +19,7 @@ import {
19
19
  pushLiveUsage,
20
20
  recordTokens,
21
21
  type StreamState,
22
- } from "../../shared/index.js";
22
+ } from "../../runtime/index.js";
23
23
  import { CODEX_LIVE_POLL_INTERVAL_MS } from "../constants.js";
24
24
  import {
25
25
  readLastRolloutSnapshot,
@@ -25,7 +25,7 @@ import { Codex } from "@openai/codex-sdk";
25
25
  import type { TalonConfig } from "../../core/config/index.js";
26
26
  import type { FrontendName } from "../../core/agent-runtime/backend-registry.js";
27
27
  import { log, logWarn } from "../../util/log.js";
28
- import { nonTerminalFrontends } from "../shared/frontends.js";
28
+ import { nonTerminalFrontends } from "../runtime/frontends.js";
29
29
  import { getState } from "./state.js";
30
30
  import { asCodexConfig, buildCodexMcpServers } from "./mcp-config.js";
31
31
  import { detectCodexAuth, type CodexAuthInfo } from "./auth.js";
@@ -29,8 +29,8 @@ import {
29
29
  pluginHubUrl,
30
30
  hubPluginServerNames,
31
31
  } from "../../core/mcp-hub/index.js";
32
- import type { ToolExclusionConfig } from "../../core/tools/mcp-env.js";
33
- import { frontendsForChat } from "../shared/frontends.js";
32
+ import type { ToolExclusionConfig } from "../../core/tools/ops/mcp-env.js";
33
+ import { frontendsForChat } from "../runtime/frontends.js";
34
34
 
35
35
  /**
36
36
  * AppToolApproval values accepted by Codex's `mcp_servers.<name>` table.
@@ -19,7 +19,7 @@
19
19
 
20
20
  import type { OneShotAgentParams, OneShotUsage } from "../../core/types.js";
21
21
  import { log, logWarn } from "../../util/log.js";
22
- import { appendBackendSuffix } from "../shared/index.js";
22
+ import { appendBackendSuffix } from "../runtime/index.js";
23
23
  import { ensureCodex, getCodexAuthInfo } from "./init.js";
24
24
  import {
25
25
  CODEX_SYSTEM_PROMPT_SUFFIX,
@@ -3,7 +3,10 @@
3
3
  * Kilo's server bindings.
4
4
  */
5
5
 
6
- import type { QueryParams, QueryResult } from "../../shared/handler-types.js";
6
+ import type {
7
+ QueryParams,
8
+ QueryResult,
9
+ } from "../../runtime/turn/handler-types.js";
7
10
  import { runRemoteChatTurn } from "../../remote-server/chat-turn.js";
8
11
  import {
9
12
  ensureServer,
@@ -10,7 +10,7 @@
10
10
  * orchestration) with MCP servers wired from the plugin system.
11
11
  */
12
12
 
13
- import { buildDeliveryContract } from "../shared/delivery-contract.js";
13
+ import { buildDeliveryContract } from "../runtime/prompt/delivery-contract.js";
14
14
 
15
15
  /**
16
16
  * Agents-specific addendum to the shared tool-only delivery contract:
@@ -13,8 +13,8 @@ import { registerBackend } from "../../core/agent-runtime/backend-registry.js";
13
13
  import { openAIAgentsDoctorChecks } from "./doctor.js";
14
14
  import type { BackendFactory } from "../../core/agent-runtime/backend-registry.js";
15
15
  import { log } from "../../util/log.js";
16
- import { handlerToEvents } from "../shared/handler-to-events.js";
17
- import { interruptChatTurn } from "../shared/turn-interrupt.js";
16
+ import { handlerToEvents } from "../runtime/turn/handler-to-events.js";
17
+ import { interruptChatTurn } from "../runtime/turn/turn-interrupt.js";
18
18
  import {
19
19
  composeBackend,
20
20
  type ChatBackend,
@@ -9,7 +9,7 @@ import {
9
9
  createStreamState,
10
10
  recordToolUse,
11
11
  recordToolCall,
12
- } from "../../shared/index.js";
12
+ } from "../../runtime/index.js";
13
13
 
14
14
  export interface HandleRunItemContext {
15
15
  state: ReturnType<typeof createStreamState>;
@@ -6,11 +6,14 @@
6
6
  * MCP bundle, iterating the `StreamedRunResult` (see `events.ts`), and
7
7
  * `.cancel()` on terminator. The post-stream phases — accounting, the
8
8
  * trailing-prose flow-violation retry, the result — are the shared ones in
9
- * `backend/shared/turn-phases.ts`.
9
+ * `backend/runtime/turn/turn-phases.ts`.
10
10
  */
11
11
 
12
12
  import { Agent, run } from "@openai/agents";
13
- import type { QueryParams, QueryResult } from "../../shared/handler-types.js";
13
+ import type {
14
+ QueryParams,
15
+ QueryResult,
16
+ } from "../../runtime/turn/handler-types.js";
14
17
  import { getSession, incrementTurns } from "../../../storage/sessions.js";
15
18
  import { getChatSettings } from "../../../storage/chat-settings.js";
16
19
  import { log, logError, logWarn } from "../../../util/log.js";
@@ -33,7 +36,7 @@ import {
33
36
  enforceTrailingProse,
34
37
  finishCallbackTurn,
35
38
  type StreamState,
36
- } from "../../shared/index.js";
39
+ } from "../../runtime/index.js";
37
40
 
38
41
  import {
39
42
  buildOpenAiAgentsSuffix,
@@ -43,7 +46,7 @@ import {
43
46
  } from "../constants.js";
44
47
  import { getState, getOrCreateSession } from "../state.js";
45
48
  import { getActiveFrontends } from "../init.js";
46
- import { frontendsForChat } from "../../shared/frontends.js";
49
+ import { frontendsForChat } from "../../runtime/frontends.js";
47
50
  import { getOrCreateBundle } from "../mcp-pool.js";
48
51
  import { OPENAI_AGENTS_BUILTIN_TOOLS } from "../builtins.js";
49
52
  import { activeAborts } from "./state.js";
@@ -105,6 +108,7 @@ function buildTurnPrompt(
105
108
  senderHandle: params.senderHandle,
106
109
  isGroup: params.isGroup,
107
110
  messageId: params.messageId,
111
+ retrievedMemory: params.retrievedMemory,
108
112
  });
109
113
  if (frontend && previousTurns === 0 && !isRetry) {
110
114
  prompt += `\n\n${buildFirstTurnReminder(frontend)}`;
@@ -46,7 +46,7 @@ import { setDefaultOpenAIClient, setOpenAIAPI } from "@openai/agents";
46
46
  import type { TalonConfig } from "../../core/config/index.js";
47
47
  import type { FrontendName } from "../../core/agent-runtime/backend-registry.js";
48
48
  import { log, logWarn } from "../../util/log.js";
49
- import { nonTerminalFrontends } from "../shared/frontends.js";
49
+ import { nonTerminalFrontends } from "../runtime/frontends.js";
50
50
  import { getState } from "./state.js";
51
51
  import { startDiscovery } from "./discovery.js";
52
52
 
@@ -30,8 +30,8 @@ import {
30
30
  pluginHubUrl,
31
31
  hubPluginServerNames,
32
32
  } from "../../core/mcp-hub/index.js";
33
- import type { ToolExclusionConfig } from "../../core/tools/mcp-env.js";
34
- import { frontendsForChat } from "../shared/frontends.js";
33
+ import type { ToolExclusionConfig } from "../../core/tools/ops/mcp-env.js";
34
+ import { frontendsForChat } from "../runtime/frontends.js";
35
35
  import { log, logWarn } from "../../util/log.js";
36
36
 
37
37
  /**
@@ -3,7 +3,10 @@
3
3
  * bound to OpenCode's server bindings.
4
4
  */
5
5
 
6
- import type { QueryParams, QueryResult } from "../../shared/handler-types.js";
6
+ import type {
7
+ QueryParams,
8
+ QueryResult,
9
+ } from "../../runtime/turn/handler-types.js";
7
10
  import { runRemoteChatTurn } from "../../remote-server/chat-turn.js";
8
11
  import {
9
12
  ensureServer,
@@ -5,7 +5,7 @@
5
5
  * Resolves the active model against the server's catalog, makes sure the
6
6
  * server, session, and this chat's MCP servers exist, builds the prompt
7
7
  * pair, drives the turn (`./turn.ts`), then runs the shared post-turn
8
- * phases (`backend/shared/turn-phases.ts`) with the one family-specific
8
+ * phases (`backend/runtime/turn/turn-phases.ts`) with the one family-specific
9
9
  * step in between: the usage fallback from the session summary.
10
10
  *
11
11
  * Kilo and OpenCode ran byte-for-byte copies of this (modulo the backend
@@ -18,8 +18,14 @@ import { getSession, incrementTurns } from "../../storage/sessions.js";
18
18
  import { getChatSettings } from "../../storage/chat-settings.js";
19
19
  import { log, logError } from "../../util/log.js";
20
20
  import { traceMessage } from "../../util/trace.js";
21
- import type { QueryParams, QueryResult } from "../shared/handler-types.js";
22
- import { frontendsForChat, nonTerminalFrontends } from "../shared/frontends.js";
21
+ import type {
22
+ QueryParams,
23
+ QueryResult,
24
+ } from "../runtime/turn/handler-types.js";
25
+ import {
26
+ frontendsForChat,
27
+ nonTerminalFrontends,
28
+ } from "../runtime/frontends.js";
23
29
  import {
24
30
  createStreamState,
25
31
  recordTokens,
@@ -33,7 +39,7 @@ import {
33
39
  accountFailedTurn,
34
40
  nameSessionFromFirstMessage,
35
41
  finishCallbackTurn,
36
- } from "../shared/index.js";
42
+ } from "../runtime/index.js";
37
43
  import type { RemoteAgentClient } from "./client.js";
38
44
  import type { RemoteServerBindings } from "./server-bindings.js";
39
45
  import { getTurnSummary, type RemoteSessionClient } from "./session-helpers.js";
@@ -68,16 +74,7 @@ export async function runRemoteChatTurn<TClient extends RemoteAgentClient>(
68
74
  const config = bindings.getConfig();
69
75
  if (!config) throw new Error(`${label} agent not initialized`);
70
76
 
71
- const {
72
- chatId,
73
- text,
74
- senderName,
75
- senderHandle,
76
- isGroup,
77
- messageId,
78
- onTextBlock,
79
- onToolUse,
80
- } = params;
77
+ const { chatId, text, senderName, onTextBlock, onToolUse } = params;
81
78
  const t0 = Date.now();
82
79
  const session = getSession(chatId);
83
80
  const previousTurns = session.turns;
@@ -108,14 +105,8 @@ export async function runRemoteChatTurn<TClient extends RemoteAgentClient>(
108
105
  pluginMcpServerNames,
109
106
  );
110
107
 
111
- // Build the prompt (time tag + sender + msg_id reference).
112
- const prompt = formatUserPrompt({
113
- text,
114
- senderName: senderName ?? "user",
115
- senderHandle,
116
- isGroup,
117
- messageId,
118
- });
108
+ // Build the prompt (time tag + sender + msg_id + recalled memory).
109
+ const prompt = buildTurnPrompt(params);
119
110
 
120
111
  // Per-session frozen prompt + this backend's delivery suffix.
121
112
  const { text: systemPrompt } = prepareSystemPrompt({
@@ -130,7 +121,7 @@ export async function runRemoteChatTurn<TClient extends RemoteAgentClient>(
130
121
  });
131
122
 
132
123
  log("agent", `[${chatId}] <- (${text.length} chars)`);
133
- traceMessage(chatId, "in", text, { senderName, isGroup });
124
+ traceMessage(chatId, "in", text, { senderName, isGroup: params.isGroup });
134
125
 
135
126
  // Bind the stream state to the chat so token mutators mirror counts
136
127
  // into the live-turn overlay — /status updates while the turn runs.
@@ -268,6 +259,23 @@ export async function runRemoteChatTurn<TClient extends RemoteAgentClient>(
268
259
  * session summaries can race on cancellation, so a failure leaves the
269
260
  * counts at zero.
270
261
  */
262
+ /**
263
+ * The turn's user prompt: the shared framing every backend emits, plus
264
+ * whatever this turn's memory retrieval produced. `formatUserPrompt` is
265
+ * the one place `retrievedMemory` is rendered — see
266
+ * `backend/runtime/prompt/prompt-format.ts`.
267
+ */
268
+ function buildTurnPrompt(params: QueryParams): string {
269
+ return formatUserPrompt({
270
+ text: params.text,
271
+ senderName: params.senderName ?? "user",
272
+ senderHandle: params.senderHandle,
273
+ isGroup: params.isGroup,
274
+ messageId: params.messageId,
275
+ retrievedMemory: params.retrievedMemory,
276
+ });
277
+ }
278
+
271
279
  async function fillUsageFromSummary(
272
280
  oc: RemoteSessionClient,
273
281
  sessionId: string,
@@ -33,7 +33,7 @@ import {
33
33
  recordToolUse,
34
34
  recordToolCall,
35
35
  type StreamState,
36
- } from "../shared/index.js";
36
+ } from "../runtime/index.js";
37
37
  import {
38
38
  extractAssistantUsage,
39
39
  type RemoteAssistantInfo,
@@ -68,7 +68,7 @@ export interface EventProcessingContext {
68
68
  chatId: string;
69
69
  /** Session id we're scoped to — events for other sessions are dropped. */
70
70
  sessionId: string;
71
- /** Stream state accumulator (shared/). */
71
+ /** Stream state accumulator (runtime/turn/). */
72
72
  state: StreamState;
73
73
  /** Tool-call ids we've already fired callbacks for. */
74
74
  seenToolCallIds: Set<string>;
@@ -164,7 +164,7 @@ export async function processStreamEvent(
164
164
  * lands so token/context stats are live mid-turn instead of appearing
165
165
  * only at the post-loop accounting. `recordTokens` also mirrors the
166
166
  * counts into the chat's live-turn overlay when the state is bound to a
167
- * chat (see `shared/stream-state.ts: pushLiveUsage`).
167
+ * chat (see `runtime/turn/stream-state.ts: pushLiveUsage`).
168
168
  *
169
169
  * Values are the message's totals (not deltas), matching what the
170
170
  * post-loop `extractAssistantUsage(lastAssistant.info)` reads — so the
@@ -28,9 +28,12 @@ import type { OneShotAgentParams, OneShotUsage } from "../../core/types.js";
28
28
  import type { TalonConfig } from "../../core/config/index.js";
29
29
  import { binaryOnPath } from "../../util/binary-on-path.js";
30
30
  import { log } from "../../util/log.js";
31
- import { handlerToEvents } from "../shared/handler-to-events.js";
32
- import type { QueryParams, QueryResult } from "../shared/handler-types.js";
33
- import { interruptChatTurn } from "../shared/turn-interrupt.js";
31
+ import { handlerToEvents } from "../runtime/turn/handler-to-events.js";
32
+ import type {
33
+ QueryParams,
34
+ QueryResult,
35
+ } from "../runtime/turn/handler-types.js";
36
+ import { interruptChatTurn } from "../runtime/turn/turn-interrupt.js";
34
37
  import type { RemoteModelProvider } from "./model-catalog/provider.js";
35
38
  import type { RemoteSessionSnapshot } from "./session-helpers.js";
36
39
 
@@ -28,7 +28,7 @@
28
28
  * What's NOT here (intentionally):
29
29
  *
30
30
  * - Tool definitions, frontend prompt format — those are backend-
31
- * agnostic and live in `core/` and `backend/shared/`.
31
+ * agnostic and live in `core/` and `backend/runtime/`.
32
32
  *
33
33
  * This barrel exposes the helper layer for tests and the conformance
34
34
  * suite; the bindings modules import from the concrete files directly.
@@ -50,7 +50,7 @@ import { ALL_TOOLS, nativeTools } from "../../core/tools/index.js";
50
50
  import {
51
51
  frontendForChatId,
52
52
  nonTerminalFrontends,
53
- } from "../shared/frontends.js";
53
+ } from "../runtime/frontends.js";
54
54
 
55
55
  // ── Constants ───────────────────────────────────────────────────────────────
56
56
 
@@ -41,7 +41,7 @@ import {
41
41
  type RemoteSessionClient,
42
42
  type RemoteAssistantInfo,
43
43
  } from "./session-helpers.js";
44
- import { appendBackendSuffix, sleep } from "../shared/index.js";
44
+ import { appendBackendSuffix, sleep } from "../runtime/index.js";
45
45
  import { buildPermissionRuleset } from "./sessions.js";
46
46
 
47
47
  // ── Client surface ──────────────────────────────────────────────────────────
@@ -25,7 +25,7 @@ import type { FrontendName } from "../../core/agent-runtime/backend-registry.js"
25
25
  import {
26
26
  buildDeliveryContract,
27
27
  type DeliveryMode,
28
- } from "../shared/delivery-contract.js";
28
+ } from "../runtime/prompt/delivery-contract.js";
29
29
  import type { RemoteAgentClient } from "./client.js";
30
30
  import { createRemoteServerState, errMsg } from "./state.js";
31
31
  import { ensureRemoteServer, stopRemoteServer } from "./lifecycle.js";
@@ -13,7 +13,7 @@
13
13
  */
14
14
 
15
15
  import { logWarn } from "../../util/log.js";
16
- import { createStreamState, recordTokens, sleep } from "../shared/index.js";
16
+ import { createStreamState, recordTokens, sleep } from "../runtime/index.js";
17
17
  import { processStreamEvent, finalizePartsIntoState } from "./events.js";
18
18
  import { findLastAssistantMessage } from "./messages.js";
19
19
  import {
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Prompt-cache rollups — the per-turn verdict, the tool fingerprint and the
3
+ * SDK's compaction boundaries, folded into counters an operator can read.
4
+ *
5
+ * `cache-telemetry.ts` next door decides WHAT a turn did (verdict,
6
+ * fingerprint, lookback risk) and is pure apart from its own per-chat maps.
7
+ * This module is the side-effecting half: it writes to the metrics store and
8
+ * logs, so the telemetry stays testable without a store.
9
+ *
10
+ * Why these numbers exist (docs/cache-economics.md, PR A): every decision in
11
+ * PR B ("one prefix per frontend") and PR C ("compact when the cache has gone
12
+ * cold") needs a before/after figure, and nothing here changes a prompt byte
13
+ * or a turn's behaviour — measurement only.
14
+ *
15
+ * - `cache.first_request.{hit,miss,none}` — did the previous turn's prefix
16
+ * survive? The turn's first request is the only one that can say, and it
17
+ * is the number that tracks cost.
18
+ * - `cache.first_request.{read,write}_tokens` — how big the prefix is, so a
19
+ * miss can be priced.
20
+ * - `cache.session_start.{hit,miss,none}` — the same verdict restricted to a
21
+ * chat's FIRST turn. A hit there means another chat had already warmed an
22
+ * identical prefix: the cross-chat sharing signal PR B is aiming at.
23
+ * - `cache.tool_fingerprint.changed` — tools render before the system
24
+ * prompt, so a change invalidates everything after it.
25
+ * - `session.compacted.{manual,auto}` + `session.compact.{pre,post}_tokens`
26
+ * — how often the SDK compacts and how much it reclaims, the baseline
27
+ * PR C's idle-compaction policy is measured against.
28
+ */
29
+
30
+ import { log } from "../../../util/log.js";
31
+ import {
32
+ incrementCounter,
33
+ noteCacheVerdict,
34
+ recordHistogram,
35
+ } from "../../../storage/metrics.js";
36
+ import {
37
+ crossTurnVerdict,
38
+ fingerprintHash,
39
+ hasToolFingerprint,
40
+ noteToolFingerprint,
41
+ type CrossTurnVerdict,
42
+ type TurnCacheStats,
43
+ } from "./cache-telemetry.js";
44
+
45
+ /**
46
+ * Roll one finished turn's cache behaviour up into the metrics store and
47
+ * remember the verdict for `/status`.
48
+ *
49
+ * `turnsIncludingThis` is the chat's turn count with this turn already
50
+ * counted, so 1 means the turn was the session's first — the only turn whose
51
+ * verdict says anything about cross-chat prefix sharing.
52
+ */
53
+ export function rollUpTurnCache(
54
+ chatId: string,
55
+ stats: TurnCacheStats,
56
+ turnsIncludingThis: number,
57
+ ): CrossTurnVerdict {
58
+ const verdict = crossTurnVerdict(stats);
59
+ incrementCounter(`cache.first_request.${verdict}`);
60
+ // Recorded on every turn, zeros included: the average is then "tokens per
61
+ // turn", which is what a miss costs, rather than an average over the
62
+ // turns that happened to be interesting.
63
+ recordHistogram("cache.first_request.read_tokens", stats.firstRead);
64
+ recordHistogram("cache.first_request.write_tokens", stats.firstWrite);
65
+ if (turnsIncludingThis <= 1) {
66
+ incrementCounter(`cache.session_start.${verdict}`);
67
+ }
68
+ noteCacheVerdict(chatId, verdict);
69
+ return verdict;
70
+ }
71
+
72
+ /**
73
+ * Record a chat's tool set: one info line the first time a chat is seen (so
74
+ * two chats on the same frontend can be compared by eye — identical hash +
75
+ * identical static prompt ⇒ shared cache), and a counter whenever it changes
76
+ * mid-process. `noteToolFingerprint` still owns the warning it already logs
77
+ * for a mid-session change.
78
+ */
79
+ export function reportToolFingerprint(
80
+ chatId: string,
81
+ fingerprint: readonly string[],
82
+ ): void {
83
+ const seen = hasToolFingerprint(chatId);
84
+ const changed = noteToolFingerprint(chatId, fingerprint);
85
+ if (!seen) {
86
+ log(
87
+ "agent",
88
+ `[${chatId}] tool fingerprint ${fingerprintHash(fingerprint)} ` +
89
+ `(${fingerprint.length} tools)`,
90
+ );
91
+ }
92
+ if (changed) incrementCounter("cache.tool_fingerprint.changed");
93
+ }
94
+
95
+ /** The SDK's compaction metadata, narrowed to the fields that are measured. */
96
+ export type CompactBoundary = {
97
+ trigger: "manual" | "auto";
98
+ pre_tokens: number;
99
+ post_tokens?: number;
100
+ };
101
+
102
+ /**
103
+ * Record a compaction boundary the SDK reported mid-stream. Purely
104
+ * observational — the turn carries on exactly as it did before, this just
105
+ * stops the event being dropped on the floor.
106
+ */
107
+ export function recordCompactBoundary(
108
+ chatId: string,
109
+ meta: CompactBoundary,
110
+ ): void {
111
+ const trigger = meta.trigger === "manual" ? "manual" : "auto";
112
+ incrementCounter(`session.compacted.${trigger}`);
113
+ const pre = Number.isFinite(meta.pre_tokens) ? meta.pre_tokens : 0;
114
+ const post =
115
+ typeof meta.post_tokens === "number" && Number.isFinite(meta.post_tokens)
116
+ ? meta.post_tokens
117
+ : undefined;
118
+ recordHistogram("session.compact.pre_tokens", pre);
119
+ if (post !== undefined) recordHistogram("session.compact.post_tokens", post);
120
+ log(
121
+ "agent",
122
+ `[${chatId}] context compacted (${trigger}): ${pre} tokens` +
123
+ (post !== undefined ? ` -> ${post}` : "") +
124
+ ` — the prefix after the boundary is new, so the next turn re-writes it`,
125
+ );
126
+ }
@@ -36,7 +36,9 @@
36
36
  * did.
37
37
  */
38
38
 
39
- import { logWarn } from "../../util/log.js";
39
+ import { createHash } from "node:crypto";
40
+
41
+ import { logWarn } from "../../../util/log.js";
40
42
 
41
43
  // ── Per-turn cache stats ────────────────────────────────────────────────────
42
44
 
@@ -244,7 +246,7 @@ export function cacheMinimumTokens(model: string): number | undefined {
244
246
  return best?.min;
245
247
  }
246
248
 
247
- /** Cheap tokenizer-free estimate (~4 chars/token), matching soul/projector. */
249
+ /** Cheap tokenizer-free estimate (~4 chars/token), the house heuristic. */
248
250
  function estimateTokens(text: string): number {
249
251
  return Math.ceil(text.length / 4);
250
252
  }
@@ -307,6 +309,24 @@ export function toolFingerprint(
307
309
  ].sort();
308
310
  }
309
311
 
312
+ /**
313
+ * Short, stable digest of a tool set — the thing to compare between two
314
+ * chats. Two chats whose fingerprints (and static prompt) match render a
315
+ * byte-identical prefix and therefore share the cache; two that don't,
316
+ * can't, however similar they look (docs/cache-economics.md, PR B).
317
+ */
318
+ export function fingerprintHash(fingerprint: readonly string[]): string {
319
+ return createHash("sha256")
320
+ .update(fingerprint.join("\n"))
321
+ .digest("hex")
322
+ .slice(0, 12);
323
+ }
324
+
325
+ /** True once a fingerprint has been recorded for this chat in this process. */
326
+ export function hasToolFingerprint(chatId: string): boolean {
327
+ return lastToolSets.has(chatId);
328
+ }
329
+
310
330
  /**
311
331
  * Record this turn's tool set for a chat and warn when it differs from the
312
332
  * previous turn's. Returns true when a change was detected.