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
@@ -15,6 +15,7 @@ import { emptyPhases, TURN_PHASES, type TurnPhase } from "./session-record.js";
15
15
  const PHASE_HISTOGRAM: Record<TurnPhase, string> = {
16
16
  queueWait: "turn.queue_wait_ms",
17
17
  warpResolve: "turn.warp_resolve_ms",
18
+ memory: "turn.memory_ms",
18
19
  firstToken: "turn.first_token_ms",
19
20
  stream: "turn.stream_ms",
20
21
  delivery: "turn.delivery_ms",
@@ -30,6 +31,42 @@ const legacyCounters = new Map<string, number>();
30
31
  */
31
32
  const processHistograms = new Map<string, MetricsLatencyAgg>();
32
33
 
34
+ /**
35
+ * A chat's last cross-turn cache verdict — did the turn's FIRST request
36
+ * read the previous turn's prefix (`hit`), pay to re-write it (`miss`), or
37
+ * was nothing cacheable at all (`none`)? Mirrors `CrossTurnVerdict` in
38
+ * backend/runtime/cache/cache-telemetry.ts, which owns the classification; the
39
+ * union is restated here because `/status` renders it and frontends may
40
+ * not import backend/ (.dependency-cruiser.cjs: frontend-not-to-backend).
41
+ */
42
+ export type CacheVerdict = "hit" | "miss" | "none";
43
+
44
+ /**
45
+ * Per-chat last verdict. Process-local and deliberately unpersisted: it
46
+ * describes the live prefix, which dies with the process anyway, and
47
+ * `/status` is the only reader. Bounded like the per-chat maps in
48
+ * cache-telemetry.ts, insertion-ordered eviction.
49
+ */
50
+ const MAX_TRACKED_CHATS = 256;
51
+ const lastCacheVerdicts = new Map<string, CacheVerdict>();
52
+
53
+ /** Record the chat's newest cross-turn cache verdict. */
54
+ export function noteCacheVerdict(chatId: string, verdict: CacheVerdict): void {
55
+ if (
56
+ lastCacheVerdicts.size >= MAX_TRACKED_CHATS &&
57
+ !lastCacheVerdicts.has(chatId)
58
+ ) {
59
+ const oldest = lastCacheVerdicts.keys().next().value;
60
+ if (oldest !== undefined) lastCacheVerdicts.delete(oldest);
61
+ }
62
+ lastCacheVerdicts.set(chatId, verdict);
63
+ }
64
+
65
+ /** The chat's last cross-turn verdict, or undefined if none was recorded. */
66
+ export function getCacheVerdict(chatId: string): CacheVerdict | undefined {
67
+ return lastCacheVerdicts.get(chatId);
68
+ }
69
+
33
70
  export type MetricsSnapshot = {
34
71
  counters: Record<string, number>;
35
72
  histograms: Record<
@@ -202,5 +239,6 @@ export function getTodayMetrics(): MetricsSnapshot {
202
239
  export function resetMetrics(): void {
203
240
  legacyCounters.clear();
204
241
  processHistograms.clear();
242
+ lastCacheVerdicts.clear();
205
243
  resetAllSessionMetrics();
206
244
  }
@@ -42,6 +42,7 @@ type Row = {
42
42
  total_response_ms: number;
43
43
  last_response_ms: number;
44
44
  fastest_response_ms: number | null;
45
+ last_turn_ended_at: number | null;
45
46
  metrics: string | null;
46
47
  };
47
48
 
@@ -81,6 +82,7 @@ function rowToSession(row: Row): SessionState {
81
82
  lastBotMessageId: row.last_bot_message_id ?? undefined,
82
83
  sessionName: row.session_name ?? undefined,
83
84
  lastModel: row.last_model ?? undefined,
85
+ lastTurnEndedAt: row.last_turn_ended_at ?? undefined,
84
86
  };
85
87
  }
86
88
 
@@ -120,6 +122,10 @@ export function upsert(chatId: string, session: SessionState): void {
120
122
  num(usage?.totalResponseMs),
121
123
  num(usage?.lastResponseMs),
122
124
  typeof fastest === "number" && Number.isFinite(fastest) ? fastest : null,
125
+ typeof session.lastTurnEndedAt === "number" &&
126
+ Number.isFinite(session.lastTurnEndedAt)
127
+ ? session.lastTurnEndedAt
128
+ : null,
123
129
  JSON.stringify(session.metrics ?? emptyMetrics()),
124
130
  );
125
131
  }
@@ -56,12 +56,14 @@ export type MetricsLatencyAgg = {
56
56
 
57
57
  /**
58
58
  * Where a turn's wall-clock went, as seen from the Weaver: waiting in the
59
- * chat's FIFO, resolving the warp, until the backend's first event, the
60
- * whole stream, and the slice of it spent inside frontend delivery.
59
+ * chat's FIFO, resolving the warp, retrieving this turn's memory, until
60
+ * the backend's first event, the whole stream, and the slice of it spent
61
+ * inside frontend delivery.
61
62
  */
62
63
  export const TURN_PHASES = [
63
64
  "queueWait",
64
65
  "warpResolve",
66
+ "memory",
65
67
  "firstToken",
66
68
  "stream",
67
69
  "delivery",
@@ -104,6 +106,14 @@ export type SessionState = {
104
106
  sessionName?: string;
105
107
  /** Model used for this session's cost tracking. */
106
108
  lastModel?: string;
109
+ /**
110
+ * When the last turn of this chat finished (ms epoch), or undefined when
111
+ * no turn has completed yet. Distinct from `lastActive`, which any
112
+ * session write moves: this one only ever means "the transcript stopped
113
+ * growing here", which is what the prompt cache's TTL runs against —
114
+ * warm iff `now - lastTurnEndedAt < ttl` (docs/cache-economics.md).
115
+ */
116
+ lastTurnEndedAt?: number;
107
117
  };
108
118
 
109
119
  export const emptyUsage = (): SessionUsage => ({
@@ -146,6 +156,7 @@ export const emptyLatency = (): MetricsLatencyAgg => ({
146
156
  export const emptyPhases = (): MetricsPhaseAggs => ({
147
157
  queueWait: emptyLatency(),
148
158
  warpResolve: emptyLatency(),
159
+ memory: emptyLatency(),
149
160
  firstToken: emptyLatency(),
150
161
  stream: emptyLatency(),
151
162
  delivery: emptyLatency(),
@@ -480,6 +480,10 @@ export function recordUsage(
480
480
  session.usage.estimatedCostUsd += turn.costUsd;
481
481
  }
482
482
  if (turn.model) session.lastModel = turn.model;
483
+ // A turn just ended — this is the timestamp the prompt cache ages
484
+ // against (docs/cache-economics.md). Set on the one seam every backend
485
+ // funnels a finished turn through, failed-but-billed turns included.
486
+ session.lastTurnEndedAt = Date.now();
483
487
  if (turn.durationMs && turn.durationMs > 0) {
484
488
  session.usage.totalResponseMs =
485
489
  (session.usage.totalResponseMs || 0) + turn.durationMs;
@@ -580,6 +584,12 @@ export type SessionInfo = {
580
584
  lastModel?: string;
581
585
  /** True when a turn is currently streaming and `usage` includes its live so-far counts. */
582
586
  turnInProgress?: boolean;
587
+ /**
588
+ * When this chat's last turn finished (ms epoch), or undefined when none
589
+ * has. `/status` reports the idle gap from it; `lastActive` can't stand in
590
+ * because any session write moves that.
591
+ */
592
+ lastTurnEndedAt?: number;
583
593
  };
584
594
 
585
595
  export function getSessionInfo(chatId: string): SessionInfo {
@@ -594,6 +604,7 @@ export function getSessionInfo(chatId: string): SessionInfo {
594
604
  sessionName: session?.sessionName,
595
605
  lastModel: session?.lastModel,
596
606
  turnInProgress: liveTurns.has(chatId),
607
+ lastTurnEndedAt: session?.lastTurnEndedAt,
597
608
  };
598
609
  }
599
610
 
@@ -615,6 +626,7 @@ export function getAllSessions(): Array<{ chatId: string; info: SessionInfo }> {
615
626
  sessionName: session.sessionName,
616
627
  lastModel: session.lastModel,
617
628
  turnInProgress: liveTurns.has(chatId),
629
+ lastTurnEndedAt: session.lastTurnEndedAt,
618
630
  },
619
631
  }));
620
632
  }
@@ -25,3 +25,8 @@ ALTER TABLE sessions ADD COLUMN metrics TEXT NOT NULL DEFAULT '{"lifetime":{"cou
25
25
  -- carry more than one attachment. Fresh databases get the column via
26
26
  -- schema.sql.
27
27
  ALTER TABLE history_messages ADD COLUMN attachments TEXT
28
+
29
+ -- name: addSessionsLastTurnEndedAtColumn
30
+ -- Column reconciliation for databases that shipped before the cache-age
31
+ -- signal existed. Fresh databases get the column via schema.sql.
32
+ ALTER TABLE sessions ADD COLUMN last_turn_ended_at INTEGER
@@ -161,6 +161,10 @@ CREATE TABLE IF NOT EXISTS sessions (
161
161
  total_response_ms REAL NOT NULL DEFAULT 0,
162
162
  last_response_ms REAL NOT NULL DEFAULT 0,
163
163
  fastest_response_ms REAL,
164
+ -- When the chat's last turn finished; NULL until one has. Read as the
165
+ -- prompt cache's age signal (docs/cache-economics.md), which is why it
166
+ -- is separate from last_active (moved by any session write).
167
+ last_turn_ended_at INTEGER,
164
168
  metrics TEXT NOT NULL DEFAULT '{"lifetime":{"counters":{"queries":0,"toolCalls":0,"turnsWithTools":0,"apiCalls":0,"inputTokens":0,"outputTokens":0,"cacheReadTokens":0,"cacheWriteTokens":0,"failedTurns":0,"flowViolationRetries":0,"flowViolationCapExhausted":0,"trailingTextDropped":0},"latency":{"count":0,"sumMs":0,"minMs":null,"maxMs":0},"toolCallsByName":{},"backend":{},"cacheHitPercent":{"count":0,"sumMs":0,"minMs":null,"maxMs":0},"toolCallsPerTurn":{"count":0,"sumMs":0,"minMs":null,"maxMs":0},"apiCallsPerTurn":{"count":0,"sumMs":0,"minMs":null,"maxMs":0}},"buckets":{}}'
165
169
  );
166
170
 
@@ -7,15 +7,15 @@ INSERT OR REPLACE INTO sessions
7
7
  created_at, last_bot_message_id, total_input_tokens, total_output_tokens,
8
8
  total_cache_read, total_cache_write, last_prompt_tokens, context_tokens,
9
9
  context_window, num_api_calls, estimated_cost_usd, total_response_ms,
10
- last_response_ms, fastest_response_ms, metrics)
11
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
10
+ last_response_ms, fastest_response_ms, last_turn_ended_at, metrics)
11
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
12
12
 
13
13
  -- name: all
14
14
  SELECT chat_id, session_id, session_name, last_model, turns, last_active,
15
15
  created_at, last_bot_message_id, total_input_tokens, total_output_tokens,
16
16
  total_cache_read, total_cache_write, last_prompt_tokens, context_tokens,
17
17
  context_window, num_api_calls, estimated_cost_usd, total_response_ms,
18
- last_response_ms, fastest_response_ms, metrics
18
+ last_response_ms, fastest_response_ms, last_turn_ended_at, metrics
19
19
  FROM sessions
20
20
 
21
21
  -- name: remove
@@ -166,6 +166,10 @@ CREATE TABLE IF NOT EXISTS sessions (
166
166
  total_response_ms REAL NOT NULL DEFAULT 0,
167
167
  last_response_ms REAL NOT NULL DEFAULT 0,
168
168
  fastest_response_ms REAL,
169
+ -- When the chat's last turn finished; NULL until one has. Read as the
170
+ -- prompt cache's age signal (docs/cache-economics.md), which is why it
171
+ -- is separate from last_active (moved by any session write).
172
+ last_turn_ended_at INTEGER,
169
173
  metrics TEXT NOT NULL DEFAULT '{"lifetime":{"counters":{"queries":0,"toolCalls":0,"turnsWithTools":0,"apiCalls":0,"inputTokens":0,"outputTokens":0,"cacheReadTokens":0,"cacheWriteTokens":0,"failedTurns":0,"flowViolationRetries":0,"flowViolationCapExhausted":0,"trailingTextDropped":0},"latency":{"count":0,"sumMs":0,"minMs":null,"maxMs":0},"toolCallsByName":{},"backend":{},"cacheHitPercent":{"count":0,"sumMs":0,"minMs":null,"maxMs":0},"toolCallsPerTurn":{"count":0,"sumMs":0,"minMs":null,"maxMs":0},"apiCallsPerTurn":{"count":0,"sumMs":0,"minMs":null,"maxMs":0}},"buckets":{}}'
170
174
  );
171
175
 
@@ -414,6 +418,9 @@ ALTER TABLE sessions ADD COLUMN metrics TEXT NOT NULL DEFAULT '{"lifetime":{"cou
414
418
  -- carry more than one attachment. Fresh databases get the column via
415
419
  -- schema.sql.
416
420
  ALTER TABLE history_messages ADD COLUMN attachments TEXT`,
421
+ addSessionsLastTurnEndedAtColumn: `-- Column reconciliation for databases that shipped before the cache-age
422
+ -- signal existed. Fresh databases get the column via schema.sql.
423
+ ALTER TABLE sessions ADD COLUMN last_turn_ended_at INTEGER`,
417
424
  } as const;
418
425
 
419
426
  export const goalsSql = {
@@ -657,13 +664,13 @@ export const sessionsSql = {
657
664
  created_at, last_bot_message_id, total_input_tokens, total_output_tokens,
658
665
  total_cache_read, total_cache_write, last_prompt_tokens, context_tokens,
659
666
  context_window, num_api_calls, estimated_cost_usd, total_response_ms,
660
- last_response_ms, fastest_response_ms, metrics)
661
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
667
+ last_response_ms, fastest_response_ms, last_turn_ended_at, metrics)
668
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
662
669
  all: `SELECT chat_id, session_id, session_name, last_model, turns, last_active,
663
670
  created_at, last_bot_message_id, total_input_tokens, total_output_tokens,
664
671
  total_cache_read, total_cache_write, last_prompt_tokens, context_tokens,
665
672
  context_window, num_api_calls, estimated_cost_usd, total_response_ms,
666
- last_response_ms, fastest_response_ms, metrics
673
+ last_response_ms, fastest_response_ms, last_turn_ended_at, metrics
667
674
  FROM sessions`,
668
675
  remove: `DELETE FROM sessions WHERE chat_id = ?`,
669
676
  } as const;
@@ -1,4 +1,4 @@
1
- -- Statements for the turn_meta table (see frontend/native/turn-meta.ts).
1
+ -- Statements for the turn_meta table (see frontend/native/turn/turn-meta.ts).
2
2
 
3
3
  -- name: get
4
4
  SELECT meta FROM turn_meta WHERE chat_id = ? AND msg_id = ?
@@ -4,7 +4,9 @@
4
4
  * has an answer in the log instead of a profiler session.
5
5
  */
6
6
 
7
- const phases: { label: string; ms: number }[] = [];
7
+ export type BootPhaseSample = { label: string; ms: number };
8
+
9
+ const phases: BootPhaseSample[] = [];
8
10
 
9
11
  /** Run one startup phase and remember how long it took. */
10
12
  export async function bootPhase<T>(
@@ -25,6 +27,18 @@ export function bootReport(now = Math.round(process.uptime() * 1000)): string {
25
27
  return parts.length ? `${now}ms (${parts.join(", ")})` : `${now}ms`;
26
28
  }
27
29
 
30
+ /**
31
+ * The recorded phases, in the order they completed.
32
+ *
33
+ * `util/` is a leaf (.dependency-cruiser.cjs: util-is-a-leaf), so this
34
+ * module cannot write to the metrics store itself. It hands the samples
35
+ * out instead and `core/daemon/resource-sampler.ts` turns them into
36
+ * `boot.<phase>_ms` histograms.
37
+ */
38
+ export function bootPhases(): readonly BootPhaseSample[] {
39
+ return phases;
40
+ }
41
+
28
42
  /** Test seam. */
29
43
  export function resetBootPhases(): void {
30
44
  phases.length = 0;
@@ -67,3 +67,33 @@ export function isWhatsAppChatId(chatId: string): boolean {
67
67
  export function isDiscordChatId(chatId: string): boolean {
68
68
  return chatId.startsWith("discord_");
69
69
  }
70
+
71
+ /**
72
+ * Classify a canonical chat ID as a one-to-one chat, a multi-party one,
73
+ * or something this grammar cannot tell apart.
74
+ *
75
+ * The ID is the only signal engine-side code has: a frontend's own
76
+ * `isGroup` flag rides on the inbound message, while stores, jobs and
77
+ * gateway actions are keyed by the string ID alone. Per frontend:
78
+ *
79
+ * - `discord_guild_…` / `discord_dm_…` and `wa_group_…` / `wa_dm_…`
80
+ * say which they are outright;
81
+ * - a Telegram ID is bare digits — negative for a group, supergroup or
82
+ * channel, positive for the user's own ID (a DM);
83
+ * - terminal (`t_…`, `"1"`) and native (`d_…`) chats are local,
84
+ * single-operator surfaces, so they are DMs;
85
+ * - `teams_chat_…` covers 1:1 *and* group Teams chats alike, so it is
86
+ * `"unknown"` — as is anything a future frontend invents.
87
+ *
88
+ * Callers decide what `"unknown"` means for them; anything trust- or
89
+ * privacy-sensitive should treat it as a group and fail closed.
90
+ */
91
+ export function chatScope(chatId: string): "dm" | "group" | "unknown" {
92
+ if (chatId.startsWith("discord_guild_")) return "group";
93
+ if (chatId.startsWith("discord_dm_")) return "dm";
94
+ if (chatId.startsWith("wa_group_")) return "group";
95
+ if (chatId.startsWith("wa_dm_")) return "dm";
96
+ if (isTelegramChatId(chatId)) return chatId.startsWith("-") ? "group" : "dm";
97
+ if (isTerminalChatId(chatId) || isNativeChatId(chatId)) return "dm";
98
+ return "unknown";
99
+ }
package/src/util/log.ts CHANGED
@@ -62,7 +62,7 @@ export type LogComponent =
62
62
  | "mempalace"
63
63
  | "mem0"
64
64
  | "playwright"
65
- | "soul"
65
+ | "memory"
66
66
  | "stickers"
67
67
  | "backend-controller";
68
68
 
package/src/util/paths.ts CHANGED
@@ -116,8 +116,6 @@ export const files = {
116
116
  triggers: resolve(TALON_ROOT, "data", "triggers.json"),
117
117
  /** Legacy JSON native turn meta (imported into talon.db on first boot) */
118
118
  nativeTurnMeta: resolve(TALON_ROOT, "data", "native-turn-meta.json"),
119
- /** Soul kernel state: ~/.talon/data/soul.json (overridable via config.soul.path) */
120
- soul: resolve(TALON_ROOT, "data", "soul.json"),
121
119
  /** Legacy JSON media index (imported into talon.db on first boot) */
122
120
  mediaIndex: resolve(TALON_ROOT, "data", "media-index.json"),
123
121
  /** Persistent memory: ~/.talon/workspace/memory/memory.md */
@@ -1,110 +0,0 @@
1
- # Soul Kernel
2
-
3
- Talon's compiled identity. Not a markdown prompt, not a JSON blob — a
4
- **content-addressed Merkle DAG** of typed identity nodes, learned **mechanically**
5
- from behavioral telemetry and projected into the system prompt by **selection**.
6
-
7
- > The model is the brain. Talon is the body. The Soul is the persistent identity
8
- > artifact that tells the brain how to be Talon.
9
-
10
- ## The thesis: the model never writes its own soul
11
-
12
- There are two jobs — _being the self_ (the reasoning model reads the soul and
13
- acts) and _writing the self_ (turning experience into identity). If the reasoning
14
- model does the second, it can rationalize, flatter itself, and confabulate a
15
- coherent self-story that isn't true. So **writing the self is strictly
16
- model-free**: continuous, deterministic, evidence-grounded. The reasoning model is
17
- a _reader_ of the soul, never its author. That is the safety property, not just a
18
- cost saving.
19
-
20
- The only neural component anywhere is an optional **local embedder** (for
21
- clustering/dedup/contradiction-distance) — a frozen fixed function, not an agent.
22
- Everything else is counters, exponential decay, dot products, and selection.
23
-
24
- ## The substrate
25
-
26
- A node's identity is the sha-256 of its canonical content. The single most
27
- important decision is the split between:
28
-
29
- - **Content** (hashed, immutable) — a node's semantic identity: kind + verbatim
30
- payload + referenced child hashes. A change of content is a new node.
31
- - **State** (mutable, keyed by hash) — salience, evidence weight, activation
32
- counts. Churns every tick; lives _outside_ the Merkle structure.
33
-
34
- So reinforcing a value a thousand times never churns a single content hash.
35
- Versioning, provenance, dedup, and partial recompilation fall out structurally;
36
- learning and decay live in the state layer.
37
-
38
- ## The five structures
39
-
40
- | Structure | Node kind | What it is |
41
- | --------- | --------------- | ------------------------------------------------------------------------------------------- |
42
- | Spine | `spine` | append-only causal narrative — _why_ Talon became Talon |
43
- | Lattice | `value` + edges | values as evidence clusters; tension/affinity edges self-organize via Hebbian co-activation |
44
- | Reflexes | `reflex` | `trigger → guard → action` rules the harness _enforces_ |
45
- | Holograph | `lens` | identity refracted per interlocutor (selection + reweighting) |
46
- | Critic | (classifiers) | frozen feature-based gates over failure modes |
47
-
48
- The atoms under all of it are `evidence` nodes — verbatim ground truth. The kernel
49
- never paraphrases; it only ever quotes.
50
-
51
- ## The model-free pipeline
52
-
53
- ```
54
- telemetry ──▶ signals ──▶ compiler ──▶ DAG (+ salience/Hebbian) ──▶ projector ──▶ runtime.md
55
- (harness) (structured) (arithmetic) (content + state) (selection) (injected)
56
- ```
57
-
58
- - **Signals** (`signals.ts`) — structured harness events (reaction, engagement,
59
- correction, reflex-fire). Interpretation already happened; no transcript reading.
60
- - **Compiler** (`compiler.ts`) — routes each signal to pure update rules. Outcome
61
- signals reinforce/penalize the values that were live in that turn's projection
62
- and Hebbian-coactivate them; corrections store verbatim evidence + a Spine event.
63
- - **Salience** (`salience.ts`) — lazy exponential decay, reinforcement, Hebbian
64
- edges, predictive-coding delta rule. The whole "learning" is here, all arithmetic.
65
- - **Projector** (`projector.ts`) — selects verbatim evidence, salience-ordered
66
- under a token budget. Reflexes are never budgeted away. The soul cannot
67
- hallucinate itself.
68
- - **Critic** (`critic.ts`) — frozen classifiers for wall-of-text, sycophancy,
69
- emoji-overload.
70
- - **Kernel** (`kernel.ts`) — orchestrator: genesis → ingest → commit → project,
71
- with a git-like commit chain and JSON persistence.
72
-
73
- ## File map
74
-
75
- ```
76
- types.ts node payloads, edges, activation state, commits, config
77
- hash.ts canonical serialization + content addressing + Merkle root
78
- dag.ts the DAG store: integrity, dirty propagation, snapshot/restore
79
- salience.ts decay / reinforce / coactivate / prediction-error (the math)
80
- reflex.ts predicate registry + enforcer + the three seed reflexes
81
- signals.ts structured telemetry types + emoji→valence
82
- compiler.ts signal → arithmetic update routing (writing the self)
83
- projector.ts DAG → runtime surface by selection (reading the self)
84
- critic.ts frozen failure-mode classifiers
85
- kernel.ts orchestrator: lifecycle, commits, persistence
86
- index.ts public surface
87
- ```
88
-
89
- ## Status
90
-
91
- Implemented and tested (150+ tests, model-free end to end):
92
-
93
- - **Substrate** — content-addressed Merkle DAG, dirty-tracking, commits, persistence.
94
- - **Learning** — salience decay/reinforce, Hebbian edges, predictive-coding updates.
95
- - **Emergence** — value clustering (`crystallize`), self-reorganizing `consolidate`
96
- ("dream") with adaptive thresholds + state migration, reflection into `theme`s.
97
- - **Embedding** — our own `TalonEmbedder` (model-free) behind a pluggable seam.
98
- - **Adaptation** — learned valence (meaning from outcomes), FSRS/DSR forgetting,
99
- ADWIN drift → Spine epochs, emergent failure modes.
100
- - **Recall** — Generative-Agents context retrieval, PageRank centrality, modern
101
- Hopfield associative recall, VSA/HDC compositional episodic memory.
102
- - **Refraction** — relational holograph lens compilation.
103
- - **Surfaces** — projector (selection), delta stream, mechanical Critic.
104
- - **Governance** — protected-node approval queue; runtime gate (off by default).
105
-
106
- See `RESEARCH.md` for the literature behind each mechanism.
107
-
108
- Remaining: **harness wiring** — telemetry taps (reactions/engagement/corrections →
109
- signals), prompt injection of the projected surface, and live reflex enforcement.
110
- This touches the live Talon runtime and is gated behind `TALON_SOUL_ENABLED`.
@@ -1,98 +0,0 @@
1
- # Soul Kernel — research grounding
2
-
3
- Every mechanism in the kernel is model-free, but few are invented. This maps the
4
- load-bearing ideas to the literature they come from, and notes where each lives.
5
-
6
- ## Implemented
7
-
8
- ### Content-addressed Merkle DAG — `hash.ts`, `dag.ts`
9
-
10
- - Merkle, "A Digital Signature Based on a Conventional Encryption Function" (1987);
11
- the git object model (Torvalds/Hamano, 2005).
12
- - Why: content addressing makes dedup, provenance, versioning, and partial
13
- recompilation structural rather than bolted on.
14
-
15
- ### Hebbian association — `salience.coactivate`, `lattice.ts`
16
-
17
- - Hebb, _The Organization of Behavior_ (1949): "cells that fire together wire
18
- together."
19
- - Why: the value lattice's affinity/tension edges self-organize from
20
- co-activation counts; no model decides what relates.
21
-
22
- ### Predictive-coding / free-energy updates — `salience.applyPredictionError`
23
-
24
- - Rao & Ballard (1999); Friston, free-energy principle (2010).
25
- - Why: the soul is a predictive model of its own behavior; a correction is a
26
- large prediction error and updates salience proportionally.
27
-
28
- ### Complementary Learning Systems / systems consolidation — `consolidate.ts`
29
-
30
- - McClelland, McNaughton & O'Reilly (1995); memory replay during sleep
31
- (Wilson & McNaughton, 1994).
32
- - Why: fast intake (crystallize) vs. slow reorganization (the "dream" pass that
33
- merges drifted-together values and migrates learned state) mirrors the
34
- fast-hippocampal / slow-neocortical split.
35
-
36
- ### Feature hashing embedder — `talon-embedder.ts`
37
-
38
- - Weinberger et al., "Feature Hashing for Large Scale Multitask Learning" (2009);
39
- signed hashing to debias collisions.
40
- - Why: a deterministic, dependency-free, model-free encoder we own — robust to
41
- typos via shared character n-grams; a real sentence encoder drops in behind the
42
- same interface.
43
-
44
- ### Generative-Agents retrieval — `retrieve.ts`
45
-
46
- - Park et al., "Generative Agents: Interactive Simulacra of Human Behavior" (2023).
47
- - Score = normalized(recency) + normalized(importance) + normalized(relevance),
48
- each min-max'd to [0,1] before summation, exactly as in the paper. Makes the
49
- projected soul _relevant to the current moment_, not just globally salient.
50
-
51
- ### FSRS / DSR adaptive forgetting — `forgetting.ts`
52
-
53
- - Wozniak's DSR model; FSRS (open-spaced-repetition); power-law forgetting
54
- (Wickelgren, 1974) which fits human retention better than an exponential.
55
- - Retrievability R(t) = (1 + FACTOR·t/S)^DECAY with R(S) ≈ 0.9; stability S grows
56
- on spaced successful recall and shrinks on a lapse. Core traits become durable,
57
- fads evaporate — emergently.
58
-
59
- ### Modern Hopfield associative recall — `associative.ts`
60
-
61
- - Ramsauer et al., "Hopfield Networks is All You Need" (2020); Krotov & Hopfield
62
- (2016, 2020). Continuous Hopfield retrieval is one softmax step — the same
63
- operation as attention.
64
- - Recall from a cue = softmax over Hebbian edge weights, p ∝ exp(β·w); `prime`
65
- applies the update to the lattice so the soul recalls in constellations.
66
-
67
- ### Reflection — `reflect.ts`
68
-
69
- - Park et al. (2023): synthesize higher-level inferences over time. Done
70
- structurally — themes form over values that are coherent AND co-active. The one
71
- optional, gated model pass (a label-only `synthesize` hook) lives here and can
72
- never change which values group.
73
-
74
- ### Concept-drift detection — `drift.ts`
75
-
76
- - Bifet & Gavaldà, "Learning from Time-Changing Data with Adaptive Windowing"
77
- (ADWIN, 2007). Detects shifts in interaction-outcome distribution and opens a
78
- developmental epoch in the Spine.
79
-
80
- ### Personalized PageRank centrality — `centrality.ts`
81
-
82
- - Page, Brin, Motwani & Winograd (1999). Structural importance over the
83
- co-activation graph; personalizable to a context.
84
-
85
- ### Hyperdimensional / Vector-Symbolic memory — `hdc.ts`
86
-
87
- - Kanerva, "Hyperdimensional Computing" (2009). bind/bundle/permute/cleanup over
88
- high-dim bipolar vectors give a compositional, model-free episodic memory:
89
- context⊗value bound and bundled, recalled by unbinding — "in a situation like
90
- this, what do I do?"
91
-
92
- ## Future directions (researched, not yet built)
93
-
94
- - **Holographic Reduced Representations** (Plate, 1995) as a circular-convolution
95
- alternative to elementwise binding, for graceful capacity scaling.
96
- - **Sparse distributed memory** (Kanerva, 1988) as an alternative episodic store.
97
- - **Active-inference planning** (Friston et al.) atop the existing predictive-
98
- coding updates, to _select_ behavior, not just learn from it.
@@ -1,98 +0,0 @@
1
- /**
2
- * Soul Kernel — associative recall (modern Hopfield / attention).
3
- *
4
- * Grounded in Ramsauer et al., "Hopfield Networks is All You Need" (2020), which
5
- * showed that a continuous modern Hopfield network retrieves a stored pattern in
6
- * ONE update via a softmax over similarities — the very operation at the heart of
7
- * attention. We apply the same rule to the lattice: the Hebbian coactivation
8
- * edges are the stored associations, and recalling from a cue is a one-step
9
- * softmax over the edge weights.
10
- *
11
- * p(neighbor) ∝ exp(β · w(cue → neighbor))
12
- *
13
- * `prime` then performs the Hopfield update against the lattice itself: it nudges
14
- * the salience of the associatively-bound partners. The behavioral effect is what
15
- * you would want from a self — thinking of one value brings its companions to
16
- * mind ("push back" primes "be direct"), so the soul recalls in coherent
17
- * constellations rather than isolated points. Still model-free: a softmax over
18
- * counts.
19
- */
20
-
21
- import type { SoulDag } from "./dag.js";
22
- import type { Hash } from "./types.js";
23
-
24
- export interface RecallResult {
25
- readonly node: Hash;
26
- /** Softmax weight in (0, 1]; the constellation sums to 1. */
27
- readonly weight: number;
28
- }
29
-
30
- export interface RecallOptions {
31
- /** Inverse temperature; higher β sharpens recall toward the strongest bond. */
32
- readonly beta?: number;
33
- /** Keep only the top-K partners (default: all). */
34
- readonly topK?: number;
35
- }
36
-
37
- /**
38
- * One-step associative retrieval from one or more cue nodes. Aggregates
39
- * coactivation edge weights to each neighbor, then softmaxes. Cues are excluded
40
- * from their own recall.
41
- */
42
- export function associativeRecall(
43
- dag: SoulDag,
44
- cues: readonly Hash[],
45
- opts: RecallOptions = {},
46
- ): RecallResult[] {
47
- const beta = opts.beta ?? 1;
48
- const cueSet = new Set(cues);
49
- const score = new Map<Hash, number>();
50
- for (const cue of cues) {
51
- for (const e of dag.edgesFrom(cue, "coactivation")) {
52
- if (cueSet.has(e.to)) continue;
53
- score.set(e.to, (score.get(e.to) ?? 0) + e.weight);
54
- }
55
- }
56
- if (score.size === 0) return [];
57
-
58
- // Numerically-stable softmax over β·score.
59
- const entries = [...score.entries()];
60
- const max = Math.max(...entries.map(([, w]) => w));
61
- const exps = entries.map(
62
- ([h, w]) => [h, Math.exp(beta * (w - max))] as const,
63
- );
64
- const z = exps.reduce((s, [, e]) => s + e, 0);
65
-
66
- const out = exps
67
- .map(([node, e]) => ({ node, weight: e / z }))
68
- .sort((a, b) => b.weight - a.weight || a.node.localeCompare(b.node));
69
- return opts.topK ? out.slice(0, opts.topK) : out;
70
- }
71
-
72
- export interface PrimeOptions extends RecallOptions {
73
- readonly now: number;
74
- /** Salience injected, distributed across the recalled constellation. */
75
- readonly gain: number;
76
- }
77
-
78
- /**
79
- * The Hopfield update applied to the lattice: recall from the cues and add
80
- * salience to the bound partners in proportion to their recall weight. Returns
81
- * the primed nodes. Caller commits.
82
- */
83
- export function prime(
84
- dag: SoulDag,
85
- cues: readonly Hash[],
86
- opts: PrimeOptions,
87
- ): Hash[] {
88
- const recalled = associativeRecall(dag, cues, opts);
89
- const primed: Hash[] = [];
90
- for (const { node, weight } of recalled) {
91
- const s = dag.stateOf(node);
92
- s.salience += opts.gain * weight;
93
- s.lastActivatedAt = opts.now;
94
- dag.touch(node);
95
- primed.push(node);
96
- }
97
- return primed;
98
- }