@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.
@@ -63,18 +63,25 @@ export declare function resolveConfig(): ResolvedConfig | null;
63
63
  */
64
64
  export declare function titleCaseSection(name: string): string;
65
65
  /**
66
- * Stable section ordering: `user` first, then `rules` (the seeded primary
67
- * sections, spec §8), then every other section alphabetically. Server-side
68
- * enumeration order (readdirSync) is FS-dependent, so we sort here for a
69
- * deterministic injection block. Exported for reuse by the OC plugin.
66
+ * Stable section ordering for the rendered `## Identity` block: the #313
67
+ * precedence contract `agent_identity` (the agent's own self + role
68
+ * conduct) first, then `user` (the principal), then `rules` (fleet-wide house
69
+ * rules), then every other section alphabetically. The list is imported from
70
+ * identity-store (SECTION_PRECEDENCE) so the server's SERVED order and every
71
+ * client's RENDERED order are the same definition — they cannot drift.
72
+ * Server-side enumeration order (readdirSync) is FS-dependent, so we sort
73
+ * here for a deterministic injection block. Exported for reuse by the OC
74
+ * plugin; the Python Hermes plugin mirrors this helper — keep them in sync.
70
75
  */
71
76
  export declare function orderSectionNames(names: string[]): string[];
72
77
  /**
73
78
  * Render the `## Identity` block from a resolved section map, or null when there
74
79
  * is nothing to inject (no sections, or every section blank after trimming).
75
80
  * Pure — no gating, no I/O. Shared verbatim by the CC hook and the OC plugin so
76
- * both harnesses emit an identical block. Sections are ordered (user, rules,
77
- * then alphabetical) and rendered under title-cased `###` headings.
81
+ * both harnesses emit an identical block. Sections are ordered (agent_identity,
82
+ * user, rules, then alphabetical) and rendered under `###` headings labeled by
83
+ * the #313 scope map (SECTION_LABELS: "Agent identity" / "User" / "Global
84
+ * rules"); unknown section names fall back to title-case.
78
85
  */
79
86
  export declare function renderIdentityBlock(sections: Record<string, string>): string | null;
80
87
  /**
@@ -116,10 +123,12 @@ export interface IdentityToolResult {
116
123
  * `agent` param forwarded so per-agent installs resolve the right scope
117
124
  * (WARNING-2: previously the tool always passed `{}` → global, so an agent
118
125
  * with an override saw the wrong identity).
119
- * 2. `injectMemorySection` — the synthetic product-owned `memory` section
120
- * (WARNING-1: the REST route + SessionStart hook inject it; the tool did
121
- * not, contradicting its "same data" docs).
122
- * 3. optional `name` filter, then `renderIdentityBlock` for the `### <Title>`
126
+ * 2. `serveIdentityBody` — the ONE composition helper (REST uses it too):
127
+ * injects the synthetic product-owned `memory` section (WARNING-1: the
128
+ * REST route + SessionStart hook inject it; the tool did not,
129
+ * contradicting its "same data" docs) and applies the SECTION_PRECEDENCE
130
+ * wire order.
131
+ * 3. optional `name` filter, then `renderIdentityBlock` for the `### <Label>`
123
132
  * markdown the hook injects.
124
133
  *
125
134
  * Pure: no I/O of its own (the only I/O is `handleIdentityGet` reading the
@@ -39,7 +39,6 @@ exports.fetchLessonsIdentity = fetchLessonsIdentity;
39
39
  const node_fs_1 = require("node:fs");
40
40
  const node_path_1 = require("node:path");
41
41
  const identity_store_js_1 = require("./identity-store.js");
42
- const memory_instructions_js_1 = require("./memory-instructions.js");
43
42
  const features_js_1 = require("./features.js");
44
43
  const extensions_js_1 = require("./extensions.js");
45
44
  const state_js_1 = require("./state.js");
@@ -152,22 +151,29 @@ function titleCaseSection(name) {
152
151
  .join(" ");
153
152
  }
154
153
  /**
155
- * Stable section ordering: `user` first, then `rules` (the seeded primary
156
- * sections, spec §8), then every other section alphabetically. Server-side
157
- * enumeration order (readdirSync) is FS-dependent, so we sort here for a
158
- * deterministic injection block. Exported for reuse by the OC plugin.
154
+ * Stable section ordering for the rendered `## Identity` block: the #313
155
+ * precedence contract `agent_identity` (the agent's own self + role
156
+ * conduct) first, then `user` (the principal), then `rules` (fleet-wide house
157
+ * rules), then every other section alphabetically. The list is imported from
158
+ * identity-store (SECTION_PRECEDENCE) so the server's SERVED order and every
159
+ * client's RENDERED order are the same definition — they cannot drift.
160
+ * Server-side enumeration order (readdirSync) is FS-dependent, so we sort
161
+ * here for a deterministic injection block. Exported for reuse by the OC
162
+ * plugin; the Python Hermes plugin mirrors this helper — keep them in sync.
159
163
  */
160
164
  function orderSectionNames(names) {
161
- const primaries = ["user", "rules"].filter((p) => names.includes(p));
162
- const rest = names.filter((n) => n !== "user" && n !== "rules").sort();
165
+ const primaries = identity_store_js_1.SECTION_PRECEDENCE.filter((p) => names.includes(p));
166
+ const rest = names.filter((n) => !identity_store_js_1.SECTION_PRECEDENCE.includes(n)).sort();
163
167
  return [...primaries, ...rest];
164
168
  }
165
169
  /**
166
170
  * Render the `## Identity` block from a resolved section map, or null when there
167
171
  * is nothing to inject (no sections, or every section blank after trimming).
168
172
  * Pure — no gating, no I/O. Shared verbatim by the CC hook and the OC plugin so
169
- * both harnesses emit an identical block. Sections are ordered (user, rules,
170
- * then alphabetical) and rendered under title-cased `###` headings.
173
+ * both harnesses emit an identical block. Sections are ordered (agent_identity,
174
+ * user, rules, then alphabetical) and rendered under `###` headings labeled by
175
+ * the #313 scope map (SECTION_LABELS: "Agent identity" / "User" / "Global
176
+ * rules"); unknown section names fall back to title-case.
171
177
  */
172
178
  function renderIdentityBlock(sections) {
173
179
  if (!sections || typeof sections !== "object" || Array.isArray(sections))
@@ -178,7 +184,11 @@ function renderIdentityBlock(sections) {
178
184
  const body = sections[name];
179
185
  if (typeof body !== "string" || body.trim() === "")
180
186
  continue;
181
- bodyParts.push(`### ${titleCaseSection(name)}`, "", body.trim());
187
+ // Own-property lookup (CR-A): "constructor" is allowlist-VALID, and a
188
+ // plain SECTION_LABELS[name] would resolve through the Object.prototype
189
+ // chain and render the inherited function's source as the heading.
190
+ const label = Object.hasOwn(identity_store_js_1.SECTION_LABELS, name) ? identity_store_js_1.SECTION_LABELS[name] : titleCaseSection(name);
191
+ bodyParts.push(`### ${label}`, "", body.trim());
182
192
  }
183
193
  if (bodyParts.length === 0)
184
194
  return null;
@@ -195,10 +205,12 @@ function renderIdentityBlock(sections) {
195
205
  * `agent` param forwarded so per-agent installs resolve the right scope
196
206
  * (WARNING-2: previously the tool always passed `{}` → global, so an agent
197
207
  * with an override saw the wrong identity).
198
- * 2. `injectMemorySection` — the synthetic product-owned `memory` section
199
- * (WARNING-1: the REST route + SessionStart hook inject it; the tool did
200
- * not, contradicting its "same data" docs).
201
- * 3. optional `name` filter, then `renderIdentityBlock` for the `### <Title>`
208
+ * 2. `serveIdentityBody` — the ONE composition helper (REST uses it too):
209
+ * injects the synthetic product-owned `memory` section (WARNING-1: the
210
+ * REST route + SessionStart hook inject it; the tool did not,
211
+ * contradicting its "same data" docs) and applies the SECTION_PRECEDENCE
212
+ * wire order.
213
+ * 3. optional `name` filter, then `renderIdentityBlock` for the `### <Label>`
202
214
  * markdown the hook injects.
203
215
  *
204
216
  * Pure: no I/O of its own (the only I/O is `handleIdentityGet` reading the
@@ -209,7 +221,10 @@ function buildIdentityToolResult(identityDir, identityClients, identityAgents, o
209
221
  // WARNING-2: forward `agent` so per-agent installs resolve the right scope.
210
222
  // An invalid id makes handleIdentityGet return a 400 → surfaced as isError.
211
223
  const query = opts.agent ? { agent: opts.agent } : {};
212
- const r = (0, identity_store_js_1.handleIdentityGet)(identityDir, identityClients, query, identityAgents);
224
+ // WARNING-1: inject the synthetic `memory` section exactly like REST + the
225
+ // SessionStart hook — via the ONE composition helper (serveIdentityBody,
226
+ // #313 CR3) so the tool's served order is the routes' served order.
227
+ const r = (0, identity_store_js_1.serveIdentityBody)((0, identity_store_js_1.handleIdentityGet)(identityDir, identityClients, query, identityAgents), opts.memoryInstructionsEnabled);
213
228
  if (r.status !== 200) {
214
229
  const errBody = r.body;
215
230
  return {
@@ -217,10 +232,6 @@ function buildIdentityToolResult(identityDir, identityClients, identityAgents, o
217
232
  isError: true,
218
233
  };
219
234
  }
220
- // WARNING-1: inject the synthetic `memory` section exactly like REST + the
221
- // SessionStart hook. `injectMemorySection` is a no-op when disabled or when
222
- // agent mode === "off".
223
- (0, memory_instructions_js_1.injectMemorySection)(r.body, opts.memoryInstructionsEnabled);
224
235
  const sections = r.body.sections ?? {};
225
236
  const filtered = opts.name
226
237
  ? (sections[opts.name] !== undefined ? { [opts.name]: sections[opts.name] } : {})
@@ -309,7 +309,7 @@ function createMcpServer() {
309
309
  const identityDir = (0, node_path_1.join)(stateDir, "identity");
310
310
  // Single pipeline shared with REST /identity + the SessionStart hook
311
311
  // (#264 CRITICAL + WARNING-1 + WARNING-2). The pure function owns
312
- // handleIdentityGet → injectMemorySection → renderIdentityBlock.
312
+ // handleIdentityGet → serveIdentityBody → renderIdentityBlock.
313
313
  const result = (0, learnings_identity_js_1.buildIdentityToolResult)(identityDir, identityClients, identityAgents, {
314
314
  name,
315
315
  agent,
@@ -975,14 +975,11 @@ async function startServer(options = {}) {
975
975
  const identityDir = (0, node_path_1.join)(stateDir, "identity");
976
976
  app.get("/identity", (req, res) => {
977
977
  try {
978
- const r = (0, identity_store_js_1.handleIdentityGet)(identityDir, identityClients, req.query, identityAgents);
979
- // #192: product-owned memory instructions ride as a synthetic read-only
980
- // `memory` section (config memoryInstructions !== false; agent mode
981
- // "off" respected inside the helper). Every harness renders it via the
982
- // shared section renderer zero client changes.
983
- if (r.status === 200) {
984
- (0, memory_instructions_js_1.injectMemorySection)(r.body, memoryInstructionsEnabled);
985
- }
978
+ // serveIdentityBody (#192 + #313): the ONE composition — synthetic
979
+ // product-owned `memory` section, then the SECTION_PRECEDENCE wire
980
+ // order. Shared with GET /context and the MCP hicortex_identity tool so
981
+ // every surface serves byte-identical ordering.
982
+ const r = (0, identity_store_js_1.serveIdentityBody)((0, identity_store_js_1.handleIdentityGet)(identityDir, identityClients, req.query, identityAgents), memoryInstructionsEnabled);
986
983
  res.status(r.status).json(r.body);
987
984
  }
988
985
  catch (err) {
@@ -1013,10 +1010,9 @@ async function startServer(options = {}) {
1013
1010
  // difference in behaviour, only the URL.
1014
1011
  app.get("/context", (req, res) => {
1015
1012
  try {
1016
- const r = (0, identity_store_js_1.handleIdentityGet)(identityDir, identityClients, req.query, identityAgents);
1017
- if (r.status === 200) {
1018
- (0, memory_instructions_js_1.injectMemorySection)(r.body, memoryInstructionsEnabled);
1019
- }
1013
+ // Same serveIdentityBody composition as GET /identity — the alias
1014
+ // serves byte-equal behaviour by construction.
1015
+ const r = (0, identity_store_js_1.serveIdentityBody)((0, identity_store_js_1.handleIdentityGet)(identityDir, identityClients, req.query, identityAgents), memoryInstructionsEnabled);
1020
1016
  res.status(r.status).json(r.body);
1021
1017
  }
1022
1018
  catch (err) {
package/dist/prompts.js CHANGED
@@ -130,6 +130,8 @@ EXTRACT into this markdown format:
130
130
 
131
131
  ### Project State Changes
132
132
  - [D] [SUBJECT]: [what changed], [from → to] (${date})
133
+ (Durable shifts only — the model/tool/approach the project now uses. A
134
+ status flip or counter change is NEVER-RECORD ephemera, not a state change)
133
135
 
134
136
  ### Key Entities & Relationships
135
137
  - [K] [entity A] → [relationship] → [entity B] (${date})
@@ -179,6 +181,23 @@ label, a sentiment ("Strong Negative"), or "User rejected…" is unfindable —
179
181
  matches every emotionally-similar prompt and no topically-relevant one. Front-load
180
182
  the subject; put reaction, intensity and reasoning AFTER it.
181
183
 
184
+ NEVER-RECORD — ephemera gate (critical):
185
+ Every candidate must pass one test: will it still be TRUE and still MATTER in 3 months?
186
+ Content whose entire value is a state that expires is NEVER recorded —
187
+ do not score it lower and write it anyway: OMIT it. A closed category of never-record ephemera:
188
+ - issue/PR/epic/merge status: "PR #173 merged", "Epic 2 CLOSED — all children merged"
189
+ - version/deploy/test-count statistics: "v0.16.0 deployed to rc dist-tag",
190
+ "Test suite grew 616→749", "Main branch updated to <sha>"
191
+ - session bookkeeping/outcomes: "Session closure: nothing remains to do",
192
+ todo deferrals ("parked until next week"), sync/replay states
193
+ - transient readings/snapshots: "disk usage snapshot: 84% at 14:20",
194
+ "uptime counter passed 40 days"
195
+ The durable part of the same event may still qualify — the CHOICE a change
196
+ embodies ("standardize on model X", user-confirmed) is [D], a configuration
197
+ that holds going forward is [K]; the version bump, merge, or count itself never is.
198
+ If EVERY item in the transcript is never-record ephemera, output ONLY:
199
+ "NO_EXTRACT" — zero memories is the correct result for a pure-status segment.
200
+
182
201
  RULES:
183
202
  - Extract MAX 20 items total (quality over quantity)
184
203
  - Use EXACT names/versions/paths/numbers as they appear in the transcript —
@@ -187,7 +206,8 @@ RULES:
187
206
  - Each must be useful if recalled in a future session
188
207
  - Skip: routine code edits, standard tool usage, trivial fixes
189
208
  - Include: architectural decisions, debugging breakthroughs, user preferences,
190
- tool configurations, API discoveries, project milestones
209
+ tool configurations, API discoveries, durable project milestones (a status
210
+ flip — shipped/merged/closed — is NEVER-RECORD ephemera, not a milestone)
191
211
  - PRIORITIZE Corrections & Rejections — these are high-value signals for learning
192
212
  what the user does NOT want. Even a single "no" or style correction is worth extracting.
193
213
  - Strong language or profanity from the user is a high-intensity signal — it indicates
package/dist/types.d.ts CHANGED
@@ -399,6 +399,20 @@ export interface HicortexConfig {
399
399
  * leaner system prompts.
400
400
  */
401
401
  lessonsLimit?: number;
402
+ /**
403
+ * Default project name for the OpenClaw plugin (#316, Hermes `default_project`
404
+ * parity): sent as the `project` fallback on /recall-index, the pre-0.14
405
+ * /search fallback, and the search/recent/ingest tools whenever the gateway
406
+ * supplies no project. Absent ⇒ no scope sent.
407
+ */
408
+ defaultProject?: string;
409
+ /**
410
+ * Max memories per recall on the OpenClaw plugin's legacy /search fallback
411
+ * (pre-0.14 servers, #316). Default 8. Does NOT size the pushed
412
+ * /recall-index — that is server config (`recallMaxItems`); the server
413
+ * accepts no client limit.
414
+ */
415
+ recallLimit?: number;
402
416
  /**
403
417
  * Soft cap on the memory corpus (default 10000). When the corpus exceeds this,
404
418
  * the nightly's capacity-eviction stage (#245) removes the lowest-
@@ -123,22 +123,38 @@ def _title_case_section(name: str) -> str:
123
123
  return " ".join(w[:1].upper() + w[1:] for w in words)
124
124
 
125
125
 
126
+ # #313 scope labels — mirror of the TS ``SECTION_LABELS`` in
127
+ # identity-store.ts (keep in sync). "Global rules" marks the section as
128
+ # fleet-wide; unknown section names fall back to title-case.
129
+ _SECTION_LABELS = {
130
+ "agent_identity": "Agent identity",
131
+ "user": "User",
132
+ "rules": "Global rules",
133
+ }
134
+
135
+
126
136
  def _order_section_names(names: Iterable[str]) -> List[str]:
127
- """Stable ordering: user, rules, then the rest alphabetically."""
137
+ """Stable ordering the #313 precedence contract, mirroring the TS
138
+ ``SECTION_PRECEDENCE`` (keep in sync): ``agent_identity`` (the agent's own
139
+ self + role conduct, per-agent only) first, then ``user`` (the principal),
140
+ then ``rules`` (fleet-wide house rules), then the rest alphabetically."""
128
141
  names = list(names)
129
- primaries = [p for p in ("user", "rules") if p in names]
130
- rest = sorted(n for n in names if n not in ("user", "rules"))
142
+ primaries = [p for p in ("agent_identity", "user", "rules") if p in names]
143
+ rest = sorted(n for n in names if n not in ("agent_identity", "user", "rules"))
131
144
  return primaries + rest
132
145
 
133
146
 
134
147
  def _render_context_block(sections: Dict[str, Any]) -> str:
135
- """Render the ``## Identity`` block, or "" when every section is blank."""
148
+ """Render the ``## Identity`` block, or "" when every section is blank.
149
+ Headings use the #313 scope labels (``_SECTION_LABELS``); unknown sections
150
+ fall back to title-case."""
136
151
  body_parts: List[str] = []
137
152
  for name in _order_section_names(sections.keys()):
138
153
  body = sections.get(name)
139
154
  if not isinstance(body, str) or not body.strip():
140
155
  continue
141
- body_parts.extend([f"### {_title_case_section(name)}", "", body.strip()])
156
+ label = _SECTION_LABELS.get(name) or _title_case_section(name)
157
+ body_parts.extend([f"### {label}", "", body.strip()])
142
158
  if not body_parts:
143
159
  return ""
144
160
  return "\n".join(["## Identity", "", *body_parts])
@@ -2,7 +2,7 @@
2
2
  "id": "hicortex",
3
3
  "name": "Hicortex — Long-term Memory That Learns",
4
4
  "description": "Your agents remember past decisions, avoid repeated mistakes, and get smarter every day. Nightly reflection generates actionable lessons that automatically update agent behavior.",
5
- "version": "0.19.1",
5
+ "version": "0.19.2",
6
6
  "kind": "lifecycle",
7
7
  "skills": ["./skills/hicortex-memory"],
8
8
  "configSchema": {
@@ -17,9 +17,14 @@
17
17
  "type": "string",
18
18
  "description": "Bearer token for the Hicortex server. Localhost bypasses auth automatically. Required for remote servers — use the authToken from ~/.hicortex/config.json on the server."
19
19
  },
20
- "licenseKey": {
20
+ "defaultProject": {
21
21
  "type": "string",
22
- "description": "Hicortex license key (hctx-...). Leave empty for free tier (250 memory cap)."
22
+ "description": "Optional default project name scoping recall, search, and ingest when the gateway supplies no project (Hermes default_project parity)."
23
+ },
24
+ "recallLimit": {
25
+ "type": "number",
26
+ "default": 8,
27
+ "description": "Max memories per recall on the pre-0.14 /search fallback (default 8). The pushed recall index is sized by SERVER config (recallMaxItems) — the server accepts no client limit."
23
28
  }
24
29
  },
25
30
  "required": []
@@ -34,10 +39,13 @@
34
39
  "sensitive": true,
35
40
  "placeholder": "Required for remote servers"
36
41
  },
37
- "licenseKey": {
38
- "label": "License Key",
39
- "placeholder": "hctx-... (optional, free tier without key)",
40
- "sensitive": true
42
+ "defaultProject": {
43
+ "label": "Default Project",
44
+ "placeholder": "Optional scopes recall/ingest when the gateway sends no project"
45
+ },
46
+ "recallLimit": {
47
+ "label": "Recall Limit",
48
+ "placeholder": "8"
41
49
  }
42
50
  }
43
51
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gamaze/hicortex",
3
- "version": "0.19.1",
3
+ "version": "0.19.2",
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, and Pi.",
5
5
  "main": "dist/index.js",
6
6
  "bin": {