Class PersonalityMutationStore

Persistence layer for personality mutations with decay, on SQLite or PostgreSQL through the storage adapter.

Follows the same ensureSchema() pattern as EmergentToolRegistry: a cached promise guards against concurrent DDL execution, and all DML methods await schema readiness before proceeding. A failed schema step is not cached: the next call runs it again.

Example

const store = new PersonalityMutationStore(storageAdapter);

// Record a mutation
const id = await store.record({
agentId: 'agent-42',
trait: 'openness',
delta: 0.1,
reasoning: 'User prefers creative responses',
baselineValue: 0.7,
mutatedValue: 0.8,
});

// Get strength-weighted effective deltas
const deltas = await store.getEffectiveDeltas('agent-42');
// => { openness: 0.1 } (strength is 1.0 initially)

// Decay all mutations by 5%
const { decayed, pruned } = await store.decayAll(0.05);

Constructors

Methods

  • Record a new personality mutation.

    Inserts a mutation record with initial strength of 1.0 and the current timestamp. The mutation ID is generated deterministically from the current time and a random suffix.

    Parameters

    Returns Promise<string>

    The generated mutation ID.

  • Compute the effective (strength-weighted) delta for each trait.

    For each active mutation, multiplies the raw delta by the mutation's current strength, then sums per trait. This gives a realistic picture of how much each trait has drifted from baseline, accounting for decay.

    Parameters

    • agentId: string

      The agent whose effective deltas to compute.

    Returns Promise<Record<string, number>>

    A map of trait name to effective delta (sum of delta * strength).

  • Decay all active mutations by the given rate and prune expired ones.

    Parameters

    • rate: number

      The amount to subtract from each mutation's strength. Typically 0.05 (the default from SelfImprovementConfig).

    Returns Promise<DecayResult>

    A DecayResult with counts of decayed and pruned mutations.

    Deprecated

    for production paths — use decayForAgent: this method is UNSCOPED (touches every agent's rows) and non-atomic (row-by-row writes double-decay on retry). Kept for administrative / maintenance use only.

    For each mutation with strength above 0.1:

    • Subtracts rate from its strength.
    • If the new strength is at or below 0.1, the mutation is deleted (pruned).
    • Otherwise, the strength is updated in place.

    This implements Ebbinghaus-style forgetting: mutations that aren't reinforced by repeated adaptation gradually fade away.

  • Agent-scoped, idempotent, atomic decay (spec batch-1 C6).

    Runs one transaction that (a) inserts the (agent_id, cycle_id) guard row FIRST — a replayed cycle id aborts as a no-op before touching any mutation row; (b) deletes this agent's rows already at/below the 0.1 prune threshold (pre-existing sub-threshold rows never decay again under decayAll's > 0.1 selector — this closes that leak); (c) decays the remaining rows, pruning any whose post-decay strength is at or below the threshold. Requires a transaction-capable adapter — the decay unit must be all-or-nothing so a mid-cycle failure cannot double-decay on retry. Runs on SQLite (3.24.0 or later, for the guard's ON CONFLICT clause) and on PostgreSQL.

    Each call writes a fresh randomUUID() token into the guard row's owner_token and the time into its applied_at, reads the token back, and decays only when the row holds its own token; every other call for that cycle returns skipped. A token belongs to one call, so two calls in one millisecond never both own a cycle. A guard row an earlier release wrote has no owner_token, so its cycle stays applied. Before its transaction the first call adds owner_token to a guard table an earlier release made without it; when that ALTER fails, the call fails and the next one runs it again.

    Parameters

    • agentId: string

      The agent whose mutations decay (other agents' rows are untouched — the class defect in decayAll).

    • rate: number

      Strength subtracted per cycle (typically SelfImprovementConfig.personality.decayRate, default 0.05).

    • cycleId: string

      Stable-per-cycle identity (e.g. day:2026-07-20 UTC buckets from decay-on-adapt). Replays are no-ops via the guard table.

    Returns Promise<DecayResult & {
        skipped?: boolean;
    }>