talon-agent 4.6.0 → 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 (225) hide show
  1. package/README.md +3 -3
  2. package/package.json +3 -2
  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 -24
  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/goals-repo.ts +2 -1
  163. package/src/storage/repositories/sessions-repo.ts +6 -0
  164. package/src/storage/session-record.ts +13 -2
  165. package/src/storage/sessions.ts +12 -0
  166. package/src/storage/sql/db.sql +5 -0
  167. package/src/storage/sql/schema.sql +4 -0
  168. package/src/storage/sql/sessions.sql +3 -3
  169. package/src/storage/sql/statements.generated.ts +10 -3
  170. package/src/storage/sql/turn-meta.sql +1 -1
  171. package/src/util/boot-timer.ts +15 -1
  172. package/src/util/chat-id.ts +30 -0
  173. package/src/util/concurrency.ts +1 -1
  174. package/src/util/log.ts +1 -1
  175. package/src/util/paths.ts +0 -2
  176. package/src/core/soul/README.md +0 -110
  177. package/src/core/soul/RESEARCH.md +0 -98
  178. package/src/core/soul/associative.ts +0 -98
  179. package/src/core/soul/centrality.ts +0 -98
  180. package/src/core/soul/cluster.ts +0 -83
  181. package/src/core/soul/compiler.ts +0 -207
  182. package/src/core/soul/consolidate.ts +0 -179
  183. package/src/core/soul/critic.ts +0 -162
  184. package/src/core/soul/dag.ts +0 -265
  185. package/src/core/soul/delta.ts +0 -123
  186. package/src/core/soul/drift.ts +0 -99
  187. package/src/core/soul/embedder.ts +0 -129
  188. package/src/core/soul/emergent-critic.ts +0 -96
  189. package/src/core/soul/forgetting.ts +0 -131
  190. package/src/core/soul/governance.ts +0 -93
  191. package/src/core/soul/hash.ts +0 -97
  192. package/src/core/soul/hdc.ts +0 -154
  193. package/src/core/soul/kernel.ts +0 -540
  194. package/src/core/soul/lattice.ts +0 -103
  195. package/src/core/soul/lens.ts +0 -110
  196. package/src/core/soul/projector.ts +0 -240
  197. package/src/core/soul/reflect.ts +0 -170
  198. package/src/core/soul/reflex.ts +0 -164
  199. package/src/core/soul/retrieve.ts +0 -146
  200. package/src/core/soul/salience.ts +0 -146
  201. package/src/core/soul/service.ts +0 -204
  202. package/src/core/soul/settings.ts +0 -47
  203. package/src/core/soul/signals.ts +0 -117
  204. package/src/core/soul/talon-embedder.ts +0 -80
  205. package/src/core/soul/taps.ts +0 -199
  206. package/src/core/soul/types.ts +0 -298
  207. package/src/core/soul/valence.ts +0 -83
  208. /package/src/backend/{shared → runtime}/frontends.ts +0 -0
  209. /package/src/backend/{shared → runtime}/metrics.ts +0 -0
  210. /package/src/backend/{shared → runtime}/sleep.ts +0 -0
  211. /package/src/backend/{shared → runtime/turn}/flow-violation.ts +0 -0
  212. /package/src/backend/{shared → runtime}/usage.ts +0 -0
  213. /package/src/core/{scripting/lua-runner.ts → scripts/lua.ts} +0 -0
  214. /package/src/frontend/native/{routes → bridge/routes}/index.ts +0 -0
  215. /package/src/frontend/native/{routes → bridge/routes}/memory.ts +0 -0
  216. /package/src/frontend/native/{routes → bridge/routes}/mesh.ts +0 -0
  217. /package/src/frontend/native/{routes → bridge/routes}/models.ts +0 -0
  218. /package/src/frontend/native/{routes → bridge/routes}/params.ts +0 -0
  219. /package/src/frontend/native/{routes → bridge/routes}/table.ts +0 -0
  220. /package/src/frontend/native/{tool-result.ts → turn/tool-result.ts} +0 -0
  221. /package/src/frontend/whatsapp/{auth-state.ts → connection/auth-state.ts} +0 -0
  222. /package/src/frontend/whatsapp/{identity.ts → connection/identity.ts} +0 -0
  223. /package/src/frontend/whatsapp/{pairing-lock.ts → connection/pairing-lock.ts} +0 -0
  224. /package/src/frontend/whatsapp/{pairing.ts → connection/pairing.ts} +0 -0
  225. /package/src/frontend/whatsapp/{pins.ts → messages/pins.ts} +0 -0
@@ -0,0 +1,199 @@
1
+ /**
2
+ * Message taps — the memory store's mechanical input stream.
3
+ *
4
+ * This is the one part of the soul kernel that was worth keeping
5
+ * (docs/memory-persona-plan.md §2): a pair of high-precision regex sets
6
+ * that recognise, without a model call, the two message shapes that carry
7
+ * durable intent — a *directive* ("from now on, …") and a *correction*
8
+ * ("no, that's wrong"). The kernel that consumed them is gone; the rows
9
+ * now land in the typed memory store, where the core view and the
10
+ * retriever already know how to read them.
11
+ *
12
+ * Three rules govern what gets written, and none of them live in the
13
+ * store:
14
+ *
15
+ * - **Trust comes from the chat.** A DM (or a local terminal/native
16
+ * session) is the operator typing, so the row is `operator` trust —
17
+ * stronger than `remember`'s `agent`, because a tap records what a
18
+ * human literally wrote rather than what the model concluded. A
19
+ * group — or a chat-id grammar that cannot tell, which fails closed —
20
+ * is `group_chat`, and a **directive is not recorded there at all**:
21
+ * durable standing intent only comes from a direct conversation with
22
+ * the operator. Same rule as the `remember` action.
23
+ * - **A repeated phrase is not a second memory.** The FTS near-dupe
24
+ * probe runs before every write, so saying "always use ripgrep" twice
25
+ * leaves one row, not two.
26
+ * - **It fails closed and it is never on the prompt-cache path.** Any
27
+ * store error is one warning and nothing else; the turn proceeds. This
28
+ * module must not import `core/prompt/invalidation.js` — a tap that
29
+ * invalidated every live session's prompt snapshot would turn a
30
+ * ~50-token claim into a 60–90 k cache write (plan §3.6).
31
+ */
32
+
33
+ import {
34
+ assertMemory,
35
+ findSimilarMemories,
36
+ type MemoryKind,
37
+ type MemorySource,
38
+ type MemoryTrust,
39
+ } from "../../storage/memory.js";
40
+ import { chatScope } from "../../util/chat-id.js";
41
+ import { log, logWarn } from "../../util/log.js";
42
+
43
+ /**
44
+ * High-precision cues that a message is a *correction* of Talon's behavior. Kept
45
+ * tight on purpose: a false positive stores a claim about how to behave, so we
46
+ * would rather miss a soft correction than mislabel ordinary chat.
47
+ */
48
+ const CORRECTION_PATTERNS: readonly RegExp[] = [
49
+ /^\s*(no|nope|nah)\b[\s,.!:-]/i,
50
+ /\bthat'?s (wrong|incorrect|not (right|correct)|false)\b/i,
51
+ /\b(you'?re|you are) wrong\b/i,
52
+ /\bnot what i (asked|meant|wanted|said)\b/i,
53
+ /\bnever (do|say) that( again)?\b/i,
54
+ /\b(stop|quit) (doing|saying) that\b/i,
55
+ /\bwrong[\s,.!]/i,
56
+ /\byou (messed|screwed|fucked) (that|this|it)? ?up\b/i,
57
+ ];
58
+
59
+ /**
60
+ * High-precision cues that a message is a *directive* about how to be — a
61
+ * standing instruction rather than a one-off request. These are stored verbatim
62
+ * as evidence, so again we favor precision over recall.
63
+ */
64
+ const DIRECTIVE_PATTERNS: readonly RegExp[] = [
65
+ /\bfrom now on\b/i,
66
+ /\bgoing forward\b/i,
67
+ /\bin (the )?future\b/i,
68
+ /\byou should (always|never)\b/i,
69
+ /\b(always|never) (do|say|reply|respond|answer|use|be|call|check)\b/i,
70
+ /\bi (want|need|'?d like) you to\b/i,
71
+ /\bmake sure (you|to|that)\b/i,
72
+ /\bremember to\b/i,
73
+ ];
74
+
75
+ export type MessageClass = "directive" | "correction" | null;
76
+
77
+ /** `source.actor` on every row this path writes — the ownership marker. */
78
+ const TAP_ACTOR = "tap";
79
+
80
+ /** The subject a tapped directive is filed under: standing operator intent. */
81
+ const DIRECTIVE_SUBJECT = "operator";
82
+
83
+ /** The subject a tapped correction is filed under. */
84
+ const CORRECTION_SUBJECT = "correction";
85
+
86
+ /**
87
+ * Classify a single inbound message. Returns "correction" or "directive" when a
88
+ * cue matches, else null. Corrections are checked first because a correction is
89
+ * the more specific (and more consequential) signal. Very long messages are
90
+ * skipped — standing instructions and corrections are terse.
91
+ */
92
+ export function classifyMessage(text: string): MessageClass {
93
+ const t = text.trim();
94
+ if (t.length === 0 || t.length > 500) return null;
95
+ if (CORRECTION_PATTERNS.some((re) => re.test(t))) return "correction";
96
+ if (DIRECTIVE_PATTERNS.some((re) => re.test(t))) return "directive";
97
+ return null;
98
+ }
99
+
100
+ /**
101
+ * The trust tier for something the human typed in this chat. A DM is the
102
+ * operator speaking for themselves; anything multi-party — or a grammar that
103
+ * cannot tell, which fails closed — is `group_chat`.
104
+ */
105
+ function trustForChat(chatKey: string): MemoryTrust {
106
+ return chatScope(chatKey) === "dm" ? "operator" : "group_chat";
107
+ }
108
+
109
+ /**
110
+ * How much of two claims' wording must coincide before the tap calls the
111
+ * second one a restatement of the first. Jaccard over the word sets, so
112
+ * word order and punctuation don't matter.
113
+ *
114
+ * A threshold is needed because the tap files every directive under the
115
+ * *same* subject, and the store's FTS probe ORs a claim's terms: "from now
116
+ * on, always use ripgrep" and "from now on, never use emoji" both match it
117
+ * on `from`/`now`/`use`. Without this, the first directive Talon ever heard
118
+ * would swallow every later one. Set high on purpose — the case this
119
+ * exists for is the same sentence typed twice.
120
+ */
121
+ const DUPLICATE_OVERLAP = 0.8;
122
+
123
+ /** The word set of a claim, lowercased, punctuation dropped. */
124
+ function wordSet(text: string): Set<string> {
125
+ return new Set(
126
+ text
127
+ .toLowerCase()
128
+ .split(/[^\p{L}\p{N}]+/u)
129
+ .filter(Boolean),
130
+ );
131
+ }
132
+
133
+ /** Jaccard similarity of two claims' word sets, 0..1. */
134
+ function overlap(a: Set<string>, b: Set<string>): number {
135
+ if (a.size === 0 || b.size === 0) return 0;
136
+ let shared = 0;
137
+ for (const w of a) if (b.has(w)) shared += 1;
138
+ return shared / (a.size + b.size - shared);
139
+ }
140
+
141
+ /**
142
+ * Write one tapped claim, unless the store already holds a restatement of it
143
+ * under the same kind and subject. Returns true when a row was inserted.
144
+ *
145
+ * The store's FTS probe is the cheap prefilter — live rows only, right kind
146
+ * and subject, best textual match first — and `DUPLICATE_OVERLAP` is the
147
+ * decision.
148
+ */
149
+ function storeClaim(
150
+ kind: MemoryKind,
151
+ subject: string,
152
+ text: string,
153
+ trust: MemoryTrust,
154
+ source: MemorySource,
155
+ speaker: string,
156
+ ): boolean {
157
+ const words = wordSet(text);
158
+ const restated = findSimilarMemories(kind, subject, text).some(
159
+ (row) => overlap(words, wordSet(row.text)) >= DUPLICATE_OVERLAP,
160
+ );
161
+ if (restated) return false;
162
+ const { id } = assertMemory({ kind, subject, text, trust, source });
163
+ log("memory", `tap: [${kind}] ${subject} from ${speaker} #${id}`);
164
+ return true;
165
+ }
166
+
167
+ /**
168
+ * Feed one inbound message to the memory store if it reads as a directive or a
169
+ * correction. Returns the class recognised, whether or not a row was written —
170
+ * a group-chat directive is classified and deliberately dropped.
171
+ *
172
+ * Never throws: the tap runs beside a turn, not inside it, so a store failure
173
+ * costs one warning and nothing else.
174
+ */
175
+ export function recordMessageSignal(opts: {
176
+ readonly text: string;
177
+ readonly chatKey: string;
178
+ readonly actor?: string;
179
+ }): MessageClass {
180
+ const cls = classifyMessage(opts.text);
181
+ if (cls === null) return null;
182
+ const text = opts.text.trim();
183
+ const trust = trustForChat(opts.chatKey);
184
+ const source: MemorySource = { chat: opts.chatKey, actor: TAP_ACTOR };
185
+ const speaker = opts.actor ?? "user";
186
+ try {
187
+ if (cls === "directive") {
188
+ // A standing instruction only counts when the operator gave it
189
+ // directly — anyone in a group could otherwise plant one.
190
+ if (trust !== "operator") return cls;
191
+ storeClaim("directive", DIRECTIVE_SUBJECT, text, trust, source, speaker);
192
+ } else {
193
+ storeClaim("episode", CORRECTION_SUBJECT, text, trust, source, speaker);
194
+ }
195
+ } catch (err) {
196
+ logWarn("memory", `tap failed to record ${cls}: ${String(err)}`);
197
+ }
198
+ return cls;
199
+ }
@@ -0,0 +1,222 @@
1
+ /**
2
+ * **Turn retrieval** — the per-turn tier of docs/memory-persona-plan.md
3
+ * §3.4, behind `TALON_MEMORY_STORE`.
4
+ *
5
+ * Retrieval has two tiers and the boundary between them is a
6
+ * prompt-cache invariant (plan §3.6):
7
+ *
8
+ * - **Core view** (`core-view.ts`) — computed once per session build,
9
+ * lives in `staticText`, frozen for the session.
10
+ * - **Turn retrieval** (this module) — keyed to the *incoming
11
+ * message*, resolved once per turn by the Weaver and injected into
12
+ * the **user turn** by `formatUserPrompt`. It never enters
13
+ * `prepareSystemPrompt()` and nothing here may ever call
14
+ * `notifyPromptInputsChanged()` — a per-turn invalidation would
15
+ * force a full-prompt cache write on every live session, every
16
+ * turn, which is the single most expensive mistake in the plan.
17
+ *
18
+ * This is the seam #639 deleted, rebuilt without the divergence that
19
+ * justified deleting it. The old `retrievedMemory` field was read by
20
+ * two backends out of six and silently dropped by the rest; now there
21
+ * is exactly ONE consumer — `backend/runtime/prompt/prompt-format.ts` — that
22
+ * every backend already calls, so a backend cannot forget to inject it
23
+ * without also losing its time tag and `msg_id` framing.
24
+ *
25
+ * **Trust policy (plan §5, #373).** Only `operator` and `agent` rows
26
+ * are ever auto-injected. `user_claim` and `group_chat` rows are
27
+ * reachable only through the explicit `recall` tool: an auto-injected
28
+ * low-trust row is a permanent prompt injection that anyone in a group
29
+ * chat can plant. `reflection` rows are excluded too — the diary is the
30
+ * persona layer and is structurally never a fact source (plan §3.5).
31
+ *
32
+ * **Fail closed.** A broken store must never block chat delivery: any
33
+ * error logs once per process and the turn runs with no injected
34
+ * memory, exactly as if the flag were off.
35
+ */
36
+
37
+ import {
38
+ formatMemory,
39
+ searchMemories,
40
+ touchMemory,
41
+ type MemoryRow,
42
+ type MemoryTrust,
43
+ } from "../../storage/memory.js";
44
+ import { logDebug, logWarn } from "../../util/log.js";
45
+ import { memoryStoreEnabled } from "./flag.js";
46
+
47
+ // ── Tunables ────────────────────────────────────────────────────────────────
48
+
49
+ /**
50
+ * Hard cap on the injected block, header excluded. Whole rows only —
51
+ * a truncated memory is a misquoted memory, and a half-line is worse
52
+ * than a missing one. Paid on *every* turn (unlike the core view,
53
+ * which is paid once per session and then cached), so it stays small.
54
+ */
55
+ export const TURN_MEMORY_MAX_CHARS = 3_000;
56
+
57
+ /**
58
+ * Candidates pulled from FTS before re-ranking. The trust filter runs
59
+ * *after* this cut, so a query whose twenty best matches are all
60
+ * low-trust injects nothing — which is the conservative direction: an
61
+ * untrusted row must never be promoted into the prompt just because
62
+ * nothing trusted matched.
63
+ */
64
+ const CANDIDATE_LIMIT = 20;
65
+
66
+ /**
67
+ * Trust tiers eligible for automatic injection (plan §5). Everything
68
+ * else stays pull-only, through the explicit `recall` tool.
69
+ */
70
+ const AUTO_INJECT_TRUST: ReadonlySet<MemoryTrust> = new Set([
71
+ "operator",
72
+ "agent",
73
+ ]);
74
+
75
+ /** `hitCount` at which the affinity term saturates. */
76
+ const HIT_SATURATION = 20;
77
+
78
+ /** Age at which the recency term reaches zero. */
79
+ const RECENCY_HORIZON_MS = 30 * 24 * 60 * 60 * 1000;
80
+
81
+ /**
82
+ * Re-ranking weights. bm25 order (as the store returned it) is the
83
+ * strongest signal — it is the only one that knows what was *asked* —
84
+ * and the three store-side signals refine it rather than overturn it:
85
+ * salience is the curator's judgement, `hitCount` is the feedback loop
86
+ * this module itself feeds, and recency breaks ties towards the live
87
+ * world. They sum to 1 so a score reads as a 0..1 fraction.
88
+ */
89
+ const W_RELEVANCE = 0.4;
90
+ const W_SALIENCE = 0.3;
91
+ const W_AFFINITY = 0.2;
92
+ const W_RECENCY = 0.1;
93
+
94
+ // ── Types ───────────────────────────────────────────────────────────────────
95
+
96
+ /** What one turn's retrieval produced. `undefined` means "inject nothing". */
97
+ export type TurnMemory = {
98
+ /** The rendered block, one row per line. Never truncated mid-row. */
99
+ text: string;
100
+ /** How many rows it carries. */
101
+ rows: number;
102
+ /** `text.length` — the per-turn cost, recorded as `turn.memory_chars`. */
103
+ chars: number;
104
+ };
105
+
106
+ /** One turn's retrieval context: the raw inbound message and its chat. */
107
+ export type TurnRetrievalInput = {
108
+ chatId: string;
109
+ /** The user's text, as the Weaver received it (no prompt framing yet). */
110
+ text: string;
111
+ isGroup: boolean;
112
+ };
113
+
114
+ // ── Ranking ─────────────────────────────────────────────────────────────────
115
+
116
+ /** Eligible for auto-injection: trusted tier, and not the diary. */
117
+ function injectable(row: MemoryRow): boolean {
118
+ return AUTO_INJECT_TRUST.has(row.trust) && row.kind !== "reflection";
119
+ }
120
+
121
+ /**
122
+ * Blend the store's bm25 order with the row's own standing. `index` is
123
+ * the row's position in the bm25 result, best first.
124
+ */
125
+ function score(row: MemoryRow, index: number, total: number, now: number) {
126
+ const relevance = total > 1 ? 1 - index / (total - 1) : 1;
127
+ const affinity = Math.min(
128
+ 1,
129
+ Math.log1p(row.hitCount) / Math.log1p(HIT_SATURATION),
130
+ );
131
+ const age = Math.max(0, now - row.lastSeenAt);
132
+ const recency = Math.max(0, 1 - age / RECENCY_HORIZON_MS);
133
+ return (
134
+ W_RELEVANCE * relevance +
135
+ W_SALIENCE * row.salience +
136
+ W_AFFINITY * affinity +
137
+ W_RECENCY * recency
138
+ );
139
+ }
140
+
141
+ /** Trust-filtered candidates, best first. */
142
+ function rank(hits: MemoryRow[], now: number): MemoryRow[] {
143
+ const scored = hits
144
+ .map((row, index) => ({ row, index }))
145
+ .filter(({ row }) => injectable(row))
146
+ .map(({ row, index }) => ({
147
+ row,
148
+ score: score(row, index, hits.length, now),
149
+ }));
150
+ scored.sort((a, b) => b.score - a.score || a.row.id - b.row.id);
151
+ return scored.map((s) => s.row);
152
+ }
153
+
154
+ /** Fill the budget with whole rows, in rank order. */
155
+ function fill(rows: MemoryRow[]): { lines: string[]; taken: MemoryRow[] } {
156
+ const lines: string[] = [];
157
+ const taken: MemoryRow[] = [];
158
+ let used = 0;
159
+ for (const row of rows) {
160
+ const line = formatMemory(row);
161
+ // +1 for the newline that joins this line to the previous one.
162
+ const cost = line.length + (lines.length > 0 ? 1 : 0);
163
+ if (used + cost > TURN_MEMORY_MAX_CHARS) break;
164
+ lines.push(line);
165
+ taken.push(row);
166
+ used += cost;
167
+ }
168
+ return { lines, taken };
169
+ }
170
+
171
+ // ── Entry point ─────────────────────────────────────────────────────────────
172
+
173
+ /** One warning per process — a broken store must not spam every turn. */
174
+ let warned = false;
175
+
176
+ function failClosed(err: unknown): undefined {
177
+ if (!warned) {
178
+ warned = true;
179
+ logWarn(
180
+ "dispatcher",
181
+ "memory turn retrieval failed (turns run without it): " +
182
+ (err instanceof Error ? err.message : String(err)),
183
+ );
184
+ }
185
+ return undefined;
186
+ }
187
+
188
+ /**
189
+ * Retrieve the memory block for one chat turn, or `undefined` when
190
+ * there is nothing to inject (flag off, no match, or any failure).
191
+ *
192
+ * Injected rows are `touch`ed — that is the ranking feedback loop:
193
+ * a row that keeps getting recalled climbs, one that never matches
194
+ * stays where it is.
195
+ */
196
+ export function retrieveForTurn(
197
+ input: TurnRetrievalInput,
198
+ ): TurnMemory | undefined {
199
+ if (!memoryStoreEnabled()) return undefined;
200
+ try {
201
+ // `match: "any"` — the query is a sentence the user wrote as a
202
+ // message, not as a search; AND-ing its every word would match
203
+ // nothing. bm25 over the OR-ed terms does the ranking.
204
+ const hits = searchMemories(input.text, {
205
+ limit: CANDIDATE_LIMIT,
206
+ match: "any",
207
+ });
208
+ if (hits.length === 0) return undefined;
209
+ const { lines, taken } = fill(rank(hits, Date.now()));
210
+ if (taken.length === 0) return undefined;
211
+ for (const row of taken) touchMemory(row.id);
212
+ const text = lines.join("\n");
213
+ logDebug(
214
+ "dispatcher",
215
+ `memory turn retrieval chat=${input.chatId}${input.isGroup ? " (group)" : ""}: ` +
216
+ `${taken.length}/${hits.length} row(s), ${text.length} chars`,
217
+ );
218
+ return { text, rows: taken.length, chars: text.length };
219
+ } catch (err) {
220
+ return failClosed(err);
221
+ }
222
+ }
@@ -30,7 +30,7 @@
30
30
  * 6. Plugin additions plugin.systemPrompt() contributions
31
31
  * (7. Delivery contract — appended by the backend as its suffix,
32
32
  * AFTER plugins, so it is the last thing the model reads.
33
- * See backend/shared/delivery-contract.ts.)
33
+ * See backend/runtime/prompt/delivery-contract.ts.)
34
34
  *
35
35
  * DYNAMIC
36
36
  * 1. Daily-memory pointer prompts/system/daily-memory.md
@@ -52,7 +52,7 @@
52
52
  * ## Deliberate omissions
53
53
  *
54
54
  * No "Current Date & Time" section: every user message already
55
- * carries a `[YYYY-MM-DD HH:MM:SS]` tag (see shared/prompt-format),
55
+ * carries a `[YYYY-MM-DD HH:MM:SS]` tag (see backend/runtime/prompt/prompt-format),
56
56
  * the daily-memory pointer names today's file, and the `check_time`
57
57
  * tool covers timezone queries. A minute-precision timestamp here was
58
58
  * the single biggest cache-buster — it guaranteed every rebuild
@@ -72,7 +72,6 @@ import { renderStickerLibraryPrompt } from "../../storage/stickers.js";
72
72
  import { recordHistogram } from "../../storage/metrics.js";
73
73
  import { renderCoreView } from "../memory/core-view.js";
74
74
  import { memoryStoreEnabled } from "../memory/flag.js";
75
- import { getSoul } from "../soul/service.js";
76
75
 
77
76
  // ── Types ───────────────────────────────────────────────────────────────────
78
77
 
@@ -213,15 +212,6 @@ export function assembleSystemPrompt(
213
212
  loaded.push("identity");
214
213
  }
215
214
 
216
- // 1.5. Soul — the compiled identity surface, when the soul is enabled.
217
- // Off by default (TALON_SOUL_ENABLED); inert deployments add nothing.
218
- // Selection-based and verbatim, so it never injects invented self-text.
219
- const soulSection = getSoul().renderPromptSection();
220
- if (soulSection) {
221
- staticParts.push(soulSection);
222
- loaded.push("soul");
223
- }
224
-
225
215
  // 2. Core behaviour — custom.md replaces base.md wholesale when present.
226
216
  const custom = readOptionalFile(resolve(promptDir, "custom.md"));
227
217
  const basePrompt = readOptionalFile(resolve(promptDir, "base.md"));
@@ -8,9 +8,9 @@
8
8
  * - `workspace-listing` — lazy workspace tree for the dynamic tail
9
9
  *
10
10
  * The per-session freezing/caching of assembled prompts lives in
11
- * `backend/shared/system-prompt.ts` (it is a backend concern); the
11
+ * `backend/runtime/prompt/system-prompt.ts` (it is a backend concern); the
12
12
  * delivery-contract suffix builders live in
13
- * `backend/shared/delivery-contract.ts`.
13
+ * `backend/runtime/prompt/delivery-contract.ts`.
14
14
  */
15
15
 
16
16
  export { loadSystemTemplate } from "./templates.js";
@@ -4,7 +4,7 @@
4
4
  * skill toggle).
5
5
  *
6
6
  * Whoever caches assembled prompts registers a hook here (today: the
7
- * per-session snapshot store in backend/shared/system-prompt.ts, at its
7
+ * per-session snapshot store in backend/runtime/prompt/system-prompt.ts, at its
8
8
  * module load). Core and frontends call `notifyPromptInputsChanged()`
9
9
  * and never import the cache — the dependency points backend → core,
10
10
  * as the layer rule requires.
@@ -2,7 +2,7 @@
2
2
  * Task table — public surface.
3
3
  *
4
4
  * See table.ts for the registry and types.ts for the vocabulary. Wiring
5
- * points: weaver (turns), heartbeat/agent, dream, background/job-oneshot
5
+ * points: weaver (turns), heartbeat/agent, dream, background/cron/job-oneshot
6
6
  * (isolated cron/trigger jobs); read surfaces: gateway `GET /tasks` +
7
7
  * `POST /tasks/kill`, CLI `talon ps` / `talon kill`.
8
8
  */
@@ -16,6 +16,7 @@ import { stickerTools } from "./stickers.js";
16
16
  import { schedulingTools } from "./scheduling.js";
17
17
  import { triggerTools } from "./triggers.js";
18
18
  import { goalTools } from "./goals.js";
19
+ import { memoryTools } from "./memory.js";
19
20
  import { scriptTools } from "./scripts.js";
20
21
  import { skillTools } from "./skills.js";
21
22
  import { webTools } from "./web.js";
@@ -38,6 +39,7 @@ export const ALL_TOOLS: readonly ToolDefinition[] = [
38
39
  ...schedulingTools,
39
40
  ...triggerTools,
40
41
  ...goalTools,
42
+ ...memoryTools,
41
43
  ...scriptTools,
42
44
  ...skillTools,
43
45
  ...webTools,
@@ -0,0 +1,128 @@
1
+ /**
2
+ * Memory tools — one typed claim at a time, in band.
3
+ *
4
+ * `remember` / `recall` / `forget` are the write path of
5
+ * docs/memory-persona-plan.md §3.2: a claim costs ~50 tokens and a
6
+ * transaction instead of a 23 KB file rewrite mid-conversation, and a
7
+ * restatement is offered back as a supersede candidate rather than
8
+ * silently becoming a second row.
9
+ */
10
+
11
+ import { z } from "zod";
12
+ import type { ToolDefinition } from "./types.js";
13
+ import type { MemoryKind } from "../../storage/memory.js";
14
+
15
+ /**
16
+ * The store's kinds, spelled out here rather than imported.
17
+ * `core/tools/` holds pure definitions; importing `storage/memory.js`
18
+ * for one constant would pull SQLite (and `db.ts`'s top-level await)
19
+ * into every backend that imports the tool registry. The `MemoryKind`
20
+ * annotation is the compile-time link, and `memory-actions.test.ts`
21
+ * asserts this list still equals `MEMORY_KINDS` exactly.
22
+ */
23
+ const MEMORY_KIND_NAMES: readonly [MemoryKind, ...MemoryKind[]] = [
24
+ "directive",
25
+ "fact",
26
+ "state",
27
+ "episode",
28
+ "relationship",
29
+ "reflection",
30
+ ];
31
+
32
+ const kindSchema = z
33
+ .enum(MEMORY_KIND_NAMES)
34
+ .describe(
35
+ "directive = standing instruction; fact = durable and supersedable; state = keyed, replaced on write; episode = dated, decays; relationship = how a person works; reflection = your own diary, never a fact source",
36
+ );
37
+
38
+ export const memoryTools: ToolDefinition[] = [
39
+ {
40
+ name: "remember",
41
+ description: `Store one durable claim in long-term memory. Cheap — prefer it to editing memory files.
42
+
43
+ One claim per call, written as a standalone sentence that will still parse months from now.
44
+ If a near-duplicate already exists the call is REFUSED and lists the matching rows: re-issue with replace_id=<that id> to fold your wording into it (the old row stays readable), or force=true only when the new claim genuinely stands beside the old one.
45
+ subject is who/what the claim is about; it defaults to this chat for episode and relationship. state also needs a key (e.g. "release.status") and replaces that key's live row.`,
46
+ schema: {
47
+ kind: kindSchema,
48
+ text: z
49
+ .string()
50
+ .describe(
51
+ "The claim itself, self-contained and specific (max 4000 chars)",
52
+ ),
53
+ subject: z
54
+ .string()
55
+ .optional()
56
+ .describe(
57
+ "Who or what this is about — a person, a project, a topic. Required except for episode/relationship, which default to this chat.",
58
+ ),
59
+ key: z
60
+ .string()
61
+ .optional()
62
+ .describe(
63
+ 'Required for kind="state": lowercase dotted namespace (e.g. "heartbeat.health"). Writing a key replaces its live row.',
64
+ ),
65
+ confidence: z
66
+ .number()
67
+ .min(0)
68
+ .max(1)
69
+ .optional()
70
+ .describe("How sure you are, 0–1 (default 1)"),
71
+ replace_id: z
72
+ .number()
73
+ .int()
74
+ .positive()
75
+ .optional()
76
+ .describe(
77
+ "Supersede this existing memory with the new text instead of adding a row — the answer to a near-duplicate refusal",
78
+ ),
79
+ force: z
80
+ .boolean()
81
+ .optional()
82
+ .describe(
83
+ "Store anyway despite a near-duplicate. Only when the claims really are distinct.",
84
+ ),
85
+ },
86
+ execute: (params, bridge) => bridge("remember", params),
87
+ tag: "memory",
88
+ },
89
+
90
+ {
91
+ name: "recall",
92
+ description: `Search long-term memory for stored claims — full-text, best match first.
93
+
94
+ Use it when you need more than the memory already in this prompt, before asking the user to repeat something. Returns lines prefixed with an id you can pass to remember(replace_id) or forget.`,
95
+ schema: {
96
+ query: z
97
+ .string()
98
+ .describe("Words to search for — names, topics, distinctive phrases"),
99
+ kind: kindSchema.optional().describe("Restrict to one kind"),
100
+ limit: z
101
+ .number()
102
+ .int()
103
+ .positive()
104
+ .optional()
105
+ .describe("Max rows to return (default and max 20)"),
106
+ },
107
+ execute: (params, bridge) => bridge("recall", params),
108
+ tag: "memory",
109
+ },
110
+
111
+ {
112
+ name: "forget",
113
+ description: `Retire a stored memory by id. It goes to the graveyard — readable and revertible, not erased.
114
+
115
+ The reason is required and is kept with the row. Use this for claims that are wrong or that the user asked you to drop; to correct a claim that is merely out of date, use remember with replace_id so the correction supersedes it.
116
+ A claim can only be retired from a context at least as trusted as the one that recorded it: operator memories are never droppable this way, and a group chat cannot drop what was learned in a DM.`,
117
+ schema: {
118
+ id: z.number().int().positive().describe("Memory id, as shown by recall"),
119
+ reason: z
120
+ .string()
121
+ .describe(
122
+ "Why it is being dropped — required, stored in the audit log",
123
+ ),
124
+ },
125
+ execute: (params, bridge) => bridge("forget", params),
126
+ tag: "memory",
127
+ },
128
+ ];
@@ -22,6 +22,7 @@ export type ToolTag =
22
22
  | "scheduling"
23
23
  | "triggers"
24
24
  | "goals"
25
+ | "memory"
25
26
  | "scripts"
26
27
  | "skills"
27
28
  | "web"
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Per-turn CPU accounting — how much of a turn the daemon itself spends
3
+ * on a CPU, as opposed to waiting on a model or a chat platform.
4
+ *
5
+ * This is the number Phase 0's kill criterion is written against
6
+ * (docs/ts-migration-plan.md): the TS control plane's share of turn
7
+ * latency is `turn.cpu_ms / response_latency_ms`, and if that share is
8
+ * small then a port buys latency nothing. `turn.stream_ms` is the wall
9
+ * clock over the identical bracket, so the two divide cleanly.
10
+ *
11
+ * `process.cpuUsage()` is process-wide, not per-turn: with concurrent
12
+ * turns each one's delta includes the others' work. That is the honest
13
+ * upper bound for "what the daemon costs while a turn is in flight", and
14
+ * it is the direction that matters — a small number stays small.
15
+ */
16
+
17
+ import { recordHistogram } from "../../storage/metrics.js";
18
+
19
+ /**
20
+ * Bracket a turn's backend stream. Returns the stop function; calling it
21
+ * records `turn.cpu_ms` (user + system, milliseconds).
22
+ */
23
+ export function startTurnCpu(): () => void {
24
+ const startedAt = process.cpuUsage();
25
+ return () => {
26
+ const used = process.cpuUsage(startedAt);
27
+ recordHistogram(
28
+ "turn.cpu_ms",
29
+ Math.round((used.user + used.system) / 1000),
30
+ );
31
+ };
32
+ }