@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/dist/index.js
CHANGED
|
@@ -42,6 +42,8 @@
|
|
|
42
42
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
43
43
|
exports.formatToolResults = formatToolResults;
|
|
44
44
|
exports.resolveOcPluginConfig = resolveOcPluginConfig;
|
|
45
|
+
exports.resolveOcWorkspaceDir = resolveOcWorkspaceDir;
|
|
46
|
+
exports.normalizeWorkspacePath = normalizeWorkspacePath;
|
|
45
47
|
const paths_js_1 = require("./paths.js");
|
|
46
48
|
const features_js_1 = require("./features.js");
|
|
47
49
|
const extensions_js_1 = require("./extensions.js");
|
|
@@ -128,6 +130,14 @@ const warnedRecallStatuses = new Set();
|
|
|
128
130
|
/** Warn-once flag for /search fallback failures (#316 CR finding 3): fires on
|
|
129
131
|
* the first failure of a streak, re-armed by any successful fallback fetch. */
|
|
130
132
|
let warnedLegacyFallbackFailure = false;
|
|
133
|
+
/** Warn-once-per-PROCESS flag (#326): unpinned gateway plugins.allow. NOT
|
|
134
|
+
* reset at start() — a gateway restart inside one process must not re-warn
|
|
135
|
+
* (a new process starts with clean flags anyway). */
|
|
136
|
+
let warnedUnpinnedPlugins = false;
|
|
137
|
+
/** Warn-once-per-PROCESS bookkeeping (#326) for dead-man scaffold SKIPS and
|
|
138
|
+
* failures — one warning per distinct cause (relative path, missing dir,
|
|
139
|
+
* non-UTF-8 file, fs error), never reset at start(). */
|
|
140
|
+
const warnedScaffoldSkips = new Set();
|
|
131
141
|
/** Plugin logger captured at service start (ctx.logger or console). */
|
|
132
142
|
let pluginLog = console.log;
|
|
133
143
|
/** Sessions whose server-side recall dedup was already reset this process
|
|
@@ -164,6 +174,22 @@ const sessionsReset = makeBoundedSessionTracker(SESSION_TRACKER_CAP);
|
|
|
164
174
|
*/
|
|
165
175
|
const identityInjected = makeBoundedSessionTracker(SESSION_TRACKER_CAP);
|
|
166
176
|
const lessonsInjected = makeBoundedSessionTracker(SESSION_TRACKER_CAP);
|
|
177
|
+
/**
|
|
178
|
+
* #327 banner lifecycle — notices memoized SEPARATELY from identityInjected.
|
|
179
|
+
* appendSystemContext persists on the session (premise above), so a FAILED
|
|
180
|
+
* identity fetch that re-injects the IDENTITY UNAVAILABLE banner every turn
|
|
181
|
+
* accumulates one copy per turn of an outage (dozens over an hour) — and the
|
|
182
|
+
* suspension wording then never retracts. The FETCH still retries every turn
|
|
183
|
+
* (identityInjected is only set on success); the NOTICE (banner or 404
|
|
184
|
+
* version-skew note) is appended once per outage, and the first success that
|
|
185
|
+
* delivers identity content prepends a one-line retraction (see
|
|
186
|
+
* IDENTITY_RESTORED_RETRACTION). Two trackers so the kinds settle
|
|
187
|
+
* independently — a 404 note must not suppress a later genuine-outage banner.
|
|
188
|
+
* Evicted on compaction/reset with the other memos: a rebuilt window may have
|
|
189
|
+
* dropped the notice, so one re-inject after the rebuild is wanted, not lost.
|
|
190
|
+
*/
|
|
191
|
+
const identityBannerShown = makeBoundedSessionTracker(SESSION_TRACKER_CAP);
|
|
192
|
+
const identityNoteShown = makeBoundedSessionTracker(SESSION_TRACKER_CAP);
|
|
167
193
|
/** In-flight {reset:true} POSTs by session key (#316). A compaction hook fires
|
|
168
194
|
* a reset fire-and-forget (F9 — no latency on compaction); the NEXT recall
|
|
169
195
|
* fetch for that session AWAITS the entry here, so a slow reset can never
|
|
@@ -347,6 +373,27 @@ const IDENTITY_UNAVAILABLE_BANNER = [
|
|
|
347
373
|
*/
|
|
348
374
|
const IDENTITY_VERSION_SKEW_NOTE = `[hicortex] Identity layer skipped — ${describeGetFailure(404, "/identity")}. ` +
|
|
349
375
|
`This is version skew, not an outage: no action suspension applies; identity returns once the server is upgraded.`;
|
|
376
|
+
/**
|
|
377
|
+
* Recovery retraction (#327): when identity content arrives for a session that
|
|
378
|
+
* earlier got the dead-man banner, this line rides FIRST in that turn's
|
|
379
|
+
* injection. The banner persists on the session prompt, so without an explicit
|
|
380
|
+
* lift the "public actions suspended" wording outlives the outage for the rest
|
|
381
|
+
* of the session. NOT sent for the 404 note (it carries no suspension) and NOT
|
|
382
|
+
* for a gated/off success — identity content is the one outcome that actually
|
|
383
|
+
* restores what the banner said was missing.
|
|
384
|
+
*/
|
|
385
|
+
const IDENTITY_RESTORED_RETRACTION = `[hicortex] The earlier IDENTITY UNAVAILABLE notice no longer applies — ` +
|
|
386
|
+
`identity restored.`;
|
|
387
|
+
/**
|
|
388
|
+
* The #313 SECONDARY layer, verbatim (#326): the bootstrap-file sentence that
|
|
389
|
+
* still guards the agent when the plugin itself cannot inject anything — the
|
|
390
|
+
* banner above needs a live hook, while this line rides the agent's persisted
|
|
391
|
+
* bootstrap instructions. Installs kept forgetting to add it by hand, so the
|
|
392
|
+
* plugin now scaffolds it itself at service start (scaffoldDeadManGuard).
|
|
393
|
+
*/
|
|
394
|
+
const 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.";
|
|
395
|
+
/** Agent workspace bootstrap file the guard line is scaffolded into (#326). */
|
|
396
|
+
const BOOTSTRAP_FILENAME = "BOOTSTRAP.md";
|
|
350
397
|
/**
|
|
351
398
|
* Fetch /lessons and build the `## Hicortex Learnings` block. `failed: true`
|
|
352
399
|
* ONLY when the fetch itself failed (serverGet null data — unreachable,
|
|
@@ -670,6 +717,13 @@ function resolveOcPluginConfig(raw) {
|
|
|
670
717
|
warn(`plugin config key "defaultProject" must be a non-empty string (got ${describeInvalid(rawProject)}) — ignoring it`);
|
|
671
718
|
resolved.defaultProject = undefined;
|
|
672
719
|
}
|
|
720
|
+
// #326 kill-switch: boolean-only. typeof (not describeInvalid — that helper
|
|
721
|
+
// names INVALID-STRING shapes and would misreport a non-empty string).
|
|
722
|
+
const rawScaffold = winner.scaffoldDeadMan;
|
|
723
|
+
if (rawScaffold !== undefined && typeof rawScaffold !== "boolean") {
|
|
724
|
+
warn(`plugin config key "scaffoldDeadMan" must be a boolean (got ${typeof rawScaffold}) — ignoring it`);
|
|
725
|
+
resolved.scaffoldDeadMan = undefined;
|
|
726
|
+
}
|
|
673
727
|
// Shadow detection (F2) — two configs disagreeing, surfaced instead of
|
|
674
728
|
// silently honoring one of them. Case 1: an OC-scaffolded EMPTY
|
|
675
729
|
// plugins.entries.hicortex.config was skipped while a bare top-level
|
|
@@ -689,6 +743,20 @@ function resolveOcPluginConfig(raw) {
|
|
|
689
743
|
}
|
|
690
744
|
return resolved;
|
|
691
745
|
}
|
|
746
|
+
/**
|
|
747
|
+
* Resolve the agent workspace directory from the RAW gateway config (#326):
|
|
748
|
+
* OpenClaw's `agents.defaults.workspace`. Pure — no module state, no fs, no
|
|
749
|
+
* mutation, never throws. Absent/non-string/empty → null (the caller falls
|
|
750
|
+
* back to the OC default workspace). Deliberately does NOT read the plugin's
|
|
751
|
+
* own config section: the workspace is a gateway-level fact, not a plugin
|
|
752
|
+
* knob, so it is resolved from ctx.config directly (the whole openclaw.json,
|
|
753
|
+
* same object resolveOcPluginConfig walks).
|
|
754
|
+
*/
|
|
755
|
+
function resolveOcWorkspaceDir(raw) {
|
|
756
|
+
const agents = isRecord(isRecord(raw)?.agents);
|
|
757
|
+
const workspace = isRecord(agents?.defaults)?.workspace;
|
|
758
|
+
return isNonEmptyString(workspace) ? workspace : null;
|
|
759
|
+
}
|
|
692
760
|
// ---------------------------------------------------------------------------
|
|
693
761
|
// Plugin export
|
|
694
762
|
// ---------------------------------------------------------------------------
|
|
@@ -740,6 +808,8 @@ exports.default = {
|
|
|
740
808
|
sessionsReset.clear();
|
|
741
809
|
identityInjected.clear();
|
|
742
810
|
lessonsInjected.clear();
|
|
811
|
+
identityBannerShown.clear();
|
|
812
|
+
identityNoteShown.clear();
|
|
743
813
|
pendingResets.clear();
|
|
744
814
|
pluginLog = log;
|
|
745
815
|
log(`[hicortex] Thin-client mode — server: ${serverUrl}`);
|
|
@@ -792,6 +862,17 @@ exports.default = {
|
|
|
792
862
|
`Run \`npx @gamaze/hicortex init\` to start the server. ` +
|
|
793
863
|
`Capture and tool calls will fail until the server is available.`);
|
|
794
864
|
}
|
|
865
|
+
// #326 self-hardening — install hygiene, both fail-soft by design:
|
|
866
|
+
// keep the dead-man guard line (#313 secondary layer) present in the
|
|
867
|
+
// agent workspace bootstrap, and surface an unpinned gateway trust
|
|
868
|
+
// list. The kill-switch (config scaffoldDeadMan, default on) disables
|
|
869
|
+
// the scaffold entirely; the trust warning always runs.
|
|
870
|
+
scaffoldDeadManGuard({
|
|
871
|
+
workspaceDir: resolveOcWorkspaceDir(ctx.config) ?? fallbackOcWorkspace(),
|
|
872
|
+
enabled: config.scaffoldDeadMan !== false,
|
|
873
|
+
log,
|
|
874
|
+
});
|
|
875
|
+
warnIfPluginsUnpinned(ctx.config, log);
|
|
795
876
|
ensureToolsAllowed(log);
|
|
796
877
|
},
|
|
797
878
|
async stop() {
|
|
@@ -869,11 +950,45 @@ exports.default = {
|
|
|
869
950
|
// note, NOT the banner (CR2: a pinned plugin on an old server must
|
|
870
951
|
// not self-suspend every turn). Lessons/recall keep their own
|
|
871
952
|
// independent fail-soft.
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
953
|
+
//
|
|
954
|
+
// #327 lifecycle: the notice is appended ONCE per outage (appends
|
|
955
|
+
// persist on the session — every-turn copies accumulate), and the
|
|
956
|
+
// first success carrying identity content prepends the retraction so
|
|
957
|
+
// the suspension wording cannot linger. Without a sessionId there is
|
|
958
|
+
// no per-session key — inject per turn (pre-#316 shape), matching
|
|
959
|
+
// the standing-block behavior above.
|
|
960
|
+
let identityBlock;
|
|
961
|
+
if (identity !== null && identity.block !== null) {
|
|
962
|
+
identityBlock = identity.block;
|
|
963
|
+
if (sKey !== undefined && identityBannerShown.has(sKey)) {
|
|
964
|
+
identityBlock = `${IDENTITY_RESTORED_RETRACTION}\n\n${identityBlock}`;
|
|
965
|
+
}
|
|
966
|
+
if (sKey !== undefined) {
|
|
967
|
+
identityBannerShown.evict(sKey);
|
|
968
|
+
identityNoteShown.evict(sKey);
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
else if (identity !== null && identity.failed) {
|
|
972
|
+
const isSkew = identity.status === 404;
|
|
973
|
+
const shown = isSkew ? identityNoteShown : identityBannerShown;
|
|
974
|
+
if (sKey === undefined || !shown.has(sKey)) {
|
|
975
|
+
if (sKey !== undefined)
|
|
976
|
+
shown.add(sKey);
|
|
977
|
+
identityBlock = isSkew ? IDENTITY_VERSION_SKEW_NOTE : IDENTITY_UNAVAILABLE_BANNER;
|
|
978
|
+
}
|
|
979
|
+
else {
|
|
980
|
+
// Already appended earlier in the outage — it persists on the
|
|
981
|
+
// session; re-sending would duplicate it (see tracker docs).
|
|
982
|
+
identityBlock = null;
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
else {
|
|
986
|
+
// Gated-null success (old server / oc ∉ clients / mode off): a
|
|
987
|
+
// settled outcome with no content. A prior banner stays standing —
|
|
988
|
+
// identity is still effectively missing to the agent, and the
|
|
989
|
+
// bootstrap dead-man guard line keeps advising caution.
|
|
990
|
+
identityBlock = null;
|
|
991
|
+
}
|
|
877
992
|
const lessonsBlock = lessons !== null ? lessons.block : null;
|
|
878
993
|
const blocks = [identityBlock, lessonsBlock, recallBlock].filter((b) => b !== null && b !== "");
|
|
879
994
|
if (blocks.length === 0)
|
|
@@ -911,6 +1026,10 @@ exports.default = {
|
|
|
911
1026
|
void resetRecallDedup(sid, key);
|
|
912
1027
|
identityInjected.evict(key);
|
|
913
1028
|
lessonsInjected.evict(key);
|
|
1029
|
+
// #327: a rebuilt window may have dropped the notice too — evict so one
|
|
1030
|
+
// re-inject (banner while still failing) can happen after the rebuild.
|
|
1031
|
+
identityBannerShown.evict(key);
|
|
1032
|
+
identityNoteShown.evict(key);
|
|
914
1033
|
};
|
|
915
1034
|
api.on("after_compaction", recallResetHook);
|
|
916
1035
|
api.on("before_reset", recallResetHook);
|
|
@@ -1212,6 +1331,168 @@ const HICORTEX_TOOLS = [
|
|
|
1212
1331
|
"hicortex_update",
|
|
1213
1332
|
"hicortex_delete",
|
|
1214
1333
|
];
|
|
1334
|
+
/**
|
|
1335
|
+
* The OpenClaw home this plugin resolves against (#326). `HICORTEX_OC_HOME`
|
|
1336
|
+
* redirects it for tests, mirroring how HICORTEX_HOME redirects the hicortex
|
|
1337
|
+
* home (paths.ts) — one resolution shared by the workspace fallback and the
|
|
1338
|
+
* unpinned-trust warning so they can never disagree.
|
|
1339
|
+
*/
|
|
1340
|
+
function ocHomeDir() {
|
|
1341
|
+
return process.env.HICORTEX_OC_HOME ?? (0, node_path_1.join)((0, node_os_1.homedir)(), ".openclaw");
|
|
1342
|
+
}
|
|
1343
|
+
/**
|
|
1344
|
+
* Normalize a gateway-config workspace path for WRITING (#326 CR1): a bare
|
|
1345
|
+
* `~` or leading `~/` expands against the real home dir; anything still
|
|
1346
|
+
* RELATIVE afterwards is rejected (null). OpenClaw's semantics for relative
|
|
1347
|
+
* workspace values are not verifiable from the plugin, and writing under
|
|
1348
|
+
* process.cwd() would place the guard where OC never reads it — a silent
|
|
1349
|
+
* no-op safety — so the caller skips with a warning instead. Pure; no fs.
|
|
1350
|
+
*/
|
|
1351
|
+
function normalizeWorkspacePath(ws) {
|
|
1352
|
+
let p = ws;
|
|
1353
|
+
if (p === "~")
|
|
1354
|
+
p = (0, node_os_1.homedir)();
|
|
1355
|
+
else if (p.startsWith("~/"))
|
|
1356
|
+
p = (0, node_path_1.join)((0, node_os_1.homedir)(), p.slice(2));
|
|
1357
|
+
return (0, node_path_1.isAbsolute)(p) ? p : null;
|
|
1358
|
+
}
|
|
1359
|
+
/**
|
|
1360
|
+
* Fallback workspace when the gateway config names none (#326): OpenClaw's
|
|
1361
|
+
* default `<ocHome>/workspace`. Returned ONLY when a real OC install is
|
|
1362
|
+
* present (`<ocHome>/openclaw.json` exists — the same touchpoint
|
|
1363
|
+
* ensureToolsAllowed reads): outside a gateway (tests, CI, a bare import) the
|
|
1364
|
+
* plugin must not conjure `~/.openclaw` into existence just to drop a
|
|
1365
|
+
* bootstrap file.
|
|
1366
|
+
*/
|
|
1367
|
+
function fallbackOcWorkspace() {
|
|
1368
|
+
const ocHome = ocHomeDir();
|
|
1369
|
+
return (0, node_fs_1.existsSync)((0, node_path_1.join)(ocHome, "openclaw.json")) ? (0, node_path_1.join)(ocHome, "workspace") : null;
|
|
1370
|
+
}
|
|
1371
|
+
/** Per-cause warn-once for scaffold skips/failures (#326). */
|
|
1372
|
+
function warnScaffoldSkipOnce(cause, log, reason) {
|
|
1373
|
+
if (warnedScaffoldSkips.has(cause))
|
|
1374
|
+
return;
|
|
1375
|
+
warnedScaffoldSkips.add(cause);
|
|
1376
|
+
log(`[hicortex] WARNING: ${reason} — the dead-man guard line was not scaffolded.`);
|
|
1377
|
+
}
|
|
1378
|
+
/**
|
|
1379
|
+
* Scaffold the dead-man guard line into the agent workspace bootstrap (#326 —
|
|
1380
|
+
* the #313 SECONDARY layer; the primary layer is the injected
|
|
1381
|
+
* IDENTITY UNAVAILABLE banner, which needs a live plugin hook). The sentence
|
|
1382
|
+
* used to be a manual install step every public-agent setup could forget, so
|
|
1383
|
+
* the plugin maintains it itself at service start:
|
|
1384
|
+
*
|
|
1385
|
+
* - bootstrap absent → created containing ONLY the guard line
|
|
1386
|
+
* - present without the line → one-time .bak of the operator's original
|
|
1387
|
+
* BYTES, then the line appended exactly once
|
|
1388
|
+
* - present with the line → untouched (idempotent: no write, no backup)
|
|
1389
|
+
*
|
|
1390
|
+
* Deliberately conservative (CR):
|
|
1391
|
+
* - the workspace DIRECTORY is never created — OC may scaffold workspaces
|
|
1392
|
+
* from templates, and a pre-created dir could interfere; absent dir →
|
|
1393
|
+
* warn once + skip (a gateway restart after the first agent run retries)
|
|
1394
|
+
* - a non-absolute workspace path (after ~ expansion) → warn once + skip
|
|
1395
|
+
* (never write somewhere speculative like process.cwd())
|
|
1396
|
+
* - a bootstrap that is not valid UTF-8 → warn once + skip; decoding would
|
|
1397
|
+
* be lossy and rewriting the file would mangle the operator's bytes
|
|
1398
|
+
*
|
|
1399
|
+
* Fail-soft by construction: any filesystem failure (unreadable path,
|
|
1400
|
+
* permissions) warns ONCE per cause and never breaks plugin start. The
|
|
1401
|
+
* kill-switch (config `scaffoldDeadMan: false`, default on) returns before a
|
|
1402
|
+
* single fs call — no write, no file creation.
|
|
1403
|
+
*/
|
|
1404
|
+
function scaffoldDeadManGuard(opts) {
|
|
1405
|
+
if (!opts.enabled)
|
|
1406
|
+
return;
|
|
1407
|
+
const rawWorkspace = opts.workspaceDir;
|
|
1408
|
+
if (!rawWorkspace)
|
|
1409
|
+
return; // no workspace resolvable — not an OC install / no workspace key
|
|
1410
|
+
const log = opts.log;
|
|
1411
|
+
const workspaceDir = normalizeWorkspacePath(rawWorkspace);
|
|
1412
|
+
if (!workspaceDir) {
|
|
1413
|
+
warnScaffoldSkipOnce("relative-workspace", log, `workspace path "${rawWorkspace}" in the gateway config is relative — OpenClaw's ` +
|
|
1414
|
+
"resolution for it is unknown, so the plugin will not write speculatively");
|
|
1415
|
+
return;
|
|
1416
|
+
}
|
|
1417
|
+
const bootstrapPath = (0, node_path_1.join)(workspaceDir, BOOTSTRAP_FILENAME);
|
|
1418
|
+
try {
|
|
1419
|
+
// CR3: never create the workspace dir itself (template interference).
|
|
1420
|
+
if (!(0, node_fs_1.existsSync)(workspaceDir)) {
|
|
1421
|
+
warnScaffoldSkipOnce("missing-workspace-dir", log, `workspace directory ${workspaceDir} does not exist yet (OpenClaw creates it; ` +
|
|
1422
|
+
"restart the gateway after the first agent run to retry)");
|
|
1423
|
+
return;
|
|
1424
|
+
}
|
|
1425
|
+
// Read BYTES (CR2): the .bak must hold the operator's original exactly,
|
|
1426
|
+
// and an append must splice onto the original bytes, not a lossy decode.
|
|
1427
|
+
let original = null;
|
|
1428
|
+
try {
|
|
1429
|
+
original = (0, node_fs_1.readFileSync)(bootstrapPath);
|
|
1430
|
+
}
|
|
1431
|
+
catch (err) {
|
|
1432
|
+
// Only "does not exist" means "create it" — anything else (EACCES,
|
|
1433
|
+
// EISDIR, …) is a genuine failure and must reach the warn below, not
|
|
1434
|
+
// be mistaken for an absent file and overwritten.
|
|
1435
|
+
if (err.code !== "ENOENT")
|
|
1436
|
+
throw err;
|
|
1437
|
+
}
|
|
1438
|
+
if (original !== null) {
|
|
1439
|
+
const decoded = original.toString("utf-8");
|
|
1440
|
+
// Invalid UTF-8 (round-trip compare): appending would rewrite the file
|
|
1441
|
+
// with mangled bytes — leave it untouched and say so once.
|
|
1442
|
+
if (!Buffer.from(decoded, "utf-8").equals(original)) {
|
|
1443
|
+
warnScaffoldSkipOnce("invalid-utf8", log, `${bootstrapPath} is not valid UTF-8 — leaving the file untouched`);
|
|
1444
|
+
return;
|
|
1445
|
+
}
|
|
1446
|
+
if (decoded.includes(DEAD_MAN_GUARD_LINE))
|
|
1447
|
+
return;
|
|
1448
|
+
}
|
|
1449
|
+
if (original === null) {
|
|
1450
|
+
(0, node_fs_1.writeFileSync)(bootstrapPath, `${DEAD_MAN_GUARD_LINE}\n`);
|
|
1451
|
+
}
|
|
1452
|
+
else {
|
|
1453
|
+
// One-time backup of the operator's original BYTES — never churned.
|
|
1454
|
+
const bakPath = `${bootstrapPath}.bak`;
|
|
1455
|
+
if (!(0, node_fs_1.existsSync)(bakPath))
|
|
1456
|
+
(0, node_fs_1.writeFileSync)(bakPath, original);
|
|
1457
|
+
// Separator at the BYTE level (0x0A), so a no-trailing-newline file is
|
|
1458
|
+
// spliced correctly without decoding.
|
|
1459
|
+
const needsSep = original.length > 0 && original[original.length - 1] !== 0x0a;
|
|
1460
|
+
(0, node_fs_1.writeFileSync)(bootstrapPath, Buffer.concat([
|
|
1461
|
+
original,
|
|
1462
|
+
needsSep ? Buffer.from("\n", "utf-8") : Buffer.alloc(0),
|
|
1463
|
+
Buffer.from(`${DEAD_MAN_GUARD_LINE}\n`, "utf-8"),
|
|
1464
|
+
]));
|
|
1465
|
+
}
|
|
1466
|
+
log(`[hicortex] Added the dead-man identity guard line to ${bootstrapPath}`);
|
|
1467
|
+
}
|
|
1468
|
+
catch (err) {
|
|
1469
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1470
|
+
warnScaffoldSkipOnce("fs-error", log, `could not scaffold the dead-man guard line into ${bootstrapPath}: ${msg}`);
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
/**
|
|
1474
|
+
* Warn once per process when the gateway's plugin trust list is unpinned
|
|
1475
|
+
* (#326): while `plugins.allow` is absent or empty, OpenClaw auto-loads ANY
|
|
1476
|
+
* extension dropped into the plugins directory. A plugin must not pin trust
|
|
1477
|
+
* itself — a self-pinned list defeats the point of the list — so this only
|
|
1478
|
+
* WARNS with the fix. It never writes plugins.allow (or any gateway config;
|
|
1479
|
+
* ensureToolsAllowed's tools.allow edit is the one intentional config write).
|
|
1480
|
+
* Any non-empty array counts as pinned → silent.
|
|
1481
|
+
*/
|
|
1482
|
+
function warnIfPluginsUnpinned(raw, log) {
|
|
1483
|
+
if (warnedUnpinnedPlugins)
|
|
1484
|
+
return;
|
|
1485
|
+
const allow = isRecord(isRecord(raw)?.plugins)?.allow;
|
|
1486
|
+
if (Array.isArray(allow) && allow.length > 0)
|
|
1487
|
+
return;
|
|
1488
|
+
warnedUnpinnedPlugins = true;
|
|
1489
|
+
log("[hicortex] WARNING: the OpenClaw plugin trust list (plugins.allow) is not " +
|
|
1490
|
+
"pinned — any extension dropped into the plugins directory loads " +
|
|
1491
|
+
'automatically. Fix: set "plugins": { "allow": ["hicortex"] } in ' +
|
|
1492
|
+
`${(0, node_path_1.join)(ocHomeDir(), "openclaw.json")} (list every plugin you trust). See ` +
|
|
1493
|
+
"https://hicortex.gamaze.com/docs/installation.html — hicortex never " +
|
|
1494
|
+
"edits the trust list itself.");
|
|
1495
|
+
}
|
|
1215
1496
|
/**
|
|
1216
1497
|
* Ensure hicortex tools are in tools.allow so they're visible to agents
|
|
1217
1498
|
* regardless of the tools.profile setting.
|
package/dist/mcp-server.d.ts
CHANGED
|
@@ -13,10 +13,11 @@
|
|
|
13
13
|
import express from "express";
|
|
14
14
|
import type { MemorySearchResult } from "./types.js";
|
|
15
15
|
/**
|
|
16
|
-
* Resolve the request body-size limit in MB (#7). Pure —
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
16
|
+
* Resolve the request body-size limit in MB (#7, #328 item 2b). Pure —
|
|
17
|
+
* exported for tests. Precedence: HICORTEX_DISTILL_BODY_LIMIT_MB env >
|
|
18
|
+
* explicit config value > hosted-mode default (5) > self-hosted default (25,
|
|
19
|
+
* the historical fixed value → no regression). A finite positive value wins
|
|
20
|
+
* at each step; invalid/absent falls through.
|
|
20
21
|
*/
|
|
21
22
|
export declare function resolveBodyLimitMb(configVal: unknown, hostedMode: boolean): number;
|
|
22
23
|
/**
|
|
@@ -29,6 +30,26 @@ export declare function resolveBodyLimitMb(configVal: unknown, hostedMode: boole
|
|
|
29
30
|
* on top of that ordering. Exported so tests exercise the real handler.
|
|
30
31
|
*/
|
|
31
32
|
export declare function makeBodyLimitErrorHandler(limitMb: number): express.ErrorRequestHandler;
|
|
33
|
+
/**
|
|
34
|
+
* #328 item 4 (package-server half, CR-corrected ORDERING): a Content-Length
|
|
35
|
+
* pre-check that MUST be registered BEFORE express.json. Registered after the
|
|
36
|
+
* parser (the first #328 pass had it inside createAuthMiddleware, which sits
|
|
37
|
+
* after the parser) it is inert as a bounding measure — body-parser buffers
|
|
38
|
+
* the body up to its own limit BEFORE auth runs and refuses oversize itself,
|
|
39
|
+
* so the check only ever saw bodies the parser had already accepted and
|
|
40
|
+
* buffered. Registered FIRST it refuses a DECLARED-oversize body before a
|
|
41
|
+
* single byte is read and before any route/auth work, on every path. The
|
|
42
|
+
* twin check inside createAuthMiddleware (viz.ts) is kept as a belt — but the
|
|
43
|
+
* GATE here is the one that actually bounds pre-auth buffering.
|
|
44
|
+
*
|
|
45
|
+
* RESIDUAL RISK (deliberate, documented): chunked transfer-encoding sends no
|
|
46
|
+
* Content-Length, so this gate cannot see it — those requests still buffer up
|
|
47
|
+
* to the parser limit inside express.json (bounded per request, no
|
|
48
|
+
* concurrency cap here). Full pre-auth bounding lives in the hosted router's
|
|
49
|
+
* webhook path (stripe.ts); the tenant data plane trusts its bearer
|
|
50
|
+
* (self-hosted threat model) or sits behind the provider's edge (hosted).
|
|
51
|
+
*/
|
|
52
|
+
export declare function makeContentLengthGate(limitBytes: number): express.RequestHandler;
|
|
32
53
|
export declare function startServer(options?: {
|
|
33
54
|
port?: number;
|
|
34
55
|
host?: string;
|
package/dist/mcp-server.js
CHANGED
|
@@ -50,6 +50,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
50
50
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
51
51
|
exports.resolveBodyLimitMb = resolveBodyLimitMb;
|
|
52
52
|
exports.makeBodyLimitErrorHandler = makeBodyLimitErrorHandler;
|
|
53
|
+
exports.makeContentLengthGate = makeContentLengthGate;
|
|
53
54
|
exports.startServer = startServer;
|
|
54
55
|
exports.formatResults = formatResults;
|
|
55
56
|
const express_1 = __importDefault(require("express"));
|
|
@@ -410,12 +411,26 @@ function createMcpServer() {
|
|
|
410
411
|
// HTTP server with SSE transport
|
|
411
412
|
// ---------------------------------------------------------------------------
|
|
412
413
|
/**
|
|
413
|
-
*
|
|
414
|
-
*
|
|
415
|
-
*
|
|
416
|
-
* config
|
|
414
|
+
* Env override for the body limit (#328 item 2b — hosted tenant-immutable
|
|
415
|
+
* pin, same ENV-WINS pattern as HICORTEX_TOKEN_CAP in token-budget.ts). The
|
|
416
|
+
* hosted tenant's /data is tenant-writable, so a `distillBodyLimitMb` in the
|
|
417
|
+
* tenant's own config.json could raise the limit the provider intended; an
|
|
418
|
+
* env baked into the container (`-e`, provisioner-written .env) cannot be
|
|
419
|
+
* mutated by the tenant process. Self-hosted installs may also use it as an
|
|
420
|
+
* operator knob — precedence below puts it above config in every mode.
|
|
421
|
+
*/
|
|
422
|
+
const DISTILL_BODY_LIMIT_MB_ENV = "HICORTEX_DISTILL_BODY_LIMIT_MB";
|
|
423
|
+
/**
|
|
424
|
+
* Resolve the request body-size limit in MB (#7, #328 item 2b). Pure —
|
|
425
|
+
* exported for tests. Precedence: HICORTEX_DISTILL_BODY_LIMIT_MB env >
|
|
426
|
+
* explicit config value > hosted-mode default (5) > self-hosted default (25,
|
|
427
|
+
* the historical fixed value → no regression). A finite positive value wins
|
|
428
|
+
* at each step; invalid/absent falls through.
|
|
417
429
|
*/
|
|
418
430
|
function resolveBodyLimitMb(configVal, hostedMode) {
|
|
431
|
+
const envCap = Number(process.env[DISTILL_BODY_LIMIT_MB_ENV]);
|
|
432
|
+
if (Number.isFinite(envCap) && envCap > 0)
|
|
433
|
+
return envCap;
|
|
419
434
|
const cfg = Number(configVal);
|
|
420
435
|
if (Number.isFinite(cfg) && cfg > 0)
|
|
421
436
|
return cfg;
|
|
@@ -441,6 +456,36 @@ function makeBodyLimitErrorHandler(limitMb) {
|
|
|
441
456
|
next(err);
|
|
442
457
|
};
|
|
443
458
|
}
|
|
459
|
+
/**
|
|
460
|
+
* #328 item 4 (package-server half, CR-corrected ORDERING): a Content-Length
|
|
461
|
+
* pre-check that MUST be registered BEFORE express.json. Registered after the
|
|
462
|
+
* parser (the first #328 pass had it inside createAuthMiddleware, which sits
|
|
463
|
+
* after the parser) it is inert as a bounding measure — body-parser buffers
|
|
464
|
+
* the body up to its own limit BEFORE auth runs and refuses oversize itself,
|
|
465
|
+
* so the check only ever saw bodies the parser had already accepted and
|
|
466
|
+
* buffered. Registered FIRST it refuses a DECLARED-oversize body before a
|
|
467
|
+
* single byte is read and before any route/auth work, on every path. The
|
|
468
|
+
* twin check inside createAuthMiddleware (viz.ts) is kept as a belt — but the
|
|
469
|
+
* GATE here is the one that actually bounds pre-auth buffering.
|
|
470
|
+
*
|
|
471
|
+
* RESIDUAL RISK (deliberate, documented): chunked transfer-encoding sends no
|
|
472
|
+
* Content-Length, so this gate cannot see it — those requests still buffer up
|
|
473
|
+
* to the parser limit inside express.json (bounded per request, no
|
|
474
|
+
* concurrency cap here). Full pre-auth bounding lives in the hosted router's
|
|
475
|
+
* webhook path (stripe.ts); the tenant data plane trusts its bearer
|
|
476
|
+
* (self-hosted threat model) or sits behind the provider's edge (hosted).
|
|
477
|
+
*/
|
|
478
|
+
function makeContentLengthGate(limitBytes) {
|
|
479
|
+
return (req, res, next) => {
|
|
480
|
+
const declared = req.headers["content-length"];
|
|
481
|
+
const declaredNum = typeof declared === "string" ? Number(declared) : NaN;
|
|
482
|
+
if (Number.isFinite(declaredNum) && declaredNum > limitBytes) {
|
|
483
|
+
res.status(413).json({ error: "request body too large" });
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
next();
|
|
487
|
+
};
|
|
488
|
+
}
|
|
444
489
|
async function startServer(options = {}) {
|
|
445
490
|
const port = options.port ?? 8787;
|
|
446
491
|
const host = options.host ?? "0.0.0.0";
|
|
@@ -527,12 +572,20 @@ async function startServer(options = {}) {
|
|
|
527
572
|
// env (provider-set, tenant-immutable) which takes precedence. Initialised here
|
|
528
573
|
// (after stateDir + savedConfig are known) so the warn-dedup can seed from state.
|
|
529
574
|
(0, token_budget_js_1.initTokenBudget)(stateDir, savedConfig?.llmTokensPerMonth);
|
|
530
|
-
// #7: request body-size limit.
|
|
531
|
-
//
|
|
532
|
-
//
|
|
533
|
-
//
|
|
575
|
+
// #7: request body-size limit. Env (HICORTEX_DISTILL_BODY_LIMIT_MB) wins;
|
|
576
|
+
// else the config key; else 5 MB hosted / 25 MB self-hosted (the prior
|
|
577
|
+
// fixed value → no regression). Guards the OOM vector (the body is fully
|
|
578
|
+
// parsed into memory before the distiller truncates to 80K chars).
|
|
579
|
+
// Legitimate capture segments are ≤60K chars (~200KB), so this never
|
|
534
580
|
// constrains real flow — it's an abuse/backstop. Oversized → 413.
|
|
581
|
+
// #328 item 2b: in HOSTED mode the env is the provider's tenant-immutable
|
|
582
|
+
// pin — the tenant-writable /data/config.json must not be able to raise it.
|
|
535
583
|
const bodyLimitMb = resolveBodyLimitMb(savedConfig?.distillBodyLimitMb, hostedMode);
|
|
584
|
+
// Label the source truthfully (token-budget.ts pattern): only claim env
|
|
585
|
+
// when the env value was actually used (a malformed env falls through).
|
|
586
|
+
if (Number(process.env.HICORTEX_DISTILL_BODY_LIMIT_MB) === bodyLimitMb) {
|
|
587
|
+
console.log(`[hicortex] Body limit: ${bodyLimitMb} MB (HICORTEX_DISTILL_BODY_LIMIT_MB env — overrides config)`);
|
|
588
|
+
}
|
|
536
589
|
if (savedConfig?.llmBackend === "claude-cli") {
|
|
537
590
|
const claudePath = (0, llm_js_1.findClaudeBinary)();
|
|
538
591
|
if (claudePath) {
|
|
@@ -643,7 +696,9 @@ async function startServer(options = {}) {
|
|
|
643
696
|
const scoringCfg = retrieval.configureScoring(savedConfig);
|
|
644
697
|
const sessionIntentCfg = retrieval.configureSessionIntent(savedConfig);
|
|
645
698
|
console.log(`[hicortex] Recall: k=${recallCfg.searchLimit}/recent=${recallCfg.recentLimit}` +
|
|
646
|
-
`/window=${recallCfg.recentWindowDays}d/cold=${recallCfg.coldExposureSlots}
|
|
699
|
+
`/window=${recallCfg.recentWindowDays}d/cold=${recallCfg.coldExposureSlots}` +
|
|
700
|
+
`/novelty=${(0, recall_index_js_1.resolveNoveltyFloorSlots)(savedConfig?.noveltyFloorSlots, savedConfig?.recallMaxItems)}` +
|
|
701
|
+
` · ` +
|
|
647
702
|
`score sim=${scoringCfg.similarity}/str=${scoringCfg.strength}/conn=${scoringCfg.connections}` +
|
|
648
703
|
`/rec=${scoringCfg.recency}, fresh=${scoringCfg.freshnessBoostWeight}@${scoringCfg.freshnessBoostDays}d, ` +
|
|
649
704
|
`superseded×${scoringCfg.supersededDemotion}` +
|
|
@@ -657,6 +712,9 @@ async function startServer(options = {}) {
|
|
|
657
712
|
maxItems: savedConfig?.recallMaxItems,
|
|
658
713
|
minPromptLength: savedConfig?.recallMinPromptChars,
|
|
659
714
|
titleChars: savedConfig?.recallTitleChars,
|
|
715
|
+
// #324 novelty floor: slots of recallMaxItems guaranteed to the
|
|
716
|
+
// pure-prompt (unblended) search's top passing hit(s). 0 disables.
|
|
717
|
+
noveltyFloorSlots: savedConfig?.noveltyFloorSlots,
|
|
660
718
|
};
|
|
661
719
|
memoryInstructionsEnabled = savedConfig?.memoryInstructions !== false;
|
|
662
720
|
if (resolvedAgents.dropped.length > 0) {
|
|
@@ -675,6 +733,10 @@ async function startServer(options = {}) {
|
|
|
675
733
|
}
|
|
676
734
|
// Express app
|
|
677
735
|
const app = (0, express_1.default)();
|
|
736
|
+
// #328 item 4: the Content-Length pre-check MUST precede express.json (the
|
|
737
|
+
// parser buffers unauthenticated bodies up to its own limit; refusing the
|
|
738
|
+
// DECLARED oversize first bounds that). See makeContentLengthGate.
|
|
739
|
+
app.use(makeContentLengthGate(bodyLimitMb * 1024 * 1024));
|
|
678
740
|
// Raise the body limit — whole-session denoised transcripts exceed the 100 kB default.
|
|
679
741
|
app.use(express_1.default.json({ limit: `${bodyLimitMb}mb` }));
|
|
680
742
|
// #7: JSON 413 on body-limit exceed (see makeBodyLimitErrorHandler). Server-side
|
|
@@ -721,7 +783,7 @@ async function startServer(options = {}) {
|
|
|
721
783
|
// /dashboard has its own shell-exemption pattern. Gives the console one entry
|
|
722
784
|
// point: http://<host>:8787/ → /dashboard.
|
|
723
785
|
app.get("/", (_req, res) => res.redirect("/dashboard"));
|
|
724
|
-
app.use((0, viz_js_1.createAuthMiddleware)(authToken, authTokenPrevious, bypassMarkerPresent));
|
|
786
|
+
app.use((0, viz_js_1.createAuthMiddleware)(authToken, authTokenPrevious, bypassMarkerPresent, bodyLimitMb * 1024 * 1024));
|
|
725
787
|
// SSE transport management — each connection gets its own McpServer instance
|
|
726
788
|
const transports = new Map();
|
|
727
789
|
// Health endpoint — PUBLIC minimal probe. Unauthenticated (the auth
|
|
@@ -877,37 +939,23 @@ async function startServer(options = {}) {
|
|
|
877
939
|
res.status(503).json({ error: "Server not initialized" });
|
|
878
940
|
return;
|
|
879
941
|
}
|
|
942
|
+
// Client-pushed project/privacy scoping (F1) rides through to retrieval,
|
|
943
|
+
// which handles the filtered over-fetch itself. The search closure itself
|
|
944
|
+
// lives in recall-index.ts (createRecallRetrieveFn): per-request prompt
|
|
945
|
+
// embed memo (ONE embed for the blended + pure-prompt searches), the
|
|
946
|
+
// session-intent centroid blend/EMA fold via retrieval.recallQueryVector
|
|
947
|
+
// (#192 session-intent keying, 0.15.3), and the #324 pure-prompt branch
|
|
948
|
+
// that searches unblended and touches no centroid state. Extracted so the
|
|
949
|
+
// exact behavior is unit-testable without HTTP (blendQueryVector
|
|
950
|
+
// precedent); this adapter stays thin.
|
|
880
951
|
const r = await (0, recall_index_js_1.handleRecallIndex)({
|
|
881
952
|
db,
|
|
882
953
|
registry: recallRegistry,
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
// re-embed. Turn 1 (no centroid yet) and weight=0 both reduce to a
|
|
889
|
-
// pure-prompt search (the kill-switch). The centroid is updated AFTER
|
|
890
|
-
// reading the prior one — so turn 1 searches with pure prompt, then
|
|
891
|
-
// seeds the centroid for turn 2+ to blend against.
|
|
892
|
-
retrieveFn: async (query, limit, filters, sessionId) => {
|
|
893
|
-
const { weight, alpha } = retrieval.getSessionIntent();
|
|
894
|
-
const promptEmb = await (0, embedder_js_1.embed)(query);
|
|
895
|
-
// weight=0 (kill-switch): the centroid is neither read nor written.
|
|
896
|
-
const centroid = weight > 0 ? recallRegistry.getCentroid(sessionId) : undefined;
|
|
897
|
-
const queryVec = retrieval.blendQueryVector(promptEmb, centroid, weight);
|
|
898
|
-
if (weight > 0)
|
|
899
|
-
recallRegistry.updateCentroid(sessionId, promptEmb, alpha);
|
|
900
|
-
return retrieval.retrieve(db, embedder_js_1.embed, query, {
|
|
901
|
-
limit,
|
|
902
|
-
noStrengthen: true,
|
|
903
|
-
// #203: project + mission_domains are SOFT affinity (zero-boost
|
|
904
|
-
// neutral), threaded into computeScore. 0.16.x: privacy is no
|
|
905
|
-
// longer threaded (vestigial column, never filtered).
|
|
906
|
-
project: filters?.project,
|
|
907
|
-
missionDomains: filters?.mission_domains,
|
|
908
|
-
queryEmbedding: queryVec,
|
|
909
|
-
});
|
|
910
|
-
},
|
|
954
|
+
retrieveFn: (0, recall_index_js_1.createRecallRetrieveFn)({
|
|
955
|
+
db,
|
|
956
|
+
registry: recallRegistry,
|
|
957
|
+
embedFn: embedder_js_1.embed,
|
|
958
|
+
}),
|
|
911
959
|
options: recallIndexOptions,
|
|
912
960
|
}, req.body);
|
|
913
961
|
res.status(r.status).json(r.body);
|
|
@@ -1575,6 +1623,21 @@ async function startServer(options = {}) {
|
|
|
1575
1623
|
};
|
|
1576
1624
|
process.on("SIGINT", shutdown);
|
|
1577
1625
|
process.on("SIGTERM", shutdown);
|
|
1626
|
+
// #329 item 2: warm the embedder NOW, in the background. The ONNX pipeline
|
|
1627
|
+
// lazy-loads inside the first embed() (~0.5-3s cold) — without this, the
|
|
1628
|
+
// first /recall-index after every restart paid that load inside its own
|
|
1629
|
+
// latency budget and the 1s client hook failed soft (silent recall loss).
|
|
1630
|
+
// Fire-and-forget AFTER listen: never blocks boot, never fatal (a failed
|
|
1631
|
+
// warm-up just logs once; the next real embed lazy-loads as before).
|
|
1632
|
+
//
|
|
1633
|
+
// HOSTED MEMORY IMPLICATION (accepted, #329 CR finding 3): this makes the
|
|
1634
|
+
// embedding model (~150-300MB resident) load in EVERY tenant container from
|
|
1635
|
+
// boot, idle tenants included — previously an idle tenant never loaded it.
|
|
1636
|
+
// Accepted for the current single-tenant-VPS sizing: containers run under
|
|
1637
|
+
// 2g caps, and any ACTIVE tenant loaded the model on first use anyway. If
|
|
1638
|
+
// tenant density grows, revisit (e.g. warm on first authenticated request
|
|
1639
|
+
// instead of boot). Capacity math: hosted/README.md (TENANT_MEMORY_LIMIT).
|
|
1640
|
+
(0, embedder_js_1.warmEmbedder)(embedder_js_1.embed);
|
|
1578
1641
|
}
|
|
1579
1642
|
// ---------------------------------------------------------------------------
|
|
1580
1643
|
// Helpers
|
package/dist/nightly.d.ts
CHANGED
|
@@ -10,6 +10,31 @@
|
|
|
10
10
|
* Every machine (server + clients) uses the same capture path: denoise locally,
|
|
11
11
|
* POST to /distill. No local LLM required for capture; distillation is server-side.
|
|
12
12
|
*/
|
|
13
|
+
/**
|
|
14
|
+
* Discovery watermark. Normally the last-nightly timestamp; with
|
|
15
|
+
* `--recapture-window <days>` (#189 Tier-2 recovery) the window may only
|
|
16
|
+
* WIDEN — since = min(lastNightly, now−N days). Taking the earlier of the two
|
|
17
|
+
* means a machine that was offline longer than N days still re-discovers every
|
|
18
|
+
* session it missed; using now−N unconditionally would NARROW the window and
|
|
19
|
+
* skip (then, via writeLastRun, permanently lose) the 8-to-N-day-old sessions
|
|
20
|
+
* (#189 review, fix 3). Per-session cursors keep the wide re-scan cheap: an
|
|
21
|
+
* already-captured session yields an empty delta.
|
|
22
|
+
*
|
|
23
|
+
* Clock-jump clamp (#327): a FUTURE-dated lastNightly (client clock error —
|
|
24
|
+
* NTP not yet synced at write time) would, once the clock corrects, sit ahead
|
|
25
|
+
* of every session mtime and permanently skip quiet sessions (their mtimes
|
|
26
|
+
* never re-cross a future watermark). Clamped to `now` with a warn; the warn
|
|
27
|
+
* fires once per affected run (this function runs once per nightly).
|
|
28
|
+
* `now` is injectable for tests.
|
|
29
|
+
*/
|
|
30
|
+
export declare function computeSince(stateDir: string, recaptureWindowDays?: number, now?: Date): Date;
|
|
31
|
+
/**
|
|
32
|
+
* Parse a `Retry-After` header into ms (#327). Handles both RFC forms —
|
|
33
|
+
* delay-seconds (`"30"`) and HTTP-date — and returns undefined for anything
|
|
34
|
+
* unparseable (the caller then falls back to its own backoff schedule).
|
|
35
|
+
* Exported for unit tests (pure on the header value).
|
|
36
|
+
*/
|
|
37
|
+
export declare function parseRetryAfterMs(resp: Response): number | undefined;
|
|
13
38
|
export declare function runNightly(options?: {
|
|
14
39
|
dryRun?: boolean;
|
|
15
40
|
captureOnly?: boolean;
|