@psiclawops/hypermem 0.1.0 → 0.5.1

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.
Files changed (153) hide show
  1. package/ARCHITECTURE.md +4 -3
  2. package/README.md +457 -174
  3. package/dist/background-indexer.d.ts +19 -4
  4. package/dist/background-indexer.d.ts.map +1 -1
  5. package/dist/background-indexer.js +329 -17
  6. package/dist/cache.d.ts +110 -0
  7. package/dist/cache.d.ts.map +1 -0
  8. package/dist/cache.js +495 -0
  9. package/dist/compaction-fence.d.ts +1 -1
  10. package/dist/compaction-fence.js +1 -1
  11. package/dist/compositor.d.ts +114 -27
  12. package/dist/compositor.d.ts.map +1 -1
  13. package/dist/compositor.js +1678 -229
  14. package/dist/content-type-classifier.d.ts +41 -0
  15. package/dist/content-type-classifier.d.ts.map +1 -0
  16. package/dist/content-type-classifier.js +181 -0
  17. package/dist/cross-agent.d.ts +5 -0
  18. package/dist/cross-agent.d.ts.map +1 -1
  19. package/dist/cross-agent.js +5 -0
  20. package/dist/db.d.ts +1 -1
  21. package/dist/db.d.ts.map +1 -1
  22. package/dist/db.js +6 -2
  23. package/dist/desired-state-store.d.ts +1 -1
  24. package/dist/desired-state-store.d.ts.map +1 -1
  25. package/dist/desired-state-store.js +15 -5
  26. package/dist/doc-chunk-store.d.ts +26 -1
  27. package/dist/doc-chunk-store.d.ts.map +1 -1
  28. package/dist/doc-chunk-store.js +114 -1
  29. package/dist/doc-chunker.d.ts +1 -1
  30. package/dist/doc-chunker.js +1 -1
  31. package/dist/dreaming-promoter.d.ts +86 -0
  32. package/dist/dreaming-promoter.d.ts.map +1 -0
  33. package/dist/dreaming-promoter.js +381 -0
  34. package/dist/episode-store.d.ts +2 -1
  35. package/dist/episode-store.d.ts.map +1 -1
  36. package/dist/episode-store.js +4 -4
  37. package/dist/fact-store.d.ts +19 -1
  38. package/dist/fact-store.d.ts.map +1 -1
  39. package/dist/fact-store.js +64 -3
  40. package/dist/fleet-store.d.ts +1 -1
  41. package/dist/fleet-store.js +1 -1
  42. package/dist/fos-mod.d.ts +178 -0
  43. package/dist/fos-mod.d.ts.map +1 -0
  44. package/dist/fos-mod.js +416 -0
  45. package/dist/hybrid-retrieval.d.ts +5 -1
  46. package/dist/hybrid-retrieval.d.ts.map +1 -1
  47. package/dist/hybrid-retrieval.js +7 -3
  48. package/dist/image-eviction.d.ts +49 -0
  49. package/dist/image-eviction.d.ts.map +1 -0
  50. package/dist/image-eviction.js +251 -0
  51. package/dist/index.d.ts +50 -11
  52. package/dist/index.d.ts.map +1 -1
  53. package/dist/index.js +73 -43
  54. package/dist/keystone-scorer.d.ts +51 -0
  55. package/dist/keystone-scorer.d.ts.map +1 -0
  56. package/dist/keystone-scorer.js +52 -0
  57. package/dist/knowledge-graph.d.ts +1 -1
  58. package/dist/knowledge-graph.js +1 -1
  59. package/dist/knowledge-lint.d.ts +29 -0
  60. package/dist/knowledge-lint.d.ts.map +1 -0
  61. package/dist/knowledge-lint.js +116 -0
  62. package/dist/knowledge-store.d.ts +1 -1
  63. package/dist/knowledge-store.d.ts.map +1 -1
  64. package/dist/knowledge-store.js +8 -2
  65. package/dist/library-schema.d.ts +3 -3
  66. package/dist/library-schema.d.ts.map +1 -1
  67. package/dist/library-schema.js +324 -3
  68. package/dist/message-store.d.ts +15 -2
  69. package/dist/message-store.d.ts.map +1 -1
  70. package/dist/message-store.js +51 -1
  71. package/dist/metrics-dashboard.d.ts +114 -0
  72. package/dist/metrics-dashboard.d.ts.map +1 -0
  73. package/dist/metrics-dashboard.js +260 -0
  74. package/dist/obsidian-exporter.d.ts +57 -0
  75. package/dist/obsidian-exporter.d.ts.map +1 -0
  76. package/dist/obsidian-exporter.js +274 -0
  77. package/dist/obsidian-watcher.d.ts +147 -0
  78. package/dist/obsidian-watcher.d.ts.map +1 -0
  79. package/dist/obsidian-watcher.js +403 -0
  80. package/dist/open-domain.d.ts +46 -0
  81. package/dist/open-domain.d.ts.map +1 -0
  82. package/dist/open-domain.js +125 -0
  83. package/dist/preference-store.d.ts +1 -1
  84. package/dist/preference-store.js +1 -1
  85. package/dist/preservation-gate.d.ts +1 -1
  86. package/dist/preservation-gate.js +1 -1
  87. package/dist/proactive-pass.d.ts +63 -0
  88. package/dist/proactive-pass.d.ts.map +1 -0
  89. package/dist/proactive-pass.js +239 -0
  90. package/dist/profiles.d.ts +44 -0
  91. package/dist/profiles.d.ts.map +1 -0
  92. package/dist/profiles.js +227 -0
  93. package/dist/provider-translator.d.ts +13 -3
  94. package/dist/provider-translator.d.ts.map +1 -1
  95. package/dist/provider-translator.js +63 -9
  96. package/dist/rate-limiter.d.ts +1 -1
  97. package/dist/rate-limiter.js +1 -1
  98. package/dist/repair-tool-pairs.d.ts +38 -0
  99. package/dist/repair-tool-pairs.d.ts.map +1 -0
  100. package/dist/repair-tool-pairs.js +138 -0
  101. package/dist/retrieval-policy.d.ts +51 -0
  102. package/dist/retrieval-policy.d.ts.map +1 -0
  103. package/dist/retrieval-policy.js +77 -0
  104. package/dist/schema.d.ts +2 -2
  105. package/dist/schema.d.ts.map +1 -1
  106. package/dist/schema.js +28 -2
  107. package/dist/secret-scanner.d.ts +1 -1
  108. package/dist/secret-scanner.js +1 -1
  109. package/dist/seed.d.ts +2 -2
  110. package/dist/seed.js +2 -2
  111. package/dist/session-flusher.d.ts +53 -0
  112. package/dist/session-flusher.d.ts.map +1 -0
  113. package/dist/session-flusher.js +69 -0
  114. package/dist/session-topic-map.d.ts +41 -0
  115. package/dist/session-topic-map.d.ts.map +1 -0
  116. package/dist/session-topic-map.js +77 -0
  117. package/dist/spawn-context.d.ts +54 -0
  118. package/dist/spawn-context.d.ts.map +1 -0
  119. package/dist/spawn-context.js +159 -0
  120. package/dist/system-store.d.ts +1 -1
  121. package/dist/system-store.js +1 -1
  122. package/dist/temporal-store.d.ts +80 -0
  123. package/dist/temporal-store.d.ts.map +1 -0
  124. package/dist/temporal-store.js +149 -0
  125. package/dist/topic-detector.d.ts +35 -0
  126. package/dist/topic-detector.d.ts.map +1 -0
  127. package/dist/topic-detector.js +249 -0
  128. package/dist/topic-store.d.ts +1 -1
  129. package/dist/topic-store.js +1 -1
  130. package/dist/topic-synthesizer.d.ts +51 -0
  131. package/dist/topic-synthesizer.d.ts.map +1 -0
  132. package/dist/topic-synthesizer.js +315 -0
  133. package/dist/trigger-registry.d.ts +63 -0
  134. package/dist/trigger-registry.d.ts.map +1 -0
  135. package/dist/trigger-registry.js +163 -0
  136. package/dist/types.d.ts +214 -10
  137. package/dist/types.d.ts.map +1 -1
  138. package/dist/types.js +1 -1
  139. package/dist/vector-store.d.ts +43 -5
  140. package/dist/vector-store.d.ts.map +1 -1
  141. package/dist/vector-store.js +189 -10
  142. package/dist/version.d.ts +34 -0
  143. package/dist/version.d.ts.map +1 -0
  144. package/dist/version.js +34 -0
  145. package/dist/wiki-page-emitter.d.ts +65 -0
  146. package/dist/wiki-page-emitter.d.ts.map +1 -0
  147. package/dist/wiki-page-emitter.js +258 -0
  148. package/dist/work-store.d.ts +1 -1
  149. package/dist/work-store.js +1 -1
  150. package/package.json +15 -5
  151. package/dist/redis.d.ts +0 -188
  152. package/dist/redis.d.ts.map +0 -1
  153. package/dist/redis.js +0 -534
@@ -0,0 +1,227 @@
1
+ /**
2
+ * hypermem configuration profiles
3
+ *
4
+ * Pre-built configs for common deployment patterns. Pass to createHyperMem()
5
+ * directly or use as a base for custom configs via mergeProfile().
6
+ *
7
+ * Profiles:
8
+ * minimal — 64k context, single agent, low resource usage
9
+ * standard — 128k context, fleet default, balanced
10
+ * rich — 200k+ context, multi-agent, full feature set
11
+ */
12
+ // ---------------------------------------------------------------------------
13
+ // Shared base (fields common across all profiles)
14
+ // ---------------------------------------------------------------------------
15
+ const BASE_CACHE = {
16
+ keyPrefix: 'hm:',
17
+ sessionTTL: 3600,
18
+ historyTTL: 86400,
19
+ };
20
+ const BASE_EMBEDDING = {
21
+ ollamaUrl: 'http://localhost:11434',
22
+ model: 'nomic-embed-text',
23
+ dimensions: 768,
24
+ timeout: 10000,
25
+ batchSize: 32,
26
+ };
27
+ // ---------------------------------------------------------------------------
28
+ // light — 64k context window, single agent, constrained resources
29
+ //
30
+ // Design intent:
31
+ // - Small local models (Mistral 7B, Phi-3, Llama 3 8B) at 64k context
32
+ // - Single agent — no cross-session context needed
33
+ // - Minimal ACA stack — no dreaming, no background indexing overhead
34
+ // - Low Redis churn — longer flush intervals, shorter history window
35
+ // - outputProfile: 'light' — ~100 token standalone directives, no fleet concepts
36
+ // - No parallel operations — sequential fact extraction only
37
+ // ---------------------------------------------------------------------------
38
+ const LIGHT_COMPOSITOR = {
39
+ defaultTokenBudget: 40000, // leaves ~24k for model output at 64k window
40
+ maxHistoryMessages: 200, // keep window tight — small models lose coherence past ~150 msgs
41
+ maxFacts: 15, // surface top facts only, don't swamp the window
42
+ maxCrossSessionContext: 0, // single agent — no cross-session
43
+ maxRecentToolPairs: 2, // minimal tool history
44
+ maxProseToolPairs: 4,
45
+ warmHistoryBudgetFraction: 0.35, // slightly less history, more room for context
46
+ contextWindowReserve: 0.35, // generous reserve — small models need output headroom
47
+ dynamicReserveEnabled: true,
48
+ dynamicReserveTurnHorizon: 3, // shorter horizon — small sessions
49
+ dynamicReserveMax: 0.50,
50
+ keystoneHistoryFraction: 0.1, // light keystone — history window is already small
51
+ keystoneMaxMessages: 5,
52
+ keystoneMinSignificance: 0.7, // higher bar — only high-signal keystone messages
53
+ targetBudgetFraction: 0.50, // Anvil spec: 0.50 for light
54
+ maxTotalTriggerTokens: 1500, // tight trigger ceiling
55
+ outputProfile: 'light', // standalone density directives only, no fleet concepts
56
+ wikiTokenCap: 300, // Anvil spec: 300 for light
57
+ };
58
+ const LIGHT_INDEXER = {
59
+ enabled: true,
60
+ factExtractionMode: 'pattern', // pattern only — tiered extraction is heavier
61
+ topicDormantAfter: '12h', // faster dormancy — small systems don't need long windows
62
+ topicClosedAfter: '48h',
63
+ factDecayRate: 0.05, // slightly faster decay — fewer facts, keep them fresh
64
+ episodeSignificanceThreshold: 0.6, // higher bar — only store meaningful episodes
65
+ periodicInterval: 120000, // 2min — less frequent background work on small systems
66
+ batchSize: 64,
67
+ maxMessagesPerTick: 200,
68
+ };
69
+ export const lightProfile = {
70
+ enabled: true,
71
+ dataDir: './hypermem-data',
72
+ cache: BASE_CACHE,
73
+ compositor: LIGHT_COMPOSITOR,
74
+ indexer: LIGHT_INDEXER,
75
+ embedding: {
76
+ ...BASE_EMBEDDING,
77
+ batchSize: 8, // smaller batches — don't spike memory on embed
78
+ timeout: 15000, // more generous timeout — local hardware can be slow
79
+ },
80
+ // dreaming: disabled (default) — don't run background promotion on small systems
81
+ // obsidian: disabled (default)
82
+ };
83
+ // ---------------------------------------------------------------------------
84
+ // standard — 128k context window, fleet default, balanced
85
+ //
86
+ // Design intent:
87
+ // - Mid-range models (Sonnet, GPT-5-mini, Gemini Flash) at 128k
88
+ // - Small multi-agent setups or single power-user agents
89
+ // - Full ACA stack — dreaming optional, background indexing on
90
+ // - outputProfile: 'standard' — full FOS, no MOD
91
+ // ---------------------------------------------------------------------------
92
+ const STANDARD_COMPOSITOR = {
93
+ defaultTokenBudget: 90000,
94
+ maxHistoryMessages: 500,
95
+ maxFacts: 30,
96
+ maxCrossSessionContext: 4000,
97
+ maxRecentToolPairs: 3,
98
+ maxProseToolPairs: 10,
99
+ warmHistoryBudgetFraction: 0.40,
100
+ contextWindowReserve: 0.25,
101
+ dynamicReserveEnabled: true,
102
+ dynamicReserveTurnHorizon: 5,
103
+ dynamicReserveMax: 0.50,
104
+ keystoneHistoryFraction: 0.20,
105
+ keystoneMaxMessages: 15,
106
+ keystoneMinSignificance: 0.5,
107
+ targetBudgetFraction: 0.65, // Anvil spec: 0.65 for standard
108
+ maxTotalTriggerTokens: 4000,
109
+ outputProfile: 'standard', // full FOS, MOD suppressed
110
+ wikiTokenCap: 600, // Anvil spec: 600 for standard
111
+ };
112
+ const STANDARD_INDEXER = {
113
+ enabled: true,
114
+ factExtractionMode: 'tiered',
115
+ topicDormantAfter: '24h',
116
+ topicClosedAfter: '72h',
117
+ factDecayRate: 0.02,
118
+ episodeSignificanceThreshold: 0.5,
119
+ periodicInterval: 60000, // 1min — standard background cadence
120
+ batchSize: 128,
121
+ maxMessagesPerTick: 500,
122
+ };
123
+ export const standardProfile = {
124
+ enabled: true,
125
+ dataDir: './hypermem-data',
126
+ cache: BASE_CACHE,
127
+ compositor: STANDARD_COMPOSITOR,
128
+ indexer: STANDARD_INDEXER,
129
+ embedding: BASE_EMBEDDING,
130
+ };
131
+ // ---------------------------------------------------------------------------
132
+ // extended — 200k+ context window, multi-agent, full feature set
133
+ //
134
+ // Design intent:
135
+ // - Large context models (Opus, GPT-5.4, Gemini Pro) at 200k+
136
+ // - Council / multi-agent fleet deployments
137
+ // - Full ACA stack including dreaming, background indexing, cross-session
138
+ // - outputProfile: 'full' — FOS + MOD, full spec
139
+ // - Higher keystone threshold — more historical context worth surfacing
140
+ // ---------------------------------------------------------------------------
141
+ const EXTENDED_COMPOSITOR = {
142
+ defaultTokenBudget: 160000,
143
+ maxHistoryMessages: 1000,
144
+ maxFacts: 60,
145
+ maxCrossSessionContext: 12000,
146
+ maxRecentToolPairs: 5,
147
+ maxProseToolPairs: 15,
148
+ warmHistoryBudgetFraction: 0.45,
149
+ contextWindowReserve: 0.20,
150
+ dynamicReserveEnabled: true,
151
+ dynamicReserveTurnHorizon: 7,
152
+ dynamicReserveMax: 0.45,
153
+ keystoneHistoryFraction: 0.25,
154
+ keystoneMaxMessages: 30,
155
+ keystoneMinSignificance: 0.4,
156
+ targetBudgetFraction: 0.55, // Anvil spec: 0.55 for extended (history is huge, budget carefully)
157
+ maxTotalTriggerTokens: 10000,
158
+ outputProfile: 'full', // FOS + MOD — full fleet spec
159
+ wikiTokenCap: 800, // Anvil spec: 800 for extended
160
+ };
161
+ const EXTENDED_INDEXER = {
162
+ enabled: true,
163
+ factExtractionMode: 'tiered',
164
+ topicDormantAfter: '48h',
165
+ topicClosedAfter: '168h', // 7 days — long-running council topics stay warm
166
+ factDecayRate: 0.01, // slow decay — preserve more institutional memory
167
+ episodeSignificanceThreshold: 0.4,
168
+ periodicInterval: 30000, // 30s — frequent background work for fleet throughput
169
+ batchSize: 256,
170
+ maxMessagesPerTick: 1000,
171
+ };
172
+ export const fullProfile = {
173
+ enabled: true,
174
+ dataDir: './hypermem-data',
175
+ cache: BASE_CACHE,
176
+ compositor: EXTENDED_COMPOSITOR,
177
+ indexer: EXTENDED_INDEXER,
178
+ embedding: {
179
+ ...BASE_EMBEDDING,
180
+ batchSize: 64, // larger batches — more throughput for fleet ingest
181
+ timeout: 8000, // tighter timeout — expect capable hardware
182
+ },
183
+ };
184
+ // Legacy aliases — kept for backward compat, removed in 1.0
185
+ export const minimalProfile = lightProfile;
186
+ export const extendedProfile = fullProfile;
187
+ export const richProfile = fullProfile;
188
+ export const PROFILES = {
189
+ light: lightProfile,
190
+ standard: standardProfile,
191
+ full: fullProfile,
192
+ };
193
+ /**
194
+ * Load a named profile.
195
+ *
196
+ * @example
197
+ * const config = getProfile('light');
198
+ * const hm = createHyperMem(config);
199
+ */
200
+ export function getProfile(name) {
201
+ // backward compat
202
+ if (name === 'extended')
203
+ return structuredClone(fullProfile);
204
+ return structuredClone(PROFILES[name]);
205
+ }
206
+ /**
207
+ * Merge a partial config on top of a named profile.
208
+ * Deep-merges compositor and indexer; top-level fields are replaced.
209
+ *
210
+ * @example
211
+ * const config = mergeProfile('light', {
212
+ * cache: { keyPrefix: 'myapp:' },
213
+ * compositor: { outputProfile: 'standard' }, // upgrade tier
214
+ * });
215
+ */
216
+ export function mergeProfile(name, overrides) {
217
+ const base = getProfile(name);
218
+ return {
219
+ ...base,
220
+ ...overrides,
221
+ compositor: { ...base.compositor, ...(overrides.compositor ?? {}) },
222
+ indexer: { ...base.indexer, ...(overrides.indexer ?? {}) },
223
+ embedding: { ...base.embedding, ...(overrides.embedding ?? {}) },
224
+ cache: { ...base.cache, ...(overrides.cache ?? {}) },
225
+ };
226
+ }
227
+ //# sourceMappingURL=profiles.js.map
@@ -1,5 +1,5 @@
1
1
  /**
2
- * HyperMem Provider Translator
2
+ * hypermem Provider Translator
3
3
  *
4
4
  * Converts between provider-neutral (NeutralMessage) and provider-specific formats.
5
5
  * This is the ONLY place where provider-specific formatting exists.
@@ -10,12 +10,22 @@
10
10
  */
11
11
  import type { NeutralMessage, NeutralToolResult, ProviderMessage } from './types.js';
12
12
  /**
13
- * Generate a HyperMem-native tool call ID.
13
+ * Final pair-integrity sweep before provider translation.
14
+ *
15
+ * Invariant: never emit a tool_result unless its matching tool_use/tool_call
16
+ * exists in the immediately prior assistant message with the same ID.
17
+ *
18
+ * If the pair is broken, degrade the orphan tool_result into plain user text
19
+ * so providers never see an invalid tool_result block.
20
+ */
21
+ export declare function repairToolCallPairs(messages: NeutralMessage[]): NeutralMessage[];
22
+ /**
23
+ * Generate a hypermem-native tool call ID.
14
24
  * These are provider-neutral and deterministic within a session.
15
25
  */
16
26
  export declare function generateToolCallId(): string;
17
27
  /**
18
- * Convert a provider-specific tool call ID to a HyperMem ID.
28
+ * Convert a provider-specific tool call ID to a hypermem ID.
19
29
  * Deterministic: same input always produces same output.
20
30
  */
21
31
  export declare function normalizeToolCallId(providerId: string): string;
@@ -1 +1 @@
1
- {"version":3,"file":"provider-translator.d.ts","sourceRoot":"","sources":["../src/provider-translator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EACV,cAAc,EAEd,iBAAiB,EACjB,eAAe,EAChB,MAAM,YAAY,CAAC;AAOpB;;;GAGG;AACH,wBAAgB,kBAAkB,IAAI,MAAM,CAK3C;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAI9D;AAID,MAAM,MAAM,YAAY,GAAG,WAAW,GAAG,QAAQ,GAAG,kBAAkB,GAAG,SAAS,CAAC;AAEnF,wBAAgB,cAAc,CAAC,cAAc,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,YAAY,CAOtF;AAgMD;;GAEG;AACH,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,cAAc,EAAE,EAC1B,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAClC,eAAe,EAAE,CAcnB;AA8ED;;GAEG;AACH,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACjC,QAAQ,EAAE,MAAM,GACf,cAAc,CAYhB;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,cAAc,CAQxG;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,iBAAiB,EAAE,GAAG,cAAc,CAOjF"}
1
+ {"version":3,"file":"provider-translator.d.ts","sourceRoot":"","sources":["../src/provider-translator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EACV,cAAc,EAEd,iBAAiB,EACjB,eAAe,EAChB,MAAM,YAAY,CAAC;AAYpB;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,cAAc,EAAE,GAAG,cAAc,EAAE,CA4ChF;AAOD;;;GAGG;AACH,wBAAgB,kBAAkB,IAAI,MAAM,CAK3C;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAI9D;AAID,MAAM,MAAM,YAAY,GAAG,WAAW,GAAG,QAAQ,GAAG,kBAAkB,GAAG,SAAS,CAAC;AAEnF,wBAAgB,cAAc,CAAC,cAAc,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,YAAY,CAOtF;AAgMD;;GAEG;AACH,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,cAAc,EAAE,EAC1B,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAClC,eAAe,EAAE,CAenB;AA8ED;;GAEG;AACH,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACjC,QAAQ,EAAE,MAAM,GACf,cAAc,CAYhB;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,cAAc,CAQxG;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,iBAAiB,EAAE,GAAG,cAAc,CAOjF"}
@@ -1,5 +1,5 @@
1
1
  /**
2
- * HyperMem Provider Translator
2
+ * hypermem Provider Translator
3
3
  *
4
4
  * Converts between provider-neutral (NeutralMessage) and provider-specific formats.
5
5
  * This is the ONLY place where provider-specific formatting exists.
@@ -8,11 +8,64 @@
8
8
  * This eliminates grafting/stripping entirely — tool calls are stored as structured
9
9
  * data, and each provider gets the format it expects at send time.
10
10
  */
11
+ function summarizeOrphanToolResult(tr) {
12
+ const toolName = tr.name || 'tool';
13
+ const status = tr.isError ? 'error' : 'result';
14
+ const content = (tr.content || '').replace(/\s+/g, ' ').trim();
15
+ const preview = content.length > 160 ? `${content.slice(0, 157)}...` : content;
16
+ return preview
17
+ ? `[${toolName} ${status} omitted: missing matching tool call] ${preview}`
18
+ : `[${toolName} ${status} omitted: missing matching tool call]`;
19
+ }
20
+ /**
21
+ * Final pair-integrity sweep before provider translation.
22
+ *
23
+ * Invariant: never emit a tool_result unless its matching tool_use/tool_call
24
+ * exists in the immediately prior assistant message with the same ID.
25
+ *
26
+ * If the pair is broken, degrade the orphan tool_result into plain user text
27
+ * so providers never see an invalid tool_result block.
28
+ */
29
+ export function repairToolCallPairs(messages) {
30
+ const repaired = [];
31
+ for (const msg of messages) {
32
+ if (msg.role !== 'user' || !msg.toolResults || msg.toolResults.length === 0) {
33
+ repaired.push(msg);
34
+ continue;
35
+ }
36
+ const prev = repaired[repaired.length - 1];
37
+ const validCallIds = new Set(prev?.role === 'assistant' && prev.toolCalls
38
+ ? prev.toolCalls.map(tc => tc.id)
39
+ : []);
40
+ const keptResults = msg.toolResults.filter(tr => validCallIds.has(tr.callId));
41
+ const orphanResults = msg.toolResults.filter(tr => !validCallIds.has(tr.callId));
42
+ if (orphanResults.length === 0) {
43
+ repaired.push(msg);
44
+ continue;
45
+ }
46
+ const orphanText = orphanResults.map(summarizeOrphanToolResult).join('\n');
47
+ const mergedText = [msg.textContent, orphanText].filter(Boolean).join('\n');
48
+ if (keptResults.length > 0) {
49
+ repaired.push({
50
+ ...msg,
51
+ textContent: mergedText || msg.textContent,
52
+ toolResults: keptResults,
53
+ });
54
+ continue;
55
+ }
56
+ repaired.push({
57
+ ...msg,
58
+ textContent: mergedText || msg.textContent || '[tool result omitted: missing matching tool call]',
59
+ toolResults: null,
60
+ });
61
+ }
62
+ return repaired;
63
+ }
11
64
  import { createHash } from 'node:crypto';
12
65
  // ─── ID Generation ───────────────────────────────────────────────
13
66
  let idCounter = 0;
14
67
  /**
15
- * Generate a HyperMem-native tool call ID.
68
+ * Generate a hypermem-native tool call ID.
16
69
  * These are provider-neutral and deterministic within a session.
17
70
  */
18
71
  export function generateToolCallId() {
@@ -22,7 +75,7 @@ export function generateToolCallId() {
22
75
  return `hm_${timestamp}_${counter}`;
23
76
  }
24
77
  /**
25
- * Convert a provider-specific tool call ID to a HyperMem ID.
78
+ * Convert a provider-specific tool call ID to a hypermem ID.
26
79
  * Deterministic: same input always produces same output.
27
80
  */
28
81
  export function normalizeToolCallId(providerId) {
@@ -52,8 +105,8 @@ export function detectProvider(providerString) {
52
105
  * The last system message BEFORE the dynamicBoundary marker gets
53
106
  * cache_control: {type: "ephemeral"} to mark the static/dynamic boundary.
54
107
  *
55
- * Static (cacheable): system prompt + identity stable across sessions
56
- * Dynamic (not cacheable): context block (facts/recall), conversation history
108
+ * Static (cacheable): system prompt + identity + stable output profile prefix
109
+ * Dynamic (not cacheable): context block (facts/recall/recent actions), conversation history
57
110
  *
58
111
  * This allows Anthropic to cache the static prefix and skip re-tokenizing it.
59
112
  */
@@ -225,17 +278,18 @@ function toOpenAIResponses(messages) {
225
278
  * Convert neutral messages to provider-specific format.
226
279
  */
227
280
  export function toProviderFormat(messages, provider) {
281
+ const repairedMessages = repairToolCallPairs(messages);
228
282
  const providerType = detectProvider(provider);
229
283
  switch (providerType) {
230
284
  case 'anthropic':
231
- return toAnthropic(messages);
285
+ return toAnthropic(repairedMessages);
232
286
  case 'openai':
233
- return toOpenAI(messages);
287
+ return toOpenAI(repairedMessages);
234
288
  case 'openai-responses':
235
- return toOpenAIResponses(messages);
289
+ return toOpenAIResponses(repairedMessages);
236
290
  default:
237
291
  // Default to OpenAI format as it's most widely compatible
238
- return toOpenAI(messages);
292
+ return toOpenAI(repairedMessages);
239
293
  }
240
294
  }
241
295
  // ─── From Provider Format ────────────────────────────────────────
@@ -1,5 +1,5 @@
1
1
  /**
2
- * HyperMem Rate Limiter
2
+ * hypermem Rate Limiter
3
3
  *
4
4
  * Token-bucket rate limiter for embedding API calls.
5
5
  * Prevents hammering Ollama during bulk indexing.
@@ -1,5 +1,5 @@
1
1
  /**
2
- * HyperMem Rate Limiter
2
+ * hypermem Rate Limiter
3
3
  *
4
4
  * Token-bucket rate limiter for embedding API calls.
5
5
  * Prevents hammering Ollama during bulk indexing.
@@ -0,0 +1,38 @@
1
+ /**
2
+ * repair-tool-pairs.ts
3
+ *
4
+ * Strips orphaned tool result entries from a pi-agent message array.
5
+ *
6
+ * Background: HyperMem compaction and in-memory trim passes can remove assistant
7
+ * messages that contain tool_use/toolCall blocks without removing the corresponding
8
+ * tool result messages that follow them. Anthropic and Gemini reject these orphaned
9
+ * tool results with a 400 error.
10
+ *
11
+ * This module provides a pure repair function that can be applied at any output
12
+ * boundary to sanitise the message list before it reaches the provider.
13
+ *
14
+ * Supported formats:
15
+ * - pi-agent: role:'toolResult' messages with toolCallId field
16
+ * - Anthropic native: user messages with content blocks of type:'tool_result' and tool_use_id
17
+ *
18
+ * Returns a new array. Does not mutate the input.
19
+ */
20
+ type AnyMessage = Record<string, unknown>;
21
+ /**
22
+ * Repair orphaned tool pairs in a pi-agent / OpenClaw message array.
23
+ *
24
+ * Orphan types handled:
25
+ * 1. role:'toolResult' message whose toolCallId has no matching toolCall/tool_use
26
+ * block in any assistant message in the array.
27
+ * 2. User message whose content contains only type:'tool_result' blocks where all
28
+ * of those blocks reference a tool_use_id that does not appear in any assistant
29
+ * message in the array. (Anthropic-native format.)
30
+ *
31
+ * Also strips orphaned assistant messages that contain ONLY tool_use/toolCall blocks
32
+ * where none of those calls has a corresponding tool result anywhere in the array.
33
+ *
34
+ * Returns a new array (does not mutate input).
35
+ */
36
+ export declare function repairToolPairs(messages: AnyMessage[]): AnyMessage[];
37
+ export {};
38
+ //# sourceMappingURL=repair-tool-pairs.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"repair-tool-pairs.d.ts","sourceRoot":"","sources":["../src/repair-tool-pairs.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,KAAK,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAE1C;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,UAAU,EAAE,GAAG,UAAU,EAAE,CAmHpE"}
@@ -0,0 +1,138 @@
1
+ /**
2
+ * repair-tool-pairs.ts
3
+ *
4
+ * Strips orphaned tool result entries from a pi-agent message array.
5
+ *
6
+ * Background: HyperMem compaction and in-memory trim passes can remove assistant
7
+ * messages that contain tool_use/toolCall blocks without removing the corresponding
8
+ * tool result messages that follow them. Anthropic and Gemini reject these orphaned
9
+ * tool results with a 400 error.
10
+ *
11
+ * This module provides a pure repair function that can be applied at any output
12
+ * boundary to sanitise the message list before it reaches the provider.
13
+ *
14
+ * Supported formats:
15
+ * - pi-agent: role:'toolResult' messages with toolCallId field
16
+ * - Anthropic native: user messages with content blocks of type:'tool_result' and tool_use_id
17
+ *
18
+ * Returns a new array. Does not mutate the input.
19
+ */
20
+ /**
21
+ * Repair orphaned tool pairs in a pi-agent / OpenClaw message array.
22
+ *
23
+ * Orphan types handled:
24
+ * 1. role:'toolResult' message whose toolCallId has no matching toolCall/tool_use
25
+ * block in any assistant message in the array.
26
+ * 2. User message whose content contains only type:'tool_result' blocks where all
27
+ * of those blocks reference a tool_use_id that does not appear in any assistant
28
+ * message in the array. (Anthropic-native format.)
29
+ *
30
+ * Also strips orphaned assistant messages that contain ONLY tool_use/toolCall blocks
31
+ * where none of those calls has a corresponding tool result anywhere in the array.
32
+ *
33
+ * Returns a new array (does not mutate input).
34
+ */
35
+ export function repairToolPairs(messages) {
36
+ if (!Array.isArray(messages) || messages.length === 0)
37
+ return messages;
38
+ // ── Pass 1: Collect all valid tool call IDs from assistant messages ────────
39
+ const validCallIds = new Set();
40
+ for (const msg of messages) {
41
+ if (msg.role !== 'assistant')
42
+ continue;
43
+ // NeutralMessage format: msg.toolCalls[]
44
+ if (Array.isArray(msg.toolCalls)) {
45
+ for (const tc of msg.toolCalls) {
46
+ if (typeof tc.id === 'string' && tc.id)
47
+ validCallIds.add(tc.id);
48
+ }
49
+ }
50
+ // Content array format: type:'toolCall' or type:'tool_use' blocks
51
+ if (Array.isArray(msg.content)) {
52
+ for (const block of msg.content) {
53
+ if ((block.type === 'toolCall' || block.type === 'tool_use') &&
54
+ typeof block.id === 'string' &&
55
+ block.id) {
56
+ validCallIds.add(block.id);
57
+ }
58
+ }
59
+ }
60
+ }
61
+ // ── Pass 2: Collect all result IDs that have a valid call ─────────────────
62
+ const validResultIds = new Set();
63
+ for (const msg of messages) {
64
+ // pi-agent ToolResultMessage
65
+ if (msg.role === 'toolResult') {
66
+ const id = typeof msg.toolCallId === 'string' ? msg.toolCallId :
67
+ typeof msg.tool_call_id === 'string' ? msg.tool_call_id : '';
68
+ if (id && validCallIds.has(id))
69
+ validResultIds.add(id);
70
+ }
71
+ // Anthropic-native tool_result blocks inside user messages
72
+ if (msg.role === 'user' && Array.isArray(msg.content)) {
73
+ for (const block of msg.content) {
74
+ if (block.type === 'tool_result' && typeof block.tool_use_id === 'string' && block.tool_use_id) {
75
+ if (validCallIds.has(block.tool_use_id))
76
+ validResultIds.add(block.tool_use_id);
77
+ }
78
+ }
79
+ }
80
+ }
81
+ // ── Pass 3: Filter out orphaned messages / blocks ─────────────────────────
82
+ const result = [];
83
+ for (const msg of messages) {
84
+ // ── pi-agent ToolResultMessage ─────────────────────────────────────────
85
+ if (msg.role === 'toolResult') {
86
+ const id = typeof msg.toolCallId === 'string' ? msg.toolCallId :
87
+ typeof msg.tool_call_id === 'string' ? msg.tool_call_id : '';
88
+ if (!id || !validCallIds.has(id)) {
89
+ // Orphaned — drop
90
+ continue;
91
+ }
92
+ result.push(msg);
93
+ continue;
94
+ }
95
+ // ── Anthropic-native: user message with tool_result content blocks ─────
96
+ if (msg.role === 'user' && Array.isArray(msg.content)) {
97
+ const content = msg.content;
98
+ const hasToolResultBlocks = content.some(b => b.type === 'tool_result');
99
+ if (hasToolResultBlocks) {
100
+ const filteredContent = content.filter(block => {
101
+ if (block.type !== 'tool_result')
102
+ return true; // keep non-tool_result blocks
103
+ const toolUseId = typeof block.tool_use_id === 'string' ? block.tool_use_id : '';
104
+ return toolUseId && validCallIds.has(toolUseId);
105
+ });
106
+ // If the message became empty after stripping all orphaned tool_result blocks, skip it
107
+ if (filteredContent.length === 0)
108
+ continue;
109
+ result.push({ ...msg, content: filteredContent });
110
+ continue;
111
+ }
112
+ }
113
+ // ── Assistant message with ONLY unmatched tool_use/toolCall blocks ─────
114
+ if (msg.role === 'assistant' && Array.isArray(msg.content)) {
115
+ const content = msg.content;
116
+ const toolCallBlocks = content.filter(b => b.type === 'toolCall' || b.type === 'tool_use');
117
+ const nonToolCallBlocks = content.filter(b => b.type !== 'toolCall' && b.type !== 'tool_use');
118
+ // Only strip if the assistant message is purely tool-calls (no text)
119
+ if (toolCallBlocks.length > 0 && nonToolCallBlocks.length === 0) {
120
+ const hasAnyResult = toolCallBlocks.some(b => {
121
+ const id = typeof b.id === 'string' ? b.id : '';
122
+ return id && validResultIds.has(id);
123
+ });
124
+ if (!hasAnyResult) {
125
+ // Pure tool-call block with no paired results — drop
126
+ continue;
127
+ }
128
+ }
129
+ }
130
+ result.push(msg);
131
+ }
132
+ const dropped = messages.length - result.length;
133
+ if (dropped > 0) {
134
+ console.log(`[hypermem] repairToolPairs: dropped ${dropped} orphaned message(s) (${messages.length} → ${result.length})`);
135
+ }
136
+ return result;
137
+ }
138
+ //# sourceMappingURL=repair-tool-pairs.js.map
@@ -0,0 +1,51 @@
1
+ /**
2
+ * hypermem Retrieval Policy
3
+ *
4
+ * Single enforced policy layer for scope-based access control during retrieval.
5
+ * Called by the compositor to filter items before they are injected into context.
6
+ *
7
+ * Scope rules:
8
+ * 'agent' (default / null / undefined): allowed if agentId matches OR is null/undefined (global)
9
+ * 'session': allowed if both agentId AND sessionKey match
10
+ * 'user': allowed if agentId matches
11
+ * 'global': always allowed
12
+ * any other value: denied with reason 'ambiguous_scope'
13
+ */
14
+ export type RetrievalScope = 'agent' | 'session' | 'user' | 'global';
15
+ export interface RetrievalContext {
16
+ agentId: string;
17
+ sessionKey: string;
18
+ }
19
+ export interface ScopeCheckResult {
20
+ allowed: boolean;
21
+ /** One of: 'allowed' | 'scope_filtered' | 'ambiguous_scope' */
22
+ reason: string;
23
+ }
24
+ /**
25
+ * Check whether a single item is accessible in the given retrieval context.
26
+ *
27
+ * @param itemScope The scope stored on the item (null/undefined → defaults to 'agent')
28
+ * @param itemAgentId The agentId stored on the item (null/undefined → global)
29
+ * @param itemSessionKey The sessionKey stored on the item (null/undefined → any)
30
+ * @param ctx The requester's retrieval context
31
+ */
32
+ export declare function checkScope(itemScope: string | null | undefined, itemAgentId: string | null | undefined, itemSessionKey: string | null | undefined, ctx: RetrievalContext): ScopeCheckResult;
33
+ /**
34
+ * Filter an array of items by scope, returning allowed items and a filtered count.
35
+ *
36
+ * Items are expected to have optional `agentId`, `sessionKey`, and `scope` fields.
37
+ * Null/undefined fields are treated as "unset" (permissive for their slot).
38
+ *
39
+ * @param items Array of items to filter
40
+ * @param ctx The requester's retrieval context
41
+ * @returns { allowed: T[], filteredCount: number }
42
+ */
43
+ export declare function filterByScope<T extends {
44
+ agentId?: string | null;
45
+ sessionKey?: string | null;
46
+ scope?: string | null;
47
+ }>(items: T[], ctx: RetrievalContext): {
48
+ allowed: T[];
49
+ filteredCount: number;
50
+ };
51
+ //# sourceMappingURL=retrieval-policy.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"retrieval-policy.d.ts","sourceRoot":"","sources":["../src/retrieval-policy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,MAAM,MAAM,cAAc,GAAG,OAAO,GAAG,SAAS,GAAG,MAAM,GAAG,QAAQ,CAAC;AAErE,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,OAAO,CAAC;IACjB,+DAA+D;IAC/D,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;GAOG;AACH,wBAAgB,UAAU,CACxB,SAAS,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EACpC,WAAW,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EACtC,cAAc,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EACzC,GAAG,EAAE,gBAAgB,GACpB,gBAAgB,CAqClB;AAED;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAAC,CAAC,SAAS;IACtC,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACvB,EACC,KAAK,EAAE,CAAC,EAAE,EACV,GAAG,EAAE,gBAAgB,GACpB;IAAE,OAAO,EAAE,CAAC,EAAE,CAAC;IAAC,aAAa,EAAE,MAAM,CAAA;CAAE,CAczC"}