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.
- package/README.md +3 -3
- package/package.json +3 -2
- package/prompts/README.md +2 -2
- package/prompts/system/memory-recall.md +16 -0
- package/src/app.ts +19 -4
- package/src/backend/claude-sdk/handler.ts +28 -7
- package/src/backend/claude-sdk/one-shot.ts +1 -1
- package/src/backend/claude-sdk/options.ts +7 -6
- package/src/backend/claude-sdk/stream.ts +30 -3
- package/src/backend/claude-sdk/warm.ts +1 -1
- package/src/backend/codex/constants.ts +1 -1
- package/src/backend/codex/factory.ts +2 -2
- package/src/backend/codex/handler/events.ts +1 -1
- package/src/backend/codex/handler/message.ts +26 -12
- package/src/backend/codex/handler/rollout-accounting.ts +1 -1
- package/src/backend/codex/init.ts +1 -1
- package/src/backend/codex/mcp-config.ts +1 -1
- package/src/backend/codex/one-shot.ts +1 -1
- package/src/backend/kilo/handler/message.ts +4 -1
- package/src/backend/openai-agents/constants.ts +1 -1
- package/src/backend/openai-agents/factory.ts +2 -2
- package/src/backend/openai-agents/handler/events.ts +1 -1
- package/src/backend/openai-agents/handler/message.ts +8 -4
- package/src/backend/openai-agents/init.ts +1 -1
- package/src/backend/openai-agents/mcp-pool.ts +1 -1
- package/src/backend/opencode/handler/message.ts +4 -1
- package/src/backend/remote-server/chat-turn.ts +31 -24
- package/src/backend/remote-server/events.ts +3 -3
- package/src/backend/remote-server/factory.ts +6 -3
- package/src/backend/remote-server/index.ts +1 -1
- package/src/backend/remote-server/mcp.ts +1 -1
- package/src/backend/remote-server/one-shot.ts +1 -1
- package/src/backend/remote-server/server-bindings.ts +1 -1
- package/src/backend/remote-server/turn.ts +1 -1
- package/src/backend/runtime/cache/cache-metrics.ts +126 -0
- package/src/backend/{shared → runtime/cache}/cache-telemetry.ts +22 -2
- package/src/backend/{shared → runtime}/index.ts +32 -23
- package/src/backend/{shared → runtime/prompt}/delivery-contract.ts +1 -1
- package/src/backend/{shared → runtime/prompt}/prompt-format.ts +46 -2
- package/src/backend/{shared → runtime/prompt}/system-prompt.ts +4 -4
- package/src/backend/{shared → runtime/turn}/delivered-text.ts +1 -1
- package/src/backend/{shared → runtime/turn}/delivery.ts +2 -2
- package/src/backend/{shared → runtime/turn}/handle-retry.ts +5 -5
- package/src/backend/{shared → runtime/turn}/handler-to-events.ts +24 -10
- package/src/backend/{shared → runtime/turn}/handler-types.ts +7 -1
- package/src/backend/{shared → runtime/turn}/model-retry.ts +2 -2
- package/src/backend/{shared → runtime/turn}/result-events.ts +2 -2
- package/src/backend/{shared → runtime/turn}/stream-state.ts +2 -2
- package/src/backend/{shared → runtime/turn}/turn-interrupt.ts +2 -2
- package/src/backend/{shared → runtime/turn}/turn-phases.ts +6 -6
- package/src/bootstrap.ts +4 -19
- package/src/cli.ts +1 -1
- package/src/core/agent-runtime/capabilities.ts +11 -0
- package/src/core/agent-runtime/contract-tests.ts +92 -1
- package/src/core/agent-runtime/events.ts +1 -1
- package/src/core/background/{isolated-agent.ts → cron/isolated-agent.ts} +3 -3
- package/src/core/background/{job-health.ts → cron/job-health.ts} +1 -1
- package/src/core/background/{job-oneshot.ts → cron/job-oneshot.ts} +5 -5
- package/src/core/background/{job-prompt.ts → cron/job-prompt.ts} +1 -1
- package/src/core/background/{cron.ts → cron/scheduler.ts} +6 -6
- package/src/core/background/{cron-spec.ts → cron/spec.ts} +1 -1
- package/src/core/background/{dream.ts → dream/index.ts} +10 -15
- package/src/core/background/{plan-alerts.ts → pulse/plan-alerts.ts} +3 -3
- package/src/core/background/{pulse.ts → pulse/pulse.ts} +8 -5
- package/src/core/background/triggers/command.ts +1 -1
- package/src/core/config/index.ts +15 -11
- package/src/core/daemon/resource-sampler.ts +121 -0
- package/src/core/engine/dispatcher.ts +16 -0
- package/src/core/engine/gateway-actions/cron.ts +2 -2
- package/src/core/engine/gateway-actions/index.ts +3 -0
- package/src/core/engine/gateway-actions/memory.ts +335 -0
- package/src/core/engine/gateway.ts +0 -6
- package/src/core/memory/import.ts +3 -2
- package/src/core/memory/taps.ts +199 -0
- package/src/core/memory/turn-retrieval.ts +222 -0
- package/src/core/prompt/assemble.ts +2 -12
- package/src/core/prompt/index.ts +2 -2
- package/src/core/prompt/invalidation.ts +1 -1
- package/src/core/tasks/index.ts +1 -1
- package/src/core/tools/index.ts +2 -0
- package/src/core/tools/memory.ts +128 -0
- package/src/core/tools/types.ts +1 -0
- package/src/core/weaver/turn-cpu.ts +32 -0
- package/src/core/weaver/weaver.ts +36 -0
- package/src/frontend/discord/admin.ts +1 -1
- package/src/frontend/discord/callbacks/components/backend-select.ts +1 -1
- package/src/frontend/discord/callbacks/components/pulse.ts +1 -1
- package/src/frontend/discord/callbacks/components/settings.ts +1 -1
- package/src/frontend/discord/callbacks/modals.ts +4 -1
- package/src/frontend/discord/commands/admin.ts +1 -35
- package/src/frontend/discord/commands/definitions.ts +0 -11
- package/src/frontend/discord/commands/router.ts +0 -3
- package/src/frontend/discord/commands/session.ts +6 -0
- package/src/frontend/discord/commands/settings.ts +1 -1
- package/src/frontend/discord/middleware.ts +1 -25
- package/src/frontend/discord/runtime.ts +1 -2
- package/src/frontend/native/{auth.ts → bridge/auth.ts} +2 -2
- package/src/frontend/native/{discovery.ts → bridge/discovery.ts} +3 -3
- package/src/frontend/native/{routes → bridge/routes}/chats.ts +1 -1
- package/src/frontend/native/{routes → bridge/routes}/daemon.ts +1 -1
- package/src/frontend/native/{routes → bridge/routes}/host.ts +6 -3
- package/src/frontend/native/{routes → bridge/routes}/pre-auth.ts +1 -1
- package/src/frontend/native/{server.ts → bridge/server.ts} +3 -3
- package/src/frontend/native/{tls.ts → bridge/tls.ts} +2 -2
- package/src/frontend/native/{chat-lifecycle.ts → chats/chat-lifecycle.ts} +3 -3
- package/src/frontend/native/{chat-wire.ts → chats/chat-wire.ts} +4 -4
- package/src/frontend/native/{chats.ts → chats/chats.ts} +4 -4
- package/src/frontend/native/{empty-chat-sweep.ts → chats/empty-chat-sweep.ts} +4 -4
- package/src/frontend/native/{history.ts → chats/history.ts} +7 -7
- package/src/frontend/native/{reset.ts → chats/reset.ts} +7 -7
- package/src/frontend/native/index.ts +13 -10
- package/src/frontend/native/{media.ts → media/media.ts} +3 -3
- package/src/frontend/native/runtime.ts +1 -1
- package/src/frontend/native/{control.ts → surface/control.ts} +4 -4
- package/src/frontend/native/{extensions.ts → surface/extensions.ts} +12 -9
- package/src/frontend/native/{handlers.ts → surface/handlers.ts} +19 -14
- package/src/frontend/native/{logs.ts → surface/logs.ts} +2 -2
- package/src/frontend/native/{memory.ts → surface/memory.ts} +2 -2
- package/src/frontend/native/{models.ts → surface/models.ts} +17 -10
- package/src/frontend/native/{settings.ts → surface/settings.ts} +9 -9
- package/src/frontend/native/{status.ts → surface/status.ts} +3 -3
- package/src/frontend/native/{actions.ts → turn/actions.ts} +7 -4
- package/src/frontend/native/{context.ts → turn/context.ts} +8 -8
- package/src/frontend/native/{emit.ts → turn/emit.ts} +7 -7
- package/src/frontend/native/{queue.ts → turn/queue.ts} +3 -3
- package/src/frontend/native/{turn-meta.ts → turn/turn-meta.ts} +2 -2
- package/src/frontend/native/{turn.ts → turn/turn.ts} +9 -9
- package/src/frontend/shared/model-commands.ts +1 -1
- package/src/frontend/shared/session-status.ts +35 -8
- package/src/frontend/shared/status-context.ts +101 -2
- package/src/frontend/telegram/admin/background.ts +1 -1
- package/src/frontend/telegram/callbacks/model/backend.ts +1 -1
- package/src/frontend/telegram/callbacks/pulse.ts +1 -1
- package/src/frontend/telegram/callbacks/settings.ts +1 -1
- package/src/frontend/telegram/commands/admin.ts +2 -37
- package/src/frontend/telegram/commands/index.ts +1 -1
- package/src/frontend/telegram/commands/session.ts +6 -0
- package/src/frontend/telegram/commands/settings.ts +1 -1
- package/src/frontend/telegram/handlers/messages.ts +0 -10
- package/src/frontend/telegram/index.ts +1 -5
- package/src/frontend/telegram/middleware.ts +1 -15
- package/src/frontend/whatsapp/access.ts +2 -2
- package/src/frontend/whatsapp/actions/history.ts +2 -2
- package/src/frontend/whatsapp/actions/messaging.ts +2 -2
- package/src/frontend/whatsapp/actions/moderation.ts +1 -1
- package/src/frontend/whatsapp/actions/send.ts +1 -1
- package/src/frontend/whatsapp/commands.ts +8 -2
- package/src/frontend/whatsapp/{connection.ts → connection/connection.ts} +5 -5
- package/src/frontend/whatsapp/{pairing-service.ts → connection/pairing-service.ts} +3 -3
- package/src/frontend/whatsapp/{wa-logger.ts → connection/wa-logger.ts} +2 -2
- package/src/frontend/whatsapp/index.ts +4 -4
- package/src/frontend/whatsapp/{inbound.ts → messages/inbound.ts} +15 -15
- package/src/frontend/whatsapp/{media-store.ts → messages/media-store.ts} +4 -4
- package/src/frontend/whatsapp/{message-store.ts → messages/message-store.ts} +1 -1
- package/src/frontend/whatsapp/{turn-recovery.ts → messages/turn-recovery.ts} +4 -4
- package/src/frontend/whatsapp/registry.ts +1 -1
- package/src/frontend/whatsapp/runtime.ts +1 -1
- package/src/index.ts +1 -1
- package/src/storage/db.ts +6 -1
- package/src/storage/memory.ts +59 -8
- package/src/storage/metrics.ts +38 -0
- package/src/storage/repositories/goals-repo.ts +2 -1
- package/src/storage/repositories/sessions-repo.ts +6 -0
- package/src/storage/session-record.ts +13 -2
- package/src/storage/sessions.ts +12 -0
- package/src/storage/sql/db.sql +5 -0
- package/src/storage/sql/schema.sql +4 -0
- package/src/storage/sql/sessions.sql +3 -3
- package/src/storage/sql/statements.generated.ts +10 -3
- package/src/storage/sql/turn-meta.sql +1 -1
- package/src/util/boot-timer.ts +15 -1
- package/src/util/chat-id.ts +30 -0
- package/src/util/concurrency.ts +1 -1
- package/src/util/log.ts +1 -1
- package/src/util/paths.ts +0 -2
- package/src/core/soul/README.md +0 -110
- package/src/core/soul/RESEARCH.md +0 -98
- package/src/core/soul/associative.ts +0 -98
- package/src/core/soul/centrality.ts +0 -98
- package/src/core/soul/cluster.ts +0 -83
- package/src/core/soul/compiler.ts +0 -207
- package/src/core/soul/consolidate.ts +0 -179
- package/src/core/soul/critic.ts +0 -162
- package/src/core/soul/dag.ts +0 -265
- package/src/core/soul/delta.ts +0 -123
- package/src/core/soul/drift.ts +0 -99
- package/src/core/soul/embedder.ts +0 -129
- package/src/core/soul/emergent-critic.ts +0 -96
- package/src/core/soul/forgetting.ts +0 -131
- package/src/core/soul/governance.ts +0 -93
- package/src/core/soul/hash.ts +0 -97
- package/src/core/soul/hdc.ts +0 -154
- package/src/core/soul/kernel.ts +0 -540
- package/src/core/soul/lattice.ts +0 -103
- package/src/core/soul/lens.ts +0 -110
- package/src/core/soul/projector.ts +0 -240
- package/src/core/soul/reflect.ts +0 -170
- package/src/core/soul/reflex.ts +0 -164
- package/src/core/soul/retrieve.ts +0 -146
- package/src/core/soul/salience.ts +0 -146
- package/src/core/soul/service.ts +0 -204
- package/src/core/soul/settings.ts +0 -47
- package/src/core/soul/signals.ts +0 -117
- package/src/core/soul/talon-embedder.ts +0 -80
- package/src/core/soul/taps.ts +0 -199
- package/src/core/soul/types.ts +0 -298
- package/src/core/soul/valence.ts +0 -83
- /package/src/backend/{shared → runtime}/frontends.ts +0 -0
- /package/src/backend/{shared → runtime}/metrics.ts +0 -0
- /package/src/backend/{shared → runtime}/sleep.ts +0 -0
- /package/src/backend/{shared → runtime/turn}/flow-violation.ts +0 -0
- /package/src/backend/{shared → runtime}/usage.ts +0 -0
- /package/src/core/{scripting/lua-runner.ts → scripts/lua.ts} +0 -0
- /package/src/frontend/native/{routes → bridge/routes}/index.ts +0 -0
- /package/src/frontend/native/{routes → bridge/routes}/memory.ts +0 -0
- /package/src/frontend/native/{routes → bridge/routes}/mesh.ts +0 -0
- /package/src/frontend/native/{routes → bridge/routes}/models.ts +0 -0
- /package/src/frontend/native/{routes → bridge/routes}/params.ts +0 -0
- /package/src/frontend/native/{routes → bridge/routes}/table.ts +0 -0
- /package/src/frontend/native/{tool-result.ts → turn/tool-result.ts} +0 -0
- /package/src/frontend/whatsapp/{auth-state.ts → connection/auth-state.ts} +0 -0
- /package/src/frontend/whatsapp/{identity.ts → connection/identity.ts} +0 -0
- /package/src/frontend/whatsapp/{pairing-lock.ts → connection/pairing-lock.ts} +0 -0
- /package/src/frontend/whatsapp/{pairing.ts → connection/pairing.ts} +0 -0
- /package/src/frontend/whatsapp/{pins.ts → messages/pins.ts} +0 -0
|
@@ -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
|
-
}
|
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Soul Kernel — graph centrality (Personalized PageRank).
|
|
3
|
-
*
|
|
4
|
-
* Degree centrality ("how many strong bonds does this value have") is a crude
|
|
5
|
-
* importance signal. PageRank (Page, Brin, Motwani & Winograd, 1999) is the
|
|
6
|
-
* principled one: a value is important if it is bonded to OTHER important values.
|
|
7
|
-
* We run it over the Hebbian co-activation graph so structural importance
|
|
8
|
-
* reflects the whole association network, not just local degree.
|
|
9
|
-
*
|
|
10
|
-
* The "personalized" variant biases the teleport distribution toward a set of
|
|
11
|
-
* seed nodes, yielding importance *relative to a context* — central with respect
|
|
12
|
-
* to whatever is currently active. Pure power iteration; model-free.
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
import type { SoulDag } from "./dag.js";
|
|
16
|
-
import type { Hash } from "./types.js";
|
|
17
|
-
|
|
18
|
-
export interface PageRankOptions {
|
|
19
|
-
readonly damping?: number;
|
|
20
|
-
readonly iterations?: number;
|
|
21
|
-
readonly epsilon?: number;
|
|
22
|
-
/**
|
|
23
|
-
* Optional personalization: teleport mass distribution over nodes. Values are
|
|
24
|
-
* normalized internally. Absent ⇒ uniform (classic PageRank).
|
|
25
|
-
*/
|
|
26
|
-
readonly personalization?: ReadonlyMap<Hash, number>;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Weighted Personalized PageRank over the value co-activation graph. Returns a
|
|
31
|
-
* score per live value summing to 1. Dangling nodes (no outbound bonds)
|
|
32
|
-
* redistribute their mass via the teleport vector, keeping it a proper
|
|
33
|
-
* distribution.
|
|
34
|
-
*/
|
|
35
|
-
export function pagerank(
|
|
36
|
-
dag: SoulDag,
|
|
37
|
-
opts: PageRankOptions = {},
|
|
38
|
-
): Map<Hash, number> {
|
|
39
|
-
const damping = opts.damping ?? 0.85;
|
|
40
|
-
const iterations = opts.iterations ?? 100;
|
|
41
|
-
const epsilon = opts.epsilon ?? 1e-9;
|
|
42
|
-
|
|
43
|
-
const nodes = dag.nodesOfKind("value").map((n) => n.hash);
|
|
44
|
-
const n = nodes.length;
|
|
45
|
-
const out = new Map<Hash, number>();
|
|
46
|
-
if (n === 0) return out;
|
|
47
|
-
|
|
48
|
-
const idx = new Map(nodes.map((h, i) => [h, i]));
|
|
49
|
-
|
|
50
|
-
// Weighted out-adjacency over coactivation edges (only among value nodes).
|
|
51
|
-
const outWeight = Array.from({ length: n }, () => 0);
|
|
52
|
-
const edges: { from: number; to: number; w: number }[] = [];
|
|
53
|
-
for (const from of nodes) {
|
|
54
|
-
for (const e of dag.edgesFrom(from, "coactivation")) {
|
|
55
|
-
const j = idx.get(e.to);
|
|
56
|
-
if (j === undefined || e.weight <= 0) continue;
|
|
57
|
-
const i = idx.get(from)!;
|
|
58
|
-
edges.push({ from: i, to: j, w: e.weight });
|
|
59
|
-
outWeight[i]! += e.weight;
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// Teleport / personalization vector.
|
|
64
|
-
const teleport = Array.from({ length: n }, () => 1 / n);
|
|
65
|
-
if (opts.personalization && opts.personalization.size > 0) {
|
|
66
|
-
let total = 0;
|
|
67
|
-
for (const [, w] of opts.personalization) total += Math.max(0, w);
|
|
68
|
-
if (total > 0) {
|
|
69
|
-
for (let i = 0; i < n; i++) teleport[i] = 0;
|
|
70
|
-
for (const [h, w] of opts.personalization) {
|
|
71
|
-
const i = idx.get(h);
|
|
72
|
-
if (i !== undefined) teleport[i]! += Math.max(0, w) / total;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
let rank = Array.from({ length: n }, () => 1 / n);
|
|
78
|
-
for (let it = 0; it < iterations; it++) {
|
|
79
|
-
const next = teleport.map((t) => (1 - damping) * t);
|
|
80
|
-
|
|
81
|
-
// Dangling mass (nodes with no out-edges) flows to the teleport vector.
|
|
82
|
-
let dangling = 0;
|
|
83
|
-
for (let i = 0; i < n; i++) if (outWeight[i] === 0) dangling += rank[i]!;
|
|
84
|
-
for (let i = 0; i < n; i++) next[i]! += damping * dangling * teleport[i]!;
|
|
85
|
-
|
|
86
|
-
for (const e of edges) {
|
|
87
|
-
next[e.to]! += (damping * rank[e.from]! * e.w) / outWeight[e.from]!;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
let delta = 0;
|
|
91
|
-
for (let i = 0; i < n; i++) delta += Math.abs(next[i]! - rank[i]!);
|
|
92
|
-
rank = next;
|
|
93
|
-
if (delta < epsilon) break;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
for (let i = 0; i < n; i++) out.set(nodes[i]!, rank[i]!);
|
|
97
|
-
return out;
|
|
98
|
-
}
|
package/src/core/soul/cluster.ts
DELETED
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Soul Kernel — emergent value clustering.
|
|
3
|
-
*
|
|
4
|
-
* This is where values *come from*: not authored, not seeded by a model, but
|
|
5
|
-
* discovered as clusters of evidence embeddings. The model never names a value —
|
|
6
|
-
* the cluster's medoid (its most central real fragment) is its label.
|
|
7
|
-
*
|
|
8
|
-
* The algorithm is single-pass leader/threshold clustering: each item joins the
|
|
9
|
-
* nearest existing cluster if it is within `threshold` cosine distance of that
|
|
10
|
-
* cluster's centroid, otherwise it founds a new cluster. Leader clustering is
|
|
11
|
-
* order-sensitive, so items are processed in a stable hash order to keep the
|
|
12
|
-
* output deterministic and reproducible across runs.
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
import { centroid, cosineDistance, medoidIndex } from "./embedder.js";
|
|
16
|
-
import type { Hash } from "./types.js";
|
|
17
|
-
|
|
18
|
-
export interface Embedded {
|
|
19
|
-
readonly hash: Hash;
|
|
20
|
-
readonly vector: readonly number[];
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export interface EvidenceCluster {
|
|
24
|
-
/** Evidence hashes in the cluster, in stable order. */
|
|
25
|
-
readonly members: Hash[];
|
|
26
|
-
/** The medoid evidence hash — the value's verbatim label. */
|
|
27
|
-
readonly medoid: Hash;
|
|
28
|
-
/** Cluster centroid, for downstream distance comparisons. */
|
|
29
|
-
readonly centroid: number[];
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
interface MutableCluster {
|
|
33
|
-
members: Hash[];
|
|
34
|
-
vectors: number[][];
|
|
35
|
-
centroid: number[];
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Cluster embedded evidence into emergent groups. `threshold` is the maximum
|
|
40
|
-
* cosine distance for an item to join a cluster (smaller = tighter, more
|
|
41
|
-
* clusters). Processing order is stabilized by hash so results are deterministic.
|
|
42
|
-
*/
|
|
43
|
-
export function clusterEvidence(
|
|
44
|
-
items: readonly Embedded[],
|
|
45
|
-
threshold: number,
|
|
46
|
-
): EvidenceCluster[] {
|
|
47
|
-
const sorted = [...items].sort((a, b) => a.hash.localeCompare(b.hash));
|
|
48
|
-
const clusters: MutableCluster[] = [];
|
|
49
|
-
|
|
50
|
-
for (const item of sorted) {
|
|
51
|
-
let nearest = -1;
|
|
52
|
-
let nearestDist = Infinity;
|
|
53
|
-
for (let c = 0; c < clusters.length; c++) {
|
|
54
|
-
const d = cosineDistance(item.vector, clusters[c]!.centroid);
|
|
55
|
-
if (d < nearestDist) {
|
|
56
|
-
nearestDist = d;
|
|
57
|
-
nearest = c;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
if (nearest >= 0 && nearestDist <= threshold) {
|
|
62
|
-
const cl = clusters[nearest]!;
|
|
63
|
-
cl.members.push(item.hash);
|
|
64
|
-
cl.vectors.push([...item.vector]);
|
|
65
|
-
cl.centroid = centroid(cl.vectors);
|
|
66
|
-
} else {
|
|
67
|
-
clusters.push({
|
|
68
|
-
members: [item.hash],
|
|
69
|
-
vectors: [[...item.vector]],
|
|
70
|
-
centroid: [...item.vector],
|
|
71
|
-
});
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
return clusters.map((cl) => {
|
|
76
|
-
const mi = medoidIndex(cl.vectors);
|
|
77
|
-
return {
|
|
78
|
-
members: cl.members,
|
|
79
|
-
medoid: cl.members[mi]!,
|
|
80
|
-
centroid: cl.centroid,
|
|
81
|
-
};
|
|
82
|
-
});
|
|
83
|
-
}
|
|
@@ -1,207 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Soul Kernel — the compiler. Maps structured signals to arithmetic updates.
|
|
3
|
-
*
|
|
4
|
-
* This is the "writing the self" process, and it contains no model call. Each
|
|
5
|
-
* signal routes to the pure update rules in salience.ts:
|
|
6
|
-
*
|
|
7
|
-
* reaction / engagement → reinforce or penalize the active values + Hebbian
|
|
8
|
-
* co-activation of the set that fired together.
|
|
9
|
-
* correction → store the verbatim text as evidence, append a Spine
|
|
10
|
-
* event (corrections are formative), penalize whatever
|
|
11
|
-
* values were live when it landed.
|
|
12
|
-
* directive → store the verbatim instruction as evidence for later
|
|
13
|
-
* clustering into a value (embedder phase).
|
|
14
|
-
* reflex-fire (block) → record the near-miss in the Spine.
|
|
15
|
-
* activation → co-activate the live set; a faint reinforcement.
|
|
16
|
-
*
|
|
17
|
-
* Evidence is always verbatim ground truth; the compiler never paraphrases.
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
import { SoulDag } from "./dag.js";
|
|
21
|
-
import { coactivate, reinforce } from "./salience.js";
|
|
22
|
-
import { reinforceFsrs } from "./forgetting.js";
|
|
23
|
-
import { emojiValence } from "./signals.js";
|
|
24
|
-
import type { CorrectionSignal, DirectiveSignal, Signal } from "./signals.js";
|
|
25
|
-
import type { EvidencePayload, Hash, SoulConfig } from "./types.js";
|
|
26
|
-
import { halfLifeForKind } from "./types.js";
|
|
27
|
-
|
|
28
|
-
export interface IngestResult {
|
|
29
|
-
/** Evidence node created from a correction/directive, if any. */
|
|
30
|
-
readonly evidenceAdded?: Hash;
|
|
31
|
-
/** Spine event appended, if any. */
|
|
32
|
-
readonly spineAdded?: Hash;
|
|
33
|
-
/** Nodes whose state was touched. */
|
|
34
|
-
readonly touched: readonly Hash[];
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/** Find the most recent spine node to chain a new event onto. */
|
|
38
|
-
function latestSpine(dag: SoulDag): Hash | undefined {
|
|
39
|
-
let best: Hash | undefined;
|
|
40
|
-
let bestAt = -Infinity;
|
|
41
|
-
for (const node of dag.nodesOfKind("spine")) {
|
|
42
|
-
if (node.payload.kind === "spine" && node.payload.at > bestAt) {
|
|
43
|
-
bestAt = node.payload.at;
|
|
44
|
-
best = node.hash;
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
return best;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/** Append a developmental-narrative event, chained to the prior spine node. */
|
|
51
|
-
export function appendSpine(
|
|
52
|
-
dag: SoulDag,
|
|
53
|
-
event: string,
|
|
54
|
-
at: number,
|
|
55
|
-
affects: readonly Hash[] = [],
|
|
56
|
-
): Hash {
|
|
57
|
-
return dag.addNode({
|
|
58
|
-
kind: "spine",
|
|
59
|
-
event,
|
|
60
|
-
at,
|
|
61
|
-
affects: [...affects],
|
|
62
|
-
prev: latestSpine(dag),
|
|
63
|
-
});
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function addEvidence(
|
|
67
|
-
dag: SoulDag,
|
|
68
|
-
text: string,
|
|
69
|
-
at: number,
|
|
70
|
-
origin: EvidencePayload["source"]["origin"],
|
|
71
|
-
actor?: string,
|
|
72
|
-
ref?: string,
|
|
73
|
-
): Hash {
|
|
74
|
-
return dag.addNode({
|
|
75
|
-
kind: "evidence",
|
|
76
|
-
text,
|
|
77
|
-
observedAt: at,
|
|
78
|
-
source: { origin, ...(actor ? { actor } : {}), ...(ref ? { ref } : {}) },
|
|
79
|
-
});
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function reinforceNode(
|
|
83
|
-
dag: SoulDag,
|
|
84
|
-
hash: Hash,
|
|
85
|
-
cfg: SoulConfig,
|
|
86
|
-
amount: number,
|
|
87
|
-
valence: number,
|
|
88
|
-
at: number,
|
|
89
|
-
): void {
|
|
90
|
-
const kind = dag.getNode(hash)?.payload.kind;
|
|
91
|
-
if (cfg.adaptiveForgetting) {
|
|
92
|
-
reinforceFsrs(dag.stateOf(hash), {
|
|
93
|
-
now: at,
|
|
94
|
-
cfg,
|
|
95
|
-
amount,
|
|
96
|
-
valence,
|
|
97
|
-
...(kind !== undefined ? { kind } : {}),
|
|
98
|
-
});
|
|
99
|
-
dag.touch(hash);
|
|
100
|
-
return;
|
|
101
|
-
}
|
|
102
|
-
reinforce(dag, hash, {
|
|
103
|
-
now: at,
|
|
104
|
-
halfLifeMs: halfLifeForKind(cfg, kind),
|
|
105
|
-
amount,
|
|
106
|
-
valence,
|
|
107
|
-
});
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function ingestOutcome(
|
|
111
|
-
dag: SoulDag,
|
|
112
|
-
nodes: readonly Hash[],
|
|
113
|
-
valence: number,
|
|
114
|
-
cfg: SoulConfig,
|
|
115
|
-
at: number,
|
|
116
|
-
): Hash[] {
|
|
117
|
-
const present = nodes.filter((h) => dag.hasNode(h));
|
|
118
|
-
for (const h of present) {
|
|
119
|
-
reinforceNode(dag, h, cfg, cfg.reinforce, valence, at);
|
|
120
|
-
}
|
|
121
|
-
if (present.length > 1) {
|
|
122
|
-
coactivate(dag, present, { now: at, increment: cfg.hebbIncrement });
|
|
123
|
-
}
|
|
124
|
-
return present;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
function ingestCorrection(
|
|
128
|
-
dag: SoulDag,
|
|
129
|
-
sig: CorrectionSignal,
|
|
130
|
-
cfg: SoulConfig,
|
|
131
|
-
): IngestResult {
|
|
132
|
-
const evidence = addEvidence(
|
|
133
|
-
dag,
|
|
134
|
-
sig.text,
|
|
135
|
-
sig.at,
|
|
136
|
-
"correction",
|
|
137
|
-
sig.actor,
|
|
138
|
-
sig.ref,
|
|
139
|
-
);
|
|
140
|
-
const spine = appendSpine(dag, `Correction: ${sig.text}`, sig.at, [evidence]);
|
|
141
|
-
// Penalize values that were live when the correction landed.
|
|
142
|
-
const penalized = ingestOutcome(dag, sig.activeNodes ?? [], -2, cfg, sig.at);
|
|
143
|
-
return { evidenceAdded: evidence, spineAdded: spine, touched: penalized };
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
function ingestDirective(dag: SoulDag, sig: DirectiveSignal): IngestResult {
|
|
147
|
-
const evidence = addEvidence(
|
|
148
|
-
dag,
|
|
149
|
-
sig.text,
|
|
150
|
-
sig.at,
|
|
151
|
-
"directive",
|
|
152
|
-
sig.actor,
|
|
153
|
-
sig.ref,
|
|
154
|
-
);
|
|
155
|
-
return { evidenceAdded: evidence, touched: [] };
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
/** Apply one signal to the kernel. Pure arithmetic + verbatim evidence.
|
|
159
|
-
*
|
|
160
|
-
* `valenceOf` resolves a cue's valence; pass a learned ValenceModel.valence to
|
|
161
|
-
* let meaning come from data, or omit to use the static emoji prior. */
|
|
162
|
-
export function ingest(
|
|
163
|
-
dag: SoulDag,
|
|
164
|
-
signal: Signal,
|
|
165
|
-
cfg: SoulConfig,
|
|
166
|
-
valenceOf: (cue: string) => number = emojiValence,
|
|
167
|
-
): IngestResult {
|
|
168
|
-
switch (signal.kind) {
|
|
169
|
-
case "reaction": {
|
|
170
|
-
const v = valenceOf(signal.emoji);
|
|
171
|
-
const touched = ingestOutcome(dag, signal.activeNodes, v, cfg, signal.at);
|
|
172
|
-
return { touched };
|
|
173
|
-
}
|
|
174
|
-
case "engagement": {
|
|
175
|
-
const v = signal.continued ? 1 : -1;
|
|
176
|
-
const touched = ingestOutcome(dag, signal.activeNodes, v, cfg, signal.at);
|
|
177
|
-
return { touched };
|
|
178
|
-
}
|
|
179
|
-
case "correction":
|
|
180
|
-
return ingestCorrection(dag, signal, cfg);
|
|
181
|
-
case "directive":
|
|
182
|
-
return ingestDirective(dag, signal);
|
|
183
|
-
case "reflex-fire": {
|
|
184
|
-
if (signal.severity !== "block") return { touched: [] };
|
|
185
|
-
const spine = appendSpine(
|
|
186
|
-
dag,
|
|
187
|
-
`Reflex blocked a misstep: ${signal.name}`,
|
|
188
|
-
signal.at,
|
|
189
|
-
);
|
|
190
|
-
return { spineAdded: spine, touched: [] };
|
|
191
|
-
}
|
|
192
|
-
case "activation": {
|
|
193
|
-
const touched = ingestOutcome(dag, signal.activeNodes, 0, cfg, signal.at);
|
|
194
|
-
return { touched };
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
/** Apply a batch of signals in order. */
|
|
200
|
-
export function ingestAll(
|
|
201
|
-
dag: SoulDag,
|
|
202
|
-
signals: readonly Signal[],
|
|
203
|
-
cfg: SoulConfig,
|
|
204
|
-
valenceOf: (cue: string) => number = emojiValence,
|
|
205
|
-
): IngestResult[] {
|
|
206
|
-
return signals.map((s) => ingest(dag, s, cfg, valenceOf));
|
|
207
|
-
}
|
|
@@ -1,179 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Soul Kernel — organic consolidation ("the dream").
|
|
3
|
-
*
|
|
4
|
-
* Crystallization clusters loose evidence once. Consolidation is what makes the
|
|
5
|
-
* value graph *alive*: it periodically re-examines the whole lattice and lets it
|
|
6
|
-
* reorganize as experience accumulates — values that have drifted together merge
|
|
7
|
-
* into one, carrying their learned salience and evidence forward.
|
|
8
|
-
*
|
|
9
|
-
* Two principles keep it organic rather than over-programmed:
|
|
10
|
-
*
|
|
11
|
-
* 1. The merge threshold is ADAPTIVE — derived from the current distribution of
|
|
12
|
-
* nearest-neighbor distances among values, not a magic constant. The lattice
|
|
13
|
-
* decides its own granularity from its own shape, and that shape changes as
|
|
14
|
-
* it grows.
|
|
15
|
-
*
|
|
16
|
-
* 2. Learned state is MIGRATED, never lost. A merge supersedes the old values
|
|
17
|
-
* with a new content-addressed node, sums their salience/evidence into it,
|
|
18
|
-
* and records `supersedes` edges + a Spine event. Nothing is deleted — the
|
|
19
|
-
* old nodes simply fall out of projection (filtered as superseded) and decay.
|
|
20
|
-
*
|
|
21
|
-
* No model decides what merges. Geometry and accumulated weight do.
|
|
22
|
-
*/
|
|
23
|
-
|
|
24
|
-
import type { SoulDag } from "./dag.js";
|
|
25
|
-
import { cosineDistance, medoidIndex, type Embedder } from "./embedder.js";
|
|
26
|
-
import { effectiveSalience } from "./salience.js";
|
|
27
|
-
import type { Hash, SoulConfig, ValuePayload } from "./types.js";
|
|
28
|
-
import { halfLifeForKind } from "./types.js";
|
|
29
|
-
|
|
30
|
-
/** A value is dead once something supersedes it. */
|
|
31
|
-
export function isSuperseded(dag: SoulDag, value: Hash): boolean {
|
|
32
|
-
return dag.allEdges("supersedes").some((e) => e.to === value);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/** Live (non-superseded) value nodes. */
|
|
36
|
-
export function liveValues(dag: SoulDag): Hash[] {
|
|
37
|
-
return dag
|
|
38
|
-
.nodesOfKind("value")
|
|
39
|
-
.map((n) => n.hash)
|
|
40
|
-
.filter((h) => !isSuperseded(dag, h));
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function evidenceText(dag: SoulDag, hash: Hash): string {
|
|
44
|
-
const node = dag.getNode(hash);
|
|
45
|
-
return node?.payload.kind === "evidence" ? node.payload.text : "";
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function valueOf(dag: SoulDag, hash: Hash): ValuePayload {
|
|
49
|
-
return dag.getNode(hash)!.payload as ValuePayload;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/** Median of a numeric array (0 for empty). */
|
|
53
|
-
function median(xs: number[]): number {
|
|
54
|
-
if (xs.length === 0) return 0;
|
|
55
|
-
const s = [...xs].sort((a, b) => a - b);
|
|
56
|
-
const mid = Math.floor(s.length / 2);
|
|
57
|
-
return s.length % 2 ? s[mid]! : (s[mid - 1]! + s[mid]!) / 2;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export interface ConsolidateOptions {
|
|
61
|
-
readonly now: number;
|
|
62
|
-
/** Override the adaptive threshold (mainly for tests). */
|
|
63
|
-
readonly mergeThreshold?: number;
|
|
64
|
-
/** Fraction of the median NN distance used as the adaptive threshold. */
|
|
65
|
-
readonly adaptiveFactor?: number;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
export interface ConsolidateResult {
|
|
69
|
-
readonly created: Hash[];
|
|
70
|
-
readonly superseded: Hash[];
|
|
71
|
-
readonly groups: number;
|
|
72
|
-
readonly threshold: number;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Run one consolidation pass. Returns what merged. Caller commits.
|
|
77
|
-
*/
|
|
78
|
-
export async function consolidate(
|
|
79
|
-
dag: SoulDag,
|
|
80
|
-
embedder: Embedder,
|
|
81
|
-
cfg: SoulConfig,
|
|
82
|
-
opts: ConsolidateOptions,
|
|
83
|
-
): Promise<ConsolidateResult> {
|
|
84
|
-
const values = liveValues(dag);
|
|
85
|
-
if (values.length < 2) {
|
|
86
|
-
return { created: [], superseded: [], groups: 0, threshold: 0 };
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// Represent each value by its medoid embedding.
|
|
90
|
-
const repTexts = values.map((h) => evidenceText(dag, valueOf(dag, h).medoid));
|
|
91
|
-
const reps = await embedder.embed(repTexts);
|
|
92
|
-
|
|
93
|
-
// Adaptive threshold: how close is "unusually close" for THIS lattice right now.
|
|
94
|
-
const nn: number[] = [];
|
|
95
|
-
for (let i = 0; i < values.length; i++) {
|
|
96
|
-
let best = Infinity;
|
|
97
|
-
for (let j = 0; j < values.length; j++) {
|
|
98
|
-
if (i === j) continue;
|
|
99
|
-
best = Math.min(best, cosineDistance(reps[i]!, reps[j]!));
|
|
100
|
-
}
|
|
101
|
-
if (Number.isFinite(best)) nn.push(best);
|
|
102
|
-
}
|
|
103
|
-
const factor = opts.adaptiveFactor ?? 0.6;
|
|
104
|
-
const threshold = opts.mergeThreshold ?? median(nn) * factor;
|
|
105
|
-
|
|
106
|
-
// Connected components over the "close enough to merge" graph (union-find).
|
|
107
|
-
const parent = values.map((_, i) => i);
|
|
108
|
-
const find = (x: number): number => {
|
|
109
|
-
while (parent[x] !== x) {
|
|
110
|
-
parent[x] = parent[parent[x]!]!;
|
|
111
|
-
x = parent[x]!;
|
|
112
|
-
}
|
|
113
|
-
return x;
|
|
114
|
-
};
|
|
115
|
-
const union = (a: number, b: number): void => {
|
|
116
|
-
parent[find(a)] = find(b);
|
|
117
|
-
};
|
|
118
|
-
for (let i = 0; i < values.length; i++) {
|
|
119
|
-
for (let j = i + 1; j < values.length; j++) {
|
|
120
|
-
if (cosineDistance(reps[i]!, reps[j]!) <= threshold) union(i, j);
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// Gather groups of size > 1.
|
|
125
|
-
const groups = new Map<number, number[]>();
|
|
126
|
-
for (let i = 0; i < values.length; i++) {
|
|
127
|
-
const root = find(i);
|
|
128
|
-
(groups.get(root) ?? groups.set(root, []).get(root)!).push(i);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
const created: Hash[] = [];
|
|
132
|
-
const superseded: Hash[] = [];
|
|
133
|
-
|
|
134
|
-
for (const members of groups.values()) {
|
|
135
|
-
if (members.length < 2) continue;
|
|
136
|
-
|
|
137
|
-
const oldValues = members.map((i) => values[i]!);
|
|
138
|
-
// Union of all member evidence across the merging values.
|
|
139
|
-
const evidenceSet = new Set<Hash>();
|
|
140
|
-
for (const v of oldValues) {
|
|
141
|
-
const p = valueOf(dag, v);
|
|
142
|
-
evidenceSet.add(p.medoid);
|
|
143
|
-
for (const m of p.members) evidenceSet.add(m);
|
|
144
|
-
}
|
|
145
|
-
const evidence = [...evidenceSet].sort();
|
|
146
|
-
|
|
147
|
-
// Recompute the medoid over the merged evidence — its new label emerges.
|
|
148
|
-
const evTexts = evidence.map((h) => evidenceText(dag, h));
|
|
149
|
-
const evVecs = await embedder.embed(evTexts);
|
|
150
|
-
const medoid = evidence[medoidIndex(evVecs)]!;
|
|
151
|
-
|
|
152
|
-
const merged = dag.addNode(
|
|
153
|
-
{ kind: "value", members: evidence, medoid },
|
|
154
|
-
opts.now,
|
|
155
|
-
);
|
|
156
|
-
created.push(merged);
|
|
157
|
-
|
|
158
|
-
// Migrate learned state forward: sum decayed salience + evidence + activations.
|
|
159
|
-
const target = dag.stateOf(merged);
|
|
160
|
-
for (const v of oldValues) {
|
|
161
|
-
const s = dag.stateOf(v);
|
|
162
|
-
target.salience += effectiveSalience(
|
|
163
|
-
s,
|
|
164
|
-
opts.now,
|
|
165
|
-
halfLifeForKind(cfg, "value"),
|
|
166
|
-
);
|
|
167
|
-
target.evidence += s.evidence;
|
|
168
|
-
target.activations += s.activations;
|
|
169
|
-
const e = dag.edge(merged, "supersedes", v, opts.now);
|
|
170
|
-
e.weight = 1;
|
|
171
|
-
e.updatedAt = opts.now;
|
|
172
|
-
if (v !== merged) superseded.push(v);
|
|
173
|
-
}
|
|
174
|
-
target.lastActivatedAt = opts.now;
|
|
175
|
-
dag.touch(merged);
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
return { created, superseded, groups: created.length, threshold };
|
|
179
|
-
}
|