bikky 0.4.2 → 0.4.3
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 +6 -4
- package/dist/config.d.ts +11 -1
- package/dist/config.js +88 -20
- package/dist/daemon/capture-policy.d.ts +0 -1
- package/dist/daemon/capture-policy.js +0 -1
- package/dist/daemon/consolidation.d.ts +2 -1
- package/dist/daemon/consolidation.js +28 -11
- package/dist/daemon/entity-typing.js +10 -0
- package/dist/daemon/episode-summary.d.ts +4 -0
- package/dist/daemon/episode-summary.js +39 -8
- package/dist/daemon/extraction.d.ts +1 -1
- package/dist/daemon/extraction.js +52 -17
- package/dist/daemon/qdrant.d.ts +32 -10
- package/dist/daemon/qdrant.js +177 -60
- package/dist/daemon/relations.d.ts +3 -3
- package/dist/daemon/relations.js +27 -15
- package/dist/daemon/session-index.d.ts +5 -0
- package/dist/daemon/session-index.js +36 -9
- package/dist/daemon/session-summary.d.ts +3 -0
- package/dist/daemon/session-summary.js +48 -15
- package/dist/daemon/staleness.js +2 -2
- package/dist/daemon/transcript-sources.js +3 -2
- package/dist/daemon/watcher.js +2 -0
- package/dist/daemon/workstream-summary.d.ts +4 -0
- package/dist/daemon/workstream-summary.js +58 -16
- package/dist/install.d.ts +11 -0
- package/dist/install.js +38 -0
- package/dist/llm/embedding/index.js +2 -1
- package/dist/llm/embedding/providers/openai.js +8 -2
- package/dist/llm/embedding/providers/portkey.js +9 -2
- package/dist/llm/inference/index.js +2 -1
- package/dist/llm/util.d.ts +12 -0
- package/dist/llm/util.js +18 -0
- package/dist/mcp/helpers.d.ts +5 -0
- package/dist/mcp/helpers.js +27 -3
- package/dist/mcp/taxonomy.js +12 -1
- package/dist/mcp/tools.js +161 -57
- package/dist/mcp/types.d.ts +12 -0
- package/dist/package-verifier.d.ts +19 -0
- package/dist/package-verifier.js +83 -0
- package/dist/provenance/origin.d.ts +57 -0
- package/dist/provenance/origin.js +254 -0
- package/docs/config/fully-hosted.md +33 -13
- package/docs/config/hosted-models.md +33 -13
- package/docs/configuration.md +18 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -101,11 +101,11 @@ bikky supports four common setup shapes. Pick based on where you want Qdrant to
|
|
|
101
101
|
| ----------------------- | ------------------------------ | ---------------------------------------------------------------------------------------- |
|
|
102
102
|
| **Node.js** | ≥ 20 | `nvm install 20` or your package manager |
|
|
103
103
|
| **Vector store** | Qdrant | Local Docker · [Qdrant Cloud](https://cloud.qdrant.io) · Self-hosted |
|
|
104
|
-
| **Embeddings** | One provider | OpenAI · Ollama · Bedrock
|
|
105
|
-
| **LLM** | One provider | OpenAI · Ollama · Bedrock
|
|
104
|
+
| **Embeddings** | One provider | Portkey · OpenAI · Ollama · Bedrock |
|
|
105
|
+
| **LLM** | One provider | Portkey · OpenAI · Ollama · Bedrock |
|
|
106
106
|
| **Docker** *(optional)* | Only if you run Qdrant locally | Docker Desktop, OrbStack, colima, etc. |
|
|
107
107
|
|
|
108
|
-
Both `embedding.provider` and `llm.provider` accept the same values: `ollama`, `openai`, `bedrock`, or `portkey`.
|
|
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.
|
|
109
109
|
|
|
110
110
|
> ⚠️ **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
111
|
|
|
@@ -128,6 +128,8 @@ Pick the setup guide above for the copy-paste config. All setup shapes use the s
|
|
|
128
128
|
|
|
129
129
|
Config lives at `~/.bikky/config.json`, or at `BIKKY_HOME/config.json` when `BIKKY_HOME` is set. You can keep credentials out of the file with environment variables such as `QDRANT_URL`, `QDRANT_API_KEY`, and provider API keys.
|
|
130
130
|
|
|
131
|
+
`bikky setup` also provisions `identity.user_id` / `identity.user_name` when they are missing. New memory writes store canonical `origin` metadata with the configured human user, the acting agent or daemon/UI surface, the interface, and the operation. MCP clients cannot supply or spoof `origin.user`; if config, env, Git, and shell identity detection all fail, bikky falls back to the local hostname.
|
|
132
|
+
|
|
131
133
|
For hosted models, custom providers, multiple profiles, or advanced tuning, use the full configuration guide.
|
|
132
134
|
|
|
133
135
|
> 📖 **Full configuration guide:** [docs/configuration.md][configuration-guide]
|
|
@@ -196,7 +198,7 @@ bikky-ui # opens http://localhost:1422
|
|
|
196
198
|
<p align="center">
|
|
197
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" />
|
|
198
200
|
</p>
|
|
199
|
-
<p align="center"><i>Memory browser — search, filter by category/kind/
|
|
201
|
+
<p align="center"><i>Memory browser — search, filter by category/kind/origin, and browse all stored facts</i></p>
|
|
200
202
|
|
|
201
203
|
<p align="center">
|
|
202
204
|
<img src="https://raw.githubusercontent.com/bikky-dev/bikky/main/docs/screenshots/graph.png" alt="Entity graph — interactive visualization of entity relationships" width="720" />
|
package/dist/config.d.ts
CHANGED
|
@@ -4,6 +4,12 @@
|
|
|
4
4
|
* Resolution order: defaults → ~/.bikky/config.json → env vars.
|
|
5
5
|
* Config directory: ~/.bikky/
|
|
6
6
|
*/
|
|
7
|
+
export declare function getBikkyDir(): string;
|
|
8
|
+
export declare function getConfigPath(): string;
|
|
9
|
+
export declare function getLogDir(): string;
|
|
10
|
+
export declare function getStateDir(): string;
|
|
11
|
+
export declare function getPidPath(): string;
|
|
12
|
+
export declare function getExtractionHealthPath(): string;
|
|
7
13
|
export declare const BIKKY_DIR: string;
|
|
8
14
|
export declare const CONFIG_PATH: string;
|
|
9
15
|
export declare const LOG_DIR: string;
|
|
@@ -63,7 +69,11 @@ export interface QdrantClientConfig {
|
|
|
63
69
|
retry_base_delay_ms: number;
|
|
64
70
|
}
|
|
65
71
|
export interface IdentityConfig {
|
|
72
|
+
user_id: string | null;
|
|
73
|
+
user_name: string | null;
|
|
74
|
+
/** @deprecated Use origin.user.id instead. */
|
|
66
75
|
actor_id: string | null;
|
|
76
|
+
/** @deprecated Use origin.user.name / origin.agent.name instead. */
|
|
67
77
|
actor_label: string | null;
|
|
68
78
|
}
|
|
69
79
|
export interface WatcherConfig {
|
|
@@ -158,7 +168,7 @@ export interface ConfigFileDiagnostics {
|
|
|
158
168
|
issues: ConfigIssue[];
|
|
159
169
|
}
|
|
160
170
|
declare const DEFAULTS: BikkyConfig;
|
|
161
|
-
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", "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_ACTOR_ID", "BIKKY_ACTOR_LABEL"];
|
|
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"];
|
|
162
172
|
export declare function validateConfigObject(raw: unknown): ConfigIssue[];
|
|
163
173
|
export declare function inspectConfigFile(configPath?: string): ConfigFileDiagnostics;
|
|
164
174
|
export declare function getActiveConfigEnvOverrides(env?: NodeJS.ProcessEnv): string[];
|
package/dist/config.js
CHANGED
|
@@ -12,15 +12,41 @@ import { z } from "zod";
|
|
|
12
12
|
// Paths
|
|
13
13
|
// ---------------------------------------------------------------------------
|
|
14
14
|
// BIKKY_HOME env var lets tests (and advanced users) override the config dir
|
|
15
|
-
// without touching the real ~/.bikky/.
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
export
|
|
15
|
+
// without touching the real ~/.bikky/. The getter functions below re-read the
|
|
16
|
+
// env var on every call, so changing BIKKY_HOME at runtime (e.g. in a test
|
|
17
|
+
// setup hook) takes effect for all subsequent saveConfig()/loadConfig() calls.
|
|
18
|
+
//
|
|
19
|
+
// The legacy `BIKKY_DIR` / `CONFIG_PATH` / etc. exports are kept for
|
|
20
|
+
// backward-compatibility, but they capture the env state at module load
|
|
21
|
+
// time and should NOT be relied on for safe writes. Internal callers — and
|
|
22
|
+
// any test that wants sandboxing — should call the getter functions instead.
|
|
23
|
+
export function getBikkyDir() {
|
|
24
|
+
return process.env.BIKKY_HOME ?? path.join(os.homedir(), ".bikky");
|
|
25
|
+
}
|
|
26
|
+
export function getConfigPath() {
|
|
27
|
+
return path.join(getBikkyDir(), "config.json");
|
|
28
|
+
}
|
|
29
|
+
export function getLogDir() {
|
|
30
|
+
return path.join(getBikkyDir(), "logs");
|
|
31
|
+
}
|
|
32
|
+
export function getStateDir() {
|
|
33
|
+
return path.join(getBikkyDir(), "state");
|
|
34
|
+
}
|
|
35
|
+
export function getPidPath() {
|
|
36
|
+
return path.join(getStateDir(), "daemon.pid");
|
|
37
|
+
}
|
|
38
|
+
export function getExtractionHealthPath() {
|
|
39
|
+
return path.join(getStateDir(), "extraction-health.json");
|
|
40
|
+
}
|
|
41
|
+
// Legacy constant exports — captured at module load. Prefer the getter
|
|
42
|
+
// functions above when you need fresh values (e.g. inside tests, or after
|
|
43
|
+
// mutating BIKKY_HOME at runtime).
|
|
44
|
+
export const BIKKY_DIR = getBikkyDir();
|
|
45
|
+
export const CONFIG_PATH = getConfigPath();
|
|
46
|
+
export const LOG_DIR = getLogDir();
|
|
47
|
+
export const STATE_DIR = getStateDir();
|
|
48
|
+
export const PID_PATH = getPidPath();
|
|
49
|
+
export const EXTRACTION_HEALTH_PATH = getExtractionHealthPath();
|
|
24
50
|
// ---------------------------------------------------------------------------
|
|
25
51
|
// Defaults
|
|
26
52
|
// ---------------------------------------------------------------------------
|
|
@@ -69,6 +95,8 @@ const DEFAULTS = {
|
|
|
69
95
|
staleness_threshold_days: 30,
|
|
70
96
|
},
|
|
71
97
|
identity: {
|
|
98
|
+
user_id: null,
|
|
99
|
+
user_name: null,
|
|
72
100
|
actor_id: null,
|
|
73
101
|
actor_label: null,
|
|
74
102
|
},
|
|
@@ -91,6 +119,7 @@ export const CONFIG_ENV_KEYS = [
|
|
|
91
119
|
"EMBEDDING_BASE_URL",
|
|
92
120
|
"EMBEDDING_DIMENSIONS",
|
|
93
121
|
"OPENAI_API_KEY",
|
|
122
|
+
"PORTKEY_API_KEY",
|
|
94
123
|
"LLM_PROVIDER",
|
|
95
124
|
"LLM_MODEL",
|
|
96
125
|
"LLM_BASE_URL",
|
|
@@ -113,6 +142,10 @@ export const CONFIG_ENV_KEYS = [
|
|
|
113
142
|
"BIKKY_DAEMON_ENTITY_TYPING_ENABLED",
|
|
114
143
|
"BIKKY_DAEMON_ENTITY_TYPING_INTERVAL_SEC",
|
|
115
144
|
"BIKKY_DAEMON_ENTITY_TYPING_MAX_ENTITIES_PER_RUN",
|
|
145
|
+
"BIKKY_USER_ID",
|
|
146
|
+
"BIKKY_USER_NAME",
|
|
147
|
+
"BIKKY_AGENT_ID",
|
|
148
|
+
"BIKKY_AGENT_NAME",
|
|
116
149
|
"BIKKY_ACTOR_ID",
|
|
117
150
|
"BIKKY_ACTOR_LABEL",
|
|
118
151
|
];
|
|
@@ -122,7 +155,7 @@ const CONFIG_ENV_PREFIXES = [
|
|
|
122
155
|
];
|
|
123
156
|
const nonNegativeInt = z.number().int().nonnegative();
|
|
124
157
|
const positiveInt = z.number().int().positive();
|
|
125
|
-
const stringRecord = z.record(z.string());
|
|
158
|
+
const stringRecord = z.record(z.string(), z.string());
|
|
126
159
|
const embeddingConfigFileSchema = z.object({
|
|
127
160
|
provider: z.string().optional(),
|
|
128
161
|
model: z.string().optional(),
|
|
@@ -175,6 +208,8 @@ const qdrantClientConfigFileSchema = z.object({
|
|
|
175
208
|
retry_base_delay_ms: nonNegativeInt.optional(),
|
|
176
209
|
}).passthrough();
|
|
177
210
|
const identityConfigFileSchema = z.object({
|
|
211
|
+
user_id: z.string().nullable().optional(),
|
|
212
|
+
user_name: z.string().nullable().optional(),
|
|
178
213
|
actor_id: z.string().nullable().optional(),
|
|
179
214
|
actor_label: z.string().nullable().optional(),
|
|
180
215
|
}).passthrough();
|
|
@@ -183,7 +218,7 @@ const destinationMatchSchema = z.object({
|
|
|
183
218
|
cwd: regexArrayField,
|
|
184
219
|
entity: regexArrayField,
|
|
185
220
|
content: regexArrayField,
|
|
186
|
-
metadata: z.record(z.array(z.string())).optional(),
|
|
221
|
+
metadata: z.record(z.string(), z.array(z.string())).optional(),
|
|
187
222
|
}).passthrough();
|
|
188
223
|
const destinationFileSchema = z.object({
|
|
189
224
|
name: z.string().min(1),
|
|
@@ -442,7 +477,7 @@ export function validateConfigObject(raw) {
|
|
|
442
477
|
validateUrlLike(llm.base_url, "llm.base_url", issues);
|
|
443
478
|
return issues;
|
|
444
479
|
}
|
|
445
|
-
export function inspectConfigFile(configPath =
|
|
480
|
+
export function inspectConfigFile(configPath = getConfigPath()) {
|
|
446
481
|
if (!fs.existsSync(configPath)) {
|
|
447
482
|
return { path: configPath, exists: false, parse_error: null, issues: [] };
|
|
448
483
|
}
|
|
@@ -490,21 +525,41 @@ export function loadConfig() {
|
|
|
490
525
|
if (_config)
|
|
491
526
|
return _config;
|
|
492
527
|
// Ensure dirs exist
|
|
493
|
-
fs.mkdirSync(
|
|
494
|
-
fs.mkdirSync(
|
|
495
|
-
fs.mkdirSync(
|
|
528
|
+
fs.mkdirSync(getBikkyDir(), { recursive: true });
|
|
529
|
+
fs.mkdirSync(getLogDir(), { recursive: true });
|
|
530
|
+
fs.mkdirSync(getStateDir(), { recursive: true });
|
|
496
531
|
// Start from defaults
|
|
497
532
|
let config = structuredClone(DEFAULTS);
|
|
498
533
|
// Merge config file
|
|
499
|
-
|
|
534
|
+
const configPath = getConfigPath();
|
|
535
|
+
let fileConfig = {};
|
|
536
|
+
if (fs.existsSync(configPath)) {
|
|
500
537
|
try {
|
|
501
|
-
|
|
538
|
+
fileConfig = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
502
539
|
config = deepMerge(config, fileConfig);
|
|
503
540
|
}
|
|
504
541
|
catch (e) {
|
|
505
|
-
console.error(`bikky: failed to parse ${
|
|
542
|
+
console.error(`bikky: failed to parse ${configPath}: ${e instanceof Error ? e.message : String(e)}`);
|
|
506
543
|
}
|
|
507
544
|
}
|
|
545
|
+
// Provider/base_url consistency (issue #131): the DEFAULTS.embedding.base_url
|
|
546
|
+
// is the ollama localhost URL, baked in for the default ollama provider. If
|
|
547
|
+
// the user picks a different provider (portkey/openai/bedrock) but doesn't
|
|
548
|
+
// set an explicit base_url, drop the inherited ollama URL so initEmbedding()
|
|
549
|
+
// can apply the provider's own default — otherwise we'd POST every embedding
|
|
550
|
+
// request to localhost:11434 and Ollama would reject the foreign model name.
|
|
551
|
+
const fileEmbedding = (fileConfig.embedding ?? {});
|
|
552
|
+
if (config.embedding.provider !== DEFAULTS.embedding.provider
|
|
553
|
+
&& typeof fileEmbedding.base_url !== "string"
|
|
554
|
+
&& !process.env.EMBEDDING_BASE_URL) {
|
|
555
|
+
config.embedding.base_url = "";
|
|
556
|
+
}
|
|
557
|
+
const fileLlm = (fileConfig.llm ?? {});
|
|
558
|
+
if (config.llm.provider !== DEFAULTS.llm.provider
|
|
559
|
+
&& typeof fileLlm.base_url !== "string"
|
|
560
|
+
&& !process.env.LLM_BASE_URL) {
|
|
561
|
+
config.llm.base_url = "";
|
|
562
|
+
}
|
|
508
563
|
// Env var overrides (highest priority)
|
|
509
564
|
if (process.env.QDRANT_URL)
|
|
510
565
|
config.qdrant_url = process.env.QDRANT_URL;
|
|
@@ -526,6 +581,12 @@ export function loadConfig() {
|
|
|
526
581
|
}
|
|
527
582
|
if (process.env.OPENAI_API_KEY)
|
|
528
583
|
config.embedding.api_key = process.env.OPENAI_API_KEY;
|
|
584
|
+
// Portkey users can supply their gateway key via PORTKEY_API_KEY without
|
|
585
|
+
// needing to repurpose OPENAI_API_KEY. Only applied when the embedding
|
|
586
|
+
// provider is Portkey, so non-Portkey setups remain untouched.
|
|
587
|
+
if (process.env.PORTKEY_API_KEY && config.embedding.provider === "portkey") {
|
|
588
|
+
config.embedding.api_key = process.env.PORTKEY_API_KEY;
|
|
589
|
+
}
|
|
529
590
|
// Generic provider-extras: BIKKY_EMBEDDING_EXTRA_<KEY>=value
|
|
530
591
|
config.embedding.extra = config.embedding.extra ?? {};
|
|
531
592
|
for (const [k, v] of Object.entries(process.env)) {
|
|
@@ -542,6 +603,9 @@ export function loadConfig() {
|
|
|
542
603
|
config.llm.base_url = process.env.LLM_BASE_URL;
|
|
543
604
|
if (process.env.OPENAI_API_KEY && !config.llm.api_key)
|
|
544
605
|
config.llm.api_key = process.env.OPENAI_API_KEY;
|
|
606
|
+
if (process.env.PORTKEY_API_KEY && config.llm.provider === "portkey" && !config.llm.api_key) {
|
|
607
|
+
config.llm.api_key = process.env.PORTKEY_API_KEY;
|
|
608
|
+
}
|
|
545
609
|
if (process.env.LLM_FALLBACK_PROVIDER)
|
|
546
610
|
config.llm.fallback_provider = process.env.LLM_FALLBACK_PROVIDER;
|
|
547
611
|
if (process.env.AWS_PROFILE)
|
|
@@ -632,6 +696,10 @@ export function loadConfig() {
|
|
|
632
696
|
const entityTypingMax = positiveInt(process.env.BIKKY_DAEMON_ENTITY_TYPING_MAX_ENTITIES_PER_RUN);
|
|
633
697
|
if (entityTypingMax !== null)
|
|
634
698
|
config.daemon.entity_typing_max_entities_per_run = entityTypingMax;
|
|
699
|
+
if (process.env.BIKKY_USER_ID)
|
|
700
|
+
config.identity.user_id = process.env.BIKKY_USER_ID;
|
|
701
|
+
if (process.env.BIKKY_USER_NAME)
|
|
702
|
+
config.identity.user_name = process.env.BIKKY_USER_NAME;
|
|
635
703
|
if (process.env.BIKKY_ACTOR_ID)
|
|
636
704
|
config.identity.actor_id = process.env.BIKKY_ACTOR_ID;
|
|
637
705
|
if (process.env.BIKKY_ACTOR_LABEL)
|
|
@@ -679,8 +747,8 @@ export function getEffectiveDestinations(config = loadConfig()) {
|
|
|
679
747
|
}
|
|
680
748
|
/** Save config to disk (used by setup command). */
|
|
681
749
|
export function saveConfig(config) {
|
|
682
|
-
fs.mkdirSync(
|
|
683
|
-
fs.writeFileSync(
|
|
750
|
+
fs.mkdirSync(getBikkyDir(), { recursive: true });
|
|
751
|
+
fs.writeFileSync(getConfigPath(), JSON.stringify(config, null, 2) + "\n");
|
|
684
752
|
_config = config;
|
|
685
753
|
}
|
|
686
754
|
/** Reset cached config (for testing). */
|
|
@@ -86,7 +86,6 @@ export declare const CAPTURE_KIND_SUBTYPES: {
|
|
|
86
86
|
export declare const FACT_CATEGORY_TO_SUBTYPE: Record<Category, MemorySubtype>;
|
|
87
87
|
export declare const DEFAULT_CAPTURE_CONTEXT: {
|
|
88
88
|
readonly domain: "software_engineering" | "product_strategy" | "business_operations" | "research" | "personal_productivity";
|
|
89
|
-
readonly source: "system";
|
|
90
89
|
readonly reviewStatus: "candidate";
|
|
91
90
|
readonly volatility: "evolving";
|
|
92
91
|
};
|
|
@@ -110,7 +110,6 @@ export const FACT_CATEGORY_TO_SUBTYPE = {
|
|
|
110
110
|
};
|
|
111
111
|
export const DEFAULT_CAPTURE_CONTEXT = {
|
|
112
112
|
domain: DEFAULT_DOMAIN,
|
|
113
|
-
source: "system",
|
|
114
113
|
reviewStatus: "candidate",
|
|
115
114
|
// Default fallback volatility when the LLM does not self-judge. Storage path
|
|
116
115
|
// overrides this with the LLM's value (or the volatility verifier's
|
|
@@ -51,6 +51,7 @@ declare const detectContradiction: (fact: {
|
|
|
51
51
|
}, _config: BikkyConfig, telemetry?: {
|
|
52
52
|
sessionId?: string;
|
|
53
53
|
workstreamKey?: string;
|
|
54
|
+
destination?: string;
|
|
54
55
|
}) => Promise<ContradictionResult>;
|
|
55
56
|
/**
|
|
56
57
|
* Main consolidation tick — called from daemon tick loop.
|
|
@@ -58,6 +59,6 @@ declare const detectContradiction: (fact: {
|
|
|
58
59
|
*/
|
|
59
60
|
declare const tick: (config: BikkyConfig, opts?: ConsolidationTickOptions) => Promise<void>;
|
|
60
61
|
/** Reset state (for testing). */
|
|
61
|
-
declare const _reset: () => void;
|
|
62
|
+
declare const _reset: (tickCount?: number) => void;
|
|
62
63
|
export { detectContradiction, tick, setLogger, _reset, };
|
|
63
64
|
//# sourceMappingURL=consolidation.d.ts.map
|
|
@@ -13,6 +13,7 @@ import { mkdirSync, writeFileSync } from "node:fs";
|
|
|
13
13
|
import { join, dirname } from "node:path";
|
|
14
14
|
import { createHash } from "node:crypto";
|
|
15
15
|
import * as qdrant from "./qdrant.js";
|
|
16
|
+
import { buildOperationOrigin } from "../provenance/origin.js";
|
|
16
17
|
import { chatCompletion } from "../llm/index.js";
|
|
17
18
|
import { categoryValues, normalizeCategory, normalizeDomain } from "../mcp/taxonomy.js";
|
|
18
19
|
import { distillPrompt, DISTILL_PROMPT_DESCRIPTOR, contradictionPrompt, briefPrompt, BRIEF_PROMPT_DESCRIPTOR, safeParseJson, } from "../prompts/index.js";
|
|
@@ -33,6 +34,8 @@ const autoDistill = async (_config, { minSummaries = 5 } = {}) => {
|
|
|
33
34
|
if (!qdrant.isReady())
|
|
34
35
|
return { distilled: false };
|
|
35
36
|
try {
|
|
37
|
+
const destination = qdrant.resolveDestination({}).name;
|
|
38
|
+
const collection = qdrant.collectionForDestination(destination);
|
|
36
39
|
// Find undistilled session summaries (support both legacy and new taxonomy)
|
|
37
40
|
const legacyFilter = {
|
|
38
41
|
must: [
|
|
@@ -47,12 +50,12 @@ const autoDistill = async (_config, { minSummaries = 5 } = {}) => {
|
|
|
47
50
|
],
|
|
48
51
|
};
|
|
49
52
|
const [legacyRes, newRes] = await Promise.all([
|
|
50
|
-
qdrant.qdrantRequest("POST", `/collections/${
|
|
53
|
+
qdrant.qdrantRequest("POST", `/collections/${collection}/points/scroll`, {
|
|
51
54
|
filter: legacyFilter, limit: 50, with_payload: true,
|
|
52
|
-
}),
|
|
53
|
-
qdrant.qdrantRequest("POST", `/collections/${
|
|
55
|
+
}, destination),
|
|
56
|
+
qdrant.qdrantRequest("POST", `/collections/${collection}/points/scroll`, {
|
|
54
57
|
filter: newFilter, limit: 50, with_payload: true,
|
|
55
|
-
}),
|
|
58
|
+
}, destination),
|
|
56
59
|
]);
|
|
57
60
|
// Deduplicate by ID
|
|
58
61
|
const seen = new Set();
|
|
@@ -107,7 +110,6 @@ const autoDistill = async (_config, { minSummaries = 5 } = {}) => {
|
|
|
107
110
|
domain: normalizeDomain(pattern.domain ?? "software_engineering"),
|
|
108
111
|
kind: "distilled",
|
|
109
112
|
entities: Array.isArray(pattern.entities) ? pattern.entities.map(e => String(e).toLowerCase()) : [],
|
|
110
|
-
source: "system",
|
|
111
113
|
confidence: 0.85,
|
|
112
114
|
importance: pattern.importance || 0.7,
|
|
113
115
|
content_hash: hash,
|
|
@@ -116,11 +118,25 @@ const autoDistill = async (_config, { minSummaries = 5 } = {}) => {
|
|
|
116
118
|
distilled_at: new Date().toISOString(),
|
|
117
119
|
distilled_by_prompt: promptStamp,
|
|
118
120
|
},
|
|
119
|
-
|
|
121
|
+
origin: buildOperationOrigin({
|
|
122
|
+
interface: "daemon",
|
|
123
|
+
action: "create",
|
|
124
|
+
subsystem: "consolidation",
|
|
125
|
+
metadata: {
|
|
126
|
+
distilled_from_count: batch.length,
|
|
127
|
+
prompt: promptStamp,
|
|
128
|
+
},
|
|
129
|
+
}),
|
|
130
|
+
}, { destination });
|
|
120
131
|
}
|
|
121
132
|
// Supersede the source summaries
|
|
122
133
|
for (const pt of batch) {
|
|
123
|
-
await qdrant.supersedeFact(pt.id, `distilled:${new Date().toISOString()}
|
|
134
|
+
await qdrant.supersedeFact(pt.id, `distilled:${new Date().toISOString()}`, destination, buildOperationOrigin({
|
|
135
|
+
interface: "daemon",
|
|
136
|
+
action: "supersede",
|
|
137
|
+
subsystem: "consolidation",
|
|
138
|
+
metadata: { prompt: promptStamp },
|
|
139
|
+
}));
|
|
124
140
|
}
|
|
125
141
|
logFn("INFO", `Auto-distill: consolidated ${batch.length} summaries into ${patterns.length} patterns`);
|
|
126
142
|
return { distilled: true, count: patterns.length };
|
|
@@ -143,12 +159,13 @@ const detectContradiction = async (fact, _config, telemetry) => {
|
|
|
143
159
|
try {
|
|
144
160
|
const vector = await qdrant.embed(fact.content);
|
|
145
161
|
// Search across all categories because contradictions can cross category lines.
|
|
146
|
-
const
|
|
162
|
+
const collection = qdrant.collectionForDestination(telemetry?.destination);
|
|
163
|
+
const results = await qdrant.qdrantRequest("POST", `/collections/${collection}/points/search`, {
|
|
147
164
|
vector,
|
|
148
165
|
filter: { must: [{ is_null: { key: "superseded_by" } }] },
|
|
149
166
|
limit: 5,
|
|
150
167
|
with_payload: true,
|
|
151
|
-
});
|
|
168
|
+
}, telemetry?.destination);
|
|
152
169
|
const candidates = (results.result || [])
|
|
153
170
|
.filter(r => r.score >= 0.75 && r.score < 0.92);
|
|
154
171
|
if (candidates.length === 0)
|
|
@@ -448,8 +465,8 @@ const tick = async (config, opts = {}) => {
|
|
|
448
465
|
}
|
|
449
466
|
};
|
|
450
467
|
/** Reset state (for testing). */
|
|
451
|
-
const _reset = () => {
|
|
452
|
-
consolidationTickCount =
|
|
468
|
+
const _reset = (tickCount = 0) => {
|
|
469
|
+
consolidationTickCount = tickCount;
|
|
453
470
|
};
|
|
454
471
|
export { detectContradiction, tick, setLogger, _reset, };
|
|
455
472
|
//# sourceMappingURL=consolidation.js.map
|
|
@@ -11,6 +11,7 @@ import { entityTypingPrompt, ENTITY_TYPING_PROMPT_DESCRIPTOR, safeParseJson } fr
|
|
|
11
11
|
import * as qdrant from "./qdrant.js";
|
|
12
12
|
import { isAttemptBackedOff, pruneRecentAttempts, readMaintenanceState, recordMaintenanceRun, shouldRunMaintenance, } from "./maintenance-state.js";
|
|
13
13
|
import { isGenericEntity } from "./relations-vocab.js";
|
|
14
|
+
import { buildOperationOrigin } from "../provenance/origin.js";
|
|
14
15
|
const FACTS_SCAN_LIMIT = 200;
|
|
15
16
|
const FACTS_PER_ENTITY = 5;
|
|
16
17
|
const DEFAULT_LOOKBACK_MS = 7 * 24 * 60 * 60 * 1000;
|
|
@@ -171,6 +172,15 @@ const upsertEntityTypePoint = async (candidate, classification, source) => {
|
|
|
171
172
|
classified_at: now,
|
|
172
173
|
updated_at: now,
|
|
173
174
|
created_at: now,
|
|
175
|
+
origin: buildOperationOrigin({
|
|
176
|
+
interface: "daemon",
|
|
177
|
+
action: "create",
|
|
178
|
+
subsystem: "entity_typing",
|
|
179
|
+
metadata: {
|
|
180
|
+
entity_name: candidate.name,
|
|
181
|
+
classification_source: source,
|
|
182
|
+
},
|
|
183
|
+
}),
|
|
174
184
|
source_fact_ids: candidate.factIds,
|
|
175
185
|
...(candidate.workstreamKeys.length > 0 ? { workstream_key: candidate.workstreamKeys[0] } : {}),
|
|
176
186
|
metadata: {
|
|
@@ -2,6 +2,7 @@ import type { BikkyConfig } from "../config.js";
|
|
|
2
2
|
import type { QdrantPayload } from "./qdrant.js";
|
|
3
3
|
import { type RedactionSummary } from "../privacy/redaction.js";
|
|
4
4
|
import { type WorkstreamRegistry } from "./workstream-resolver.js";
|
|
5
|
+
import { type OperationOrigin } from "../provenance/origin.js";
|
|
5
6
|
export { buildEpisodeSummaryMessages } from "../prompts/index.js";
|
|
6
7
|
export interface WorkspaceScope {
|
|
7
8
|
workspaceId?: string;
|
|
@@ -33,6 +34,7 @@ export interface EpisodeSummaryWriteResult {
|
|
|
33
34
|
action: "stored" | "updated" | "skipped";
|
|
34
35
|
factId?: string;
|
|
35
36
|
episodeId?: string;
|
|
37
|
+
destination?: string;
|
|
36
38
|
workstreamKey?: string | null;
|
|
37
39
|
reason?: string;
|
|
38
40
|
}
|
|
@@ -57,6 +59,8 @@ export declare const buildEpisodeSummaryPayload: (input: {
|
|
|
57
59
|
enabled: boolean;
|
|
58
60
|
redactPii: boolean;
|
|
59
61
|
};
|
|
62
|
+
config?: BikkyConfig;
|
|
63
|
+
origin?: OperationOrigin;
|
|
60
64
|
}) => EpisodeSummaryPayloadResult;
|
|
61
65
|
export declare const updateEpisodeSummary: (input: {
|
|
62
66
|
segment: EpisodeSegment;
|
|
@@ -13,6 +13,7 @@ import { CAPTURE_POLICY_VERSION, CAPTURE_TRIGGERS, DEFAULT_CAPTURE_CONTEXT, PROM
|
|
|
13
13
|
import * as qdrant from "./qdrant.js";
|
|
14
14
|
import { combineRedactions, redactStorageText, } from "../privacy/redaction.js";
|
|
15
15
|
import { resolveWorkstreamKey, } from "./workstream-resolver.js";
|
|
16
|
+
import { buildOperationOrigin } from "../provenance/origin.js";
|
|
16
17
|
export { buildEpisodeSummaryMessages } from "../prompts/index.js";
|
|
17
18
|
const DEFAULT_EPISODE_IMPORTANCE = 0.75;
|
|
18
19
|
const contentHash = (text) => createHash("sha256").update(`episode:${text}`).digest("hex");
|
|
@@ -120,6 +121,17 @@ export const buildEpisodeSummaryPayload = (input) => {
|
|
|
120
121
|
]);
|
|
121
122
|
const existingPayload = input.existing?.payload ?? {};
|
|
122
123
|
const workstreamKey = input.draft.workstream_key ?? input.segment.workstream_key ?? null;
|
|
124
|
+
const operationOrigin = input.origin ?? buildOperationOrigin({
|
|
125
|
+
interface: "daemon",
|
|
126
|
+
action: input.existing ? "update" : "create",
|
|
127
|
+
subsystem: "episode_summary",
|
|
128
|
+
config: input.config,
|
|
129
|
+
metadata: {
|
|
130
|
+
session_id: input.sessionId,
|
|
131
|
+
episode_id: input.segment.episode_id,
|
|
132
|
+
event_count: input.segment.event_count,
|
|
133
|
+
},
|
|
134
|
+
});
|
|
123
135
|
const payload = {
|
|
124
136
|
...existingPayload,
|
|
125
137
|
content: redactedContent.text,
|
|
@@ -129,9 +141,9 @@ export const buildEpisodeSummaryPayload = (input) => {
|
|
|
129
141
|
memory_subtype: "episode",
|
|
130
142
|
layer: "episode",
|
|
131
143
|
...(input.scope.workspaceId ? { workspace_id: input.scope.workspaceId } : {}),
|
|
132
|
-
|
|
144
|
+
origin: existingPayload.origin ?? operationOrigin,
|
|
145
|
+
...(input.existing ? { last_operation_origin: operationOrigin } : {}),
|
|
133
146
|
entities: redactedEntities.map((entity) => entity.text.toLowerCase()),
|
|
134
|
-
source: "system",
|
|
135
147
|
confidence: 1.0,
|
|
136
148
|
importance: input.draft.importance,
|
|
137
149
|
content_hash: contentHash(redactedContent.text),
|
|
@@ -184,19 +196,36 @@ const summarizeEpisodeTranscript = async (input) => {
|
|
|
184
196
|
throw new Error("Episode summary LLM returned null");
|
|
185
197
|
return parseEpisodeSummaryDraft(result);
|
|
186
198
|
};
|
|
187
|
-
const findExistingEpisodeSummary = async (episodeId, scope) => {
|
|
188
|
-
const
|
|
199
|
+
const findExistingEpisodeSummary = async (episodeId, scope, destination) => {
|
|
200
|
+
const collection = qdrant.collectionForDestination(destination);
|
|
201
|
+
const result = await qdrant.qdrantRequest("POST", `/collections/${collection}/points/scroll`, {
|
|
189
202
|
filter: buildEpisodeSummaryFilter(episodeId, scope),
|
|
190
203
|
limit: 1,
|
|
191
204
|
with_payload: true,
|
|
192
|
-
});
|
|
205
|
+
}, destination);
|
|
193
206
|
return result.result?.points?.[0] ?? null;
|
|
194
207
|
};
|
|
195
208
|
export const updateEpisodeSummary = async (input) => {
|
|
196
209
|
if (!input.segment.transcript.trim()) {
|
|
197
210
|
return { action: "skipped", reason: "empty_transcript", episodeId: input.segment.episode_id };
|
|
198
211
|
}
|
|
199
|
-
const
|
|
212
|
+
const destination = qdrant.resolveDestination({
|
|
213
|
+
content: input.segment.transcript,
|
|
214
|
+
entities: [
|
|
215
|
+
input.sessionId,
|
|
216
|
+
...(input.segment.workstream_key ? [input.segment.workstream_key] : []),
|
|
217
|
+
],
|
|
218
|
+
metadata: {
|
|
219
|
+
session_id: input.sessionId,
|
|
220
|
+
episode_id: input.segment.episode_id,
|
|
221
|
+
...(input.segment.workstream_key ? { workstream_key: input.segment.workstream_key } : {}),
|
|
222
|
+
memory_subtype: "episode",
|
|
223
|
+
kind: "summary",
|
|
224
|
+
origin_interface: "daemon",
|
|
225
|
+
origin_agent_type: "daemon",
|
|
226
|
+
},
|
|
227
|
+
});
|
|
228
|
+
const existing = await findExistingEpisodeSummary(input.segment.episode_id, input.scope, destination.name);
|
|
200
229
|
const draft = await summarizeEpisodeTranscript({
|
|
201
230
|
transcript: input.segment.transcript,
|
|
202
231
|
sessionId: input.sessionId,
|
|
@@ -222,6 +251,7 @@ export const updateEpisodeSummary = async (input) => {
|
|
|
222
251
|
scope: input.scope,
|
|
223
252
|
now,
|
|
224
253
|
existing,
|
|
254
|
+
config: input.config,
|
|
225
255
|
redactionOptions: {
|
|
226
256
|
enabled: true,
|
|
227
257
|
redactPii: false,
|
|
@@ -229,13 +259,14 @@ export const updateEpisodeSummary = async (input) => {
|
|
|
229
259
|
});
|
|
230
260
|
const vector = await qdrant.embed(String(payload.content));
|
|
231
261
|
const factId = existing?.id ?? randomUUID();
|
|
232
|
-
await qdrant.qdrantRequest("PUT", `/collections/${
|
|
262
|
+
await qdrant.qdrantRequest("PUT", `/collections/${destination.collection}/points`, {
|
|
233
263
|
points: [{ id: factId, vector, payload }],
|
|
234
|
-
});
|
|
264
|
+
}, destination.name);
|
|
235
265
|
return {
|
|
236
266
|
action: existing ? "updated" : "stored",
|
|
237
267
|
factId,
|
|
238
268
|
episodeId: input.segment.episode_id,
|
|
269
|
+
destination: destination.name,
|
|
239
270
|
workstreamKey: resolved.key,
|
|
240
271
|
};
|
|
241
272
|
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Events-based memory extraction — reads supported coding-agent transcripts,
|
|
3
|
-
* extracts facts via LLM, and stores them in Qdrant with
|
|
3
|
+
* extracts facts via LLM, and stores them in Qdrant with daemon origin metadata.
|
|
4
4
|
*
|
|
5
5
|
* Uses a JSON file for extraction state (high-water byte offsets) instead of SQLite.
|
|
6
6
|
* Copilot session detection uses lock files. Claude Code detection uses
|