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
@@ -0,0 +1,493 @@
1
+ /**
2
+ * The five chat reports — metrics, doctor, mesh, usage, settings — in one
3
+ * place, parameterised by the platform's markup dialect.
4
+ *
5
+ * Discord and Telegram carried line-for-line copies of these renderers,
6
+ * differing only in how they spell bold/italic/code, whether text needs
7
+ * HTML escaping, and how much fits in one message. Two copies of a
8
+ * report is two places to fix a bug and two places to drift, which is
9
+ * exactly what happened: the `/metrics` leaderboard cap and the metric
10
+ * label budget landed on Telegram and never reached Discord.
11
+ *
12
+ * The seam is `ReportFormatter`. Everything a platform does differently
13
+ * is one of its fields; everything else is written once here. Discord's
14
+ * `escape` is the identity function, so applying it at every point
15
+ * Telegram escapes leaves Discord's bytes untouched — and Telegram keeps
16
+ * escaping exactly where it always did, which is what stops a model id
17
+ * or a device name containing `<` from 400-ing the whole send.
18
+ *
19
+ * Where the two platforms differ in *content* rather than markup, both
20
+ * behaviours survive: `/metrics` really is two reports (Discord splits
21
+ * across messages, Telegram shrinks into one panel), and they share the
22
+ * section model rather than the output shape.
23
+ */
24
+
25
+ import type { DoctorReport } from "../../core/doctor/index.js";
26
+ import type { MeshPingResult } from "../../core/mesh/service.js";
27
+ import type { BackendUsageEntry } from "./plan-usage-report.js";
28
+ import {
29
+ DEFAULT_PULSE_INTERVAL_MS,
30
+ formatDuration,
31
+ formatBytes,
32
+ formatModelLabel,
33
+ } from "./format.js";
34
+
35
+ /**
36
+ * One platform's report dialect.
37
+ *
38
+ * Every field exists because the two implementations differed on it. Add
39
+ * a field only when a third frontend genuinely differs — a report that
40
+ * needs a new *shape* is a new function here, not a new flag.
41
+ */
42
+ export interface ReportFormatter {
43
+ /** Bold run: `**s**` on Discord, `<b>s</b>` on Telegram. */
44
+ bold(s: string): string;
45
+ /** Italic run: `_s_` on Discord, `<i>s</i>` on Telegram. */
46
+ italic(s: string): string;
47
+ /**
48
+ * The *other* italic spelling: `*s*` on Discord, `<i>s</i>` on
49
+ * Telegram. Discord markdown has two markers for one effect and
50
+ * `/usage` has always used this one for the staleness label; keeping
51
+ * both spellings is what makes that report byte-identical.
52
+ */
53
+ emphasis(s: string): string;
54
+ /** Inline code run: `` `s` `` on Discord, `<code>s</code>` on Telegram. */
55
+ code(s: string): string;
56
+ /**
57
+ * Escape text that is not markup. Telegram parses with `parse_mode:
58
+ * HTML`, so every interpolated value — model ids, device names, metric
59
+ * keys, backend notes — must go through this or the send fails with
60
+ * `can't parse entities`. Discord needs none, and passes the identity
61
+ * function.
62
+ */
63
+ escape(s: string): string;
64
+ /** Characters this platform accepts in one report message. */
65
+ lineLimit: number;
66
+ /** Longest metric key rendered before it is elided (60 vs 80). */
67
+ metricLabelMax: number;
68
+ /** Settings-panel pulse row label — Discord's carries a 🔔, Telegram's doesn't. */
69
+ pulseLabel: string;
70
+ }
71
+
72
+ export type MetricsSnapshot = {
73
+ counters: Record<string, number>;
74
+ histograms: Record<
75
+ string,
76
+ { count: number; avg: number; min: number; max: number }
77
+ >;
78
+ };
79
+
80
+ /**
81
+ * One titled block of a metrics report. `hidden` counts rows dropped to
82
+ * make the report fit; it renders as a trailing "…and N more".
83
+ */
84
+ type Section = { title: string; rows: string[]; hidden: number };
85
+
86
+ function truncateMetricLabel(label: string, max: number): string {
87
+ return label.length <= max ? label : `${label.slice(0, max - 3)}...`;
88
+ }
89
+
90
+ /**
91
+ * Group a snapshot into the report's sections.
92
+ *
93
+ * `toolCallsTopN` pre-caps the `tool_calls` leaderboard: a long-lived
94
+ * bot accumulates a lifetime entry per distinct tool name (easily 100+
95
+ * once plugin and MCP tools are counted) and past the top rows the tail
96
+ * is one-call tools nobody reads. Discord passes `Infinity` — it splits
97
+ * across messages instead and has never capped.
98
+ */
99
+ function buildMetricsSections(
100
+ metrics: MetricsSnapshot,
101
+ fmt: ReportFormatter,
102
+ toolCallsTopN: number,
103
+ ): Section[] {
104
+ const sections: Section[] = [];
105
+
106
+ // Histograms come in two flavours: durations (keys ending in `_ms`,
107
+ // rendered as human times) and plain counts like `tool_calls_per_turn`
108
+ // (rendered as bare numbers — "min=1ms" for a count is nonsense).
109
+ const histKeys = Object.keys(metrics.histograms).sort();
110
+ const durationKeys = histKeys.filter((key) => key.endsWith("_ms"));
111
+ const countKeys = histKeys.filter((key) => !key.endsWith("_ms"));
112
+ const histLine = (key: string, format: (v: number) => string): string => {
113
+ const h = metrics.histograms[key]!;
114
+ return (
115
+ ` ${fmt.code(fmt.escape(truncateMetricLabel(key, fmt.metricLabelMax)))}` +
116
+ ` n=${h.count} avg=${format(h.avg)} min=${format(h.min)} ` +
117
+ `max=${format(h.max)}`
118
+ );
119
+ };
120
+ if (durationKeys.length > 0) {
121
+ sections.push({
122
+ title: fmt.bold("Latency"),
123
+ rows: durationKeys.map((key) => histLine(key, formatDuration)),
124
+ hidden: 0,
125
+ });
126
+ }
127
+ if (countKeys.length > 0) {
128
+ sections.push({
129
+ title: fmt.bold("Distributions"),
130
+ rows: countKeys.map((key) => histLine(key, String)),
131
+ hidden: 0,
132
+ });
133
+ }
134
+
135
+ for (const [prefix, keys] of groupCounters(metrics)) {
136
+ const shown = keys.slice(0, toolCallsTopN);
137
+ sections.push({
138
+ title: fmt.bold(fmt.escape(prefix)),
139
+ rows: shown.map((key) => {
140
+ const label = key.includes(".")
141
+ ? key.split(".").slice(1).join(".")
142
+ : key;
143
+ const name = truncateMetricLabel(label, fmt.metricLabelMax);
144
+ return (
145
+ ` ${fmt.code(fmt.escape(name))} ` +
146
+ `${metrics.counters[key]!.toLocaleString()}`
147
+ );
148
+ }),
149
+ hidden: keys.length - shown.length,
150
+ });
151
+ }
152
+
153
+ return sections;
154
+ }
155
+
156
+ /** Counter keys bucketed by their dotted namespace, groups in name order. */
157
+ function groupCounters(metrics: MetricsSnapshot): Array<[string, string[]]> {
158
+ const groups = new Map<string, string[]>();
159
+ for (const key of Object.keys(metrics.counters).sort()) {
160
+ const prefix = key.includes(".") ? key.split(".")[0]! : "general";
161
+ if (!groups.has(prefix)) groups.set(prefix, []);
162
+ groups.get(prefix)!.push(key);
163
+ }
164
+ return [...groups.keys()].sort().map((prefix): [string, string[]] => {
165
+ // tool_calls reads best as a leaderboard — busiest tools first.
166
+ // Other groups keep alphabetical order (stable lookup by name).
167
+ const keys = groups.get(prefix)!;
168
+ return [
169
+ prefix,
170
+ prefix === "tool_calls"
171
+ ? [...keys].sort(
172
+ (a, b) =>
173
+ metrics.counters[b]! - metrics.counters[a]! || a.localeCompare(b),
174
+ )
175
+ : keys,
176
+ ];
177
+ });
178
+ }
179
+
180
+ /** A section as lines: title, rows, and the elision note when rows were dropped. */
181
+ function sectionLines(section: Section, fmt: ReportFormatter): string[] {
182
+ return [
183
+ section.title,
184
+ ...section.rows,
185
+ ...(section.hidden > 0
186
+ ? [` ${fmt.italic(`…and ${section.hidden} more`)}`]
187
+ : []),
188
+ ];
189
+ }
190
+
191
+ /**
192
+ * Render the metrics report across as many messages as it takes, each
193
+ * ≤ `maxLen`, continuation messages re-stating the title. Discord's
194
+ * shape: nothing is dropped, the report just gets longer.
195
+ */
196
+ export function renderMetricsMessages(
197
+ metrics: MetricsSnapshot,
198
+ fmt: ReportFormatter,
199
+ maxLen = fmt.lineLimit,
200
+ title = "📊 Metrics",
201
+ ): string[] {
202
+ const firstHeader = fmt.bold(title);
203
+ const continuationHeader = fmt.bold(`${title} (cont.)`);
204
+ const sections = buildMetricsSections(metrics, fmt, Infinity);
205
+ if (sections.length === 0) {
206
+ return [`${firstHeader}\n\n${fmt.italic("No metrics recorded yet.")}`];
207
+ }
208
+
209
+ const chunks: string[] = [];
210
+ let header = firstHeader;
211
+ let current = header;
212
+ const flush = () => {
213
+ chunks.push(current);
214
+ header = continuationHeader;
215
+ current = header;
216
+ };
217
+ const appendLine = (line: string) => {
218
+ if (!line && current === header) return;
219
+ const candidate = `${current}\n${line}`;
220
+ if (candidate.length <= maxLen) {
221
+ current = candidate;
222
+ return;
223
+ }
224
+ if (current !== header) {
225
+ flush();
226
+ if (!line) return;
227
+ }
228
+ const available = maxLen - header.length - 1;
229
+ if (available < 0) return;
230
+ const safeLine =
231
+ line.length <= available
232
+ ? line
233
+ : available >= 4
234
+ ? `${line.slice(0, available - 3)}...`
235
+ : line.slice(0, available);
236
+ current = `${current}\n${safeLine}`;
237
+ };
238
+
239
+ for (const section of sections) {
240
+ appendLine("");
241
+ for (const line of sectionLines(section, fmt)) appendLine(line);
242
+ }
243
+ if (current !== header || chunks.length === 0) chunks.push(current);
244
+ return chunks;
245
+ }
246
+
247
+ /**
248
+ * Render the metrics report as a SINGLE message, shrinking to fit.
249
+ *
250
+ * Telegram's shape: rather than split the report across messages — which
251
+ * is what the tappable panel replaced — rows are dropped from the longest
252
+ * section first (each drop bumping that section's "…and N more") until
253
+ * the whole thing fits. Sections always keep at least their first row, so
254
+ * every group stays visible.
255
+ */
256
+ export function renderMetricsPanel(
257
+ metrics: MetricsSnapshot,
258
+ fmt: ReportFormatter,
259
+ title: string,
260
+ maxLen = fmt.lineLimit,
261
+ toolCallsTopN = Infinity,
262
+ ): string {
263
+ const header = fmt.bold(fmt.escape(title));
264
+ const sections = buildMetricsSections(metrics, fmt, toolCallsTopN);
265
+ if (sections.length === 0) {
266
+ return `${header}\n\n${fmt.italic("No metrics recorded yet.")}`;
267
+ }
268
+
269
+ const render = () =>
270
+ [header, ...sections.map((s) => sectionLines(s, fmt).join("\n"))].join(
271
+ "\n\n",
272
+ );
273
+
274
+ let out = render();
275
+ while (out.length > maxLen) {
276
+ let target: Section | undefined;
277
+ for (const section of sections) {
278
+ if (section.rows.length <= 1) continue;
279
+ if (!target || section.rows.length > target.rows.length) target = section;
280
+ }
281
+ if (!target) break;
282
+ target.rows.pop();
283
+ target.hidden += 1;
284
+ out = render();
285
+ }
286
+
287
+ if (out.length <= maxLen) return out;
288
+ // Nothing left to shed (a pathologically small maxLen). Cut on a line
289
+ // boundary so we never slice through an HTML tag and fail the parse.
290
+ const cut = out.lastIndexOf("\n", maxLen - 1);
291
+ return cut > 0 ? out.slice(0, cut) : out.slice(0, maxLen);
292
+ }
293
+
294
+ const DOCTOR_ICONS: Record<string, string> = {
295
+ ok: "✅",
296
+ warn: "⚠️",
297
+ fail: "❌",
298
+ info: "▫️",
299
+ };
300
+
301
+ /**
302
+ * Render a DoctorReport as one message body.
303
+ *
304
+ * Same data as `talon doctor` (src/core/doctor/) plus in-process runtime
305
+ * info — when this renders, the bot is by definition running, so the
306
+ * CLI's "is the bot up" probe becomes an uptime line instead. Callers
307
+ * that have a message cap split the result themselves.
308
+ */
309
+ export function renderDoctorReport(
310
+ report: DoctorReport,
311
+ fmt: ReportFormatter,
312
+ ): string {
313
+ const lines = [fmt.bold("🩺 Talon Doctor"), "", fmt.bold("Environment")];
314
+
315
+ const render = (check: DoctorReport["checks"][number]): string => {
316
+ const detail = check.detail ? ` (${fmt.escape(check.detail)})` : "";
317
+ return `${DOCTOR_ICONS[check.status]} ${fmt.escape(check.label)}${detail}`;
318
+ };
319
+
320
+ for (const check of report.checks.filter((c) => !c.inactive)) {
321
+ lines.push(render(check));
322
+ }
323
+
324
+ // Configured-but-idle backends get their own block: they describe what a
325
+ // switch would run into, not the state of the running deployment.
326
+ const idle = report.checks.filter((c) => c.inactive);
327
+ if (idle.length > 0) {
328
+ lines.push("", fmt.bold("Other backends"), ...idle.map(render));
329
+ }
330
+
331
+ lines.push("", fmt.bold("Native modules"));
332
+ for (const mod of report.native) {
333
+ const size =
334
+ mod.sizeBytes !== undefined ? ` · ${formatBytes(mod.sizeBytes)}` : "";
335
+ const note = mod.note ? ` (${fmt.escape(mod.note)})` : "";
336
+ lines.push(
337
+ `${mod.ok ? DOCTOR_ICONS.ok : DOCTOR_ICONS.fail} ` +
338
+ `${fmt.code(fmt.escape(mod.name))} — ${fmt.escape(mod.language)} → ` +
339
+ `${fmt.escape(mod.target)}${size}${note}`,
340
+ );
341
+ }
342
+
343
+ lines.push(
344
+ "",
345
+ fmt.bold("Process"),
346
+ `Uptime ${formatDuration(process.uptime() * 1000)} · PID ${process.pid} · ` +
347
+ `Node ${fmt.escape(process.versions.node)}`,
348
+ "",
349
+ report.issues === 0
350
+ ? `${DOCTOR_ICONS.ok} All checks passed.`
351
+ : `${DOCTOR_ICONS.warn} ${report.issues} issue(s) found.`,
352
+ );
353
+
354
+ return lines.join("\n");
355
+ }
356
+
357
+ function meshDeviceLine(
358
+ r: MeshPingResult,
359
+ fmt: ReportFormatter,
360
+ now: number,
361
+ ): string {
362
+ const d = r.device;
363
+ const bits: string[] = [fmt.escape(d.platform)];
364
+ if (r.reachable && typeof r.latencyMs === "number") {
365
+ bits.push(`${r.latencyMs} ms`);
366
+ } else if (d.online && r.error) {
367
+ bits.push(fmt.escape(r.error));
368
+ } else if (!d.online) {
369
+ bits.push(`last seen ${formatDuration(now - d.lastSeen)} ago`);
370
+ }
371
+ if (typeof d.battery === "number") {
372
+ bits.push(`${d.battery}%${d.charging ? " charging" : ""}`);
373
+ }
374
+ return ` ${fmt.bold(fmt.escape(d.name))} — ${bits.join(" · ")}`;
375
+ }
376
+
377
+ /**
378
+ * Render the `/mesh` fleet report.
379
+ *
380
+ * Devices group under a state heading — Responding, Unreachable, Offline
381
+ * — rather than carrying a coloured status glyph per row: the grouping
382
+ * already says what the glyph said, and the report stays readable when
383
+ * the fleet grows. Empty groups are omitted entirely. `footer` is the
384
+ * platform's own trailer (Telegram appends the bridge block; Discord
385
+ * appends nothing).
386
+ */
387
+ export function renderMeshReport(
388
+ results: MeshPingResult[],
389
+ fmt: ReportFormatter,
390
+ now = Date.now(),
391
+ footer: string[] = [],
392
+ ): string {
393
+ const heading = fmt.bold("Mesh");
394
+ if (results.length === 0) {
395
+ return [
396
+ heading,
397
+ "",
398
+ fmt.italic("No devices have registered yet."),
399
+ ...footer,
400
+ ].join("\n");
401
+ }
402
+
403
+ const responding = results
404
+ .filter((r) => r.reachable)
405
+ .sort((a, b) => (a.latencyMs ?? Infinity) - (b.latencyMs ?? Infinity));
406
+ const unreachable = results
407
+ .filter((r) => !r.reachable && r.device.online)
408
+ .sort((a, b) => a.device.name.localeCompare(b.device.name));
409
+ const offline = results
410
+ .filter((r) => !r.reachable && !r.device.online)
411
+ .sort((a, b) => b.device.lastSeen - a.device.lastSeen);
412
+
413
+ const summary = [
414
+ `${results.length} device${results.length === 1 ? "" : "s"}`,
415
+ `${responding.length} responding`,
416
+ ...(unreachable.length > 0 ? [`${unreachable.length} unreachable`] : []),
417
+ ...(offline.length > 0 ? [`${offline.length} offline`] : []),
418
+ ].join(" · ");
419
+
420
+ const lines = [heading, summary];
421
+ const section = (title: string, entries: MeshPingResult[]): void => {
422
+ if (entries.length === 0) return;
423
+ lines.push(
424
+ "",
425
+ fmt.bold(title),
426
+ ...entries.map((r) => meshDeviceLine(r, fmt, now)),
427
+ );
428
+ };
429
+ section("Responding", responding);
430
+ section("Unreachable", unreachable);
431
+ section("Offline", offline);
432
+ lines.push(...footer);
433
+
434
+ return lines.join("\n");
435
+ }
436
+
437
+ /** Render the `/usage` report — one block per exposed backend. */
438
+ export function renderUsageMessage(
439
+ entries: BackendUsageEntry[],
440
+ fmt: ReportFormatter,
441
+ ): string {
442
+ const lines = [fmt.bold("📊 Plan usage")];
443
+
444
+ for (const entry of entries) {
445
+ const name = fmt.escape(entry.label || entry.id);
446
+ if (!entry.plan) {
447
+ lines.push(
448
+ "",
449
+ `${fmt.bold(name)} — ${fmt.italic(fmt.escape(entry.note ?? ""))}`,
450
+ );
451
+ continue;
452
+ }
453
+ const age = entry.plan.ageLabel
454
+ ? ` ${fmt.emphasis(`(${entry.plan.ageLabel})`)}`
455
+ : "";
456
+ const plan = entry.plan.plan ? ` · ${fmt.escape(entry.plan.plan)}` : "";
457
+ lines.push("", `${fmt.bold(name)}${plan}${age}`);
458
+ if (entry.plan.resetsAvailable) {
459
+ const n = entry.plan.resetsAvailable;
460
+ const resets = `usage limit reset${n === 1 ? "" : "s"} available`;
461
+ lines.push(` • You have ${fmt.bold(String(n))} ${resets}`);
462
+ }
463
+ for (const w of entry.plan.windows) {
464
+ const reset = w.resetLabel ? ` reset ${w.resetLabel}` : "";
465
+ const bar = `${fmt.escape(w.label.padEnd(6))}${w.bar} ${String(w.percent).padStart(3)}%`;
466
+ lines.push(` ${fmt.code(bar)}${reset}`);
467
+ }
468
+ }
469
+
470
+ return lines.join("\n");
471
+ }
472
+
473
+ /** Settings panel body — model, effort, and the pulse row. */
474
+ export function renderSettingsText(
475
+ fmt: ReportFormatter,
476
+ model: string,
477
+ effort: string,
478
+ proactive: boolean,
479
+ pulseIntervalMs?: number,
480
+ modelDetails?: Array<string>,
481
+ ): string {
482
+ const intervalStr = formatDuration(
483
+ pulseIntervalMs || DEFAULT_PULSE_INTERVAL_MS,
484
+ );
485
+ return [
486
+ fmt.bold("🦅 Settings"),
487
+ "",
488
+ `${fmt.bold("Model:")} ${fmt.code(fmt.escape(formatModelLabel(model)))}`,
489
+ ...(modelDetails?.length ? modelDetails.map((d) => fmt.escape(d)) : []),
490
+ `${fmt.bold("Effort:")} ${effort}`,
491
+ `${fmt.bold(fmt.pulseLabel)} ${proactive ? "on" : "off"} (every ${intervalStr})`,
492
+ ].join("\n");
493
+ }
@@ -18,20 +18,25 @@ import {
18
18
  } from "../../storage/sessions.js";
19
19
  import { clearHistory } from "../../storage/history.js";
20
20
  import { getChatSettings } from "../../storage/chat-settings.js";
21
- import { resetPulseCheckpoint } from "../../core/background/pulse.js";
22
- import { isPulseEnabled } from "../../core/background/pulse.js";
23
- import { getWorkspaceDiskUsage } from "../../util/workspace.js";
21
+ import { resetPulseCheckpoint } from "../../core/background/pulse/pulse.js";
22
+ import { isPulseEnabled } from "../../core/background/pulse/pulse.js";
23
+ import { getWorkspaceDiskUsage } from "../../core/vfs/workspace.js";
24
24
  import { appendDailyLog } from "../../storage/daily-log.js";
25
25
  import { resolveActiveModelForChat } from "../../core/models/active-model.js";
26
26
  import { getPooledBackend } from "../../core/engine/backend-controller/index.js";
27
27
  import {
28
28
  buildCacheDisplay,
29
+ buildCacheTempDisplay,
29
30
  buildContextDisplay,
31
+ buildDaemonDisplay,
30
32
  buildPlanDisplay,
31
33
  type CacheDisplay,
34
+ type CacheTempDisplay,
32
35
  type ContextDisplay,
36
+ type DaemonDisplay,
33
37
  type PlanDisplay,
34
38
  } from "./status-context.js";
39
+ import { getCacheVerdict, getMetrics } from "../../storage/metrics.js";
35
40
  import { formatDuration } from "./format.js";
36
41
 
37
42
  /**
@@ -79,6 +84,12 @@ export interface SessionStatusData {
79
84
  pulseOn: boolean;
80
85
  context: ContextDisplay;
81
86
  cache: CacheDisplay | null;
87
+ /**
88
+ * Whether the chat's prompt prefix is still warm: the last turn's
89
+ * cross-turn cache verdict and how long ago that turn ended. Null when no
90
+ * turn has finished in this process (docs/cache-economics.md).
91
+ */
92
+ cacheTemp: CacheTempDisplay | null;
82
93
  plan: PlanDisplay | null;
83
94
  inputTokens: number;
84
95
  outputTokens: number;
@@ -98,6 +109,16 @@ export interface SessionStatusData {
98
109
  runtime: string;
99
110
  /** Daemon resident set size, in bytes. */
100
111
  rssBytes: number;
112
+ /**
113
+ * What the daemon itself costs: resident memory now, plus the CPU a turn
114
+ * spends inside Talon (docs/ts-migration-plan.md, Phase 0).
115
+ */
116
+ daemon: DaemonDisplay;
117
+ }
118
+
119
+ /** Mean turn duration, or 0 before any turn has been timed. */
120
+ function averageResponseMs(turns: number, totalResponseMs: number): number {
121
+ return turns > 0 && totalResponseMs ? Math.round(totalResponseMs / turns) : 0;
101
122
  }
102
123
 
103
124
  /**
@@ -182,10 +203,7 @@ export async function collectSessionStatus(
182
203
  await planSource?.usage?.getPlanUsage?.().catch(() => undefined),
183
204
  );
184
205
 
185
- const avgResponseMs =
186
- info.turns > 0 && u.totalResponseMs
187
- ? Math.round(u.totalResponseMs / info.turns)
188
- : 0;
206
+ const memory = process.memoryUsage();
189
207
 
190
208
  return {
191
209
  activeModel,
@@ -195,6 +213,10 @@ export async function collectSessionStatus(
195
213
  pulseOn: isPulseEnabled(chatId),
196
214
  context,
197
215
  cache,
216
+ cacheTemp: buildCacheTempDisplay({
217
+ verdict: getCacheVerdict(chatId),
218
+ lastTurnEndedAt: info.lastTurnEndedAt,
219
+ }),
198
220
  plan,
199
221
  inputTokens,
200
222
  outputTokens,
@@ -202,7 +224,7 @@ export async function collectSessionStatus(
202
224
  turns: info.turns,
203
225
  turnsModelLabel,
204
226
  lastResponseMs: u.lastResponseMs || 0,
205
- avgResponseMs,
227
+ avgResponseMs: averageResponseMs(info.turns, u.totalResponseMs),
206
228
  fastestMs: u.fastestResponseMs === Infinity ? 0 : u.fastestResponseMs || 0,
207
229
  diskBytes: getWorkspaceDiskUsage(config.workspace),
208
230
  sessionAge: info.createdAt
@@ -215,6 +237,11 @@ export async function collectSessionStatus(
215
237
  runtime: process.versions.bun
216
238
  ? `Bun ${process.versions.bun}`
217
239
  : `Node ${process.versions.node}`,
218
- rssBytes: process.memoryUsage().rss,
240
+ rssBytes: memory.rss,
241
+ daemon: buildDaemonDisplay({
242
+ rssBytes: memory.rss,
243
+ heapBytes: memory.heapUsed,
244
+ cpuPerTurn: getMetrics().histograms["turn.cpu_ms"],
245
+ }),
219
246
  };
220
247
  }