@gamaze/hicortex 0.16.9 → 0.17.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 +6 -3
- package/assets/dashboard.html +454 -0
- package/dist/cli.js +12 -1
- package/dist/config-read.d.ts +20 -0
- package/dist/config-read.js +84 -0
- package/dist/consolidate.d.ts +17 -1
- package/dist/consolidate.js +22 -4
- package/dist/dashboard.d.ts +162 -0
- package/dist/dashboard.js +410 -0
- package/dist/db.js +24 -0
- package/dist/hermes-transcript-reader.d.ts +15 -6
- package/dist/hermes-transcript-reader.js +32 -7
- package/dist/init.d.ts +26 -4
- package/dist/init.js +254 -55
- package/dist/mcp-server.js +21 -0
- package/dist/nightly.d.ts +8 -0
- package/dist/nightly.js +126 -12
- package/dist/telemetry.d.ts +11 -0
- package/dist/types.d.ts +29 -0
- package/dist/uninstall.js +12 -5
- package/dist/viz.d.ts +16 -0
- package/dist/viz.js +49 -0
- package/package.json +1 -1
package/dist/db.js
CHANGED
|
@@ -464,6 +464,30 @@ const MIGRATIONS = [
|
|
|
464
464
|
}
|
|
465
465
|
},
|
|
466
466
|
},
|
|
467
|
+
{
|
|
468
|
+
version: 12,
|
|
469
|
+
name: "add_dashboard_snapshots",
|
|
470
|
+
up: (db) => {
|
|
471
|
+
// #224 memory analytics dashboard. One row per FULL nightly run (never
|
|
472
|
+
// capture-only — the snapshot reflects corpus state, so it is written
|
|
473
|
+
// from the same !dryRun && !captureOnly block that runs consolidation).
|
|
474
|
+
// `run_at` is the ISO timestamp of the run (PRIMARY KEY so a re-run for
|
|
475
|
+
// the same instant is idempotent — the nightly never retries within one
|
|
476
|
+
// process, and a manual re-run uses a fresh `now`). `metrics` is a JSON
|
|
477
|
+
// blob (not typed columns): the metric set is meant to evolve without a
|
|
478
|
+
// migration (add a key, ship), so the schema stays one column. The
|
|
479
|
+
// backfill (dashboard.ts:backfillSnapshots) also writes here, one row
|
|
480
|
+
// per derived day. Adoption fields are point-in-time and can't be
|
|
481
|
+
// reconstructed from created_at — backfilled rows leave them null.
|
|
482
|
+
// IF NOT EXISTS keeps it idempotent across partial migrations.
|
|
483
|
+
db.exec(`
|
|
484
|
+
CREATE TABLE IF NOT EXISTS dashboard_snapshots (
|
|
485
|
+
run_at TEXT PRIMARY KEY,
|
|
486
|
+
metrics TEXT NOT NULL
|
|
487
|
+
)
|
|
488
|
+
`);
|
|
489
|
+
},
|
|
490
|
+
},
|
|
467
491
|
];
|
|
468
492
|
/**
|
|
469
493
|
* Run all pending migrations against the database.
|
|
@@ -14,15 +14,24 @@
|
|
|
14
14
|
* readable here nightly. No runtime plugin capture is needed; the Hermes plugin
|
|
15
15
|
* is recall-only.
|
|
16
16
|
*
|
|
17
|
-
* We process
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
17
|
+
* We process ENDED sessions (ended_at set) that ended since the last run PLUS
|
|
18
|
+
* OPEN sessions (ended_at NULL) that have messages — a Discord conversation is
|
|
19
|
+
* one long-lived thread that stays open and accrues content, so gating only on
|
|
20
|
+
* ended_at would drop the whole interactive corpus (#240). Open sessions are
|
|
21
|
+
* delta-sliced by the per-session cursor; per-session dedup stays clean
|
|
22
|
+
* (chunks are stored as `<sessionId>#<chunkIndex>`; see nightly.ts).
|
|
21
23
|
*/
|
|
22
24
|
import type { TranscriptBatch, CursorMap } from "./transcript-reader.js";
|
|
23
25
|
/**
|
|
24
|
-
* Read Hermes sessions
|
|
25
|
-
*
|
|
26
|
+
* Read Hermes sessions across all profiles: ended sessions newer than `since`
|
|
27
|
+
* (the bulk watermark) PLUS open sessions (`ended_at IS NULL`) that have any
|
|
28
|
+
* message. Returns one batch per session, parallel to readCcTranscripts().
|
|
29
|
+
*
|
|
30
|
+
* Why open sessions are included (#240): in Hermes' Discord model a
|
|
31
|
+
* conversation is one long-lived thread = one session that stays open for
|
|
32
|
+
* days/weeks and accumulates the interactive content. The per-session cursor
|
|
33
|
+
* slices each such thread to its unseen delta across runs (same discover-
|
|
34
|
+
* broadly / delta-narrowly model as CC readers).
|
|
26
35
|
*
|
|
27
36
|
* @param cursors Per-session capture cursors (#189), keyed `hermes:<profile>:<sid>`.
|
|
28
37
|
* The cursor value is the max `messages.id` already captured; a resumed +
|
|
@@ -15,10 +15,12 @@
|
|
|
15
15
|
* readable here nightly. No runtime plugin capture is needed; the Hermes plugin
|
|
16
16
|
* is recall-only.
|
|
17
17
|
*
|
|
18
|
-
* We process
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
18
|
+
* We process ENDED sessions (ended_at set) that ended since the last run PLUS
|
|
19
|
+
* OPEN sessions (ended_at NULL) that have messages — a Discord conversation is
|
|
20
|
+
* one long-lived thread that stays open and accrues content, so gating only on
|
|
21
|
+
* ended_at would drop the whole interactive corpus (#240). Open sessions are
|
|
22
|
+
* delta-sliced by the per-session cursor; per-session dedup stays clean
|
|
23
|
+
* (chunks are stored as `<sessionId>#<chunkIndex>`; see nightly.ts).
|
|
22
24
|
*/
|
|
23
25
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
24
26
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
@@ -45,8 +47,15 @@ const NON_PRIMARY_SOURCES = new Set(["cron"]);
|
|
|
45
47
|
*/
|
|
46
48
|
const NOISE_ROLES = new Set(["tool", "session_meta"]);
|
|
47
49
|
/**
|
|
48
|
-
* Read Hermes sessions
|
|
49
|
-
*
|
|
50
|
+
* Read Hermes sessions across all profiles: ended sessions newer than `since`
|
|
51
|
+
* (the bulk watermark) PLUS open sessions (`ended_at IS NULL`) that have any
|
|
52
|
+
* message. Returns one batch per session, parallel to readCcTranscripts().
|
|
53
|
+
*
|
|
54
|
+
* Why open sessions are included (#240): in Hermes' Discord model a
|
|
55
|
+
* conversation is one long-lived thread = one session that stays open for
|
|
56
|
+
* days/weeks and accumulates the interactive content. The per-session cursor
|
|
57
|
+
* slices each such thread to its unseen delta across runs (same discover-
|
|
58
|
+
* broadly / delta-narrowly model as CC readers).
|
|
50
59
|
*
|
|
51
60
|
* @param cursors Per-session capture cursors (#189), keyed `hermes:<profile>:<sid>`.
|
|
52
61
|
* The cursor value is the max `messages.id` already captured; a resumed +
|
|
@@ -64,8 +73,24 @@ function readHermesSessions(since, hermesHome = HERMES_HOME, cursors = {}) {
|
|
|
64
73
|
continue; // locked / unreadable / wrong owner — skip, retry next run
|
|
65
74
|
}
|
|
66
75
|
try {
|
|
76
|
+
// Discovery (#240): ended sessions past the watermark (bulk) PLUS open
|
|
77
|
+
// sessions (ended_at IS NULL) that have any message. In Hermes' Discord
|
|
78
|
+
// model a conversation is ONE long-lived thread = one session that stays
|
|
79
|
+
// open for days/weeks and accumulates essentially all the interactive
|
|
80
|
+
// content. Gating discovery only on `ended_at IS NOT NULL` meant those
|
|
81
|
+
// open threads were NEVER discovered → distilled — the entire interactive
|
|
82
|
+
// corpus was silently dropped. The per-session cursor below slices each
|
|
83
|
+
// open session to its unseen delta across runs (same model as CC: discover
|
|
84
|
+
// broadly, delta narrowly), and `rows.length === 0` skips an open session
|
|
85
|
+
// that is already current. The EXISTS probe is one cheap check per open
|
|
86
|
+
// session (typically one open thread per agent).
|
|
67
87
|
const sessions = db
|
|
68
|
-
.prepare(
|
|
88
|
+
.prepare(`SELECT id, ended_at, source FROM sessions
|
|
89
|
+
WHERE (ended_at IS NOT NULL AND ended_at > ?)
|
|
90
|
+
OR (ended_at IS NULL AND EXISTS (
|
|
91
|
+
SELECT 1 FROM messages WHERE messages.session_id = sessions.id
|
|
92
|
+
))
|
|
93
|
+
ORDER BY (ended_at IS NULL), ended_at`)
|
|
69
94
|
.all(sinceEpoch);
|
|
70
95
|
// Cursor is a message id (INTEGER PRIMARY KEY AUTOINCREMENT — strictly
|
|
71
96
|
// increasing, never reused), so `id > ?` returns exactly the rows added
|
package/dist/init.d.ts
CHANGED
|
@@ -295,9 +295,31 @@ export declare function runInit(options?: {
|
|
|
295
295
|
repairConfig?: boolean;
|
|
296
296
|
}): Promise<void>;
|
|
297
297
|
/**
|
|
298
|
-
* Resolve the
|
|
299
|
-
*
|
|
300
|
-
*
|
|
301
|
-
*
|
|
298
|
+
* Resolve the CONSOLIDATION hours (the only slot-based timer in 0.17). The
|
|
299
|
+
* CAPTURE mechanism is the watchdog (an interval timer, not slots) — uniform
|
|
300
|
+
* across client + server/co-located — so this function no longer returns a
|
|
301
|
+
* capture schedule. Returns null in client mode (no local DB → no
|
|
302
|
+
* consolidation timer).
|
|
303
|
+
*
|
|
304
|
+
* Priority: `consolidationHours` array → legacy `nightlyHour` (single int, only
|
|
305
|
+
* when the array key is absent → one consolidation slot at H, preserving the
|
|
306
|
+
* pre-0.17 "one daily job" intent) → role default ([10, 22] server / null client).
|
|
307
|
+
*/
|
|
308
|
+
export declare function resolveConsolidationHours(mode: "server" | "client", configDir?: string): number[] | null;
|
|
309
|
+
/**
|
|
310
|
+
* Legacy single-hour resolver (pre-0.17). Kept for backward compat + the
|
|
311
|
+
* existing tests; new scheduling goes through `resolveConsolidationHours`.
|
|
302
312
|
*/
|
|
303
313
|
export declare function resolveNightlyHour(mode: "server" | "client", configDir?: string): number;
|
|
314
|
+
/**
|
|
315
|
+
* One `OnCalendar=*-*-* HH:00:00` line per hour, newline-joined — systemd fires
|
|
316
|
+
* a timer at EACH OnCalendar entry (multi-slot in a single timer). Hours are
|
|
317
|
+
* sorted so the generated file is stable/diffable. Exported for testing.
|
|
318
|
+
*/
|
|
319
|
+
export declare function formatOnCalendarLines(hours: number[]): string;
|
|
320
|
+
/**
|
|
321
|
+
* The launchd `StartCalendarInterval` ARRAY body — one `<dict>` per hour.
|
|
322
|
+
* launchd fires the job at each dict; a single dict is the 1-slot special case
|
|
323
|
+
* but the array form is uniform across 1..N. Exported for testing.
|
|
324
|
+
*/
|
|
325
|
+
export declare function formatLaunchdIntervals(hours: number[]): string;
|
package/dist/init.js
CHANGED
|
@@ -36,7 +36,10 @@ exports.isEphemeralNpxPath = isEphemeralNpxPath;
|
|
|
36
36
|
exports.installSessionStartHook = installSessionStartHook;
|
|
37
37
|
exports.installRecallHooks = installRecallHooks;
|
|
38
38
|
exports.runInit = runInit;
|
|
39
|
+
exports.resolveConsolidationHours = resolveConsolidationHours;
|
|
39
40
|
exports.resolveNightlyHour = resolveNightlyHour;
|
|
41
|
+
exports.formatOnCalendarLines = formatOnCalendarLines;
|
|
42
|
+
exports.formatLaunchdIntervals = formatLaunchdIntervals;
|
|
40
43
|
const paths_js_1 = require("./paths.js");
|
|
41
44
|
const telemetry_js_1 = require("./telemetry.js");
|
|
42
45
|
const node_fs_1 = require("node:fs");
|
|
@@ -46,6 +49,7 @@ const node_child_process_1 = require("node:child_process");
|
|
|
46
49
|
const node_readline_1 = require("node:readline");
|
|
47
50
|
const node_crypto_1 = require("node:crypto");
|
|
48
51
|
const claude_md_js_1 = require("./claude-md.js");
|
|
52
|
+
const config_read_js_1 = require("./config-read.js");
|
|
49
53
|
const context_store_js_1 = require("./context-store.js");
|
|
50
54
|
const HICORTEX_HOME = (0, paths_js_1.hicortexHome)();
|
|
51
55
|
/** This package's version, for the install lifecycle ping (0.15.2). */
|
|
@@ -1452,10 +1456,13 @@ async function runInit(options = {}) {
|
|
|
1452
1456
|
console.log(` ✓ Agent name set to '${decision.value}'`);
|
|
1453
1457
|
}
|
|
1454
1458
|
}
|
|
1455
|
-
// Install the
|
|
1456
|
-
//
|
|
1457
|
-
//
|
|
1458
|
-
|
|
1459
|
+
// Install the scheduling timers (0.17): a CAPTURE WATCHDOG (short-interval
|
|
1460
|
+
// poll, success-cooldown-throttled — uniform with clients) + a CONSOLIDATION
|
|
1461
|
+
// timer (the full nightly, fixed slots). Re-init rewrites both; customize the
|
|
1462
|
+
// consolidation slots via consolidationHours in config.json (capture cadence
|
|
1463
|
+
// via captureCooldownHours).
|
|
1464
|
+
installCaptureWatchdogTimer();
|
|
1465
|
+
installConsolidationTimer(resolveConsolidationHours("server") ?? DEFAULT_CONSOLIDATION_HOURS);
|
|
1459
1466
|
// Install daemon if needed
|
|
1460
1467
|
if (!d.localServer && !d.remoteServer) {
|
|
1461
1468
|
installDaemon();
|
|
@@ -1657,8 +1664,12 @@ async function runClientInit(serverUrl, agentName) {
|
|
|
1657
1664
|
if ((0, claude_md_js_1.removeLessonsBlock)(claudeMdPath)) {
|
|
1658
1665
|
console.log(` ✓ Removed old static lessons block from CLAUDE.md — lessons now injected at session start`);
|
|
1659
1666
|
}
|
|
1660
|
-
// Step 7: Install
|
|
1661
|
-
|
|
1667
|
+
// Step 7: Install the CAPTURE WATCHDOG (denoise locally, POST to server
|
|
1668
|
+
// /distill, throttled + preflight-gated) — the same capture mechanism every
|
|
1669
|
+
// install gets. Clients have no local DB → no consolidation timer; also
|
|
1670
|
+
// remove any legacy full-nightly timer a pre-0.17 install left behind.
|
|
1671
|
+
installCaptureWatchdogTimer();
|
|
1672
|
+
removeConsolidationTimer();
|
|
1662
1673
|
// Step 8: Setup Hermes if detected
|
|
1663
1674
|
if ((0, node_fs_1.existsSync)(HERMES_HOME)) {
|
|
1664
1675
|
console.log("\nHermes detected — installing plugin...");
|
|
@@ -1686,59 +1697,164 @@ async function runClientInit(serverUrl, agentName) {
|
|
|
1686
1697
|
console.log("Restart your agents to activate.");
|
|
1687
1698
|
}
|
|
1688
1699
|
/**
|
|
1689
|
-
*
|
|
1690
|
-
*
|
|
1691
|
-
*
|
|
1692
|
-
*
|
|
1700
|
+
* Scheduling (0.17): one UNIFORM capture mechanism + one role-specific
|
|
1701
|
+
* consolidation timer, replacing the old single daily full-nightly.
|
|
1702
|
+
*
|
|
1703
|
+
* - CAPTURE WATCHDOG (`hicortex-capture`): a short-interval timer fires
|
|
1704
|
+
* `nightly --capture-only --watchdog`. The watchdog throttles by a
|
|
1705
|
+
* success-cooldown (`captureCooldownHours`, default 6 ≈ 4 captures/day) and
|
|
1706
|
+
* preflights before capturing, so a transient fire-instant network miss
|
|
1707
|
+
* retries in minutes, not at the next daily slot (#239: a once-daily client
|
|
1708
|
+
* fire that caught a slow/flaky link lost ~24h of capture). Installed
|
|
1709
|
+
* UNIFORMLY for client AND server/co-located — one capture code path, no
|
|
1710
|
+
* per-topology branch. A failed preflight retries on the next tick; a
|
|
1711
|
+
* successful capture waits the cooldown (success-based cooldown — the
|
|
1712
|
+
* better semantics; the custom server watchdog used trigger-based).
|
|
1713
|
+
* - CONSOLIDATION timer (`hicortex-nightly`): full `nightly` (capture +
|
|
1714
|
+
* distill + score + reflect + link). Server/co-located ONLY — clients have
|
|
1715
|
+
* no local DB, so no consolidation timer is installed (and a legacy one
|
|
1716
|
+
* left by a pre-0.17 install is removed). Fixed slots (default [10, 22]).
|
|
1717
|
+
*
|
|
1718
|
+
* Customize: `consolidationHours` (the consolidation slots) and
|
|
1719
|
+
* `captureCooldownHours` (the watchdog throttle) in config.json. Re-init
|
|
1720
|
+
* rewrites the timers to the resolved standard (the pre-0.17 "never overwrite"
|
|
1721
|
+
* contract is intentionally relaxed so the fleet adopts the standard). The
|
|
1722
|
+
* legacy single `nightlyHour` is honoured as a one-slot consolidation fallback
|
|
1723
|
+
* only when `consolidationHours` is absent (preserves "one daily job at H").
|
|
1724
|
+
*/
|
|
1725
|
+
const DEFAULT_CONSOLIDATION_HOURS = [10, 22];
|
|
1726
|
+
/**
|
|
1727
|
+
* Capture-watchdog poll interval (minutes). The capture timer fires
|
|
1728
|
+
* `nightly --capture-only --watchdog` this often; the watchdog itself throttles
|
|
1729
|
+
* by the success-cooldown (`captureCooldownHours`, read at runtime). Short so a
|
|
1730
|
+
* transient fire-instant network miss retries in minutes, not at the next daily
|
|
1731
|
+
* slot (#239). Cheap: a tick against a down server is one 5s preflight.
|
|
1732
|
+
*/
|
|
1733
|
+
const CAPTURE_WATCHDOG_INTERVAL_MIN = 20;
|
|
1734
|
+
/**
|
|
1735
|
+
* Resolve the CONSOLIDATION hours (the only slot-based timer in 0.17). The
|
|
1736
|
+
* CAPTURE mechanism is the watchdog (an interval timer, not slots) — uniform
|
|
1737
|
+
* across client + server/co-located — so this function no longer returns a
|
|
1738
|
+
* capture schedule. Returns null in client mode (no local DB → no
|
|
1739
|
+
* consolidation timer).
|
|
1740
|
+
*
|
|
1741
|
+
* Priority: `consolidationHours` array → legacy `nightlyHour` (single int, only
|
|
1742
|
+
* when the array key is absent → one consolidation slot at H, preserving the
|
|
1743
|
+
* pre-0.17 "one daily job" intent) → role default ([10, 22] server / null client).
|
|
1744
|
+
*/
|
|
1745
|
+
function resolveConsolidationHours(mode, configDir = HICORTEX_HOME) {
|
|
1746
|
+
let config = {};
|
|
1747
|
+
try {
|
|
1748
|
+
config = JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.join)(configDir, "config.json"), "utf-8"));
|
|
1749
|
+
}
|
|
1750
|
+
catch { /* no config yet — use the standard default */ }
|
|
1751
|
+
if (mode === "client")
|
|
1752
|
+
return null; // clients have no local DB → no consolidation timer
|
|
1753
|
+
const arr = (0, config_read_js_1.parseHours)(config, "consolidationHours");
|
|
1754
|
+
if (arr)
|
|
1755
|
+
return arr;
|
|
1756
|
+
const legacy = readLegacyNightlyHour(config);
|
|
1757
|
+
if (legacy !== null)
|
|
1758
|
+
return [legacy];
|
|
1759
|
+
return DEFAULT_CONSOLIDATION_HOURS;
|
|
1760
|
+
}
|
|
1761
|
+
/** Read the legacy `nightlyHour` (single int 0–23) if validly set, else null. */
|
|
1762
|
+
function readLegacyNightlyHour(config) {
|
|
1763
|
+
const h = config.nightlyHour;
|
|
1764
|
+
if (typeof h === "number" && Number.isInteger(h) && h >= 0 && h <= 23)
|
|
1765
|
+
return h;
|
|
1766
|
+
return null;
|
|
1767
|
+
}
|
|
1768
|
+
/**
|
|
1769
|
+
* Legacy single-hour resolver (pre-0.17). Kept for backward compat + the
|
|
1770
|
+
* existing tests; new scheduling goes through `resolveConsolidationHours`.
|
|
1693
1771
|
*/
|
|
1694
1772
|
function resolveNightlyHour(mode, configDir = HICORTEX_HOME) {
|
|
1695
1773
|
try {
|
|
1696
1774
|
const config = JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.join)(configDir, "config.json"), "utf-8"));
|
|
1697
|
-
const
|
|
1698
|
-
if (
|
|
1699
|
-
return
|
|
1775
|
+
const legacy = readLegacyNightlyHour(config);
|
|
1776
|
+
if (legacy !== null)
|
|
1777
|
+
return legacy;
|
|
1700
1778
|
}
|
|
1701
1779
|
catch { /* no config yet — use the default */ }
|
|
1702
1780
|
return mode === "server" ? 3 : 2;
|
|
1703
1781
|
}
|
|
1704
|
-
|
|
1782
|
+
/**
|
|
1783
|
+
* One `OnCalendar=*-*-* HH:00:00` line per hour, newline-joined — systemd fires
|
|
1784
|
+
* a timer at EACH OnCalendar entry (multi-slot in a single timer). Hours are
|
|
1785
|
+
* sorted so the generated file is stable/diffable. Exported for testing.
|
|
1786
|
+
*/
|
|
1787
|
+
function formatOnCalendarLines(hours) {
|
|
1788
|
+
return [...hours]
|
|
1789
|
+
.sort((a, b) => a - b)
|
|
1790
|
+
.map((h) => `OnCalendar=*-*-* ${String(h).padStart(2, "0")}:00:00`)
|
|
1791
|
+
.join("\n");
|
|
1792
|
+
}
|
|
1793
|
+
/**
|
|
1794
|
+
* The launchd `StartCalendarInterval` ARRAY body — one `<dict>` per hour.
|
|
1795
|
+
* launchd fires the job at each dict; a single dict is the 1-slot special case
|
|
1796
|
+
* but the array form is uniform across 1..N. Exported for testing.
|
|
1797
|
+
*/
|
|
1798
|
+
function formatLaunchdIntervals(hours) {
|
|
1799
|
+
return [...hours]
|
|
1800
|
+
.sort((a, b) => a - b)
|
|
1801
|
+
.map((h) => ` <dict>
|
|
1802
|
+
<key>Hour</key>
|
|
1803
|
+
<integer>${h}</integer>
|
|
1804
|
+
<key>Minute</key>
|
|
1805
|
+
<integer>0</integer>
|
|
1806
|
+
</dict>`)
|
|
1807
|
+
.join("\n");
|
|
1808
|
+
}
|
|
1809
|
+
/**
|
|
1810
|
+
* Write + enable one schedule unit (timer + service on Linux, plist on macOS),
|
|
1811
|
+
* multi-slot. Shared by the capture and consolidation installers. Always
|
|
1812
|
+
* rewrites both files (the 0.17 migration decision: re-init brings an install
|
|
1813
|
+
* up to the resolved standard; customization is via config keys, not hand-
|
|
1814
|
+
* edited unit files). Logs the resolved slots.
|
|
1815
|
+
*/
|
|
1816
|
+
function writeScheduleUnit(opts) {
|
|
1705
1817
|
const binaryArgs = resolveBinaryArgs();
|
|
1706
1818
|
const os = (0, node_os_1.platform)();
|
|
1707
|
-
const hh = String(hour).padStart(2, "0");
|
|
1708
1819
|
// PATH must start with the binary's own directory (see installLaunchd for rationale).
|
|
1709
1820
|
const binDir = (0, node_path_1.dirname)(binaryArgs[0]);
|
|
1710
1821
|
// One canonical nightly log path across platforms — status output, docs,
|
|
1711
1822
|
// and support instructions all reference this single location.
|
|
1712
1823
|
const logPath = (0, node_path_1.join)(HICORTEX_HOME, "nightly.log");
|
|
1824
|
+
const isInterval = typeof opts.intervalSec === "number";
|
|
1825
|
+
if (!isInterval && (!opts.hours || opts.hours.length === 0)) {
|
|
1826
|
+
throw new Error("writeScheduleUnit: provide either hours or intervalSec");
|
|
1827
|
+
}
|
|
1828
|
+
// Human-readable schedule label for the log line.
|
|
1829
|
+
const slotLabel = isInterval
|
|
1830
|
+
? `every ${Math.round(opts.intervalSec / 60)} min`
|
|
1831
|
+
: [...opts.hours].sort((a, b) => a - b).map((h) => `${String(h).padStart(2, "0")}:00`).join(", ");
|
|
1713
1832
|
if (os === "darwin") {
|
|
1714
1833
|
const plistDir = (0, node_path_1.join)((0, node_os_1.homedir)(), "Library", "LaunchAgents");
|
|
1715
|
-
const plistPath = (0, node_path_1.join)(plistDir,
|
|
1716
|
-
|
|
1717
|
-
// capture windows, quiet hours). Fresh installs only.
|
|
1718
|
-
if ((0, node_fs_1.existsSync)(plistPath)) {
|
|
1719
|
-
console.log(` ✓ Nightly cron already installed — leaving existing schedule as-is`);
|
|
1720
|
-
return;
|
|
1721
|
-
}
|
|
1722
|
-
const programArgs = [...binaryArgs, "nightly"]
|
|
1834
|
+
const plistPath = (0, node_path_1.join)(plistDir, `${opts.plistLabel}.plist`);
|
|
1835
|
+
const programArgs = [...binaryArgs, ...opts.nightlyArgs]
|
|
1723
1836
|
.map((a) => ` <string>${a}</string>`)
|
|
1724
1837
|
.join("\n");
|
|
1838
|
+
// Schedule block: StartInterval (seconds) for the watchdog poll, or
|
|
1839
|
+
// StartCalendarInterval as an ARRAY of dicts (one per hour) for slots.
|
|
1840
|
+
// RunAtLoad ONLY on the interval (watchdog) plist — so a Mac that reboots
|
|
1841
|
+
// gets a first capture tick on load (~parity with systemd's OnBootSec=2min),
|
|
1842
|
+
// not 20 min later. The cooldown gate makes a load-time fire a cheap no-op
|
|
1843
|
+
// if a capture ran recently.
|
|
1844
|
+
const scheduleBlock = isInterval
|
|
1845
|
+
? ` <key>StartInterval</key>\n <integer>${opts.intervalSec}</integer>\n <key>RunAtLoad</key>\n <true/>`
|
|
1846
|
+
: ` <key>StartCalendarInterval</key>\n <array>\n${formatLaunchdIntervals(opts.hours)}\n </array>`;
|
|
1725
1847
|
const plist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
1726
1848
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
1727
1849
|
<plist version="1.0">
|
|
1728
1850
|
<dict>
|
|
1729
1851
|
<key>Label</key>
|
|
1730
|
-
<string
|
|
1852
|
+
<string>${opts.plistLabel}</string>
|
|
1731
1853
|
<key>ProgramArguments</key>
|
|
1732
1854
|
<array>
|
|
1733
1855
|
${programArgs}
|
|
1734
1856
|
</array>
|
|
1735
|
-
|
|
1736
|
-
<dict>
|
|
1737
|
-
<key>Hour</key>
|
|
1738
|
-
<integer>${hour}</integer>
|
|
1739
|
-
<key>Minute</key>
|
|
1740
|
-
<integer>0</integer>
|
|
1741
|
-
</dict>
|
|
1857
|
+
${scheduleBlock}
|
|
1742
1858
|
<key>StandardOutPath</key>
|
|
1743
1859
|
<string>${logPath}</string>
|
|
1744
1860
|
<key>StandardErrorPath</key>
|
|
@@ -1756,63 +1872,146 @@ ${programArgs}
|
|
|
1756
1872
|
try {
|
|
1757
1873
|
(0, node_child_process_1.execSync)(`launchctl unload ${plistPath} 2>/dev/null`, { stdio: "pipe" });
|
|
1758
1874
|
}
|
|
1759
|
-
catch { }
|
|
1875
|
+
catch { /* not loaded */ }
|
|
1760
1876
|
(0, node_child_process_1.execSync)(`launchctl load ${plistPath}`, { stdio: "pipe" });
|
|
1761
|
-
console.log(` ✓ Installed
|
|
1877
|
+
console.log(` ✓ Installed ${opts.timerDesc} (${slotLabel})`);
|
|
1762
1878
|
}
|
|
1763
1879
|
catch {
|
|
1764
|
-
console.log(` ⚠ Could not load
|
|
1880
|
+
console.log(` ⚠ Could not load plist. Load manually: launchctl load ${plistPath}`);
|
|
1765
1881
|
}
|
|
1766
1882
|
}
|
|
1767
1883
|
else if (os === "linux") {
|
|
1768
1884
|
const configDir = (0, node_path_1.join)((0, node_os_1.homedir)(), ".config", "systemd", "user");
|
|
1769
|
-
const servicePath = (0, node_path_1.join)(configDir,
|
|
1770
|
-
const timerPath = (0, node_path_1.join)(configDir,
|
|
1771
|
-
const execStart = [...binaryArgs,
|
|
1885
|
+
const servicePath = (0, node_path_1.join)(configDir, `${opts.unitBase}.service`);
|
|
1886
|
+
const timerPath = (0, node_path_1.join)(configDir, `${opts.unitBase}.timer`);
|
|
1887
|
+
const execStart = [...binaryArgs, ...opts.nightlyArgs].join(" ");
|
|
1772
1888
|
// File logging, not journal: oneshot runs on machines with a volatile
|
|
1773
1889
|
// journal (e.g. Raspberry Pi defaults) otherwise fail without a trace.
|
|
1774
1890
|
// Same log path as the macOS plist. append: needs systemd ≥ 240 (2018).
|
|
1775
1891
|
const service = `[Unit]
|
|
1776
|
-
Description
|
|
1892
|
+
Description=${opts.serviceDesc}
|
|
1777
1893
|
|
|
1778
1894
|
[Service]
|
|
1779
1895
|
Type=oneshot
|
|
1780
1896
|
ExecStart=${execStart}
|
|
1781
|
-
StandardOutput=append:${logPath}
|
|
1897
|
+
${opts.timeoutMin ? `TimeoutStartSec=${opts.timeoutMin}min\n` : ""}StandardOutput=append:${logPath}
|
|
1782
1898
|
StandardError=append:${logPath}
|
|
1783
1899
|
Environment=PATH=${binDir}:/usr/local/bin:/usr/bin:/bin
|
|
1784
1900
|
Environment=HOME=${(0, node_os_1.homedir)()}
|
|
1785
1901
|
WorkingDirectory=${(0, node_os_1.homedir)()}`;
|
|
1902
|
+
// Timer body: OnUnitActiveSec (interval, watchdog) or one OnCalendar line
|
|
1903
|
+
// per hour (multi-slot). systemd ORs multiple OnCalendar entries.
|
|
1904
|
+
const timerBody = isInterval
|
|
1905
|
+
? `OnBootSec=2min\nOnUnitActiveSec=${Math.round(opts.intervalSec / 60)}min`
|
|
1906
|
+
: formatOnCalendarLines(opts.hours);
|
|
1786
1907
|
const timer = `[Unit]
|
|
1787
|
-
Description
|
|
1908
|
+
Description=${opts.timerDesc}
|
|
1788
1909
|
|
|
1789
1910
|
[Timer]
|
|
1790
|
-
|
|
1911
|
+
${timerBody}
|
|
1791
1912
|
Persistent=true
|
|
1792
1913
|
|
|
1793
1914
|
[Install]
|
|
1794
1915
|
WantedBy=timers.target`;
|
|
1795
1916
|
(0, node_fs_1.mkdirSync)(configDir, { recursive: true });
|
|
1796
|
-
//
|
|
1797
|
-
//
|
|
1798
|
-
// schedule and is never overwritten.
|
|
1917
|
+
// Always rewrite both .service and .timer (0.17 migration: re-init adopts
|
|
1918
|
+
// the resolved standard schedule; the config keys are the tuning surface).
|
|
1799
1919
|
(0, node_fs_1.writeFileSync)(servicePath, service);
|
|
1800
|
-
if ((0, node_fs_1.existsSync)(timerPath)) {
|
|
1801
|
-
try {
|
|
1802
|
-
(0, node_child_process_1.execSync)("systemctl --user daemon-reload", { stdio: "pipe" });
|
|
1803
|
-
}
|
|
1804
|
-
catch { /* fine */ }
|
|
1805
|
-
console.log(` ✓ Nightly service refreshed — existing timer schedule kept as-is`);
|
|
1806
|
-
return;
|
|
1807
|
-
}
|
|
1808
1920
|
(0, node_fs_1.writeFileSync)(timerPath, timer);
|
|
1809
1921
|
try {
|
|
1810
1922
|
(0, node_child_process_1.execSync)("systemctl --user daemon-reload", { stdio: "pipe" });
|
|
1811
|
-
(0, node_child_process_1.execSync)(
|
|
1812
|
-
console.log(` ✓ Installed
|
|
1923
|
+
(0, node_child_process_1.execSync)(`systemctl --user enable --now ${opts.unitBase}.timer`, { stdio: "pipe" });
|
|
1924
|
+
console.log(` ✓ Installed ${opts.unitBase}.timer (${slotLabel})`);
|
|
1813
1925
|
}
|
|
1814
1926
|
catch {
|
|
1815
|
-
console.log(` ⚠ Could not enable
|
|
1927
|
+
console.log(` ⚠ Could not enable ${opts.unitBase}.timer. Enable manually: ` +
|
|
1928
|
+
`systemctl --user enable --now ${opts.unitBase}.timer`);
|
|
1929
|
+
}
|
|
1930
|
+
}
|
|
1931
|
+
}
|
|
1932
|
+
/**
|
|
1933
|
+
* Install the CAPTURE WATCHDOG timer — a short-interval poll that fires
|
|
1934
|
+
* `nightly --capture-only --watchdog`. The watchdog itself throttles by the
|
|
1935
|
+
* success-cooldown (`captureCooldownHours`) and preflights, so a transient
|
|
1936
|
+
* fire-instant network miss retries in minutes, not at the next daily slot
|
|
1937
|
+
* (#239). Installed UNIFORMLY for client + server/co-located (one capture
|
|
1938
|
+
* mechanism everywhere — no per-topology branch).
|
|
1939
|
+
*/
|
|
1940
|
+
function installCaptureWatchdogTimer() {
|
|
1941
|
+
writeScheduleUnit({
|
|
1942
|
+
unitBase: "hicortex-capture",
|
|
1943
|
+
plistLabel: "com.gamaze.hicortex-capture",
|
|
1944
|
+
serviceDesc: "Hicortex Capture Watchdog (denoise + POST /distill, throttled)",
|
|
1945
|
+
timerDesc: "Hicortex Capture Watchdog",
|
|
1946
|
+
nightlyArgs: ["nightly", "--capture-only", "--watchdog"],
|
|
1947
|
+
intervalSec: CAPTURE_WATCHDOG_INTERVAL_MIN * 60,
|
|
1948
|
+
timeoutMin: 30, // backstop only — capture is no-LLM, bounded; 30min catches a stuck POST
|
|
1949
|
+
});
|
|
1950
|
+
}
|
|
1951
|
+
/**
|
|
1952
|
+
* Install the CONSOLIDATION timer (full `nightly`). Reuses the existing
|
|
1953
|
+
* `hicortex-nightly` unit name (repurposed from the pre-0.17 single full-nightly).
|
|
1954
|
+
*/
|
|
1955
|
+
function installConsolidationTimer(hours) {
|
|
1956
|
+
writeScheduleUnit({
|
|
1957
|
+
unitBase: "hicortex-nightly",
|
|
1958
|
+
plistLabel: "com.gamaze.hicortex-nightly",
|
|
1959
|
+
serviceDesc: "Hicortex Nightly (capture + consolidate)",
|
|
1960
|
+
timerDesc: "Hicortex Consolidation Timer",
|
|
1961
|
+
nightlyArgs: ["nightly"],
|
|
1962
|
+
hours,
|
|
1963
|
+
// Backstop only (NOT an operating limit) — set well above the longest
|
|
1964
|
+
// legitimate run so it catches a true hang, never a slow-but-progressing
|
|
1965
|
+
// one. ~5000 LLM calls × ~1–3s/call ≈ 1.4–4.2h → 6h clears it with margin.
|
|
1966
|
+
// Coupled to the consolidateMaxLlmCalls budget (#241): raise together.
|
|
1967
|
+
timeoutMin: 360,
|
|
1968
|
+
});
|
|
1969
|
+
}
|
|
1970
|
+
/**
|
|
1971
|
+
* Remove the consolidation timer + service (and the macOS plist). Used on
|
|
1972
|
+
* CLIENT installs: clients have no local DB, so a pre-0.17 `hicortex-nightly`
|
|
1973
|
+
* timer (which auto-capture-onlys on clients) is redundant with the new capture
|
|
1974
|
+
* timer — remove it so it doesn't double-fire.
|
|
1975
|
+
*/
|
|
1976
|
+
function removeConsolidationTimer() {
|
|
1977
|
+
const os = (0, node_os_1.platform)();
|
|
1978
|
+
if (os === "darwin") {
|
|
1979
|
+
const plistPath = (0, node_path_1.join)((0, node_os_1.homedir)(), "Library", "LaunchAgents", "com.gamaze.hicortex-nightly.plist");
|
|
1980
|
+
if ((0, node_fs_1.existsSync)(plistPath)) {
|
|
1981
|
+
try {
|
|
1982
|
+
(0, node_child_process_1.execSync)(`launchctl unload ${plistPath} 2>/dev/null`, { stdio: "pipe" });
|
|
1983
|
+
}
|
|
1984
|
+
catch { /* not loaded */ }
|
|
1985
|
+
try {
|
|
1986
|
+
(0, node_fs_1.rmSync)(plistPath);
|
|
1987
|
+
console.log(" ✓ Removed legacy nightly timer (client mode — capture-only)");
|
|
1988
|
+
}
|
|
1989
|
+
catch { /* leave it */ }
|
|
1990
|
+
}
|
|
1991
|
+
}
|
|
1992
|
+
else if (os === "linux") {
|
|
1993
|
+
try {
|
|
1994
|
+
(0, node_child_process_1.execSync)("systemctl --user disable --now hicortex-nightly.timer 2>/dev/null", { stdio: "pipe" });
|
|
1995
|
+
}
|
|
1996
|
+
catch { /* not installed */ }
|
|
1997
|
+
const unitDir = (0, node_path_1.join)((0, node_os_1.homedir)(), ".config", "systemd", "user");
|
|
1998
|
+
let removed = false;
|
|
1999
|
+
for (const name of ["hicortex-nightly.timer", "hicortex-nightly.service"]) {
|
|
2000
|
+
const p = (0, node_path_1.join)(unitDir, name);
|
|
2001
|
+
if ((0, node_fs_1.existsSync)(p)) {
|
|
2002
|
+
try {
|
|
2003
|
+
(0, node_fs_1.rmSync)(p);
|
|
2004
|
+
removed = true;
|
|
2005
|
+
}
|
|
2006
|
+
catch { /* leave it */ }
|
|
2007
|
+
}
|
|
2008
|
+
}
|
|
2009
|
+
if (removed) {
|
|
2010
|
+
try {
|
|
2011
|
+
(0, node_child_process_1.execSync)("systemctl --user daemon-reload 2>/dev/null", { stdio: "pipe" });
|
|
2012
|
+
}
|
|
2013
|
+
catch { /* fine */ }
|
|
2014
|
+
console.log(" ✓ Removed legacy nightly timer (client mode — capture-only)");
|
|
1816
2015
|
}
|
|
1817
2016
|
}
|
|
1818
2017
|
}
|
package/dist/mcp-server.js
CHANGED
|
@@ -57,11 +57,13 @@ const zod_1 = require("zod");
|
|
|
57
57
|
const db_js_1 = require("./db.js");
|
|
58
58
|
const llm_js_1 = require("./llm.js");
|
|
59
59
|
const features_js_1 = require("./features.js");
|
|
60
|
+
const config_read_js_1 = require("./config-read.js");
|
|
60
61
|
const state_js_1 = require("./state.js");
|
|
61
62
|
const embedder_js_1 = require("./embedder.js");
|
|
62
63
|
const storage = __importStar(require("./storage.js"));
|
|
63
64
|
const graph_js_1 = require("./graph.js");
|
|
64
65
|
const viz_js_1 = require("./viz.js");
|
|
66
|
+
const dashboard_js_1 = require("./dashboard.js");
|
|
65
67
|
const context_store_js_1 = require("./context-store.js");
|
|
66
68
|
const retrieval = __importStar(require("./retrieval.js"));
|
|
67
69
|
const recall_registry_js_1 = require("./recall-registry.js");
|
|
@@ -373,6 +375,9 @@ async function startServer(options = {}) {
|
|
|
373
375
|
// If nothing is configured: start recall-only with an unmissable warning.
|
|
374
376
|
// One model serves all phases (#231) — no per-tier overlay here.
|
|
375
377
|
const savedConfig = readConfigFile(stateDir);
|
|
378
|
+
// 0.16.8 upgrade guard: per-stage keys are silently ignored now. Warn loudly
|
|
379
|
+
// so a carried-over distill/reflect model doesn't silently downgrade quality.
|
|
380
|
+
(0, config_read_js_1.warnIgnoredConfigKeys)(savedConfig);
|
|
376
381
|
// 0.16.2 activation gap: self-heal the agentId provenance field for
|
|
377
382
|
// pre-0.16.2 server installs on first boot after upgrade. The server's own
|
|
378
383
|
// nightly captures its sessions to localhost:8787/distill and needs this id;
|
|
@@ -1172,6 +1177,22 @@ async function startServer(options = {}) {
|
|
|
1172
1177
|
// bypass). The page collects the token client-side: ?token= URL param
|
|
1173
1178
|
// (stripped on load) or an in-page prompt on 401, persisted in localStorage.
|
|
1174
1179
|
app.get("/context/ui", (0, viz_js_1.contextUiHandler)());
|
|
1180
|
+
// GET /dashboard — view-only memory analytics page (#224).
|
|
1181
|
+
//
|
|
1182
|
+
// Self-contained HTML (inline CSS/JS, hand-rolled inline SVG charts, zero
|
|
1183
|
+
// external requests) served from assets/dashboard.html. Fetches
|
|
1184
|
+
// /dashboard/data from its own origin. The page SHELL is public (exempted
|
|
1185
|
+
// in createAuthMiddleware, like /viz and /context/ui — it carries no data);
|
|
1186
|
+
// the /dashboard/data fetch is bearer-only (localhost bypass). The page
|
|
1187
|
+
// collects the token client-side: ?token= URL param (stripped on load) or an
|
|
1188
|
+
// in-page prompt on 401, persisted in localStorage.
|
|
1189
|
+
app.get("/dashboard", (0, viz_js_1.dashboardHandler)());
|
|
1190
|
+
// GET /dashboard/data — the metric payload (series, composition, digest).
|
|
1191
|
+
// Bearer-only (the auth middleware is installed at app boot); no separate
|
|
1192
|
+
// exemption. The handler lives in src/dashboard.ts (pure); this is the thin
|
|
1193
|
+
// express adapter that injects the live db + config. STRICTLY view-only —
|
|
1194
|
+
// no mutation endpoints on the dashboard surface.
|
|
1195
|
+
app.get("/dashboard/data", (0, dashboard_js_1.dashboardDataHandler)(() => db, () => readConfigFile(stateDir)));
|
|
1175
1196
|
// SSE endpoint — each connection gets its own McpServer + transport
|
|
1176
1197
|
app.get("/sse", async (req, res) => {
|
|
1177
1198
|
const transport = new sse_js_1.SSEServerTransport("/messages", res);
|
package/dist/nightly.d.ts
CHANGED
|
@@ -17,4 +17,12 @@ export declare function runNightly(options?: {
|
|
|
17
17
|
stateDir?: string;
|
|
18
18
|
/** #189 Tier-2 recovery: override discovery to now−N days for one run. */
|
|
19
19
|
recaptureWindowDays?: number;
|
|
20
|
+
/**
|
|
21
|
+
* Watchdog mode (0.17, #239): the capture timer fires `nightly --watchdog`
|
|
22
|
+
* on a short interval so a transient fire-instant network miss retries in
|
|
23
|
+
* minutes, not at the next daily slot. The gate throttles (success-cooldown)
|
|
24
|
+
* and preflights BEFORE capture; the watchdog never consolidates (it forces
|
|
25
|
+
* capture-only). Uniform across client + server/co-located.
|
|
26
|
+
*/
|
|
27
|
+
watchdog?: boolean;
|
|
20
28
|
}): Promise<void>;
|