@psiclawops/hypercompositor 0.5.4 → 0.6.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.
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAIH,OAAO,KAAK,EACV,cAAc,EACd,eAAe,EACf,iBAAiB,EACjB,cAAc,EACd,aAAa,EAId,MAAM,sBAAsB,CAAC;AAU9B,YAAY,EAAE,cAAc,EAAE,eAAe,EAAE,iBAAiB,EAAE,cAAc,EAAE,aAAa,EAAE,CAAC;AAsrElG;;;;GAIG;AACH,wBAAsB,iBAAiB,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAU1F;;;;;;;;AAMD,wBAwBG"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAMH,OAAO,KAAK,EACV,cAAc,EACd,eAAe,EACf,iBAAiB,EACjB,cAAc,EACd,aAAa,EAId,MAAM,sBAAsB,CAAC;AAW9B,YAAY,EAAE,cAAc,EAAE,eAAe,EAAE,iBAAiB,EAAE,cAAc,EAAE,aAAa,EAAE,CAAC;AA6uElG;;;;GAIG;AACH,wBAAsB,iBAAiB,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAU1F;;;;;;;;AA8DD,wBA4CG"}
package/dist/index.js CHANGED
@@ -19,20 +19,24 @@
19
19
  *
20
20
  * Session key format expected: "agent:<agentId>:<channel>:<name>"
21
21
  */
22
- import { definePluginEntry, emptyPluginConfigSchema } from 'openclaw/plugin-sdk/plugin-entry';
23
- import { detectTopicShift, stripMessageMetadata, SessionTopicMap, applyToolGradientToWindow, canPersistReshapedHistory } from '@psiclawops/hypermem';
22
+ import { definePluginEntry } from 'openclaw/plugin-sdk/plugin-entry';
23
+ import { buildPluginConfigSchema } from 'openclaw/plugin-sdk/core';
24
+ import { z } from 'zod';
25
+ import { detectTopicShift, stripMessageMetadata, SessionTopicMap, applyToolGradientToWindow, canPersistReshapedHistory, OPENCLAW_BOOTSTRAP_FILES } from '@psiclawops/hypermem';
24
26
  import { evictStaleContent } from '@psiclawops/hypermem/image-eviction';
25
27
  import { repairToolPairs } from '@psiclawops/hypermem';
26
28
  import os from 'os';
27
29
  import path from 'path';
28
30
  import fs from 'fs/promises';
29
31
  import { createRequire } from 'module';
32
+ import { fileURLToPath } from 'url';
30
33
  // ─── hypermem singleton ────────────────────────────────────────
31
34
  // Runtime load is dynamic (hypermem is a sibling package loaded from repo dist,
32
35
  // not installed via npm). Types come from the core package devDependency.
33
36
  // This pattern keeps the runtime path stable while TypeScript resolves types
34
37
  // from the canonical source — no more local shim drift.
35
- const HYPERMEM_PATH = path.join(os.homedir(), '.openclaw/workspace/repo/hypermem/dist/index.js');
38
+ // Resolved at init time: pluginConfig.hyperMemPath > require.resolve('@psiclawops/hypermem') > dev fallback
39
+ let HYPERMEM_PATH = '';
36
40
  const require = createRequire(import.meta.url);
37
41
  let _hm = null;
38
42
  let _hmInitPromise = null;
@@ -108,25 +112,52 @@ function computeEffectiveBudget(tokenBudget) {
108
112
  // Derived from window config: floor to avoid fractional tokens
109
113
  return Math.floor(_contextWindowSize * (1 - _contextWindowReserve));
110
114
  }
115
+ // ─── Plugin config cache ───────────────────────────────────────
116
+ // Populated from openclaw.json plugins.entries.hypercompositor.config
117
+ // during register(). loadUserConfig() merges this over config.json.
118
+ let _pluginConfig = {};
111
119
  /**
112
- * Load optional user config from ~/.openclaw/hypermem/config.json.
113
- * Supports overriding compositor tuning knobs without editing plugin source.
114
- * Unknown keys are ignored. Missing file is silently skipped.
120
+ * Load user config with priority: pluginConfig (openclaw.json) > config.json (legacy).
121
+ * pluginConfig values win; config.json provides fallback for keys not set in openclaw.json.
122
+ * This allows gradual migration from the shadow config.json to central config.
115
123
  */
116
124
  async function loadUserConfig() {
117
- const configPath = path.join(os.homedir(), '.openclaw/hypermem/config.json');
125
+ // Resolve data dir: pluginConfig > default
126
+ const dataDir = _pluginConfig.dataDir ?? path.join(os.homedir(), '.openclaw/hypermem');
127
+ const configPath = path.join(dataDir, 'config.json');
128
+ let fileConfig = {};
118
129
  try {
119
130
  const raw = await fs.readFile(configPath, 'utf-8');
120
- const parsed = JSON.parse(raw);
121
- console.log(`[hypermem-plugin] Loaded user config from ${configPath}`);
122
- return parsed;
131
+ fileConfig = JSON.parse(raw);
132
+ console.log(`[hypermem-plugin] Loaded legacy config from ${configPath}`);
123
133
  }
124
134
  catch (err) {
125
135
  if (err.code !== 'ENOENT') {
126
136
  console.warn(`[hypermem-plugin] Failed to parse config.json (using defaults):`, err.message);
127
137
  }
128
- return {};
129
138
  }
139
+ // Merge: pluginConfig (openclaw.json) wins over fileConfig (legacy config.json).
140
+ // Top-level scalar keys from pluginConfig override fileConfig.
141
+ // Nested objects (compositor, eviction, embedding) are shallow-merged.
142
+ const merged = { ...fileConfig };
143
+ if (_pluginConfig.contextWindowSize != null)
144
+ merged.contextWindowSize = _pluginConfig.contextWindowSize;
145
+ if (_pluginConfig.contextWindowReserve != null)
146
+ merged.contextWindowReserve = _pluginConfig.contextWindowReserve;
147
+ if (_pluginConfig.deferToolPruning != null)
148
+ merged.deferToolPruning = _pluginConfig.deferToolPruning;
149
+ if (_pluginConfig.subagentWarming != null)
150
+ merged.subagentWarming = _pluginConfig.subagentWarming;
151
+ if (_pluginConfig.compositor)
152
+ merged.compositor = { ...merged.compositor, ..._pluginConfig.compositor };
153
+ if (_pluginConfig.eviction)
154
+ merged.eviction = { ...merged.eviction, ..._pluginConfig.eviction };
155
+ if (_pluginConfig.embedding)
156
+ merged.embedding = { ...merged.embedding, ..._pluginConfig.embedding };
157
+ if (Object.keys(fileConfig).length > 0 && Object.keys(_pluginConfig).filter(k => k !== 'hyperMemPath' && k !== 'dataDir').length > 0) {
158
+ console.log('[hypermem-plugin] Note: migrating config.json keys to plugins.entries.hypercompositor.config in openclaw.json is recommended');
159
+ }
160
+ return merged;
130
161
  }
131
162
  async function getHyperMem() {
132
163
  if (_hm)
@@ -150,16 +181,24 @@ async function getHyperMem() {
150
181
  // (VectorStore init) and the _generateEmbeddings closure above.
151
182
  if (userConfig.embedding) {
152
183
  const ue = userConfig.embedding;
184
+ // Provider-specific model/dimension/batch defaults
185
+ const providerDefaults = ue.provider === 'gemini'
186
+ ? { model: 'gemini-embedding-001', dimensions: 3072, batchSize: 100, timeout: 15000 }
187
+ : ue.provider === 'openai'
188
+ ? { model: 'text-embedding-3-small', dimensions: 1536, batchSize: 128, timeout: 10000 }
189
+ : { model: 'nomic-embed-text', dimensions: 768, batchSize: 32, timeout: 10000 };
153
190
  _embeddingConfig = {
154
191
  provider: ue.provider ?? 'ollama',
155
192
  ollamaUrl: ue.ollamaUrl ?? 'http://localhost:11434',
156
193
  openaiBaseUrl: ue.openaiBaseUrl ?? 'https://api.openai.com/v1',
157
194
  openaiApiKey: ue.openaiApiKey,
158
- // Apply provider-specific model + dimension defaults when not explicitly set
159
- model: ue.model ?? (ue.provider === 'openai' ? 'text-embedding-3-small' : 'nomic-embed-text'),
160
- dimensions: ue.dimensions ?? (ue.provider === 'openai' ? 1536 : 768),
161
- timeout: ue.timeout ?? 10000,
162
- batchSize: ue.batchSize ?? (ue.provider === 'openai' ? 128 : 32),
195
+ geminiBaseUrl: ue.geminiBaseUrl,
196
+ geminiIndexTaskType: ue.geminiIndexTaskType,
197
+ geminiQueryTaskType: ue.geminiQueryTaskType,
198
+ model: ue.model ?? providerDefaults.model,
199
+ dimensions: ue.dimensions ?? providerDefaults.dimensions,
200
+ timeout: ue.timeout ?? providerDefaults.timeout,
201
+ batchSize: ue.batchSize ?? providerDefaults.batchSize,
163
202
  };
164
203
  console.log(`[hypermem-plugin] Embedding provider: ${_embeddingConfig.provider} ` +
165
204
  `(model: ${_embeddingConfig.model}, ${_embeddingConfig.dimensions}d, batch: ${_embeddingConfig.batchSize})`);
@@ -192,7 +231,7 @@ async function getHyperMem() {
192
231
  `${Math.round(_contextWindowReserve * 100)}% reserved (${reservedTokens} tokens), ` +
193
232
  `effective history budget: ${_contextWindowSize - reservedTokens} tokens`);
194
233
  const instance = await HyperMem.create({
195
- dataDir: path.join(os.homedir(), '.openclaw/hypermem'),
234
+ dataDir: _pluginConfig.dataDir ?? path.join(os.homedir(), '.openclaw/hypermem'),
196
235
  cache: {
197
236
  keyPrefix: 'hm:',
198
237
  sessionTTL: 14400, // 4h for system/identity/meta slots
@@ -679,7 +718,7 @@ function createHyperMemEngine() {
679
718
  info: {
680
719
  id: 'hypermem',
681
720
  name: 'hypermem context engine',
682
- version: '0.5.3',
721
+ version: '0.5.4',
683
722
  // We own compaction — assemble() trims to budget via the compositor safety
684
723
  // valve, so runtime compaction is never needed. compact() handles any
685
724
  // explicit calls by trimming the Redis history window directly.
@@ -746,10 +785,12 @@ function createHyperMemEngine() {
746
785
  return { bootstrapped: true };
747
786
  }
748
787
  // Cold start: warm Redis with the session — pre-loads history + slots
749
- // CRIT-002: Load identity block (SOUL.md + IDENTITY.md + MOTIVATIONS.md)
750
- // and pass into warm() so the compositor identity slot is populated.
751
- // Previously opts.identity was always undefined the slot was allocated
752
- // but always empty. Non-fatal: missing files are silently skipped.
788
+ // CRIT-002: Load supplemental identity files (MOTIVATIONS.md, STYLE.md) that are
789
+ // NOT already injected by OpenClaw's contextInjection into the system prompt.
790
+ // SOUL.md and IDENTITY.md are filtered out here because OpenClaw injects them
791
+ // via workspace bootstrap — re-injecting them via the identity slot would cause
792
+ // duplication. Only agent-specific extras (MOTIVATIONS.md, STYLE.md) are included.
793
+ // Non-fatal: missing files are silently skipped.
753
794
  let identityBlock;
754
795
  try {
755
796
  // Council agents live at workspace-council/<agentId>/
@@ -764,7 +805,8 @@ function createHyperMemEngine() {
764
805
  catch {
765
806
  wsPath = workspacePath;
766
807
  }
767
- const identityFiles = ['SOUL.md', 'IDENTITY.md', 'MOTIVATIONS.md', 'STYLE.md'];
808
+ const identityFiles = ['SOUL.md', 'IDENTITY.md', 'MOTIVATIONS.md', 'STYLE.md']
809
+ .filter(f => !OPENCLAW_BOOTSTRAP_FILES.has(f));
768
810
  const parts = [];
769
811
  for (const fname of identityFiles) {
770
812
  try {
@@ -891,17 +933,24 @@ function createHyperMemEngine() {
891
933
  const redisTokens = await estimateWindowTokens(hm, agentId, sk);
892
934
  const effectiveBudget = computeEffectiveBudget(undefined);
893
935
  const redisPressure = redisTokens / effectiveBudget;
936
+ // Error tool results are always preserved intact — they're small and
937
+ // the model needs the error signal to understand what went wrong.
938
+ const hasErrorResult = neutral.toolResults.some(tr => tr.isError);
894
939
  if (redisPressure > 0.85) {
895
940
  // FIX (Bug 4): Never skip a tool result entirely — that leaves an orphaned
896
941
  // tool_call in Redis history (the assistant message was already recorded).
897
942
  // Anthropic rejects assistant messages with tool_calls that have no matching result.
898
943
  // Instead, record a compact stub that preserves pair integrity in history.
899
- const stubbedResults = neutral.toolResults.map(tr => ({
900
- ...tr,
901
- content: `[tool result omitted by wave-guard at ${(redisPressure * 100).toFixed(0)}% Redis pressure]`,
902
- }));
944
+ const stubbedResults = neutral.toolResults.map(tr => {
945
+ if (tr.isError)
946
+ return tr; // preserve error results intact
947
+ return {
948
+ ...tr,
949
+ content: `[tool result omitted by wave-guard at ${(redisPressure * 100).toFixed(0)}% Redis pressure]`,
950
+ };
951
+ });
903
952
  const stubNeutral = { ...neutral, toolResults: stubbedResults };
904
- console.log(`[hypermem] ingest wave-guard: stubbing toolResult (Redis pressure ${(redisPressure * 100).toFixed(0)}% > 85%) — preserving pair integrity`);
953
+ console.log(`[hypermem] ingest wave-guard: stubbing toolResult (Redis pressure ${(redisPressure * 100).toFixed(0)}% > 85%)${hasErrorResult ? ' error results preserved' : ''} — preserving pair integrity`);
905
954
  await hm.recordAssistantMessage(agentId, sk, stubNeutral);
906
955
  return { ingested: true };
907
956
  }
@@ -911,6 +960,8 @@ function createHyperMemEngine() {
911
960
  neutral = {
912
961
  ...neutral,
913
962
  toolResults: neutral.toolResults.map(tr => {
963
+ if (tr.isError)
964
+ return tr; // preserve error results intact
914
965
  const content = typeof tr.content === 'string' ? tr.content : JSON.stringify(tr.content);
915
966
  if (content.length <= MAX_TOOL_RESULT_CHARS)
916
967
  return tr;
@@ -920,7 +971,7 @@ function createHyperMemEngine() {
920
971
  };
921
972
  }),
922
973
  };
923
- console.log(`[hypermem] ingest wave-guard: truncated toolResult (Redis pressure ${(redisPressure * 100).toFixed(0)}% > 70%)`);
974
+ console.log(`[hypermem] ingest wave-guard: truncated toolResult (Redis pressure ${(redisPressure * 100).toFixed(0)}% > 70%)${hasErrorResult ? ' — error results preserved' : ''}`);
924
975
  }
925
976
  }
926
977
  await hm.recordAssistantMessage(agentId, sk, neutral);
@@ -2028,6 +2079,58 @@ export async function bustAssemblyCache(agentId, sessionKey) {
2028
2079
  // Non-fatal
2029
2080
  }
2030
2081
  }
2082
+ // ─── Plugin Config Schema ────────────────────────────────────────
2083
+ // Exposed via openclaw.json → plugins.entries.hypercompositor.config
2084
+ // Validated by OpenClaw on gateway start. Visible via `openclaw config get`.
2085
+ const hypercompositorConfigSchema = z.object({
2086
+ /** Path to HyperMem core dist/index.js. Auto-resolved if omitted. */
2087
+ hyperMemPath: z.string().optional(),
2088
+ /** HyperMem data directory. Default: ~/.openclaw/hypermem */
2089
+ dataDir: z.string().optional(),
2090
+ /** Full model context window size in tokens. Default: 128000 */
2091
+ contextWindowSize: z.number().int().positive().optional(),
2092
+ /** Fraction [0.0–0.5] reserved for system prompts + headroom. Default: 0.25 */
2093
+ contextWindowReserve: z.number().min(0).max(0.5).optional(),
2094
+ /** Defer tool pruning to OpenClaw's contextPruning. Default: false */
2095
+ deferToolPruning: z.boolean().optional(),
2096
+ /** Subagent context injection: 'full' | 'light' | 'off'. Default: 'light' */
2097
+ subagentWarming: z.enum(['full', 'light', 'off']).optional(),
2098
+ /** Compositor tuning overrides */
2099
+ compositor: z.object({
2100
+ defaultTokenBudget: z.number().int().positive().optional(),
2101
+ maxHistoryMessages: z.number().int().positive().optional(),
2102
+ maxFacts: z.number().int().positive().optional(),
2103
+ maxCrossSessionContext: z.number().int().nonnegative().optional(),
2104
+ maxRecentToolPairs: z.number().int().nonnegative().optional(),
2105
+ maxProseToolPairs: z.number().int().nonnegative().optional(),
2106
+ warmHistoryBudgetFraction: z.number().min(0).max(1).optional(),
2107
+ keystoneHistoryFraction: z.number().min(0).max(1).optional(),
2108
+ keystoneMaxMessages: z.number().int().nonnegative().optional(),
2109
+ keystoneMinSignificance: z.number().min(0).max(1).optional(),
2110
+ }).optional(),
2111
+ /** Image/tool eviction settings */
2112
+ eviction: z.object({
2113
+ enabled: z.boolean().optional(),
2114
+ imageAgeTurns: z.number().int().nonnegative().optional(),
2115
+ toolResultAgeTurns: z.number().int().nonnegative().optional(),
2116
+ minTokensToEvict: z.number().int().nonnegative().optional(),
2117
+ keepPreviewChars: z.number().int().nonnegative().optional(),
2118
+ }).optional(),
2119
+ /** Embedding provider config */
2120
+ embedding: z.object({
2121
+ provider: z.enum(['ollama', 'openai', 'gemini']).optional(),
2122
+ ollamaUrl: z.string().optional(),
2123
+ openaiApiKey: z.string().optional(),
2124
+ openaiBaseUrl: z.string().optional(),
2125
+ geminiBaseUrl: z.string().optional(),
2126
+ geminiIndexTaskType: z.string().optional(),
2127
+ geminiQueryTaskType: z.string().optional(),
2128
+ model: z.string().optional(),
2129
+ dimensions: z.number().int().positive().optional(),
2130
+ timeout: z.number().int().positive().optional(),
2131
+ batchSize: z.number().int().positive().optional(),
2132
+ }).optional(),
2133
+ });
2031
2134
  // ─── Plugin Entry ───────────────────────────────────────────────
2032
2135
  const engine = createHyperMemEngine();
2033
2136
  export default definePluginEntry({
@@ -2035,8 +2138,28 @@ export default definePluginEntry({
2035
2138
  name: 'HyperCompositor — context engine',
2036
2139
  description: 'Four-layer memory architecture for OpenClaw agents: Redis hot cache, message history, vector search, and structured library.',
2037
2140
  kind: 'context-engine',
2038
- configSchema: emptyPluginConfigSchema(),
2141
+ configSchema: buildPluginConfigSchema(hypercompositorConfigSchema),
2039
2142
  register(api) {
2143
+ // ── Resolve plugin config from openclaw.json ──
2144
+ const pluginCfg = (api.pluginConfig ?? {});
2145
+ _pluginConfig = pluginCfg;
2146
+ // ── Resolve HYPERMEM_PATH: pluginConfig > npm resolve > dev fallback ──
2147
+ if (pluginCfg.hyperMemPath) {
2148
+ HYPERMEM_PATH = pluginCfg.hyperMemPath;
2149
+ console.log(`[hypermem-plugin] Using configured hyperMemPath: ${HYPERMEM_PATH}`);
2150
+ }
2151
+ else {
2152
+ try {
2153
+ HYPERMEM_PATH = require.resolve('@psiclawops/hypermem');
2154
+ console.log(`[hypermem-plugin] Resolved @psiclawops/hypermem from node_modules: ${HYPERMEM_PATH}`);
2155
+ }
2156
+ catch {
2157
+ // Dev fallback: resolve relative to plugin directory
2158
+ const __pluginDir = path.dirname(fileURLToPath(import.meta.url));
2159
+ HYPERMEM_PATH = path.resolve(__pluginDir, '../../dist/index.js');
2160
+ console.log(`[hypermem-plugin] Falling back to dev path: ${HYPERMEM_PATH}`);
2161
+ }
2162
+ }
2040
2163
  api.registerContextEngine('hypercompositor', () => engine);
2041
2164
  // P1.7: Bind TaskFlow runtime for task visibility — best-effort.
2042
2165
  // Guard: api.runtime.taskFlow may not exist on older OpenClaw versions.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@psiclawops/hypercompositor",
3
- "version": "0.5.4",
3
+ "version": "0.6.2",
4
4
  "description": "HyperCompositor — context engine plugin for OpenClaw",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -23,7 +23,15 @@
23
23
  },
24
24
  "extensions": [
25
25
  "./dist/index.js"
26
- ]
26
+ ],
27
+ "compat": {
28
+ "pluginApi": ">=2026.4.5",
29
+ "minGatewayVersion": "2026.4.5"
30
+ },
31
+ "build": {
32
+ "openclawVersion": "2026.4.9",
33
+ "pluginSdkVersion": "2026.4.5"
34
+ }
27
35
  },
28
36
  "scripts": {
29
37
  "build": "tsc",