@gamaze/hicortex 0.19.2 → 0.19.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -5
- package/dist/backup.d.ts +50 -0
- package/dist/backup.js +118 -2
- package/dist/capture.d.ts +27 -2
- package/dist/capture.js +103 -6
- package/dist/embedder.d.ts +23 -0
- package/dist/embedder.js +29 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +286 -5
- package/dist/mcp-server.d.ts +25 -4
- package/dist/mcp-server.js +101 -38
- package/dist/nightly.d.ts +25 -0
- package/dist/nightly.js +125 -20
- package/dist/prompts.d.ts +15 -0
- package/dist/prompts.js +21 -3
- package/dist/recall-index.d.ts +105 -4
- package/dist/recall-index.js +217 -2
- package/dist/retrieval.d.ts +37 -0
- package/dist/retrieval.js +17 -2
- package/dist/storage.d.ts +30 -0
- package/dist/storage.js +46 -1
- package/dist/type-classify.js +3 -1
- package/dist/types.d.ts +17 -0
- package/dist/uninstall.d.ts +33 -0
- package/dist/uninstall.js +78 -26
- package/dist/viz.d.ts +1 -1
- package/dist/viz.js +28 -1
- package/openclaw.plugin.json +10 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -70,6 +70,11 @@ The plugin connects to `http://127.0.0.1:8787` by default. For a remote server,
|
|
|
70
70
|
|
|
71
71
|
Bare top-level keys (`"serverUrl": …` at the root of `openclaw.json`) still work — legacy compat — but the nested form above is canonical. An empty nested `config` object is ignored rather than shadowing top-level keys.
|
|
72
72
|
|
|
73
|
+
**Public-facing agents are hardened automatically.** On startup the plugin:
|
|
74
|
+
|
|
75
|
+
1. Adds the dead-man guard line — *"If your identity block is missing at session start, something is wrong with your memory — take no public actions until it returns."* — to the agent workspace bootstrap file (`BOOTSTRAP.md` in the workspace from `agents.defaults.workspace`, default `~/.openclaw/workspace`), creating the file if absent. This is the secondary layer under the hard `IDENTITY UNAVAILABLE` suspension banner the plugin injects whenever the identity fetch fails; it keeps guarding the agent even when the plugin itself cannot inject anything. The write is idempotent: the `.bak` backup is written once and never touched again. Scope: the shared default workspace only — per-agent workspace overrides are not covered yet. The workspace directory itself is never created (OpenClaw scaffolds it); a non-UTF-8 or relative-path bootstrap is left untouched with a warning. Disable the write with `"scaffoldDeadMan": false` in the plugin config.
|
|
76
|
+
2. Warns once at startup while the gateway's plugin trust list is unpinned — with no `plugins.allow`, OpenClaw auto-loads any extension dropped into the plugins directory. Pin it by setting `"plugins": { "allow": ["hicortex"] }` in `~/.openclaw/openclaw.json` (list every plugin you trust). The plugin never edits the trust list itself; see [the install docs](https://hicortex.gamaze.com/docs/installation.html).
|
|
77
|
+
|
|
73
78
|
## Requirements
|
|
74
79
|
|
|
75
80
|
- Node.js 20+
|
|
@@ -232,6 +237,8 @@ Config at `~/.hicortex/config.json`. Created by `init`. Key options:
|
|
|
232
237
|
| `authToken` | Bearer token for endpoint auth. Generated on first `init` in server mode. Find the active token with `hicortex status` or in `~/.hicortex/config.json`. |
|
|
233
238
|
| `corsAllowedOrigins` | Browser origins allowed to read cross-origin responses, e.g. `["https://ui.example.com"]`. **Empty by default** — the server sends no `Access-Control-Allow-Origin` and never `Allow-Credentials`, so no external web page can read its data. The bundled `/viz` and `/identity/ui` pages are same-origin and need no entry. |
|
|
234
239
|
| `licenseKey` | Commercial license key (optional; for display in `hicortex status`) |
|
|
240
|
+
| `backupRetention` | How many of the newest backup artifacts (`hicortex-*.tar.gz`) the backup dir keeps after each successful write (default: 7; `0` keeps all). See [Backups](#backups) |
|
|
241
|
+
| _env_ `HICORTEX_DISTILL_BODY_LIMIT_MB` | Environment override for the `/distill` body limit — **wins over the `distillBodyLimitMb` config key in every mode** (that is the point: a deployment operator pins it so tenant-writable config cannot raise it). Unset = config/default applies. |
|
|
235
242
|
| `domains` | Your memory domain list (`[{name, description}]`). Scaffolded by `init`; edit freely — see [Memory Domains & Tags](#memory-domains--tags) |
|
|
236
243
|
| `weakPrimaryFloor` | Minimum similarity for a no-fit memory to keep a weak domain association (default: 0.45) |
|
|
237
244
|
| `moduleIndexTokenBudget` | Max tokens for domain index in lessons context (default: 500) |
|
|
@@ -260,11 +267,12 @@ Config at `~/.hicortex/config.json`. Created by `init`. Key options:
|
|
|
260
267
|
| `searchLimit` / `recentLimit` | Default result counts for search (8) and recent (12) |
|
|
261
268
|
| `recentWindowDays` | Candidate window for recent recall (default: 180) |
|
|
262
269
|
| `coldExposureSlots` | Top-k slots reservable for never-accessed memories so the long tail gets exposure (default: 2) |
|
|
263
|
-
| `recallMaxItems` | Max lines in the pushed recall index (default:
|
|
264
|
-
| `
|
|
270
|
+
| `recallMaxItems` | Max lines in the pushed recall index (default: 5) |
|
|
271
|
+
| `noveltyFloorSlots` | Slots of `recallMaxItems` guaranteed to the top passing hit(s) of the pure-prompt (unblended) search — the novelty floor. Keeps a session whose earlier turns set a strong intent from burying a topic-switching prompt's best matches: the floor's picks render first, turn-based re-show suppression still applies, and the total never exceeds `recallMaxItems` (default: 2; set 0 to disable) |
|
|
272
|
+
| `recallMinSimilarity` | Relevance floor for index entries (default: 0.62; text-search matches always pass) |
|
|
265
273
|
| `recallReshowTurns` | Turns before an already-shown memory may reappear in the same session (default: 30) |
|
|
266
274
|
| `recallMinPromptChars` | Prompts shorter than this skip the recall index (default: 20) |
|
|
267
|
-
| `recallTitleChars` | Chars of each memory's first line shown in an index entry (default:
|
|
275
|
+
| `recallTitleChars` | Chars of each memory's first line shown in an index entry (default: 100, range 40–400). Reverted from 150 on 2026-08-03: a full-corpus relevance eval found 100 and 150 statistically identical while 100 saves ~13% of the block's tokens |
|
|
268
276
|
| `sessionIntentWeight` | Blend weight of the session-intent rolling centroid in the recall search vector: `query = (1-w)·prompt + w·centroid` (default: 0.33; set 0 to disable — pure-prompt recall, the kill-switch). The first turn of a session searches with pure prompt and seeds the centroid; subsequent turns blend so recall follows the session's intent instead of being query-literal. The EMA rate (0.4) is a shipped constant, not configurable |
|
|
269
277
|
| `dedupMergeThreshold` | Minimum cosine similarity for `hicortex dedup` to cluster memories as near-duplicates (default: 0.92) |
|
|
270
278
|
| `supersessionMinSimilarity` | Minimum cosine similarity for a nightly supersession candidate pair (default: 0.80) |
|
|
@@ -316,6 +324,7 @@ Optional config (add to plugin entry in `~/.openclaw/openclaw.json`):
|
|
|
316
324
|
| `authToken` | _(none)_ | Bearer token. Localhost bypasses auth; required for remote servers. Get the token from `hicortex status` on the server. |
|
|
317
325
|
| `defaultProject` | _(none)_ | Project name sent on recall, search, recent, and ingest whenever the gateway supplies no project (Hermes `default_project` parity). |
|
|
318
326
|
| `recallLimit` | `8` | Max memories per recall on the pre-0.14 `/search` fallback. The pushed recall index is sized by SERVER config (`recallMaxItems`) — the server accepts no client limit. |
|
|
327
|
+
| `scaffoldDeadMan` | `true` | Auto-scaffold the dead-man identity-guard line into the agent workspace bootstrap (`BOOTSTRAP.md`) at startup. Set `false` to disable the write and any file creation entirely. |
|
|
319
328
|
|
|
320
329
|
If `serverUrl`/`authToken` are absent from the config, the `HICORTEX_URL` and `HICORTEX_AUTH_TOKEN` environment variables are used as fallbacks (config always wins).
|
|
321
330
|
|
|
@@ -354,10 +363,13 @@ The artifact contains the irreplaceable data only: `hicortex.db`, the whole `ide
|
|
|
354
363
|
```jsonc
|
|
355
364
|
{
|
|
356
365
|
"backupDir": "/mnt/backups/hicortex", // optional; default <home>/backups
|
|
357
|
-
"backupCommand": "rclone copyto"
|
|
366
|
+
"backupCommand": "rclone copyto", // invoked: rclone copyto <path> remote:hicortex/
|
|
367
|
+
"backupRetention": 7 // keep the N newest artifacts (default 7; 0 = keep all)
|
|
358
368
|
}
|
|
359
369
|
```
|
|
360
370
|
|
|
371
|
+
**Retention.** After every successful write, the backup dir is pruned to the `backupRetention` newest artifacts — without it each nightly adds a tar.gz forever. Only files matching the product's own `hicortex-*.tar.gz` pattern are ever removed; anything else you keep in the dir is untouched. That pattern is the pruner's — **do not name your own copies `hicortex-*.tar.gz`**: a manual copy kept in the backup dir under that shape counts against retention and will be deleted once it falls past the boundary. Name operator copies differently (e.g. `manual-2026-08-19.tar.gz`). Applies to the nightly stage and `hicortex backup` alike (not to `--stdout`, which writes nothing on disk).
|
|
372
|
+
|
|
361
373
|
A failing, missing, or timed-out hook (5 min) reports failure and **never throws** — the artifact is already on disk; only the offsite copy didn't land. Both `hicortex backup` (non-zero exit) and the nightly stage surface the failure.
|
|
362
374
|
|
|
363
375
|
**Stream to stdout** (pipe to any offsite transport, no on-disk artifact):
|
|
@@ -368,7 +380,7 @@ npx @gamaze/hicortex backup --stdout | rclone rcat remote:hicortex/$(date -I).ta
|
|
|
368
380
|
|
|
369
381
|
`--stdout` is mutually exclusive with `--out`/`backupDir`.
|
|
370
382
|
|
|
371
|
-
**Nightly stage.** Every full nightly run
|
|
383
|
+
**Nightly stage.** Every full nightly run takes a backup automatically after consolidation and runs the hook if configured — and prunes per `backupRetention`. `--consolidate-only` runs back up too, but at most once a day: the stage is skipped only while the newest existing artifact is younger than ~20 hours (so a timer that fires several times a day still yields exactly one backup per day, bounded by retention). `--capture-only` never backs up (it is frequent and stateless). Backup failure does NOT fail the nightly — capture + consolidation have already succeeded; the failure surfaces as `backupOk:false` in the dashboard snapshot and telemetry for alerting.
|
|
372
384
|
|
|
373
385
|
**Restore (manual):**
|
|
374
386
|
|
package/dist/backup.d.ts
CHANGED
|
@@ -31,6 +31,13 @@
|
|
|
31
31
|
* `memories.content` via the embedder — non-destructive either way.
|
|
32
32
|
*/
|
|
33
33
|
import type Database from "better-sqlite3";
|
|
34
|
+
/**
|
|
35
|
+
* Default number of backup artifacts kept in the backup dir (#327). The
|
|
36
|
+
* nightly backup stage writes one `hicortex-<ISO>.tar.gz` per run with no
|
|
37
|
+
* pruning — unbounded growth (per hosted tenant too). Configurable via
|
|
38
|
+
* `backupRetention`; 0 keeps everything.
|
|
39
|
+
*/
|
|
40
|
+
export declare const DEFAULT_BACKUP_RETENTION = 7;
|
|
34
41
|
export interface CreateBackupOptions {
|
|
35
42
|
/** Open live DB handle (caller manages lifetime). Backed up via db.backup(). */
|
|
36
43
|
db: Database.Database;
|
|
@@ -46,12 +53,27 @@ export interface CreateBackupOptions {
|
|
|
46
53
|
* set. Configurable via `config.backupDir`.
|
|
47
54
|
*/
|
|
48
55
|
outDir?: string;
|
|
56
|
+
/**
|
|
57
|
+
* Directory for the transient DB snapshot (defaults to the OS tmpdir).
|
|
58
|
+
* Injectable so tests observe a PRIVATE dir — vitest runs files in parallel,
|
|
59
|
+
* and a sibling test's in-flight snapshot in the shared tmpdir reads as a
|
|
60
|
+
* "leak" to a before/after diff (final consolidation CR finding 1).
|
|
61
|
+
*/
|
|
62
|
+
snapshotDir?: string;
|
|
49
63
|
/**
|
|
50
64
|
* Stream the tar.gz to `process.stdout` instead of writing a file (the
|
|
51
65
|
* offsite pattern: `hicortex backup --stdout | rclone rcat ...`). Mutually
|
|
52
66
|
* exclusive with `outFile`/`outDir`.
|
|
53
67
|
*/
|
|
54
68
|
stdout?: boolean;
|
|
69
|
+
/**
|
|
70
|
+
* Retention (#327): how many of the newest `hicortex-*.tar.gz` artifacts to
|
|
71
|
+
* keep in the (dir-managed) output dir after a successful write. 0 keeps
|
|
72
|
+
* all. Defaults to DEFAULT_BACKUP_RETENTION; callers resolve the
|
|
73
|
+
* `backupRetention` config key. Only consulted when the artifact goes to
|
|
74
|
+
* `outDir`/the default dir — an explicit `outFile` is caller-owned.
|
|
75
|
+
*/
|
|
76
|
+
retention?: number;
|
|
55
77
|
}
|
|
56
78
|
export interface CreateBackupResult {
|
|
57
79
|
/** Absolute path of the written artifact; undefined when `stdout:true`. */
|
|
@@ -60,6 +82,11 @@ export interface CreateBackupResult {
|
|
|
60
82
|
bytes: number;
|
|
61
83
|
/** Number of files packaged (DB snapshot + identity tree + state files). */
|
|
62
84
|
files: number;
|
|
85
|
+
/**
|
|
86
|
+
* Old artifacts removed by the retention pass (#327). Undefined when no
|
|
87
|
+
* retention pass ran (stdout / explicit outFile); 0 = nothing to prune.
|
|
88
|
+
*/
|
|
89
|
+
pruned?: number;
|
|
63
90
|
}
|
|
64
91
|
export interface BackupHookResult {
|
|
65
92
|
ok: boolean;
|
|
@@ -92,6 +119,29 @@ export declare function createBackup(opts: CreateBackupOptions): Promise<CreateB
|
|
|
92
119
|
* here is deliberately dumb to avoid re-implementing a shell parser.
|
|
93
120
|
*/
|
|
94
121
|
export declare function runBackupHook(artifactPath: string, command: string | undefined): Promise<BackupHookResult>;
|
|
122
|
+
/**
|
|
123
|
+
* mtime (ms) of the NEWEST `hicortex-*.tar.gz` in `dir`, or undefined when the
|
|
124
|
+
* dir has none / cannot be read. The nightly consolidate-only artifact-age
|
|
125
|
+
* gate (#327 CR) uses this to bound the hosted backup cadence to ~1/day
|
|
126
|
+
* without a new state file: the newest artifact on disk IS the
|
|
127
|
+
* "last backed up" marker. Same product-owned filename pattern the pruner
|
|
128
|
+
* matches, so the gate and the pruner can never disagree about what counts.
|
|
129
|
+
*/
|
|
130
|
+
export declare function newestBackupArtifactMs(dir: string): number | undefined;
|
|
131
|
+
/**
|
|
132
|
+
* Prune a backup dir to the `retention` newest artifacts (#327). Matches ONLY
|
|
133
|
+
* files named `hicortex-*.tar.gz` (the nightly/CLI artifact pattern) — anything
|
|
134
|
+
* else in the dir (operator copies, notes) is never touched. Keeps the newest
|
|
135
|
+
* `retention` by mtime, with the ISO filename (time-ordered by construction)
|
|
136
|
+
* as a DESCENDING tie-break so equal mtimes resolve deterministically (the
|
|
137
|
+
* just-written artifact is the newest and always survives); deletes the rest,
|
|
138
|
+
* oldest first. `retention <= 0` keeps all.
|
|
139
|
+
*
|
|
140
|
+
* Best-effort by design: a per-file unlink failure logs and continues (a
|
|
141
|
+
* stale extra artifact is cheap; failing the nightly AFTER a good backup was
|
|
142
|
+
* written is not). Returns the number actually removed.
|
|
143
|
+
*/
|
|
144
|
+
export declare function pruneBackupArtifacts(dir: string, retention: number): number;
|
|
95
145
|
export interface BackupCliOptions {
|
|
96
146
|
/** `--out <dir>` — output directory (the artifact is auto-named). Takes precedence over `config.backupDir`. Mutually exclusive with stdout. */
|
|
97
147
|
outDir?: string;
|
package/dist/backup.js
CHANGED
|
@@ -32,8 +32,11 @@
|
|
|
32
32
|
* `memories.content` via the embedder — non-destructive either way.
|
|
33
33
|
*/
|
|
34
34
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
35
|
+
exports.DEFAULT_BACKUP_RETENTION = void 0;
|
|
35
36
|
exports.createBackup = createBackup;
|
|
36
37
|
exports.runBackupHook = runBackupHook;
|
|
38
|
+
exports.newestBackupArtifactMs = newestBackupArtifactMs;
|
|
39
|
+
exports.pruneBackupArtifacts = pruneBackupArtifacts;
|
|
37
40
|
exports.runBackupCli = runBackupCli;
|
|
38
41
|
const node_child_process_1 = require("node:child_process");
|
|
39
42
|
const node_crypto_1 = require("node:crypto");
|
|
@@ -45,6 +48,7 @@ const tar_stream_1 = require("tar-stream");
|
|
|
45
48
|
const paths_js_1 = require("./paths.js");
|
|
46
49
|
const db_js_1 = require("./db.js");
|
|
47
50
|
const init_js_1 = require("./init.js");
|
|
51
|
+
const config_read_js_1 = require("./config-read.js");
|
|
48
52
|
/**
|
|
49
53
|
* Hook timeout — a stuck offsite upload (rclone/aws/B2 hung on a dead network)
|
|
50
54
|
* must NOT hang the nightly. 5 min is generous for typical tarball uploads; the
|
|
@@ -52,6 +56,13 @@ const init_js_1 = require("./init.js");
|
|
|
52
56
|
* block forever" discipline.
|
|
53
57
|
*/
|
|
54
58
|
const HOOK_TIMEOUT_MS = 5 * 60 * 1000;
|
|
59
|
+
/**
|
|
60
|
+
* Default number of backup artifacts kept in the backup dir (#327). The
|
|
61
|
+
* nightly backup stage writes one `hicortex-<ISO>.tar.gz` per run with no
|
|
62
|
+
* pruning — unbounded growth (per hosted tenant too). Configurable via
|
|
63
|
+
* `backupRetention`; 0 keeps everything.
|
|
64
|
+
*/
|
|
65
|
+
exports.DEFAULT_BACKUP_RETENTION = 7;
|
|
55
66
|
/**
|
|
56
67
|
* Create a backup artifact. Snapshots the live DB via the online-backup API,
|
|
57
68
|
* packages it with the identity tree + state into a single `tar.gz`, and writes
|
|
@@ -69,7 +80,7 @@ async function createBackup(opts) {
|
|
|
69
80
|
// 1. Snapshot the live DB via the WAL-safe online-backup API (proven at
|
|
70
81
|
// dedup.ts:499). Runs concurrently with the live DB — no writer blocking,
|
|
71
82
|
// no torn copy. The temp file is cleaned up in `finally` below.
|
|
72
|
-
const snapshotPath = (0, node_path_1.join)((0, node_os_1.tmpdir)(), `hicortex-backup-${(0, node_crypto_1.randomBytes)(6).toString("hex")}.db`);
|
|
83
|
+
const snapshotPath = (0, node_path_1.join)(opts.snapshotDir ?? (0, node_os_1.tmpdir)(), `hicortex-backup-${(0, node_crypto_1.randomBytes)(6).toString("hex")}.db`);
|
|
73
84
|
await opts.db.backup(snapshotPath);
|
|
74
85
|
let totalBytes = 0;
|
|
75
86
|
let fileCount = 0;
|
|
@@ -144,7 +155,14 @@ async function createBackup(opts) {
|
|
|
144
155
|
}
|
|
145
156
|
packStream.finalize();
|
|
146
157
|
await sinkFinished;
|
|
147
|
-
|
|
158
|
+
// Retention (#327) — only after a SUCCESSFUL write (a failed one removed
|
|
159
|
+
// its own partial artifact above), and only for dir-managed targets: an
|
|
160
|
+
// explicit outFile is caller-owned, and --stdout writes nothing to prune.
|
|
161
|
+
let pruned;
|
|
162
|
+
if (!opts.stdout && opts.outFile === undefined) {
|
|
163
|
+
pruned = pruneBackupArtifacts((0, node_path_1.dirname)(outFile), opts.retention ?? exports.DEFAULT_BACKUP_RETENTION);
|
|
164
|
+
}
|
|
165
|
+
return { path: outFile, bytes: totalBytes, files: fileCount, pruned };
|
|
148
166
|
}
|
|
149
167
|
catch (err) {
|
|
150
168
|
// Never leave a truncated/half-written artifact on disk masquerading as a
|
|
@@ -231,6 +249,101 @@ async function runBackupHook(artifactPath, command) {
|
|
|
231
249
|
// ---------------------------------------------------------------------------
|
|
232
250
|
// File-set helpers
|
|
233
251
|
// ---------------------------------------------------------------------------
|
|
252
|
+
/**
|
|
253
|
+
* mtime (ms) of the NEWEST `hicortex-*.tar.gz` in `dir`, or undefined when the
|
|
254
|
+
* dir has none / cannot be read. The nightly consolidate-only artifact-age
|
|
255
|
+
* gate (#327 CR) uses this to bound the hosted backup cadence to ~1/day
|
|
256
|
+
* without a new state file: the newest artifact on disk IS the
|
|
257
|
+
* "last backed up" marker. Same product-owned filename pattern the pruner
|
|
258
|
+
* matches, so the gate and the pruner can never disagree about what counts.
|
|
259
|
+
*/
|
|
260
|
+
function newestBackupArtifactMs(dir) {
|
|
261
|
+
let names;
|
|
262
|
+
try {
|
|
263
|
+
names = (0, node_fs_1.readdirSync)(dir);
|
|
264
|
+
}
|
|
265
|
+
catch {
|
|
266
|
+
// Missing/unreadable dir — no artifact, no gate (a fresh install backs up).
|
|
267
|
+
return undefined;
|
|
268
|
+
}
|
|
269
|
+
let newest;
|
|
270
|
+
for (const n of names) {
|
|
271
|
+
if (!/^hicortex-.*\.tar\.gz$/.test(n))
|
|
272
|
+
continue;
|
|
273
|
+
try {
|
|
274
|
+
const ms = (0, node_fs_1.statSync)((0, node_path_1.join)(dir, n)).mtimeMs;
|
|
275
|
+
if (newest === undefined || ms > newest)
|
|
276
|
+
newest = ms;
|
|
277
|
+
}
|
|
278
|
+
catch {
|
|
279
|
+
// Raced/unreadable — an unknown-age artifact cannot inform the gate.
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
return newest;
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Prune a backup dir to the `retention` newest artifacts (#327). Matches ONLY
|
|
286
|
+
* files named `hicortex-*.tar.gz` (the nightly/CLI artifact pattern) — anything
|
|
287
|
+
* else in the dir (operator copies, notes) is never touched. Keeps the newest
|
|
288
|
+
* `retention` by mtime, with the ISO filename (time-ordered by construction)
|
|
289
|
+
* as a DESCENDING tie-break so equal mtimes resolve deterministically (the
|
|
290
|
+
* just-written artifact is the newest and always survives); deletes the rest,
|
|
291
|
+
* oldest first. `retention <= 0` keeps all.
|
|
292
|
+
*
|
|
293
|
+
* Best-effort by design: a per-file unlink failure logs and continues (a
|
|
294
|
+
* stale extra artifact is cheap; failing the nightly AFTER a good backup was
|
|
295
|
+
* written is not). Returns the number actually removed.
|
|
296
|
+
*/
|
|
297
|
+
function pruneBackupArtifacts(dir, retention) {
|
|
298
|
+
if (!Number.isFinite(retention) || retention <= 0)
|
|
299
|
+
return 0;
|
|
300
|
+
let names;
|
|
301
|
+
try {
|
|
302
|
+
names = (0, node_fs_1.readdirSync)(dir);
|
|
303
|
+
}
|
|
304
|
+
catch {
|
|
305
|
+
// Missing/unreadable dir — nothing to prune (e.g. a fresh install).
|
|
306
|
+
return 0;
|
|
307
|
+
}
|
|
308
|
+
const artifacts = names
|
|
309
|
+
.filter((n) => /^hicortex-.*\.tar\.gz$/.test(n))
|
|
310
|
+
.map((n) => {
|
|
311
|
+
const abs = (0, node_path_1.join)(dir, n);
|
|
312
|
+
let mtimeMs = 0;
|
|
313
|
+
try {
|
|
314
|
+
mtimeMs = (0, node_fs_1.statSync)(abs).mtimeMs;
|
|
315
|
+
}
|
|
316
|
+
catch {
|
|
317
|
+
// Raced/unreadable — sort it oldest so the prune attempt happens
|
|
318
|
+
// anyway (force-unlink below may still succeed).
|
|
319
|
+
}
|
|
320
|
+
return { name: n, abs, mtimeMs };
|
|
321
|
+
})
|
|
322
|
+
// Newest first; the ISO filename (`hicortex-<ISO>.tar.gz`) is time-ordered
|
|
323
|
+
// by construction, so it is the tie-break in DESCENDING order — identical
|
|
324
|
+
// mtimes (same-second writes, or an operator restoring a copy) otherwise
|
|
325
|
+
// leave the retention boundary to readdir order, which is FS-arbitrary
|
|
326
|
+
// and can differ run to run (#327 CR).
|
|
327
|
+
.sort((a, b) => b.mtimeMs - a.mtimeMs ||
|
|
328
|
+
(a.name < b.name ? 1 : a.name > b.name ? -1 : 0));
|
|
329
|
+
const doomed = artifacts.slice(Math.floor(retention));
|
|
330
|
+
let pruned = 0;
|
|
331
|
+
for (const a of doomed) {
|
|
332
|
+
try {
|
|
333
|
+
(0, node_fs_1.rmSync)(a.abs, { force: true });
|
|
334
|
+
pruned++;
|
|
335
|
+
}
|
|
336
|
+
catch (err) {
|
|
337
|
+
console.error(`[hicortex] backup retention: could not prune ${a.abs}: ` +
|
|
338
|
+
`${err instanceof Error ? err.message : String(err)}`);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
if (pruned > 0) {
|
|
342
|
+
console.log(`[hicortex] Backup retention: pruned ${pruned} old artifact(s), ` +
|
|
343
|
+
`keeping ${artifacts.length - pruned}`);
|
|
344
|
+
}
|
|
345
|
+
return pruned;
|
|
346
|
+
}
|
|
234
347
|
/**
|
|
235
348
|
* Recursively append every regular file under `absDir` to `entries`, with
|
|
236
349
|
* in-tar paths rooted at `relRoot`. Symlinks are SKIPPED (security — a symlink
|
|
@@ -304,6 +417,8 @@ async function runBackupCli(opts) {
|
|
|
304
417
|
const backupCommand = typeof config.backupCommand === "string" && config.backupCommand.trim()
|
|
305
418
|
? config.backupCommand
|
|
306
419
|
: undefined;
|
|
420
|
+
// Same reader + default as the nightly stage (#327) — one knob everywhere.
|
|
421
|
+
const retention = (0, config_read_js_1.readNonNegativeConfig)(config, "backupRetention", exports.DEFAULT_BACKUP_RETENTION);
|
|
307
422
|
const db = (0, db_js_1.initDb)((0, db_js_1.resolveDbPath)());
|
|
308
423
|
try {
|
|
309
424
|
const result = await createBackup({
|
|
@@ -315,6 +430,7 @@ async function runBackupCli(opts) {
|
|
|
315
430
|
// exclusivity check would otherwise reject it).
|
|
316
431
|
outDir: opts.stdout ? undefined : (opts.outDir ?? backupDir),
|
|
317
432
|
stdout: opts.stdout,
|
|
433
|
+
retention,
|
|
318
434
|
});
|
|
319
435
|
if (result.path) {
|
|
320
436
|
console.log(`[hicortex] Backup written: ${result.path} ` +
|
package/dist/capture.d.ts
CHANGED
|
@@ -71,6 +71,12 @@ export interface PostResult {
|
|
|
71
71
|
dropped?: string[];
|
|
72
72
|
skipped?: boolean;
|
|
73
73
|
error?: string;
|
|
74
|
+
/**
|
|
75
|
+
* Parsed `Retry-After` of a 429 response, in ms (#327). Absent when the
|
|
76
|
+
* response carried no parseable header. Honored (capped) by the in-run
|
|
77
|
+
* rate-limit retry; a token-budget 429 ignores it (terminal, not transient).
|
|
78
|
+
*/
|
|
79
|
+
retryAfterMs?: number;
|
|
74
80
|
/**
|
|
75
81
|
* The segment's metered LLM usage from the 201 body (#287). Absent on a
|
|
76
82
|
* pre-#287 daemon (or any non-201) — callers must treat absent as zero.
|
|
@@ -106,9 +112,20 @@ export interface CaptureResult {
|
|
|
106
112
|
hadTransientFailure: boolean;
|
|
107
113
|
/**
|
|
108
114
|
* Set when the loop stopped early on a terminal server response: "limit"
|
|
109
|
-
* (429
|
|
115
|
+
* (token-budget 429, mcp-server.ts's `"token budget exceeded"` gate) or
|
|
116
|
+
* "auth" (401). A rate-limit 429 never sets this — it is transient (#327).
|
|
117
|
+
* The caller decides watermark handling.
|
|
110
118
|
*/
|
|
111
119
|
stopped?: "limit" | "auth";
|
|
120
|
+
/**
|
|
121
|
+
* Run-global rate-429 latch (#327 CR): true when at least one session
|
|
122
|
+
* SURRENDERED to a rate-limit 429 (the transient kind — postWithRateRetry
|
|
123
|
+
* exhausted its in-run retries) and the remaining sessions were skipped
|
|
124
|
+
* without POSTing. Mirrors `stopped`'s role but is transient, not terminal:
|
|
125
|
+
* hadTransientFailure is also set, so the watermark holds and every skipped
|
|
126
|
+
* session retries next run with its cursor untouched.
|
|
127
|
+
*/
|
|
128
|
+
rateLimited: boolean;
|
|
112
129
|
/**
|
|
113
130
|
* Sum of the successful segments' reported LLM usage (#287) — the run's
|
|
114
131
|
* distill spend, for the dashboard snapshot's token totals. Zero-filled when
|
|
@@ -138,11 +155,19 @@ export declare function hardSplitText(text: string, maxChars?: number): string[]
|
|
|
138
155
|
* hard-split pieces (A2).
|
|
139
156
|
*/
|
|
140
157
|
export declare function packSegments(entries: unknown[], startCursor: number, entryCursors: number[], maxChars?: number): Segment[];
|
|
158
|
+
/** True when a 429 result is the tenant token-budget gate (terminal). */
|
|
159
|
+
export declare function isTokenBudget429(result: PostResult): boolean;
|
|
160
|
+
/** In-run retries for a rate-limit 429 before giving up as transient. */
|
|
161
|
+
export declare const RATE_LIMIT_RETRIES = 3;
|
|
141
162
|
/**
|
|
142
163
|
* Capture a list of session delta batches: pack, POST in order, advance
|
|
143
164
|
* per-session cursors on success. Segments of one session POST in order; the
|
|
144
165
|
* first hard failure stops THAT session (cursor holds at the last confirmed
|
|
145
|
-
* boundary) while other sessions continue. A 429/401 stops the whole
|
|
166
|
+
* boundary) while other sessions continue. A budget-429/401 stops the whole
|
|
167
|
+
* loop; a rate-429 is retried in-run before giving up as transient — and a
|
|
168
|
+
* SURRENDERED rate-429 latches the whole run: remaining sessions skip
|
|
169
|
+
* without POSTing (their cursors hold; next run retries) instead of each
|
|
170
|
+
* re-paying the Retry-After ladder (#327).
|
|
146
171
|
*/
|
|
147
172
|
export declare function captureBatches(batches: TranscriptBatch[], opts: CaptureOptions): Promise<CaptureResult>;
|
|
148
173
|
/**
|
package/dist/capture.js
CHANGED
|
@@ -14,9 +14,10 @@
|
|
|
14
14
|
* multi-night simulation can run as a pure unit test with no HTTP listener.
|
|
15
15
|
*/
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
-
exports.MIN_SEGMENT_CHARS = exports.SEGMENT_MAX_CHARS = void 0;
|
|
17
|
+
exports.RATE_LIMIT_RETRIES = exports.MIN_SEGMENT_CHARS = exports.SEGMENT_MAX_CHARS = void 0;
|
|
18
18
|
exports.hardSplitText = hardSplitText;
|
|
19
19
|
exports.packSegments = packSegments;
|
|
20
|
+
exports.isTokenBudget429 = isTokenBudget429;
|
|
20
21
|
exports.captureBatches = captureBatches;
|
|
21
22
|
exports.acquireCaptureLock = acquireCaptureLock;
|
|
22
23
|
const node_fs_1 = require("node:fs");
|
|
@@ -127,11 +128,80 @@ function packSegments(entries, startCursor, entryCursors, maxChars = exports.SEG
|
|
|
127
128
|
flushGroup(entries.length);
|
|
128
129
|
return segments;
|
|
129
130
|
}
|
|
131
|
+
// ---------------------------------------------------------------------------
|
|
132
|
+
// 429 taxonomy (#327) — two producers share one status code
|
|
133
|
+
// ---------------------------------------------------------------------------
|
|
134
|
+
// - The tenant server's token-budget gate (mcp-server.ts:1205) answers
|
|
135
|
+
// 429 `{"error":"token budget exceeded"}` — TERMINAL for the billing
|
|
136
|
+
// period; retrying cannot help, so capture stops with stopped:"limit".
|
|
137
|
+
// - A rate limit (e.g. a hosted router's per-token limiter answering
|
|
138
|
+
// 429 `{"error":"rate limit exceeded"}` + Retry-After) is TRANSIENT —
|
|
139
|
+
// the window passes in seconds/minutes. Treating it as the terminal
|
|
140
|
+
// limit-stop left nightlies failing forever with the wrong diagnosis.
|
|
141
|
+
// The body's error string is the discriminator: only the budget gate says
|
|
142
|
+
// "token budget".
|
|
143
|
+
/** True when a 429 result is the tenant token-budget gate (terminal). */
|
|
144
|
+
function isTokenBudget429(result) {
|
|
145
|
+
return typeof result.error === "string" && /token budget/i.test(result.error);
|
|
146
|
+
}
|
|
147
|
+
/** In-run retries for a rate-limit 429 before giving up as transient. */
|
|
148
|
+
exports.RATE_LIMIT_RETRIES = 3;
|
|
149
|
+
/**
|
|
150
|
+
* Backoff schedule (ms) between rate-429 retries: 5s, 15s, 30s — short enough
|
|
151
|
+
* to ride out a per-token limiter window inside one nightly, long enough to
|
|
152
|
+
* let a busy shared backend drain. Overridable via
|
|
153
|
+
* HICORTEX_RATE_LIMIT_BACKOFF_MS (comma-separated ms list — tests only).
|
|
154
|
+
*/
|
|
155
|
+
const RATE_LIMIT_BACKOFF_MS = [5_000, 15_000, 30_000];
|
|
156
|
+
/**
|
|
157
|
+
* Cap on an honored Retry-After. The header is advisory and can name a window
|
|
158
|
+
* far beyond one nightly (an hourly limiter saying 3600); waiting it out
|
|
159
|
+
* in-run would stall the run for hours with the capture lock held. Past the
|
|
160
|
+
* cap we give up as transient — the cursor holds and the next run retries.
|
|
161
|
+
*/
|
|
162
|
+
const RATE_LIMIT_RETRY_AFTER_CAP_MS = 5 * 60 * 1000;
|
|
163
|
+
function rateLimitBackoffSchedule() {
|
|
164
|
+
const env = process.env.HICORTEX_RATE_LIMIT_BACKOFF_MS;
|
|
165
|
+
if (env && env.trim()) {
|
|
166
|
+
const parsed = env.split(",").map((s) => Number(s.trim()));
|
|
167
|
+
if (parsed.length > 0 && parsed.every((n) => Number.isFinite(n) && n >= 0)) {
|
|
168
|
+
return parsed;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return RATE_LIMIT_BACKOFF_MS;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* POST one segment, retrying in-run while the answer is a TRANSIENT
|
|
175
|
+
* (non-budget) 429. Returns the final result — a budget-429 passes through
|
|
176
|
+
* untouched (the caller's terminal branch) and a rate-429 that survived every
|
|
177
|
+
* retry is returned as-is for the caller's transient branch. Network errors
|
|
178
|
+
* propagate to the caller's existing catch (message preserved).
|
|
179
|
+
*/
|
|
180
|
+
async function postWithRateRetry(post, body) {
|
|
181
|
+
const schedule = rateLimitBackoffSchedule();
|
|
182
|
+
for (let attempt = 0;; attempt++) {
|
|
183
|
+
const result = await post(body);
|
|
184
|
+
if (result.status !== 429 || isTokenBudget429(result))
|
|
185
|
+
return result;
|
|
186
|
+
if (attempt >= exports.RATE_LIMIT_RETRIES || attempt >= schedule.length)
|
|
187
|
+
return result;
|
|
188
|
+
const waitMs = result.retryAfterMs != null && result.retryAfterMs > 0
|
|
189
|
+
? Math.min(result.retryAfterMs, RATE_LIMIT_RETRY_AFTER_CAP_MS)
|
|
190
|
+
: schedule[attempt];
|
|
191
|
+
console.log(`[hicortex] Rate limited (${result.error ?? "429"}) — retrying segment in ` +
|
|
192
|
+
`${Math.round(waitMs / 1000)}s (retry ${attempt + 1}/${exports.RATE_LIMIT_RETRIES})`);
|
|
193
|
+
await sleep(waitMs);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
130
196
|
/**
|
|
131
197
|
* Capture a list of session delta batches: pack, POST in order, advance
|
|
132
198
|
* per-session cursors on success. Segments of one session POST in order; the
|
|
133
199
|
* first hard failure stops THAT session (cursor holds at the last confirmed
|
|
134
|
-
* boundary) while other sessions continue. A 429/401 stops the whole
|
|
200
|
+
* boundary) while other sessions continue. A budget-429/401 stops the whole
|
|
201
|
+
* loop; a rate-429 is retried in-run before giving up as transient — and a
|
|
202
|
+
* SURRENDERED rate-429 latches the whole run: remaining sessions skip
|
|
203
|
+
* without POSTing (their cursors hold; next run retries) instead of each
|
|
204
|
+
* re-paying the Retry-After ladder (#327).
|
|
135
205
|
*/
|
|
136
206
|
async function captureBatches(batches, opts) {
|
|
137
207
|
const { post, cursorStore, dryRun = false, segmentMaxChars = exports.SEGMENT_MAX_CHARS, sourceAgentId, sourceDomain } = opts;
|
|
@@ -139,10 +209,16 @@ async function captureBatches(batches, opts) {
|
|
|
139
209
|
let sessionsSent = 0;
|
|
140
210
|
let hadTransientFailure = false;
|
|
141
211
|
let stopped;
|
|
212
|
+
// Run-global rate-429 latch (#327 CR): once one session has paid the full
|
|
213
|
+
// Retry-After ladder and surrendered, the limiter window is exhausted for
|
|
214
|
+
// THIS run — letting each remaining session re-pay the ladder (worst case
|
|
215
|
+
// ~15 min each at the 5-min cap, with the capture lock held) can stretch a
|
|
216
|
+
// 20-session run to hours for zero accepted segments.
|
|
217
|
+
let rateLimited = false;
|
|
142
218
|
// #287: distill tokens reported by successful 201s only — skips (200) and
|
|
143
219
|
// failures metered nothing client-side.
|
|
144
220
|
const distillUsage = { prompt: 0, completion: 0, total: 0 };
|
|
145
|
-
for (const batch of batches) {
|
|
221
|
+
for (const [batchIdx, batch] of batches.entries()) {
|
|
146
222
|
const short = batch.sessionId.slice(0, 8);
|
|
147
223
|
const segments = packSegments(batch.entries, batch.startCursor, batch.entryCursors, segmentMaxChars);
|
|
148
224
|
if (segments.length === 0)
|
|
@@ -198,7 +274,7 @@ async function captureBatches(batches, opts) {
|
|
|
198
274
|
};
|
|
199
275
|
let result;
|
|
200
276
|
try {
|
|
201
|
-
result = await post
|
|
277
|
+
result = await postWithRateRetry(post, body);
|
|
202
278
|
}
|
|
203
279
|
catch (err) {
|
|
204
280
|
console.error(`[hicortex] Capture failed: ${err instanceof Error ? err.message : String(err)} — will retry next run`);
|
|
@@ -228,11 +304,24 @@ async function captureBatches(batches, opts) {
|
|
|
228
304
|
if (result.skipped)
|
|
229
305
|
console.log(`[hicortex] Segment ${body.segment_id} already ingested`);
|
|
230
306
|
}
|
|
231
|
-
else if (result.status === 429) {
|
|
307
|
+
else if (result.status === 429 && isTokenBudget429(result)) {
|
|
232
308
|
console.log(`[hicortex] Memory limit reached: ${result.error}. Stopping capture.`);
|
|
233
309
|
stopped = "limit";
|
|
234
310
|
break;
|
|
235
311
|
}
|
|
312
|
+
else if (result.status === 429) {
|
|
313
|
+
// Rate-limit 429 that already burned its in-run retries (postWithRateRetry).
|
|
314
|
+
// Transient, NOT a limit stop (#327): hold the cursor, let the next run
|
|
315
|
+
// retry — a misdiagnosed "memory limit" here failed nightlies forever.
|
|
316
|
+
console.error(`[hicortex] Rate limited: ${result.error ?? "429"} — giving up this run, ` +
|
|
317
|
+
`will retry next run (cursor held)`);
|
|
318
|
+
hadTransientFailure = true;
|
|
319
|
+
// Latch the WHOLE run (see the declaration above): remaining sessions
|
|
320
|
+
// are skipped below, cursors untouched — they retry next run instead
|
|
321
|
+
// of each re-paying the full Retry-After ladder.
|
|
322
|
+
rateLimited = true;
|
|
323
|
+
break;
|
|
324
|
+
}
|
|
236
325
|
else if (result.status === 401) {
|
|
237
326
|
console.error(`[hicortex] Auth failed. Check authToken in ~/.hicortex/config.json`);
|
|
238
327
|
stopped = "auth";
|
|
@@ -265,8 +354,16 @@ async function captureBatches(batches, opts) {
|
|
|
265
354
|
}
|
|
266
355
|
if (stopped)
|
|
267
356
|
break;
|
|
357
|
+
if (rateLimited) {
|
|
358
|
+
const remaining = batches.length - batchIdx - 1;
|
|
359
|
+
if (remaining > 0) {
|
|
360
|
+
console.error(`[hicortex] Rate limited earlier this run — skipping ${remaining} remaining ` +
|
|
361
|
+
`session(s); they retry next run (cursors untouched)`);
|
|
362
|
+
}
|
|
363
|
+
break;
|
|
364
|
+
}
|
|
268
365
|
}
|
|
269
|
-
return { memoriesIngested, sessionsSent, hadTransientFailure, stopped, distillUsage };
|
|
366
|
+
return { memoriesIngested, sessionsSent, hadTransientFailure, stopped, rateLimited, distillUsage };
|
|
270
367
|
}
|
|
271
368
|
// ---------------------------------------------------------------------------
|
|
272
369
|
// Single-flight guard (A5)
|
package/dist/embedder.d.ts
CHANGED
|
@@ -25,6 +25,29 @@ export declare function embed(text: string): Promise<Float32Array>;
|
|
|
25
25
|
* Embed multiple texts. Returns an array of Float32Array embeddings.
|
|
26
26
|
*/
|
|
27
27
|
export declare function embedBatch(texts: string[]): Promise<Float32Array[]>;
|
|
28
|
+
/**
|
|
29
|
+
* Fire-and-forget embedder warm-up (#329 item 2), called at the END of server
|
|
30
|
+
* boot. The ONNX pipeline lazy-loads inside the first embed() (~0.5-3s cold),
|
|
31
|
+
* so without this the FIRST /recall-index after every restart paid the model
|
|
32
|
+
* load inside its own latency budget — the 1s client hook budget blows and
|
|
33
|
+
* that turn silently loses recall.
|
|
34
|
+
*
|
|
35
|
+
* Contract (unit-pinned in tests/embedder-warm.test.ts):
|
|
36
|
+
* - fires exactly ONE embed call ("warmup"), NEVER awaited — returns
|
|
37
|
+
* synchronously so boot/listen is never blocked;
|
|
38
|
+
* - a failing warm-up is logged once (console.warn) and swallowed —
|
|
39
|
+
* warm-up is an optimization, never a boot dependency. The next real
|
|
40
|
+
* embed() retries the lazy load on its own terms.
|
|
41
|
+
*
|
|
42
|
+
* `embedFn` is injectable for tests; production passes the module's embed().
|
|
43
|
+
*
|
|
44
|
+
* MEMORY NOTE (accepted trade-off, #329 CR finding 3): warming at boot makes
|
|
45
|
+
* the model (~150-300MB resident) load in every server process from startup —
|
|
46
|
+
* including idle hosted tenant containers, which previously never loaded it.
|
|
47
|
+
* Accepted at current hosted sizing (2g per-tenant caps; active tenants load
|
|
48
|
+
* it on first use anyway). See the warm-site comment in mcp-server.ts.
|
|
49
|
+
*/
|
|
50
|
+
export declare function warmEmbedder(embedFn?: (text: string) => Promise<Float32Array>): void;
|
|
28
51
|
/**
|
|
29
52
|
* Return the embedding dimension count.
|
|
30
53
|
*/
|
package/dist/embedder.js
CHANGED
|
@@ -11,6 +11,7 @@ exports.EMBEDDING_DIMENSIONS = void 0;
|
|
|
11
11
|
exports.resolveModelCacheDir = resolveModelCacheDir;
|
|
12
12
|
exports.embed = embed;
|
|
13
13
|
exports.embedBatch = embedBatch;
|
|
14
|
+
exports.warmEmbedder = warmEmbedder;
|
|
14
15
|
exports.dimensions = dimensions;
|
|
15
16
|
const node_fs_1 = require("node:fs");
|
|
16
17
|
const node_path_1 = require("node:path");
|
|
@@ -104,6 +105,34 @@ async function embedBatch(texts) {
|
|
|
104
105
|
}
|
|
105
106
|
return results;
|
|
106
107
|
}
|
|
108
|
+
/**
|
|
109
|
+
* Fire-and-forget embedder warm-up (#329 item 2), called at the END of server
|
|
110
|
+
* boot. The ONNX pipeline lazy-loads inside the first embed() (~0.5-3s cold),
|
|
111
|
+
* so without this the FIRST /recall-index after every restart paid the model
|
|
112
|
+
* load inside its own latency budget — the 1s client hook budget blows and
|
|
113
|
+
* that turn silently loses recall.
|
|
114
|
+
*
|
|
115
|
+
* Contract (unit-pinned in tests/embedder-warm.test.ts):
|
|
116
|
+
* - fires exactly ONE embed call ("warmup"), NEVER awaited — returns
|
|
117
|
+
* synchronously so boot/listen is never blocked;
|
|
118
|
+
* - a failing warm-up is logged once (console.warn) and swallowed —
|
|
119
|
+
* warm-up is an optimization, never a boot dependency. The next real
|
|
120
|
+
* embed() retries the lazy load on its own terms.
|
|
121
|
+
*
|
|
122
|
+
* `embedFn` is injectable for tests; production passes the module's embed().
|
|
123
|
+
*
|
|
124
|
+
* MEMORY NOTE (accepted trade-off, #329 CR finding 3): warming at boot makes
|
|
125
|
+
* the model (~150-300MB resident) load in every server process from startup —
|
|
126
|
+
* including idle hosted tenant containers, which previously never loaded it.
|
|
127
|
+
* Accepted at current hosted sizing (2g per-tenant caps; active tenants load
|
|
128
|
+
* it on first use anyway). See the warm-site comment in mcp-server.ts.
|
|
129
|
+
*/
|
|
130
|
+
function warmEmbedder(embedFn = embed) {
|
|
131
|
+
embedFn("warmup").catch((err) => {
|
|
132
|
+
console.warn(`[hicortex] Embedder warm-up failed (first search will lazy-load instead): ` +
|
|
133
|
+
(err instanceof Error ? err.message : String(err)));
|
|
134
|
+
});
|
|
135
|
+
}
|
|
107
136
|
/**
|
|
108
137
|
* Return the embedding dimension count.
|
|
109
138
|
*/
|
package/dist/index.d.ts
CHANGED
|
@@ -69,6 +69,16 @@ export declare function formatToolResults(results: MemorySearchResult[]): {
|
|
|
69
69
|
* the plugin half-initialized.
|
|
70
70
|
*/
|
|
71
71
|
export declare function resolveOcPluginConfig(raw: unknown): HicortexConfig;
|
|
72
|
+
/**
|
|
73
|
+
* Resolve the agent workspace directory from the RAW gateway config (#326):
|
|
74
|
+
* OpenClaw's `agents.defaults.workspace`. Pure — no module state, no fs, no
|
|
75
|
+
* mutation, never throws. Absent/non-string/empty → null (the caller falls
|
|
76
|
+
* back to the OC default workspace). Deliberately does NOT read the plugin's
|
|
77
|
+
* own config section: the workspace is a gateway-level fact, not a plugin
|
|
78
|
+
* knob, so it is resolved from ctx.config directly (the whole openclaw.json,
|
|
79
|
+
* same object resolveOcPluginConfig walks).
|
|
80
|
+
*/
|
|
81
|
+
export declare function resolveOcWorkspaceDir(raw: unknown): string | null;
|
|
72
82
|
declare const _default: {
|
|
73
83
|
id: string;
|
|
74
84
|
name: string;
|
|
@@ -76,3 +86,12 @@ declare const _default: {
|
|
|
76
86
|
register(api: any): void;
|
|
77
87
|
};
|
|
78
88
|
export default _default;
|
|
89
|
+
/**
|
|
90
|
+
* Normalize a gateway-config workspace path for WRITING (#326 CR1): a bare
|
|
91
|
+
* `~` or leading `~/` expands against the real home dir; anything still
|
|
92
|
+
* RELATIVE afterwards is rejected (null). OpenClaw's semantics for relative
|
|
93
|
+
* workspace values are not verifiable from the plugin, and writing under
|
|
94
|
+
* process.cwd() would place the guard where OC never reads it — a silent
|
|
95
|
+
* no-op safety — so the caller skips with a warning instead. Pure; no fs.
|
|
96
|
+
*/
|
|
97
|
+
export declare function normalizeWorkspacePath(ws: string): string | null;
|