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,162 +0,0 @@
1
- /**
2
- * Soul Kernel — the Critic. A self-model of failure modes, made mechanical.
3
- *
4
- * In v3 the Critic is not a reasoning model second-guessing the first; it is a
5
- * set of frozen, feature-based classifiers. Each scores a candidate text (a
6
- * drafted reply, or a projection surface) against one of Talon's documented
7
- * failure modes — wall-of-text, sycophancy, emoji-overload — using a linear
8
- * combination of cheap lexical/structural features and a fixed threshold. A
9
- * classifier is a fixed function, not an agent, so this stays strictly within
10
- * the model-free principle.
11
- *
12
- * Embedding-based gates (contradiction against the KG, drift from the core
13
- * centroid) are a later phase; they too are distance comparisons, not model
14
- * calls. The lexical classifiers here are the immediately-useful, fully
15
- * dependency-free core.
16
- */
17
-
18
- import { estimateTokens } from "./projector.js";
19
-
20
- type FailureMode = "wall-of-text" | "sycophancy" | "emoji-overload";
21
-
22
- export interface CritiqueFeatures {
23
- readonly tokens: number;
24
- readonly sentences: number;
25
- /** Hedging phrases per 100 tokens. */
26
- readonly hedgeRate: number;
27
- /** Emoji characters per 100 tokens. */
28
- readonly emojiDensity: number;
29
- /** Count of sycophancy-lexicon hits. */
30
- readonly sycophancyHits: number;
31
- }
32
-
33
- export interface Critique {
34
- readonly mode: FailureMode;
35
- /** Continuous severity; >= 1 means the fixed threshold was crossed. */
36
- readonly score: number;
37
- readonly flagged: boolean;
38
- readonly reason: string;
39
- }
40
-
41
- const HEDGES = [
42
- "i think",
43
- "i guess",
44
- "maybe",
45
- "perhaps",
46
- "sort of",
47
- "kind of",
48
- "i suppose",
49
- "probably",
50
- "it seems",
51
- ];
52
-
53
- const SYCOPHANCY = [
54
- "great question",
55
- "excellent question",
56
- "you're absolutely right",
57
- "you are absolutely right",
58
- "i'd be happy to",
59
- "i would be happy to",
60
- "absolutely!",
61
- "certainly!",
62
- "of course!",
63
- "happy to help",
64
- "great point",
65
- ];
66
-
67
- // Emoji detection without a heavy dependency: pictographic + symbol ranges.
68
- const EMOJI_RE =
69
- // Variation selectors are combining marks — alternated separately so the
70
- // character class holds only standalone pictographic ranges
71
- // (no-misleading-character-class).
72
- /[\u{1F300}-\u{1FAFF}\u{2600}-\u{27BF}\u{2190}-\u{21FF}\u{2B00}-\u{2BFF}\u{1F000}-\u{1F0FF}]|[\u{FE00}-\u{FE0F}]/gu;
73
-
74
- function countOccurrences(
75
- haystack: string,
76
- needles: readonly string[],
77
- ): number {
78
- let n = 0;
79
- for (const needle of needles) {
80
- let from = 0;
81
- for (;;) {
82
- const i = haystack.indexOf(needle, from);
83
- if (i === -1) break;
84
- n++;
85
- from = i + needle.length;
86
- }
87
- }
88
- return n;
89
- }
90
-
91
- /** Extract the cheap features the classifiers run over. Pure + deterministic. */
92
- export function extractFeatures(text: string): CritiqueFeatures {
93
- const lower = text.toLowerCase();
94
- const tokens = Math.max(1, estimateTokens(text));
95
- const sentences = Math.max(1, (text.match(/[.!?](\s|$)/g) ?? []).length);
96
- const emoji = (text.match(EMOJI_RE) ?? []).length;
97
- const hedges = countOccurrences(lower, HEDGES);
98
- return {
99
- tokens,
100
- sentences,
101
- hedgeRate: (hedges / tokens) * 100,
102
- emojiDensity: (emoji / tokens) * 100,
103
- sycophancyHits: countOccurrences(lower, SYCOPHANCY),
104
- };
105
- }
106
-
107
- export interface CritiqueThresholds {
108
- /** Token count above which prose is "wall-of-text". */
109
- readonly maxTokens: number;
110
- /** Sycophancy score (hits + hedge contribution) threshold. */
111
- readonly maxSycophancy: number;
112
- /** Emoji-per-100-tokens threshold. */
113
- readonly maxEmojiDensity: number;
114
- }
115
-
116
- export const DEFAULT_THRESHOLDS: CritiqueThresholds = {
117
- maxTokens: 320,
118
- maxSycophancy: 1,
119
- maxEmojiDensity: 6,
120
- };
121
-
122
- /**
123
- * Run the frozen classifiers over a candidate text. Returns one critique per
124
- * failure mode; `flagged` is true when its normalized score reaches 1.
125
- */
126
- export function critique(
127
- text: string,
128
- thresholds: CritiqueThresholds = DEFAULT_THRESHOLDS,
129
- ): Critique[] {
130
- const f = extractFeatures(text);
131
-
132
- const sycophancyScore = f.sycophancyHits + f.hedgeRate / 5;
133
- const wall = f.tokens / thresholds.maxTokens;
134
- const syco = sycophancyScore / Math.max(0.0001, thresholds.maxSycophancy);
135
- const emoji = f.emojiDensity / Math.max(0.0001, thresholds.maxEmojiDensity);
136
-
137
- return [
138
- {
139
- mode: "wall-of-text",
140
- score: wall,
141
- flagged: wall >= 1,
142
- reason: `${f.tokens} tokens vs budget ${thresholds.maxTokens}`,
143
- },
144
- {
145
- mode: "sycophancy",
146
- score: syco,
147
- flagged: syco >= 1,
148
- reason: `${f.sycophancyHits} sycophancy hits, hedge ${f.hedgeRate.toFixed(1)}/100tok`,
149
- },
150
- {
151
- mode: "emoji-overload",
152
- score: emoji,
153
- flagged: emoji >= 1,
154
- reason: `${f.emojiDensity.toFixed(1)} emoji/100tok vs ${thresholds.maxEmojiDensity}`,
155
- },
156
- ];
157
- }
158
-
159
- /** True if any failure mode tripped its threshold. */
160
- export function isFlagged(critiques: readonly Critique[]): boolean {
161
- return critiques.some((c) => c.flagged);
162
- }
@@ -1,265 +0,0 @@
1
- /**
2
- * Soul Kernel — the content-addressed DAG store.
3
- *
4
- * Holds three things, kept rigorously separate:
5
- *
6
- * 1. Nodes — immutable, content-addressed identity content (the Merkle DAG).
7
- * 2. State — mutable per-node "weather" (salience, evidence weight).
8
- * 3. Edges — mutable associative links (Hebbian co-activation, tension).
9
- *
10
- * Referential integrity is enforced on insert: a node may only reference child
11
- * hashes that already exist, so the DAG can never contain a dangling pointer and
12
- * the Merkle root is always sound.
13
- *
14
- * Dirty-tracking is structural and underpins "partial recompilation": only the
15
- * nodes added or whose state mutated since the last commit are reported dirty, so
16
- * downstream consumers (projector, critic) reprocess a handful of nodes, not the
17
- * whole self.
18
- */
19
-
20
- import { hashPayload, merkleRoot } from "./hash.js";
21
- import type {
22
- ActivationState,
23
- AssocEdge,
24
- EdgeKind,
25
- Hash,
26
- NodePayload,
27
- SoulNode,
28
- } from "./types.js";
29
-
30
- /** The structural (hashed) children a payload references, by kind. */
31
- export function structuralChildren(payload: NodePayload): Hash[] {
32
- switch (payload.kind) {
33
- case "evidence":
34
- case "reflex":
35
- return [];
36
- case "value":
37
- return dedupe([payload.medoid, ...payload.members]);
38
- case "theme":
39
- return dedupe([payload.medoid, ...payload.values]);
40
- case "spine":
41
- return dedupe([
42
- ...payload.affects,
43
- ...(payload.prev ? [payload.prev] : []),
44
- ]);
45
- case "lens":
46
- return dedupe([
47
- ...payload.amplify.map((a) => a.node),
48
- ...payload.evidence,
49
- ]);
50
- }
51
- }
52
-
53
- function dedupe(hashes: Hash[]): Hash[] {
54
- return [...new Set(hashes)];
55
- }
56
-
57
- function freshState(now: number): ActivationState {
58
- return { salience: 0, evidence: 0, activations: 0, lastActivatedAt: now };
59
- }
60
-
61
- /** Edge map key — one edge per (from, kind, to) triple. */
62
- function edgeKey(from: Hash, kind: EdgeKind, to: Hash): string {
63
- return `${from}|${kind}|${to}`;
64
- }
65
-
66
- export interface DagSnapshot {
67
- readonly nodes: readonly SoulNode[];
68
- readonly state: readonly (readonly [Hash, ActivationState])[];
69
- readonly edges: readonly AssocEdge[];
70
- }
71
-
72
- export class SoulDag {
73
- private readonly nodes = new Map<Hash, SoulNode>();
74
- private readonly state = new Map<Hash, ActivationState>();
75
- private readonly edges = new Map<string, AssocEdge>();
76
- /** Reverse adjacency for structural children → parents, for dirty propagation. */
77
- private readonly parents = new Map<Hash, Set<Hash>>();
78
- private readonly dirty = new Set<Hash>();
79
-
80
- // ── Nodes ──────────────────────────────────────────────────────────────────
81
-
82
- /**
83
- * Insert a node by content. Idempotent: identical content returns the same
84
- * hash and does not re-dirty. Throws if any structural child is absent.
85
- */
86
- addNode(payload: NodePayload, now = Date.now()): Hash {
87
- const hash = hashPayload(payload);
88
- if (this.nodes.has(hash)) return hash;
89
-
90
- const children = structuralChildren(payload);
91
- for (const child of children) {
92
- if (!this.nodes.has(child)) {
93
- throw new Error(
94
- `SoulDag.addNode: dangling reference ${child} from ${payload.kind} node`,
95
- );
96
- }
97
- }
98
-
99
- this.nodes.set(hash, { hash, payload });
100
- this.state.set(hash, freshState(now));
101
- for (const child of children) {
102
- let set = this.parents.get(child);
103
- if (!set) {
104
- set = new Set();
105
- this.parents.set(child, set);
106
- }
107
- set.add(hash);
108
- }
109
- this.dirty.add(hash);
110
- return hash;
111
- }
112
-
113
- getNode(hash: Hash): SoulNode | undefined {
114
- return this.nodes.get(hash);
115
- }
116
-
117
- hasNode(hash: Hash): boolean {
118
- return this.nodes.has(hash);
119
- }
120
-
121
- /** All node hashes of a given kind. */
122
- nodesOfKind(kind: NodePayload["kind"]): SoulNode[] {
123
- const out: SoulNode[] = [];
124
- for (const node of this.nodes.values()) {
125
- if (node.payload.kind === kind) out.push(node);
126
- }
127
- return out;
128
- }
129
-
130
- get size(): number {
131
- return this.nodes.size;
132
- }
133
-
134
- // ── State ──────────────────────────────────────────────────────────────────
135
-
136
- /** Mutable activation state for a node (lazily created). */
137
- stateOf(hash: Hash): ActivationState {
138
- let s = this.state.get(hash);
139
- if (!s) {
140
- s = freshState(Date.now());
141
- this.state.set(hash, s);
142
- }
143
- return s;
144
- }
145
-
146
- /** Mark a node (and its structural ancestors) dirty after a state change. */
147
- touch(hash: Hash): void {
148
- if (!this.nodes.has(hash)) return;
149
- this.dirty.add(hash);
150
- }
151
-
152
- // ── Edges ──────────────────────────────────────────────────────────────────
153
-
154
- /** Create or fetch the edge for a triple; weight starts at 0. */
155
- edge(from: Hash, kind: EdgeKind, to: Hash, now = Date.now()): AssocEdge {
156
- const key = edgeKey(from, kind, to);
157
- let e = this.edges.get(key);
158
- if (!e) {
159
- e = { from, to, kind, weight: 0, updatedAt: now };
160
- this.edges.set(key, e);
161
- }
162
- return e;
163
- }
164
-
165
- getEdge(from: Hash, kind: EdgeKind, to: Hash): AssocEdge | undefined {
166
- return this.edges.get(edgeKey(from, kind, to));
167
- }
168
-
169
- /** All edges originating at `from`, optionally filtered by kind. */
170
- edgesFrom(from: Hash, kind?: EdgeKind): AssocEdge[] {
171
- const out: AssocEdge[] = [];
172
- for (const e of this.edges.values()) {
173
- if (e.from === from && (!kind || e.kind === kind)) out.push(e);
174
- }
175
- return out;
176
- }
177
-
178
- /** Every edge in the graph, optionally filtered by kind. */
179
- allEdges(kind?: EdgeKind): AssocEdge[] {
180
- const out: AssocEdge[] = [];
181
- for (const e of this.edges.values()) {
182
- if (!kind || e.kind === kind) out.push(e);
183
- }
184
- return out;
185
- }
186
-
187
- // ── Structure ────────────────────────────────────────────────────────────────
188
-
189
- childrenOf(hash: Hash): Hash[] {
190
- const node = this.nodes.get(hash);
191
- return node ? structuralChildren(node.payload) : [];
192
- }
193
-
194
- parentsOf(hash: Hash): Hash[] {
195
- return [...(this.parents.get(hash) ?? [])];
196
- }
197
-
198
- /** Merkle root over the full set of node content present. The version id. */
199
- root(): Hash {
200
- return merkleRoot(this.nodes.keys());
201
- }
202
-
203
- // ── Dirty-tracking ───────────────────────────────────────────────────────────
204
-
205
- /** Nodes added or touched since the last clear, plus their ancestors. */
206
- dirtySet(): Set<Hash> {
207
- const out = new Set<Hash>();
208
- const stack = [...this.dirty];
209
- while (stack.length) {
210
- const h = stack.pop()!;
211
- if (out.has(h)) continue;
212
- out.add(h);
213
- for (const parent of this.parents.get(h) ?? []) stack.push(parent);
214
- }
215
- return out;
216
- }
217
-
218
- clearDirty(): void {
219
- this.dirty.clear();
220
- }
221
-
222
- // ── Serialization ────────────────────────────────────────────────────────────
223
-
224
- snapshot(): DagSnapshot {
225
- return {
226
- nodes: [...this.nodes.values()],
227
- state: [...this.state.entries()].map(([h, s]) => [h, { ...s }]),
228
- edges: [...this.edges.values()].map((e) => ({ ...e })),
229
- };
230
- }
231
-
232
- /**
233
- * Rebuild a DAG from a snapshot. Nodes are re-inserted in dependency order so
234
- * referential integrity is re-verified on load (a corrupt snapshot throws
235
- * rather than silently producing a broken graph).
236
- */
237
- static restore(snap: DagSnapshot): SoulDag {
238
- const dag = new SoulDag();
239
- const byHash = new Map(snap.nodes.map((n) => [n.hash, n]));
240
- const inserted = new Set<Hash>();
241
-
242
- const insert = (node: SoulNode): void => {
243
- if (inserted.has(node.hash)) return;
244
- for (const child of structuralChildren(node.payload)) {
245
- const childNode = byHash.get(child);
246
- if (!childNode) {
247
- throw new Error(`SoulDag.restore: snapshot missing child ${child}`);
248
- }
249
- insert(childNode);
250
- }
251
- dag.addNode(node.payload);
252
- inserted.add(node.hash);
253
- };
254
- for (const node of snap.nodes) insert(node);
255
-
256
- for (const [hash, s] of snap.state) {
257
- if (dag.nodes.has(hash)) dag.state.set(hash, { ...s });
258
- }
259
- for (const e of snap.edges) {
260
- dag.edges.set(edgeKey(e.from, e.kind, e.to), { ...e });
261
- }
262
- dag.clearDirty();
263
- return dag;
264
- }
265
- }
@@ -1,123 +0,0 @@
1
- /**
2
- * Soul Kernel — the delta stream ("what changed in me, and why").
3
- *
4
- * One of the kernel's projection targets is a human-readable account of how the
5
- * soul changed between two versions. It is computed by diffing two DAG snapshots
6
- * and TEMPLATED from the structural difference — never model-written — so the
7
- * change log is as auditable as the identity itself. The kernel uses it to give
8
- * each commit a meaningful summary and to power a `/soul` introspection view.
9
- */
10
-
11
- import type { DagSnapshot } from "./dag.js";
12
- import type {
13
- Hash,
14
- NodePayload,
15
- SpinePayload,
16
- ThemePayload,
17
- ValuePayload,
18
- } from "./types.js";
19
-
20
- export interface SoulDelta {
21
- readonly addedValues: string[];
22
- readonly addedThemes: string[];
23
- readonly addedSpine: string[];
24
- readonly addedReflexes: string[];
25
- readonly addedLenses: string[];
26
- readonly supersededCount: number;
27
- readonly newTensions: number;
28
- }
29
-
30
- function payloadMap(snap: DagSnapshot): Map<Hash, NodePayload> {
31
- return new Map(snap.nodes.map((n) => [n.hash, n.payload]));
32
- }
33
-
34
- function evidenceText(map: Map<Hash, NodePayload>, hash: Hash): string {
35
- const p = map.get(hash);
36
- return p?.kind === "evidence" ? p.text : "";
37
- }
38
-
39
- function countEdges(snap: DagSnapshot, kind: string): number {
40
- return snap.edges.filter((e) => e.kind === kind).length;
41
- }
42
-
43
- /** Diff two snapshots into a structural delta. Append-only ⇒ only additions. */
44
- export function diffSnapshots(prev: DagSnapshot, curr: DagSnapshot): SoulDelta {
45
- const prevHashes = new Set(prev.nodes.map((n) => n.hash));
46
- const map = payloadMap(curr);
47
-
48
- const addedValues: string[] = [];
49
- const addedThemes: string[] = [];
50
- const addedSpine: string[] = [];
51
- const addedReflexes: string[] = [];
52
- const addedLenses: string[] = [];
53
-
54
- for (const node of curr.nodes) {
55
- if (prevHashes.has(node.hash)) continue;
56
- const p = node.payload;
57
- switch (p.kind) {
58
- case "value":
59
- addedValues.push(evidenceText(map, (p as ValuePayload).medoid));
60
- break;
61
- case "theme":
62
- addedThemes.push(
63
- (p as ThemePayload).insight ??
64
- evidenceText(map, (p as ThemePayload).medoid),
65
- );
66
- break;
67
- case "spine":
68
- addedSpine.push((p as SpinePayload).event);
69
- break;
70
- case "reflex":
71
- addedReflexes.push(p.name);
72
- break;
73
- case "lens":
74
- addedLenses.push(p.subject);
75
- break;
76
- default:
77
- break;
78
- }
79
- }
80
-
81
- return {
82
- addedValues,
83
- addedThemes,
84
- addedSpine,
85
- addedReflexes,
86
- addedLenses,
87
- supersededCount:
88
- countEdges(curr, "supersedes") - countEdges(prev, "supersedes"),
89
- newTensions: countEdges(curr, "tension") - countEdges(prev, "tension"),
90
- };
91
- }
92
-
93
- /** Render a delta as a compact, templated one-paragraph summary. */
94
- export function renderDelta(delta: SoulDelta): string {
95
- const parts: string[] = [];
96
- if (delta.addedValues.length)
97
- parts.push(`+${delta.addedValues.length} value(s)`);
98
- if (delta.addedThemes.length)
99
- parts.push(`+${delta.addedThemes.length} theme(s)`);
100
- if (delta.supersededCount > 0) parts.push(`${delta.supersededCount} merged`);
101
- if (delta.newTensions > 0) parts.push(`+${delta.newTensions} tension(s)`);
102
- if (delta.addedReflexes.length)
103
- parts.push(`+reflex ${delta.addedReflexes.join(", ")}`);
104
- if (delta.addedLenses.length)
105
- parts.push(`+lens ${delta.addedLenses.join(", ")}`);
106
- if (delta.addedSpine.length) parts.push(`+${delta.addedSpine.length} spine`);
107
- return parts.length ? parts.join(", ") : "no structural change";
108
- }
109
-
110
- /** A fuller multi-line introspection of the delta (for a `/soul` view). */
111
- export function explainDelta(delta: SoulDelta): string {
112
- const lines: string[] = [];
113
- for (const v of delta.addedValues) lines.push(`+ value: "${v}"`);
114
- for (const t of delta.addedThemes) lines.push(`+ theme: ${t}`);
115
- for (const s of delta.addedSpine) lines.push(`+ spine: ${s}`);
116
- for (const r of delta.addedReflexes) lines.push(`+ reflex: ${r}`);
117
- for (const l of delta.addedLenses) lines.push(`+ lens: ${l}`);
118
- if (delta.supersededCount > 0)
119
- lines.push(`~ ${delta.supersededCount} value(s) merged away`);
120
- if (delta.newTensions > 0)
121
- lines.push(`~ ${delta.newTensions} new tension(s)`);
122
- return lines.length ? lines.join("\n") : "no structural change";
123
- }
@@ -1,99 +0,0 @@
1
- /**
2
- * Soul Kernel — concept-drift detection (ADWIN).
3
- *
4
- * Bifet & Gavaldà, "Learning from Time-Changing Data with Adaptive Windowing"
5
- * (2007). ADWIN keeps a window of recent observations and, whenever the means of
6
- * an older and a newer sub-window differ beyond a variance-aware Hoeffding bound,
7
- * declares a change and forgets the stale older half. It needs no fixed window
8
- * size — the window adapts to how stationary the stream is.
9
- *
10
- * The soul feeds it the scalar outcome of interactions (the valence of what
11
- * happens). A detected change is a genuine shift in how Talon's behavior is
12
- * landing — a developmental inflection — which the kernel records as an epoch in
13
- * the Spine. Pure statistics; model-free.
14
- *
15
- * This is the readable ADWIN0 variant (exhaustive cut search over a flat window)
16
- * rather than the bucketed/exponential-histogram optimization, which is faithful
17
- * to the test and bound but O(n) per insert — fine at the soul's data rate.
18
- */
19
-
20
- export interface AdwinChange {
21
- /** True on the insert that triggered a change. */
22
- readonly changed: boolean;
23
- /** Mean before the cut (the era being forgotten), when changed. */
24
- readonly meanBefore?: number;
25
- /** Mean after the cut (the new era), when changed. */
26
- readonly meanAfter?: number;
27
- }
28
-
29
- export class Adwin {
30
- private window: number[] = [];
31
-
32
- /** @param delta confidence; smaller = fewer false alarms (default 0.002). */
33
- constructor(private readonly delta = 0.002) {}
34
-
35
- get width(): number {
36
- return this.window.length;
37
- }
38
-
39
- get total(): number {
40
- return this.window.reduce((s, x) => s + x, 0);
41
- }
42
-
43
- get mean(): number {
44
- return this.window.length ? this.total / this.window.length : 0;
45
- }
46
-
47
- private static stats(xs: number[]): { mean: number; variance: number } {
48
- const n = xs.length;
49
- if (n === 0) return { mean: 0, variance: 0 };
50
- const mean = xs.reduce((s, x) => s + x, 0) / n;
51
- const variance = xs.reduce((s, x) => s + (x - mean) ** 2, 0) / n;
52
- return { mean, variance };
53
- }
54
-
55
- /**
56
- * Add an observation. Searches every split; if a split's sub-window means
57
- * differ beyond the variance-aware Hoeffding bound, drops the older half and
58
- * reports the change.
59
- */
60
- add(value: number): AdwinChange {
61
- this.window.push(value);
62
- const n = this.window.length;
63
- if (n < 2) return { changed: false };
64
-
65
- const wholeVar = Adwin.stats(this.window).variance;
66
-
67
- for (let cut = 1; cut < n; cut++) {
68
- const left = this.window.slice(0, cut);
69
- const right = this.window.slice(cut);
70
- const n0 = left.length;
71
- const n1 = right.length;
72
- const m0 = left.reduce((s, x) => s + x, 0) / n0;
73
- const m1 = right.reduce((s, x) => s + x, 0) / n1;
74
-
75
- // Harmonic window size and the ADWIN variance-aware bound.
76
- const m = 1 / (1 / n0 + 1 / n1);
77
- const deltaPrime = this.delta / n;
78
- const eps =
79
- Math.sqrt((2 / m) * wholeVar * Math.log(2 / deltaPrime)) +
80
- (2 / (3 * m)) * Math.log(2 / deltaPrime);
81
-
82
- if (Math.abs(m0 - m1) > eps) {
83
- this.window = right; // forget the stale older era
84
- return { changed: true, meanBefore: m0, meanAfter: m1 };
85
- }
86
- }
87
- return { changed: false };
88
- }
89
-
90
- snapshot(): readonly number[] {
91
- return [...this.window];
92
- }
93
-
94
- static restore(window: readonly number[], delta = 0.002): Adwin {
95
- const a = new Adwin(delta);
96
- a.window = [...window];
97
- return a;
98
- }
99
- }