@psiclawops/hypermem 0.9.7 → 0.9.9

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 (60) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/INSTALL.md +29 -9
  3. package/README.md +5 -1
  4. package/assets/default-config.json +20 -5
  5. package/assets/runtime-validation-fixture.json +123 -0
  6. package/bin/hypermem-cleanup.mjs +334 -0
  7. package/bin/hypermem-doctor.mjs +71 -0
  8. package/bin/hypermem-validate-runtime.mjs +282 -0
  9. package/dist/compositor.d.ts +43 -5
  10. package/dist/compositor.d.ts.map +1 -1
  11. package/dist/compositor.js +802 -30
  12. package/dist/entity-bridge-backfill.d.ts +66 -0
  13. package/dist/entity-bridge-backfill.d.ts.map +1 -0
  14. package/dist/entity-bridge-backfill.js +145 -0
  15. package/dist/entity-bridge-store.d.ts +164 -0
  16. package/dist/entity-bridge-store.d.ts.map +1 -0
  17. package/dist/entity-bridge-store.js +488 -0
  18. package/dist/entity-extractor.d.ts +124 -0
  19. package/dist/entity-extractor.d.ts.map +1 -0
  20. package/dist/entity-extractor.js +382 -0
  21. package/dist/entity-ppr.d.ts +55 -0
  22. package/dist/entity-ppr.d.ts.map +1 -0
  23. package/dist/entity-ppr.js +180 -0
  24. package/dist/hybrid-retrieval.d.ts +27 -0
  25. package/dist/hybrid-retrieval.d.ts.map +1 -1
  26. package/dist/hybrid-retrieval.js +26 -1
  27. package/dist/index.d.ts +19 -0
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +63 -13
  30. package/dist/message-store.d.ts +36 -0
  31. package/dist/message-store.d.ts.map +1 -1
  32. package/dist/message-store.js +155 -1
  33. package/dist/open-domain.d.ts +13 -4
  34. package/dist/open-domain.d.ts.map +1 -1
  35. package/dist/open-domain.js +222 -20
  36. package/dist/profiles.js +13 -13
  37. package/dist/question-shape.d.ts +73 -0
  38. package/dist/question-shape.d.ts.map +1 -0
  39. package/dist/question-shape.js +230 -0
  40. package/dist/schema.d.ts +1 -1
  41. package/dist/schema.d.ts.map +1 -1
  42. package/dist/schema.js +92 -1
  43. package/dist/topic-detector.d.ts.map +1 -1
  44. package/dist/topic-detector.js +22 -9
  45. package/dist/types.d.ts +176 -2
  46. package/dist/types.d.ts.map +1 -1
  47. package/dist/vector-store.d.ts +6 -0
  48. package/dist/vector-store.d.ts.map +1 -1
  49. package/dist/vector-store.js +3 -0
  50. package/docs/DIAGNOSTICS.md +32 -0
  51. package/docs/INTEGRATION_VALIDATION.md +9 -4
  52. package/docs/TUNING.md +21 -21
  53. package/memory-plugin/dist/index.js +3 -1
  54. package/memory-plugin/package.json +8 -7
  55. package/package.json +10 -4
  56. package/plugin/dist/index.d.ts.map +1 -1
  57. package/plugin/dist/index.js +114 -11
  58. package/plugin/dist/index.js.map +1 -1
  59. package/plugin/package.json +9 -8
  60. package/scripts/install-runtime.mjs +4 -1
package/docs/TUNING.md CHANGED
@@ -72,7 +72,7 @@ Estimates on a 200k model (Claude Sonnet). Scale proportionally for smaller wind
72
72
  | `standard` (default) | **35–50k** | 55–80k | All layers, default caps |
73
73
  | `full` | **40–55k** | 60–85k | All layers, raised caps, cross-session on |
74
74
 
75
- Standard turn 1 is lower than full because `warmHistoryBudgetFraction` is the same, but full enables cross-session context which adds tokens immediately; by turn 5 full overtakes standard as more layers accumulate history.
75
+ Standard turn 1 is intentionally conservative. Full keeps cross-session context opt-in and should be used only when operators accept the extra pressure.
76
76
 
77
77
  **Turn 1 is where token-conscious users will react.** Light vs full on turn 1 is roughly 25–40k tokens — the number that shows up in provider dashboards. By turn 5 the gap is still real, but the value case is easier to make because the user has already experienced continuity.
78
78
 
@@ -145,12 +145,12 @@ The standard semantic configuration. All memory layers are active with 0.9.4 rec
145
145
  "budgetFraction": 0.6,
146
146
  "contextWindowReserve": 0.25,
147
147
  "targetBudgetFraction": 0.5,
148
- "warmHistoryBudgetFraction": 0.45,
149
- "maxFacts": 28,
148
+ "warmHistoryBudgetFraction": 0.27,
149
+ "maxFacts": 25,
150
150
  "maxHistoryMessages": 250,
151
151
  "maxCrossSessionContext": 0,
152
- "keystoneHistoryFraction": 0.20,
153
- "keystoneMaxMessages": 15,
152
+ "keystoneHistoryFraction": 0.15,
153
+ "keystoneMaxMessages": 12,
154
154
  "hyperformProfile": "standard"
155
155
  },
156
156
  "indexer": {
@@ -187,11 +187,11 @@ For long-running sessions, multi-agent fleets, or any deployment where the agent
187
187
  "compositor": {
188
188
  "budgetFraction": 0.70,
189
189
  "contextWindowReserve": 0.22,
190
- "maxFacts": 40,
190
+ "maxFacts": 35,
191
191
  "maxHistoryMessages": 500,
192
- "maxCrossSessionContext": 6000,
193
- "keystoneHistoryFraction": 0.22,
194
- "keystoneMaxMessages": 20,
192
+ "maxCrossSessionContext": 4000,
193
+ "keystoneHistoryFraction": 0.18,
194
+ "keystoneMaxMessages": 18,
195
195
  "wikiTokenCap": 600,
196
196
  "hyperformProfile": "full"
197
197
  },
@@ -234,7 +234,7 @@ Three pre-built profiles ship with hypermem. Each configures every setting to a
234
234
  | Profile | Context window | Budget | Hyperform | Best for |
235
235
  |---|---|---|---|---|
236
236
  | `light` | 64k | 40k effective | `light` (behavior only) | Single agent, small models, constrained resources |
237
- | `standard` | 128k | 90k effective | `standard` (behavior + structure) | Normal deployments, small fleets |
237
+ | `standard` | 128k | ~77k effective before reserve | `standard` (behavior + structure) | Normal deployments, safe starter posture |
238
238
  | `full` | 200k+ | 160k effective | `full` (behavior + model adaptation) | Multi-agent fleets, large-context models |
239
239
 
240
240
  ```ts
@@ -432,10 +432,10 @@ effective budget × (1 - targetBudgetFraction) = history budget
432
432
  **Worked example (standard profile, 128k model):**
433
433
 
434
434
  ```
435
- 128,000 × 0.703 = 89,984 (effective budget before reserve)
436
- 89,984 × (1 - 0.25) = 67,488 (effective budget after reserve)
437
- 67,488 × 0.65 = 43,867 (context assembly budget)
438
- 67,488 × 0.35 = 23,621 (history budget)
435
+ 128,000 × 0.60 = 76,800 (effective budget before reserve)
436
+ 76,800 × (1 - 0.25) = 57,600 (effective budget after reserve)
437
+ 57,600 × 0.50 = 28,800 (context assembly budget)
438
+ 57,600 × 0.50 = 28,800 (history budget)
439
439
  ```
440
440
 
441
441
  **Model swap resilience:** The budget is computed from the model's actual context window at compose time when OpenClaw passes `tokenBudget`. If runtime metadata is missing, HyperMem falls back to `contextWindowOverrides` and then `contextWindowSize`. Structured tool history is guarded from being overwritten during a budget downshift — the compositor computes the new allocation but doesn't persist a lower-context snapshot to disk, preserving the full history for when the larger model returns.
@@ -542,7 +542,7 @@ This gives only 45% to context assembly and 55% to history. Useful for coding ag
542
542
  }
543
543
  ```
544
544
 
545
- Raises the fact injection cap from the default 30 to 50, and reduces wiki page space from 600 to 400 tokens.
545
+ Raises the fact injection cap from the default 25 to 50, and reduces wiki page space from 600 to 400 tokens.
546
546
 
547
547
  **More keystone history (recalled older messages):**
548
548
 
@@ -555,7 +555,7 @@ Raises the fact injection cap from the default 30 to 50, and reduces wiki page s
555
555
  }
556
556
  ```
557
557
 
558
- Reserves 30% of the history budget for keystones (up from default 20%) and allows up to 25 keystone messages (up from 15). Keystones are high-significance older messages that survive pressure trimming ahead of ordinary history.
558
+ Reserves 30% of the history budget for keystones (up from default 15%) and allows up to 25 keystone messages (up from 12). Keystones are high-significance older messages that survive pressure trimming ahead of ordinary history.
559
559
 
560
560
  ### Adjusting for model context size
561
561
 
@@ -880,7 +880,7 @@ Higher `contextWindowReserve` (0.30) gives more headroom for large tool results.
880
880
 
881
881
  | Knob | Type | Default | What it controls |
882
882
  |---|---|---|---|
883
- | `maxFacts` | number | 30 | Maximum facts surfaced per compose pass. |
883
+ | `maxFacts` | number | 25 | Maximum facts surfaced per compose pass. |
884
884
  | `wikiTokenCap` | tokens | 600 | Hard ceiling on wiki/knowledge injection per pass. |
885
885
  | `maxTotalTriggerTokens` | tokens | 4000 | Ceiling across all trigger-fired doc chunk collections. |
886
886
 
@@ -888,9 +888,9 @@ Higher `contextWindowReserve` (0.30) gives more headroom for large tool results.
888
888
 
889
889
  | Knob | Type | Default | What it controls |
890
890
  |---|---|---|---|
891
- | `maxHistoryMessages` | number | 500 | Maximum messages in the hot history window. |
892
- | `keystoneHistoryFraction` | 0.00.5 | 0.20 | Fraction of history budget reserved for keystones. 0 disables. |
893
- | `keystoneMaxMessages` | number | 15 | Max keystone messages injected per pass. |
891
+ | `maxHistoryMessages` | number | 250 | Maximum messages in the hot history window. |
892
+ | `keystoneHistoryFraction` | 0.0-0.5 | 0.15 | Fraction of history budget reserved for keystones. 0 disables. |
893
+ | `keystoneMaxMessages` | number | 12 | Max keystone messages injected per pass. |
894
894
  | `keystoneMinSignificance` | 0.0–1.0 | 0.5 | Minimum episode significance for keystone qualification. |
895
895
 
896
896
  ### Tool history
@@ -899,7 +899,7 @@ Higher `contextWindowReserve` (0.30) gives more headroom for large tool results.
899
899
  |---|---|---|---|
900
900
  | `maxRecentToolPairs` | number | 3 | Tool call/result pairs kept verbatim. |
901
901
  | `maxProseToolPairs` | number | 10 | Older pairs converted to prose stubs. Beyond this, payloads dropped. |
902
- | `maxCrossSessionContext` | tokens | 4000 | Token ceiling for cross-agent context. 0 disables. |
902
+ | `maxCrossSessionContext` | tokens | 0 | Token ceiling for cross-session context. 0 disables and is the default. |
903
903
 
904
904
  ### Dynamic reserve
905
905
 
@@ -181,9 +181,11 @@ function createMemorySearchManager(hm, agentId, workspaceDir) {
181
181
  try {
182
182
  const vectorStore = hm.getVectorStore();
183
183
  if (vectorStore) {
184
- const vectorResults = await hm.semanticSearch(agentId, query, {
184
+ const semanticSearch = hm.semanticSearch;
185
+ const vectorResults = await semanticSearch(agentId, query, {
185
186
  limit: maxResults,
186
187
  maxDistance: 1.5,
188
+ allowInlineQueryEmbedding: false,
187
189
  });
188
190
  for (const vr of vectorResults) {
189
191
  const score = 1.0 - (vr.distance / 2.0); // normalize distance to 0-1 score
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@psiclawops/hypermem-memory",
3
- "version": "0.9.7",
4
- "description": "HyperMem memory plugin for OpenClaw bridges HyperMem retrieval into the memory slot",
3
+ "version": "0.9.9",
4
+ "description": "HyperMem memory plugin for OpenClaw \u2014 bridges HyperMem retrieval into the memory slot",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "license": "Apache-2.0",
@@ -29,8 +29,8 @@
29
29
  "minGatewayVersion": "2026.4.12"
30
30
  },
31
31
  "build": {
32
- "openclawVersion": "2026.5.2",
33
- "pluginSdkVersion": "2026.5.2"
32
+ "openclawVersion": "2026.5.7",
33
+ "pluginSdkVersion": "2026.5.7"
34
34
  }
35
35
  },
36
36
  "scripts": {
@@ -40,10 +40,10 @@
40
40
  "typecheck": "tsc --noEmit"
41
41
  },
42
42
  "dependencies": {
43
- "@psiclawops/hypermem": "0.9.7"
43
+ "@psiclawops/hypermem": "0.9.9"
44
44
  },
45
45
  "devDependencies": {
46
- "openclaw": "2026.5.2",
46
+ "openclaw": "2026.5.7",
47
47
  "typescript": "^5.4.0"
48
48
  },
49
49
  "peerDependencies": {
@@ -64,6 +64,7 @@
64
64
  "LICENSE"
65
65
  ],
66
66
  "overrides": {
67
- "uuid": "14.0.0"
67
+ "uuid": "14.0.0",
68
+ "fast-xml-builder": "1.2.0"
68
69
  }
69
70
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@psiclawops/hypermem",
3
- "version": "0.9.7",
3
+ "version": "0.9.9",
4
4
  "description": "Agent-centric memory and context composition engine for OpenClaw",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -10,7 +10,9 @@
10
10
  "hypermem-install": "scripts/install-runtime.mjs",
11
11
  "hypermem-model-audit": "bin/hypermem-model-audit.mjs",
12
12
  "hypermem-bench": "bin/hypermem-bench.mjs",
13
- "hypermem-doctor": "bin/hypermem-doctor.mjs"
13
+ "hypermem-doctor": "bin/hypermem-doctor.mjs",
14
+ "hypermem-cleanup": "bin/hypermem-cleanup.mjs",
15
+ "hypermem-validate-runtime": "bin/hypermem-validate-runtime.mjs"
14
16
  },
15
17
  "exports": {
16
18
  ".": {
@@ -31,13 +33,14 @@
31
33
  "install:runtime": "node scripts/install-runtime.mjs",
32
34
  "dev": "tsc --watch",
33
35
  "typecheck": "tsc --noEmit",
34
- "test": "npm run build && node test/smoke.mjs && node test/lightweight-mode.mjs && node test/cross-agent.mjs && node test/vector-search.mjs && node test/embed-existing-coverage.mjs && node test/library.mjs && node test/compositor.mjs && node test/fleet-cache.mjs && node test/fleet-startup-seeding.mjs && node test/rotation.mjs && node test/knowledge-graph.mjs && node test/rate-limiter.mjs && node test/doc-chunker.mjs && node test/compaction-fence-gate.mjs && node test/hybrid-retrieval.mjs && node test/reranker-hotfix.mjs && node test/live-org-registry.mjs && node test/multi-turn-session.mjs && node test/secret-scanner.mjs && node --test test/content-type-classifier.mjs && node --test test/composition-snapshot-integrity.test.mjs && node --test test/composition-snapshot-store.test.mjs && node test/keystone-history.mjs && node test/cross-topic-keystone.mjs && node test/proactive-pass.mjs && node test/topic-synthesis.mjs && node test/session-topic-map.mjs && node test/virtual-sessions.mjs && node test/budget-downshift.mjs && node --test test/image-eviction.mjs && node test/fos-mod.mjs && node test/history-depth-estimator.mjs && node test/unified-pressure-signal.mjs && node test/sprint4-prompt-placement.mjs && node test/model-aware-budgeting-b4.mjs && node test/adaptive-lifecycle.mjs && node test/repair-tool-pairs.mjs && node test/tool-result-guards.mjs && node test/phase-c-fixtures.mjs && node test/oversized-artifact-c2.mjs && node test/oversized-artifact-guards.mjs && node test/doc-chunk-artifact-retrieval.mjs && node test/dreaming-promoter-temporal.mjs",
36
+ "test": "npm run build && node test/smoke.mjs && node test/lightweight-mode.mjs && node test/cross-agent.mjs && node test/vector-search.mjs && node test/embed-existing-coverage.mjs && node test/library.mjs && node test/compositor.mjs && node test/fleet-cache.mjs && node test/fleet-startup-seeding.mjs && node test/rotation.mjs && node test/knowledge-graph.mjs && node test/rate-limiter.mjs && node test/doc-chunker.mjs && node test/compaction-fence-gate.mjs && node test/hybrid-retrieval.mjs && node --test test/entity-extractor.mjs test/question-shape.mjs test/entity-bridge.mjs && node test/reranker-hotfix.mjs && node test/live-org-registry.mjs && node test/multi-turn-session.mjs && node test/secret-scanner.mjs && node --test test/content-type-classifier.mjs && node --test test/composition-snapshot-integrity.test.mjs && node --test test/composition-snapshot-store.test.mjs && node test/keystone-history.mjs && node test/cross-topic-keystone.mjs && node test/proactive-pass.mjs && node test/topic-synthesis.mjs && node test/session-topic-map.mjs && node test/virtual-sessions.mjs && node test/budget-downshift.mjs && node --test test/image-eviction.mjs && node test/fos-mod.mjs && node test/history-depth-estimator.mjs && node test/unified-pressure-signal.mjs && node test/sprint4-prompt-placement.mjs && node test/model-aware-budgeting-b4.mjs && node test/adaptive-lifecycle.mjs && node test/repair-tool-pairs.mjs && node test/tool-result-guards.mjs && node test/phase-c-fixtures.mjs && node test/oversized-artifact-c2.mjs && node test/oversized-artifact-guards.mjs && node test/doc-chunk-artifact-retrieval.mjs && node test/dreaming-promoter-temporal.mjs",
35
37
  "test:quick": "node test/smoke.mjs && node test/lightweight-mode.mjs && node test/library.mjs && node test/compositor.mjs",
36
38
  "validate:plugin-pipeline": "npm --prefix plugin run build && node test/plugin-pipeline.mjs && node test/batch-trim-b3.mjs",
37
39
  "validate:release-path": "npm run build && npm --prefix plugin run build && node test/release-gateway-path.mjs",
38
40
  "test:multi-turn": "node test/multi-turn-session.mjs",
39
41
  "test:ci": "npm test && npm --prefix plugin run typecheck",
40
42
  "smoke": "node scripts/smoke-test.mjs",
43
+ "validate:runtime": "node bin/hypermem-validate-runtime.mjs --data-dir /tmp/hypermem-runtime-validation --allow-no-embedding",
41
44
  "sync-public": "node scripts/sync-public.mjs",
42
45
  "validate:docs": "node scripts/validate-docs.mjs",
43
46
  "validate:config": "node scripts/validate-config-surface.mjs",
@@ -45,7 +48,7 @@
45
48
  "validate:public-surface": "node scripts/validate-public-surface.mjs",
46
49
  "bench:memory": "node bench/data-access-bench.mjs --iterations 1000 --warmup 50",
47
50
  "validate:doctor": "node test/doctor-cli.mjs",
48
- "build:all": "npm run build && npm --prefix plugin install --prefer-offline && npm --prefix plugin run build && npm --prefix memory-plugin install --prefer-offline && npm --prefix memory-plugin run build",
51
+ "build:all": "npm run build && npm --prefix plugin run build && npm --prefix memory-plugin run build",
49
52
  "validate:history-query": "npm run build && npm --prefix memory-plugin run build && node scripts/validate-history-query.mjs",
50
53
  "install:runtime:packed": "node scripts/install-packed-runtime.mjs",
51
54
  "release:install-smoke": "bash release-gate-internal/fresh-install-smoke.sh",
@@ -77,9 +80,12 @@
77
80
  "dist/**/*.d.ts",
78
81
  "dist/**/*.d.ts.map",
79
82
  "assets/default-config.json",
83
+ "assets/runtime-validation-fixture.json",
80
84
  "bin/hypermem-status.mjs",
81
85
  "bin/hypermem-model-audit.mjs",
82
86
  "bin/hypermem-doctor.mjs",
87
+ "bin/hypermem-cleanup.mjs",
88
+ "bin/hypermem-validate-runtime.mjs",
83
89
  "bench/README.md",
84
90
  "bench/data-access-bench.mjs",
85
91
  "bin/hypermem-bench.mjs",
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -812,6 +812,67 @@ function extractTextFromInboundContent(content) {
812
812
  .map(part => part.text ?? '')
813
813
  .join('\n');
814
814
  }
815
+ function isToolResultRole(role) {
816
+ return role === 'tool' || role === 'tool_result' || role === 'toolResult';
817
+ }
818
+ function isPersistableInboundRole(role) {
819
+ return role === 'user' || role === 'assistant' || role === 'system' || isToolResultRole(role);
820
+ }
821
+ function isSenderMetadataText(text) {
822
+ const trimmed = text.trim();
823
+ if (!trimmed)
824
+ return false;
825
+ if (/^Sender \(untrusted metadata\):/i.test(trimmed))
826
+ return true;
827
+ return stripMessageMetadata(trimmed).trim().length === 0 && /Sender \(untrusted metadata\):/i.test(trimmed);
828
+ }
829
+ function shouldDropInboundMessage(msg) {
830
+ if (isPersistableInboundRole(msg.role))
831
+ return false;
832
+ const text = extractTextFromInboundContent(msg.content);
833
+ // OpenClaw/webchat may carry adjacent sender envelopes as role='custom'. They
834
+ // are transport metadata, not conversation turns. Persisting or replaying them
835
+ // lets the metadata row become the latest assistant-ish history item, which can
836
+ // mask the real user prompt under load.
837
+ if (msg.role === 'custom' && isSenderMetadataText(text))
838
+ return true;
839
+ // Unknown roles are not part of the provider transcript contract. Dropping is
840
+ // safer than casting them to assistant in toNeutralMessage().
841
+ return true;
842
+ }
843
+ function sanitizeInboundMessageForModel(msg) {
844
+ if (shouldDropInboundMessage(msg))
845
+ return null;
846
+ // OpenClaw prepends untrusted inbound metadata directly to user-role text for
847
+ // the current provider prompt. That context is useful for the live inbound
848
+ // turn, but if HyperMem replays it from the message window it becomes duplicate
849
+ // user content and can make the model repeat the previous message. Strip it at
850
+ // the context-engine boundary while preserving the actual user text.
851
+ if (msg.role !== 'user')
852
+ return msg;
853
+ if (typeof msg.content === 'string') {
854
+ const stripped = stripMessageMetadata(msg.content);
855
+ return stripped === msg.content ? msg : { ...msg, content: stripped };
856
+ }
857
+ if (Array.isArray(msg.content)) {
858
+ let changed = false;
859
+ const content = msg.content.map((part) => {
860
+ if (!part || part.type !== 'text' || typeof part.text !== 'string')
861
+ return part;
862
+ const stripped = stripMessageMetadata(part.text);
863
+ if (stripped !== part.text)
864
+ changed = true;
865
+ return stripped === part.text ? part : { ...part, text: stripped };
866
+ });
867
+ return changed ? { ...msg, content } : msg;
868
+ }
869
+ return msg;
870
+ }
871
+ function sanitizeInboundMessagesForModel(messages) {
872
+ return messages
873
+ .map((msg) => sanitizeInboundMessageForModel(msg))
874
+ .filter((msg) => Boolean(msg));
875
+ }
815
876
  /**
816
877
  * Determine whether a user turn is "topic-bearing" (substantive).
817
878
  *
@@ -1045,7 +1106,7 @@ function toNeutralMessage(msg) {
1045
1106
  toolCalls = contentBlockToolCalls;
1046
1107
  }
1047
1108
  // OpenClaw uses role 'toolResult' (camelCase). Support all three spellings.
1048
- const isToolResultMsg = msg.role === 'tool' || msg.role === 'tool_result' || msg.role === 'toolResult';
1109
+ const isToolResultMsg = isToolResultRole(msg.role);
1049
1110
  // Tool results must stay on the result side of the transcript. If we persist them as
1050
1111
  // assistant rows with orphaned toolResults, later replay can retain a tool_result after
1051
1112
  // trimming away the matching assistant tool_use, which Anthropic rejects with a 400.
@@ -1066,6 +1127,27 @@ function toNeutralMessage(msg) {
1066
1127
  toolResults,
1067
1128
  };
1068
1129
  }
1130
+ function isPlainUserTurnMessage(msg) {
1131
+ if (msg.role !== 'user' || shouldDropInboundMessage(msg))
1132
+ return false;
1133
+ const neutral = toNeutralMessage(msg);
1134
+ return neutral.role === 'user'
1135
+ && !neutral.toolResults?.length
1136
+ && stripMessageMetadata(neutral.textContent ?? '').trim().length > 0;
1137
+ }
1138
+ function findBoundaryPlainUserMessage(messages, prePromptMessageCount) {
1139
+ const start = Math.min(prePromptMessageCount - 1, messages.length - 1);
1140
+ for (let i = start; i >= 0; i--) {
1141
+ const msg = messages[i];
1142
+ if (shouldDropInboundMessage(msg))
1143
+ continue;
1144
+ if (isPlainUserTurnMessage(msg))
1145
+ return msg;
1146
+ if (msg.role === 'assistant' || msg.role === 'system' || isToolResultRole(msg.role))
1147
+ return null;
1148
+ }
1149
+ return null;
1150
+ }
1069
1151
  // ─── Context Engine Implementation ─────────────────────────────
1070
1152
  /**
1071
1153
  * In-flight warm dedup map.
@@ -1639,7 +1721,7 @@ function createHyperMemEngine() {
1639
1721
  }
1640
1722
  // Skip system messages — they come from the runtime, not the conversation
1641
1723
  const msg = message;
1642
- if (msg.role === 'system') {
1724
+ if (msg.role === 'system' || shouldDropInboundMessage(msg)) {
1643
1725
  return { ingested: false };
1644
1726
  }
1645
1727
  try {
@@ -1754,7 +1836,7 @@ function createHyperMemEngine() {
1754
1836
  const agentId = extractAgentId(sk);
1755
1837
  for (const message of messages) {
1756
1838
  const msg = message;
1757
- if (msg.role === 'system')
1839
+ if (msg.role === 'system' || shouldDropInboundMessage(msg))
1758
1840
  continue;
1759
1841
  const neutral = toNeutralMessage(msg);
1760
1842
  if (neutral.role === 'user' && !neutral.toolResults?.length) {
@@ -1784,6 +1866,11 @@ function createHyperMemEngine() {
1784
1866
  * systemPromptAddition — facts/recall/episodes injected before runtime system prompt
1785
1867
  */
1786
1868
  async assemble({ sessionId, sessionKey, messages, tokenBudget, prompt, model }) {
1869
+ // Drop non-provider transcript metadata before any pass-through, token
1870
+ // estimate, or persistence-adjacent decision. The current user prompt is
1871
+ // carried separately as `prompt`; adjacent role='custom' sender envelopes
1872
+ // must never become the final model message.
1873
+ messages = sanitizeInboundMessagesForModel(messages);
1787
1874
  // ── Tool-loop guard ──────────────────────────────────────────────────────
1788
1875
  // When the last message is a toolResult, the runtime is mid tool-loop:
1789
1876
  // the model already has full context from the initial turn assembly.
@@ -2794,7 +2881,10 @@ ${replayRecovery.emittedText}`
2794
2881
  *
2795
2882
  * IMPORTANT: When afterTurn is defined, the runtime calls ONLY afterTurn —
2796
2883
  * it never calls ingest() or ingestBatch(). So we must ingest the new
2797
- * messages here, using messages.slice(prePromptMessageCount).
2884
+ * messages here. Some OpenClaw runtimes include the current user + adjacent
2885
+ * transport metadata in prePromptMessageCount, leaving only assistant/tool
2886
+ * outputs in messages.slice(prePromptMessageCount). Reconcile the boundary
2887
+ * user turn explicitly so SQLite preserves the real user→assistant order.
2798
2888
  */
2799
2889
  async afterTurn({ sessionId, sessionKey, messages, prePromptMessageCount, isHeartbeat, runtimeContext }) {
2800
2890
  if (isHeartbeat)
@@ -2803,12 +2893,25 @@ ${replayRecovery.emittedText}`
2803
2893
  const hm = await getHyperMem();
2804
2894
  const sk = resolveSessionKey(sessionId, sessionKey);
2805
2895
  const agentId = extractAgentId(sk);
2806
- // Ingest only the new messages produced this turn
2896
+ // Ingest only the new messages produced this turn, plus the boundary
2897
+ // user turn when OpenClaw counted it as pre-prompt context. The write
2898
+ // path is idempotent, so if an ingress/runtime variant already persisted
2899
+ // this user message, recordUserMessage() suppresses the duplicate.
2807
2900
  const newMessages = messages.slice(prePromptMessageCount);
2808
- for (const msg of newMessages) {
2901
+ const hasNewPlainUserMessage = newMessages
2902
+ .map(m => m)
2903
+ .some(m => isPlainUserTurnMessage(m));
2904
+ const boundaryUserMessage = hasNewPlainUserMessage
2905
+ ? null
2906
+ : findBoundaryPlainUserMessage(messages, prePromptMessageCount);
2907
+ const currentTurnMessages = boundaryUserMessage
2908
+ ? [boundaryUserMessage, ...newMessages]
2909
+ : newMessages;
2910
+ for (const msg of currentTurnMessages) {
2809
2911
  const m = msg;
2810
- // Skip system messages — they come from the runtime, not the conversation
2811
- if (m.role === 'system')
2912
+ // Skip system and non-provider metadata messages — they come from the
2913
+ // runtime/transport, not the conversation.
2914
+ if (m.role === 'system' || shouldDropInboundMessage(m))
2812
2915
  continue;
2813
2916
  if (m.role === 'toolResult' && extractTextFromInboundContent(m.content).trim() === SYNTHETIC_MISSING_TOOL_RESULT_TEXT) {
2814
2917
  const toolCallId = typeof m.toolCallId === 'string' ? m.toolCallId : 'unknown';
@@ -2844,7 +2947,7 @@ ${replayRecovery.emittedText}`
2844
2947
  }
2845
2948
  }
2846
2949
  try {
2847
- const lastAssistantMessage = [...newMessages].reverse().find(m => m.role === 'assistant');
2950
+ const lastAssistantMessage = [...currentTurnMessages].reverse().find(m => m.role === 'assistant');
2848
2951
  if (lastAssistantMessage) {
2849
2952
  const modelState = await hm.cache.getModelState(agentId, sk).catch(() => null);
2850
2953
  const promptCacheUsage = runtimeContext?.promptCache?.lastCallUsage;
@@ -2892,7 +2995,7 @@ ${replayRecovery.emittedText}`
2892
2995
  // Non-fatal: topic detection never blocks afterTurn
2893
2996
  let adaptiveTopicShiftConfidence;
2894
2997
  try {
2895
- const inboundUserMsg = newMessages
2998
+ const inboundUserMsg = currentTurnMessages
2896
2999
  .map(m => m)
2897
3000
  .find(m => m.role === 'user');
2898
3001
  if (inboundUserMsg) {
@@ -2953,7 +3056,7 @@ ${replayRecovery.emittedText}`
2953
3056
  const modelState = await hm.cache.getModelState(agentId, sk);
2954
3057
  const gradientBudget = modelState?.tokenBudget;
2955
3058
  const gradientDepth = modelState?.historyDepth;
2956
- const inboundUserMsg = newMessages
3059
+ const inboundUserMsg = currentTurnMessages
2957
3060
  .map(m => m)
2958
3061
  .find(m => m.role === 'user');
2959
3062
  const inboundUserText = inboundUserMsg