bikky 0.4.3 → 0.4.4

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 CHANGED
@@ -2,48 +2,69 @@
2
2
 
3
3
  <p align="center"><b>Persistent memory for AI coding agents — built for teams and multi-agent engineering workflows.</b></p>
4
4
 
5
- bikky gives AI coding agents (GitHub Copilot, Claude Code, Cursor, and other MCP clients) long-term memory that persists across sessions, across tools, and across your whole team. When multiple engineers, agents, or repos need to build on the same knowledge base, bikky captures what's learned *during* sessions so future sessions start smarter.
6
-
7
- ### Who it's for
8
-
9
- - 👥 **Teams & software factories** — What one engineer's agent learns today, every agent on the team can recall tomorrow. Shared memory turns institutional knowledge into something queryable instead of tribal — onboarding accelerates, conventions stop drifting, and the same lesson never gets re-learned twice.
10
- - 🤖 **Multi-agent engineering workflows** — Multiple Cursor / Claude Code / Copilot sessions can share codebase context, conventions, and recent decisions instead of re-learning them from scratch.
11
-
12
5
  <p align="center">
13
6
  <img src="https://raw.githubusercontent.com/bikky-dev/bikky/main/docs/diagrams/team-memory.svg" alt="Memory — facts flow from individual sessions into a self-curating knowledge store shared across your team" width="720" />
14
7
  </p>
15
8
 
16
- <p align="center"><i>Knowledge flows from every session into a store that curates itself over time — deduplicating, distilling, and decaying stale facts — so every future session starts smarter across the team.</i></p>
9
+ <p align="center"><i>Selected knowledge flows from supported sessions into a store that curates itself over time — deduplicating, distilling, and decaying stale facts — so future sessions can start with more context.</i></p>
17
10
 
18
- ---
11
+ bikky provides long-term memory for MCP-capable AI coding agents. It exposes memory tools over the Model Context Protocol (MCP), stores facts in Qdrant, and can run a local daemon that extracts durable facts from supported transcript sources. Teams can share memory across tools, repos, and engineers without treating chat history or closed PRs as the source of truth.
19
12
 
20
- ### The problem
13
+ ### Who it's for
14
+
15
+ - 👥 **Teams & software factories** — What one engineer's agent learns today can be recalled by other agents on the team tomorrow. Shared memory makes institutional knowledge queryable, helps onboarding, and reduces convention drift and repeated rediscovery.
16
+ - 🤖 **Multi-agent engineering workflows** — Multiple MCP-capable agent sessions can share codebase context, conventions, and recent decisions instead of re-learning them from scratch.
21
17
 
22
- The most valuable things you and your agents learn — why a config value exists, which deploy step matters, what broke last quarter, the convention you settled on yesterday — happen *during* sessions. And then they vanish when the session closes. Across teams, repos, and tools, knowledge still lives in heads, chat threads, and closed PRs, and every new agent session has to learn it from scratch. Hand-written docs drift the moment they're published.
18
+ ---
23
19
 
24
- ### How bikky solves it
20
+ ### How bikky works
25
21
 
26
- bikky gives your agent memory tools and runs a small background service after `bikky setup`. You keep working normally; bikky captures useful facts, organizes them, recalls them in future sessions, and keeps the store tidy over time.
22
+ bikky gives your agent memory tools and runs a small background service after `bikky setup`. You keep working normally; bikky captures useful facts from supported transcript sources, organizes them, recalls them in future sessions, and keeps the store tidy over time.
27
23
 
28
- - **Capture** — Facts are extracted automatically from session transcripts; no manual docs to write.
29
- - **Classify** — Memories are grouped as **engineering**, **product**, **human**, or **system** so they stay easy to browse and filter.
30
- - **Recall** — Every new session, yours or a teammate's, recalls from the same store via semantic search.
24
+ - **Capture** — Facts are extracted automatically from supported session transcripts without requiring manual notes for every fact.
25
+ - **Classify** — Memories are grouped as **engineering**, **product**, or **system** so they stay easy to browse and filter.
26
+ - **Recall** — New sessions can recall from the same store via semantic search.
31
27
  - **Curate** — bikky merges duplicates, fades stale facts, resolves contradictions, distills recurring patterns, and builds an entity graph over time.
32
- - **Compound** — Session 50 is dramatically better than session 1 because memory accumulates.
28
+ - **Compound** — Later sessions can start with more context because memory accumulates.
33
29
  - **Route** — Optionally keep team, client, or environment-specific memory in separate Qdrant destinations from one install. See [separate memory stores](#optional-separate-memory-stores).
34
30
 
35
31
  Subtypes keep recall precise without making setup harder:
36
32
 
37
- - **Engineering** — codebase maps, architecture decisions, infra topology, access patterns, operational procedures, troubleshooting gotchas, and conventions.
33
+ - **Engineering** — codebase maps, architecture decisions, infra topology, access patterns, operational procedures, troubleshooting gotchas, conventions, preferences, person/ownership context, working agreements, and durable activity events.
38
34
  - **Product** — domain rules, product decisions, requirements, user workflows, roadmap items, success metrics, and market insights.
39
- - **Human** — preferences, person profiles, ownership notes, working agreements, and activity events.
40
35
  - **System** — session indexes, episodes, workstreams, and feedback signals.
41
36
 
42
37
  ---
43
38
 
39
+ ## Supported integrations
40
+
41
+ bikky has two integration surfaces: MCP tool access for agents and optional background transcript capture. Tool access is broader than transcript capture.
42
+
43
+ ### Coding agents and MCP clients
44
+
45
+ | Client or agent | MCP tool access | `bikky setup` registration | Background transcript capture |
46
+ | --- | --- | --- | --- |
47
+ | GitHub Copilot | Supported | Supported via `~/.copilot/mcp-config.json` | Supported from `~/.copilot/session-state` |
48
+ | Claude Code | Supported | Supported via the `claude` CLI or `~/.claude.json` fallback | Supported from `~/.claude/projects` |
49
+ | Cursor and other stdio MCP clients | Standard MCP server is available via `npx -y bikky mcp` | Not auto-configured today | No built-in watcher today |
50
+
51
+ If your client can launch a stdio MCP server, it can use bikky's memory tools after manual configuration. bikky does not currently ship Cursor-specific setup or transcript parsing. Automatic transcript ingestion is implemented for GitHub Copilot and Claude Code.
52
+
53
+ ### Storage and model providers
54
+
55
+ | Component | Supported today | Notes |
56
+ | --- | --- | --- |
57
+ | Vector store | Qdrant | Local Docker, Qdrant Cloud, or self-hosted Qdrant. Qdrant is required. |
58
+ | `embedding.provider` | `ollama`, `openai`, `bedrock`, `portkey` | Used to embed memories for semantic search. |
59
+ | `llm.provider` | `ollama`, `openai`, `bedrock`, `portkey` | Used for extraction, curation, distillation, and relation inference. |
60
+
61
+ Portkey support means bikky talks to Portkey as the configured gateway; upstream model availability, routing, and fallbacks are controlled by your Portkey configuration. Providers not listed above are not built in today, but the provider registry is designed to make additions small and reviewable.
62
+
63
+ ---
64
+
44
65
  ## Quick start
45
66
 
46
- This is the fastest path to a working memory store: Qdrant runs locally, while hosted embeddings and LLM calls provide strong extraction and recall quality without running local models.
67
+ This is the fastest path to a working memory store: Qdrant runs locally, while hosted embeddings and LLM calls handle extraction and recall without running local models.
47
68
 
48
69
  ```bash
49
70
  # 1. Pull and run Qdrant (vector store)
@@ -60,7 +81,7 @@ cat > ~/.bikky/config.json <<'JSON'
60
81
  "embedding": {
61
82
  "provider": "openai",
62
83
  "model": "text-embedding-3-small",
63
- "dimensions": 1536,
84
+ "dimensions": 1024,
64
85
  "api_key": "sk-..."
65
86
  },
66
87
  "llm": {
@@ -73,19 +94,19 @@ JSON
73
94
  # qdrant_api_key is optional; leave it empty or omit it for local Qdrant.
74
95
  # Prefer env vars? Omit api_key above and set OPENAI_API_KEY instead.
75
96
 
76
- # 3. Register bikky with your editor and start the background service
77
- bikky setup # writes MCP config for Copilot + Claude Code, then starts the daemon
97
+ # 3. Register bikky with supported clients and start the background service
98
+ bikky setup # writes MCP config for GitHub Copilot + Claude Code, then starts the daemon
78
99
  ```
79
100
 
80
101
  `npm install -g bikky` runs a best-effort postinstall setup hook for convenience. It never fails the install, and you should still run `bikky setup` after writing your config to make setup explicit and repeatable.
81
102
 
82
- Restart your editor. The memory tools appear automatically in supported MCP clients.
103
+ Restart your editor. The memory tools appear automatically in GitHub Copilot and Claude Code; configure other stdio MCP clients manually with `npx -y bikky mcp`.
83
104
 
84
105
  ```bash
85
106
  bikky status # confirms Qdrant, embeddings, daemon, and UI health
86
107
  ```
87
108
 
88
- That's it. You can keep Qdrant local forever, or move the vector store to Qdrant Cloud later for a shared team setup.
109
+ At this point, you can continue with local Qdrant or move the vector store to Qdrant Cloud later for a shared team setup.
89
110
 
90
111
  For other deployment shapes — fully hosted, 100% local, or hosted Qdrant with local models — see [Setup options](#setup-options).
91
112
 
@@ -105,18 +126,18 @@ bikky supports four common setup shapes. Pick based on where you want Qdrant to
105
126
  | **LLM** | One provider | Portkey · OpenAI · Ollama · Bedrock |
106
127
  | **Docker** *(optional)* | Only if you run Qdrant locally | Docker Desktop, OrbStack, colima, etc. |
107
128
 
108
- Both `embedding.provider` and `llm.provider` accept the same values: `ollama`, `openai`, `bedrock`, or `portkey`. **Portkey is the easiest cloud option** one API key, any upstream provider, with built-in routing/fallbacks. Bikky's canonical embedding dimension is **1024**, portable across every modern provider.
129
+ Both `embedding.provider` and `llm.provider` accept the same values: `ollama`, `openai`, `bedrock`, or `portkey`. Portkey can be used as a hosted gateway when you want one configured provider in bikky and upstream routing/fallbacks managed outside bikky. The documented examples use **1024-dimensional embeddings** because that size works across the built-in provider examples. Some providers expose larger native dimensions (for example OpenAI `text-embedding-3-small` can return 1536), but using 1024 keeps the documented setup portable without rebuilding every collection.
109
130
 
110
131
  > ⚠️ **Qdrant Cloud free tier does not include automatic backups.** Deleted collections cannot be recovered. If your memory data is valuable, use a paid Qdrant Cloud plan (which includes daily backups), run Qdrant locally with your own backup strategy, or periodically export snapshots via the [Qdrant snapshots API](https://qdrant.tech/documentation/concepts/snapshots/).
111
132
 
112
133
  ### Choose a setup
113
134
 
114
- | Setup | Best for | Config |
115
- | -------------------------------- | -------------------------------------------------------------- | ------------------------------------------------------------------------- |
116
- | **Fully hosted** | Best performance and teams; managed vector storage and models | [Fully hosted config][fully-hosted-config] |
117
- | **Local Qdrant + hosted models** | Local vector storage with hosted extraction and embedding | [Hosted models config][hosted-models-config] |
118
- | **Local and free** | Local evaluation; quality depends on local models | [Local config guide][local-config] |
119
- | **Hosted Qdrant + local Ollama** | Shared vector storage while keeping model calls local | [Hosted Qdrant + local models][hosted-qdrant-local-models-config] |
135
+ | Setup | Use when | Config |
136
+ | -------------------------------- | -------------------------------------------------------------- | ----------------------------------------------------------------------- |
137
+ | **Fully hosted** | Teams want managed vector storage and hosted models | [Fully hosted config][fully-hosted-config] |
138
+ | **Local Qdrant + hosted models** | You want local vector storage with hosted extraction/embedding | [Hosted models config][hosted-models-config] |
139
+ | **Local and free** | You are evaluating locally and can accept local-model quality | [Local config guide][local-config] |
140
+ | **Hosted Qdrant + local Ollama** | You want shared vectors while keeping model calls local | [Hosted Qdrant + local models][hosted-qdrant-local-models-config] |
120
141
 
121
142
  ### Configuration basics
122
143
 
@@ -196,9 +217,9 @@ bikky-ui # opens http://localhost:1422
196
217
  <p align="center"><i>Dashboard — memory stats, category breakdown, and recent facts at a glance</i></p>
197
218
 
198
219
  <p align="center">
199
- <img src="https://raw.githubusercontent.com/bikky-dev/bikky/main/docs/screenshots/memory.png" alt="Memory browser — search, filter, and browse all stored facts" width="720" />
220
+ <img src="https://raw.githubusercontent.com/bikky-dev/bikky/main/docs/screenshots/memory.png" alt="Memory browser — search, filter, and browse current user-facing memories" width="720" />
200
221
  </p>
201
- <p align="center"><i>Memory browser — search, filter by category/kind/origin, and browse all stored facts</i></p>
222
+ <p align="center"><i>Memory browser — search, filter by category, subtype, entity, usefulness, date, and sort order</i></p>
202
223
 
203
224
  <p align="center">
204
225
  <img src="https://raw.githubusercontent.com/bikky-dev/bikky/main/docs/screenshots/graph.png" alt="Entity graph — interactive visualization of entity relationships" width="720" />
@@ -207,11 +228,15 @@ bikky-ui # opens http://localhost:1422
207
228
 
208
229
  The UI reads from your existing `~/.bikky/config.json` (or `BIKKY_HOME/config.json`) — no extra configuration required.
209
230
 
231
+ By default, the dashboard, memory list, and search results show current user-facing memories only. Internal telemetry, system lifecycle summaries (`session_index`, `episode`, `workstream`), entity sidecars, and superseded archive records are hidden from the main views so counts match what you normally mean by "memories." Diagnostic API queries can still request those records explicitly, including superseded records with `include_superseded=true`.
232
+
233
+ Memory cards and detail pages also surface provenance from canonical `origin` metadata: the configured user, origin surface/operation, agent, last operation, repo, branch, workstream, task, session, and episode when present. Older records that only have legacy `source`, `actor_id`, or `metadata.actor_label` still display useful fallback labels.
234
+
210
235
  ## CLI
211
236
 
212
237
  ```bash
213
238
  bikky mcp # start MCP server (stdio) — used by editors
214
- bikky setup # install MCP configs for Copilot + Claude Code, then start the daemon
239
+ bikky setup # install MCP configs for GitHub Copilot + Claude Code, then start the daemon
215
240
  bikky start # alias for setup
216
241
  bikky stop # stop the background daemon
217
242
  bikky daemon # run the daemon in the foreground
package/dist/config.d.ts CHANGED
@@ -61,6 +61,10 @@ export interface DaemonConfig {
61
61
  entity_typing_enabled: boolean;
62
62
  entity_typing_interval_sec: number;
63
63
  entity_typing_max_entities_per_run: number;
64
+ memory_quality_rollups_enabled: boolean;
65
+ memory_quality_rollups_interval_sec: number;
66
+ memory_quality_rollups_low_confidence_threshold: number;
67
+ memory_quality_rollups_max_scopes_per_run: number;
64
68
  staleness_threshold_days: number;
65
69
  }
66
70
  export interface QdrantClientConfig {
@@ -168,7 +172,7 @@ export interface ConfigFileDiagnostics {
168
172
  issues: ConfigIssue[];
169
173
  }
170
174
  declare const DEFAULTS: BikkyConfig;
171
- export declare const CONFIG_ENV_KEYS: readonly ["QDRANT_URL", "QDRANT_API_KEY", "BIKKY_COLLECTION", "EMBEDDING_PROVIDER", "EMBEDDING_MODEL", "EMBEDDING_BASE_URL", "EMBEDDING_DIMENSIONS", "OPENAI_API_KEY", "PORTKEY_API_KEY", "LLM_PROVIDER", "LLM_MODEL", "LLM_BASE_URL", "LLM_FALLBACK_PROVIDER", "AWS_PROFILE", "AWS_BEDROCK_REGION", "AWS_REGION", "QDRANT_TIMEOUT_MS", "QDRANT_RETRIES", "QDRANT_RETRY_BASE_DELAY_MS", "BIKKY_EMBEDDING_TIMEOUT_MS", "BIKKY_EMBEDDING_RETRIES", "BIKKY_EMBEDDING_RETRY_BASE_DELAY_MS", "BIKKY_LLM_TIMEOUT_MS", "BIKKY_LLM_RETRIES", "BIKKY_LLM_RETRY_BASE_DELAY_MS", "BIKKY_DAEMON_RELATION_INFERENCE_ENABLED", "BIKKY_DAEMON_RELATION_INFERENCE_INTERVAL_SEC", "BIKKY_DAEMON_RELATION_INFERENCE_MAX_PAIRS_PER_RUN", "BIKKY_DAEMON_ENTITY_TYPING_ENABLED", "BIKKY_DAEMON_ENTITY_TYPING_INTERVAL_SEC", "BIKKY_DAEMON_ENTITY_TYPING_MAX_ENTITIES_PER_RUN", "BIKKY_USER_ID", "BIKKY_USER_NAME", "BIKKY_AGENT_ID", "BIKKY_AGENT_NAME", "BIKKY_ACTOR_ID", "BIKKY_ACTOR_LABEL"];
175
+ export declare const CONFIG_ENV_KEYS: readonly ["QDRANT_URL", "QDRANT_API_KEY", "BIKKY_COLLECTION", "EMBEDDING_PROVIDER", "EMBEDDING_MODEL", "EMBEDDING_BASE_URL", "EMBEDDING_DIMENSIONS", "OPENAI_API_KEY", "PORTKEY_API_KEY", "LLM_PROVIDER", "LLM_MODEL", "LLM_BASE_URL", "LLM_FALLBACK_PROVIDER", "AWS_PROFILE", "AWS_BEDROCK_REGION", "AWS_REGION", "QDRANT_TIMEOUT_MS", "QDRANT_RETRIES", "QDRANT_RETRY_BASE_DELAY_MS", "BIKKY_EMBEDDING_TIMEOUT_MS", "BIKKY_EMBEDDING_RETRIES", "BIKKY_EMBEDDING_RETRY_BASE_DELAY_MS", "BIKKY_LLM_TIMEOUT_MS", "BIKKY_LLM_RETRIES", "BIKKY_LLM_RETRY_BASE_DELAY_MS", "BIKKY_DAEMON_RELATION_INFERENCE_ENABLED", "BIKKY_DAEMON_RELATION_INFERENCE_INTERVAL_SEC", "BIKKY_DAEMON_RELATION_INFERENCE_MAX_PAIRS_PER_RUN", "BIKKY_DAEMON_ENTITY_TYPING_ENABLED", "BIKKY_DAEMON_ENTITY_TYPING_INTERVAL_SEC", "BIKKY_DAEMON_ENTITY_TYPING_MAX_ENTITIES_PER_RUN", "BIKKY_DAEMON_MEMORY_QUALITY_ROLLUPS_ENABLED", "BIKKY_DAEMON_MEMORY_QUALITY_ROLLUPS_INTERVAL_SEC", "BIKKY_DAEMON_MEMORY_QUALITY_ROLLUPS_LOW_CONFIDENCE_THRESHOLD", "BIKKY_DAEMON_MEMORY_QUALITY_ROLLUPS_MAX_SCOPES_PER_RUN", "BIKKY_USER_ID", "BIKKY_USER_NAME", "BIKKY_AGENT_ID", "BIKKY_AGENT_NAME", "BIKKY_ACTOR_ID", "BIKKY_ACTOR_LABEL"];
172
176
  export declare function validateConfigObject(raw: unknown): ConfigIssue[];
173
177
  export declare function inspectConfigFile(configPath?: string): ConfigFileDiagnostics;
174
178
  export declare function getActiveConfigEnvOverrides(env?: NodeJS.ProcessEnv): string[];
package/dist/config.js CHANGED
@@ -92,6 +92,10 @@ const DEFAULTS = {
92
92
  entity_typing_enabled: true,
93
93
  entity_typing_interval_sec: 900,
94
94
  entity_typing_max_entities_per_run: 5,
95
+ memory_quality_rollups_enabled: true,
96
+ memory_quality_rollups_interval_sec: 3600,
97
+ memory_quality_rollups_low_confidence_threshold: 0.6,
98
+ memory_quality_rollups_max_scopes_per_run: 100,
95
99
  staleness_threshold_days: 30,
96
100
  },
97
101
  identity: {
@@ -142,6 +146,10 @@ export const CONFIG_ENV_KEYS = [
142
146
  "BIKKY_DAEMON_ENTITY_TYPING_ENABLED",
143
147
  "BIKKY_DAEMON_ENTITY_TYPING_INTERVAL_SEC",
144
148
  "BIKKY_DAEMON_ENTITY_TYPING_MAX_ENTITIES_PER_RUN",
149
+ "BIKKY_DAEMON_MEMORY_QUALITY_ROLLUPS_ENABLED",
150
+ "BIKKY_DAEMON_MEMORY_QUALITY_ROLLUPS_INTERVAL_SEC",
151
+ "BIKKY_DAEMON_MEMORY_QUALITY_ROLLUPS_LOW_CONFIDENCE_THRESHOLD",
152
+ "BIKKY_DAEMON_MEMORY_QUALITY_ROLLUPS_MAX_SCOPES_PER_RUN",
145
153
  "BIKKY_USER_ID",
146
154
  "BIKKY_USER_NAME",
147
155
  "BIKKY_AGENT_ID",
@@ -190,6 +198,10 @@ const daemonConfigFileSchema = z.object({
190
198
  entity_typing_enabled: z.boolean().optional(),
191
199
  entity_typing_interval_sec: nonNegativeInt.optional(),
192
200
  entity_typing_max_entities_per_run: nonNegativeInt.optional(),
201
+ memory_quality_rollups_enabled: z.boolean().optional(),
202
+ memory_quality_rollups_interval_sec: nonNegativeInt.optional(),
203
+ memory_quality_rollups_low_confidence_threshold: z.number().min(0).max(1).optional(),
204
+ memory_quality_rollups_max_scopes_per_run: positiveInt.optional(),
193
205
  staleness_threshold_days: nonNegativeInt.optional(),
194
206
  }).passthrough();
195
207
  const watcherConfigFileSchema = z.object({
@@ -696,6 +708,22 @@ export function loadConfig() {
696
708
  const entityTypingMax = positiveInt(process.env.BIKKY_DAEMON_ENTITY_TYPING_MAX_ENTITIES_PER_RUN);
697
709
  if (entityTypingMax !== null)
698
710
  config.daemon.entity_typing_max_entities_per_run = entityTypingMax;
711
+ const qualityRollupsEnabled = booleanEnv(process.env.BIKKY_DAEMON_MEMORY_QUALITY_ROLLUPS_ENABLED);
712
+ if (qualityRollupsEnabled !== null)
713
+ config.daemon.memory_quality_rollups_enabled = qualityRollupsEnabled;
714
+ const qualityRollupsInterval = positiveInt(process.env.BIKKY_DAEMON_MEMORY_QUALITY_ROLLUPS_INTERVAL_SEC);
715
+ if (qualityRollupsInterval !== null)
716
+ config.daemon.memory_quality_rollups_interval_sec = qualityRollupsInterval;
717
+ const qualityRollupsThresholdRaw = process.env.BIKKY_DAEMON_MEMORY_QUALITY_ROLLUPS_LOW_CONFIDENCE_THRESHOLD;
718
+ if (qualityRollupsThresholdRaw) {
719
+ const threshold = Number.parseFloat(qualityRollupsThresholdRaw);
720
+ if (Number.isFinite(threshold) && threshold >= 0 && threshold <= 1) {
721
+ config.daemon.memory_quality_rollups_low_confidence_threshold = threshold;
722
+ }
723
+ }
724
+ const qualityRollupsMaxScopes = positiveInt(process.env.BIKKY_DAEMON_MEMORY_QUALITY_ROLLUPS_MAX_SCOPES_PER_RUN);
725
+ if (qualityRollupsMaxScopes !== null)
726
+ config.daemon.memory_quality_rollups_max_scopes_per_run = qualityRollupsMaxScopes;
699
727
  if (process.env.BIKKY_USER_ID)
700
728
  config.identity.user_id = process.env.BIKKY_USER_ID;
701
729
  if (process.env.BIKKY_USER_NAME)
@@ -105,7 +105,6 @@ export const CAPTURE_KIND_SUBTYPES = {
105
105
  export const FACT_CATEGORY_TO_SUBTYPE = {
106
106
  engineering: "codebase_map",
107
107
  product: "domain_rule",
108
- human: "preference",
109
108
  system: "codebase_map",
110
109
  };
111
110
  export const DEFAULT_CAPTURE_CONTEXT = {
@@ -360,7 +360,6 @@ const formatHealthReport = (report) => {
360
360
  const CATEGORY_TO_HEADING = {
361
361
  engineering: "Engineering",
362
362
  product: "Product",
363
- human: "Human",
364
363
  system: "System",
365
364
  // Legacy stored categories remain readable before any data migration.
366
365
  codebase: "Engineering",
@@ -371,9 +370,10 @@ const CATEGORY_TO_HEADING = {
371
370
  projects: "System",
372
371
  observation: "Engineering",
373
372
  observations: "Engineering",
374
- preferences: "Human",
375
- people: "Human",
376
- team: "Human",
373
+ human: "Engineering",
374
+ preferences: "Engineering",
375
+ people: "Engineering",
376
+ team: "Engineering",
377
377
  };
378
378
  const generateMemoryBrief = async (_config) => {
379
379
  if (!qdrant.isReady())
@@ -10,7 +10,7 @@ import type { BikkyConfig } from "../config.js";
10
10
  import type { LogFn } from "./qdrant.js";
11
11
  import { type TranscriptSource } from "./transcript-sources.js";
12
12
  export declare const setLogger: (fn: LogFn) => void;
13
- export declare const DEFAULT_EXTRACTION_PROMPT = "You are Bikky's memory extraction agent for open-source coding agents. Extract durable, reusable facts that help a future agent continue work without rereading the whole transcript.\n\n## Core rule\nExtract fewer, sharper memories. A candidate fact must be independently useful after the session is gone.\n\n## Quality gate\nEvery fact must pass at least one gate:\n1. GREPPABLE: names a file path, package, symbol, config key, CLI flag, issue/PR, service, or API a future agent can search for.\n2. RUNNABLE: contains a command, URL, setting, port, or procedure that can be executed or checked.\n3. NAVIGABLE: tells a future agent where to look and what that location means.\n4. DECISIVE: records a durable decision, rationale, constraint, convention, or preference.\n5. DIAGNOSTIC: captures a repeatable failure mode, root cause, or troubleshooting gotcha.\n\n## Ontology\n- domain is the activity profile. For coding-agent captures use \"software_engineering\".\n- category is subject matter: engineering | product | human | system.\n- kind is object shape. For this prompt, emit only kind=\"fact\".\n- memory_subtype must be one of:\n codebase_map | architecture_decision | infra_topology | access_pattern | operational_procedure | domain_rule | product_decision | product_requirement | user_workflow | roadmap_item | success_metric | market_insight | troubleshooting_gotcha | preference | person_profile | ownership_note | working_agreement | activity_event.\n\n## Examples\nGOOD:\n- \"The UI smoke tests live in packages/ui/tests/smoke.spec.ts and run through npm run test:e2e with mocked /api/memory/* responses.\"\n- \"Use workspace_id as the tenancy/access boundary; domain is reserved for activity profile such as software_engineering.\"\n- \"If Qdrant order_by fails with a missing index error, create a datetime payload index for the sorted field before retrying.\"\n- \"The memory page should show categories and concrete subtype chips directly; a sub-tab layer makes the ontology harder to understand.\"\n- \"Saber prefers Node's built-in test runner for root tests; do not add Jest just for daemon unit tests.\"\n- \"Saber merged PR #85 after approving the subtype UX copy changes.\"\n\nBAD:\n- \"The tests were fixed.\" (status only)\n- \"We reviewed the code.\" (session narration)\n- \"The deployment succeeded.\" (transient and not reusable)\n- \"The agent used npm.\" (tool narration)\n- \"There was an error.\" (no root cause or reusable detail)\n\n## Output format\nReturn strict JSON:\n{\"facts\":[\n {\n \"content\":\"One self-contained durable fact.\",\n \"category\":\"engineering\",\n \"memory_subtype\":\"codebase_map\",\n \"action_actor\":\"optional actor for activity_event only\",\n \"action_type\":\"optional action verb for activity_event only\",\n \"action_object\":\"optional durable object for activity_event only\",\n \"action_outcome\":\"optional durable outcome for activity_event only\",\n \"entities\":[\"repo-or-tool\",\"specific-module\"],\n \"confidence\":0.9,\n \"importance\":0.7,\n \"quality_score\":0.8,\n \"confidence_reason\":\"Explicitly stated in the transcript.\",\n \"repo\":\"optional/repo-or-package\",\n \"branch\":\"optional-branch\",\n \"task_key\":\"optional issue/PR/task key\",\n \"workstream_key\":\"optional stable workstream key\"\n }\n]}\n\nScoring:\n- confidence: 0.9 explicit, 0.7 strong inference, 0.55 weak but useful inference.\n- importance: 0.8+ for decisions, infra, procedures, access, recurring failures, product requirements, ownership, and state-changing activity events; 0.6+ for useful codebase maps/preferences.\n- quality_score: 0.8+ passes multiple gates, 0.6+ passes one strong gate, below 0.6 should usually be omitted.\n\nIf nothing passes the quality gate, return {\"facts\":[]}.";
13
+ export declare const DEFAULT_EXTRACTION_PROMPT = "You are Bikky's memory extraction agent for open-source coding agents. Extract durable, reusable facts that help a future agent continue work without rereading the whole transcript.\n\n## Core rule\nExtract fewer, sharper memories. A candidate fact must be independently useful after the session is gone.\n\n## Quality gate\nEvery fact must pass at least one gate:\n1. GREPPABLE: names a file path, package, symbol, config key, CLI flag, issue/PR, service, or API a future agent can search for.\n2. RUNNABLE: contains a command, URL, setting, port, or procedure that can be executed or checked.\n3. NAVIGABLE: tells a future agent where to look and what that location means.\n4. DECISIVE: records a durable decision, rationale, constraint, convention, or preference.\n5. DIAGNOSTIC: captures a repeatable failure mode, root cause, or troubleshooting gotcha.\n\n## Ontology\n- domain is the activity profile. For coding-agent captures use \"software_engineering\".\n- category is subject matter: engineering | product | system.\n- kind is object shape. For this prompt, emit only kind=\"fact\".\n- memory_subtype must be one of:\n codebase_map | architecture_decision | infra_topology | access_pattern | operational_procedure | domain_rule | product_decision | product_requirement | user_workflow | roadmap_item | success_metric | market_insight | troubleshooting_gotcha | preference | person_profile | ownership_note | working_agreement | activity_event.\n\n## Examples\nGOOD:\n- \"The UI smoke tests live in packages/ui/tests/smoke.spec.ts and run through npm run test:e2e with mocked /api/memory/* responses.\"\n- \"Use workspace_id as the tenancy/access boundary; domain is reserved for activity profile such as software_engineering.\"\n- \"If Qdrant order_by fails with a missing index error, create a datetime payload index for the sorted field before retrying.\"\n- \"The memory page should show categories and concrete subtype chips directly; a sub-tab layer makes the ontology harder to understand.\"\n- \"Saber prefers Node's built-in test runner for root tests; do not add Jest just for daemon unit tests.\"\n- \"Saber merged PR #85 after approving the subtype UX copy changes.\"\n\nBAD:\n- \"The tests were fixed.\" (status only)\n- \"We reviewed the code.\" (session narration)\n- \"The deployment succeeded.\" (transient and not reusable)\n- \"The agent used npm.\" (tool narration)\n- \"There was an error.\" (no root cause or reusable detail)\n\n## Output format\nReturn strict JSON:\n{\"facts\":[\n {\n \"content\":\"One self-contained durable fact.\",\n \"category\":\"engineering\",\n \"memory_subtype\":\"codebase_map\",\n \"action_actor\":\"optional actor for activity_event only\",\n \"action_type\":\"optional action verb for activity_event only\",\n \"action_object\":\"optional durable object for activity_event only\",\n \"action_outcome\":\"optional durable outcome for activity_event only\",\n \"entities\":[\"repo-or-tool\",\"specific-module\"],\n \"confidence\":0.9,\n \"importance\":0.7,\n \"quality_score\":0.8,\n \"confidence_reason\":\"Explicitly stated in the transcript.\",\n \"repo\":\"optional/repo-or-package\",\n \"branch\":\"optional-branch\",\n \"task_key\":\"optional issue/PR/task key\",\n \"workstream_key\":\"optional stable workstream key\"\n }\n]}\n\nScoring:\n- confidence: 0.9 explicit, 0.7 strong inference, 0.55 weak but useful inference.\n- importance: 0.8+ for decisions, infra, procedures, access, recurring failures, product requirements, ownership, and state-changing activity events; 0.6+ for useful codebase maps/preferences.\n- quality_score: 0.8+ passes multiple gates, 0.6+ passes one strong gate, below 0.6 should usually be omitted.\n\nIf nothing passes the quality gate, return {\"facts\":[]}.";
14
14
  export type Volatility = "stable" | "evolving" | "transient" | "ephemeral";
15
15
  export interface ExtractedFact {
16
16
  content: string;
@@ -43,7 +43,7 @@ Every fact must pass at least one gate:
43
43
 
44
44
  ## Ontology
45
45
  - domain is the activity profile. For coding-agent captures use "software_engineering".
46
- - category is subject matter: engineering | product | human | system.
46
+ - category is subject matter: engineering | product | system.
47
47
  - kind is object shape. For this prompt, emit only kind="fact".
48
48
  - memory_subtype must be one of:
49
49
  codebase_map | architecture_decision | infra_topology | access_pattern | operational_procedure | domain_rule | product_decision | product_requirement | user_workflow | roadmap_item | success_metric | market_insight | troubleshooting_gotcha | preference | person_profile | ownership_note | working_agreement | activity_event.
@@ -210,8 +210,8 @@ export const factQualitySignals = (fact) => {
210
210
  const isPreferenceLike = subtype === "preference" || subtype === "domain_rule" || subtype === "working_agreement";
211
211
  const isDecisionLike = subtype === "architecture_decision" || subtype === "product_decision" || subtype === "troubleshooting_gotcha";
212
212
  const isProductLike = subtype === "product_requirement" || subtype === "user_workflow" || subtype === "roadmap_item" || subtype === "success_metric" || subtype === "market_insight";
213
- const isHumanLike = subtype === "person_profile" || subtype === "ownership_note" || subtype === "activity_event";
214
- const shortUseful = wordCount >= 7 && wordCount <= 22 && (isPreferenceLike || isDecisionLike || isProductLike || isHumanLike) && (entities.length > 0 || durableAnchor);
213
+ const isCollaborationLike = subtype === "person_profile" || subtype === "ownership_note" || subtype === "activity_event";
214
+ const shortUseful = wordCount >= 7 && wordCount <= 22 && (isPreferenceLike || isDecisionLike || isProductLike || isCollaborationLike) && (entities.length > 0 || durableAnchor);
215
215
  let score = 0.25;
216
216
  if (wordCount >= 8)
217
217
  score += 0.1;
@@ -219,7 +219,7 @@ export const factQualitySignals = (fact) => {
219
219
  score += 0.1;
220
220
  if (durableAnchor)
221
221
  score += 0.25;
222
- if (isPreferenceLike || isDecisionLike || isProductLike || isHumanLike)
222
+ if (isPreferenceLike || isDecisionLike || isProductLike || isCollaborationLike)
223
223
  score += 0.15;
224
224
  if ((fact.confidence ?? 0) >= 0.75)
225
225
  score += 0.1;
@@ -254,8 +254,16 @@ const subtypeForRawCategoryHint = (rawCategory, category) => {
254
254
  return "operational_procedure";
255
255
  if (hint.includes("decision"))
256
256
  return "architecture_decision";
257
- if (hint.includes("people") || hint.includes("preference") || hint.includes("owner"))
257
+ if (hint.includes("preference"))
258
258
  return "preference";
259
+ if (hint.includes("owner"))
260
+ return "ownership_note";
261
+ if (hint.includes("agreement"))
262
+ return "working_agreement";
263
+ if (hint.includes("activity") || hint.includes("actor"))
264
+ return "activity_event";
265
+ if (hint.includes("people") || hint.includes("person") || hint.includes("team"))
266
+ return "person_profile";
259
267
  if (hint.includes("product") || hint.includes("domain"))
260
268
  return "domain_rule";
261
269
  return subtypeForCategory(normalizeCategory(category));
@@ -11,6 +11,7 @@ import { tick as extractionTick, setLogger as setExtractionLogger } from "./extr
11
11
  import { tick as consolidationTick, setLogger as setConsolidationLogger } from "./consolidation.js";
12
12
  import { tick as relationsTick, setLogger as setRelationsLogger } from "./relations.js";
13
13
  import { tick as entityTypingTick, setLogger as setEntityTypingLogger } from "./entity-typing.js";
14
+ import { tick as qualityRollupsTick, setLogger as setQualityRollupsLogger } from "./quality-rollups.js";
14
15
  import { scanStaleFacts, setLogger as setStalenessLogger } from "./staleness.js";
15
16
  import { inspectWatcherPaths, formatIssue } from "./watcher-health.js";
16
17
  // createLogger returns (LogLevel, ...args) but daemon modules accept (string, ...args).
@@ -34,6 +35,7 @@ export async function startDaemon() {
34
35
  setConsolidationLogger(log);
35
36
  setRelationsLogger(log);
36
37
  setEntityTypingLogger(log);
38
+ setQualityRollupsLogger(log);
37
39
  setStalenessLogger(log);
38
40
  // Initialize LLM client from config
39
41
  initLLM({
@@ -107,6 +109,12 @@ export async function startDaemon() {
107
109
  catch (e) {
108
110
  log("ERROR", `Entity typing tick failed: ${e.message}`);
109
111
  }
112
+ try {
113
+ await qualityRollupsTick(cfg);
114
+ }
115
+ catch (e) {
116
+ log("ERROR", `Memory quality rollups tick failed: ${e.message}`);
117
+ }
110
118
  // Staleness scans every 1000 ticks (~83 min at 5s interval)
111
119
  if (tickCount % 1000 === 0) {
112
120
  try {
@@ -1,6 +1,6 @@
1
1
  import type { LogFn } from "./qdrant.js";
2
2
  export declare const MAINTENANCE_STATE_PATH: string;
3
- export type MaintenanceJobName = "relation_inference" | "entity_typing";
3
+ export type MaintenanceJobName = "relation_inference" | "entity_typing" | "memory_quality_rollups";
4
4
  export interface MaintenanceRunSummary {
5
5
  job: MaintenanceJobName;
6
6
  ran_at: string;
@@ -13,6 +13,7 @@ export const defaultMaintenanceState = () => ({
13
13
  jobs: {
14
14
  relation_inference: defaultJobState(),
15
15
  entity_typing: defaultJobState(),
16
+ memory_quality_rollups: defaultJobState(),
16
17
  },
17
18
  });
18
19
  const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
@@ -42,6 +43,7 @@ export const readMaintenanceState = (log = () => { }) => {
42
43
  jobs: {
43
44
  relation_inference: coerceJobState(jobs.relation_inference),
44
45
  entity_typing: coerceJobState(jobs.entity_typing),
46
+ memory_quality_rollups: coerceJobState(jobs.memory_quality_rollups),
45
47
  },
46
48
  };
47
49
  }
@@ -15,6 +15,7 @@ import { buildResolver } from "../routing.js";
15
15
  import { DEFAULT_DOMAIN, QDRANT_INDEXES, categoryForMemorySubtype, layerForMemorySubtype, normalizeCategory, normalizeDomain, normalizeKind, validateMemorySubtype, } from "../mcp/taxonomy.js";
16
16
  import { combineRedactions, redactStorageText, } from "../privacy/redaction.js";
17
17
  import { buildOperationOrigin } from "../provenance/origin.js";
18
+ import { buildMemoryRoutingInput, mergeRoutingInputs } from "../routing-context.js";
18
19
  // ---------------------------------------------------------------------------
19
20
  // State
20
21
  // ---------------------------------------------------------------------------
@@ -56,10 +57,8 @@ const pathForDestination = (urlPath, destination) => {
56
57
  return urlPath;
57
58
  return urlPath.replace(/^\/collections\/[^/]+/, `/collections/${destination.collection}`);
58
59
  };
59
- const routingInputForFact = (fact, normalizedContent, normalizedEntities, extraMetadata = {}) => ({
60
- content: normalizedContent,
61
- entities: normalizedEntities,
62
- metadata: {
60
+ const routingInputForFact = (fact, normalizedContent, normalizedEntities, extraMetadata = {}) => {
61
+ const metadata = {
63
62
  ...(fact.metadata ?? {}),
64
63
  ...extraMetadata,
65
64
  category: fact.category,
@@ -75,8 +74,31 @@ const routingInputForFact = (fact, normalizedContent, normalizedEntities, extraM
75
74
  ...(fact.repo ? { repo: fact.repo } : {}),
76
75
  ...(fact.branch ? { branch: fact.branch } : {}),
77
76
  ...(fact.surface ? { surface: fact.surface } : {}),
78
- },
79
- });
77
+ ...(fact.issue_id ? { issue_id: fact.issue_id } : {}),
78
+ ...(fact.pr_id ? { pr_id: fact.pr_id } : {}),
79
+ ...(fact.source_event_ids ? { source_event_ids: fact.source_event_ids } : {}),
80
+ ...(fact.source_fact_ids ? { source_fact_ids: fact.source_fact_ids } : {}),
81
+ ...(fact.source_episode_ids ? { source_episode_ids: fact.source_episode_ids } : {}),
82
+ ...(fact.prompt_version ? { prompt_version: fact.prompt_version } : {}),
83
+ ...(fact.capture_policy_version ? { capture_policy_version: fact.capture_policy_version } : {}),
84
+ ...(fact.review_status ? { review_status: fact.review_status } : {}),
85
+ ...(fact.volatility ? { volatility: fact.volatility } : {}),
86
+ ...(fact.valid_from ? { valid_from: fact.valid_from } : {}),
87
+ ...(fact.expires_at ? { expires_at: fact.expires_at } : {}),
88
+ ...(fact.confidence_reason ? { confidence_reason: fact.confidence_reason } : {}),
89
+ ...(fact.relation ? {
90
+ from_entity: fact.relation.from,
91
+ relation_type: fact.relation.type,
92
+ to_entity: fact.relation.to,
93
+ } : {}),
94
+ };
95
+ return buildMemoryRoutingInput({
96
+ content: normalizedContent,
97
+ entities: normalizedEntities,
98
+ metadata,
99
+ extraContent: [fact.origin, fact.last_operation_origin, fact.relation],
100
+ });
101
+ };
80
102
  // ---------------------------------------------------------------------------
81
103
  // Init — reads credentials from loadConfig()
82
104
  // ---------------------------------------------------------------------------
@@ -373,12 +395,12 @@ const storeFact = async (fact, routeInput) => {
373
395
  if (redaction.redacted) {
374
396
  payload.redaction = redaction;
375
397
  }
376
- const destination = resolveDestination(routeInput ?? routingInputForFact(fact, redactedContent.text, payload.entities, {
398
+ const destination = resolveDestination(mergeRoutingInputs(routingInputForFact(fact, redactedContent.text, payload.entities, {
377
399
  category: normalizedCategory,
378
400
  domain: normalizedDomain,
379
401
  kind: normalizedKind,
380
402
  ...(normalizedSubtype ? { memory_subtype: normalizedSubtype } : {}),
381
- }));
403
+ }), routeInput));
382
404
  const vector = await embed(redactedContent.text);
383
405
  await qdrantRequest("PUT", `/collections/${destination.collection}/points`, {
384
406
  points: [{ id, vector, payload }],
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Backend aggregation for memory quality telemetry.
3
+ */
4
+ import type { BikkyConfig, Destination } from "../config.js";
5
+ import type { FactPayload } from "../mcp/types.js";
6
+ import type { LogFn } from "./qdrant.js";
7
+ export type QualityScopeType = "destination" | "repo" | "workstream_key" | "task_key" | "entity" | "origin_user" | "origin_agent";
8
+ export interface QualityPoint {
9
+ id: string;
10
+ destination: string;
11
+ payload: Partial<FactPayload>;
12
+ }
13
+ export interface QualityRollup {
14
+ destination: string;
15
+ scope_type: QualityScopeType;
16
+ scope_value: string;
17
+ active_fact_count: number;
18
+ recall_count: number;
19
+ useful_count: number;
20
+ misleading_count: number;
21
+ wrong_count: number;
22
+ stale_count: number;
23
+ low_confidence_count: number;
24
+ generated_at: string;
25
+ source_fact_ids: string[];
26
+ source_event_ids: string[];
27
+ }
28
+ export interface QualityRollupResult {
29
+ destinations_seen: number;
30
+ facts_seen: number;
31
+ events_seen: number;
32
+ rollups_upserted: number;
33
+ scopes_capped: boolean;
34
+ }
35
+ export interface QualityRollupDeps {
36
+ isReady: () => boolean;
37
+ activeDestinations: () => Destination[];
38
+ qdrantRequest: (method: string, urlPath: string, body?: unknown, destinationRef?: Destination | string | null) => Promise<Record<string, unknown>>;
39
+ embed: (text: string) => Promise<number[]>;
40
+ }
41
+ export declare const setLogger: (fn: LogFn) => void;
42
+ export declare const buildQualityRollups: (input: {
43
+ facts: QualityPoint[];
44
+ events?: QualityPoint[];
45
+ generatedAt?: Date;
46
+ staleThresholdDays?: number;
47
+ lowConfidenceThreshold?: number;
48
+ }) => QualityRollup[];
49
+ export declare const aggregateMemoryQualitySignals: (config: BikkyConfig, deps?: QualityRollupDeps) => Promise<QualityRollupResult>;
50
+ export declare const tick: (config: BikkyConfig, deps?: QualityRollupDeps) => Promise<void>;
51
+ //# sourceMappingURL=quality-rollups.d.ts.map