@gamaze/hicortex 0.22.3 → 0.23.0
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 +4 -2
- package/dist/calibration.d.ts +26 -11
- package/dist/calibration.js +33 -13
- package/dist/capture.js +4 -1
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +26 -0
- package/dist/cluster.d.ts +8 -5
- package/dist/cluster.js +3 -7
- package/dist/consolidate.d.ts +4 -64
- package/dist/consolidate.js +15 -285
- package/dist/db.js +21 -0
- package/dist/dedup.d.ts +14 -9
- package/dist/dedup.js +33 -25
- package/dist/distiller.d.ts +18 -0
- package/dist/distiller.js +94 -9
- package/dist/eval/planted-harness.js +1 -1
- package/dist/eval/run-eval.js +2 -4
- package/dist/nightly.js +2 -4
- package/dist/recall-hook-cli.js +10 -0
- package/dist/recall-index.js +12 -0
- package/dist/reconsolidation.d.ts +17 -11
- package/dist/reconsolidation.js +38 -28
- package/dist/relink.d.ts +2 -2
- package/dist/relink.js +2 -2
- package/dist/retrieval.d.ts +5 -4
- package/dist/retrieval.js +5 -4
- package/dist/state.d.ts +8 -8
- package/dist/storage.d.ts +2 -2
- package/dist/storage.js +2 -2
- package/dist/sweep-volatile.d.ts +55 -0
- package/dist/sweep-volatile.js +184 -0
- package/dist/types.d.ts +26 -28
- package/dist/wrapper-prompt.d.ts +43 -0
- package/dist/wrapper-prompt.js +122 -0
- package/package.json +1 -1
- package/server.json +2 -2
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `hicortex sweep-volatile` (#489, deliverable iii — owner decision 3).
|
|
3
|
+
*
|
|
4
|
+
* The ONE-SHOT store sweep: the live corpus already holds the GH-status /
|
|
5
|
+
* version-bump rows gold set A adjudicated ("not healthy to have GH in mem").
|
|
6
|
+
* This command reuses the SAME deterministic gate the distill path applies
|
|
7
|
+
* (distiller.ts isVolatileStatusEntry — one gate, one meaning) over the stored
|
|
8
|
+
* corpus. Mirrors `dedup`:
|
|
9
|
+
*
|
|
10
|
+
* - DRY RUN by default: report candidates only, zero writes.
|
|
11
|
+
* - `--apply` is explicit and ordered capture-lock (fail fast) →
|
|
12
|
+
* pre-sweep backup (abort ALL writes if it fails) → one transaction.
|
|
13
|
+
* - NO hard deletes: swept rows are DEMOTED via storage.absorbMemory —
|
|
14
|
+
* status 'absorbed' (recall-invisible everywhere: vector + FTS rows
|
|
15
|
+
* dropped, and every read path already handles the state), plain row +
|
|
16
|
+
* links retained as evidence. Recovery posture = the dedup posture: the
|
|
17
|
+
* pre-sweep backup IS the rollback. The shared absorb primitive means no
|
|
18
|
+
* new status vocabulary to thread through candidate paths.
|
|
19
|
+
* - Tags cleared + domain NULL'd before absorb (dedup's rule: an absorbed
|
|
20
|
+
* row must not count in moduleIndex/tag recomputes).
|
|
21
|
+
* - Audit trail: one `volatile_sweep_log` row per swept memory (migration
|
|
22
|
+
* v23) — inspectable forever, never silent. No runtime consumer; the
|
|
23
|
+
* CAPTURE-side volatility gate is the re-ingest safety net.
|
|
24
|
+
*
|
|
25
|
+
* Server-mode only (needs the local DB), like dedup/relink/classify-domains.
|
|
26
|
+
*/
|
|
27
|
+
import { acquireCaptureLock } from "./capture.js";
|
|
28
|
+
export interface SweepVolatileOptions {
|
|
29
|
+
/** Execute the sweep. Default false = dry run (report only, zero writes). */
|
|
30
|
+
apply?: boolean;
|
|
31
|
+
/** DB path override (tests / manual snapshot verification). */
|
|
32
|
+
dbPath?: string;
|
|
33
|
+
/** State dir override (tests). Defaults to ~/.hicortex. Backup lands under
|
|
34
|
+
* here/backups/. */
|
|
35
|
+
stateDir?: string;
|
|
36
|
+
/** Config override (tests). Defaults to reading stateDir/config.json. */
|
|
37
|
+
config?: Record<string, unknown> | null;
|
|
38
|
+
/** Capture-lock acquirer override (tests). Defaults to capture.ts's lock. */
|
|
39
|
+
acquireLock?: typeof acquireCaptureLock;
|
|
40
|
+
}
|
|
41
|
+
export interface SweepVolatileReport {
|
|
42
|
+
dryRun: boolean;
|
|
43
|
+
/** Live rows the gate flags, in store order. Empty when the kill-switch is
|
|
44
|
+
* off (the sweep is inert without the gate — same release-managed switch,
|
|
45
|
+
* one meaning). */
|
|
46
|
+
candidates: Array<{
|
|
47
|
+
id: string;
|
|
48
|
+
preview: string;
|
|
49
|
+
}>;
|
|
50
|
+
/** --apply only: rows actually swept (absorbed). */
|
|
51
|
+
swept?: number;
|
|
52
|
+
/** --apply only: path to the pre-sweep backup. */
|
|
53
|
+
backupPath?: string;
|
|
54
|
+
}
|
|
55
|
+
export declare function runSweepVolatile(options?: SweepVolatileOptions): Promise<SweepVolatileReport>;
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* `hicortex sweep-volatile` (#489, deliverable iii — owner decision 3).
|
|
4
|
+
*
|
|
5
|
+
* The ONE-SHOT store sweep: the live corpus already holds the GH-status /
|
|
6
|
+
* version-bump rows gold set A adjudicated ("not healthy to have GH in mem").
|
|
7
|
+
* This command reuses the SAME deterministic gate the distill path applies
|
|
8
|
+
* (distiller.ts isVolatileStatusEntry — one gate, one meaning) over the stored
|
|
9
|
+
* corpus. Mirrors `dedup`:
|
|
10
|
+
*
|
|
11
|
+
* - DRY RUN by default: report candidates only, zero writes.
|
|
12
|
+
* - `--apply` is explicit and ordered capture-lock (fail fast) →
|
|
13
|
+
* pre-sweep backup (abort ALL writes if it fails) → one transaction.
|
|
14
|
+
* - NO hard deletes: swept rows are DEMOTED via storage.absorbMemory —
|
|
15
|
+
* status 'absorbed' (recall-invisible everywhere: vector + FTS rows
|
|
16
|
+
* dropped, and every read path already handles the state), plain row +
|
|
17
|
+
* links retained as evidence. Recovery posture = the dedup posture: the
|
|
18
|
+
* pre-sweep backup IS the rollback. The shared absorb primitive means no
|
|
19
|
+
* new status vocabulary to thread through candidate paths.
|
|
20
|
+
* - Tags cleared + domain NULL'd before absorb (dedup's rule: an absorbed
|
|
21
|
+
* row must not count in moduleIndex/tag recomputes).
|
|
22
|
+
* - Audit trail: one `volatile_sweep_log` row per swept memory (migration
|
|
23
|
+
* v23) — inspectable forever, never silent. No runtime consumer; the
|
|
24
|
+
* CAPTURE-side volatility gate is the re-ingest safety net.
|
|
25
|
+
*
|
|
26
|
+
* Server-mode only (needs the local DB), like dedup/relink/classify-domains.
|
|
27
|
+
*/
|
|
28
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
29
|
+
if (k2 === undefined) k2 = k;
|
|
30
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
31
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
32
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
33
|
+
}
|
|
34
|
+
Object.defineProperty(o, k2, desc);
|
|
35
|
+
}) : (function(o, m, k, k2) {
|
|
36
|
+
if (k2 === undefined) k2 = k;
|
|
37
|
+
o[k2] = m[k];
|
|
38
|
+
}));
|
|
39
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
40
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
41
|
+
}) : function(o, v) {
|
|
42
|
+
o["default"] = v;
|
|
43
|
+
});
|
|
44
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
45
|
+
var ownKeys = function(o) {
|
|
46
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
47
|
+
var ar = [];
|
|
48
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
49
|
+
return ar;
|
|
50
|
+
};
|
|
51
|
+
return ownKeys(o);
|
|
52
|
+
};
|
|
53
|
+
return function (mod) {
|
|
54
|
+
if (mod && mod.__esModule) return mod;
|
|
55
|
+
var result = {};
|
|
56
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
57
|
+
__setModuleDefault(result, mod);
|
|
58
|
+
return result;
|
|
59
|
+
};
|
|
60
|
+
})();
|
|
61
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
62
|
+
exports.runSweepVolatile = runSweepVolatile;
|
|
63
|
+
const paths_js_1 = require("./paths.js");
|
|
64
|
+
const node_fs_1 = require("node:fs");
|
|
65
|
+
const node_path_1 = require("node:path");
|
|
66
|
+
const db_js_1 = require("./db.js");
|
|
67
|
+
const storage = __importStar(require("./storage.js"));
|
|
68
|
+
const distiller_js_1 = require("./distiller.js");
|
|
69
|
+
const capture_js_1 = require("./capture.js");
|
|
70
|
+
const config_read_js_1 = require("./config-read.js");
|
|
71
|
+
const calibration_js_1 = require("./calibration.js");
|
|
72
|
+
const backup_js_1 = require("./backup.js");
|
|
73
|
+
const HICORTEX_HOME = (0, paths_js_1.hicortexHome)();
|
|
74
|
+
/** Pre-sweep backup filename pattern — scoped retention (like pre-dedup). */
|
|
75
|
+
const PRE_SWEEP_BACKUP_PATTERN = /^pre-sweep-volatile-.*\.db$/;
|
|
76
|
+
function readConfig(stateDir) {
|
|
77
|
+
try {
|
|
78
|
+
return JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.join)(stateDir, "config.json"), "utf-8"));
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/** Live-row filter: active or content-rewritten rows only. Already-retired
|
|
85
|
+
* states are not candidates — 'absorbed' is invisible already; 'superseded'
|
|
86
|
+
* / 'retracted' are demoted already. */
|
|
87
|
+
const LIVE_ROW_WHERE = `COALESCE(status, '') NOT IN ('absorbed', 'superseded', 'retracted')`;
|
|
88
|
+
/**
|
|
89
|
+
* Pre-sweep DB backup to <stateDir>/backups/pre-sweep-volatile-<ISO>.db,
|
|
90
|
+
* pruned to `backupRetention` newest (pattern-scoped, like pre-dedup).
|
|
91
|
+
* THROWS on failure — runSweepVolatile aborts the whole sweep when it does.
|
|
92
|
+
*/
|
|
93
|
+
async function takePreSweepBackup(db, stateDir, config) {
|
|
94
|
+
const backupDir = (0, node_path_1.join)(stateDir, "backups");
|
|
95
|
+
(0, node_fs_1.mkdirSync)(backupDir, { recursive: true });
|
|
96
|
+
const backupPath = (0, node_path_1.join)(backupDir, `pre-sweep-volatile-${new Date().toISOString().replace(/[:.]/g, "-")}.db`);
|
|
97
|
+
await db.backup(backupPath);
|
|
98
|
+
const retention = (0, config_read_js_1.readNonNegativeConfig)(config ?? {}, "backupRetention", backup_js_1.DEFAULT_BACKUP_RETENTION);
|
|
99
|
+
(0, backup_js_1.pruneBackupArtifacts)(backupDir, retention, PRE_SWEEP_BACKUP_PATTERN);
|
|
100
|
+
return backupPath;
|
|
101
|
+
}
|
|
102
|
+
async function runSweepVolatile(options = {}) {
|
|
103
|
+
const stateDir = options.stateDir ?? HICORTEX_HOME;
|
|
104
|
+
const config = options.config !== undefined ? options.config : readConfig(stateDir);
|
|
105
|
+
// Server-mode only — client installs have no local DB.
|
|
106
|
+
if (config?.mode === "client") {
|
|
107
|
+
throw new Error("[hicortex] sweep-volatile is server-mode only (it needs the local DB). " +
|
|
108
|
+
`This machine is a client of ${config.serverUrl ?? "a remote server"} — run sweep-volatile on the server.`);
|
|
109
|
+
}
|
|
110
|
+
const apply = options.apply ?? false;
|
|
111
|
+
const dbPath = (0, db_js_1.resolveDbPath)(options.dbPath);
|
|
112
|
+
const db = (0, db_js_1.initDb)(dbPath);
|
|
113
|
+
try {
|
|
114
|
+
console.log(`[hicortex] sweep-volatile starting (${apply ? "APPLY" : "dry-run"}): db ${dbPath}`);
|
|
115
|
+
const rows = db
|
|
116
|
+
.prepare(`SELECT id, content FROM memories WHERE ${LIVE_ROW_WHERE}`)
|
|
117
|
+
.all();
|
|
118
|
+
const candidates = calibration_js_1.VOLATILE_STATUS_FILTER
|
|
119
|
+
? rows
|
|
120
|
+
.filter((r) => (0, distiller_js_1.isVolatileStatusEntry)(r.content))
|
|
121
|
+
.map((r) => ({ id: r.id, preview: r.content.slice(0, 120) }))
|
|
122
|
+
: [];
|
|
123
|
+
if (!calibration_js_1.VOLATILE_STATUS_FILTER) {
|
|
124
|
+
console.warn("[hicortex] sweep-volatile: the volatility gate is switched off in this release (VOLATILE_STATUS_FILTER) — nothing to sweep.");
|
|
125
|
+
}
|
|
126
|
+
const report = { dryRun: !apply, candidates };
|
|
127
|
+
console.log(`[hicortex] sweep-volatile: ${candidates.length} volatile row(s) among ${rows.length} live (` +
|
|
128
|
+
`${apply ? "demoting via absorb" : "dry run — zero writes"})`);
|
|
129
|
+
if (!apply) {
|
|
130
|
+
for (const c of candidates) {
|
|
131
|
+
console.log(`[hicortex] ${c.id.slice(0, 8)}: "${c.preview}"`);
|
|
132
|
+
}
|
|
133
|
+
if (candidates.length === 0)
|
|
134
|
+
console.log("[hicortex] (nothing matched the gate)");
|
|
135
|
+
return report;
|
|
136
|
+
}
|
|
137
|
+
if (candidates.length === 0)
|
|
138
|
+
return report;
|
|
139
|
+
// --apply: fail fast on a busy capture lock — the audit rows and absorb
|
|
140
|
+
// writes must not race a nightly/capture run (dedup posture).
|
|
141
|
+
const acquireLock = options.acquireLock ?? capture_js_1.acquireCaptureLock;
|
|
142
|
+
const releaseLock = await acquireLock(stateDir, 0);
|
|
143
|
+
if (!releaseLock) {
|
|
144
|
+
throw new Error("[hicortex] sweep-volatile --apply aborted: another capture/nightly run holds the lock. Retry when it finishes.");
|
|
145
|
+
}
|
|
146
|
+
try {
|
|
147
|
+
// Backup FIRST — abort entirely (no writes attempted) if it fails.
|
|
148
|
+
let backupPath;
|
|
149
|
+
try {
|
|
150
|
+
backupPath = await takePreSweepBackup(db, stateDir, config);
|
|
151
|
+
}
|
|
152
|
+
catch (err) {
|
|
153
|
+
throw new Error(`[hicortex] sweep-volatile --apply aborted: backup failed (${err instanceof Error ? err.message : String(err)}). No rows swept.`);
|
|
154
|
+
}
|
|
155
|
+
console.log(`[hicortex] Backup written: ${backupPath}`);
|
|
156
|
+
report.backupPath = backupPath;
|
|
157
|
+
// ONE transaction: audit row + tag/domain clear + absorb per row. Any
|
|
158
|
+
// failure rolls back the whole sweep (all-or-nothing, like /distill's
|
|
159
|
+
// insert phase) — a partial sweep is never left behind.
|
|
160
|
+
const sweep = db.transaction(() => {
|
|
161
|
+
const clearTags = db.prepare("DELETE FROM memory_tags WHERE memory_id = ?");
|
|
162
|
+
const audit = db.prepare("INSERT OR REPLACE INTO volatile_sweep_log (memory_id, swept_at, preview) VALUES (?, ?, ?)");
|
|
163
|
+
const now = new Date().toISOString();
|
|
164
|
+
for (const c of candidates) {
|
|
165
|
+
audit.run(c.id, now, c.preview);
|
|
166
|
+
clearTags.run(c.id);
|
|
167
|
+
storage.updateMemory(db, c.id, { domain: null });
|
|
168
|
+
storage.absorbMemory(db, c.id);
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
sweep();
|
|
172
|
+
report.swept = candidates.length;
|
|
173
|
+
console.log(`[hicortex] sweep-volatile: swept (absorbed) ${report.swept} row(s); ` +
|
|
174
|
+
`rollback = restore ${backupPath}`);
|
|
175
|
+
return report;
|
|
176
|
+
}
|
|
177
|
+
finally {
|
|
178
|
+
releaseLock();
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
finally {
|
|
182
|
+
db.close();
|
|
183
|
+
}
|
|
184
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -37,9 +37,13 @@ export interface Memory {
|
|
|
37
37
|
* never config: NULL/absent = active (the default, and every pre-v14 row);
|
|
38
38
|
* 'superseded'/'retracted' = marked stale or wrong (demoted in ranking);
|
|
39
39
|
* 'corrected' = rewritten in place (does NOT demote — demoting it would
|
|
40
|
-
* bury the correction); 'absorbed' = invisible to recall (
|
|
41
|
-
*
|
|
42
|
-
*
|
|
40
|
+
* bury the correction); 'absorbed' = invisible to recall (no vector/FTS
|
|
41
|
+
* row, plain row + links kept as evidence and rollback reference). Writers
|
|
42
|
+
* of 'absorbed': the reconsolidation rewrite path (trigger folded into a
|
|
43
|
+
* corrected target), dedup merge losers (#392), and the one-shot
|
|
44
|
+
* `sweep-volatile` demotion (#489 — volatile rows retire through the SAME
|
|
45
|
+
* primitive, so no new status vocabulary exists to thread through read
|
|
46
|
+
* paths; recovery is the pre-sweep backup).
|
|
43
47
|
*/
|
|
44
48
|
status?: string | null;
|
|
45
49
|
/**
|
|
@@ -128,8 +132,13 @@ export interface ResolutionBandStat {
|
|
|
128
132
|
merge_below_gate: number;
|
|
129
133
|
/** Sum of verdict confidences (divide by `pairs` for the mean). Deterministic merges count 1.0 each. */
|
|
130
134
|
conf_sum: number;
|
|
131
|
-
/**
|
|
132
|
-
|
|
135
|
+
/**
|
|
136
|
+
* Deterministic band only: clusters refused by the project rail. #206
|
|
137
|
+
* decision 2 renamed this from `metadata_skipped` when the source_agent
|
|
138
|
+
* rail was removed — pre-rename cumulative values stay on disk unread
|
|
139
|
+
* (their semantics conflated both rails).
|
|
140
|
+
*/
|
|
141
|
+
project_skipped?: number;
|
|
133
142
|
}
|
|
134
143
|
/**
|
|
135
144
|
* Report of the deterministic merge zone (#392) — the band at/above the merge
|
|
@@ -151,8 +160,8 @@ export interface DeterministicMergeZoneReport {
|
|
|
151
160
|
losers_merged: number;
|
|
152
161
|
/** Loser links re-pointed onto canonicals this run. */
|
|
153
162
|
links_repointed: number;
|
|
154
|
-
/** Clusters skipped — members disagree on project
|
|
155
|
-
|
|
163
|
+
/** Clusters skipped — members disagree on project (#206 decision 2: the source_agent rail is removed). */
|
|
164
|
+
skipped_project_mismatch: number;
|
|
156
165
|
/**
|
|
157
166
|
* #393 guard-C: clusters skipped because a member pair holds a `conflicts`
|
|
158
167
|
* link — a judge-flagged genuine conflict is never blended, both records
|
|
@@ -251,27 +260,15 @@ export interface ConsolidationReport {
|
|
|
251
260
|
heuristic_fallback?: number;
|
|
252
261
|
failed: number;
|
|
253
262
|
};
|
|
254
|
-
/** Supersession detection (#191 Phase B) — runs after linking, before decay/prune. */
|
|
255
|
-
supersession?: {
|
|
256
|
-
/** Decision/correction-shaped candidates examined this run. */
|
|
257
|
-
scanned: number;
|
|
258
|
-
/** Older-neighbor pairs actually sent to the classify-tier LLM. */
|
|
259
|
-
evaluated: number;
|
|
260
|
-
/** Pairs the LLM judged superseded — a `superseded_by` link was created. */
|
|
261
|
-
superseded: number;
|
|
262
|
-
/** Pairs skipped on a parse/infra error (retried naturally next night). */
|
|
263
|
-
skipped_infra: number;
|
|
264
|
-
/** Pairs skipped because a superseded_by link already existed (either direction). */
|
|
265
|
-
skipped_idempotent: number;
|
|
266
|
-
/** supersessionCursor after this run (unchanged in dry-run). */
|
|
267
|
-
cursor: number;
|
|
268
|
-
};
|
|
269
263
|
/**
|
|
270
|
-
* Reconsolidation (#384) — runs after
|
|
264
|
+
* Reconsolidation (#384) — runs after linking, before decay/prune.
|
|
271
265
|
* Since #392 this is THE unified resolution stage: its verdict also carries
|
|
272
266
|
* a `merge` disposition, and the deterministic merge zone (pairs at/above
|
|
273
267
|
* the merge ceiling — release-managed since #408) runs inside it,
|
|
274
|
-
* LLM-free, before the scan.
|
|
268
|
+
* LLM-free, before the scan. Since #206-B (owner decision 6) it is also
|
|
269
|
+
* the ONLY true-update detector — the standalone supersession stage
|
|
270
|
+
* (3.7, #191 Phase B) is retired into its `supersedes` verdict action,
|
|
271
|
+
* and its former `supersession` report slot no longer exists.
|
|
275
272
|
*/
|
|
276
273
|
reconsolidation?: {
|
|
277
274
|
/** Candidates examined this run (rowid > cursor; no shape filter). */
|
|
@@ -358,11 +355,12 @@ export interface ConsolidationReport {
|
|
|
358
355
|
*/
|
|
359
356
|
skipped_above_ceiling: number;
|
|
360
357
|
/**
|
|
361
|
-
* #392: judged merge pairs refused by the
|
|
362
|
-
*
|
|
363
|
-
* the verdict was rendered, this
|
|
358
|
+
* #392: judged merge pairs refused by the project rail (project
|
|
359
|
+
* disagreement — the only metadata rail, #206 decision 2). Both
|
|
360
|
+
* memories kept; the cursor advances — the verdict was rendered, this
|
|
361
|
+
* is not an infra failure.
|
|
364
362
|
*/
|
|
365
|
-
|
|
363
|
+
skipped_project_mismatch: number;
|
|
366
364
|
/**
|
|
367
365
|
* #393 guard-C: verdicts that flagged a genuine conflict — a `conflicts`
|
|
368
366
|
* link was written, both memories stay live (no status change, no
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shared wrapper-prompt classifier (#489).
|
|
3
|
+
*
|
|
4
|
+
* ONE function imported by BOTH recall layers — the CC hook client
|
|
5
|
+
* (recall-hook-cli.ts buildHookRequest) and the server handler
|
|
6
|
+
* (recall-index.ts handleRecallIndex) — so the two can never drift on what
|
|
7
|
+
* counts as harness plumbing.
|
|
8
|
+
*
|
|
9
|
+
* What CC delivers as user-role wrapper messages (wild-verified over 250
|
|
10
|
+
* sampled session files + the latest live recall_pushes rows, #489
|
|
11
|
+
* refinement): `<task-notification>`, `<agent-message>`, local-command-caveat`,
|
|
12
|
+
* `<command-name>`/`<command-message>`/`<command-args>` (slash-command
|
|
13
|
+
* envelope), `<local-command-stdout>`, `<system-reminder>`, and
|
|
14
|
+
* `[Request interrupted…]` markers. These reach /recall-index verbatim today —
|
|
15
|
+
* ~15% of live pushes — inflating shown_count, burning hook latency, and
|
|
16
|
+
* polluting the Memory Precision denominator.
|
|
17
|
+
*
|
|
18
|
+
* The rule (owner decision 4, refinement A1 — skip ONLY pure plumbing):
|
|
19
|
+
* strip every known wrapper element, then measure PAYLOAD PROSE = the text
|
|
20
|
+
* left outside wrappers PLUS the bodies of `<result>` elements and
|
|
21
|
+
* `<agent-message>` elements (the payload channels — a subagent hand-back is
|
|
22
|
+
* real prose even though it arrives wrapped; the gold-set exception recalled
|
|
23
|
+
* usefully). Pure wrapper ⟺ payload prose is shorter than
|
|
24
|
+
* RECALL_MIN_PROMPT_CHARS — the SAME bar as the short-prompt gate: one
|
|
25
|
+
* constant, one meaning, "enough words to recall on". `<summary>` ("Agent X
|
|
26
|
+
* finished") and `<note>` are plumbing, never prose — otherwise every
|
|
27
|
+
* notification would pass and the skip would never fire.
|
|
28
|
+
*
|
|
29
|
+
* Conservative by construction:
|
|
30
|
+
* - NO wrapper marker at all → not a pure wrapper, whatever the length (a
|
|
31
|
+
* short plain prompt is the short-prompt gate's business, not this one).
|
|
32
|
+
* - A malformed or truncated wrapper does not match its strip regex, so its
|
|
33
|
+
* text counts as payload prose → not a pure wrapper → recalls.
|
|
34
|
+
* - An empty string is NOT classified here — the caller's empty check owns
|
|
35
|
+
* that case.
|
|
36
|
+
*/
|
|
37
|
+
/**
|
|
38
|
+
* True when the prompt is PURE plumbing: after removing known wrapper
|
|
39
|
+
* elements, the payload prose (leftover text + result/agent-message bodies)
|
|
40
|
+
* is shorter than RECALL_MIN_PROMPT_CHARS. False for anything prose-bearing,
|
|
41
|
+
* malformed, wrapper-free, or empty — those recall exactly as today.
|
|
42
|
+
*/
|
|
43
|
+
export declare function isPureWrapperPrompt(prompt: string): boolean;
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* The shared wrapper-prompt classifier (#489).
|
|
4
|
+
*
|
|
5
|
+
* ONE function imported by BOTH recall layers — the CC hook client
|
|
6
|
+
* (recall-hook-cli.ts buildHookRequest) and the server handler
|
|
7
|
+
* (recall-index.ts handleRecallIndex) — so the two can never drift on what
|
|
8
|
+
* counts as harness plumbing.
|
|
9
|
+
*
|
|
10
|
+
* What CC delivers as user-role wrapper messages (wild-verified over 250
|
|
11
|
+
* sampled session files + the latest live recall_pushes rows, #489
|
|
12
|
+
* refinement): `<task-notification>`, `<agent-message>`, local-command-caveat`,
|
|
13
|
+
* `<command-name>`/`<command-message>`/`<command-args>` (slash-command
|
|
14
|
+
* envelope), `<local-command-stdout>`, `<system-reminder>`, and
|
|
15
|
+
* `[Request interrupted…]` markers. These reach /recall-index verbatim today —
|
|
16
|
+
* ~15% of live pushes — inflating shown_count, burning hook latency, and
|
|
17
|
+
* polluting the Memory Precision denominator.
|
|
18
|
+
*
|
|
19
|
+
* The rule (owner decision 4, refinement A1 — skip ONLY pure plumbing):
|
|
20
|
+
* strip every known wrapper element, then measure PAYLOAD PROSE = the text
|
|
21
|
+
* left outside wrappers PLUS the bodies of `<result>` elements and
|
|
22
|
+
* `<agent-message>` elements (the payload channels — a subagent hand-back is
|
|
23
|
+
* real prose even though it arrives wrapped; the gold-set exception recalled
|
|
24
|
+
* usefully). Pure wrapper ⟺ payload prose is shorter than
|
|
25
|
+
* RECALL_MIN_PROMPT_CHARS — the SAME bar as the short-prompt gate: one
|
|
26
|
+
* constant, one meaning, "enough words to recall on". `<summary>` ("Agent X
|
|
27
|
+
* finished") and `<note>` are plumbing, never prose — otherwise every
|
|
28
|
+
* notification would pass and the skip would never fire.
|
|
29
|
+
*
|
|
30
|
+
* Conservative by construction:
|
|
31
|
+
* - NO wrapper marker at all → not a pure wrapper, whatever the length (a
|
|
32
|
+
* short plain prompt is the short-prompt gate's business, not this one).
|
|
33
|
+
* - A malformed or truncated wrapper does not match its strip regex, so its
|
|
34
|
+
* text counts as payload prose → not a pure wrapper → recalls.
|
|
35
|
+
* - An empty string is NOT classified here — the caller's empty check owns
|
|
36
|
+
* that case.
|
|
37
|
+
*/
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.isPureWrapperPrompt = isPureWrapperPrompt;
|
|
40
|
+
const calibration_js_1 = require("./calibration.js");
|
|
41
|
+
/**
|
|
42
|
+
* Opening-tag detector — the precondition: only a prompt that actually
|
|
43
|
+
* CARRIES a known wrapper marker can be a pure wrapper. Bare and unclosed
|
|
44
|
+
* tags both match this; the strip pass below then decides how much survives
|
|
45
|
+
* as prose.
|
|
46
|
+
*/
|
|
47
|
+
const WRAPPER_MARKER = /<(?:task-notification|agent-message|local-command-caveat|command-name|command-message|command-args|local-command-stdout|system-reminder|result)\b/;
|
|
48
|
+
/** Interrupt-marker presence — carries the same precondition weight as a
|
|
49
|
+
* wrapper tag (a marker-only prompt has no tags to detect). */
|
|
50
|
+
const INTERRUPT_MARKER_ANY = /\[Request interrupted by user[^\]]*\]/;
|
|
51
|
+
/**
|
|
52
|
+
* Wrapper element signatures. Non-greedy, bounded (the cleanMessageContent
|
|
53
|
+
* regex posture — no unbounded backtracking on pathological input; bodies
|
|
54
|
+
* beyond the cap fail to match and count as prose, the conservative
|
|
55
|
+
* direction). `[\s\S]` so multi-line bodies strip correctly.
|
|
56
|
+
*
|
|
57
|
+
* Payload channels (`task-notification`'s `<result>`, `agent-message` bodies)
|
|
58
|
+
* are handled separately below — their content is EXTRACTED as prose, not
|
|
59
|
+
* stripped as plumbing.
|
|
60
|
+
*/
|
|
61
|
+
const WRAPPER_ELEMENTS = [
|
|
62
|
+
// task-notification: envelope stripped; an inner <result> body is harvested
|
|
63
|
+
// as prose by PAYLOAD_ELEMENT_BODIES (runs BEFORE this strip).
|
|
64
|
+
/<task-notification[^>]*>[\s\S]{0,50000}?<\/task-notification>/g,
|
|
65
|
+
/<local-command-caveat>[\s\S]{0,10000}?<\/local-command-caveat>/g,
|
|
66
|
+
/<command-name>[\s\S]{0,1000}?<\/command-name>/g,
|
|
67
|
+
/<command-message>[\s\S]{0,5000}?<\/command-message>/g,
|
|
68
|
+
/<command-args>[\s\S]{0,10000}?<\/command-args>/g,
|
|
69
|
+
/<local-command-stdout[^>]*>[\s\S]{0,100000}?<\/local-command-stdout>/g,
|
|
70
|
+
/<system-reminder[^>]*>[\s\S]{0,10000}?<\/system-reminder>/g,
|
|
71
|
+
// Payload-channel ENVELOPES strip here too — AFTER their bodies were
|
|
72
|
+
// harvested below (harvest runs first). The tags themselves are plumbing:
|
|
73
|
+
// an empty <agent-message></agent-message> must leave nothing behind, and
|
|
74
|
+
// a lone <result>…</result> must measure only its body.
|
|
75
|
+
/<agent-message[^>]*>[\s\S]{0,100000}?<\/agent-message>/g,
|
|
76
|
+
/<result[^>]*>[\s\S]{0,100000}?<\/result>/g,
|
|
77
|
+
];
|
|
78
|
+
/**
|
|
79
|
+
* Payload channels — bodies that COUNT AS PROSE even though they sit inside a
|
|
80
|
+
* wrapper: the `<result>` of a task-notification (a report) and the full body
|
|
81
|
+
* of an `<agent-message>` (inter-agent prose, e.g. a subagent hand-back).
|
|
82
|
+
* `<agent-message>` is NOT in WRAPPER_ELEMENTS for exactly this reason: its
|
|
83
|
+
* whole body is payload, so it is harvested here and never stripped.
|
|
84
|
+
*/
|
|
85
|
+
const PAYLOAD_ELEMENT_BODIES = [
|
|
86
|
+
/<result[^>]*>([\s\S]{0,100000}?)<\/result>/g,
|
|
87
|
+
/<agent-message[^>]*>([\s\S]{0,100000}?)<\/agent-message>/g,
|
|
88
|
+
];
|
|
89
|
+
/** Interrupt markers — whole-line plumbing (either flavor). */
|
|
90
|
+
const INTERRUPT_MARKER = /^\s*\[Request interrupted by user[^\]]*\]\s*$/gm;
|
|
91
|
+
/**
|
|
92
|
+
* True when the prompt is PURE plumbing: after removing known wrapper
|
|
93
|
+
* elements, the payload prose (leftover text + result/agent-message bodies)
|
|
94
|
+
* is shorter than RECALL_MIN_PROMPT_CHARS. False for anything prose-bearing,
|
|
95
|
+
* malformed, wrapper-free, or empty — those recall exactly as today.
|
|
96
|
+
*/
|
|
97
|
+
function isPureWrapperPrompt(prompt) {
|
|
98
|
+
if (!prompt)
|
|
99
|
+
return false;
|
|
100
|
+
if (!WRAPPER_MARKER.test(prompt) && !INTERRUPT_MARKER_ANY.test(prompt))
|
|
101
|
+
return false;
|
|
102
|
+
let payload = "";
|
|
103
|
+
// Harvest payload-channel bodies FIRST (agent-message is never stripped;
|
|
104
|
+
// task-notification's <result> is harvested before its envelope strips).
|
|
105
|
+
for (const re of PAYLOAD_ELEMENT_BODIES) {
|
|
106
|
+
for (const m of prompt.matchAll(re)) {
|
|
107
|
+
payload += " " + (m[1] ?? "");
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
// Strip the remaining wrapper envelopes + interrupt markers; what is left
|
|
111
|
+
// over is outside-wrapper prose.
|
|
112
|
+
let rest = prompt;
|
|
113
|
+
for (const re of WRAPPER_ELEMENTS) {
|
|
114
|
+
rest = rest.replace(re, " ");
|
|
115
|
+
}
|
|
116
|
+
rest = rest.replace(INTERRUPT_MARKER, " ");
|
|
117
|
+
payload += " " + rest;
|
|
118
|
+
// Whitespace-collapsed length is the prose bar — indentation and newlines
|
|
119
|
+
// are not words.
|
|
120
|
+
const prose = payload.replace(/\s+/g, " ").trim();
|
|
121
|
+
return prose.length < calibration_js_1.RECALL_MIN_PROMPT_CHARS;
|
|
122
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gamaze/hicortex",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.23.0",
|
|
4
4
|
"description": "Persistent agent identity for AI agents \u2014 a hand-edited identity layer, nightly-distilled experience, and lessons injected every session, shared across your whole fleet. Works with Hermes, OpenClaw, Claude Code, Pi, and opencode.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
package/server.json
CHANGED
|
@@ -3,12 +3,12 @@
|
|
|
3
3
|
"name": "io.github.gamaze-labs/hicortex",
|
|
4
4
|
"title": "Hicortex \u2014 AI Fleet Memory",
|
|
5
5
|
"description": "Shared fleet memory for AI agents: nightly self-correction, recall every prompt (supported agents).",
|
|
6
|
-
"version": "0.
|
|
6
|
+
"version": "0.23.0",
|
|
7
7
|
"packages": [
|
|
8
8
|
{
|
|
9
9
|
"registryType": "npm",
|
|
10
10
|
"identifier": "@gamaze/hicortex",
|
|
11
|
-
"version": "0.
|
|
11
|
+
"version": "0.23.0",
|
|
12
12
|
"transport": {
|
|
13
13
|
"type": "stdio",
|
|
14
14
|
"command": "npx",
|