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
@@ -1,80 +0,0 @@
1
- /**
2
- * Soul Kernel — Talon's own embedder.
3
- *
4
- * We own this rather than borrowing MemPalace's: the soul should not depend on
5
- * another subsystem's model lifecycle, and identity geometry deserves a fixed,
6
- * versioned function we control. It is deterministic, dependency-free, and
7
- * model-free — a strong lexical encoder, not a neural one — so it can run
8
- * anywhere, forever, without weights or a runtime.
9
- *
10
- * Construction (richer than plain n-gram hashing):
11
- * - word unigrams and bigrams capture phrasing,
12
- * - character 3/4/5-grams capture morphology and typos (paraphrase robustness),
13
- * - sublinear term weighting (1 + log count) damps repetition,
14
- * - signed feature hashing into a wide space limits collision bias,
15
- * - L2 normalization makes cosine the natural metric.
16
- *
17
- * The Embedder interface is the seam: if we ever want a real sentence-transformer
18
- * (e.g. ONNX, local), it drops in behind the same interface with zero changes
19
- * upstream. Until then, this is the canonical embedder for the kernel.
20
- */
21
-
22
- import { createHash } from "node:crypto";
23
- import { normalize, type Embedder } from "./embedder.js";
24
-
25
- const TOKEN_RE = /[a-z0-9]+/g;
26
-
27
- export interface TalonEmbedderOptions {
28
- readonly dim?: number;
29
- readonly charNgrams?: readonly number[];
30
- readonly wordBigrams?: boolean;
31
- }
32
-
33
- export class TalonEmbedder implements Embedder {
34
- readonly dim: number;
35
- private readonly charNgrams: readonly number[];
36
- private readonly wordBigrams: boolean;
37
-
38
- constructor(opts: TalonEmbedderOptions = {}) {
39
- this.dim = opts.dim ?? 1024;
40
- this.charNgrams = opts.charNgrams ?? [3, 4, 5];
41
- this.wordBigrams = opts.wordBigrams ?? true;
42
- }
43
-
44
- async embed(texts: readonly string[]): Promise<number[][]> {
45
- return texts.map((t) => this.embedOne(t));
46
- }
47
-
48
- /** Synchronous single-text embedding — handy for internal hot paths. */
49
- embedOne(text: string): number[] {
50
- const counts = new Map<string, number>();
51
- const bump = (feature: string): void => {
52
- counts.set(feature, (counts.get(feature) ?? 0) + 1);
53
- };
54
-
55
- const lower = text.toLowerCase();
56
- const words = lower.match(TOKEN_RE) ?? [];
57
- for (const w of words) bump(`w:${w}`);
58
- if (this.wordBigrams) {
59
- for (let i = 0; i + 1 < words.length; i++) {
60
- bump(`b:${words[i]}_${words[i + 1]}`);
61
- }
62
- }
63
- const stream = ` ${words.join(" ")} `;
64
- for (const n of this.charNgrams) {
65
- for (let i = 0; i + n <= stream.length; i++) {
66
- bump(`c${n}:${stream.slice(i, i + n)}`);
67
- }
68
- }
69
-
70
- const vec = Array.from({ length: this.dim }, () => 0);
71
- for (const [feature, count] of counts) {
72
- const h = createHash("md5").update(feature).digest();
73
- const idx = ((h[0]! << 16) | (h[1]! << 8) | h[2]!) % this.dim;
74
- const sign = (h[3]! & 1) === 0 ? 1 : -1;
75
- const weight = 1 + Math.log(count); // sublinear term weighting
76
- vec[idx]! += sign * weight;
77
- }
78
- return normalize(vec);
79
- }
80
- }
@@ -1,199 +0,0 @@
1
- /**
2
- * Soul Kernel — runtime taps.
3
- *
4
- * The thin glue that turns live runtime events into kernel signals. The kernel
5
- * itself never reads a transcript; it consumes *structured* events the harness
6
- * already produces. This module is where those events are recognized and handed
7
- * to {@link SoulService}.
8
- *
9
- * Everything here is gated on the soul being enabled (`getSoul().enabled`). When
10
- * it's off, every function is a cheap no-op and nothing is tracked or allocated
11
- * beyond the empty maps — so a deployment without a soul behaves byte-identically.
12
- *
13
- * Two responsibilities:
14
- * 1. Remember which messages Talon *itself* sent, so a later reaction update
15
- * can be attributed to one of our own messages. A reaction to Talon is
16
- * feedback about Talon; a reaction to someone else's message is not.
17
- * 2. Classify inbound user text that is addressed to Talon into directives and
18
- * corrections, using high-precision mechanical heuristics (no model).
19
- */
20
-
21
- import { getSoul } from "./service.js";
22
- import { log } from "../../util/log.js";
23
-
24
- // ── Bot-message memory ───────────────────────────────────────────────────────
25
-
26
- /**
27
- * Per-chat bounded ring of recently-sent bot message IDs. Telegram's
28
- * `message_reaction` update tells us *which* message was reacted to but not who
29
- * authored it, so we keep a small FIFO of our own outgoing IDs and attribute a
30
- * reaction only when it lands on one of them. Bounded so a long-lived process
31
- * never grows this without limit.
32
- */
33
- const RING_PER_CHAT = 200;
34
- const botMessages = new Map<string, number[]>();
35
-
36
- /**
37
- * The action names that produce a *new* outgoing bot message worth tracking.
38
- * Deliberately excludes `react`, `edit_message`, `pin_message`, etc.: those
39
- * either return someone else's message id or mutate an existing one, so crediting
40
- * a reaction to them would be wrong.
41
- */
42
- export const BOT_MESSAGE_ACTIONS: ReadonlySet<string> = new Set([
43
- "send_message",
44
- "reply_to",
45
- "send_message_with_buttons",
46
- "send_file",
47
- "send_photo",
48
- "send_video",
49
- "send_animation",
50
- "send_voice",
51
- "send_audio",
52
- "send_sticker",
53
- "send_poll",
54
- "send_location",
55
- "send_contact",
56
- "send_dice",
57
- "copy_message",
58
- "forward_message",
59
- ]);
60
-
61
- /** Record that Talon sent a message, so a later reaction can be attributed. */
62
- export function noteBotMessage(chatId: string | number, msgId: number): void {
63
- if (!getSoul().enabled) return;
64
- if (!Number.isFinite(msgId) || msgId <= 0) return;
65
- const key = String(chatId);
66
- const ring = botMessages.get(key) ?? [];
67
- if (ring.includes(msgId)) return;
68
- ring.push(msgId);
69
- if (ring.length > RING_PER_CHAT) ring.shift();
70
- botMessages.set(key, ring);
71
- }
72
-
73
- /** True when `msgId` is one of Talon's own recently-sent messages in this chat. */
74
- export function isBotMessage(chatId: string | number, msgId: number): boolean {
75
- return botMessages.get(String(chatId))?.includes(msgId) ?? false;
76
- }
77
-
78
- /** Drop all tracked bot messages (tests / explicit reset). */
79
- export function resetBotMessages(): void {
80
- botMessages.clear();
81
- }
82
-
83
- // ── Reaction tap ─────────────────────────────────────────────────────────────
84
-
85
- /** A `ReactionType`-shaped item from Telegram's reaction arrays. */
86
- export interface ReactionTypeLike {
87
- readonly type: string;
88
- readonly emoji?: string;
89
- }
90
-
91
- /**
92
- * The emojis present in `next` but not in `prev` — i.e. reactions just *added*.
93
- * Custom and paid reactions (no `.emoji`) are ignored; only standard emoji carry
94
- * a valence the kernel understands.
95
- */
96
- export function newlyAddedEmojis(
97
- prev: readonly ReactionTypeLike[],
98
- next: readonly ReactionTypeLike[],
99
- ): string[] {
100
- const before = new Set(
101
- prev.filter((r) => r.type === "emoji" && r.emoji).map((r) => r.emoji),
102
- );
103
- return next
104
- .filter((r) => r.type === "emoji" && r.emoji && !before.has(r.emoji))
105
- .map((r) => r.emoji as string);
106
- }
107
-
108
- /**
109
- * A user added reactions to one of Talon's messages → reinforce/penalize the
110
- * values that were on stage when that message was produced. No-op unless the
111
- * soul is on, at least one emoji was added, and the target is a tracked bot
112
- * message.
113
- */
114
- export function recordReactionToBot(
115
- chatId: string | number,
116
- msgId: number,
117
- addedEmojis: readonly string[],
118
- ): boolean {
119
- if (!getSoul().enabled) return false;
120
- if (addedEmojis.length === 0) return false;
121
- if (!isBotMessage(chatId, msgId)) return false;
122
- for (const emoji of addedEmojis) getSoul().recordReaction(emoji);
123
- log("soul", `reaction ${addedEmojis.join(" ")} on bot msg ${msgId}`);
124
- return true;
125
- }
126
-
127
- // ── Directive / correction tap ───────────────────────────────────────────────
128
-
129
- /**
130
- * High-precision cues that a message is a *correction* of Talon's behavior. Kept
131
- * tight on purpose: a false positive penalizes whatever values were on stage, so
132
- * we would rather miss a soft correction than mislabel ordinary chat.
133
- */
134
- const CORRECTION_PATTERNS: readonly RegExp[] = [
135
- /^\s*(no|nope|nah)\b[\s,.!:-]/i,
136
- /\bthat'?s (wrong|incorrect|not (right|correct)|false)\b/i,
137
- /\b(you'?re|you are) wrong\b/i,
138
- /\bnot what i (asked|meant|wanted|said)\b/i,
139
- /\bnever (do|say) that( again)?\b/i,
140
- /\b(stop|quit) (doing|saying) that\b/i,
141
- /\bwrong[\s,.!]/i,
142
- /\byou (messed|screwed|fucked) (that|this|it)? ?up\b/i,
143
- ];
144
-
145
- /**
146
- * High-precision cues that a message is a *directive* about how to be — a
147
- * standing instruction rather than a one-off request. These are stored verbatim
148
- * as evidence, so again we favor precision over recall.
149
- */
150
- const DIRECTIVE_PATTERNS: readonly RegExp[] = [
151
- /\bfrom now on\b/i,
152
- /\bgoing forward\b/i,
153
- /\bin (the )?future\b/i,
154
- /\byou should (always|never)\b/i,
155
- /\b(always|never) (do|say|reply|respond|answer|use|be|call|check)\b/i,
156
- /\bi (want|need|'?d like) you to\b/i,
157
- /\bmake sure (you|to|that)\b/i,
158
- /\bremember to\b/i,
159
- ];
160
-
161
- export type MessageClass = "directive" | "correction" | null;
162
-
163
- /**
164
- * Classify a single inbound message. Returns "correction" or "directive" when a
165
- * cue matches, else null. Corrections are checked first because a correction is
166
- * the more specific (and more consequential) signal. Very long messages are
167
- * skipped — standing instructions and corrections are terse.
168
- */
169
- export function classifyMessage(text: string): MessageClass {
170
- const t = text.trim();
171
- if (t.length === 0 || t.length > 500) return null;
172
- if (CORRECTION_PATTERNS.some((re) => re.test(t))) return "correction";
173
- if (DIRECTIVE_PATTERNS.some((re) => re.test(t))) return "directive";
174
- return null;
175
- }
176
-
177
- /**
178
- * Feed an inbound message to the kernel if it is addressed to Talon and reads as
179
- * a directive or correction. Returns the class recorded (or null). No-op unless
180
- * the soul is on.
181
- */
182
- export function recordMessageSignal(opts: {
183
- readonly text: string;
184
- readonly actor?: string;
185
- readonly addressedToBot: boolean;
186
- }): MessageClass {
187
- if (!getSoul().enabled) return null;
188
- if (!opts.addressedToBot) return null;
189
- const cls = classifyMessage(opts.text);
190
- const text = opts.text.trim();
191
- if (cls === "correction") {
192
- getSoul().recordCorrection(text, opts.actor);
193
- log("soul", `correction recorded from ${opts.actor ?? "user"}`);
194
- } else if (cls === "directive") {
195
- getSoul().recordDirective(text, opts.actor);
196
- log("soul", `directive recorded from ${opts.actor ?? "user"}`);
197
- }
198
- return cls;
199
- }
@@ -1,298 +0,0 @@
1
- /**
2
- * Soul Kernel — core type system.
3
- *
4
- * The Soul is Talon's compiled identity: a content-addressed Merkle DAG of
5
- * typed nodes, layered with mutable "activation state" (salience, evidence
6
- * weights) that changes with experience. The reasoning model never *writes*
7
- * this structure — it is compiled mechanically from behavioral telemetry and a
8
- * frozen local embedder, then *read* by the model at runtime.
9
- *
10
- * The single most important design decision lives here: the split between
11
- * IMMUTABLE CONTENT and MUTABLE STATE.
12
- *
13
- * - Content — a node's semantic identity: its kind, its verbatim payload,
14
- * and the hashes of the nodes it structurally references. This
15
- * is what gets hashed; the hash IS the node's identity. Content
16
- * never changes — a change of content is, by definition, a new
17
- * node.
18
- *
19
- * - State — salience, accumulated evidence weight, activation counts,
20
- * timestamps. This churns every tick. It is keyed BY the content
21
- * hash and lives outside the Merkle structure, so reinforcing a
22
- * value a thousand times does not churn a single content hash.
23
- *
24
- * Consequence: the structural root hash versions *what Talon is made of*
25
- * (git-like), while salience is the "weather" layered on top. Rollback,
26
- * provenance, and dedup fall out of the content layer; learning and decay live
27
- * in the state layer.
28
- */
29
-
30
- /** A content address: lowercase hex sha-256, prefixed. e.g. "sha256:ab12…". */
31
- export type Hash = string & { readonly __brand: "SoulHash" };
32
-
33
- /** The kinds of node the kernel can hold. */
34
- export type NodeKind =
35
- | "evidence" // a verbatim ground-truth fragment — the atoms
36
- | "value" // an emergent cluster of evidence (a trait/value)
37
- | "theme" // a higher-order reflection over co-activating, coherent values
38
- | "spine" // an append-only developmental-narrative event
39
- | "reflex" // a compiled behavioral rule (trigger → guard → action)
40
- | "lens"; // a per-relationship refraction of identity
41
-
42
- // ── Node payloads (the hashed content) ───────────────────────────────────────
43
-
44
- /**
45
- * Where a piece of ground truth came from. Provenance is content, not metadata:
46
- * an evidence node's identity includes *who said it, when, and where*.
47
- */
48
- interface EvidenceSource {
49
- /** Origin channel: a chat correction, a logged event, a diary line, etc. */
50
- readonly origin:
51
- | "correction" // someone (usually Dylan) corrected behavior
52
- | "directive" // an explicit instruction about how to be
53
- | "event" // something that happened
54
- | "reaction" // an emoji / engagement signal rendered to text
55
- | "diary" // a self-written reflection (still ground truth: it was written)
56
- | "seed"; // hand-seeded at genesis from identity.md / About Me
57
- /** Free-form locator, e.g. "telegram:msg:4360" or "memory.md#about-me". */
58
- readonly ref?: string;
59
- /** Who produced the ground truth, if known (e.g. "dylan"). */
60
- readonly actor?: string;
61
- }
62
-
63
- /** A verbatim fragment of ground truth. The kernel never paraphrases these. */
64
- export interface EvidencePayload {
65
- readonly kind: "evidence";
66
- /** The exact text. Never generated, never rewritten. */
67
- readonly text: string;
68
- /** When the ground truth was observed (unix ms). Part of identity. */
69
- readonly observedAt: number;
70
- readonly source: EvidenceSource;
71
- }
72
-
73
- /**
74
- * An emergent value/trait. Not authored — discovered by clustering evidence
75
- * embeddings. Its "label" is the medoid: the single most central *real*
76
- * evidence fragment, never generated prose.
77
- */
78
- export interface ValuePayload {
79
- readonly kind: "value";
80
- /** Hashes of the evidence nodes in this cluster (sorted, deduped). */
81
- readonly members: readonly Hash[];
82
- /** The medoid evidence node — the representative real sentence. */
83
- readonly medoid: Hash;
84
- /**
85
- * Optional axis this value sits on, for tension detection. Discovered, not
86
- * authored — two values on the same axis with high embedding distance but
87
- * frequent co-activation form a tension edge.
88
- */
89
- readonly axis?: string;
90
- }
91
-
92
- /**
93
- * A higher-order reflection: an abstraction over values that are both
94
- * semantically coherent and frequently co-active. The soul's "higher-level
95
- * inferences over time" (Generative Agents). Structure is discovered
96
- * mechanically; the optional `insight` is the ONE place a single gated model
97
- * pass may write a natural-language label — it can never change which values are
98
- * grouped, and falls back to the medoid evidence when absent.
99
- */
100
- export interface ThemePayload {
101
- readonly kind: "theme";
102
- /** The value-node hashes this theme abstracts over (sorted, deduped). */
103
- readonly values: readonly Hash[];
104
- /** Representative real evidence fragment across all member values. */
105
- readonly medoid: Hash;
106
- /** Optional model-written label; absent ⇒ the medoid text is used. */
107
- readonly insight?: string;
108
- }
109
-
110
- /**
111
- * An append-only developmental-narrative event: the *causal story* of how
112
- * Talon became Talon. Never rewritten, only appended. Gives continuity of self
113
- * across resets and model swaps.
114
- */
115
- export interface SpinePayload {
116
- readonly kind: "spine";
117
- /** What happened, verbatim where possible. */
118
- readonly event: string;
119
- readonly at: number;
120
- /** Nodes this event created or reweighted — the causal links. */
121
- readonly affects: readonly Hash[];
122
- /** Prior spine node, forming the narrative chain (genesis = undefined). */
123
- readonly prev?: Hash;
124
- }
125
-
126
- /** Severity of a reflex when its guard trips. */
127
- export type ReflexSeverity = "block" | "warn" | "advise";
128
-
129
- /**
130
- * A compiled behavioral rule. The genuinely model-free, *enforceable* core:
131
- * load-bearing rules become `trigger → guard → action`, checked by the harness,
132
- * not merely hoped-for from the prompt. Trigger/guard reference a registry of
133
- * named predicates (see reflex.ts) so they stay pure data, not code.
134
- */
135
- export interface ReflexPayload {
136
- readonly kind: "reflex";
137
- /** Stable human name, e.g. "RULE-0-DELIVERY". */
138
- readonly name: string;
139
- /** Named predicate that arms the rule. */
140
- readonly trigger: string;
141
- /** Named predicate that, when true under trigger, fires the action. */
142
- readonly guard: string;
143
- /** What to do when it fires. */
144
- readonly action: string;
145
- readonly severity: ReflexSeverity;
146
- }
147
-
148
- /**
149
- * A per-relationship refraction. Identity is not global — it shows up
150
- * differently per interlocutor. A lens is a *selection + reweighting*, never
151
- * generated text: which values amplify, which reflexes tighten, which evidence
152
- * applies for this person.
153
- */
154
- export interface LensPayload {
155
- readonly kind: "lens";
156
- /** Subject identifier, e.g. "dylan". */
157
- readonly subject: string;
158
- /** Value-node hashes to amplify for this subject, with multipliers. */
159
- readonly amplify: readonly { readonly node: Hash; readonly factor: number }[];
160
- /** Evidence specific to this relationship. */
161
- readonly evidence: readonly Hash[];
162
- }
163
-
164
- export type NodePayload =
165
- | EvidencePayload
166
- | ValuePayload
167
- | ThemePayload
168
- | SpinePayload
169
- | ReflexPayload
170
- | LensPayload;
171
-
172
- /** A node = its hashed content. State lives separately (see ActivationState). */
173
- export interface SoulNode {
174
- readonly hash: Hash;
175
- readonly payload: NodePayload;
176
- }
177
-
178
- // ── Associative edges (mutable state, NOT hashed) ────────────────────────────
179
-
180
- export type EdgeKind =
181
- | "coactivation" // Hebbian: fired together in a successful interaction
182
- | "tension" // opposed values frequently co-active (navigated, not resolved)
183
- | "supersedes"; // one node mechanically overrode another
184
-
185
- export interface AssocEdge {
186
- readonly from: Hash;
187
- readonly to: Hash;
188
- readonly kind: EdgeKind;
189
- /** Learned weight (counts / EMA). Mutable — never part of any content hash. */
190
- weight: number;
191
- updatedAt: number;
192
- }
193
-
194
- // ── Activation state (mutable, keyed by node hash) ───────────────────────────
195
-
196
- /**
197
- * The "weather" over a node. Reinforced on activation, decayed by time. Drives
198
- * projection priority. Crucially separate from content so learning never churns
199
- * the Merkle structure.
200
- */
201
- export interface ActivationState {
202
- /** EMA salience in [0, ∞); decays toward 0 without activation. */
203
- salience: number;
204
- /**
205
- * Net evidence weight: positive when behavior expressing this node earns
206
- * positive signals, negative when it earns corrections. Drives confidence.
207
- */
208
- evidence: number;
209
- /** Number of times activated. */
210
- activations: number;
211
- /** Last activation (unix ms) — used by the decay clock. */
212
- lastActivatedAt: number;
213
- /**
214
- * Optional FSRS/DSR memory stability (ms-scale time constant). Present only
215
- * under adaptive forgetting; grows with successful recall so well-established
216
- * traits decay slowly and transients fade fast. When absent, decay is the
217
- * fixed-half-life exponential.
218
- */
219
- stability?: number;
220
- }
221
-
222
- // ── Commits (the version chain) ──────────────────────────────────────────────
223
-
224
- /**
225
- * A snapshot of identity. Mirrors git: a structural root plus a parent link,
226
- * plus a digest of the mutable state at commit time (so a rollback restores both
227
- * structure and weather). Commits form the identity's history.
228
- */
229
- export interface SoulCommit {
230
- /** Merkle root over all structural content at this version. */
231
- readonly root: Hash;
232
- /** Digest of the activation/edge state captured with this commit. */
233
- readonly stateDigest: Hash;
234
- readonly parent?: Hash;
235
- readonly at: number;
236
- /** Templated, human-readable summary of what changed (never model-written). */
237
- readonly summary: string;
238
- }
239
-
240
- /** Default kernel tuning. Pure numbers — the whole "compiler" is arithmetic. */
241
- export interface SoulConfig {
242
- /** Salience reinforcement increment per activation. */
243
- readonly reinforce: number;
244
- /** Multiplicative decay applied per decay-clock tick. */
245
- readonly decay: number;
246
- /** Half-life of the decay clock in ms (informational; see salience.ts). */
247
- readonly decayHalfLifeMs: number;
248
- /**
249
- * Per-kind decay half-life overrides (ms). Kinds without an entry fall back
250
- * to `decayHalfLifeMs`. `Number.POSITIVE_INFINITY` disables decay for a kind
251
- * entirely. The three node families have different temporal properties:
252
- * reflexes are deliberately seeded behavioral blockers — correct by
253
- * definition, they must never soften with disuse; evidence (corrections)
254
- * should fade naturally; spine causal links are context-specific and should
255
- * go stale faster than traits. See `halfLifeForKind`.
256
- */
257
- readonly decayHalfLifeByKindMs?: Partial<Record<NodeKind, number>>;
258
- /** Hebbian edge increment per co-activation. */
259
- readonly hebbIncrement: number;
260
- /** Cosine distance above which two embeddings are "different". */
261
- readonly clusterDistance: number;
262
- /** Distance from core centroid above which a mutation is "drift". */
263
- readonly driftThreshold: number;
264
- /** Token budget for the runtime projection surface. */
265
- readonly runtimeBudgetTokens: number;
266
- /**
267
- * When true, use FSRS/DSR adaptive forgetting (per-node stability that grows
268
- * with recall) instead of fixed-half-life exponential decay. Default false to
269
- * preserve the simple baseline.
270
- */
271
- readonly adaptiveForgetting?: boolean;
272
- }
273
-
274
- export const DEFAULT_SOUL_CONFIG: SoulConfig = {
275
- reinforce: 1,
276
- decay: 0.98,
277
- decayHalfLifeMs: 1000 * 60 * 60 * 24 * 7, // one week
278
- decayHalfLifeByKindMs: {
279
- reflex: Number.POSITIVE_INFINITY, // behavioral blockers never soften
280
- spine: 1000 * 60 * 60 * 24 * 3.5, // causal links go stale faster than traits
281
- },
282
- hebbIncrement: 1,
283
- clusterDistance: 0.35,
284
- driftThreshold: 0.55,
285
- runtimeBudgetTokens: 1200,
286
- };
287
-
288
- /**
289
- * Effective decay half-life for a node kind: the per-kind override when one is
290
- * configured, otherwise the base `decayHalfLifeMs`. `Number.POSITIVE_INFINITY`
291
- * means the kind never decays. Callers that don't know the kind (or operate on
292
- * mixed sets) may omit it and get the base rate.
293
- */
294
- export function halfLifeForKind(cfg: SoulConfig, kind?: NodeKind): number {
295
- const override =
296
- kind === undefined ? undefined : cfg.decayHalfLifeByKindMs?.[kind];
297
- return override ?? cfg.decayHalfLifeMs;
298
- }
@@ -1,83 +0,0 @@
1
- /**
2
- * Soul Kernel — learned valence.
3
- *
4
- * Hardcoding "👍 = +1" is exactly the kind of over-programming the soul should
5
- * outgrow. What a cue *means* is not a constant — it is whatever outcome reliably
6
- * follows it. So valence is learned: when an interaction's downstream engagement
7
- * resolves (the conversation continued, or died), every categorical cue that
8
- * preceded it (the emoji used, say) is nudged toward that outcome. Over time the
9
- * model discovers, from Talon's own data, that 👍 tends to precede continuation
10
- * and 👎 tends to precede silence — or whatever is actually true here, which may
11
- * differ from the prior.
12
- *
13
- * A weak prior (the old static table) seeds cold-start so early behavior is
14
- * sane, but evidence overrides it: the effective valence is a count-weighted
15
- * blend that converges to the observed mean as observations accumulate.
16
- */
17
-
18
- import { emojiValence } from "./signals.js";
19
-
20
- interface CueStat {
21
- sum: number;
22
- count: number;
23
- }
24
-
25
- export interface ValenceSnapshot {
26
- readonly priorWeight: number;
27
- readonly stats: readonly (readonly [string, CueStat])[];
28
- }
29
-
30
- export class ValenceModel {
31
- private readonly stats = new Map<string, CueStat>();
32
-
33
- /**
34
- * @param prior cold-start valence for an unseen cue (default: the old
35
- * emoji table — a fading prior, not a rule).
36
- * @param priorWeight pseudo-count for the prior; evidence dominates past it.
37
- */
38
- constructor(
39
- private readonly prior: (cue: string) => number = emojiValence,
40
- private readonly priorWeight = 2,
41
- ) {}
42
-
43
- /** Record that `cue` was followed by `outcome` (e.g. +1 continued, -1 died). */
44
- observe(cue: string, outcome: number): void {
45
- const s = this.stats.get(cue) ?? { sum: 0, count: 0 };
46
- s.sum += outcome;
47
- s.count += 1;
48
- this.stats.set(cue, s);
49
- }
50
-
51
- /**
52
- * Effective learned valence: a count-weighted blend of the prior and the
53
- * observed mean. With no observations it equals the prior; as observations
54
- * accumulate it converges to sum/count.
55
- */
56
- valence(cue: string): number {
57
- const s = this.stats.get(cue);
58
- const p = this.prior(cue);
59
- if (!s || s.count === 0) return p;
60
- return (this.priorWeight * p + s.sum) / (this.priorWeight + s.count);
61
- }
62
-
63
- /** How much evidence backs a cue (0 = pure prior). */
64
- confidence(cue: string): number {
65
- return this.stats.get(cue)?.count ?? 0;
66
- }
67
-
68
- snapshot(): ValenceSnapshot {
69
- return {
70
- priorWeight: this.priorWeight,
71
- stats: [...this.stats.entries()].map(([k, v]) => [k, { ...v }]),
72
- };
73
- }
74
-
75
- static restore(
76
- snap: ValenceSnapshot,
77
- prior: (cue: string) => number = emojiValence,
78
- ): ValenceModel {
79
- const model = new ValenceModel(prior, snap.priorWeight);
80
- for (const [cue, stat] of snap.stats) model.stats.set(cue, { ...stat });
81
- return model;
82
- }
83
- }
File without changes
File without changes
File without changes
File without changes