@gamaze/hicortex 0.23.2 → 0.24.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/assets/dashboard.html +39 -0
- package/dist/calibration.d.ts +31 -0
- package/dist/calibration.js +38 -1
- package/dist/capture.d.ts +7 -0
- package/dist/capture.js +10 -1
- package/dist/dashboard.d.ts +14 -0
- package/dist/dashboard.js +8 -0
- package/dist/db.js +45 -0
- package/dist/distill-queue.d.ts +203 -0
- package/dist/distill-queue.js +440 -0
- package/dist/health.d.ts +13 -1
- package/dist/health.js +6 -1
- package/dist/hosted-boot.d.ts +1 -1
- package/dist/localhost-bypass.js +1 -1
- package/dist/mcp-server.js +86 -5
- package/dist/nightly.js +87 -0
- package/dist/nofit.d.ts +1 -1
- package/dist/nofit.js +1 -1
- package/dist/schema-prototypes.d.ts +1 -1
- package/dist/schema-prototypes.js +1 -1
- package/dist/status.d.ts +11 -0
- package/dist/status.js +25 -0
- package/dist/types.d.ts +20 -0
- package/hermes-plugin/hicortex/README.md +13 -6
- package/hermes-plugin/hicortex/__init__.py +7 -0
- package/hermes-plugin/hicortex/client.py +64 -2
- package/hermes-plugin/hicortex/plugin.yaml +1 -1
- package/hermes-plugin/hicortex/provider.py +1 -1
- package/package.json +1 -1
- package/server.json +2 -2
package/dist/nightly.js
CHANGED
|
@@ -77,6 +77,8 @@ const state_js_1 = require("./state.js");
|
|
|
77
77
|
const identity_store_js_1 = require("./identity-store.js");
|
|
78
78
|
const capture_cursors_js_1 = require("./capture-cursors.js");
|
|
79
79
|
const capture_js_1 = require("./capture.js");
|
|
80
|
+
const distill_queue_js_1 = require("./distill-queue.js");
|
|
81
|
+
const token_budget_js_1 = require("./token-budget.js");
|
|
80
82
|
const run_deadline_js_1 = require("./run-deadline.js");
|
|
81
83
|
const dashboard_js_1 = require("./dashboard.js");
|
|
82
84
|
const recall_precision_js_1 = require("./recall-precision.js");
|
|
@@ -224,6 +226,43 @@ function makeRemotePost(serverUrl, authToken, deadline) {
|
|
|
224
226
|
return normalizePostResult(resp);
|
|
225
227
|
};
|
|
226
228
|
}
|
|
229
|
+
/**
|
|
230
|
+
* #529: the REAL yield-signal probe (the drain's is injected so the stage is
|
|
231
|
+
* unit-testable without HTTP). GETs the configured URL with a short timeout
|
|
232
|
+
* and classifies: busy = 2xx AND (a JSON body with a truthy `busy` field OR
|
|
233
|
+
* the plain body "busy"); a non-2xx answer means the endpoint ANSWERED and is
|
|
234
|
+
* simply not reporting busy → idle; a network error/timeout → unreachable
|
|
235
|
+
* (the drain proceeds fail-open and warns once). The URL lives in the nightly
|
|
236
|
+
* (not distill-queue.ts) so the drain module stays fetch-free.
|
|
237
|
+
*/
|
|
238
|
+
function makeDrainYieldProbe() {
|
|
239
|
+
return async (url) => {
|
|
240
|
+
try {
|
|
241
|
+
const resp = await fetch(url, {
|
|
242
|
+
signal: AbortSignal.timeout(calibration_js_1.DRAIN_YIELD_PROBE_TIMEOUT_MS),
|
|
243
|
+
});
|
|
244
|
+
if (!resp.ok)
|
|
245
|
+
return "idle";
|
|
246
|
+
const body = (await resp.text()).trim();
|
|
247
|
+
try {
|
|
248
|
+
const parsed = JSON.parse(body);
|
|
249
|
+
if (typeof parsed === "object" &&
|
|
250
|
+
parsed !== null &&
|
|
251
|
+
"busy" in parsed &&
|
|
252
|
+
Boolean(parsed.busy)) {
|
|
253
|
+
return "busy";
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
catch {
|
|
257
|
+
// Not JSON — the plain-body form below is the whole remaining check.
|
|
258
|
+
}
|
|
259
|
+
return body.toLowerCase() === "busy" ? "busy" : "idle";
|
|
260
|
+
}
|
|
261
|
+
catch {
|
|
262
|
+
return "unreachable";
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
}
|
|
227
266
|
/**
|
|
228
267
|
* Per-POST timeout: 20 min (synchronous distillation of a large segment can
|
|
229
268
|
* take minutes), clamped to the run deadline's remaining time when one is in
|
|
@@ -244,6 +283,9 @@ async function normalizePostResult(resp) {
|
|
|
244
283
|
status: 201,
|
|
245
284
|
distilled: data.distilled ?? 0,
|
|
246
285
|
dropped: data.dropped ?? [],
|
|
286
|
+
// #529: a queue-mode server confirms durable storage, not distillation
|
|
287
|
+
// — the client only swaps its log line on this flag.
|
|
288
|
+
queued: data.queued === true,
|
|
247
289
|
...(usage ? { usage } : {}),
|
|
248
290
|
};
|
|
249
291
|
}
|
|
@@ -665,6 +707,51 @@ async function runNightly(options = {}) {
|
|
|
665
707
|
}
|
|
666
708
|
}
|
|
667
709
|
} // end capture block (consolidateOnly else)
|
|
710
|
+
// Step 2.5 (#529): drain the distill inbox — distill everything the
|
|
711
|
+
// daemon queued since the last run, BEFORE consolidation so drained
|
|
712
|
+
// memories are scored/linked/reflected in the SAME run. Full AND
|
|
713
|
+
// consolidate-only runs drain (the same gate as consolidation — hosted
|
|
714
|
+
// tenants deliver through their daemon too); capture-only/watchdog and
|
|
715
|
+
// dry-run never do (compute stays scheduled; capture-only keeps its
|
|
716
|
+
// no-LLM contract). All LLM goes through the one LlmClient (ladder,
|
|
717
|
+
// breaker, single-flight, dispatcher); token gate + run deadline are
|
|
718
|
+
// checked between items; the optional yield signal (drainYieldUrl) waits
|
|
719
|
+
// out interactive use of the endpoint. Runs even when capture above was
|
|
720
|
+
// skipped (lock wait / consolidate-only) — the inbox is independent of
|
|
721
|
+
// this machine's capture.
|
|
722
|
+
if (!dryRun && !captureOnly) {
|
|
723
|
+
const drainReport = await (0, distill_queue_js_1.drainDistillQueue)(db, {
|
|
724
|
+
llm,
|
|
725
|
+
llmConfig,
|
|
726
|
+
embed: embedder_js_1.embed,
|
|
727
|
+
budgetExceeded: () => (0, token_budget_js_1.isTokenBudgetExceeded)(stateDir),
|
|
728
|
+
deadline,
|
|
729
|
+
yieldUrl: (0, config_read_js_1.readStringConfig)(savedConfig ?? {}, "drainYieldUrl") ?? undefined,
|
|
730
|
+
probeBusy: makeDrainYieldProbe(),
|
|
731
|
+
});
|
|
732
|
+
// #5 metering, same as the daemon's sync path: record tokens against
|
|
733
|
+
// the monthly budget (incl. a failed item's partial usage) and fold
|
|
734
|
+
// them into the snapshot's distill share so new_this_run.tokens stays
|
|
735
|
+
// the run's TRUE total.
|
|
736
|
+
if (drainReport.usage.total > 0) {
|
|
737
|
+
(0, token_budget_js_1.recordDistillUsage)(stateDir, drainReport.usage);
|
|
738
|
+
distillUsage = distillUsage
|
|
739
|
+
? {
|
|
740
|
+
prompt: distillUsage.prompt + drainReport.usage.prompt,
|
|
741
|
+
completion: distillUsage.completion + drainReport.usage.completion,
|
|
742
|
+
total: distillUsage.total + drainReport.usage.total,
|
|
743
|
+
}
|
|
744
|
+
: drainReport.usage;
|
|
745
|
+
}
|
|
746
|
+
if (drainReport.outcome !== "empty" && drainReport.outcome !== "completed") {
|
|
747
|
+
// A non-clean drain is a health signal, not a run failure: processed
|
|
748
|
+
// items are durable and the rest retries next scheduled run. Make it
|
|
749
|
+
// visible next to the capture-complete line so the queue-depth/age
|
|
750
|
+
// warning in status has a log-side counterpart.
|
|
751
|
+
console.warn(`[hicortex] Distill drain ${drainReport.outcome}: ` +
|
|
752
|
+
`${drainReport.processed} processed, ${drainReport.remaining} still queued`);
|
|
753
|
+
}
|
|
754
|
+
}
|
|
668
755
|
// Step 3: Consolidation — skipped in capture-only mode, dry-run, or no LLM.
|
|
669
756
|
// Runs even if capture had transient failures (opens DB directly, independent
|
|
670
757
|
// of the HTTP capture path). Full nightly only — capture-only runs are
|
package/dist/nofit.d.ts
CHANGED
package/dist/nofit.js
CHANGED
package/dist/status.d.ts
CHANGED
|
@@ -18,4 +18,15 @@ export declare function statusAgentLine(config: Record<string, unknown>): string
|
|
|
18
18
|
* the full status printer. Unknown keys pass through verbatim (forward-compat).
|
|
19
19
|
*/
|
|
20
20
|
export declare function formatTypeBreakdown(byType: Record<string, number>): string;
|
|
21
|
+
/**
|
|
22
|
+
* The distill-inbox lines for `hicortex status` (#529). Pure on the
|
|
23
|
+
* {@link readQueueStats} shape so the rendering (incl. the 24 h stale-item
|
|
24
|
+
* warning, DRAIN_QUEUE_WARN_AGE_HOURS) is unit-testable without the full
|
|
25
|
+
* status printer. An empty inbox prints nothing — depth 0 is the healthy
|
|
26
|
+
* steady state, not worth a line.
|
|
27
|
+
*/
|
|
28
|
+
export declare function formatDistillQueueLines(stats: {
|
|
29
|
+
depth: number;
|
|
30
|
+
oldest_age_hours: number | null;
|
|
31
|
+
}): string[];
|
|
21
32
|
export declare function runStatus(): Promise<void>;
|
package/dist/status.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.statusAgentLine = statusAgentLine;
|
|
7
7
|
exports.formatTypeBreakdown = formatTypeBreakdown;
|
|
8
|
+
exports.formatDistillQueueLines = formatDistillQueueLines;
|
|
8
9
|
exports.runStatus = runStatus;
|
|
9
10
|
const paths_js_1 = require("./paths.js");
|
|
10
11
|
const node_fs_1 = require("node:fs");
|
|
@@ -16,6 +17,8 @@ const features_js_1 = require("./features.js");
|
|
|
16
17
|
const state_js_1 = require("./state.js");
|
|
17
18
|
const identity_store_js_1 = require("./identity-store.js");
|
|
18
19
|
const type_labels_js_1 = require("./type-labels.js");
|
|
20
|
+
const distill_queue_js_1 = require("./distill-queue.js");
|
|
21
|
+
const calibration_js_1 = require("./calibration.js");
|
|
19
22
|
const HICORTEX_HOME = (0, paths_js_1.hicortexHome)();
|
|
20
23
|
const CC_SETTINGS = (0, node_path_1.join)((0, node_os_1.homedir)(), ".claude", "settings.json");
|
|
21
24
|
const OC_CONFIG = (0, node_path_1.join)((0, node_os_1.homedir)(), ".openclaw", "openclaw.json");
|
|
@@ -50,6 +53,24 @@ function formatTypeBreakdown(byType) {
|
|
|
50
53
|
.map(([k, v]) => `${(0, type_labels_js_1.labelForType)(k)}=${v}`)
|
|
51
54
|
.join(", ");
|
|
52
55
|
}
|
|
56
|
+
/**
|
|
57
|
+
* The distill-inbox lines for `hicortex status` (#529). Pure on the
|
|
58
|
+
* {@link readQueueStats} shape so the rendering (incl. the 24 h stale-item
|
|
59
|
+
* warning, DRAIN_QUEUE_WARN_AGE_HOURS) is unit-testable without the full
|
|
60
|
+
* status printer. An empty inbox prints nothing — depth 0 is the healthy
|
|
61
|
+
* steady state, not worth a line.
|
|
62
|
+
*/
|
|
63
|
+
function formatDistillQueueLines(stats) {
|
|
64
|
+
if (stats.depth === 0)
|
|
65
|
+
return [];
|
|
66
|
+
const age = stats.oldest_age_hours ?? 0;
|
|
67
|
+
const lines = [`Distill queue: ${stats.depth} pending (oldest ${age.toFixed(1)}h)`];
|
|
68
|
+
if (age >= calibration_js_1.DRAIN_QUEUE_WARN_AGE_HOURS) {
|
|
69
|
+
lines.push(` ⚠ Distill inbox oldest item is ${age.toFixed(1)}h old (>${calibration_js_1.DRAIN_QUEUE_WARN_AGE_HOURS}h) — ` +
|
|
70
|
+
`check the drain outcome in the last nightly log`);
|
|
71
|
+
}
|
|
72
|
+
return lines;
|
|
73
|
+
}
|
|
53
74
|
async function runStatus() {
|
|
54
75
|
console.log("Hicortex Status");
|
|
55
76
|
console.log("─".repeat(40));
|
|
@@ -66,6 +87,10 @@ async function runStatus() {
|
|
|
66
87
|
console.log(`Memories: ${stats.memories} (${typeStr || "none"})`);
|
|
67
88
|
console.log(`Links: ${stats.links}`);
|
|
68
89
|
console.log(`DB size: ${(stats.db_size_bytes / 1024).toFixed(1)} KB`);
|
|
90
|
+
// #529: inbox visibility — depth + oldest-item age, warn above 24 h.
|
|
91
|
+
for (const line of formatDistillQueueLines((0, distill_queue_js_1.readQueueStats)(db))) {
|
|
92
|
+
console.log(line);
|
|
93
|
+
}
|
|
69
94
|
// 0.21 migration detection (#425): pre-0.21 stores have inflated importance
|
|
70
95
|
// scores (median ~0.80 vs the honest ~0.40). If the live median is high,
|
|
71
96
|
// recommend the one-shot rescore.
|
package/dist/types.d.ts
CHANGED
|
@@ -642,6 +642,26 @@ export interface HicortexConfig {
|
|
|
642
642
|
* probe per window. Nightly runs are single-shot and never cache.
|
|
643
643
|
*/
|
|
644
644
|
llmProbeTtlMs?: number;
|
|
645
|
+
/**
|
|
646
|
+
* #529 kill switch for the durable distill inbox. Default TRUE (queue
|
|
647
|
+
* mode): POST /distill stores the redacted segment durably and answers the
|
|
648
|
+
* SAME 201 shape with zeroed counts + `queued: true` — no LLM call — and
|
|
649
|
+
* the nightly's drain stage distills the inbox before consolidation, so
|
|
650
|
+
* all distill LLM traffic happens inside the scheduled runs. `false`
|
|
651
|
+
* restores synchronous distill-on-POST byte-for-byte (every delivery
|
|
652
|
+
* lands in the sync flow). Read through readStrictBoolean — a non-boolean
|
|
653
|
+
* value is ignored (warned), never coerced.
|
|
654
|
+
*/
|
|
655
|
+
distillQueue?: boolean;
|
|
656
|
+
/**
|
|
657
|
+
* #529 optional interactive-yield signal: a URL the drain polls BETWEEN
|
|
658
|
+
* items. A 2xx answer whose body is JSON with a truthy `busy` field — or
|
|
659
|
+
* the plain text `busy` — makes the drain wait and re-check (bounded by
|
|
660
|
+
* the run deadline and a per-item cap); unset or unreachable proceeds
|
|
661
|
+
* fail-open (one warn per run). Seam only for now — the drain side ships
|
|
662
|
+
* with this contract, endpoints can adopt it whenever.
|
|
663
|
+
*/
|
|
664
|
+
drainYieldUrl?: string;
|
|
645
665
|
/**
|
|
646
666
|
* Max lessons injected into an agent's session-start context (default 10).
|
|
647
667
|
* Lessons are ranked per-session by project/domain affinity + recency +
|
|
@@ -2,11 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
> **Install:** `hermes plugins install gamaze-labs/hicortex-hermes-plugin` → `hermes memory setup hicortex` → restart your gateway.
|
|
4
4
|
>
|
|
5
|
-
> The [gamaze-labs/hicortex-hermes-plugin](https://github.com/gamaze-labs/hicortex-hermes-plugin) repo is a **generated read-only mirror** of `hermes-plugin/hicortex/` in the main Hicortex repo — do not open PRs there. Requires a running [Hicortex server](https://hicortex.gamaze.com/docs/installation.html) (local or remote) for recall; capture of Hermes sessions is
|
|
5
|
+
> The [gamaze-labs/hicortex-hermes-plugin](https://github.com/gamaze-labs/hicortex-hermes-plugin) repo is a **generated read-only mirror** of `hermes-plugin/hicortex/` in the main Hicortex repo — do not open PRs there. Requires a running [Hicortex server](https://hicortex.gamaze.com/docs/installation.html) (local or remote) for recall; capture of Hermes sessions is the nightly job's, not this plugin's (see [Data flow](#data-flow)).
|
|
6
6
|
|
|
7
|
-
Gives [Hermes](https://github.com/nousresearch/hermes-agent) agents self-learning memory backed by a [Hicortex](https://hicortex.gamaze.com/) server: their experience is distilled into lessons overnight, and they wake up wiser. **Recall-only:** the plugin
|
|
7
|
+
Gives [Hermes](https://github.com/nousresearch/hermes-agent) agents self-learning memory backed by a [Hicortex](https://hicortex.gamaze.com/) server: their experience is distilled into lessons overnight, and they wake up wiser. **Recall-only:** each turn the plugin sends the user's message to the configured server and injects the returned recall index, plus distilled lessons into the system prompt. It has **no local LLM, no capture, no cron** — it is a thin recall shim.
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
## Data flow
|
|
10
|
+
|
|
11
|
+
Both directions of the wire, stated plainly:
|
|
12
|
+
|
|
13
|
+
- **Recall — every turn.** Each user message is sent to your configured Hicortex server for recall (`POST /recall-index`, or `GET /search` on pre-0.14 servers), and session distillation runs on the server. The server therefore sees prompt text as it arrives — point `hicortex_url` at a server you trust (default `http://localhost:8787`).
|
|
14
|
+
- **Capture — nightly.** Session logs are read locally from each machine's own Hermes store (Hermes keeps full history in `~/.hermes/profiles/<agent>/state.db`); the nightly ships only denoised text to the server, and the server stores distilled memories — raw session logs stay on the capturing machine. Nothing is captured in real time.
|
|
15
|
+
|
|
16
|
+
The plugin warns once at startup when `hicortex_url` is plain `http://` on a non-loopback host while an auth token is set — credentials and prompts then cross the network in cleartext. Use `https://`, or keep the server on a trusted private network (plain http over a private/overlay network is a legitimate setup; the warning is advisory, not a rejection). The HTTP client also strips the `Authorization` header on any redirect that leaves the original host.
|
|
10
17
|
|
|
11
18
|
## How it works
|
|
12
19
|
|
|
@@ -26,9 +33,9 @@ Instead of injecting full memory content every turn, `prefetch` sends the user's
|
|
|
26
33
|
|
|
27
34
|
### Per-agent standing context (0.13)
|
|
28
35
|
|
|
29
|
-
`system_prompt_block()` also injects the hand-edited **standing context layer** (`##
|
|
36
|
+
`system_prompt_block()` also injects the hand-edited **standing context layer** (`## Identity`, above the lessons block) — "who you are + how to work", distinct from episodic memory. The server resolves it **per agent**: this profile's own sections override the global set (`override`), or it can be `global` or `off`. See the main repo's `/context` layer docs.
|
|
30
37
|
|
|
31
|
-
> **Note (#264 rename):** the server-side layer was renamed Context → Identity in 0.18. The `/context` endpoint remains as an alias so this plugin keeps working unchanged
|
|
38
|
+
> **Note (#264 rename):** the server-side layer was renamed Context → Identity in 0.18. The `/context` endpoint remains as an alias so this plugin keeps working unchanged, and the injected heading renders as `## Identity`. No action needed.
|
|
32
39
|
|
|
33
40
|
The plugin sends its **profile name** as `?agent=`, resolved in this order:
|
|
34
41
|
|
|
@@ -91,7 +98,7 @@ Env overrides: `HICORTEX_URL`, `HICORTEX_AUTH_TOKEN`.
|
|
|
91
98
|
## Topology
|
|
92
99
|
|
|
93
100
|
- **Server host:** runs Hicortex. Set `hicortex_url: http://localhost:8787` (localhost bypasses auth).
|
|
94
|
-
- **Other Hermes boxes:** set `hicortex_url` to the server's hostname (e.g. `http://memory-server:8787`) and `HICORTEX_AUTH_TOKEN` to the server's token. Each box recalls from the same shared brain.
|
|
101
|
+
- **Other Hermes boxes:** set `hicortex_url` to the server's hostname (e.g. `http://memory-server:8787`) and `HICORTEX_AUTH_TOKEN` to the server's token. Each box recalls from the same shared brain — and every user message travels to that server each turn (see [Data flow](#data-flow)). Over plain `http://` the token and prompts cross the network in cleartext (one startup warning); prefer `https://` or a trusted private network.
|
|
95
102
|
|
|
96
103
|
## Notes
|
|
97
104
|
|
|
@@ -16,3 +16,10 @@ from agent.memory_provider import MemoryProvider # noqa: F401 (loader scans fo
|
|
|
16
16
|
from .provider import HicortexProvider
|
|
17
17
|
|
|
18
18
|
__all__ = ["HicortexProvider"]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def register(ctx) -> None:
|
|
22
|
+
"""Hermes plugin entry point (memory-provider guide convention): hand this
|
|
23
|
+
provider to the host. Complements the direct import path — the loader
|
|
24
|
+
scans for ``MemoryProvider`` subclasses either way."""
|
|
25
|
+
ctx.register_memory_provider(HicortexProvider())
|
|
@@ -6,10 +6,55 @@ Stdlib-only (no pip dependencies) so the plugin installs with zero friction.
|
|
|
6
6
|
from __future__ import annotations
|
|
7
7
|
|
|
8
8
|
import json
|
|
9
|
+
import logging
|
|
9
10
|
import urllib.error
|
|
10
11
|
import urllib.parse
|
|
11
12
|
import urllib.request
|
|
12
|
-
from typing import Any, Optional
|
|
13
|
+
from typing import Any, Optional, Tuple
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
# One-shot-per-process guard for the cleartext-transport warning. Clients are
|
|
18
|
+
# rebuilt per provider/config read; warning per construction would spam the
|
|
19
|
+
# gateway log on every rebuild.
|
|
20
|
+
_cleartext_warned = False
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _origin(url: str) -> Tuple[Optional[str], Optional[int]]:
|
|
24
|
+
"""(host, port) origin of a URL, default-port-normalized (http→80,
|
|
25
|
+
https→443) and lower-cased — the identity a redirect must preserve for the
|
|
26
|
+
Authorization header to be safe to keep. Unparseable URLs never compare
|
|
27
|
+
equal, so they fail CLOSED (token stripped)."""
|
|
28
|
+
try:
|
|
29
|
+
parsed = urllib.parse.urlparse(url)
|
|
30
|
+
port = parsed.port or (443 if parsed.scheme == "https" else 80)
|
|
31
|
+
return (parsed.hostname or "").lower(), port
|
|
32
|
+
except ValueError:
|
|
33
|
+
return (url, None)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class _AuthStrippingRedirectHandler(urllib.request.HTTPRedirectHandler):
|
|
37
|
+
"""Strip ``Authorization`` on any redirect that leaves the original origin.
|
|
38
|
+
|
|
39
|
+
urllib's default redirect handler forwards the request's headers to the
|
|
40
|
+
redirect target (only content headers — length/type/encoding — are
|
|
41
|
+
dropped), so a cross-host 302 hands the bearer token to whoever answers
|
|
42
|
+
the redirect. Same-origin redirects (path changes, trailing-slash fixes)
|
|
43
|
+
keep authenticating as before."""
|
|
44
|
+
|
|
45
|
+
def redirect_request(self, req, fp, code, msg, headers, newurl):
|
|
46
|
+
new_req = super().redirect_request(req, fp, code, msg, headers, newurl)
|
|
47
|
+
if new_req is not None and _origin(req.full_url) != _origin(newurl):
|
|
48
|
+
# Request headers are Capitalized by add_header ("Authorization").
|
|
49
|
+
new_req.headers.pop("Authorization", None)
|
|
50
|
+
new_req.unredirected_hdrs.pop("Authorization", None)
|
|
51
|
+
return new_req
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# Shared opener: urllib's defaults plus the auth-stripping redirect handler.
|
|
55
|
+
# `urlopen` cannot be used for this — it goes through the process-global
|
|
56
|
+
# default opener, which we must not reconfigure from inside a plugin.
|
|
57
|
+
_OPENER = urllib.request.build_opener(_AuthStrippingRedirectHandler)
|
|
13
58
|
|
|
14
59
|
|
|
15
60
|
class HicortexClient:
|
|
@@ -31,6 +76,23 @@ class HicortexClient:
|
|
|
31
76
|
if host in ("127.0.0.1", "localhost", "::1", "::ffff:127.0.0.1")
|
|
32
77
|
else auth_token
|
|
33
78
|
)
|
|
79
|
+
# Cleartext transport warning (catalog review 130093): a plain http://
|
|
80
|
+
# URL to a non-loopback host while a token is set means the token AND
|
|
81
|
+
# every prompt sent for recall cross the network unencrypted. WARN,
|
|
82
|
+
# never reject — plain http on a trusted private network (overlay
|
|
83
|
+
# networks, private overlay meshes) is a legitimate setup.
|
|
84
|
+
if self.auth_token and urllib.parse.urlparse(self.base_url).scheme != "https":
|
|
85
|
+
global _cleartext_warned
|
|
86
|
+
if not _cleartext_warned:
|
|
87
|
+
_cleartext_warned = True
|
|
88
|
+
logger.warning(
|
|
89
|
+
"hicortex: %s uses plain http:// on a non-loopback host "
|
|
90
|
+
"while an auth token is set — the token and every prompt "
|
|
91
|
+
"sent for recall cross the network in cleartext. Use "
|
|
92
|
+
"https://, or keep the server on a trusted private "
|
|
93
|
+
"network. (This warning fires once per process.)",
|
|
94
|
+
self.base_url,
|
|
95
|
+
)
|
|
34
96
|
self.timeout = timeout
|
|
35
97
|
|
|
36
98
|
def _headers(self) -> dict[str, str]:
|
|
@@ -69,7 +131,7 @@ class HicortexClient:
|
|
|
69
131
|
errors converted to statuses (never raised)."""
|
|
70
132
|
req = urllib.request.Request(url, data=data, headers=self._headers(), method=method)
|
|
71
133
|
try:
|
|
72
|
-
with
|
|
134
|
+
with _OPENER.open(req, timeout=timeout or self.timeout) as resp:
|
|
73
135
|
return resp.status, json.loads(resp.read().decode("utf-8"))
|
|
74
136
|
except urllib.error.HTTPError as e:
|
|
75
137
|
return e.code, self._parse_http_error(e)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
name: hicortex
|
|
2
|
-
version: 0.7.
|
|
2
|
+
version: 0.7.10
|
|
3
3
|
description: "Self-learning memory for Hermes agents — every session is distilled into lessons overnight, and your agent wakes up wiser. Pushes a compact per-turn recall index (lazy-loaded with hicortex_get), injects fresh lessons plus a per-agent standing context block, and exposes the full 9-tool memory surface (search, get, recent, ingest, lessons, index, graph, update, delete) via a shared Hicortex server. Stdlib-only. AFTER INSTALL: run `hermes memory setup`, select hicortex, enter the server URL + token (see the token env var description for where to find it)."
|
|
4
4
|
pip_dependencies: []
|
|
5
5
|
hooks: []
|
|
@@ -26,7 +26,7 @@ Recall: prefetch() -> POST /recall-index (pushed recall index, 0.14
|
|
|
26
26
|
|
|
27
27
|
Capture is NOT the plugin's job. A nightly reader on the Hicortex server
|
|
28
28
|
distills each agent's own session store (Hermes: ~/.hermes/profiles/<agent>/
|
|
29
|
-
state.db) centrally
|
|
29
|
+
state.db) centrally. This
|
|
30
30
|
plugin has no local LLM, no spool, no timer, and no capture path.
|
|
31
31
|
"""
|
|
32
32
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gamaze/hicortex",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.24.0",
|
|
4
4
|
"description": "Persistent agent identity for AI agents \u2014 a hand-edited identity layer, nightly-distilled experience, and lessons injected every session, shared across your whole fleet. Works with Hermes, OpenClaw, Claude Code, Pi, and opencode.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
package/server.json
CHANGED
|
@@ -3,12 +3,12 @@
|
|
|
3
3
|
"name": "io.github.gamaze-labs/hicortex",
|
|
4
4
|
"title": "Hicortex \u2014 AI Fleet Memory",
|
|
5
5
|
"description": "Shared fleet memory for AI agents: nightly self-correction, recall every prompt (supported agents).",
|
|
6
|
-
"version": "0.
|
|
6
|
+
"version": "0.24.0",
|
|
7
7
|
"packages": [
|
|
8
8
|
{
|
|
9
9
|
"registryType": "npm",
|
|
10
10
|
"identifier": "@gamaze/hicortex",
|
|
11
|
-
"version": "0.
|
|
11
|
+
"version": "0.24.0",
|
|
12
12
|
"transport": {
|
|
13
13
|
"type": "stdio",
|
|
14
14
|
"command": "npx",
|