@gamaze/hicortex 0.19.1 → 0.19.2
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 +4 -1
- package/assets/identity.html +59 -8
- package/dist/identity-store.d.ts +65 -1
- package/dist/identity-store.js +132 -9
- package/dist/index.d.ts +22 -7
- package/dist/index.js +510 -86
- package/dist/learnings-identity.d.ts +19 -10
- package/dist/learnings-identity.js +30 -19
- package/dist/mcp-server.js +9 -13
- package/dist/prompts.js +21 -1
- package/dist/types.d.ts +14 -0
- package/hermes-plugin/hicortex/provider.py +21 -5
- package/openclaw.plugin.json +15 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -314,7 +314,10 @@ Optional config (add to plugin entry in `~/.openclaw/openclaw.json`):
|
|
|
314
314
|
|-------|---------|-------------|
|
|
315
315
|
| `serverUrl` | `http://127.0.0.1:8787` | Hicortex server URL. Change for remote servers. |
|
|
316
316
|
| `authToken` | _(none)_ | Bearer token. Localhost bypasses auth; required for remote servers. Get the token from `hicortex status` on the server. |
|
|
317
|
-
| `
|
|
317
|
+
| `defaultProject` | _(none)_ | Project name sent on recall, search, recent, and ingest whenever the gateway supplies no project (Hermes `default_project` parity). |
|
|
318
|
+
| `recallLimit` | `8` | Max memories per recall on the pre-0.14 `/search` fallback. The pushed recall index is sized by SERVER config (`recallMaxItems`) — the server accepts no client limit. |
|
|
319
|
+
|
|
320
|
+
If `serverUrl`/`authToken` are absent from the config, the `HICORTEX_URL` and `HICORTEX_AUTH_TOKEN` environment variables are used as fallbacks (config always wins).
|
|
318
321
|
|
|
319
322
|
## LLM Configuration
|
|
320
323
|
|
package/assets/identity.html
CHANGED
|
@@ -291,6 +291,17 @@
|
|
|
291
291
|
}
|
|
292
292
|
})();
|
|
293
293
|
|
|
294
|
+
// Deep link (#313): /identity/ui?agent=<id> opens the editor in that
|
|
295
|
+
// agent's scope. The param STAYS in the URL (shareable bookmark; unlike
|
|
296
|
+
// ?token= it is not a secret). It cannot be adopted before the first load —
|
|
297
|
+
// the bare GET is what populates the `agents` map the scope selector needs —
|
|
298
|
+
// so it is stashed here and applied in load()'s success path.
|
|
299
|
+
var pendingScope = null;
|
|
300
|
+
(function initScopeParam() {
|
|
301
|
+
var p = new URLSearchParams(window.location.search).get("agent");
|
|
302
|
+
if (p) pendingScope = p;
|
|
303
|
+
})();
|
|
304
|
+
|
|
294
305
|
// Shared console nav (#249): append ?token= to internal nav links on click
|
|
295
306
|
// so navigation between pages doesn't re-authenticate. Reads token from any
|
|
296
307
|
// page-scoped localStorage key (each page historically used its own).
|
|
@@ -374,23 +385,32 @@
|
|
|
374
385
|
var active = null; // active section name
|
|
375
386
|
|
|
376
387
|
// Display labels for known sections (the API key stays lowercase; the UI
|
|
377
|
-
// shows a friendlier name). Unknown sections display their raw name.
|
|
388
|
+
// shows a friendlier name). Unknown sections display their raw name. The
|
|
389
|
+
// three #313 scope labels mirror SECTION_LABELS in identity-store.ts —
|
|
390
|
+
// keep in sync ("Global rules" marks the section as fleet-wide).
|
|
378
391
|
var DISPLAY_LABELS = {
|
|
392
|
+
"agent_identity": "Agent identity",
|
|
379
393
|
"user": "User",
|
|
380
|
-
"rules": "
|
|
394
|
+
"rules": "Global rules",
|
|
381
395
|
"memory": "Memory instructions",
|
|
382
396
|
};
|
|
383
397
|
// Preferred tab order — known sections first in this order, then any custom
|
|
384
|
-
// sections alphabetically.
|
|
385
|
-
|
|
398
|
+
// sections alphabetically. Mirrors the #313 served precedence
|
|
399
|
+
// (SECTION_PRECEDENCE in identity-store.ts): the agent's own identity
|
|
400
|
+
// first, then the principal, then fleet rules.
|
|
401
|
+
var TAB_PRIORITY = ["agent_identity", "user", "rules", "memory"];
|
|
386
402
|
var afterAuth = null; // action to retry after a successful token prompt
|
|
387
403
|
|
|
388
404
|
// Per-agent scope (0.13). scope = null → the global set; otherwise an agent
|
|
389
405
|
// id, and edits target PUT /identity?agent=<id>. `agents` is the id -> mode
|
|
390
406
|
// map from the bare (global) GET; `origins` is the id -> "global"|"agent"
|
|
391
407
|
// provenance from an agent-scope GET (used to dim inherited tabs).
|
|
408
|
+
// `agents` is PROTOTYPE-LESS (Object.create(null)): a lookup like
|
|
409
|
+
// agents["constructor"] must miss so a bogus ?agent= id cannot slip through
|
|
410
|
+
// the registration check via the Object.prototype chain (CR5). Every
|
|
411
|
+
// assignment from server data re-sanitizes into a fresh prototype-less map.
|
|
392
412
|
var scope = null;
|
|
393
|
-
var agents =
|
|
413
|
+
var agents = Object.create(null);
|
|
394
414
|
var origins = {};
|
|
395
415
|
|
|
396
416
|
// ?agent= query for the active scope. Written so the literal fetch("/identity"
|
|
@@ -425,7 +445,12 @@
|
|
|
425
445
|
var inherited = scope && origins[name] === "global";
|
|
426
446
|
b.className = "tab" + (name === active ? " active" : "") + (inherited ? " inherited" : "");
|
|
427
447
|
b.type = "button";
|
|
428
|
-
|
|
448
|
+
// Own-property lookup (CR-A): a section legitimately named "constructor"
|
|
449
|
+
// must display its raw name — DISPLAY_LABELS[name] would resolve through
|
|
450
|
+
// the Object.prototype chain.
|
|
451
|
+
b.appendChild(document.createTextNode(
|
|
452
|
+
Object.prototype.hasOwnProperty.call(DISPLAY_LABELS, name) ? DISPLAY_LABELS[name] : name
|
|
453
|
+
));
|
|
429
454
|
if (inherited) {
|
|
430
455
|
var inh = document.createElement("span");
|
|
431
456
|
inh.className = "inh";
|
|
@@ -505,8 +530,12 @@
|
|
|
505
530
|
overlay.classList.remove("open");
|
|
506
531
|
// The bare (global) response carries the `agents` map; an agent-scope
|
|
507
532
|
// response carries `origins`. Retain the last-known agents list when the
|
|
508
|
-
// current response omits it so the selector stays populated.
|
|
509
|
-
|
|
533
|
+
// current response omits it so the selector stays populated. Copied
|
|
534
|
+
// into a PROTOTYPE-LESS map (CR5): `agents` is keyed by client- and
|
|
535
|
+
// server-supplied ids, and lookups must never hit Object.prototype.
|
|
536
|
+
if (scope === null && data.agents && typeof data.agents === "object") {
|
|
537
|
+
agents = Object.assign(Object.create(null), data.agents);
|
|
538
|
+
}
|
|
510
539
|
origins = (data.origins && typeof data.origins === "object") ? data.origins : {};
|
|
511
540
|
var incoming = data.sections || {};
|
|
512
541
|
var names = Object.keys(incoming).sort(function (a, b) {
|
|
@@ -529,9 +558,27 @@
|
|
|
529
558
|
populateScopeOptions();
|
|
530
559
|
renderTabs();
|
|
531
560
|
setMsg(order.length ? "" : "", "dim");
|
|
561
|
+
// Deep link (#313): adopt the stashed ?agent= scope now that the
|
|
562
|
+
// agents map is loaded. An unknown-but-valid id registers locally as
|
|
563
|
+
// "override" (same as Add agent — the dir is created on first Save);
|
|
564
|
+
// an INVALID id is dropped, landing on Global rather than erroring.
|
|
565
|
+
if (pendingScope) {
|
|
566
|
+
var want = pendingScope;
|
|
567
|
+
pendingScope = null;
|
|
568
|
+
if (validName(want)) {
|
|
569
|
+
if (!agents[want]) agents[want] = "override";
|
|
570
|
+
switchScope(want);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
532
573
|
})
|
|
533
574
|
.catch(function (err) {
|
|
575
|
+
// unauthorized: load() is retried after the token prompt (afterAuth)
|
|
576
|
+
// — the deep link may still adopt on that retry, so keep pendingScope.
|
|
534
577
|
if (err.message === "unauthorized") return;
|
|
578
|
+
// A FAILED load must not leave the deep link armed: the user may
|
|
579
|
+
// manually switch scope meanwhile, and the next successful load would
|
|
580
|
+
// yank them back to the ?agent= scope (CR5). Cancel it here.
|
|
581
|
+
pendingScope = null;
|
|
535
582
|
setMsg("Failed to load: " + err.message, "err");
|
|
536
583
|
});
|
|
537
584
|
}
|
|
@@ -624,6 +671,10 @@
|
|
|
624
671
|
}
|
|
625
672
|
|
|
626
673
|
function switchScope(next) {
|
|
674
|
+
// A manual switch CANCELS a pending ?agent= deep link (CR5): the deep
|
|
675
|
+
// link only ever applies to the FIRST successful load. The adoption path
|
|
676
|
+
// also routes through here, after clearing pendingScope itself.
|
|
677
|
+
pendingScope = null;
|
|
627
678
|
if (anyDirty() && !window.confirm("Discard unsaved changes and switch scope?")) {
|
|
628
679
|
scopeSel.value = scope || "";
|
|
629
680
|
return;
|
package/dist/identity-store.d.ts
CHANGED
|
@@ -26,6 +26,12 @@
|
|
|
26
26
|
* `context/`), reads ADDITIVELY MERGE: per-section `identity/` wins, and
|
|
27
27
|
* legacy-only sections are included so nothing is lost — at both the global
|
|
28
28
|
* and the per-agent (`agents/<id>/`) level.
|
|
29
|
+
*
|
|
30
|
+
* #313 adds the per-agent `agent_identity` section (who THIS agent is + its
|
|
31
|
+
* role conduct; per-agent ONLY, never global) and makes the served section
|
|
32
|
+
* order a defined contract — see SECTION_PRECEDENCE below. `user` remains the
|
|
33
|
+
* PRINCIPAL (global) and `rules` the fleet-wide house rules (never
|
|
34
|
+
* overridden per-agent: section-level replacement would shadow them).
|
|
29
35
|
*/
|
|
30
36
|
/** A valid section name: lowercase alnum start, then alnum / `_` / `-`. */
|
|
31
37
|
export declare const SECTION_NAME_RE: RegExp;
|
|
@@ -116,6 +122,49 @@ export declare function migrateIdentityDir(home: string): {
|
|
|
116
122
|
to: string;
|
|
117
123
|
reason?: string;
|
|
118
124
|
};
|
|
125
|
+
/**
|
|
126
|
+
* The order sections are SERVED in (#313): `agent_identity` → `user` →
|
|
127
|
+
* `rules`, then every other section alphabetically. Semantics behind the
|
|
128
|
+
* order:
|
|
129
|
+
* - `agent_identity` — who THIS agent is (name, origin, character, voice,
|
|
130
|
+
* genuine interests) plus its role conduct; per-agent only, at
|
|
131
|
+
* `identity/agents/<id>/agent_identity.md`. It leads so the agent reads
|
|
132
|
+
* its own self before anything else.
|
|
133
|
+
* - `user` — the PRINCIPAL the whole fleet serves (global, never the agent).
|
|
134
|
+
* - `rules` — fleet-wide house rules. Global only: section-level replacement
|
|
135
|
+
* means a per-agent rules.md would SHADOW house rules, so role-specific
|
|
136
|
+
* conduct lives INSIDE agent_identity.md (additive by construction).
|
|
137
|
+
* Before #313 this ordering rode on file-read order (readdirSync, FS-dependent)
|
|
138
|
+
* where it existed at all — now it is a defined, tested contract. The SAME
|
|
139
|
+
* list is shared by the client renderers (orderSectionNames in
|
|
140
|
+
* learnings-identity.ts imports it; the Hermes plugin mirrors it) so server
|
|
141
|
+
* and clients can never drift: the precedence is defined ONCE, here.
|
|
142
|
+
*/
|
|
143
|
+
export declare const SECTION_PRECEDENCE: readonly ["agent_identity", "user", "rules"];
|
|
144
|
+
/**
|
|
145
|
+
* Scope display labels (#313): ONE map, consumed by the injected-block
|
|
146
|
+
* renderer (renderIdentityBlock — CC hook + OC plugin + MCP tool), the UI
|
|
147
|
+
* editor (DISPLAY_LABELS mirrors it), and the Hermes plugin (mirror in
|
|
148
|
+
* provider.py). "Global rules" says what the section IS (fleet-wide house
|
|
149
|
+
* rules), distinguishing it from per-agent content. Unknown section names
|
|
150
|
+
* fall back to title-case (titleCaseSection) — no file renames, the API keys
|
|
151
|
+
* stay the section names on disk.
|
|
152
|
+
*/
|
|
153
|
+
export declare const SECTION_LABELS: Readonly<Record<string, string>>;
|
|
154
|
+
/**
|
|
155
|
+
* Rebuild a sections record with its keys inserted in SECTION_PRECEDENCE
|
|
156
|
+
* order, then the rest alphabetically. Pure: same values, same (string) keys,
|
|
157
|
+
* deterministic order — JSON serialisation preserves insertion order, so this
|
|
158
|
+
* IS the wire contract. Idempotent; a map with no known primaries is just
|
|
159
|
+
* alphabetised.
|
|
160
|
+
*
|
|
161
|
+
* DOCUMENTED EXCEPTION: a canonically-numeric section name (e.g. "42") is an
|
|
162
|
+
* integer-index key in JS and serialises BEFORE every string key regardless
|
|
163
|
+
* of insertion order — it escapes the contract by engine semantics, not by
|
|
164
|
+
* our choice. That is why writeSections rejects purely-numeric names on new
|
|
165
|
+
* writes (reads keep serving existing numeric files).
|
|
166
|
+
*/
|
|
167
|
+
export declare function orderSections(sections: Record<string, string>): Record<string, string>;
|
|
119
168
|
export interface ReadResult {
|
|
120
169
|
sections: Record<string, string>;
|
|
121
170
|
/** ISO timestamp of the latest included file's mtime, or null when none. */
|
|
@@ -143,7 +192,10 @@ export declare function readSectionsWithFallback(identityDir: string): ReadResul
|
|
|
143
192
|
/**
|
|
144
193
|
* Partial upsert of the named sections. Contract (spec §1):
|
|
145
194
|
* - Validate ALL names first; any invalid → throw InvalidSectionNameError and
|
|
146
|
-
* write NOTHING (atomic request semantics).
|
|
195
|
+
* write NOTHING (atomic request semantics). Purely-numeric names are
|
|
196
|
+
* rejected here too (NUMERIC_SECTION_RE): they serialize ahead of the
|
|
197
|
+
* precedence contract (see orderSections), so no new ones may be created —
|
|
198
|
+
* existing numeric files remain readable, just no longer writable.
|
|
147
199
|
* - Only the named sections are touched; omitted sections stay untouched
|
|
148
200
|
* (omission is not deletion — deletion is filesystem-only).
|
|
149
201
|
* - Each write goes temp-file-then-rename in the same dir (no half-applied
|
|
@@ -281,6 +333,18 @@ export interface HandlerResult {
|
|
|
281
333
|
* paramless legacy caller is covered by the migration docs, not this guard.)
|
|
282
334
|
*/
|
|
283
335
|
export declare function handleIdentityGet(identityDir: string, clients: string[], query: Record<string, unknown>, identityAgents?: Record<string, AgentMode>): HandlerResult;
|
|
336
|
+
/**
|
|
337
|
+
* Compose the served GET /identity (and /context alias) body — the ONE
|
|
338
|
+
* composition site (#313 CR3): take a handleIdentityGet HandlerResult, inject
|
|
339
|
+
* the synthetic product-owned `memory` section (#192, when enabled and mode
|
|
340
|
+
* ≠ off), then apply the SECTION_PRECEDENCE order (injectMemorySection
|
|
341
|
+
* APPENDS the section last, so the order must be re-applied after it). Used
|
|
342
|
+
* by both REST adapters and buildIdentityToolResult so the final wire key
|
|
343
|
+
* order — including memory's alphabetical slot among "rest" — can never drift
|
|
344
|
+
* between them. Non-200 results pass through untouched. Mutates and returns
|
|
345
|
+
* the SAME HandlerResult (the body object is shared with the caller).
|
|
346
|
+
*/
|
|
347
|
+
export declare function serveIdentityBody(handlerResult: HandlerResult, memoryEnabled: boolean): HandlerResult;
|
|
284
348
|
/**
|
|
285
349
|
* PUT /identity. Validates the body shape and section content types, then
|
|
286
350
|
* delegates to writeSections (which owns the name allowlist + atomicity +
|
package/dist/identity-store.js
CHANGED
|
@@ -27,14 +27,21 @@
|
|
|
27
27
|
* `context/`), reads ADDITIVELY MERGE: per-section `identity/` wins, and
|
|
28
28
|
* legacy-only sections are included so nothing is lost — at both the global
|
|
29
29
|
* and the per-agent (`agents/<id>/`) level.
|
|
30
|
+
*
|
|
31
|
+
* #313 adds the per-agent `agent_identity` section (who THIS agent is + its
|
|
32
|
+
* role conduct; per-agent ONLY, never global) and makes the served section
|
|
33
|
+
* order a defined contract — see SECTION_PRECEDENCE below. `user` remains the
|
|
34
|
+
* PRINCIPAL (global) and `rules` the fleet-wide house rules (never
|
|
35
|
+
* overridden per-agent: section-level replacement would shadow them).
|
|
30
36
|
*/
|
|
31
37
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
32
|
-
exports.KNOWN_CONTEXT_CLIENTS = exports.KNOWN_IDENTITY_CLIENTS = exports.CONTEXT_SIZE_WARN_BYTES = exports.IDENTITY_SIZE_WARN_BYTES = exports.isValidAgentId = exports.AGENTS_DIR = exports.InvalidSectionNameError = exports.SECTION_NAME_MAX = exports.SECTION_NAME_RE = void 0;
|
|
38
|
+
exports.KNOWN_CONTEXT_CLIENTS = exports.KNOWN_IDENTITY_CLIENTS = exports.CONTEXT_SIZE_WARN_BYTES = exports.IDENTITY_SIZE_WARN_BYTES = exports.SECTION_LABELS = exports.SECTION_PRECEDENCE = exports.isValidAgentId = exports.AGENTS_DIR = exports.InvalidSectionNameError = exports.SECTION_NAME_MAX = exports.SECTION_NAME_RE = void 0;
|
|
33
39
|
exports.isValidSectionName = isValidSectionName;
|
|
34
40
|
exports.sanitizeAgentId = sanitizeAgentId;
|
|
35
41
|
exports.resolveAgentIdentity = resolveAgentIdentity;
|
|
36
42
|
exports.resolveIdentityAgents = resolveIdentityAgents;
|
|
37
43
|
exports.migrateIdentityDir = migrateIdentityDir;
|
|
44
|
+
exports.orderSections = orderSections;
|
|
38
45
|
exports.readSections = readSections;
|
|
39
46
|
exports.readSectionsWithFallback = readSectionsWithFallback;
|
|
40
47
|
exports.writeSections = writeSections;
|
|
@@ -47,10 +54,12 @@ exports.resolveAgentMode = resolveAgentMode;
|
|
|
47
54
|
exports.listAgents = listAgents;
|
|
48
55
|
exports.readResolvedSections = readResolvedSections;
|
|
49
56
|
exports.handleIdentityGet = handleIdentityGet;
|
|
57
|
+
exports.serveIdentityBody = serveIdentityBody;
|
|
50
58
|
exports.handleIdentityPut = handleIdentityPut;
|
|
51
59
|
const node_fs_1 = require("node:fs");
|
|
52
60
|
const node_path_1 = require("node:path");
|
|
53
61
|
const node_crypto_1 = require("node:crypto");
|
|
62
|
+
const memory_instructions_js_1 = require("./memory-instructions.js");
|
|
54
63
|
// ---------------------------------------------------------------------------
|
|
55
64
|
// Section-name allowlist (security contract)
|
|
56
65
|
// ---------------------------------------------------------------------------
|
|
@@ -77,6 +86,15 @@ class InvalidSectionNameError extends Error {
|
|
|
77
86
|
}
|
|
78
87
|
}
|
|
79
88
|
exports.InvalidSectionNameError = InvalidSectionNameError;
|
|
89
|
+
/**
|
|
90
|
+
* A purely-numeric section name (`^\d+$`). Allowlist-VALID (reads must keep
|
|
91
|
+
* serving existing numeric files) but rejected on writes: integer-index
|
|
92
|
+
* object keys serialise before every string key, so a numeric section escapes
|
|
93
|
+
* the SECTION_PRECEDENCE wire contract entirely (see orderSections' documented
|
|
94
|
+
* exception). Enforced in writeSections only — never in isValidSectionName,
|
|
95
|
+
* which gates READS as well.
|
|
96
|
+
*/
|
|
97
|
+
const NUMERIC_SECTION_RE = /^\d+$/;
|
|
80
98
|
/** Reserved subdir under <identity>/ holding per-agent sections. NOT a section. */
|
|
81
99
|
exports.AGENTS_DIR = "agents";
|
|
82
100
|
/**
|
|
@@ -185,6 +203,64 @@ function migrateIdentityDir(home) {
|
|
|
185
203
|
return { renamed: false, from, to, reason: `rename failed: ${err instanceof Error ? err.message : String(err)}` };
|
|
186
204
|
}
|
|
187
205
|
}
|
|
206
|
+
// ---------------------------------------------------------------------------
|
|
207
|
+
// Section precedence — the served ordering contract (#313)
|
|
208
|
+
// ---------------------------------------------------------------------------
|
|
209
|
+
/**
|
|
210
|
+
* The order sections are SERVED in (#313): `agent_identity` → `user` →
|
|
211
|
+
* `rules`, then every other section alphabetically. Semantics behind the
|
|
212
|
+
* order:
|
|
213
|
+
* - `agent_identity` — who THIS agent is (name, origin, character, voice,
|
|
214
|
+
* genuine interests) plus its role conduct; per-agent only, at
|
|
215
|
+
* `identity/agents/<id>/agent_identity.md`. It leads so the agent reads
|
|
216
|
+
* its own self before anything else.
|
|
217
|
+
* - `user` — the PRINCIPAL the whole fleet serves (global, never the agent).
|
|
218
|
+
* - `rules` — fleet-wide house rules. Global only: section-level replacement
|
|
219
|
+
* means a per-agent rules.md would SHADOW house rules, so role-specific
|
|
220
|
+
* conduct lives INSIDE agent_identity.md (additive by construction).
|
|
221
|
+
* Before #313 this ordering rode on file-read order (readdirSync, FS-dependent)
|
|
222
|
+
* where it existed at all — now it is a defined, tested contract. The SAME
|
|
223
|
+
* list is shared by the client renderers (orderSectionNames in
|
|
224
|
+
* learnings-identity.ts imports it; the Hermes plugin mirrors it) so server
|
|
225
|
+
* and clients can never drift: the precedence is defined ONCE, here.
|
|
226
|
+
*/
|
|
227
|
+
exports.SECTION_PRECEDENCE = ["agent_identity", "user", "rules"];
|
|
228
|
+
/**
|
|
229
|
+
* Scope display labels (#313): ONE map, consumed by the injected-block
|
|
230
|
+
* renderer (renderIdentityBlock — CC hook + OC plugin + MCP tool), the UI
|
|
231
|
+
* editor (DISPLAY_LABELS mirrors it), and the Hermes plugin (mirror in
|
|
232
|
+
* provider.py). "Global rules" says what the section IS (fleet-wide house
|
|
233
|
+
* rules), distinguishing it from per-agent content. Unknown section names
|
|
234
|
+
* fall back to title-case (titleCaseSection) — no file renames, the API keys
|
|
235
|
+
* stay the section names on disk.
|
|
236
|
+
*/
|
|
237
|
+
exports.SECTION_LABELS = {
|
|
238
|
+
agent_identity: "Agent identity",
|
|
239
|
+
user: "User",
|
|
240
|
+
rules: "Global rules",
|
|
241
|
+
};
|
|
242
|
+
/**
|
|
243
|
+
* Rebuild a sections record with its keys inserted in SECTION_PRECEDENCE
|
|
244
|
+
* order, then the rest alphabetically. Pure: same values, same (string) keys,
|
|
245
|
+
* deterministic order — JSON serialisation preserves insertion order, so this
|
|
246
|
+
* IS the wire contract. Idempotent; a map with no known primaries is just
|
|
247
|
+
* alphabetised.
|
|
248
|
+
*
|
|
249
|
+
* DOCUMENTED EXCEPTION: a canonically-numeric section name (e.g. "42") is an
|
|
250
|
+
* integer-index key in JS and serialises BEFORE every string key regardless
|
|
251
|
+
* of insertion order — it escapes the contract by engine semantics, not by
|
|
252
|
+
* our choice. That is why writeSections rejects purely-numeric names on new
|
|
253
|
+
* writes (reads keep serving existing numeric files).
|
|
254
|
+
*/
|
|
255
|
+
function orderSections(sections) {
|
|
256
|
+
const names = Object.keys(sections);
|
|
257
|
+
const primaries = exports.SECTION_PRECEDENCE.filter((p) => names.includes(p));
|
|
258
|
+
const rest = names.filter((n) => !exports.SECTION_PRECEDENCE.includes(n)).sort();
|
|
259
|
+
const ordered = {};
|
|
260
|
+
for (const name of [...primaries, ...rest])
|
|
261
|
+
ordered[name] = sections[name];
|
|
262
|
+
return ordered;
|
|
263
|
+
}
|
|
188
264
|
/**
|
|
189
265
|
* Enumerate the identity dir and return the served sections. Only regular files
|
|
190
266
|
* whose basename (sans `.md`) passes the allowlist are included; symlinks are
|
|
@@ -285,7 +361,10 @@ function readSectionsWithFallback(identityDir) {
|
|
|
285
361
|
/**
|
|
286
362
|
* Partial upsert of the named sections. Contract (spec §1):
|
|
287
363
|
* - Validate ALL names first; any invalid → throw InvalidSectionNameError and
|
|
288
|
-
* write NOTHING (atomic request semantics).
|
|
364
|
+
* write NOTHING (atomic request semantics). Purely-numeric names are
|
|
365
|
+
* rejected here too (NUMERIC_SECTION_RE): they serialize ahead of the
|
|
366
|
+
* precedence contract (see orderSections), so no new ones may be created —
|
|
367
|
+
* existing numeric files remain readable, just no longer writable.
|
|
289
368
|
* - Only the named sections are touched; omitted sections stay untouched
|
|
290
369
|
* (omission is not deletion — deletion is filesystem-only).
|
|
291
370
|
* - Each write goes temp-file-then-rename in the same dir (no half-applied
|
|
@@ -310,7 +389,7 @@ function readSectionsWithFallback(identityDir) {
|
|
|
310
389
|
* written THROUGH (never-follow-symlinks-on-write).
|
|
311
390
|
*/
|
|
312
391
|
function writeSections(dir, sections) {
|
|
313
|
-
const invalid = Object.keys(sections).filter((n) => !isValidSectionName(n));
|
|
392
|
+
const invalid = Object.keys(sections).filter((n) => !isValidSectionName(n) || NUMERIC_SECTION_RE.test(n));
|
|
314
393
|
if (invalid.length > 0)
|
|
315
394
|
throw new InvalidSectionNameError(invalid);
|
|
316
395
|
(0, node_fs_1.mkdirSync)(dir, { recursive: true });
|
|
@@ -596,7 +675,9 @@ function listAgents(identityDir, identityAgents) {
|
|
|
596
675
|
function readResolvedSections(identityDir, agentId, identityAgents) {
|
|
597
676
|
if (agentId === null) {
|
|
598
677
|
const g = readSectionsWithFallback(identityDir);
|
|
599
|
-
|
|
678
|
+
// #313: every served read carries the precedence contract — the bare
|
|
679
|
+
// global set is ordered exactly like a resolved per-agent set.
|
|
680
|
+
return { sections: orderSections(g.sections), updatedAt: g.updatedAt };
|
|
600
681
|
}
|
|
601
682
|
const mode = resolveAgentMode(identityDir, agentId, identityAgents);
|
|
602
683
|
if (mode === "off") {
|
|
@@ -604,22 +685,26 @@ function readResolvedSections(identityDir, agentId, identityAgents) {
|
|
|
604
685
|
}
|
|
605
686
|
const global = readSectionsWithFallback(identityDir);
|
|
606
687
|
if (mode === "global") {
|
|
607
|
-
return { sections: global.sections, updatedAt: global.updatedAt, agent: agentId, mode };
|
|
688
|
+
return { sections: orderSections(global.sections), updatedAt: global.updatedAt, agent: agentId, mode };
|
|
608
689
|
}
|
|
609
690
|
// override — read the agent dir (identity + legacy fallback merged so the
|
|
610
691
|
// both-dirs edge case doesn't lose legacy-only agent sections).
|
|
611
692
|
const agentRead = readAgentSectionsWithFallback(identityDir, agentId);
|
|
612
|
-
|
|
693
|
+
// #313: build the merged map, then serve it in precedence order
|
|
694
|
+
// (agent_identity → user → rules → rest alphabetical) — one ordering for
|
|
695
|
+
// every read, so what the client renders is defined server-side.
|
|
696
|
+
const merged = { ...global.sections, ...agentRead.sections };
|
|
697
|
+
const sections = orderSections(merged);
|
|
613
698
|
const origins = {};
|
|
614
699
|
let latestMtimeMs = 0;
|
|
615
|
-
for (const name of Object.keys(
|
|
700
|
+
for (const name of Object.keys(merged)) {
|
|
616
701
|
const fromAgent = name in agentRead.sections;
|
|
617
702
|
origins[name] = fromAgent ? "agent" : "global";
|
|
618
703
|
const mt = fromAgent ? agentRead.mtimes[name] : global.mtimes[name];
|
|
619
704
|
if (mt && mt > latestMtimeMs)
|
|
620
705
|
latestMtimeMs = mt;
|
|
621
706
|
}
|
|
622
|
-
const updatedAt = Object.keys(
|
|
707
|
+
const updatedAt = Object.keys(merged).length > 0 ? new Date(latestMtimeMs).toISOString() : null;
|
|
623
708
|
return { sections, updatedAt, agent: agentId, mode, origins };
|
|
624
709
|
}
|
|
625
710
|
// ---------------------------------------------------------------------------
|
|
@@ -667,7 +752,10 @@ function handleIdentityGet(identityDir, clients, query, identityAgents = {}) {
|
|
|
667
752
|
// No agent → the plain global read plus the additive `agents` map the UI
|
|
668
753
|
// selector needs (backward compatible: existing callers ignore unknown keys).
|
|
669
754
|
if (agentId === null) {
|
|
670
|
-
|
|
755
|
+
// #313: route through readResolvedSections so the bare read serves the
|
|
756
|
+
// SAME precedence-ordered sections as a per-agent read (it also drops the
|
|
757
|
+
// duplicated readSectionsWithFallback call this handler used to carry).
|
|
758
|
+
const { sections, updatedAt } = readResolvedSections(identityDir, null, identityAgents);
|
|
671
759
|
return {
|
|
672
760
|
status: 200,
|
|
673
761
|
body: { sections, updated_at: updatedAt, clients, agents: listAgents(identityDir, identityAgents) },
|
|
@@ -685,6 +773,26 @@ function handleIdentityGet(identityDir, clients, query, identityAgents = {}) {
|
|
|
685
773
|
body.origins = resolved.origins;
|
|
686
774
|
return { status: 200, body };
|
|
687
775
|
}
|
|
776
|
+
/**
|
|
777
|
+
* Compose the served GET /identity (and /context alias) body — the ONE
|
|
778
|
+
* composition site (#313 CR3): take a handleIdentityGet HandlerResult, inject
|
|
779
|
+
* the synthetic product-owned `memory` section (#192, when enabled and mode
|
|
780
|
+
* ≠ off), then apply the SECTION_PRECEDENCE order (injectMemorySection
|
|
781
|
+
* APPENDS the section last, so the order must be re-applied after it). Used
|
|
782
|
+
* by both REST adapters and buildIdentityToolResult so the final wire key
|
|
783
|
+
* order — including memory's alphabetical slot among "rest" — can never drift
|
|
784
|
+
* between them. Non-200 results pass through untouched. Mutates and returns
|
|
785
|
+
* the SAME HandlerResult (the body object is shared with the caller).
|
|
786
|
+
*/
|
|
787
|
+
function serveIdentityBody(handlerResult, memoryEnabled) {
|
|
788
|
+
if (handlerResult.status !== 200)
|
|
789
|
+
return handlerResult;
|
|
790
|
+
const body = handlerResult.body;
|
|
791
|
+
(0, memory_instructions_js_1.injectMemorySection)(body, memoryEnabled);
|
|
792
|
+
if (body.sections)
|
|
793
|
+
body.sections = orderSections(body.sections);
|
|
794
|
+
return handlerResult;
|
|
795
|
+
}
|
|
688
796
|
/**
|
|
689
797
|
* PUT /identity. Validates the body shape and section content types, then
|
|
690
798
|
* delegates to writeSections (which owns the name allowlist + atomicity +
|
|
@@ -703,6 +811,21 @@ function handleIdentityPut(identityDir, body, query = {}, identityAgents = {}) {
|
|
|
703
811
|
return { status: 400, body: { error: `Section '${name}' content must be a string` } };
|
|
704
812
|
}
|
|
705
813
|
}
|
|
814
|
+
// CR3: numeric names get their OWN 400 message, not the generic
|
|
815
|
+
// invalid-name one. The name passes every other check (it even GETs fine —
|
|
816
|
+
// reads keep serving existing numeric files), so "invalid section name"
|
|
817
|
+
// would read as a bug rather than policy. writeSections enforces the same
|
|
818
|
+
// rejection for direct callers (defense in depth).
|
|
819
|
+
const numeric = Object.keys(sections).filter((n) => NUMERIC_SECTION_RE.test(n));
|
|
820
|
+
if (numeric.length > 0) {
|
|
821
|
+
return {
|
|
822
|
+
status: 400,
|
|
823
|
+
body: {
|
|
824
|
+
error: `Section name(s) ${numeric.map((n) => `'${n}'`).join(", ")}: purely-numeric names are read-only via the API ` +
|
|
825
|
+
`(integer-index keys break the served section ordering); existing numeric files stay readable — write a renamed section instead`,
|
|
826
|
+
},
|
|
827
|
+
};
|
|
828
|
+
}
|
|
706
829
|
const targetDir = agentId === null ? identityDir : (0, node_path_1.join)(identityDir, exports.AGENTS_DIR, agentId);
|
|
707
830
|
if (agentId !== null) {
|
|
708
831
|
// A1 write-path guard: never write through a symlinked/non-dir root or leaf
|
package/dist/index.d.ts
CHANGED
|
@@ -8,14 +8,29 @@
|
|
|
8
8
|
* Install once: `openclaw plugins install @gamaze/hicortex`
|
|
9
9
|
* Run server: `npx @gamaze/hicortex init`
|
|
10
10
|
*
|
|
11
|
-
* Responsibilities (recall-only adapter,
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* /recall-index
|
|
11
|
+
* Responsibilities (recall-only adapter, behaviorally aligned with the Hermes
|
|
12
|
+
* reference plugin — hermes-plugin/hicortex/provider.py — since #316):
|
|
13
|
+
* - before_agent_start → GET /identity + GET /lessons once per SESSION
|
|
14
|
+
* (#316: standing blocks are not re-sent every turn; a FAILED identity
|
|
15
|
+
* fetch is retried next turn — only success memoizes, so the #313 dead-man
|
|
16
|
+
* banner keeps firing until identity returns) + POST /recall-index EVERY
|
|
17
|
+
* turn (fail-soft, concurrent; recall hot path capped at 1.5 s like
|
|
18
|
+
* Hermes). The FIRST recall fetch of a session is preceded by an AWAITED
|
|
19
|
+
* {reset:true} so a gateway restart resuming a session cannot inherit a
|
|
20
|
+
* stale server-side shown-set, and no reset can land after a fetch and
|
|
21
|
+
* wipe what it built. In OpenClaw every inbound message spawns an embedded
|
|
22
|
+
* run, so this hook fires PER TURN — it is the per-turn /recall-index
|
|
23
|
+
* surface, not just session start.
|
|
24
|
+
* - 404 on /recall-index → 600 s TTL latch + GET /search fallback that
|
|
25
|
+
* renders CONTENT (Hermes's legacy `_format_hits` shape — the pushed
|
|
26
|
+
* index's `hicortex_get(id)` menu is useless against the pre-0.14 servers
|
|
27
|
+
* the fallback exists for) — old servers keep full-content recall instead
|
|
28
|
+
* of degrading to nothing. Auth/5xx errors do NOT latch-fallback (they
|
|
29
|
+
* are errors, not version skew): fail soft per turn + warn ONCE per HTTP
|
|
30
|
+
* status.
|
|
17
31
|
* - after_compaction / before_reset → POST /recall-index {reset:true}
|
|
18
|
-
* (context window rebuilt → the server's per-session shown-set is stale
|
|
32
|
+
* (context window rebuilt → the server's per-session shown-set is stale
|
|
33
|
+
* AND the standing blocks may have been dropped → re-injected next turn)
|
|
19
34
|
* - Tools → HTTP proxies to /search, /memory, /recent, /ingest, /lessons
|
|
20
35
|
*
|
|
21
36
|
* CAPTURE IS NOT THIS PLUGIN'S JOB. OpenClaw persists sessions at
|