talon-agent 4.6.1 → 5.0.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 (223) 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 +19 -4
  6. package/src/backend/claude-sdk/handler.ts +28 -7
  7. package/src/backend/claude-sdk/one-shot.ts +1 -1
  8. package/src/backend/claude-sdk/options.ts +7 -6
  9. package/src/backend/claude-sdk/stream.ts +30 -3
  10. package/src/backend/claude-sdk/warm.ts +1 -1
  11. package/src/backend/codex/constants.ts +1 -1
  12. package/src/backend/codex/factory.ts +2 -2
  13. package/src/backend/codex/handler/events.ts +1 -1
  14. package/src/backend/codex/handler/message.ts +26 -12
  15. package/src/backend/codex/handler/rollout-accounting.ts +1 -1
  16. package/src/backend/codex/init.ts +1 -1
  17. package/src/backend/codex/mcp-config.ts +1 -1
  18. package/src/backend/codex/one-shot.ts +1 -1
  19. package/src/backend/kilo/handler/message.ts +4 -1
  20. package/src/backend/openai-agents/constants.ts +1 -1
  21. package/src/backend/openai-agents/factory.ts +2 -2
  22. package/src/backend/openai-agents/handler/events.ts +1 -1
  23. package/src/backend/openai-agents/handler/message.ts +8 -4
  24. package/src/backend/openai-agents/init.ts +1 -1
  25. package/src/backend/openai-agents/mcp-pool.ts +1 -1
  26. package/src/backend/opencode/handler/message.ts +4 -1
  27. package/src/backend/remote-server/chat-turn.ts +31 -23
  28. package/src/backend/remote-server/events.ts +3 -3
  29. package/src/backend/remote-server/factory.ts +6 -3
  30. package/src/backend/remote-server/index.ts +1 -1
  31. package/src/backend/remote-server/mcp.ts +1 -1
  32. package/src/backend/remote-server/one-shot.ts +1 -1
  33. package/src/backend/remote-server/server-bindings.ts +1 -1
  34. package/src/backend/remote-server/turn.ts +1 -1
  35. package/src/backend/runtime/cache/cache-metrics.ts +126 -0
  36. package/src/backend/{shared → runtime/cache}/cache-telemetry.ts +22 -2
  37. package/src/backend/{shared → runtime}/index.ts +32 -23
  38. package/src/backend/{shared → runtime/prompt}/delivery-contract.ts +1 -1
  39. package/src/backend/{shared → runtime/prompt}/prompt-format.ts +46 -2
  40. package/src/backend/{shared → runtime/prompt}/system-prompt.ts +4 -4
  41. package/src/backend/{shared → runtime/turn}/delivered-text.ts +1 -1
  42. package/src/backend/{shared → runtime/turn}/delivery.ts +2 -2
  43. package/src/backend/{shared → runtime/turn}/handle-retry.ts +5 -5
  44. package/src/backend/{shared → runtime/turn}/handler-to-events.ts +24 -10
  45. package/src/backend/{shared → runtime/turn}/handler-types.ts +7 -1
  46. package/src/backend/{shared → runtime/turn}/model-retry.ts +2 -2
  47. package/src/backend/{shared → runtime/turn}/result-events.ts +2 -2
  48. package/src/backend/{shared → runtime/turn}/stream-state.ts +2 -2
  49. package/src/backend/{shared → runtime/turn}/turn-interrupt.ts +2 -2
  50. package/src/backend/{shared → runtime/turn}/turn-phases.ts +6 -6
  51. package/src/bootstrap.ts +4 -19
  52. package/src/cli.ts +1 -1
  53. package/src/core/agent-runtime/capabilities.ts +11 -0
  54. package/src/core/agent-runtime/contract-tests.ts +92 -1
  55. package/src/core/agent-runtime/events.ts +1 -1
  56. package/src/core/background/{isolated-agent.ts → cron/isolated-agent.ts} +3 -3
  57. package/src/core/background/{job-health.ts → cron/job-health.ts} +1 -1
  58. package/src/core/background/{job-oneshot.ts → cron/job-oneshot.ts} +5 -5
  59. package/src/core/background/{job-prompt.ts → cron/job-prompt.ts} +1 -1
  60. package/src/core/background/{cron.ts → cron/scheduler.ts} +6 -6
  61. package/src/core/background/{cron-spec.ts → cron/spec.ts} +1 -1
  62. package/src/core/background/{dream.ts → dream/index.ts} +10 -15
  63. package/src/core/background/{plan-alerts.ts → pulse/plan-alerts.ts} +3 -3
  64. package/src/core/background/{pulse.ts → pulse/pulse.ts} +8 -5
  65. package/src/core/background/triggers/command.ts +1 -1
  66. package/src/core/config/index.ts +15 -11
  67. package/src/core/daemon/resource-sampler.ts +121 -0
  68. package/src/core/engine/dispatcher.ts +16 -0
  69. package/src/core/engine/gateway-actions/cron.ts +2 -2
  70. package/src/core/engine/gateway-actions/index.ts +3 -0
  71. package/src/core/engine/gateway-actions/memory.ts +335 -0
  72. package/src/core/engine/gateway.ts +0 -6
  73. package/src/core/memory/import.ts +3 -2
  74. package/src/core/memory/taps.ts +199 -0
  75. package/src/core/memory/turn-retrieval.ts +222 -0
  76. package/src/core/prompt/assemble.ts +2 -12
  77. package/src/core/prompt/index.ts +2 -2
  78. package/src/core/prompt/invalidation.ts +1 -1
  79. package/src/core/tasks/index.ts +1 -1
  80. package/src/core/tools/index.ts +2 -0
  81. package/src/core/tools/memory.ts +128 -0
  82. package/src/core/tools/types.ts +1 -0
  83. package/src/core/weaver/turn-cpu.ts +32 -0
  84. package/src/core/weaver/weaver.ts +36 -0
  85. package/src/frontend/discord/admin.ts +1 -1
  86. package/src/frontend/discord/callbacks/components/backend-select.ts +1 -1
  87. package/src/frontend/discord/callbacks/components/pulse.ts +1 -1
  88. package/src/frontend/discord/callbacks/components/settings.ts +1 -1
  89. package/src/frontend/discord/callbacks/modals.ts +4 -1
  90. package/src/frontend/discord/commands/admin.ts +1 -35
  91. package/src/frontend/discord/commands/definitions.ts +0 -11
  92. package/src/frontend/discord/commands/router.ts +0 -3
  93. package/src/frontend/discord/commands/session.ts +6 -0
  94. package/src/frontend/discord/commands/settings.ts +1 -1
  95. package/src/frontend/discord/middleware.ts +1 -25
  96. package/src/frontend/discord/runtime.ts +1 -2
  97. package/src/frontend/native/{auth.ts → bridge/auth.ts} +2 -2
  98. package/src/frontend/native/{discovery.ts → bridge/discovery.ts} +3 -3
  99. package/src/frontend/native/{routes → bridge/routes}/chats.ts +1 -1
  100. package/src/frontend/native/{routes → bridge/routes}/daemon.ts +1 -1
  101. package/src/frontend/native/{routes → bridge/routes}/host.ts +6 -3
  102. package/src/frontend/native/{routes → bridge/routes}/pre-auth.ts +1 -1
  103. package/src/frontend/native/{server.ts → bridge/server.ts} +3 -3
  104. package/src/frontend/native/{tls.ts → bridge/tls.ts} +2 -2
  105. package/src/frontend/native/{chat-lifecycle.ts → chats/chat-lifecycle.ts} +3 -3
  106. package/src/frontend/native/{chat-wire.ts → chats/chat-wire.ts} +4 -4
  107. package/src/frontend/native/{chats.ts → chats/chats.ts} +4 -4
  108. package/src/frontend/native/{empty-chat-sweep.ts → chats/empty-chat-sweep.ts} +4 -4
  109. package/src/frontend/native/{history.ts → chats/history.ts} +7 -7
  110. package/src/frontend/native/{reset.ts → chats/reset.ts} +7 -7
  111. package/src/frontend/native/index.ts +13 -10
  112. package/src/frontend/native/{media.ts → media/media.ts} +3 -3
  113. package/src/frontend/native/runtime.ts +1 -1
  114. package/src/frontend/native/{control.ts → surface/control.ts} +4 -4
  115. package/src/frontend/native/{extensions.ts → surface/extensions.ts} +12 -9
  116. package/src/frontend/native/{handlers.ts → surface/handlers.ts} +19 -14
  117. package/src/frontend/native/{logs.ts → surface/logs.ts} +2 -2
  118. package/src/frontend/native/{memory.ts → surface/memory.ts} +2 -2
  119. package/src/frontend/native/{models.ts → surface/models.ts} +17 -10
  120. package/src/frontend/native/{settings.ts → surface/settings.ts} +9 -9
  121. package/src/frontend/native/{status.ts → surface/status.ts} +3 -3
  122. package/src/frontend/native/{actions.ts → turn/actions.ts} +7 -4
  123. package/src/frontend/native/{context.ts → turn/context.ts} +8 -8
  124. package/src/frontend/native/{emit.ts → turn/emit.ts} +7 -7
  125. package/src/frontend/native/{queue.ts → turn/queue.ts} +3 -3
  126. package/src/frontend/native/{turn-meta.ts → turn/turn-meta.ts} +2 -2
  127. package/src/frontend/native/{turn.ts → turn/turn.ts} +9 -9
  128. package/src/frontend/shared/model-commands.ts +1 -1
  129. package/src/frontend/shared/session-status.ts +35 -8
  130. package/src/frontend/shared/status-context.ts +101 -2
  131. package/src/frontend/telegram/admin/background.ts +1 -1
  132. package/src/frontend/telegram/callbacks/model/backend.ts +1 -1
  133. package/src/frontend/telegram/callbacks/pulse.ts +1 -1
  134. package/src/frontend/telegram/callbacks/settings.ts +1 -1
  135. package/src/frontend/telegram/commands/admin.ts +2 -37
  136. package/src/frontend/telegram/commands/index.ts +1 -1
  137. package/src/frontend/telegram/commands/session.ts +6 -0
  138. package/src/frontend/telegram/commands/settings.ts +1 -1
  139. package/src/frontend/telegram/handlers/messages.ts +0 -10
  140. package/src/frontend/telegram/index.ts +1 -5
  141. package/src/frontend/telegram/middleware.ts +1 -15
  142. package/src/frontend/whatsapp/access.ts +2 -2
  143. package/src/frontend/whatsapp/actions/history.ts +2 -2
  144. package/src/frontend/whatsapp/actions/messaging.ts +2 -2
  145. package/src/frontend/whatsapp/actions/moderation.ts +1 -1
  146. package/src/frontend/whatsapp/actions/send.ts +1 -1
  147. package/src/frontend/whatsapp/commands.ts +8 -2
  148. package/src/frontend/whatsapp/{connection.ts → connection/connection.ts} +5 -5
  149. package/src/frontend/whatsapp/{pairing-service.ts → connection/pairing-service.ts} +3 -3
  150. package/src/frontend/whatsapp/{wa-logger.ts → connection/wa-logger.ts} +2 -2
  151. package/src/frontend/whatsapp/index.ts +4 -4
  152. package/src/frontend/whatsapp/{inbound.ts → messages/inbound.ts} +15 -15
  153. package/src/frontend/whatsapp/{media-store.ts → messages/media-store.ts} +4 -4
  154. package/src/frontend/whatsapp/{message-store.ts → messages/message-store.ts} +1 -1
  155. package/src/frontend/whatsapp/{turn-recovery.ts → messages/turn-recovery.ts} +4 -4
  156. package/src/frontend/whatsapp/registry.ts +1 -1
  157. package/src/frontend/whatsapp/runtime.ts +1 -1
  158. package/src/index.ts +1 -1
  159. package/src/storage/db.ts +6 -1
  160. package/src/storage/memory.ts +59 -8
  161. package/src/storage/metrics.ts +38 -0
  162. package/src/storage/repositories/sessions-repo.ts +6 -0
  163. package/src/storage/session-record.ts +13 -2
  164. package/src/storage/sessions.ts +12 -0
  165. package/src/storage/sql/db.sql +5 -0
  166. package/src/storage/sql/schema.sql +4 -0
  167. package/src/storage/sql/sessions.sql +3 -3
  168. package/src/storage/sql/statements.generated.ts +10 -3
  169. package/src/storage/sql/turn-meta.sql +1 -1
  170. package/src/util/boot-timer.ts +15 -1
  171. package/src/util/chat-id.ts +30 -0
  172. package/src/util/log.ts +1 -1
  173. package/src/util/paths.ts +0 -2
  174. package/src/core/soul/README.md +0 -110
  175. package/src/core/soul/RESEARCH.md +0 -98
  176. package/src/core/soul/associative.ts +0 -98
  177. package/src/core/soul/centrality.ts +0 -98
  178. package/src/core/soul/cluster.ts +0 -83
  179. package/src/core/soul/compiler.ts +0 -207
  180. package/src/core/soul/consolidate.ts +0 -179
  181. package/src/core/soul/critic.ts +0 -162
  182. package/src/core/soul/dag.ts +0 -265
  183. package/src/core/soul/delta.ts +0 -123
  184. package/src/core/soul/drift.ts +0 -99
  185. package/src/core/soul/embedder.ts +0 -129
  186. package/src/core/soul/emergent-critic.ts +0 -96
  187. package/src/core/soul/forgetting.ts +0 -131
  188. package/src/core/soul/governance.ts +0 -93
  189. package/src/core/soul/hash.ts +0 -97
  190. package/src/core/soul/hdc.ts +0 -154
  191. package/src/core/soul/kernel.ts +0 -540
  192. package/src/core/soul/lattice.ts +0 -103
  193. package/src/core/soul/lens.ts +0 -110
  194. package/src/core/soul/projector.ts +0 -240
  195. package/src/core/soul/reflect.ts +0 -170
  196. package/src/core/soul/reflex.ts +0 -164
  197. package/src/core/soul/retrieve.ts +0 -146
  198. package/src/core/soul/salience.ts +0 -146
  199. package/src/core/soul/service.ts +0 -204
  200. package/src/core/soul/settings.ts +0 -47
  201. package/src/core/soul/signals.ts +0 -117
  202. package/src/core/soul/talon-embedder.ts +0 -80
  203. package/src/core/soul/taps.ts +0 -199
  204. package/src/core/soul/types.ts +0 -298
  205. package/src/core/soul/valence.ts +0 -83
  206. /package/src/backend/{shared → runtime}/frontends.ts +0 -0
  207. /package/src/backend/{shared → runtime}/metrics.ts +0 -0
  208. /package/src/backend/{shared → runtime}/sleep.ts +0 -0
  209. /package/src/backend/{shared → runtime/turn}/flow-violation.ts +0 -0
  210. /package/src/backend/{shared → runtime}/usage.ts +0 -0
  211. /package/src/core/{scripting/lua-runner.ts → scripts/lua.ts} +0 -0
  212. /package/src/frontend/native/{routes → bridge/routes}/index.ts +0 -0
  213. /package/src/frontend/native/{routes → bridge/routes}/memory.ts +0 -0
  214. /package/src/frontend/native/{routes → bridge/routes}/mesh.ts +0 -0
  215. /package/src/frontend/native/{routes → bridge/routes}/models.ts +0 -0
  216. /package/src/frontend/native/{routes → bridge/routes}/params.ts +0 -0
  217. /package/src/frontend/native/{routes → bridge/routes}/table.ts +0 -0
  218. /package/src/frontend/native/{tool-result.ts → turn/tool-result.ts} +0 -0
  219. /package/src/frontend/whatsapp/{auth-state.ts → connection/auth-state.ts} +0 -0
  220. /package/src/frontend/whatsapp/{identity.ts → connection/identity.ts} +0 -0
  221. /package/src/frontend/whatsapp/{pairing-lock.ts → connection/pairing-lock.ts} +0 -0
  222. /package/src/frontend/whatsapp/{pairing.ts → connection/pairing.ts} +0 -0
  223. /package/src/frontend/whatsapp/{pins.ts → messages/pins.ts} +0 -0
@@ -17,18 +17,21 @@
17
17
  * gating on top.
18
18
  */
19
19
 
20
- import type { Backend } from "../../core/agent-runtime/capabilities.js";
21
- import { performPluginReload } from "../../core/engine/gateway-actions/plugins.js";
22
- import { getPluginPromptAdditions } from "../../core/plugin/index.js";
23
- import { listPluginItems, setPluginEnabled } from "../../core/plugin/manage.js";
24
- import { notifyPromptInputsChanged } from "../../core/prompt/invalidation.js";
25
- import { listSkills, setSkillEnabled } from "../../storage/skills.js";
20
+ import type { Backend } from "../../../core/agent-runtime/capabilities.js";
21
+ import { performPluginReload } from "../../../core/engine/gateway-actions/plugins.js";
22
+ import { getPluginPromptAdditions } from "../../../core/plugin/index.js";
23
+ import {
24
+ listPluginItems,
25
+ setPluginEnabled,
26
+ } from "../../../core/plugin/manage.js";
27
+ import { notifyPromptInputsChanged } from "../../../core/prompt/invalidation.js";
28
+ import { listSkills, setSkillEnabled } from "../../../storage/skills.js";
26
29
  import {
27
30
  rebuildSystemPrompt,
28
31
  type TalonConfig,
29
- } from "../../core/config/index.js";
30
- import { log } from "../../util/log.js";
31
- import type { PluginItem, SkillItem, ToggleResult } from "./protocol.js";
32
+ } from "../../../core/config/index.js";
33
+ import { log } from "../../../util/log.js";
34
+ import type { PluginItem, SkillItem, ToggleResult } from "../protocol.js";
32
35
  import { persistConfigPatch } from "./settings.js";
33
36
 
34
37
  export function pluginItems(config: TalonConfig): PluginItem[] {
@@ -4,11 +4,11 @@
4
4
  * anything with logic of its own lives in the module named for it.
5
5
  */
6
6
 
7
- import { files } from "../../util/paths.js";
8
- import { setChatPulse } from "../../storage/chat-settings.js";
9
- import { getPooledBackend } from "../../core/engine/backend-controller/index.js";
10
- import { createChat, deleteChat, renameChat } from "./chat-lifecycle.js";
11
- import { broadcastChatUpdated, toClientChat } from "./chat-wire.js";
7
+ import { files } from "../../../util/paths.js";
8
+ import { setChatPulse } from "../../../storage/chat-settings.js";
9
+ import { getPooledBackend } from "../../../core/engine/backend-controller/index.js";
10
+ import { createChat, deleteChat, renameChat } from "../chats/chat-lifecycle.js";
11
+ import { broadcastChatUpdated, toClientChat } from "../chats/chat-wire.js";
12
12
  import { control } from "./control.js";
13
13
  import {
14
14
  pluginItems,
@@ -16,7 +16,7 @@ import {
16
16
  togglePlugin,
17
17
  toggleSkill,
18
18
  } from "./extensions.js";
19
- import { historyPage, searchHistory } from "./history.js";
19
+ import { historyPage, searchHistory } from "../chats/history.js";
20
20
  import { readLogEntries } from "./logs.js";
21
21
  import { listMemory, memoryWhy } from "./memory.js";
22
22
  import {
@@ -24,7 +24,7 @@ import {
24
24
  MAX_UPLOAD_BYTES,
25
25
  resolveUpload,
26
26
  saveUploadStream,
27
- } from "./media.js";
27
+ } from "../media/media.js";
28
28
  import {
29
29
  effortLevels,
30
30
  listBackends,
@@ -33,15 +33,20 @@ import {
33
33
  setEffort,
34
34
  setModel,
35
35
  } from "./models.js";
36
- import { setQueued } from "./queue.js";
37
- import { resetChat } from "./reset.js";
38
- import type { NativeRuntime } from "./runtime.js";
39
- import type { BridgeServerHandlers, SendOptions } from "./server.js";
40
- import type { ClientAttachment } from "./protocol.js";
36
+ import { setQueued } from "../turn/queue.js";
37
+ import { resetChat } from "../chats/reset.js";
38
+ import type { NativeRuntime } from "../runtime.js";
39
+ import type { BridgeServerHandlers, SendOptions } from "../bridge/server.js";
40
+ import type { ClientAttachment } from "../protocol.js";
41
41
  import { configSnapshot, applyConfigUpdate } from "./settings.js";
42
42
  import { bridgeStatus, broadcastStatus } from "./status.js";
43
- import { logWarn } from "../../util/log.js";
44
- import { interruptTurn, isBusy, liveTurnEvents, startTurn } from "./turn.js";
43
+ import { logWarn } from "../../../util/log.js";
44
+ import {
45
+ interruptTurn,
46
+ isBusy,
47
+ liveTurnEvents,
48
+ startTurn,
49
+ } from "../turn/turn.js";
45
50
 
46
51
  /**
47
52
  * Turn a `/send` body's attachment references into the records this daemon
@@ -7,8 +7,8 @@
7
7
  * Pure functions over a raw tail string; file I/O stays with the caller.
8
8
  */
9
9
 
10
- import { tailFile } from "../../util/tail-file.js";
11
- import type { LogEntry, LogLevel } from "./protocol.js";
10
+ import { tailFile } from "../../../util/tail-file.js";
11
+ import type { LogEntry, LogLevel } from "../protocol.js";
12
12
 
13
13
  /** pino numeric levels → wire level names. */
14
14
  const PINO_LEVELS: Record<number, LogLevel> = {
@@ -23,12 +23,12 @@ import {
23
23
  type MemoryHistoryRow,
24
24
  type MemoryKind,
25
25
  type MemoryRow,
26
- } from "../../storage/memory.js";
26
+ } from "../../../storage/memory.js";
27
27
  import type {
28
28
  MemoryHistoryWire,
29
29
  MemoryRowWire,
30
30
  MemoryWhyWire,
31
- } from "./protocol.js";
31
+ } from "../protocol.js";
32
32
 
33
33
  /** Hard ceiling on one listing, whatever the client asks for. */
34
34
  const MAX_LIMIT = 100;
@@ -12,7 +12,7 @@ import {
12
12
  setChatBackend,
13
13
  EFFORT_LEVELS,
14
14
  type EffortLevel,
15
- } from "../../storage/chat-settings.js";
15
+ } from "../../../storage/chat-settings.js";
16
16
  import {
17
17
  getBackendForChat,
18
18
  getBackendIdForChat,
@@ -20,13 +20,13 @@ import {
20
20
  acquireBackendInstance,
21
21
  listAvailableBackends,
22
22
  rebindChat,
23
- } from "../../core/engine/backend-controller/index.js";
24
- import { getActiveReasoningLevels } from "../shared/reasoning-levels.js";
25
- import { broadcastChatUpdated } from "./chat-wire.js";
26
- import { emitSystem } from "./emit.js";
27
- import type { BackendOption, ModelOption } from "./protocol.js";
28
- import { wipeChatConversation } from "./reset.js";
29
- import type { NativeRuntime } from "./runtime.js";
23
+ } from "../../../core/engine/backend-controller/index.js";
24
+ import { getActiveReasoningLevels } from "../../shared/reasoning-levels.js";
25
+ import { broadcastChatUpdated } from "../chats/chat-wire.js";
26
+ import { emitSystem } from "../turn/emit.js";
27
+ import type { BackendOption, ModelOption } from "../protocol.js";
28
+ import { wipeChatConversation } from "../chats/reset.js";
29
+ import type { NativeRuntime } from "../runtime.js";
30
30
  import { broadcastStatus } from "./status.js";
31
31
 
32
32
  export async function listModels(
@@ -104,8 +104,15 @@ export function setModel(
104
104
  // Persisted setting first — see toClientChat. Writing the pick under the
105
105
  // live binding's key would attach it to the wrong backend whenever the
106
106
  // boot-time rebind to the persisted backend is still pending.
107
- const backendId =
108
- getChatSettings(chatId).backend ?? getBackendIdForChat(chatId);
107
+ let backendId: string;
108
+ try {
109
+ backendId = getChatSettings(chatId).backend ?? getBackendIdForChat(chatId);
110
+ } catch {
111
+ // Pool not ready — the same fallback listModels uses in this state, so
112
+ // the pick lands under the backend whose catalog the client was shown.
113
+ // Letting this throw would 400 the route and drop the pick entirely.
114
+ backendId = runtime.config.backend;
115
+ }
109
116
  setChatModelForBackend(chatId, backendId, model.trim() || undefined);
110
117
  broadcastChatUpdated(runtime, entry);
111
118
  }
@@ -14,21 +14,21 @@
14
14
  import { existsSync, readFileSync, mkdirSync } from "node:fs";
15
15
  import writeFileAtomic from "write-file-atomic";
16
16
  import { dirname } from "node:path";
17
- import type { TalonConfig } from "../../core/config/index.js";
18
- import { files as pathFiles } from "../../util/paths.js";
19
- import { setTimezone } from "../../util/time.js";
20
- import { resolveModel } from "../../core/models/catalog.js";
21
- import { getHealthStatus } from "../../util/watchdog.js";
22
- import { getActiveSessionCount } from "../../storage/sessions.js";
17
+ import type { TalonConfig } from "../../../core/config/index.js";
18
+ import { files as pathFiles } from "../../../util/paths.js";
19
+ import { setTimezone } from "../../../util/time.js";
20
+ import { resolveModel } from "../../../core/models/catalog.js";
21
+ import { getHealthStatus } from "../../../util/watchdog.js";
22
+ import { getActiveSessionCount } from "../../../storage/sessions.js";
23
23
  import {
24
24
  startPulseTimer,
25
25
  stopPulseTimer,
26
- } from "../../core/background/pulse.js";
26
+ } from "../../../core/background/pulse/pulse.js";
27
27
  import {
28
28
  startHeartbeatTimer,
29
29
  stopHeartbeatTimer,
30
- } from "../../core/background/heartbeat/index.js";
31
- import { log } from "../../util/log.js";
30
+ } from "../../../core/background/heartbeat/index.js";
31
+ import { log } from "../../../util/log.js";
32
32
 
33
33
  /** Config keys a client may change through the bridge. */
34
34
  export const EDITABLE = [
@@ -3,9 +3,9 @@
3
3
  * as a `status` event whenever something it reports changes.
4
4
  */
5
5
 
6
- import { resolveModel } from "../../core/models/catalog.js";
7
- import { BRIDGE_PROTOCOL_VERSION, type BridgeStatus } from "./protocol.js";
8
- import type { NativeRuntime } from "./runtime.js";
6
+ import { resolveModel } from "../../../core/models/catalog.js";
7
+ import { BRIDGE_PROTOCOL_VERSION, type BridgeStatus } from "../protocol.js";
8
+ import type { NativeRuntime } from "../runtime.js";
9
9
 
10
10
  export function bridgeStatus(runtime: NativeRuntime): BridgeStatus {
11
11
  const { config } = runtime;
@@ -8,10 +8,13 @@
8
8
  * fallback knows the turn already produced output).
9
9
  */
10
10
 
11
- import type { FrontendActionHandler, ActionResult } from "../../core/types.js";
12
- import type { Gateway } from "../../core/engine/gateway.js";
13
- import type { NativeChats, ChatEntry } from "./chats.js";
14
- import type { BridgeEvent, ClientButton } from "./protocol.js";
11
+ import type {
12
+ FrontendActionHandler,
13
+ ActionResult,
14
+ } from "../../../core/types.js";
15
+ import type { Gateway } from "../../../core/engine/gateway.js";
16
+ import type { NativeChats, ChatEntry } from "../chats/chats.js";
17
+ import type { BridgeEvent, ClientButton } from "../protocol.js";
15
18
 
16
19
  export type NativeActionDeps = {
17
20
  chats: NativeChats;
@@ -3,17 +3,17 @@
3
3
  * cache it on the runtime, and push changes to clients.
4
4
  */
5
5
 
6
- import { getSessionInfo } from "../../storage/sessions.js";
7
- import { buildContextDisplay } from "../shared/status-context.js";
8
- import { resolveActiveModelForChat } from "../../core/models/active-model.js";
6
+ import { getSessionInfo } from "../../../storage/sessions.js";
7
+ import { buildContextDisplay } from "../../shared/status-context.js";
8
+ import { resolveActiveModelForChat } from "../../../core/models/active-model.js";
9
9
  import {
10
10
  getBackendForChat,
11
11
  getBackendIdForChat,
12
- } from "../../core/engine/backend-controller/index.js";
13
- import { broadcastChatUpdated } from "./chat-wire.js";
14
- import type { ChatEntry } from "./chats.js";
15
- import type { ContextInfo } from "./protocol.js";
16
- import type { NativeRuntime } from "./runtime.js";
12
+ } from "../../../core/engine/backend-controller/index.js";
13
+ import { broadcastChatUpdated } from "../chats/chat-wire.js";
14
+ import type { ChatEntry } from "../chats/chats.js";
15
+ import type { ContextInfo } from "../protocol.js";
16
+ import type { NativeRuntime } from "../runtime.js";
17
17
 
18
18
  type ContextOptions = { resolveModel?: boolean };
19
19
 
@@ -3,19 +3,19 @@
3
3
  * persist), touch the chat, and broadcast it to every client.
4
4
  */
5
5
 
6
- import { pushMessage } from "../../storage/history.js";
7
- import { extractSessionName } from "../../util/session-name.js";
8
- import { broadcastChatUpdated } from "./chat-wire.js";
9
- import { DEFAULT_CHAT_TITLE, type ChatEntry } from "./chats.js";
10
- import { mediaUrl, registerMedia } from "./media.js";
6
+ import { pushMessage } from "../../../storage/history.js";
7
+ import { extractSessionName } from "../../../util/session-name.js";
8
+ import { broadcastChatUpdated } from "../chats/chat-wire.js";
9
+ import { DEFAULT_CHAT_TITLE, type ChatEntry } from "../chats/chats.js";
10
+ import { mediaUrl, registerMedia } from "../media/media.js";
11
11
  import {
12
12
  BOT_SENDER_ID,
13
13
  USER_SENDER_ID,
14
14
  type ClientButton,
15
15
  type ClientAttachment,
16
16
  type ClientMessage,
17
- } from "./protocol.js";
18
- import type { NativeRuntime } from "./runtime.js";
17
+ } from "../protocol.js";
18
+ import type { NativeRuntime } from "../runtime.js";
19
19
 
20
20
  /**
21
21
  * Name a chat from its first user message — instantly and for free.
@@ -3,9 +3,9 @@
3
3
  * is running. Every mutation is synced to clients as a `chat_updated`.
4
4
  */
5
5
 
6
- import { broadcastChatUpdated } from "./chat-wire.js";
7
- import type { ChatEntry } from "./chats.js";
8
- import type { NativeRuntime, QueuedEntry } from "./runtime.js";
6
+ import { broadcastChatUpdated } from "../chats/chat-wire.js";
7
+ import type { ChatEntry } from "../chats/chats.js";
8
+ import type { NativeRuntime, QueuedEntry } from "../runtime.js";
9
9
 
10
10
  /**
11
11
  * Set (or replace) a chat's queued follow-up and sync it to every client via
@@ -13,8 +13,8 @@ import {
13
13
  recordTurnMeta as storeRecordTurnMeta,
14
14
  getTurnMeta as storeGetTurnMeta,
15
15
  clearTurnMeta as storeClearTurnMeta,
16
- } from "../../storage/turn-meta.js";
17
- import type { ClientToolCall } from "./protocol.js";
16
+ } from "../../../storage/turn-meta.js";
17
+ import type { ClientToolCall } from "../protocol.js";
18
18
 
19
19
  export type TurnMeta = {
20
20
  durationMs?: number;
@@ -6,19 +6,19 @@
6
6
  * backends) — mirroring the terminal renderer's delivery semantics.
7
7
  */
8
8
 
9
- import type { ExecuteResult } from "../../core/types.js";
10
- import type { AgentEvent } from "../../core/agent-runtime/events.js";
11
- import { execute } from "../../core/engine/dispatcher.js";
12
- import { toolInputToRecord } from "../../core/agent-runtime/events.js";
13
- import { getBackendForChat } from "../../core/engine/backend-controller/index.js";
14
- import { isDeliveryTool } from "../../core/tools/index.js";
9
+ import type { ExecuteResult } from "../../../core/types.js";
10
+ import type { AgentEvent } from "../../../core/agent-runtime/events.js";
11
+ import { execute } from "../../../core/engine/dispatcher.js";
12
+ import { toolInputToRecord } from "../../../core/agent-runtime/events.js";
13
+ import { getBackendForChat } from "../../../core/engine/backend-controller/index.js";
14
+ import { isDeliveryTool } from "../../../core/tools/index.js";
15
15
  import { recordTurnMeta } from "./turn-meta.js";
16
- import type { ChatEntry } from "./chats.js";
16
+ import type { ChatEntry } from "../chats/chats.js";
17
17
  import { refreshContext } from "./context.js";
18
18
  import { emitAssistant, emitUser } from "./emit.js";
19
- import type { BridgeEvent, ClientAttachment } from "./protocol.js";
19
+ import type { BridgeEvent, ClientAttachment } from "../protocol.js";
20
20
  import { takeQueued } from "./queue.js";
21
- import type { LiveToolEntry, NativeRuntime } from "./runtime.js";
21
+ import type { LiveToolEntry, NativeRuntime } from "../runtime.js";
22
22
  import { summarizeToolResult } from "./tool-result.js";
23
23
 
24
24
  /** The tool calls of one running turn, in call order. */
@@ -33,7 +33,7 @@ import {
33
33
  } from "../../core/engine/backend-controller/index.js";
34
34
  import { resetSession } from "../../storage/sessions.js";
35
35
  import { clearHistory } from "../../storage/history.js";
36
- import { resetPulseCheckpoint } from "../../core/background/pulse.js";
36
+ import { resetPulseCheckpoint } from "../../core/background/pulse/pulse.js";
37
37
  import { logWarn } from "../../util/log.js";
38
38
  import {
39
39
  displayReasoningEffort,
@@ -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";
21
+ import { resetPulseCheckpoint } from "../../core/background/pulse/pulse.js";
22
+ import { isPulseEnabled } from "../../core/background/pulse/pulse.js";
23
23
  import { getWorkspaceDiskUsage } from "../../util/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
  }
@@ -1,12 +1,14 @@
1
1
  import type { CacheMetricsSupport } from "../../core/types.js";
2
2
  import type { PlanUsage } from "../../core/agent-runtime/capabilities.js";
3
+ import type { CacheVerdict } from "../../storage/metrics.js";
3
4
  import { formatSmartTimestamp, formatRelativeAge } from "../../util/time.js";
5
+ import { formatDuration } from "./format.js";
4
6
 
5
7
  // ── /context breakdown ────────────────────────────────────────────────────────
6
8
 
7
9
  /**
8
10
  * Rough token estimate — ~4 chars/token, the house heuristic used everywhere
9
- * (soul/projector, cache-telemetry). No real tokenizer is wired, so every
11
+ * (cache-telemetry, and the prompt budgets). No real tokenizer is wired, so every
10
12
  * measured figure in the breakdown below is an estimate at this fidelity.
11
13
  */
12
14
  export function estimateContextTokens(text: string): number {
@@ -271,7 +273,7 @@ export function buildCacheDisplay(input: {
271
273
  ? input.cacheWrite
272
274
  : 0;
273
275
 
274
- // Match the canonical formula in `src/backend/shared/usage.ts:cacheHitPercent`:
276
+ // Match the canonical formula in `src/backend/runtime/usage.ts:cacheHitPercent`:
275
277
  // cache_write is tokens being *written to* cache on this call, not served
276
278
  // from it, so it must not dilute the hit ratio. The denominator is
277
279
  // `input + read` ("effective input"), regardless of whether the backend
@@ -287,6 +289,103 @@ export function buildCacheDisplay(input: {
287
289
  };
288
290
  }
289
291
 
292
+ // ── Cache temperature ───────────────────────────────────────────────────────
293
+
294
+ export interface CacheTempDisplay {
295
+ /** Last cross-turn verdict, or "unknown" when this process saw no turn. */
296
+ verdict: CacheVerdict | "unknown";
297
+ /** Time since the last turn ended, or "—" when no turn has finished. */
298
+ idle: string;
299
+ }
300
+
301
+ /**
302
+ * Resolve the "is this chat's prompt prefix still warm?" line.
303
+ *
304
+ * The aggregate `Cache: NN% hit` above it answers a different question — it
305
+ * folds in every request of every turn, and an agentic turn's later requests
306
+ * always read the prefix the first one paid for. What costs money is whether
307
+ * the NEXT turn will find the prefix still cached, and the two inputs to that
308
+ * are the last turn's cross-turn verdict and how long ago that turn ended
309
+ * (docs/cache-economics.md).
310
+ *
311
+ * Returns null when neither is known — a chat that has not completed a turn
312
+ * in this process has nothing to say, and a fabricated "none / 0s" would read
313
+ * as a measurement.
314
+ */
315
+ export function buildCacheTempDisplay(input: {
316
+ verdict?: CacheVerdict;
317
+ lastTurnEndedAt?: number;
318
+ now?: number;
319
+ }): CacheTempDisplay | null {
320
+ const endedAt =
321
+ typeof input.lastTurnEndedAt === "number" &&
322
+ Number.isFinite(input.lastTurnEndedAt) &&
323
+ input.lastTurnEndedAt > 0
324
+ ? input.lastTurnEndedAt
325
+ : undefined;
326
+ if (!input.verdict && endedAt === undefined) return null;
327
+ const now = input.now ?? Date.now();
328
+ return {
329
+ verdict: input.verdict ?? "unknown",
330
+ idle:
331
+ endedAt === undefined ? "—" : formatDuration(Math.max(0, now - endedAt)),
332
+ };
333
+ }
334
+
335
+ /** The one-line rendering every frontend's /status shares. */
336
+ export function formatCacheTempLine(temp: CacheTempDisplay): string {
337
+ return `Cache: ${temp.verdict} last turn · idle ${temp.idle}`;
338
+ }
339
+
340
+ // ── Daemon resources ────────────────────────────────────────────────────────
341
+
342
+ export interface DaemonDisplay {
343
+ rssMb: number;
344
+ heapMb: number;
345
+ /**
346
+ * Mean daemon CPU per turn, in ms, or undefined before any turn has been
347
+ * measured. The metrics store's histograms expose count/avg/min/max only
348
+ * (`storage/metrics.ts`) — there is no percentile to read, so the line
349
+ * says "avg" rather than implying a p50 it cannot compute.
350
+ */
351
+ cpuPerTurnMs: number | undefined;
352
+ /** How many turns the average is over. */
353
+ cpuSamples: number;
354
+ }
355
+
356
+ /**
357
+ * Resolve the "what does the daemon itself cost?" line — resident memory
358
+ * now, and the CPU a turn spends inside Talon rather than waiting on a
359
+ * model (docs/ts-migration-plan.md, Phase 0). Pure: the caller reads
360
+ * `process.memoryUsage()` and the `turn.cpu_ms` histogram.
361
+ */
362
+ export function buildDaemonDisplay(input: {
363
+ rssBytes: number;
364
+ heapBytes: number;
365
+ cpuPerTurn?: { count: number; avg: number };
366
+ }): DaemonDisplay {
367
+ const toMb = (bytes: number): number =>
368
+ Number.isFinite(bytes) && bytes > 0 ? Math.round(bytes / (1024 * 1024)) : 0;
369
+ const cpu = input.cpuPerTurn;
370
+ const measured =
371
+ cpu !== undefined && cpu.count > 0 && Number.isFinite(cpu.avg);
372
+ return {
373
+ rssMb: toMb(input.rssBytes),
374
+ heapMb: toMb(input.heapBytes),
375
+ cpuPerTurnMs: measured ? Math.round(cpu.avg) : undefined,
376
+ cpuSamples: measured ? cpu.count : 0,
377
+ };
378
+ }
379
+
380
+ /** The one-line rendering every frontend's /status shares. */
381
+ export function formatDaemonLine(daemon: DaemonDisplay): string {
382
+ const cpu =
383
+ daemon.cpuPerTurnMs === undefined
384
+ ? "cpu/turn —"
385
+ : `cpu/turn avg ${daemon.cpuPerTurnMs} ms (n=${daemon.cpuSamples})`;
386
+ return `Daemon: rss ${daemon.rssMb} MB · heap ${daemon.heapMb} MB · ${cpu}`;
387
+ }
388
+
290
389
  // ── Plan limits ─────────────────────────────────────────────────────────────
291
390
 
292
391
  /** Figures older than this are labelled with their age in /status. */
@@ -9,7 +9,7 @@ import {
9
9
  describeSchedule,
10
10
  nextRunAt,
11
11
  } from "../../../storage/cron.js";
12
- import { getPulseStatus } from "../../../core/background/pulse.js";
12
+ import { getPulseStatus } from "../../../core/background/pulse/pulse.js";
13
13
  import { replyHtmlChunked } from "./chunked-reply.js";
14
14
 
15
15
  export async function replyCronJobs(ctx: Context): Promise<void> {
@@ -14,7 +14,7 @@ import {
14
14
  rebindChat,
15
15
  releaseChat,
16
16
  } from "../../../../core/engine/backend-controller/index.js";
17
- import { resetPulseCheckpoint } from "../../../../core/background/pulse.js";
17
+ import { resetPulseCheckpoint } from "../../../../core/background/pulse/pulse.js";
18
18
  import { resolveActiveModelForChat } from "../../../../core/models/active-model.js";
19
19
  import { logWarn } from "../../../../util/log.js";
20
20
  import type { ModelCallback } from "../../model-callbacks.js";
@@ -8,7 +8,7 @@ import {
8
8
  disablePulse,
9
9
  enablePulse,
10
10
  isPulseEnabled,
11
- } from "../../../core/background/pulse.js";
11
+ } from "../../../core/background/pulse/pulse.js";
12
12
  import { answerCallbackQuerySafe } from "./query.js";
13
13
 
14
14
  export async function handlePulseCallback(
@@ -14,7 +14,7 @@ import {
14
14
  disablePulse,
15
15
  enablePulse,
16
16
  isPulseEnabled,
17
- } from "../../../core/background/pulse.js";
17
+ } from "../../../core/background/pulse/pulse.js";
18
18
  import { getBackendIdForChat } from "../../../core/engine/backend-controller/index.js";
19
19
  import {
20
20
  renderSettingsText,