Sprint Brief: Knowledge Architecture KA-6
Entity Weight Override + Behavioral Feedback Loop
What You’re Building
KA-1 through KA-5 built the knowledge graph, wired traversal retrieval, prepared structural features for the predictor, and added dashboard visibility. But the system has a fundamental gap: it can only refine what it already knows is important. There is no mechanism to front-load importance on an entity before behavioral data proves it, and no mechanism for behavioral outcomes to flow back into the structural graph.
KA-6 closes this loop in two ways:
-
Entity weight override — users (or the system) can pin entities as focal, ensuring they’re always traversed regardless of project path matching or query tokens. This is the exploration mechanism: a bet that an entity matters before the evidence accumulates.
-
Behavioral feedback — session outcomes flow back into the graph. FTS overlap (memories the user actually searched for during a session) adjusts aspect weights. Per-entity predictor win rates surface as graph health signals. Superseded memory labels propagate to entity_attributes status.
Think of it as: KA-1-5 built the map and the instruments. KA-6 lets you place pins on the map and lets the territory reshape the map over time.
Why This Sprint Exists
Read docs/KNOWLEDGE-ARCHITECTURE.md, section “Love, Hate, and the
Exploration Problem” before writing any code.
The entire pipeline up to this point is exploitation — refining what the system already knows. This sprint adds the exploration mechanism.
Weight override is love expressed as architecture. It is the only way to assign importance to something before the evidence justifies it. Without it, the system only maps what it has evidence is worth mapping. With it, the system maps the unknown because something in there matters to the user even before they know what they will find.
The manual pin and the predictor’s learned intuition are the same mechanism at different scales. The pin is explicit: “this matters.” The predictor is implicit: “based on patterns I’ve learned, this should matter.” The pin is training data for the predictor. Over time, the predictor starts making the same bets the user makes manually — that is the system learning to explore on its own.
FTS overlap feedback is the system learning from its own bets. When a memory is injected at session-start and the user later searches for it (FTS hit), that’s behavioral confirmation — the bet paid off. That signal flows back to aspect weights, raising the importance of aspects that produce useful memories. Without this feedback loop, aspect weights stagnate and the structural graph diverges from what the user actually needs.
Aspect decay is the system forgetting what no longer serves it. Without decay, weights only go up. Aspects that were important six months ago but haven’t been confirmed recently hold the same weight as aspects confirmed yesterday. Decay ensures the graph reflects current reality, not historical accumulation.
Weight override at minimum is hate — “stop surfacing this.” The floor matters as much as the ceiling. The system needs to learn to stop betting on entities that don’t pay off just as much as it needs to learn to bet on new ones.
The danger: love can build enormous infrastructure around an entity that turns out to be wrong. The most catastrophic load-bearing failures come from betting too hard on the wrong thing. The comparison slices (per-entity win rate, trend) are the safety net — they tell you when a bet is failing before the infrastructure collapses.
If the implementation focuses only on the technical mechanics without understanding this, it will optimize metrics without serving the user. Every deliverable in this sprint exists to answer one of two questions: “what does the user need to know right now?” (exploitation) or “what does the user need to explore right now?” (exploration). The tension between those questions is what makes the system alive rather than merely efficient.
Required Reading
docs/KNOWLEDGE-ARCHITECTURE.md— start here, especially “Love, Hate, and the Exploration Problem”. This explains WHY every deliverable in this sprint exists, not just what it does.docs/specs/INDEX.md— Integration Contracts, especially “Knowledge Architecture <-> Predictive Scorer”docs/specs/complete/knowledge-architecture-schema.md— sections 7 (retrieval contract) and 8 (predictor integration)docs/specs/SPRINT-BRIEF-KA4.md— predictor comparison slices (the behavioral signal source)docs/specs/SPRINT-BRIEF-KA5.md— dashboard Knowledge tab (where feedback visibility lives)
Prerequisites
KA-1 through KA-5 must be complete:
- Knowledge graph populated, traversal wired, structural features assembled
predictor_comparisonstable exists with per-entity slicessession_memoriestable tracksfts_hit_countandsource- Knowledge tab exists in dashboard
resolveFocalEntities()has the priority chain: checkpoint > project > query
Deliverables
1. Entity pinning
Entity pinning is the manual exploration mechanism. It is how the
user says “this matters” before the system has evidence. See
docs/KNOWLEDGE-ARCHITECTURE.md section “Love, Hate, and the
Exploration Problem” for the full rationale.
A pinned entity is always included as focal during traversal. This means its aspects, attributes, constraints, and dependencies are walked every session — regardless of whether the project path or query tokens would have resolved it. The predictor then observes whether this bet pays off through normal comparison scoring. Over time, the predictor learns the pattern and starts making similar bets autonomously.
The pin at maximum is exploration. The pin at minimum (or unpin + low weight) is pruning — “stop betting on this.” Both directions matter.
Migration: New migration 022-entity-pinning.ts:
ALTER TABLE entities ADD COLUMN pinned INTEGER NOT NULL DEFAULT 0;
ALTER TABLE entities ADD COLUMN pinned_at TEXT;
Use the standard addColumnIfMissing / PRAGMA table_info pattern.
Register as version 22 in packages/core/src/migrations/index.ts.
Knowledge graph helpers: Add to
packages/daemon/src/knowledge-graph.ts:
export function pinEntity(
accessor: DbAccessor,
entityId: string,
agentId: string,
): void;
export function unpinEntity(
accessor: DbAccessor,
entityId: string,
agentId: string,
): void;
export function getPinnedEntities(
accessor: DbAccessor,
agentId: string,
): ReadonlyArray<{ id: string; name: string; pinnedAt: string }>;
pinEntity sets pinned = 1, pinned_at = now() where
id = ? AND agent_id = ?. unpinEntity sets pinned = 0, pinned_at = NULL. getPinnedEntities returns all pinned entities
for an agent, ordered by pinned_at DESC.
Focal entity resolution: Update resolveFocalEntities() in
packages/daemon/src/pipeline/graph-traversal.ts. Pinned entities
are priority 0 — they are ALWAYS included as focal entities,
regardless of which signal source resolves additional entities.
New resolution order:
- Pinned entities — always included (union, not replacement)
- Checkpoint entity IDs — if recovery checkpoint has them
- Project path — match against entity canonical_name
- Query tokens — tokenize and match
The key change: pinned entities are unioned with whatever other source resolves, not a fallback. If the user pins entity A and the project path resolves entity B, both A and B are focal.
Update FocalEntityResult to track pinned vs resolved:
export interface FocalEntityResult {
readonly entityIds: string[];
readonly entityNames: string[];
readonly pinnedEntityIds: string[];
readonly source: "project" | "checkpoint" | "query" | "session_key";
}
The source field still reflects the non-pinned resolution source.
pinnedEntityIds is the subset that came from pinning.
API endpoints: Add to packages/daemon/src/daemon.ts:
POST /api/knowledge/entities/:id/pin
?agent_id=default
Returns: { pinned: true, pinnedAt: string }
DELETE /api/knowledge/entities/:id/pin
?agent_id=default
Returns: { pinned: false }
GET /api/knowledge/entities/pinned
?agent_id=default
Returns: Array<{ id, name, pinnedAt }>
Dashboard: Add a pin/unpin toggle to the entity detail panel in the Knowledge tab (KA-5). A small pin icon button next to each entity name. Pinned entities get a visual indicator (badge or highlight) in the entity list.
2. Aspect weight feedback from FTS overlap
This is the system learning from its own bets. Without this feedback loop, aspect weights are set once by structural assignment and never updated. The structural graph diverges from what the user actually needs. With it, the graph reshapes itself based on outcomes.
Where: packages/daemon/src/pipeline/aspect-feedback.ts (new)
When a session ends, the system already knows which memories were
injected at session-start and which the user actually searched for
during the session (via fts_hit_count in session_memories).
Memories with FTS hits are behaviorally confirmed — the user went
looking for them, validating that they were relevant.
This signal should flow back to aspect weights:
export interface AspectFeedbackResult {
readonly aspectsUpdated: number;
readonly totalFtsConfirmations: number;
}
/**
* After a session ends, compute FTS overlap feedback and adjust
* aspect weights for confirmed memories.
*
* For each memory with fts_hit_count > 0:
* 1. Look up its entity_attributes row to find its aspect_id
* 2. Increment the aspect's weight by a small delta
*
* Weight adjustment: aspect.weight += delta * confirmations,
* clamped to [0.1, 1.0]. Default delta = 0.02 per confirmation.
*/
export function applyFtsOverlapFeedback(
accessor: DbAccessor,
sessionKey: string,
agentId: string,
config: {
readonly delta: number; // default 0.02
readonly maxWeight: number; // default 1.0
readonly minWeight: number; // default 0.1
},
): AspectFeedbackResult;
Implementation:
-
Query
session_memoriesfor the session wherefts_hit_count > 0:SELECT memory_id, fts_hit_count FROM session_memories WHERE session_key = ? AND fts_hit_count > 0 -
For each confirmed memory, look up its
entity_attributesrow to getaspect_id:SELECT aspect_id FROM entity_attributes WHERE memory_id = ? AND agent_id = ? AND status = 'active' LIMIT 1 -
Accumulate confirmations per aspect_id, then batch-update:
UPDATE entity_aspects SET weight = MIN(?, weight + ? * ?), updated_at = ? WHERE id = ? AND agent_id = ?
Wiring: Call applyFtsOverlapFeedback during session-end
processing in hooks.ts, after recordSessionCandidates and before
the session summary. Guard with a config flag:
readonly feedback?: {
readonly enabled: boolean; // default true
readonly ftsWeightDelta: number; // default 0.02
readonly maxAspectWeight: number; // default 1.0
readonly minAspectWeight: number; // default 0.1
readonly decayEnabled: boolean; // default true
readonly decayRate: number; // default 0.005
readonly decayIntervalSessions: number; // default 10
};
Add to PipelineV2Config in packages/core/src/types.ts and wire
defaults in packages/daemon/src/memory-config.ts.
3. Aspect weight decay
Where: packages/daemon/src/pipeline/aspect-feedback.ts
Without decay, aspect weights only go up. Aspects that were important six months ago but haven’t been confirmed recently should lose weight over time.
/**
* Apply passive decay to aspect weights.
* Run periodically (e.g., every N sessions or daily via maintenance).
*
* weight = max(minWeight, weight - decayRate)
* Only aspects not updated in the last N sessions are decayed.
*/
export function decayAspectWeights(
accessor: DbAccessor,
agentId: string,
config: {
readonly decayRate: number; // default 0.005
readonly minWeight: number; // default 0.1
readonly staleDays: number; // default 14
},
): number; // returns count of aspects decayed
Implementation:
UPDATE entity_aspects
SET weight = MAX(?, weight - ?),
updated_at = ?
WHERE agent_id = ?
AND updated_at < ? -- older than staleDays
AND weight > ? -- above minimum
Wiring: Call from the maintenance worker
(packages/daemon/src/pipeline/maintenance-worker.ts) on a
configurable interval — default every 10 sessions or daily,
whichever comes first.
4. Per-entity health signal
Where: packages/daemon/src/knowledge-graph.ts
Add a helper that computes per-entity health from predictor comparison data:
export interface EntityHealth {
readonly entityId: string;
readonly entityName: string;
readonly comparisonCount: number;
readonly winRate: number;
readonly avgMargin: number;
readonly trend: "improving" | "stable" | "declining";
}
/**
* Compute per-entity predictor health from comparison slices.
* Returns entities with enough comparison data to be meaningful
* (minimum 3 comparisons).
*/
export function getEntityHealth(
accessor: DbAccessor,
agentId: string,
since?: string,
minComparisons?: number, // default 3
): ReadonlyArray<EntityHealth>;
trend is computed by comparing win rate in the first half vs
second half of the comparison window. If second half win rate is
10% higher → improving, >10% lower → declining, else stable.
API endpoint:
GET /api/knowledge/entities/health
?agent_id=default
?since=2026-03-01
?min_comparisons=3
Returns: Array<EntityHealth>
Dashboard: Add a health indicator to entity cards in the Knowledge tab. Entities with comparison data show a small colored dot: green (improving/high win rate), yellow (stable/medium), red (declining/low win rate). Entities without comparison data show no indicator.
5. Superseded memory propagation
Where: packages/daemon/src/knowledge-graph.ts
When a memory is superseded (via supersedeAttribute), its
entity_attributes row is already marked status = 'superseded'.
But if the memory itself is later deleted or superseded in the
memories table, the entity_attributes status doesn’t update
automatically.
Add a maintenance function:
/**
* Find entity_attributes rows whose linked memory has been
* deleted or superseded, and update their status accordingly.
* Run during maintenance sweeps.
*/
export function propagateMemoryStatus(
accessor: DbAccessor,
agentId: string,
): number; // returns count of attributes updated
Implementation:
UPDATE entity_attributes
SET status = 'superseded', updated_at = ?
WHERE agent_id = ?
AND status = 'active'
AND memory_id IS NOT NULL
AND memory_id NOT IN (
SELECT id FROM memories WHERE is_deleted = 0
)
Wiring: Call from the maintenance worker alongside the existing retention sweep.
6. Feedback telemetry
Add feedback metrics to the session-end log entry and the
/api/pipeline/status endpoint:
// Session-end telemetry
feedbackAspectsUpdated: number;
feedbackFtsConfirmations: number;
feedbackDecayedAspects: number;
feedbackPropagatedAttributes: number;
Also add to the Knowledge tab stats section:
- Aspects updated by feedback (last 7 days)
- Average aspect weight across all entities
- Aspects at max weight (1.0)
- Aspects at min weight (0.1)
Data source: compute from entity_aspects table directly.
7. Feedback configuration
Add to PipelineV2Config in packages/core/src/types.ts:
readonly feedback?: {
readonly enabled: boolean;
readonly ftsWeightDelta: number;
readonly maxAspectWeight: number;
readonly minAspectWeight: number;
readonly decayEnabled: boolean;
readonly decayRate: number;
readonly staleDays: number;
};
Wire defaults in packages/daemon/src/memory-config.ts with YAML
parsing, same pattern as traversal and structural configs.
Guard: feedback only runs when feedback.enabled && graph.enabled.
Key Files
packages/core/src/migrations/022-entity-pinning.ts— new migration for pinned columnpackages/core/src/migrations/index.ts— register migration 022packages/core/src/types.ts— feedback config typespackages/daemon/src/knowledge-graph.ts— pin/unpin helpers, entity health, superseded propagationpackages/daemon/src/pipeline/graph-traversal.ts— pinned entity resolution inresolveFocalEntities()packages/daemon/src/pipeline/aspect-feedback.ts— new, FTS overlap feedback and aspect decaypackages/daemon/src/pipeline/maintenance-worker.ts— wire decay and superseded propagationpackages/daemon/src/hooks.ts— wire FTS feedback at session-endpackages/daemon/src/daemon.ts— pin/unpin and health API endpointspackages/daemon/src/memory-config.ts— feedback config defaultspackages/cli/dashboard/src/lib/components/tabs/KnowledgeTab.svelte— pin toggle, health indicators, feedback stats
What NOT to Build
- Automatic entity pinning based on heuristics (future — manual only for now)
- Aspect weight feedback from predictor scores (requires scorer phase 3 integration, not just comparisons)
- Cross-entity weight balancing (e.g., reducing weight on entity A when entity B gains weight — adds complexity for unclear benefit)
- Entity archiving or lifecycle management (separate concern)
- Graph visualization with force-directed layout (still future)
- Traversal depth modulation based on win rates (future — KA-6 surfaces the signal, doesn’t act on it automatically)
Verification
bun run build— no type errorsbun test— existing tests passbun run typecheck— clean- Migration 022 adds
pinnedandpinned_atcolumns idempotently pinEntity()/unpinEntity()work correctlyresolveFocalEntities()always includes pinned entities alongside other resolved entities- Pinned entity appears as focal in session-start regardless of project path
- Pin/unpin API endpoints work with correct agent_id scoping
- Dashboard pin toggle updates entity state and refreshes view
- FTS overlap feedback increases aspect weight for confirmed aspects
- Weight stays clamped to [0.1, 1.0]
- Feedback is a no-op when no FTS hits exist for the session
- Aspect decay reduces weights on stale aspects
- Decay respects minWeight floor
- Superseded propagation catches orphaned entity_attributes
- Entity health computes correct win rate and trend
- Health indicators appear on entity cards in dashboard
- Feedback telemetry appears in session-end logs
- Feedback config respects
enabledflag - Everything is a no-op when KA tables don’t exist