@gamaze/hicortex 0.16.1 → 0.16.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 +11 -0
- package/dist/capture.d.ts +18 -1
- package/dist/capture.js +3 -2
- package/dist/classify-domains.d.ts +1 -1
- package/dist/classify-domains.js +5 -7
- package/dist/cli.js +11 -3
- package/dist/cluster.d.ts +5 -4
- package/dist/cluster.js +2 -3
- package/dist/consolidate.js +3 -5
- package/dist/db.js +23 -0
- package/dist/dedup.js +1 -1
- package/dist/distiller.js +2 -2
- package/dist/domain-classify.d.ts +1 -1
- package/dist/domain-classify.js +1 -5
- package/dist/eval/run-eval.js +0 -1
- package/dist/index.js +0 -1
- package/dist/init.d.ts +165 -0
- package/dist/init.js +283 -57
- package/dist/mcp-server.js +71 -25
- package/dist/nightly.js +121 -12
- package/dist/nofit.d.ts +1 -1
- package/dist/nofit.js +1 -2
- package/dist/pi-transcript-reader.d.ts +1 -1
- package/dist/pi-transcript-reader.js +1 -1
- package/dist/prompts.js +0 -7
- package/dist/recall-index.d.ts +15 -19
- package/dist/recall-index.js +13 -23
- package/dist/redact.d.ts +2 -2
- package/dist/redact.js +2 -2
- package/dist/retrieval.d.ts +7 -7
- package/dist/retrieval.js +20 -24
- package/dist/schema-prototypes.d.ts +8 -13
- package/dist/schema-prototypes.js +13 -22
- package/dist/seed-lesson.js +0 -1
- package/dist/storage.d.ts +9 -12
- package/dist/storage.js +19 -21
- package/dist/telemetry.d.ts +13 -2
- package/dist/telemetry.js +5 -1
- package/dist/types.d.ts +70 -24
- package/domains.example.json +2 -3
- package/hermes-plugin/hicortex/README.md +3 -1
- package/hermes-plugin/hicortex/config.py +34 -3
- package/hermes-plugin/hicortex/plugin.yaml +1 -1
- package/hermes-plugin/hicortex/provider.py +7 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -164,6 +164,7 @@ npx @gamaze/hicortex context edit <name> # Edit a context section in $EDIT
|
|
|
164
164
|
npx @gamaze/hicortex context show --agent <id> # Show a specific agent's resolved context (0.13)
|
|
165
165
|
npx @gamaze/hicortex init --agent-name <name> # Opt in to a per-agent context id (default: unset — shared global context)
|
|
166
166
|
npx @gamaze/hicortex init --agent-name "" # Clear it back to global context
|
|
167
|
+
npx @gamaze/hicortex init --repair-config # Recover from a malformed config.json (see below)
|
|
167
168
|
npx @gamaze/hicortex telemetry # Show exactly what anonymous telemetry sends
|
|
168
169
|
npx @gamaze/hicortex status # Show config, DB stats
|
|
169
170
|
npx @gamaze/hicortex uninstall # Remove CC integration (keeps DB)
|
|
@@ -211,6 +212,9 @@ Config at `~/.hicortex/config.json`. Created by `init`. Key options:
|
|
|
211
212
|
| `contextAgents` | Per-agent context modes (0.13): `{ "<id>": "override" \| "global" \| "off" }`. Absent + no `agents/<id>/` dir → every agent gets the global set. Boot-time (restart to apply) — see [Per-agent context](#per-agent-context-013) |
|
|
212
213
|
| `agentName` | This install's per-agent context id sent as `?agent=`. **Unset by default** (CC shares the global context — no `?agent=` sent). Explicit opt-in via `init --agent-name <name>`; `init --agent-name ""` clears it. An empty/whitespace value equals unset |
|
|
213
214
|
| `nightlyHour` | Local hour (0–23) for the nightly job installed by `init` (defaults: client 2, server 3). Applied on fresh installs; existing schedules are never overwritten |
|
|
215
|
+
| `preflightTimeoutMs` | **Client mode only.** Per-attempt timeout for the nightly's server-reachability check before it starts capturing (default: 15000 ms) |
|
|
216
|
+
| `preflightAttempts` | **Client mode only.** Reachability-check retries before the nightly aborts (default: 3; floored at 1). `1` = single try, no retry |
|
|
217
|
+
| `preflightRetryGapMs` | **Client mode only.** Delay between reachability retries (default: 60000 ms). Note: timers don't advance while the machine is asleep, so on a sleeping laptop this gap counts awake-time, not wall-clock |
|
|
214
218
|
| `scoreSimilarityWeight` | Weight of semantic similarity in the ranking score (default: 0.50) |
|
|
215
219
|
| `scoreStrengthWeight` | Weight of effective strength — importance/use/recency of access (default: 0.20) |
|
|
216
220
|
| `scoreConnectionsWeight` | Weight of graph centrality (default: 0.15) |
|
|
@@ -330,6 +334,13 @@ npm test
|
|
|
330
334
|
|
|
331
335
|
## Troubleshooting
|
|
332
336
|
|
|
337
|
+
**`init` fails with "Refusing to write ~/.hicortex/config.json":** the file exists but is not valid JSON — usually a hand-edit slip (a trailing comma, a truncated write). `init` refuses rather than overwriting it, because overwriting would lose `authToken`, `licenseKey`, `distillApiKey`, and your `domains` list. Two ways out:
|
|
338
|
+
|
|
339
|
+
1. **Preferred — fix the JSON.** The error names the parse failure and its position. Correct it and re-run `init`. Nothing is lost.
|
|
340
|
+
2. **`npx @gamaze/hicortex init --repair-config`.** Moves the broken file to `config.json.corrupt-<timestamp>` and rebuilds from scratch. Nothing is deleted, and it prints the top-level key names it found (names only — never secret values) so you know what to copy back. **This mints a new `authToken`**, so every thin client pointing at this server must be updated or its recall will silently 401 (recall is fail-soft — you will see no error, just no memories).
|
|
341
|
+
|
|
342
|
+
The nightly and the server behave differently on purpose: a malformed config makes them log a warning and run degraded rather than refuse to start, so a broken config never takes recall offline.
|
|
343
|
+
|
|
333
344
|
**Tools not visible to agent (OC):** The plugin auto-adds tools to `tools.allow` on startup. Restart the gateway after install.
|
|
334
345
|
|
|
335
346
|
**OC plugin: "Server unreachable":** The plugin requires a running Hicortex server. Run `npx @gamaze/hicortex init` on the same machine, or set `serverUrl` in the plugin config to point at a remote server.
|
package/dist/capture.d.ts
CHANGED
|
@@ -52,11 +52,17 @@ export interface Segment {
|
|
|
52
52
|
export interface DistillBody {
|
|
53
53
|
text: string;
|
|
54
54
|
source_agent: string;
|
|
55
|
+
/** Stable client UUID (config.json `agentId`). Attribution only. */
|
|
56
|
+
source_agent_id?: string | null;
|
|
57
|
+
/** Client-declared topic/domain of the capturing agent. Provenance only. */
|
|
58
|
+
source_domain?: string | null;
|
|
55
59
|
project: string;
|
|
56
60
|
session_id: string;
|
|
57
61
|
segment_id: string;
|
|
58
62
|
session_date: string;
|
|
59
|
-
|
|
63
|
+
/** 0.16.x: optional/vestigial. The distiller no longer sets it; a legacy
|
|
64
|
+
* client may. Honored if present, else the memory stores NULL. */
|
|
65
|
+
privacy?: string;
|
|
60
66
|
}
|
|
61
67
|
/** Normalized POST result the caller's transport returns. */
|
|
62
68
|
export interface PostResult {
|
|
@@ -73,6 +79,17 @@ export interface CaptureOptions {
|
|
|
73
79
|
dryRun?: boolean;
|
|
74
80
|
/** Segment size cap; defaults to SEGMENT_MAX_CHARS. Lowered in tests. */
|
|
75
81
|
segmentMaxChars?: number;
|
|
82
|
+
/**
|
|
83
|
+
* Per-client attribution UUID (config.json `agentId`). Sent on every
|
|
84
|
+
* segment as `source_agent_id`. Null when the client has no `agentId`
|
|
85
|
+
* (e.g. a pre-0.16.x config that has not re-run init).
|
|
86
|
+
*/
|
|
87
|
+
sourceAgentId?: string | null;
|
|
88
|
+
/**
|
|
89
|
+
* Per-client declared topic/domain (config.json `sourceDomain`). Sent as
|
|
90
|
+
* `source_domain` provenance. Null when undeclared.
|
|
91
|
+
*/
|
|
92
|
+
sourceDomain?: string | null;
|
|
76
93
|
}
|
|
77
94
|
export interface CaptureResult {
|
|
78
95
|
memoriesIngested: number;
|
package/dist/capture.js
CHANGED
|
@@ -134,7 +134,7 @@ function packSegments(entries, startCursor, entryCursors, maxChars = exports.SEG
|
|
|
134
134
|
* boundary) while other sessions continue. A 429/401 stops the whole loop.
|
|
135
135
|
*/
|
|
136
136
|
async function captureBatches(batches, opts) {
|
|
137
|
-
const { post, cursorStore, dryRun = false, segmentMaxChars = exports.SEGMENT_MAX_CHARS } = opts;
|
|
137
|
+
const { post, cursorStore, dryRun = false, segmentMaxChars = exports.SEGMENT_MAX_CHARS, sourceAgentId, sourceDomain } = opts;
|
|
138
138
|
let memoriesIngested = 0;
|
|
139
139
|
let sessionsSent = 0;
|
|
140
140
|
let hadTransientFailure = false;
|
|
@@ -186,11 +186,12 @@ async function captureBatches(batches, opts) {
|
|
|
186
186
|
const body = {
|
|
187
187
|
text: seg.text,
|
|
188
188
|
source_agent: batch.sourceAgent ?? `claude-code/${batch.projectName}`,
|
|
189
|
+
source_agent_id: sourceAgentId ?? null,
|
|
190
|
+
source_domain: sourceDomain ?? null,
|
|
189
191
|
project: batch.projectName,
|
|
190
192
|
session_id: batch.sessionId,
|
|
191
193
|
segment_id: `${genPrefix}${seg.segStart}-${seg.segEnd}${seg.idSuffix}`,
|
|
192
194
|
session_date: batch.date,
|
|
193
|
-
privacy: "WORK",
|
|
194
195
|
};
|
|
195
196
|
let result;
|
|
196
197
|
try {
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* reflect tier), same as the nightly. The LLM emits ONLY the ordered tag
|
|
17
17
|
* set; per-tag weights come from the domain prototypes (computed once at
|
|
18
18
|
* run start) and the PRIMARY (memories.domain) is derived (argmax weight,
|
|
19
|
-
*
|
|
19
|
+
* LLM order breaking ties) inside
|
|
20
20
|
* storage.setMemoryTags. After a completed (non-aborted) run the
|
|
21
21
|
* prototypes, all weights, and all primaries are recomputed from the
|
|
22
22
|
* final tag sets — same reconsolidation pass as the nightly.
|
package/dist/classify-domains.js
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* reflect tier), same as the nightly. The LLM emits ONLY the ordered tag
|
|
18
18
|
* set; per-tag weights come from the domain prototypes (computed once at
|
|
19
19
|
* run start) and the PRIMARY (memories.domain) is derived (argmax weight,
|
|
20
|
-
*
|
|
20
|
+
* LLM order breaking ties) inside
|
|
21
21
|
* storage.setMemoryTags. After a completed (non-aborted) run the
|
|
22
22
|
* prototypes, all weights, and all primaries are recomputed from the
|
|
23
23
|
* final tag sets — same reconsolidation pass as the nightly.
|
|
@@ -195,7 +195,6 @@ async function runClassifyDomains(options = {}) {
|
|
|
195
195
|
// Prototypes once at run start — newly classified memories get their
|
|
196
196
|
// weights from these; the post-run reconsolidation pass refreshes
|
|
197
197
|
// everything from the final tag sets.
|
|
198
|
-
const compartments = (0, schema_prototypes_js_1.compartmentSet)(domains);
|
|
199
198
|
const { prototypes } = await (0, schema_prototypes_js_1.computeDomainPrototypes)(db, domains, getEmbedFn);
|
|
200
199
|
// Scope filter: default = NULL / not-in-set / no tags yet; --all = everything.
|
|
201
200
|
const placeholders = domains.map(() => "?").join(", ");
|
|
@@ -255,10 +254,9 @@ async function runClassifyDomains(options = {}) {
|
|
|
255
254
|
continue;
|
|
256
255
|
}
|
|
257
256
|
// Derived primary (argmax weight from the run-start prototypes,
|
|
258
|
-
//
|
|
259
|
-
// setMemoryTags will write below.
|
|
257
|
+
// LLM order breaking ties) — the same value setMemoryTags writes below.
|
|
260
258
|
const weights = (0, schema_prototypes_js_1.computeTagWeights)(db, row.id, result.tags, prototypes);
|
|
261
|
-
const derived = (0, schema_prototypes_js_1.derivePrimary)(result.tags.map((tag) => ({ tag, weight: weights[tag] ?? null }))
|
|
259
|
+
const derived = (0, schema_prototypes_js_1.derivePrimary)(result.tags.map((tag) => ({ tag, weight: weights[tag] ?? null })));
|
|
262
260
|
if (derived === row.domain) {
|
|
263
261
|
batchUnchanged++;
|
|
264
262
|
}
|
|
@@ -273,10 +271,10 @@ async function runClassifyDomains(options = {}) {
|
|
|
273
271
|
const tx = db.transaction(() => {
|
|
274
272
|
for (const w of writes) {
|
|
275
273
|
if (w.kind === "tags") {
|
|
276
|
-
storage.setMemoryTags(db, w.id, w.tags, { weights: w.weights
|
|
274
|
+
storage.setMemoryTags(db, w.id, w.tags, { weights: w.weights });
|
|
277
275
|
}
|
|
278
276
|
else if (w.resolution.kind === "weak_primary") {
|
|
279
|
-
(0, nofit_js_1.applyWeakPrimary)(db, w.id, w.resolution.domain, w.resolution.weight
|
|
277
|
+
(0, nofit_js_1.applyWeakPrimary)(db, w.id, w.resolution.domain, w.resolution.weight);
|
|
280
278
|
}
|
|
281
279
|
else {
|
|
282
280
|
(0, nofit_js_1.applyNoAssociationDecay)(db, w.id);
|
package/dist/cli.js
CHANGED
|
@@ -40,12 +40,17 @@ switch (command) {
|
|
|
40
40
|
agentName = (0, cli_args_js_1.readValueFlag)(process.argv, "--agent-name");
|
|
41
41
|
}
|
|
42
42
|
catch {
|
|
43
|
-
console.error("[hicortex] init: --agent-name requires a value, e.g. --agent-name
|
|
43
|
+
console.error("[hicortex] init: --agent-name requires a value, e.g. --agent-name my-agent");
|
|
44
44
|
process.exit(1);
|
|
45
45
|
}
|
|
46
|
+
const repairConfig = process.argv.includes("--repair-config");
|
|
46
47
|
import("./init.js").then(({ runInit }) => {
|
|
47
|
-
runInit({ serverUrl, agentName }).catch((err) => {
|
|
48
|
-
|
|
48
|
+
runInit({ serverUrl, agentName, repairConfig }).catch((err) => {
|
|
49
|
+
// Operator-fixable failures (a malformed config.json) carry a complete,
|
|
50
|
+
// actionable message — print that alone. A stack trace would bury it.
|
|
51
|
+
// Anything else is a real bug and gets the full error object.
|
|
52
|
+
const operatorFixable = err instanceof Error && /^Refusing to (read|write) /.test(err.message);
|
|
53
|
+
console.error("[hicortex] Init failed:", operatorFixable ? err.message : err);
|
|
49
54
|
process.exit(1);
|
|
50
55
|
});
|
|
51
56
|
});
|
|
@@ -250,6 +255,9 @@ Commands:
|
|
|
250
255
|
init --server <url> Set up as client (remote server)
|
|
251
256
|
init --agent-name <name> Opt in to a per-agent context id (default: unset — shared global context)
|
|
252
257
|
Pass --agent-name "" to clear it back to global
|
|
258
|
+
init --repair-config Recover from a malformed ~/.hicortex/config.json: move it to
|
|
259
|
+
config.json.corrupt-<timestamp> and rebuild. Nothing is deleted.
|
|
260
|
+
Mints a NEW authToken — every thin client must be updated.
|
|
253
261
|
nightly Run nightly denoise + capture + consolidate
|
|
254
262
|
relink Resumable link-discovery pass over the ENTIRE corpus (server mode)
|
|
255
263
|
dedup Cluster + merge near-duplicate memories (server mode; dry run by default)
|
package/dist/cluster.d.ts
CHANGED
|
@@ -25,18 +25,19 @@ export declare class UnionFind {
|
|
|
25
25
|
export declare function clusterEdges(edges: Edge[], threshold: number): string[][];
|
|
26
26
|
/** Excess = sum(cluster size − 1) — rows that would disappear if every cluster merged to one. */
|
|
27
27
|
export declare function clusterExcess(clusters: string[][]): number;
|
|
28
|
-
/**
|
|
28
|
+
/** Metadata fields a merge candidate cluster must agree on. `privacy` is on the row (the
|
|
29
|
+
* column still exists) but is NOT a merge-safety field — vestigial since 0.16.2. */
|
|
29
30
|
export interface ClusterMetaRow {
|
|
30
31
|
project: string | null;
|
|
31
|
-
privacy: string;
|
|
32
|
+
privacy: string | null;
|
|
32
33
|
source_agent: string;
|
|
33
34
|
}
|
|
34
35
|
export interface ClusterMetadataMismatch {
|
|
35
36
|
projectMismatch: boolean;
|
|
36
|
-
privacyMismatch: boolean;
|
|
37
37
|
sourceAgentMismatch: boolean;
|
|
38
38
|
}
|
|
39
|
-
/** Do cluster members disagree on project/
|
|
39
|
+
/** Do cluster members disagree on project / source_agent? (merge-safety input for #100).
|
|
40
|
+
* Privacy is intentionally NOT checked — it is vestigial since 0.16.2. */
|
|
40
41
|
export declare function clusterMetadataMismatch(members: ClusterMetaRow[]): ClusterMetadataMismatch;
|
|
41
42
|
/**
|
|
42
43
|
* Build the max-cosine edge set via top-K KNN on `memory_vectors`, keeping
|
package/dist/cluster.js
CHANGED
|
@@ -69,14 +69,13 @@ function clusterEdges(edges, threshold) {
|
|
|
69
69
|
function clusterExcess(clusters) {
|
|
70
70
|
return clusters.reduce((sum, c) => sum + (c.length - 1), 0);
|
|
71
71
|
}
|
|
72
|
-
/** Do cluster members disagree on project/
|
|
72
|
+
/** Do cluster members disagree on project / source_agent? (merge-safety input for #100).
|
|
73
|
+
* Privacy is intentionally NOT checked — it is vestigial since 0.16.2. */
|
|
73
74
|
function clusterMetadataMismatch(members) {
|
|
74
75
|
const projects = new Set(members.map((m) => m.project ?? "\u0000null"));
|
|
75
|
-
const privacies = new Set(members.map((m) => m.privacy));
|
|
76
76
|
const agents = new Set(members.map((m) => m.source_agent));
|
|
77
77
|
return {
|
|
78
78
|
projectMismatch: projects.size > 1,
|
|
79
|
-
privacyMismatch: privacies.size > 1,
|
|
80
79
|
sourceAgentMismatch: agents.size > 1,
|
|
81
80
|
};
|
|
82
81
|
}
|
package/dist/consolidate.js
CHANGED
|
@@ -356,7 +356,6 @@ async function stageReflection(db, memories, llm, budget, embedFn, dryRun) {
|
|
|
356
356
|
project,
|
|
357
357
|
memoryType: "lesson",
|
|
358
358
|
baseStrength: baseStrength[severity] ?? 0.8,
|
|
359
|
-
privacy: "WORK",
|
|
360
359
|
});
|
|
361
360
|
generated++;
|
|
362
361
|
}
|
|
@@ -436,7 +435,6 @@ async function stageContentDomains(db, domains, llm, budget, embedFn, dryRun, st
|
|
|
436
435
|
return { curated: false, domains: domains.length, classified: 0, reason: `dry_run (${rows.length} would classify)` };
|
|
437
436
|
}
|
|
438
437
|
const getEmbedFn = async () => embedFn;
|
|
439
|
-
const compartments = (0, schema_prototypes_js_1.compartmentSet)(domains);
|
|
440
438
|
let classified = 0;
|
|
441
439
|
let weakPrimary = 0;
|
|
442
440
|
let noAssociationDecayed = 0;
|
|
@@ -465,7 +463,7 @@ async function stageContentDomains(db, domains, llm, budget, embedFn, dryRun, st
|
|
|
465
463
|
// double-halves.
|
|
466
464
|
const resolution = (0, nofit_js_1.resolveNoFit)(db, row.id, domains, startPrototypes, weakPrimaryFloor);
|
|
467
465
|
if (resolution.kind === "weak_primary") {
|
|
468
|
-
(0, nofit_js_1.applyWeakPrimary)(db, row.id, resolution.domain, resolution.weight
|
|
466
|
+
(0, nofit_js_1.applyWeakPrimary)(db, row.id, resolution.domain, resolution.weight);
|
|
469
467
|
weakPrimary++;
|
|
470
468
|
}
|
|
471
469
|
else {
|
|
@@ -475,7 +473,7 @@ async function stageContentDomains(db, domains, llm, budget, embedFn, dryRun, st
|
|
|
475
473
|
continue;
|
|
476
474
|
}
|
|
477
475
|
const weights = (0, schema_prototypes_js_1.computeTagWeights)(db, row.id, result.tags, startPrototypes);
|
|
478
|
-
storage.setMemoryTags(db, row.id, result.tags, { weights
|
|
476
|
+
storage.setMemoryTags(db, row.id, result.tags, { weights });
|
|
479
477
|
classified++;
|
|
480
478
|
}
|
|
481
479
|
}
|
|
@@ -834,7 +832,7 @@ function stageHubBoost(db, dryRun) {
|
|
|
834
832
|
//
|
|
835
833
|
// A later decision/correction can reverse, replace, or invalidate an earlier
|
|
836
834
|
// one — e.g. "chose Ollama for distillation" superseded a month later by
|
|
837
|
-
// "switched distillation to
|
|
835
|
+
// "switched distillation to a local 35B model over a mesh VPN". Left
|
|
838
836
|
// unlinked, retrieval and lesson selection can surface the stale one. This
|
|
839
837
|
// stage links OLD → NEW with relationship `superseded_by` and accelerates the
|
|
840
838
|
// old memory's decay, WITHOUT deleting it (unlike `hicortex dedup`'s merge —
|
package/dist/db.js
CHANGED
|
@@ -441,6 +441,29 @@ const MIGRATIONS = [
|
|
|
441
441
|
`);
|
|
442
442
|
},
|
|
443
443
|
},
|
|
444
|
+
{
|
|
445
|
+
version: 11,
|
|
446
|
+
name: "add_source_attribution",
|
|
447
|
+
up: (db) => {
|
|
448
|
+
// 0.16.x attribution + provenance. Two nullable columns on `memories`,
|
|
449
|
+
// both populated ONLY by capture (/distill) from client-declared values;
|
|
450
|
+
// nothing filters, scopes, or scores on either (attribution + echo).
|
|
451
|
+
//
|
|
452
|
+
// `source_agent_id`: the capturing client's stable UUID (config.json
|
|
453
|
+
// `agentId`, generated once by init). Survives agent/machine renames
|
|
454
|
+
// — unlike `source_agent`, a readable name. NULL on legacy rows.
|
|
455
|
+
// `source_domain`: the client-declared topic/domain of the capturing
|
|
456
|
+
// agent (config.json `domain`). Distinct from the content-classified
|
|
457
|
+
// `domain` column (which stays the LLM/prototype-derived primary).
|
|
458
|
+
// Guarded with hasColumn for idempotency across partially-migrated DBs.
|
|
459
|
+
if (!hasColumn(db, "memories", "source_agent_id")) {
|
|
460
|
+
db.exec("ALTER TABLE memories ADD COLUMN source_agent_id TEXT");
|
|
461
|
+
}
|
|
462
|
+
if (!hasColumn(db, "memories", "source_domain")) {
|
|
463
|
+
db.exec("ALTER TABLE memories ADD COLUMN source_domain TEXT");
|
|
464
|
+
}
|
|
465
|
+
},
|
|
466
|
+
},
|
|
444
467
|
];
|
|
445
468
|
/**
|
|
446
469
|
* Run all pending migrations against the database.
|
package/dist/dedup.js
CHANGED
|
@@ -287,7 +287,7 @@ async function runDedup(options = {}) {
|
|
|
287
287
|
if (members.length < 2)
|
|
288
288
|
continue; // defensive — a member vanished between KNN and load
|
|
289
289
|
const mismatch = (0, cluster_js_1.clusterMetadataMismatch)(members);
|
|
290
|
-
if (mismatch.projectMismatch || mismatch.
|
|
290
|
+
if (mismatch.projectMismatch || mismatch.sourceAgentMismatch) {
|
|
291
291
|
mismatchSkipped.push({ size: members.length, memberIds: members.map((m) => m.id), mismatch });
|
|
292
292
|
continue;
|
|
293
293
|
}
|
package/dist/distiller.js
CHANGED
|
@@ -419,8 +419,8 @@ function parseDistilledEntries(markdown) {
|
|
|
419
419
|
const lines = markdown.split("\n");
|
|
420
420
|
for (const line of lines) {
|
|
421
421
|
const trimmed = line.trim();
|
|
422
|
-
// Skip all markdown headers (session title,
|
|
423
|
-
//
|
|
422
|
+
// Skip all markdown headers (session title, section headings). Sections
|
|
423
|
+
// are NOT prefixed onto entries: each bullet already
|
|
424
424
|
// starts with its [SUBJECT] (topic-first, enforced by prompts.ts), and
|
|
425
425
|
// prepending "[Section]" re-introduced the category-first prefix the
|
|
426
426
|
// 2026-08-02 corpus rewrite removed. The section label is unused
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* (the order is used solely as an exact-weight tiebreak downstream).
|
|
19
19
|
* The PRIMARY (memories.domain) is NO LONGER requested from the LLM — audits
|
|
20
20
|
* proved LLM primaries a coin-flip on overlapping spheres. It is DERIVED
|
|
21
|
-
* deterministically (argmax association weight
|
|
21
|
+
* deterministically (argmax association weight, LLM tag order breaking ties) in
|
|
22
22
|
* schema-prototypes.ts / storage.setMemoryTags.
|
|
23
23
|
*
|
|
24
24
|
* NO-FIT = EMPTY TAG SET (owner amendment 07.07): "Unsorted" is a non-tag —
|
package/dist/domain-classify.js
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* (the order is used solely as an exact-weight tiebreak downstream).
|
|
20
20
|
* The PRIMARY (memories.domain) is NO LONGER requested from the LLM — audits
|
|
21
21
|
* proved LLM primaries a coin-flip on overlapping spheres. It is DERIVED
|
|
22
|
-
* deterministically (argmax association weight
|
|
22
|
+
* deterministically (argmax association weight, LLM tag order breaking ties) in
|
|
23
23
|
* schema-prototypes.ts / storage.setMemoryTags.
|
|
24
24
|
*
|
|
25
25
|
* NO-FIT = EMPTY TAG SET (owner amendment 07.07): "Unsorted" is a non-tag —
|
|
@@ -84,10 +84,6 @@ function parseConfigDomains(config) {
|
|
|
84
84
|
if (!name)
|
|
85
85
|
continue;
|
|
86
86
|
const def = { name, description };
|
|
87
|
-
// Compartment policy passthrough (graded-schema spec): a domain flagged
|
|
88
|
-
// `compartment: true` becomes the primary whenever tagged.
|
|
89
|
-
if (d.compartment === true)
|
|
90
|
-
def.compartment = true;
|
|
91
87
|
out.push(def);
|
|
92
88
|
}
|
|
93
89
|
return out.length > 0 ? out : null;
|
package/dist/eval/run-eval.js
CHANGED
|
@@ -65,7 +65,6 @@ function renderDups(d) {
|
|
|
65
65
|
const mismatch = cluster.metadataMismatch;
|
|
66
66
|
const mismatchFlags = [
|
|
67
67
|
mismatch.projectMismatch ? "project" : null,
|
|
68
|
-
mismatch.privacyMismatch ? "privacy" : null,
|
|
69
68
|
mismatch.sourceAgentMismatch ? "source_agent" : null,
|
|
70
69
|
].filter(Boolean);
|
|
71
70
|
lines.push(`**Cluster ${i + 1}** — size ${cluster.size}, attribution: ${cluster.attribution.recoveryReingest} recovery-pair(s) / ${cluster.attribution.organic} organic-pair(s)` +
|
package/dist/index.js
CHANGED
|
@@ -470,7 +470,6 @@ exports.default = {
|
|
|
470
470
|
source_agent: `openclaw/${context?.agentId ?? "manual"}`,
|
|
471
471
|
project: args.project,
|
|
472
472
|
memory_type: args.memory_type ?? "episode",
|
|
473
|
-
privacy: "WORK",
|
|
474
473
|
}, 15000);
|
|
475
474
|
if (!result.ok) {
|
|
476
475
|
return { error: `Ingest failed: ${result.data?.error ?? `HTTP ${result.status}`}` };
|
package/dist/init.d.ts
CHANGED
|
@@ -37,6 +37,84 @@ export declare function parseEnvFile(content: string): Record<string, string>;
|
|
|
37
37
|
* a `models.score` would then silently shadow (nested > flat).
|
|
38
38
|
*/
|
|
39
39
|
export declare function isLlmConfigured(config: Record<string, unknown>): boolean;
|
|
40
|
+
/**
|
|
41
|
+
* Detect or ask for LLM config and persist to ~/.hicortex/config.json.
|
|
42
|
+
* The daemon can't inherit shell env vars, so we persist here.
|
|
43
|
+
* LLM choice is always user-controlled: candidates are detected and presented
|
|
44
|
+
* as a numbered list; the user picks one. Nothing is auto-applied.
|
|
45
|
+
* If the user cancels, the server runs recall-only (no LLM).
|
|
46
|
+
*/
|
|
47
|
+
export declare function persistLlmConfig(configPath?: string): Promise<void>;
|
|
48
|
+
/**
|
|
49
|
+
* Strict config loader — the SINGLE source of truth for "read config.json or
|
|
50
|
+
* fail loudly". Every config writer in init (persistLlmConfig, persistAuthToken,
|
|
51
|
+
* ensureAndPersistAgentId, scaffoldDefaultDomains) loads through this, and the
|
|
52
|
+
* runtime readers (nightly, server boot) route through it too (catching the
|
|
53
|
+
* throw to fail-soft with a visible WARN).
|
|
54
|
+
*
|
|
55
|
+
* The 0.16.x BLOCKER this closes: the bare `try { JSON.parse(readFileSync) }
|
|
56
|
+
* catch { /* new file *\/ }` pattern, on a config.json that EXISTS but won't
|
|
57
|
+
* parse (a hand-edit syntax slip, truncation, corruption), silently seeded `{}`
|
|
58
|
+
* and the writer then OVERWROTE the file — `persistAuthToken` minted a fresh
|
|
59
|
+
* token (fleet-wide 401), `scaffoldDefaultDomains` re-seeded the generic
|
|
60
|
+
* vocabulary over the owner list, etc. `authToken` / `licenseKey` /
|
|
61
|
+
* `distillApiKey` / `domains` / `weakPrimaryFloor` / `contextClients` all gone.
|
|
62
|
+
* The early-return guards (existing-key checks) did NOT save them: those only
|
|
63
|
+
* fire on a VALID parse that reads the key, not on a corrupted file.
|
|
64
|
+
*
|
|
65
|
+
* Contract:
|
|
66
|
+
* - ENOENT (genuinely no file) → `{ config: {}, hadFile: false }` (a new
|
|
67
|
+
* install; the caller decides whether to persist).
|
|
68
|
+
* - Any OTHER read failure on an existing file (EACCES, etc.) → THROW.
|
|
69
|
+
* - A parse failure (bad JSON) on an existing file → THROW.
|
|
70
|
+
* - A non-object JSON value (null / array / true / 5 / "x") → THROW. Such a
|
|
71
|
+
* value is not a valid config and must not be silently replaced with {}.
|
|
72
|
+
*
|
|
73
|
+
* Refusing is the right DEFAULT, but it dead-ends the operator: `init` is
|
|
74
|
+
* exactly what you would run to repair a broken install, and it now won't run.
|
|
75
|
+
* `init --repair-config` is the explicit escape hatch — see
|
|
76
|
+
* quarantineMalformedConfig below. Never quarantine implicitly: that path mints
|
|
77
|
+
* a fresh authToken (fleet-wide 401), so it must be a deliberate choice.
|
|
78
|
+
*
|
|
79
|
+
* Exported so nightly.ts / mcp-server.ts readers can route through it.
|
|
80
|
+
*/
|
|
81
|
+
export declare function loadConfigStrict(configPath: string): {
|
|
82
|
+
config: Record<string, unknown>;
|
|
83
|
+
hadFile: boolean;
|
|
84
|
+
};
|
|
85
|
+
/**
|
|
86
|
+
* `init --repair-config` escape hatch: move a malformed config.json aside so
|
|
87
|
+
* init can rebuild, instead of dead-ending on loadConfigStrict's throw.
|
|
88
|
+
*
|
|
89
|
+
* Why this exists: refusing to overwrite a corrupt config is right (it closed
|
|
90
|
+
* the 0.16.x wipe BLOCKER), but it leaves the operator stuck — `init` is the
|
|
91
|
+
* natural repair action and it now refuses to run. Deleting the file by hand
|
|
92
|
+
* works but silently loses `licenseKey` / `authToken` / `distillApiKey`.
|
|
93
|
+
*
|
|
94
|
+
* Why it is OPT-IN and never automatic: rebuilding mints a fresh `authToken`,
|
|
95
|
+
* which 401s every thin client on the fleet until they are re-pointed. That is
|
|
96
|
+
* a deliberate operator decision, not a fallback.
|
|
97
|
+
*
|
|
98
|
+
* Behaviour:
|
|
99
|
+
* - Config absent or valid → no-op (`{ quarantined: false }`).
|
|
100
|
+
* - Malformed → rename to `config.json.corrupt-<ISO>` (colons stripped for
|
|
101
|
+
* Windows), and report the TOP-LEVEL KEY NAMES recovered from the raw text
|
|
102
|
+
* so the operator knows what to restore.
|
|
103
|
+
*
|
|
104
|
+
* SECURITY: key NAMES only, never values. `authToken`, `licenseKey`,
|
|
105
|
+
* `distillApiKey` and `reflectApiKey` are secrets — printing them would leak
|
|
106
|
+
* into terminal scrollback, CI logs, and screen shares. The operator reads the
|
|
107
|
+
* values out of the backup file themselves.
|
|
108
|
+
*
|
|
109
|
+
* Exported for testability.
|
|
110
|
+
*/
|
|
111
|
+
export declare function quarantineMalformedConfig(configPath: string): {
|
|
112
|
+
quarantined: false;
|
|
113
|
+
} | {
|
|
114
|
+
quarantined: true;
|
|
115
|
+
backupPath: string;
|
|
116
|
+
keys: string[];
|
|
117
|
+
};
|
|
40
118
|
/**
|
|
41
119
|
* Generate a random auth token in the format hctx-<32 hex chars>.
|
|
42
120
|
* Exported for testability.
|
|
@@ -52,6 +130,56 @@ export declare function persistAuthToken(configPath: string): {
|
|
|
52
130
|
token: string;
|
|
53
131
|
generated: boolean;
|
|
54
132
|
};
|
|
133
|
+
/**
|
|
134
|
+
* Ensure a stable per-install `agentId` UUID is set on a config object.
|
|
135
|
+
*
|
|
136
|
+
* The id is the client's attribution identity (stored on each captured
|
|
137
|
+
* memory as `source_agent_id`) — it survives agent/machine renames, unlike
|
|
138
|
+
* the readable `source_agent` name. Generated once, never rotated (idempotent:
|
|
139
|
+
* an existing valid `agentId` is always kept). Pure: mutates `config` in place
|
|
140
|
+
* and does NO file IO — BOTH server and client init call this on their
|
|
141
|
+
* in-memory config object before saving (the server path loads/saves
|
|
142
|
+
* config.json around it; the client builds in-memory and saves once).
|
|
143
|
+
* Exported for testability.
|
|
144
|
+
*/
|
|
145
|
+
export declare function ensureAgentId(config: Record<string, unknown>): {
|
|
146
|
+
agentId: string;
|
|
147
|
+
generated: boolean;
|
|
148
|
+
};
|
|
149
|
+
/**
|
|
150
|
+
* Load → ensure → persist wrapper for the `agentId` provenance field. This is
|
|
151
|
+
* the runtime activation path: `ensureAgentId` was historically called ONLY
|
|
152
|
+
* inside `init`, so pre-0.16.2 installs that already ran init never get an
|
|
153
|
+
* `agentId` written → nightly + server boot capture sent `source_agent_id:
|
|
154
|
+
* null` forever (the feature was inert for the entire existing fleet). Both
|
|
155
|
+
* nightly and server boot call THIS on startup so the field self-heals on the
|
|
156
|
+
* first run after upgrade — one read, one conditional write, idempotent.
|
|
157
|
+
*
|
|
158
|
+
* Built on the pure `ensureAgentId` (which init's client path and the unit
|
|
159
|
+
* test still call directly); this wrapper adds the disk IO.
|
|
160
|
+
*
|
|
161
|
+
* Hardening (0.16.x CR BLOCKER): the naive "try { read } catch { seed {} }"
|
|
162
|
+
* + unconditional save WIPES config.json when the file exists but is
|
|
163
|
+
* unparseable (a hand-edit syntax slip) — the catch swallows the parse error,
|
|
164
|
+
* {} is seeded, and the save overwrites the file with just {"agentId": ...},
|
|
165
|
+
* destroying authToken / licenseKey / domains / weakPrimaryFloor, then
|
|
166
|
+
* cascades into scaffoldDefaultDomains re-seeding the generic vocabulary.
|
|
167
|
+
* This wrapper refuses that path:
|
|
168
|
+
* - ENOENT (file genuinely absent) → seed {} is correct (new install).
|
|
169
|
+
* - Any OTHER read/parse failure on a file that EXISTS (corruption,
|
|
170
|
+
* truncation, bad JSON) → THROW. Swallowing would overwrite the file; the
|
|
171
|
+
* operator must fix the JSON instead of silently losing it.
|
|
172
|
+
* Save happens ONLY when a new id was generated AND the file already existed
|
|
173
|
+
* — a missing config.json means init was never run (a separate problem), so we
|
|
174
|
+
* do not create a stub file just to hold an agentId. The returned id is still
|
|
175
|
+
* usable in-memory for the run either way.
|
|
176
|
+
*
|
|
177
|
+
* Exported for use by init's server path, nightly.ts, and mcp-server.ts boot.
|
|
178
|
+
*/
|
|
179
|
+
export declare function ensureAndPersistAgentId(configPath: string): {
|
|
180
|
+
agentId: string;
|
|
181
|
+
generated: boolean;
|
|
182
|
+
};
|
|
55
183
|
/**
|
|
56
184
|
* Decide the per-agent context id to persist at init (#179; CC default = global,
|
|
57
185
|
* owner decision 20.07.2026). `agentName` is an explicit opt-in only — there is
|
|
@@ -75,6 +203,42 @@ export declare function decideAgentName(existing: unknown, flag: string | undefi
|
|
|
75
203
|
clear?: boolean;
|
|
76
204
|
error?: string;
|
|
77
205
|
};
|
|
206
|
+
/** Read config.json, set agentName, write it back. Used by the server path.
|
|
207
|
+
* Routes through loadConfigStrict — a malformed existing config throws rather
|
|
208
|
+
* than being wiped to a `{agentName: …}` stub (0.16.x BLOCKER; same pattern as
|
|
209
|
+
* the other four writers). Exported for wipe-protection coverage. */
|
|
210
|
+
export declare function writeAgentNameConfig(configPath: string, value: string): void;
|
|
211
|
+
/**
|
|
212
|
+
* Client-init config write: strict-load → apply the client overrides → save.
|
|
213
|
+
* This is the testable seam for the client path's config build (the rest of
|
|
214
|
+
* runClientInit is interactive / mutates ~/.claude / installs the daemon, so it
|
|
215
|
+
* is not unit-testable; this helper is).
|
|
216
|
+
*
|
|
217
|
+
* Strict load closes the 0.16.x BLOCKER for the client path: the old bare
|
|
218
|
+
* `try { parse } catch { warn }` seeded `{}` on a malformed existing config,
|
|
219
|
+
* then proceeded to set mode/serverUrl/authToken/agentId and `saveConfig` —
|
|
220
|
+
* OVERWRITING the file and losing the client's existing `authToken` (→ 401 on
|
|
221
|
+
* the next /search) and `licenseKey`, the same class as the server-side wipe.
|
|
222
|
+
* Now a malformed existing config THROWS (the user is interactive at `init`;
|
|
223
|
+
* they can fix the JSON and re-run, same as the server path). ENOENT → `{}`
|
|
224
|
+
* → a genuinely new client config is built fresh and saved.
|
|
225
|
+
*
|
|
226
|
+
* `agentNameDecision` is resolved by the caller via decideAgentName (which owns
|
|
227
|
+
* the process.exit on an invalid --agent-name flag). A no-flag run passes a
|
|
228
|
+
* {write:false} decision → this helper does not touch `agentName`, preserving
|
|
229
|
+
* whatever the loaded config already carries. Exported for wipe-protection
|
|
230
|
+
* coverage.
|
|
231
|
+
*/
|
|
232
|
+
export declare function writeClientConfig(configPath: string, overrides: {
|
|
233
|
+
serverUrl: string;
|
|
234
|
+
authToken?: string;
|
|
235
|
+
}, agentNameDecision?: {
|
|
236
|
+
write: boolean;
|
|
237
|
+
value: string | null;
|
|
238
|
+
clear?: boolean;
|
|
239
|
+
}): {
|
|
240
|
+
config: Record<string, unknown>;
|
|
241
|
+
};
|
|
78
242
|
/**
|
|
79
243
|
* Generic default memory domains scaffolded by server-mode init (issue #150).
|
|
80
244
|
* Deliberately broad, high-level spheres — an editable STARTING POINT, not a
|
|
@@ -129,6 +293,7 @@ export declare function installRecallHooks(settingsPath?: string): void;
|
|
|
129
293
|
export declare function runInit(options?: {
|
|
130
294
|
serverUrl?: string;
|
|
131
295
|
agentName?: string;
|
|
296
|
+
repairConfig?: boolean;
|
|
132
297
|
}): Promise<void>;
|
|
133
298
|
/**
|
|
134
299
|
* Resolve the nightly hour (0–23, local time) for the generated schedule.
|