@gamaze/hicortex 0.16.2 → 0.16.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -212,6 +212,9 @@ Config at `~/.hicortex/config.json`. Created by `init`. Key options:
212
212
  | `contextAgents` | Per-agent context modes (0.13): `{ "<id>": "override" \| "global" \| "off" }`. Absent + no `agents/<id>/` dir → every agent gets the global set. Boot-time (restart to apply) — see [Per-agent context](#per-agent-context-013) |
213
213
  | `agentName` | This install's per-agent context id sent as `?agent=`. **Unset by default** (CC shares the global context — no `?agent=` sent). Explicit opt-in via `init --agent-name <name>`; `init --agent-name ""` clears it. An empty/whitespace value equals unset |
214
214
  | `nightlyHour` | Local hour (0–23) for the nightly job installed by `init` (defaults: client 2, server 3). Applied on fresh installs; existing schedules are never overwritten |
215
+ | `preflightTimeoutMs` | **Client mode only.** Per-attempt timeout for the nightly's server-reachability check before it starts capturing (default: 15000 ms) |
216
+ | `preflightAttempts` | **Client mode only.** Reachability-check retries before the nightly aborts (default: 3; floored at 1). `1` = single try, no retry |
217
+ | `preflightRetryGapMs` | **Client mode only.** Delay between reachability retries (default: 60000 ms). Note: timers don't advance while the machine is asleep, so on a sleeping laptop this gap counts awake-time, not wall-clock |
215
218
  | `scoreSimilarityWeight` | Weight of semantic similarity in the ranking score (default: 0.50) |
216
219
  | `scoreStrengthWeight` | Weight of effective strength — importance/use/recency of access (default: 0.20) |
217
220
  | `scoreConnectionsWeight` | Weight of graph centrality (default: 0.15) |
package/dist/cli.js CHANGED
@@ -40,7 +40,7 @@ switch (command) {
40
40
  agentName = (0, cli_args_js_1.readValueFlag)(process.argv, "--agent-name");
41
41
  }
42
42
  catch {
43
- console.error("[hicortex] init: --agent-name requires a value, e.g. --agent-name lenovo");
43
+ console.error("[hicortex] init: --agent-name requires a value, e.g. --agent-name my-agent");
44
44
  process.exit(1);
45
45
  }
46
46
  const repairConfig = process.argv.includes("--repair-config");
@@ -832,7 +832,7 @@ function stageHubBoost(db, dryRun) {
832
832
  //
833
833
  // A later decision/correction can reverse, replace, or invalidate an earlier
834
834
  // one — e.g. "chose Ollama for distillation" superseded a month later by
835
- // "switched distillation to the MBP's 35B model over Tailscale". Left
835
+ // "switched distillation to a local 35B model over a mesh VPN". Left
836
836
  // unlinked, retrieval and lesson selection can surface the stale one. This
837
837
  // stage links OLD → NEW with relationship `superseded_by` and accelerates the
838
838
  // old memory's decay, WITHOUT deleting it (unlike `hicortex dedup`'s merge —
package/dist/nightly.js CHANGED
@@ -187,6 +187,27 @@ function captureLockWaitMs() {
187
187
  const env = Number(process.env.HICORTEX_CAPTURE_LOCK_WAIT_MS);
188
188
  return Number.isFinite(env) && env >= 0 ? env : CAPTURE_LOCK_WAIT_MS;
189
189
  }
190
+ /**
191
+ * Read a positive finite number from a nightly config key, falling back to
192
+ * `def` when the key is absent/invalid. Used by the pre-flight retry knobs
193
+ * (#163): tuning knobs live in config, never hardcoded (cf. decayHalfLifeDays,
194
+ * recallMinSimilarity).
195
+ *
196
+ * A value that is PRESENT but rejected (non-number, non-finite, or ≤ 0) warns
197
+ * — e.g. an operator who sets `preflightAttempts: 0` intending "don't retry"
198
+ * would otherwise silently get the default 3. (Single-try is
199
+ * `preflightAttempts: 1`, so there's no functional gap — this just makes the
200
+ * silent coercion visible, consistent with the fail-explicit pattern.)
201
+ */
202
+ function readPositiveConfig(config, key, def) {
203
+ const v = config[key];
204
+ if (v === undefined)
205
+ return def;
206
+ if (typeof v === "number" && Number.isFinite(v) && v > 0)
207
+ return v;
208
+ console.warn(`[hicortex] config "${key}" = ${String(v)} is not a positive finite number — using default ${def}.`);
209
+ return def;
210
+ }
190
211
  const NIGHTLY_LOG_MAX_BYTES = 1024 * 1024; // 1 MB — years of normal runs
191
212
  /**
192
213
  * Keep ~/.hicortex/nightly.log bounded. The launchd plist and systemd unit
@@ -489,16 +510,72 @@ async function runClientNightly(config, dryRun, stateDir = HICORTEX_HOME, recapt
489
510
  const authToken = config.authToken;
490
511
  console.log(`[hicortex] Client nightly starting${dryRun ? " (dry run)" : ""}`);
491
512
  console.log(`[hicortex] Server: ${serverUrl}`);
492
- // Verify server is reachable
493
- try {
494
- const resp = await fetch(`${serverUrl}/health`, { signal: AbortSignal.timeout(5000) });
495
- if (!resp.ok)
496
- throw new Error(`HTTP ${resp.status}`);
497
- const data = await resp.json();
498
- console.log(`[hicortex] Server OK: v${data.version}, ${data.memories} memories`);
513
+ // Verify server is reachable. Retry so a client waking from sleep (its
514
+ // network link not yet re-established) or a transient blip doesn't abort
515
+ // the whole run the pre-flight only needs the link back, which can take
516
+ // ~1 min after wake.
517
+ //
518
+ // Config-overridable (#163): a wired Pi vs a sleeping laptop want different
519
+ // values. Defaults: 15s per-attempt timeout, 3 attempts, 60s gap.
520
+ //
521
+ // WALL-CLOCK NOTE: setTimeout and AbortSignal.timeout do NOT advance while
522
+ // macOS is asleep, so the ~2m45s worst case (3×15s + 2×60s) is wall-clock-
523
+ // optimistic — a sleeping laptop can straddle sleep cycles and the real
524
+ // elapsed time can exceed it. Not a defect: the capture lock isn't held
525
+ // during the retry and the cursor design is dup-over-loss, so a late success
526
+ // is harmless. Just don't treat 2m45s as a hard wall-clock bound.
527
+ const PREFLIGHT_TIMEOUT_MS = readPositiveConfig(config, "preflightTimeoutMs", 15_000);
528
+ const PREFLIGHT_ATTEMPTS = Math.max(1, Math.floor(readPositiveConfig(config, "preflightAttempts", 3)));
529
+ const PREFLIGHT_RETRY_GAP_MS = readPositiveConfig(config, "preflightRetryGapMs", 60_000);
530
+ let reachable = false;
531
+ for (let attempt = 1; attempt <= PREFLIGHT_ATTEMPTS; attempt++) {
532
+ try {
533
+ const resp = await fetch(`${serverUrl}/health`, { signal: AbortSignal.timeout(PREFLIGHT_TIMEOUT_MS) });
534
+ if (!resp.ok)
535
+ throw new Error(`HTTP ${resp.status}`);
536
+ const data = await resp.json();
537
+ console.log(`[hicortex] Server OK: v${data.version}, ${data.memories} memories`);
538
+ reachable = true;
539
+ break;
540
+ }
541
+ catch (err) {
542
+ const msg = err instanceof Error ? err.message : String(err);
543
+ if (attempt < PREFLIGHT_ATTEMPTS) {
544
+ console.error(`[hicortex] Server unreachable at ${serverUrl} (attempt ${attempt}/${PREFLIGHT_ATTEMPTS}): ${msg} — retrying in ${PREFLIGHT_RETRY_GAP_MS / 1000}s`);
545
+ await new Promise((r) => setTimeout(r, PREFLIGHT_RETRY_GAP_MS));
546
+ }
547
+ else {
548
+ console.error(`[hicortex] Server unreachable at ${serverUrl} after ${PREFLIGHT_ATTEMPTS} attempts: ${msg}`);
549
+ }
550
+ }
499
551
  }
500
- catch (err) {
501
- console.error(`[hicortex] Server unreachable at ${serverUrl}: ${err instanceof Error ? err.message : String(err)}`);
552
+ if (!reachable) {
553
+ // The abort was invisible for weeks once: a plain `return` let the oneshot
554
+ // exit 0, so systemd/launchd recorded success and the capture gap went
555
+ // unnoticed. Exit non-zero so `systemctl --user status` / launchd show the
556
+ // unit failed (safe — this is a timer-driven oneshot with no Restart=, so
557
+ // no loop), and fire the telemetry ping (ok=false) so the abort is
558
+ // distinguishable from "powered off / uninstalled" in the activity aggregate.
559
+ process.exitCode = 1;
560
+ if (!dryRun && (0, telemetry_js_1.isTelemetryEnabled)(config)) {
561
+ await (0, telemetry_js_1.sendTelemetry)({
562
+ id: (0, telemetry_js_1.getTelemetryId)(stateDir),
563
+ v: VERSION,
564
+ pv: telemetry_js_1.TELEMETRY_PAYLOAD_VERSION,
565
+ event: "nightly",
566
+ mode: "client",
567
+ // `agent` deliberately OMITTED: no transcripts have been read at
568
+ // pre-flight, so the type is genuinely unknown. The admin summary
569
+ // buckets a missing agent as "?" (distinct from cc/pi/oc/mixed) —
570
+ // sending "cc" here would miscount an aborting Hermes/OC-only client
571
+ // as a cc install. The success-path ping sends the real type once
572
+ // session sources are known.
573
+ mem: 0,
574
+ lessons: 0,
575
+ sessions: 0,
576
+ ok: false,
577
+ });
578
+ }
502
579
  console.error(`[hicortex] Aborting. Will retry next run.`);
503
580
  return; // Don't update last-run so we retry
504
581
  }
@@ -19,7 +19,7 @@
19
19
  * --home-alice-projects-myagent--/
20
20
  * 2026-04-10T18-37-44-615Z_<uuid>.jsonl
21
21
  * 2026-04-11T07-51-28-282Z_<uuid>.jsonl
22
- * --home-agents-Development-MAIC--/
22
+ * --home-user-projects-ExampleApp--/
23
23
  * ...
24
24
  *
25
25
  * The encoded-cwd uses double-dash separators: /home/alice/projects/myagent
@@ -20,7 +20,7 @@
20
20
  * --home-alice-projects-myagent--/
21
21
  * 2026-04-10T18-37-44-615Z_<uuid>.jsonl
22
22
  * 2026-04-11T07-51-28-282Z_<uuid>.jsonl
23
- * --home-agents-Development-MAIC--/
23
+ * --home-user-projects-ExampleApp--/
24
24
  * ...
25
25
  *
26
26
  * The encoded-cwd uses double-dash separators: /home/alice/projects/myagent
@@ -75,8 +75,8 @@ export declare function formatIndexLine(r: MemorySearchResult & {
75
75
  * NOTE: FTS hits BYPASS the similarity floor, so raising the floor shifts
76
76
  * weight toward FTS-sourced entries. In practice FTS is currently inert on
77
77
  * real prompts — eval #3 had 0 FTS rows / 2,208 (2,203 vector + 5 graph), and
78
- * a 12-prompt live bedrock sample returned 96/96 vector — so the floor change
79
- * is safe as measured. But FTS quality is unmeasured; if FTS starts firing
78
+ * a relevance sample returned 96/96 vector — so the floor change is safe as
79
+ * measured. But FTS quality is unmeasured; if FTS starts firing
80
80
  * (e.g. as #205's fielded-BM25 retune beds in), give it its own eval.
81
81
  */
82
82
  export declare function passesRelevanceGate(r: MemorySearchResult, minSimilarity: number): boolean;
@@ -129,8 +129,8 @@ function formatIndexLine(r, maxLen = DEFAULT_TITLE_CHARS) {
129
129
  * NOTE: FTS hits BYPASS the similarity floor, so raising the floor shifts
130
130
  * weight toward FTS-sourced entries. In practice FTS is currently inert on
131
131
  * real prompts — eval #3 had 0 FTS rows / 2,208 (2,203 vector + 5 graph), and
132
- * a 12-prompt live bedrock sample returned 96/96 vector — so the floor change
133
- * is safe as measured. But FTS quality is unmeasured; if FTS starts firing
132
+ * a relevance sample returned 96/96 vector — so the floor change is safe as
133
+ * measured. But FTS quality is unmeasured; if FTS starts firing
134
134
  * (e.g. as #205's fielded-BM25 retune beds in), give it its own eval.
135
135
  */
136
136
  function passesRelevanceGate(r, minSimilarity) {
package/dist/redact.d.ts CHANGED
@@ -5,8 +5,8 @@
5
5
  * Why this exists:
6
6
  * - Session transcripts contain tool output: file reads, command output,
7
7
  * env var dumps. These regularly contain API keys, tokens, and paths.
8
- * - The distillation LLM is often remote (e.g., Ollama on MBP via
9
- * Tailscale). Secrets in the transcript travel over the network.
8
+ * - The distillation LLM is often remote (e.g., a mesh VPN link to a
9
+ * GPU box). Secrets in the transcript travel over the network.
10
10
  * - Even if the LLM correctly classifies the memory as SENSITIVE, the
11
11
  * secret is already stored and searchable via hicortex_search.
12
12
  * - Redaction runs BEFORE the LLM sees the text, eliminating the risk.
package/dist/redact.js CHANGED
@@ -6,8 +6,8 @@
6
6
  * Why this exists:
7
7
  * - Session transcripts contain tool output: file reads, command output,
8
8
  * env var dumps. These regularly contain API keys, tokens, and paths.
9
- * - The distillation LLM is often remote (e.g., Ollama on MBP via
10
- * Tailscale). Secrets in the transcript travel over the network.
9
+ * - The distillation LLM is often remote (e.g., a mesh VPN link to a
10
+ * GPU box). Secrets in the transcript travel over the network.
11
11
  * - Even if the LLM correctly classifies the memory as SENSITIVE, the
12
12
  * secret is already stored and searchable via hicortex_search.
13
13
  * - Redaction runs BEFORE the LLM sees the text, eliminating the risk.
@@ -6,7 +6,11 @@
6
6
  * v — package version
7
7
  * pv — payload schema version
8
8
  * mode — server or client
9
- * agent — cc, pi, oc, or mixed (detected from session sources)
9
+ * agent — cc, pi, oc, or mixed (detected from session sources); OMITTED
10
+ * when the agent type is genuinely unknown (pre-flight abort — no
11
+ * transcripts read yet). The admin summary buckets a missing agent
12
+ * as "?", distinct from any real type, so an aborting Hermes/OC
13
+ * client is never miscounted as "cc".
10
14
  * mem — total memory count
11
15
  * lessons — total lesson count
12
16
  * sessions — sessions distilled this run
@@ -41,7 +45,14 @@ export interface TelemetryPayload {
41
45
  id: string;
42
46
  v: string;
43
47
  mode: string;
44
- agent: string;
48
+ /**
49
+ * Agent type detected from session sources (cc/pi/oc/mixed). OMITTED when
50
+ * unknown — currently only the pre-flight abort path, where no transcripts
51
+ * have been read yet (sending "cc" there mislabelled aborting Hermes/OC
52
+ * clients in the admin aggregate). The admin summary buckets a missing agent
53
+ * as "?", which is the honest signal.
54
+ */
55
+ agent?: string;
45
56
  mem: number;
46
57
  lessons: number;
47
58
  sessions: number;
package/dist/telemetry.js CHANGED
@@ -7,7 +7,11 @@
7
7
  * v — package version
8
8
  * pv — payload schema version
9
9
  * mode — server or client
10
- * agent — cc, pi, oc, or mixed (detected from session sources)
10
+ * agent — cc, pi, oc, or mixed (detected from session sources); OMITTED
11
+ * when the agent type is genuinely unknown (pre-flight abort — no
12
+ * transcripts read yet). The admin summary buckets a missing agent
13
+ * as "?", distinct from any real type, so an aborting Hermes/OC
14
+ * client is never miscounted as "cc".
11
15
  * mem — total memory count
12
16
  * lessons — total lesson count
13
17
  * sessions — sessions distilled this run
package/dist/types.d.ts CHANGED
@@ -54,7 +54,7 @@ export interface MemorySearchResult {
54
54
  access_count: number;
55
55
  memory_type: string;
56
56
  project: string | null;
57
- /** Origin agent (e.g. "hermes/atlas", "cc/mbp5") — surfaced in the recall
57
+ /** Origin agent (e.g. "hermes/profile-name", "cc/machine-name") — surfaced in the recall
58
58
  * one-liner so agents can calibrate trust (#202 provenance). Optional on the
59
59
  * result type (matches how `domain` is threaded) to avoid breaking fixtures. */
60
60
  source_agent?: string | null;
@@ -260,6 +260,28 @@ export interface HicortexConfig {
260
260
  * near its lower tail). See domains.example.json for a worked example.
261
261
  */
262
262
  weakPrimaryFloor?: number;
263
+ /**
264
+ * Per-attempt timeout (ms) for the CLIENT nightly's pre-flight GET /health
265
+ * check before capturing (#163). Default 15000. Overridable per machine —
266
+ * a wired Pi vs a sleeping laptop want different values. See runClientNightly
267
+ * in nightly.ts. No effect in server mode (server capture is localhost).
268
+ */
269
+ preflightTimeoutMs?: number;
270
+ /**
271
+ * Max attempts for the client nightly's pre-flight /health retry loop (#163).
272
+ * Default 3. Attempts are spaced preflightRetryGapMs apart; on exhaustion the
273
+ * run aborts with a non-zero exit code and an ok=false telemetry ping so the
274
+ * failure is visible to systemd/launchd and the activity aggregate.
275
+ */
276
+ preflightAttempts?: number;
277
+ /**
278
+ * Gap (ms) between pre-flight /health attempts in the client nightly (#163).
279
+ * Default 60000. Wall-clock-optimistic on a sleeping laptop — setTimeout does
280
+ * NOT advance while macOS is asleep, so real elapsed time can exceed the
281
+ * nominal worst case. Not a defect (capture lock isn't held; cursor design is
282
+ * dup-over-loss); just don't treat the nominal sum as a hard bound.
283
+ */
284
+ preflightRetryGapMs?: number;
263
285
  }
264
286
  /** A config-owned life-sphere domain (see HicortexConfig.domains). */
265
287
  export interface DomainDef {
@@ -27,7 +27,7 @@ CONFIG_SCHEMA: list[dict[str, Any]] = [
27
27
  "description": (
28
28
  "URL of the Hicortex memory server. On the server host use "
29
29
  "http://localhost:8787; on other machines use the server's "
30
- "Tailscale hostname, e.g. http://memory-server:8787."
30
+ "private hostname, e.g. http://memory-server:8787."
31
31
  ),
32
32
  "default": "http://localhost:8787",
33
33
  "required": True,
@@ -210,7 +210,8 @@ class HicortexProvider(MemoryProvider):
210
210
  self._recall_limit = 5
211
211
  self._privacy = cfg.get("privacy_filter", "WORK,PERSONAL")
212
212
  # #203 scope: declared knowledge domains for this role-bound agent
213
- # (e.g. Lenny → Health). Soft affinity boost on recall; never excludes.
213
+ # (e.g. a health-focused agent → Health). Soft affinity boost on
214
+ # recall; never excludes.
214
215
  _md_raw = cfg.get("mission_domains") or ""
215
216
  self._mission_domains = [d.strip() for d in _md_raw.split(",") if d.strip()]
216
217
  self._agent_name = _resolve_agent_name(cfg)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gamaze/hicortex",
3
- "version": "0.16.2",
3
+ "version": "0.16.3",
4
4
  "description": "Self-learning memory for AI agents — experience captured automatically, distilled into lessons overnight, shared across your whole fleet. Works with Hermes, OpenClaw, Claude Code, and Pi.",
5
5
  "main": "dist/index.js",
6
6
  "bin": {