@psiclawops/hypercompositor 0.8.3 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +190 -0
- package/README.md +20 -0
- package/dist/index.d.ts +55 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +391 -44
- package/package.json +6 -6
package/LICENSE
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
6
|
+
|
|
7
|
+
1. Definitions.
|
|
8
|
+
|
|
9
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
10
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
11
|
+
|
|
12
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
13
|
+
the copyright owner that is granting the License.
|
|
14
|
+
|
|
15
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
16
|
+
other entities that control, are controlled by, or are under common
|
|
17
|
+
control with that entity. For the purposes of this definition,
|
|
18
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
19
|
+
direction or management of such entity, whether by contract or
|
|
20
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
21
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
22
|
+
|
|
23
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
24
|
+
exercising permissions granted by this License.
|
|
25
|
+
|
|
26
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
27
|
+
including but not limited to software source code, documentation
|
|
28
|
+
source, and configuration files.
|
|
29
|
+
|
|
30
|
+
"Object" form shall mean any form resulting from mechanical
|
|
31
|
+
transformation or translation of a Source form, including but
|
|
32
|
+
not limited to compiled object code, generated documentation,
|
|
33
|
+
and conversions to other media types.
|
|
34
|
+
|
|
35
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
36
|
+
Object form, made available under the License, as indicated by a
|
|
37
|
+
copyright notice that is included in or attached to the work
|
|
38
|
+
(an example is provided in the Appendix below).
|
|
39
|
+
|
|
40
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
41
|
+
form, that is based on (or derived from) the Work and for which the
|
|
42
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
43
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
44
|
+
of this License, Derivative Works shall not include works that remain
|
|
45
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
46
|
+
the Work and Derivative Works thereof.
|
|
47
|
+
|
|
48
|
+
"Contribution" shall mean any work of authorship, including
|
|
49
|
+
the original version of the Work and any modifications or additions
|
|
50
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
51
|
+
submitted to the Licensor for inclusion in the Work by the copyright owner
|
|
52
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
53
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
54
|
+
means any form of electronic, verbal, or written communication sent
|
|
55
|
+
to the Licensor or its representatives, including but not limited to
|
|
56
|
+
communication on electronic mailing lists, source code control systems,
|
|
57
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
58
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
59
|
+
excluding communication that is conspicuously marked or otherwise
|
|
60
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
61
|
+
|
|
62
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
63
|
+
on behalf of whom a Contribution has been received by the Licensor and
|
|
64
|
+
subsequently incorporated within the Work.
|
|
65
|
+
|
|
66
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
67
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
68
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
69
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
70
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
71
|
+
Work and such Derivative Works in Source or Object form.
|
|
72
|
+
|
|
73
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
74
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
75
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
76
|
+
(except as stated in this section) patent license to make, have made,
|
|
77
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
78
|
+
where such license applies only to those patent claims licensable
|
|
79
|
+
by such Contributor that are necessarily infringed by their
|
|
80
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
81
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
82
|
+
institute patent litigation against any entity (including a
|
|
83
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
84
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
85
|
+
or contributory patent infringement, then any patent licenses
|
|
86
|
+
granted to You under this License for that Work shall terminate
|
|
87
|
+
as of the date such litigation is filed.
|
|
88
|
+
|
|
89
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
90
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
91
|
+
modifications, and in Source or Object form, provided that You
|
|
92
|
+
meet the following conditions:
|
|
93
|
+
|
|
94
|
+
(a) You must give any other recipients of the Work or
|
|
95
|
+
Derivative Works a copy of this License; and
|
|
96
|
+
|
|
97
|
+
(b) You must cause any modified files to carry prominent notices
|
|
98
|
+
stating that You changed the files; and
|
|
99
|
+
|
|
100
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
101
|
+
that You distribute, all copyright, patent, trademark, and
|
|
102
|
+
attribution notices from the Source form of the Work,
|
|
103
|
+
excluding those notices that do not pertain to any part of
|
|
104
|
+
the Derivative Works; and
|
|
105
|
+
|
|
106
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
107
|
+
distribution, then any Derivative Works that You distribute must
|
|
108
|
+
include a readable copy of the attribution notices contained
|
|
109
|
+
within such NOTICE file, excluding any notices that do not
|
|
110
|
+
pertain to any part of the Derivative Works, in at least one
|
|
111
|
+
of the following places: within a NOTICE text file distributed
|
|
112
|
+
as part of the Derivative Works; within the Source form or
|
|
113
|
+
documentation, if provided along with the Derivative Works; or,
|
|
114
|
+
within a display generated by the Derivative Works, if and
|
|
115
|
+
wherever such third-party notices normally appear. The contents
|
|
116
|
+
of the NOTICE file are for informational purposes only and
|
|
117
|
+
do not modify the License. You may add Your own attribution
|
|
118
|
+
notices within Derivative Works that You distribute, alongside
|
|
119
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
120
|
+
that such additional attribution notices cannot be construed
|
|
121
|
+
as modifying the License.
|
|
122
|
+
|
|
123
|
+
You may add Your own copyright statement to Your modifications and
|
|
124
|
+
may provide additional or different license terms and conditions
|
|
125
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
126
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
127
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
128
|
+
the conditions stated in this License.
|
|
129
|
+
|
|
130
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
131
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
132
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
133
|
+
this License, without any additional terms or conditions.
|
|
134
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
135
|
+
the terms of any separate license agreement you may have executed
|
|
136
|
+
with Licensor regarding such Contributions.
|
|
137
|
+
|
|
138
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
139
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
140
|
+
except as required for reasonable and customary use in describing the
|
|
141
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
142
|
+
|
|
143
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
144
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
145
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
146
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
147
|
+
implied, including, without limitation, any warranties or conditions
|
|
148
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
149
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
150
|
+
appropriateness of using or redistributing the Work and assume any
|
|
151
|
+
risks associated with Your exercise of permissions under this License.
|
|
152
|
+
|
|
153
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
154
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
155
|
+
unless required by applicable law (such as deliberate and grossly
|
|
156
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
157
|
+
liable to You for damages, including any direct, indirect, special,
|
|
158
|
+
incidental, or consequential damages of any character arising as a
|
|
159
|
+
result of this License or out of the use or inability to use the
|
|
160
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
161
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
162
|
+
other commercial damages or losses), even if such Contributor
|
|
163
|
+
has been advised of the possibility of such damages.
|
|
164
|
+
|
|
165
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
166
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
167
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
168
|
+
or other liability obligations and/or rights consistent with this
|
|
169
|
+
License. However, in accepting such obligations, You may act only
|
|
170
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
171
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
172
|
+
defend, and hold each Contributor harmless for any liability
|
|
173
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
174
|
+
of your accepting any such warranty or additional liability.
|
|
175
|
+
|
|
176
|
+
END OF TERMS AND CONDITIONS
|
|
177
|
+
|
|
178
|
+
Copyright 2026 PsiClawOps
|
|
179
|
+
|
|
180
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
181
|
+
you may not use this file except in compliance with the License.
|
|
182
|
+
You may obtain a copy of the License at
|
|
183
|
+
|
|
184
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
185
|
+
|
|
186
|
+
Unless required by applicable law or agreed to in writing, software
|
|
187
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
188
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
189
|
+
See the License for the specific language governing permissions and
|
|
190
|
+
limitations under the License.
|
package/README.md
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# HyperCompositor
|
|
2
|
+
|
|
3
|
+
HyperCompositor is the OpenClaw context-engine plugin for HyperMem. It connects HyperMem's adaptive context lifecycle, recall, trimming, compaction, and diagnostics into the OpenClaw context assembly path.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @psiclawops/hypercompositor
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
See the main HyperMem install guide for runtime staging and OpenClaw activation:
|
|
12
|
+
|
|
13
|
+
- https://github.com/PsiClawOps/hypermem
|
|
14
|
+
- https://github.com/PsiClawOps/hypermem/blob/main/INSTALL.md
|
|
15
|
+
|
|
16
|
+
## Package
|
|
17
|
+
|
|
18
|
+
- Plugin id: `hypercompositor`
|
|
19
|
+
- Plugin kind: `context-engine`
|
|
20
|
+
- License: Apache-2.0
|
package/dist/index.d.ts
CHANGED
|
@@ -53,8 +53,36 @@ declare function assembleTrace(fields: {
|
|
|
53
53
|
path: 'cold' | 'replay' | 'subagent';
|
|
54
54
|
toolLoop: boolean;
|
|
55
55
|
msgCount: number;
|
|
56
|
+
prefixChanged?: boolean;
|
|
57
|
+
prefixHash?: string;
|
|
58
|
+
rerankerStatus?: string;
|
|
59
|
+
rerankerCandidates?: number;
|
|
60
|
+
rerankerProvider?: string | null;
|
|
61
|
+
slotSpans?: Record<string, {
|
|
62
|
+
allocated: number;
|
|
63
|
+
filled: number;
|
|
64
|
+
overflow: boolean;
|
|
65
|
+
}>;
|
|
66
|
+
compactionEligibleCount?: number;
|
|
67
|
+
compactionEligibleRatio?: number;
|
|
68
|
+
compactionProcessedCount?: number;
|
|
69
|
+
composeTopicSource?: 'request-topic-id' | 'session-topic-map' | 'none';
|
|
70
|
+
composeTopicState?: 'no-active-topic' | 'active-topic-ready' | 'active-topic-missing-stamped-history' | 'history-disabled';
|
|
71
|
+
composeTopicMessageCount?: number;
|
|
72
|
+
composeTopicStampedMessageCount?: number;
|
|
73
|
+
composeTopicTelemetryStatus?: 'emitted' | 'intentionally-omitted';
|
|
56
74
|
}): void;
|
|
57
75
|
declare function degradationTelemetry(fields: DegradationTelemetryFields): void;
|
|
76
|
+
declare function lifecyclePolicyTelemetry(fields: {
|
|
77
|
+
path: 'compose.eviction' | 'compose.preRecall' | 'afterTurn.gradient';
|
|
78
|
+
agentId: string;
|
|
79
|
+
sessionKey: string;
|
|
80
|
+
band: string;
|
|
81
|
+
pressurePct?: number;
|
|
82
|
+
topicShiftConfidence?: number;
|
|
83
|
+
trimSoftTarget?: number;
|
|
84
|
+
reasons?: string[];
|
|
85
|
+
}): void;
|
|
58
86
|
declare function nextTurnId(): string;
|
|
59
87
|
declare const GUARD_TELEMETRY_REASONS: readonly ["warmstart-pressure-demoted", "reshape-downshift-demoted", "duplicate-claim-suppressed", "afterturn-secondary-demoted", "window-within-budget-skip", "pressure-accounting-anomaly"];
|
|
60
88
|
type GuardTelemetryReason = typeof GUARD_TELEMETRY_REASONS[number];
|
|
@@ -107,6 +135,7 @@ export declare const __telemetryForTests: {
|
|
|
107
135
|
assembleTrace: typeof assembleTrace;
|
|
108
136
|
degradationTelemetry: typeof degradationTelemetry;
|
|
109
137
|
guardTelemetry: typeof guardTelemetry;
|
|
138
|
+
lifecyclePolicyTelemetry: typeof lifecyclePolicyTelemetry;
|
|
110
139
|
nextTurnId: typeof nextTurnId;
|
|
111
140
|
beginTrimOwnerTurn: typeof beginTrimOwnerTurn;
|
|
112
141
|
endTrimOwnerTurn: typeof endTrimOwnerTurn;
|
|
@@ -136,6 +165,32 @@ export declare function resolveEffectiveBudget(args: {
|
|
|
136
165
|
budget: number;
|
|
137
166
|
source: string;
|
|
138
167
|
};
|
|
168
|
+
export interface ModelIdentity {
|
|
169
|
+
rawModel: string | null;
|
|
170
|
+
modelKey: string | null;
|
|
171
|
+
provider: string | null;
|
|
172
|
+
modelId: string | null;
|
|
173
|
+
}
|
|
174
|
+
export declare function resolveModelIdentity(model?: string): ModelIdentity;
|
|
175
|
+
export declare function diffModelState(previous: {
|
|
176
|
+
model?: string;
|
|
177
|
+
modelKey?: string | null;
|
|
178
|
+
provider?: string | null;
|
|
179
|
+
modelId?: string | null;
|
|
180
|
+
tokenBudget?: number;
|
|
181
|
+
} | null | undefined, current: {
|
|
182
|
+
model?: string;
|
|
183
|
+
tokenBudget?: number;
|
|
184
|
+
}): {
|
|
185
|
+
previousIdentity: ModelIdentity;
|
|
186
|
+
currentIdentity: ModelIdentity;
|
|
187
|
+
modelChanged: boolean;
|
|
188
|
+
providerChanged: boolean;
|
|
189
|
+
modelIdChanged: boolean;
|
|
190
|
+
budgetChanged: boolean;
|
|
191
|
+
budgetDownshift: boolean;
|
|
192
|
+
budgetUplift: boolean;
|
|
193
|
+
};
|
|
139
194
|
/**
|
|
140
195
|
* Bust the assembly cache for a specific agent+session.
|
|
141
196
|
* Call this after writing to identity files (SOUL.md, IDENTITY.md, TOOLS.md,
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAaH,OAAO,KAAK,EACV,cAAc,EACd,eAAe,EACf,iBAAiB,EACjB,cAAc,EACd,aAAa,EAId,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAUL,kBAAkB,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -22,12 +22,15 @@
|
|
|
22
22
|
import { definePluginEntry } from 'openclaw/plugin-sdk/plugin-entry';
|
|
23
23
|
import { buildPluginConfigSchema } from 'openclaw/plugin-sdk/core';
|
|
24
24
|
import { z } from 'zod';
|
|
25
|
-
import { detectTopicShift, stripMessageMetadata, SessionTopicMap, applyToolGradientToWindow, OPENCLAW_BOOTSTRAP_FILES, rotateSessionContext, TRIM_SOFT_TARGET, TRIM_GROWTH_THRESHOLD, TRIM_HEADROOM_FRACTION, resolveTrimBudgets, formatToolChainStub, decideReplayRecovery, isReplayState,
|
|
25
|
+
import { detectTopicShift, stripMessageMetadata, SessionTopicMap, applyToolGradientToWindow, OPENCLAW_BOOTSTRAP_FILES, rotateSessionContext, TRIM_SOFT_TARGET, TRIM_GROWTH_THRESHOLD, TRIM_HEADROOM_FRACTION, resolveTrimBudgets, resolveAdaptiveLifecyclePolicy, formatToolChainStub, decideReplayRecovery, isReplayState, recordOutputMetrics,
|
|
26
|
+
// Sprint 3: unified pressure signal
|
|
27
|
+
computeUnifiedPressure, PRESSURE_SOURCE, } from '@psiclawops/hypermem';
|
|
26
28
|
import { evictStaleContent } from '@psiclawops/hypermem/image-eviction';
|
|
27
29
|
import { repairToolPairs } from '@psiclawops/hypermem';
|
|
28
30
|
import os from 'os';
|
|
29
31
|
import path from 'path';
|
|
30
32
|
import fs from 'fs/promises';
|
|
33
|
+
import { randomUUID } from 'node:crypto';
|
|
31
34
|
import { fileURLToPath } from 'url';
|
|
32
35
|
import fsSync from 'fs';
|
|
33
36
|
let _telemetryStream = null;
|
|
@@ -107,6 +110,24 @@ function degradationTelemetry(fields) {
|
|
|
107
110
|
// Telemetry must never throw
|
|
108
111
|
}
|
|
109
112
|
}
|
|
113
|
+
function lifecyclePolicyTelemetry(fields) {
|
|
114
|
+
if (!telemetryEnabled())
|
|
115
|
+
return;
|
|
116
|
+
const stream = getTelemetryStream();
|
|
117
|
+
if (!stream)
|
|
118
|
+
return;
|
|
119
|
+
try {
|
|
120
|
+
const record = {
|
|
121
|
+
event: 'lifecycle-policy',
|
|
122
|
+
ts: new Date().toISOString(),
|
|
123
|
+
...fields,
|
|
124
|
+
};
|
|
125
|
+
stream.write(JSON.stringify(record) + '\n');
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
// Telemetry must never throw
|
|
129
|
+
}
|
|
130
|
+
}
|
|
110
131
|
function nextTurnId() {
|
|
111
132
|
_telemetryTurnCounter = (_telemetryTurnCounter + 1) >>> 0;
|
|
112
133
|
return `${Date.now().toString(36)}-${_telemetryTurnCounter.toString(36)}`;
|
|
@@ -279,6 +300,7 @@ export const __telemetryForTests = {
|
|
|
279
300
|
assembleTrace,
|
|
280
301
|
degradationTelemetry,
|
|
281
302
|
guardTelemetry,
|
|
303
|
+
lifecyclePolicyTelemetry,
|
|
282
304
|
nextTurnId,
|
|
283
305
|
beginTrimOwnerTurn,
|
|
284
306
|
endTrimOwnerTurn,
|
|
@@ -400,6 +422,48 @@ export function resolveEffectiveBudget(args) {
|
|
|
400
422
|
source: 'fallback contextWindowSize',
|
|
401
423
|
};
|
|
402
424
|
}
|
|
425
|
+
export function resolveModelIdentity(model) {
|
|
426
|
+
const modelKey = normalizeModelKey(model);
|
|
427
|
+
if (!modelKey) {
|
|
428
|
+
return {
|
|
429
|
+
rawModel: model ?? null,
|
|
430
|
+
modelKey: null,
|
|
431
|
+
provider: null,
|
|
432
|
+
modelId: null,
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
const slash = modelKey.indexOf('/');
|
|
436
|
+
return {
|
|
437
|
+
rawModel: model ?? null,
|
|
438
|
+
modelKey,
|
|
439
|
+
provider: slash > 0 ? modelKey.slice(0, slash) : null,
|
|
440
|
+
modelId: slash > 0 && slash < modelKey.length - 1 ? modelKey.slice(slash + 1) : modelKey,
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
export function diffModelState(previous, current) {
|
|
444
|
+
const previousIdentity = previous?.modelKey || previous?.provider || previous?.modelId
|
|
445
|
+
? {
|
|
446
|
+
rawModel: previous.model ?? null,
|
|
447
|
+
modelKey: previous.modelKey ?? normalizeModelKey(previous.model),
|
|
448
|
+
provider: previous.provider ?? resolveModelIdentity(previous.model).provider,
|
|
449
|
+
modelId: previous.modelId ?? resolveModelIdentity(previous.model).modelId,
|
|
450
|
+
}
|
|
451
|
+
: resolveModelIdentity(previous?.model);
|
|
452
|
+
const currentIdentity = resolveModelIdentity(current.model);
|
|
453
|
+
const previousBudget = previous?.tokenBudget;
|
|
454
|
+
const currentBudget = current.tokenBudget;
|
|
455
|
+
const budgetChanged = previousBudget != null && currentBudget != null && previousBudget !== currentBudget;
|
|
456
|
+
return {
|
|
457
|
+
previousIdentity,
|
|
458
|
+
currentIdentity,
|
|
459
|
+
modelChanged: previousIdentity.modelKey !== currentIdentity.modelKey,
|
|
460
|
+
providerChanged: previousIdentity.provider !== currentIdentity.provider,
|
|
461
|
+
modelIdChanged: previousIdentity.modelId !== currentIdentity.modelId,
|
|
462
|
+
budgetChanged,
|
|
463
|
+
budgetDownshift: previousBudget != null && currentBudget != null && currentBudget < previousBudget,
|
|
464
|
+
budgetUplift: previousBudget != null && currentBudget != null && currentBudget > previousBudget,
|
|
465
|
+
};
|
|
466
|
+
}
|
|
403
467
|
function normalizeModelKey(model) {
|
|
404
468
|
if (!model)
|
|
405
469
|
return null;
|
|
@@ -422,6 +486,7 @@ function resolveConfiguredWindow(model) {
|
|
|
422
486
|
// Subagent warming mode: 'full' | 'light' | 'off'. Default: 'light'.
|
|
423
487
|
// Controls how much HyperMem context is injected into subagent sessions.
|
|
424
488
|
let _subagentWarming = 'light';
|
|
489
|
+
const FORKED_CONTEXT_META_SLOT = 'forkedContextMeta';
|
|
425
490
|
// Cache replay threshold: 15min default. Set to 0 in user config to disable.
|
|
426
491
|
let _cacheReplayThresholdMs = 900_000;
|
|
427
492
|
// ─── System overhead cache ────────────────────────────────────
|
|
@@ -537,6 +602,8 @@ async function loadUserConfig() {
|
|
|
537
602
|
merged.eviction = { ...merged.eviction, ..._pluginConfig.eviction };
|
|
538
603
|
if (_pluginConfig.embedding)
|
|
539
604
|
merged.embedding = { ...merged.embedding, ..._pluginConfig.embedding };
|
|
605
|
+
if (_pluginConfig.reranker)
|
|
606
|
+
merged.reranker = { ...merged.reranker, ..._pluginConfig.reranker };
|
|
540
607
|
if (Object.keys(fileConfig).length > 0 && Object.keys(_pluginConfig).filter(k => k !== 'hyperMemPath' && k !== 'dataDir').length > 0) {
|
|
541
608
|
console.log('[hypermem-plugin] Note: migrating config.json keys to plugins.entries.hypercompositor.config in openclaw.json is recommended');
|
|
542
609
|
}
|
|
@@ -621,15 +688,19 @@ async function getHyperMem() {
|
|
|
621
688
|
`effective history budget: ${_contextWindowSize - reservedTokens} tokens`);
|
|
622
689
|
verboseLog(`[hypermem-plugin] warmCacheReplayThresholdMs=${_cacheReplayThresholdMs}`);
|
|
623
690
|
verboseLog(`[hypermem-plugin] contextWindowOverrides keys=${Object.keys(_contextWindowOverrides).join(', ') || '(none)'}`);
|
|
691
|
+
const cacheConfig = userConfig.cache;
|
|
624
692
|
const instance = await HyperMem.create({
|
|
625
693
|
dataDir: _pluginConfig.dataDir ?? path.join(os.homedir(), '.openclaw/hypermem'),
|
|
626
694
|
cache: {
|
|
627
|
-
keyPrefix: 'hm:',
|
|
628
|
-
sessionTTL: 14400, // 4h for system/identity/meta slots
|
|
629
|
-
historyTTL: 86400, // 24h for history
|
|
695
|
+
keyPrefix: cacheConfig?.keyPrefix ?? 'hm:',
|
|
696
|
+
sessionTTL: cacheConfig?.sessionTTL ?? 14400, // 4h default for system/identity/meta slots
|
|
697
|
+
historyTTL: cacheConfig?.historyTTL ?? 86400, // 24h default for history/cursor hot cache
|
|
630
698
|
},
|
|
631
699
|
...(userConfig.compositor ? { compositor: userConfig.compositor } : {}),
|
|
632
700
|
...(_embeddingConfig ? { embedding: _embeddingConfig } : {}),
|
|
701
|
+
...(userConfig.reranker
|
|
702
|
+
? { reranker: userConfig.reranker }
|
|
703
|
+
: {}),
|
|
633
704
|
});
|
|
634
705
|
_hm = instance;
|
|
635
706
|
// Wire up fleet store and background indexer from dynamic module
|
|
@@ -723,6 +794,58 @@ function extractTextFromInboundContent(content) {
|
|
|
723
794
|
.map(part => part.text ?? '')
|
|
724
795
|
.join('\n');
|
|
725
796
|
}
|
|
797
|
+
function resolveAssistantTokenCount(msg, runtimeContext) {
|
|
798
|
+
const usage = msg.usage;
|
|
799
|
+
if (usage && typeof usage === 'object') {
|
|
800
|
+
const candidates = [
|
|
801
|
+
usage.total,
|
|
802
|
+
usage.totalTokens,
|
|
803
|
+
usage.total_tokens,
|
|
804
|
+
usage.output,
|
|
805
|
+
usage.outputTokens,
|
|
806
|
+
usage.output_tokens,
|
|
807
|
+
usage.completionTokens,
|
|
808
|
+
usage.completion_tokens,
|
|
809
|
+
];
|
|
810
|
+
for (const candidate of candidates) {
|
|
811
|
+
if (typeof candidate === 'number' && Number.isFinite(candidate) && candidate > 0) {
|
|
812
|
+
return Math.floor(candidate);
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
const runtimeTokenCount = runtimeContext?.currentTokenCount;
|
|
817
|
+
if (typeof runtimeTokenCount === 'number' && Number.isFinite(runtimeTokenCount) && runtimeTokenCount > 0) {
|
|
818
|
+
return Math.floor(runtimeTokenCount);
|
|
819
|
+
}
|
|
820
|
+
return undefined;
|
|
821
|
+
}
|
|
822
|
+
function resolveAssistantOutputTokenCount(msg, runtimeContext) {
|
|
823
|
+
const usage = msg.usage;
|
|
824
|
+
if (usage && typeof usage === 'object') {
|
|
825
|
+
const candidates = [
|
|
826
|
+
usage.output,
|
|
827
|
+
usage.outputTokens,
|
|
828
|
+
usage.output_tokens,
|
|
829
|
+
usage.completionTokens,
|
|
830
|
+
usage.completion_tokens,
|
|
831
|
+
usage.totalTokens,
|
|
832
|
+
usage.total_tokens,
|
|
833
|
+
usage.total,
|
|
834
|
+
];
|
|
835
|
+
for (const candidate of candidates) {
|
|
836
|
+
if (typeof candidate === 'number' && Number.isFinite(candidate) && candidate > 0) {
|
|
837
|
+
return Math.floor(candidate);
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
const runtimeTokenCount = runtimeContext?.currentTokenCount;
|
|
842
|
+
if (typeof runtimeTokenCount === 'number' && Number.isFinite(runtimeTokenCount) && runtimeTokenCount > 0) {
|
|
843
|
+
return Math.floor(runtimeTokenCount);
|
|
844
|
+
}
|
|
845
|
+
const text = extractTextFromInboundContent(msg.content);
|
|
846
|
+
const tokenEstimate = Math.ceil(text.length / 4);
|
|
847
|
+
return tokenEstimate > 0 ? tokenEstimate : undefined;
|
|
848
|
+
}
|
|
726
849
|
function collectNeutralToolPairStats(messages) {
|
|
727
850
|
const callIds = new Set();
|
|
728
851
|
const resultIds = new Set();
|
|
@@ -1291,10 +1414,10 @@ function createHyperMemEngine() {
|
|
|
1291
1414
|
// Non-fatal: missing files are silently skipped.
|
|
1292
1415
|
let identityBlock;
|
|
1293
1416
|
try {
|
|
1294
|
-
// Council agents live at workspace
|
|
1417
|
+
// Council agents live at workspace/<agentId>/
|
|
1295
1418
|
// Other agents at workspace/<agentId>/ — try council path first
|
|
1296
1419
|
const homedir = os.homedir();
|
|
1297
|
-
const councilPath = path.join(homedir, '.openclaw', 'workspace
|
|
1420
|
+
const councilPath = path.join(homedir, '.openclaw', 'workspace', agentId);
|
|
1298
1421
|
const workspacePath = path.join(homedir, '.openclaw', 'workspace', agentId);
|
|
1299
1422
|
let wsPath = councilPath;
|
|
1300
1423
|
try {
|
|
@@ -1326,7 +1449,7 @@ function createHyperMemEngine() {
|
|
|
1326
1449
|
let _wsPathForSeed;
|
|
1327
1450
|
try {
|
|
1328
1451
|
const homedir2 = os.homedir();
|
|
1329
|
-
const councilPath2 = path.join(homedir2, '.openclaw', 'workspace
|
|
1452
|
+
const councilPath2 = path.join(homedir2, '.openclaw', 'workspace', agentId);
|
|
1330
1453
|
const workspacePath2 = path.join(homedir2, '.openclaw', 'workspace', agentId);
|
|
1331
1454
|
try {
|
|
1332
1455
|
await fs.access(councilPath2);
|
|
@@ -1361,7 +1484,7 @@ function createHyperMemEngine() {
|
|
|
1361
1484
|
// Post-warm pressure check: if messages.db had accumulated history,
|
|
1362
1485
|
// warm() may have loaded the session straight to 80%+. Pre-trim now
|
|
1363
1486
|
// so the first turn has headroom instead of starting saturated.
|
|
1364
|
-
// This is the "restart at 98%" failure mode reported by
|
|
1487
|
+
// This is the "restart at 98%" failure mode reported by Eve 2026-04-05:
|
|
1365
1488
|
// JSONL truncation + Redis flush isn't enough if messages.db is still full
|
|
1366
1489
|
// and warm() reloads it. Trim here closes the loop.
|
|
1367
1490
|
try {
|
|
@@ -1708,7 +1831,9 @@ function createHyperMemEngine() {
|
|
|
1708
1831
|
});
|
|
1709
1832
|
const replayMarkerText = replayRecovery.emittedText;
|
|
1710
1833
|
const preTrimTokens = runtimeTokens;
|
|
1711
|
-
|
|
1834
|
+
// Sprint 3: unified pressure signal — tool-loop assemble path
|
|
1835
|
+
const s3ToolLoopPressure = computeUnifiedPressure(preTrimTokens, effectiveBudget, PRESSURE_SOURCE.TOOLLOOP_RUNTIME_ARRAY);
|
|
1836
|
+
const pressure = s3ToolLoopPressure.fraction;
|
|
1712
1837
|
// Pressure-tiered trim targets use a single authority: the working
|
|
1713
1838
|
// message array. Redis drift is logged as an anomaly, never used as
|
|
1714
1839
|
// a trim trigger. Replay recovery gets its own explicit bounded mode
|
|
@@ -1860,17 +1985,17 @@ function createHyperMemEngine() {
|
|
|
1860
1985
|
const kept = keptClusters.flat();
|
|
1861
1986
|
const keptCount = processedConvMsgs.length - kept.length;
|
|
1862
1987
|
if (keptCount > 0) {
|
|
1863
|
-
console.log(`[hypermem-plugin] tool-loop trim: pressure=${
|
|
1988
|
+
console.log(`[hypermem-plugin] tool-loop trim: pressure=${s3ToolLoopPressure.pct}% source=${s3ToolLoopPressure.source} → ` +
|
|
1864
1989
|
`target=${(trimTarget * 100).toFixed(0)}% (redis=${trimmed} msgs, messages=${keptCount} dropped)`);
|
|
1865
1990
|
trimmedMessages = [...systemMsgs, ...kept];
|
|
1866
1991
|
}
|
|
1867
1992
|
else if (trimmed > 0) {
|
|
1868
|
-
console.log(`[hypermem-plugin] tool-loop trim: pressure=${
|
|
1993
|
+
console.log(`[hypermem-plugin] tool-loop trim: pressure=${s3ToolLoopPressure.pct}% source=${s3ToolLoopPressure.source} → ` +
|
|
1869
1994
|
`target=${(trimTarget * 100).toFixed(0)}% (redis=${trimmed} msgs)`);
|
|
1870
1995
|
}
|
|
1871
1996
|
}
|
|
1872
1997
|
else if (trimmed > 0) {
|
|
1873
|
-
console.log(`[hypermem-plugin] tool-loop trim: pressure=${
|
|
1998
|
+
console.log(`[hypermem-plugin] tool-loop trim: pressure=${s3ToolLoopPressure.pct}% source=${s3ToolLoopPressure.source} → ` +
|
|
1874
1999
|
`target=${(trimTarget * 100).toFixed(0)}% (redis=${trimmed} msgs)`);
|
|
1875
2000
|
}
|
|
1876
2001
|
// Apply tool gradient to compress large tool results before returning.
|
|
@@ -2063,23 +2188,32 @@ function createHyperMemEngine() {
|
|
|
2063
2188
|
console.warn('[hypermem-plugin] assemble: Redis trim failed (non-fatal):', trimErr.message);
|
|
2064
2189
|
}
|
|
2065
2190
|
// ── Budget downshift: proactive reshape pass ───────────────────────────────────────
|
|
2066
|
-
//
|
|
2067
|
-
//
|
|
2068
|
-
//
|
|
2069
|
-
//
|
|
2070
|
-
//
|
|
2071
|
-
//
|
|
2072
|
-
//
|
|
2073
|
-
// Bug fix: previously read from getWindow() which is always null here
|
|
2074
|
-
// (afterTurn invalidates it every turn). Also fixed: was doing setWindow()
|
|
2075
|
-
// then invalidateWindow() which is a write-then-delete no-op. Now reads
|
|
2076
|
-
// from history list and writes back via replaceHistory().
|
|
2191
|
+
// Detect provider/model identity changes as well as raw budget changes.
|
|
2192
|
+
// Provider routing matters operationally because the same model family can
|
|
2193
|
+
// land on a different effective context window, for example Copilot Sonnet
|
|
2194
|
+
// vs direct Anthropic Sonnet. Only budget downshifts trigger the demoted
|
|
2195
|
+
// reshape guard, but verbose logs now show provider/model swaps even when
|
|
2196
|
+
// the effective budget stays flat or increases.
|
|
2077
2197
|
let lastState = null;
|
|
2078
2198
|
try {
|
|
2079
2199
|
lastState = await hm.cache.getModelState(agentId, sk);
|
|
2080
2200
|
const DOWNSHIFT_THRESHOLD = 0.10;
|
|
2081
|
-
const
|
|
2082
|
-
|
|
2201
|
+
const modelDelta = diffModelState(lastState, {
|
|
2202
|
+
model,
|
|
2203
|
+
tokenBudget: effectiveBudget,
|
|
2204
|
+
});
|
|
2205
|
+
const downshiftFraction = lastState?.tokenBudget
|
|
2206
|
+
? (lastState.tokenBudget - effectiveBudget) / lastState.tokenBudget
|
|
2207
|
+
: 0;
|
|
2208
|
+
const isDownshift = modelDelta.budgetDownshift && downshiftFraction > DOWNSHIFT_THRESHOLD;
|
|
2209
|
+
if (lastState && (modelDelta.modelChanged || modelDelta.budgetChanged)) {
|
|
2210
|
+
verboseLog(`[hypermem-plugin] model state change: ` +
|
|
2211
|
+
`prev=${modelDelta.previousIdentity.modelKey ?? 'unknown'} ` +
|
|
2212
|
+
`next=${modelDelta.currentIdentity.modelKey ?? 'unknown'} ` +
|
|
2213
|
+
`providerChanged=${modelDelta.providerChanged} ` +
|
|
2214
|
+
`modelIdChanged=${modelDelta.modelIdChanged} ` +
|
|
2215
|
+
`budget=${lastState.tokenBudget}->${effectiveBudget}`);
|
|
2216
|
+
}
|
|
2083
2217
|
if (isDownshift && !_deferToolPruning) {
|
|
2084
2218
|
// Sprint 2.2a: demote reshape to guard telemetry.
|
|
2085
2219
|
//
|
|
@@ -2129,6 +2263,7 @@ function createHyperMemEngine() {
|
|
|
2129
2263
|
path: 'replay',
|
|
2130
2264
|
toolLoop: isToolLoop,
|
|
2131
2265
|
msgCount: messages.length,
|
|
2266
|
+
composeTopicTelemetryStatus: 'intentionally-omitted',
|
|
2132
2267
|
});
|
|
2133
2268
|
}
|
|
2134
2269
|
}
|
|
@@ -2141,6 +2276,20 @@ function createHyperMemEngine() {
|
|
|
2141
2276
|
// Subagent light mode: skip library/wiki/semantic/keystones/doc chunks.
|
|
2142
2277
|
// Keeps: system, identity, history, active facts, output profile, tool gradient.
|
|
2143
2278
|
const subagentLight = isSubagent && _subagentWarming === 'light';
|
|
2279
|
+
let forkedContext;
|
|
2280
|
+
if (isSubagent) {
|
|
2281
|
+
try {
|
|
2282
|
+
const rawForkedContext = await hm.cache.getSlot(agentId, sk, FORKED_CONTEXT_META_SLOT);
|
|
2283
|
+
if (rawForkedContext) {
|
|
2284
|
+
const parsed = JSON.parse(rawForkedContext);
|
|
2285
|
+
if (parsed?.enabled === true)
|
|
2286
|
+
forkedContext = parsed;
|
|
2287
|
+
}
|
|
2288
|
+
}
|
|
2289
|
+
catch {
|
|
2290
|
+
// Fork metadata is advisory; fall back to normal subagent lifecycle.
|
|
2291
|
+
}
|
|
2292
|
+
}
|
|
2144
2293
|
const request = {
|
|
2145
2294
|
agentId,
|
|
2146
2295
|
sessionKey: sk,
|
|
@@ -2155,6 +2304,7 @@ function createHyperMemEngine() {
|
|
|
2155
2304
|
includeSemanticRecall: subagentLight ? false : undefined, // skip vector/FTS recall
|
|
2156
2305
|
includeKeystones: subagentLight ? false : undefined, // skip keystone history injection
|
|
2157
2306
|
prompt,
|
|
2307
|
+
forkedContext,
|
|
2158
2308
|
skipProviderTranslation: true, // runtime handles provider translation
|
|
2159
2309
|
};
|
|
2160
2310
|
const result = await hm.compose(request);
|
|
@@ -2170,6 +2320,63 @@ function createHyperMemEngine() {
|
|
|
2170
2320
|
replayState: replayRecovery.emittedMarker?.state,
|
|
2171
2321
|
replayReason: replayRecovery.emittedMarker?.reason,
|
|
2172
2322
|
});
|
|
2323
|
+
// Sprint 1: emit assemble-level trace with full observability fields
|
|
2324
|
+
// after a full compose (not replay). Surfaces prefix stability,
|
|
2325
|
+
// reranker outcome, slot spans, and compaction eligibility.
|
|
2326
|
+
if (telemetryEnabled() && !cachedContextBlock) {
|
|
2327
|
+
const diag = result.diagnostics;
|
|
2328
|
+
// prefixChanged: compare current prefixHash against prevPrefixHash
|
|
2329
|
+
// (surfaced by the compositor when a cache bypass detected prefix mutation).
|
|
2330
|
+
// When no previous hash is available (first turn), leave prefixChanged undefined.
|
|
2331
|
+
let prefixChanged;
|
|
2332
|
+
if (diag?.prefixHash && diag?.prevPrefixHash) {
|
|
2333
|
+
prefixChanged = diag.prefixHash !== diag.prevPrefixHash;
|
|
2334
|
+
}
|
|
2335
|
+
assembleTrace({
|
|
2336
|
+
agentId,
|
|
2337
|
+
sessionKey: sk,
|
|
2338
|
+
turnId: _asmTurnId,
|
|
2339
|
+
path: isSubagent ? 'subagent' : 'cold',
|
|
2340
|
+
toolLoop: isToolLoop,
|
|
2341
|
+
msgCount: result.messages.length,
|
|
2342
|
+
prefixChanged,
|
|
2343
|
+
prefixHash: diag?.prefixHash,
|
|
2344
|
+
rerankerStatus: diag?.rerankerStatus,
|
|
2345
|
+
rerankerCandidates: diag?.rerankerCandidates,
|
|
2346
|
+
rerankerProvider: diag?.rerankerProvider,
|
|
2347
|
+
slotSpans: diag?.slotSpans,
|
|
2348
|
+
compactionEligibleCount: diag?.compactionEligibleCount,
|
|
2349
|
+
compactionEligibleRatio: diag?.compactionEligibleRatio,
|
|
2350
|
+
compactionProcessedCount: diag?.compactionProcessedCount,
|
|
2351
|
+
composeTopicSource: diag?.composeTopicSource,
|
|
2352
|
+
composeTopicState: diag?.composeTopicState,
|
|
2353
|
+
composeTopicMessageCount: diag?.composeTopicMessageCount,
|
|
2354
|
+
composeTopicStampedMessageCount: diag?.composeTopicStampedMessageCount,
|
|
2355
|
+
composeTopicTelemetryStatus: diag?.composeTopicTelemetryStatus,
|
|
2356
|
+
});
|
|
2357
|
+
if (diag?.adaptiveLifecycleBand) {
|
|
2358
|
+
lifecyclePolicyTelemetry({
|
|
2359
|
+
path: 'compose.preRecall',
|
|
2360
|
+
agentId,
|
|
2361
|
+
sessionKey: sk,
|
|
2362
|
+
band: diag.adaptiveLifecycleBand,
|
|
2363
|
+
pressurePct: diag.adaptiveLifecyclePressurePct,
|
|
2364
|
+
trimSoftTarget: diag.adaptiveTrimSoftTarget,
|
|
2365
|
+
reasons: diag.adaptiveLifecycleReasons,
|
|
2366
|
+
});
|
|
2367
|
+
}
|
|
2368
|
+
if (diag?.adaptiveEvictionLifecycleBand) {
|
|
2369
|
+
lifecyclePolicyTelemetry({
|
|
2370
|
+
path: 'compose.eviction',
|
|
2371
|
+
agentId,
|
|
2372
|
+
sessionKey: sk,
|
|
2373
|
+
band: diag.adaptiveEvictionLifecycleBand,
|
|
2374
|
+
pressurePct: diag.adaptiveEvictionPressurePct,
|
|
2375
|
+
trimSoftTarget: diag.adaptiveTrimSoftTarget,
|
|
2376
|
+
reasons: diag.adaptiveLifecycleBandDiverged ? ['diverged-from-preRecall'] : undefined,
|
|
2377
|
+
});
|
|
2378
|
+
}
|
|
2379
|
+
}
|
|
2173
2380
|
// Use cached contextBlock if available (cache replay), otherwise use fresh result.
|
|
2174
2381
|
// After a full compose, write the new contextBlock to cache for the next turn.
|
|
2175
2382
|
if (cachedContextBlock) {
|
|
@@ -2242,10 +2449,17 @@ ${replayRecovery.emittedText}`
|
|
|
2242
2449
|
const runtimeSystemTokens = getOverheadFallback(tier);
|
|
2243
2450
|
_overheadCache.set(sk, contextBlockTokens + runtimeSystemTokens);
|
|
2244
2451
|
await persistReplayRecoveryState(hm, agentId, sk, replayRecovery.nextState);
|
|
2452
|
+
if (forkedContext) {
|
|
2453
|
+
await hm.cache.setSlot(agentId, sk, FORKED_CONTEXT_META_SLOT, '').catch(() => { });
|
|
2454
|
+
}
|
|
2245
2455
|
// Update model state for downshift detection on next turn
|
|
2246
2456
|
try {
|
|
2457
|
+
const modelIdentity = resolveModelIdentity(model);
|
|
2247
2458
|
await hm.cache.setModelState(agentId, sk, {
|
|
2248
2459
|
model: model ?? 'unknown',
|
|
2460
|
+
modelKey: modelIdentity.modelKey ?? undefined,
|
|
2461
|
+
provider: modelIdentity.provider ?? undefined,
|
|
2462
|
+
modelId: modelIdentity.modelId ?? undefined,
|
|
2249
2463
|
tokenBudget: effectiveBudget,
|
|
2250
2464
|
composedAt: new Date().toISOString(),
|
|
2251
2465
|
historyDepth,
|
|
@@ -2326,6 +2540,9 @@ ${replayRecovery.emittedText}`
|
|
|
2326
2540
|
// budget the history is competing for. We trim history to make room.
|
|
2327
2541
|
const effectiveBudget = computeEffectiveBudget(tokenBudget, model);
|
|
2328
2542
|
const tokensBefore = await estimateWindowTokens(hm, agentId, sk);
|
|
2543
|
+
// Sprint 3: Unified pressure signal — compact path (Redis estimate)
|
|
2544
|
+
const s3CompactPressure = computeUnifiedPressure(tokensBefore, effectiveBudget, PRESSURE_SOURCE.COMPACT_REDIS_ESTIMATE);
|
|
2545
|
+
console.log(`[hypermem-plugin] compact: pressure=${s3CompactPressure.pct}% source=${s3CompactPressure.source} tokens=${tokensBefore}/${effectiveBudget}`);
|
|
2329
2546
|
// Target depth for both Redis trimming and JSONL truncation.
|
|
2330
2547
|
// Target 50% of budget capacity, assume ~500 tokens/message average.
|
|
2331
2548
|
const targetDepth = Math.max(20, Math.floor((effectiveBudget * 0.5) / 500));
|
|
@@ -2339,6 +2556,10 @@ ${replayRecovery.emittedText}`
|
|
|
2339
2556
|
// Also triggered when reshape ran recently but the session is still
|
|
2340
2557
|
// critically full — bypass the reshape guard in that case.
|
|
2341
2558
|
const NUCLEAR_THRESHOLD = 0.85;
|
|
2559
|
+
// Sprint 3: runtime-total pressure for nuclear check uses its own source label
|
|
2560
|
+
const s3NuclearPressure = currentTokenCount != null
|
|
2561
|
+
? computeUnifiedPressure(currentTokenCount, effectiveBudget, PRESSURE_SOURCE.COMPACT_RUNTIME_TOTAL)
|
|
2562
|
+
: s3CompactPressure;
|
|
2342
2563
|
const isNuclear = currentTokenCount != null && currentTokenCount > effectiveBudget * NUCLEAR_THRESHOLD;
|
|
2343
2564
|
if (isNuclear) {
|
|
2344
2565
|
// Cut deep: target 20% of normal depth = ~25 messages for a 128k session.
|
|
@@ -2357,11 +2578,11 @@ ${replayRecovery.emittedText}`
|
|
|
2357
2578
|
postTokens: tokensAfter,
|
|
2358
2579
|
removed: nuclearRemoved,
|
|
2359
2580
|
cacheInvalidated: true,
|
|
2360
|
-
reason:
|
|
2581
|
+
reason: `${s3NuclearPressure.source}:${s3NuclearPressure.pct}% currentTokenCount=${currentTokenCount}/${effectiveBudget}`,
|
|
2361
2582
|
});
|
|
2362
2583
|
}
|
|
2363
|
-
console.log(`[hypermem-plugin] compact: NUCLEAR —
|
|
2364
|
-
`
|
|
2584
|
+
console.log(`[hypermem-plugin] compact: NUCLEAR — pressure=${s3NuclearPressure.pct}% source=${s3NuclearPressure.source} ` +
|
|
2585
|
+
`session at ${currentTokenCount}/${effectiveBudget} tokens, ` +
|
|
2365
2586
|
`deep-trimmed JSONL to ${nuclearDepth} messages, Redis ${tokensBefore}→${tokensAfter} tokens`);
|
|
2366
2587
|
return { ok: true, compacted: true, result: { tokensBefore, tokensAfter } };
|
|
2367
2588
|
}
|
|
@@ -2448,10 +2669,10 @@ ${replayRecovery.emittedText}`
|
|
|
2448
2669
|
postTokens: tokensAfter,
|
|
2449
2670
|
removed: historyTrimmed,
|
|
2450
2671
|
cacheInvalidated: true,
|
|
2451
|
-
reason:
|
|
2672
|
+
reason: `${s3CompactPressure.source}:${s3CompactPressure.pct}% over-budget tokensBefore=${tokensBefore}/${effectiveBudget}`,
|
|
2452
2673
|
});
|
|
2453
2674
|
}
|
|
2454
|
-
console.log(`[hypermem-plugin] compact: trimmed ${tokensBefore} → ${tokensAfter} tokens (budget: ${effectiveBudget})`);
|
|
2675
|
+
console.log(`[hypermem-plugin] compact: trimmed ${tokensBefore} → ${tokensAfter} tokens (budget: ${effectiveBudget}, pressure=${s3CompactPressure.pct}% source=${s3CompactPressure.source})`);
|
|
2455
2676
|
// Density-aware JSONL truncation: derive target depth from actual avg tokens/message
|
|
2456
2677
|
// rather than assuming a fixed 500 tokens/message. This prevents a large-message
|
|
2457
2678
|
// session (e.g. 145 msgs × 882 tok = 128k) from bypassing the 1.5x guard and
|
|
@@ -2481,7 +2702,7 @@ ${replayRecovery.emittedText}`
|
|
|
2481
2702
|
* it never calls ingest() or ingestBatch(). So we must ingest the new
|
|
2482
2703
|
* messages here, using messages.slice(prePromptMessageCount).
|
|
2483
2704
|
*/
|
|
2484
|
-
async afterTurn({ sessionId, sessionKey, messages, prePromptMessageCount, isHeartbeat }) {
|
|
2705
|
+
async afterTurn({ sessionId, sessionKey, messages, prePromptMessageCount, isHeartbeat, runtimeContext }) {
|
|
2485
2706
|
if (isHeartbeat)
|
|
2486
2707
|
return;
|
|
2487
2708
|
try {
|
|
@@ -2523,11 +2744,59 @@ ${replayRecovery.emittedText}`
|
|
|
2523
2744
|
await hm.recordUserMessage(agentId, sk, stripMessageMetadata(neutral.textContent ?? ''));
|
|
2524
2745
|
}
|
|
2525
2746
|
else {
|
|
2526
|
-
await hm.recordAssistantMessage(agentId, sk, neutral
|
|
2747
|
+
await hm.recordAssistantMessage(agentId, sk, neutral, {
|
|
2748
|
+
tokenCount: neutral.role === 'assistant' ? resolveAssistantTokenCount(m, runtimeContext) : undefined,
|
|
2749
|
+
});
|
|
2750
|
+
}
|
|
2751
|
+
}
|
|
2752
|
+
try {
|
|
2753
|
+
const lastAssistantMessage = [...newMessages].reverse().find(m => m.role === 'assistant');
|
|
2754
|
+
if (lastAssistantMessage) {
|
|
2755
|
+
const modelState = await hm.cache.getModelState(agentId, sk).catch(() => null);
|
|
2756
|
+
const promptCacheUsage = runtimeContext?.promptCache?.lastCallUsage;
|
|
2757
|
+
const outputTokens = resolveAssistantOutputTokenCount(lastAssistantMessage, runtimeContext) ?? 1;
|
|
2758
|
+
const inputTokens = typeof promptCacheUsage?.input === 'number'
|
|
2759
|
+
? Math.floor(promptCacheUsage.input)
|
|
2760
|
+
: typeof runtimeContext?.currentTokenCount === 'number'
|
|
2761
|
+
? Math.floor(runtimeContext.currentTokenCount)
|
|
2762
|
+
: null;
|
|
2763
|
+
const cacheReadTokens = typeof promptCacheUsage?.cacheRead === 'number'
|
|
2764
|
+
? Math.floor(promptCacheUsage.cacheRead)
|
|
2765
|
+
: null;
|
|
2766
|
+
const modelId = typeof lastAssistantMessage.model === 'string'
|
|
2767
|
+
? lastAssistantMessage.model
|
|
2768
|
+
: modelState?.modelId ?? modelState?.model ?? 'unknown';
|
|
2769
|
+
const provider = typeof lastAssistantMessage.provider === 'string'
|
|
2770
|
+
? lastAssistantMessage.provider
|
|
2771
|
+
: modelState?.provider ?? 'unknown';
|
|
2772
|
+
const taskType = typeof runtimeContext?.taskType === 'string'
|
|
2773
|
+
? runtimeContext.taskType ?? null
|
|
2774
|
+
: null;
|
|
2775
|
+
recordOutputMetrics(hm.dbManager.getLibraryDb(), {
|
|
2776
|
+
id: `turn-metric-${agentId}-${Date.now()}-${randomUUID()}`,
|
|
2777
|
+
timestamp: new Date().toISOString(),
|
|
2778
|
+
agent_id: agentId,
|
|
2779
|
+
session_key: sk,
|
|
2780
|
+
model_id: modelId,
|
|
2781
|
+
provider,
|
|
2782
|
+
fos_version: null,
|
|
2783
|
+
mod_version: null,
|
|
2784
|
+
mod_id: null,
|
|
2785
|
+
task_type: taskType,
|
|
2786
|
+
output_tokens: outputTokens,
|
|
2787
|
+
input_tokens: inputTokens,
|
|
2788
|
+
cache_read_tokens: cacheReadTokens,
|
|
2789
|
+
corrections_fired: [],
|
|
2790
|
+
latency_ms: null,
|
|
2791
|
+
});
|
|
2527
2792
|
}
|
|
2528
2793
|
}
|
|
2794
|
+
catch {
|
|
2795
|
+
// Non-fatal telemetry path
|
|
2796
|
+
}
|
|
2529
2797
|
// P3.1: Topic detection on the inbound user message
|
|
2530
2798
|
// Non-fatal: topic detection never blocks afterTurn
|
|
2799
|
+
let adaptiveTopicShiftConfidence;
|
|
2531
2800
|
try {
|
|
2532
2801
|
const inboundUserMsg = newMessages
|
|
2533
2802
|
.map(m => m)
|
|
@@ -2544,6 +2813,7 @@ ${replayRecovery.emittedText}`
|
|
|
2544
2813
|
const topicMap = new SessionTopicMap(db);
|
|
2545
2814
|
const activeTopic = topicMap.getActiveTopic(sk);
|
|
2546
2815
|
const signal = detectTopicShift(neutralUser, contextMessages, activeTopic?.id ?? null);
|
|
2816
|
+
adaptiveTopicShiftConfidence = signal.confidence;
|
|
2547
2817
|
if (signal.isNewTopic && signal.topicName) {
|
|
2548
2818
|
const newTopicId = topicMap.createTopic(sk, signal.topicName);
|
|
2549
2819
|
// New topic starts with count 1 (the message that triggered the shift)
|
|
@@ -2583,13 +2853,36 @@ ${replayRecovery.emittedText}`
|
|
|
2583
2853
|
// gradient-compressed window to budget before writing to Redis. Without
|
|
2584
2854
|
// this, afterTurn writes up to 250 messages regardless of budget, causing
|
|
2585
2855
|
// trimHistoryToTokenBudget to fire and trim ~200 messages on every
|
|
2586
|
-
// subsequent assemble() — the churn loop seen in
|
|
2856
|
+
// subsequent assemble() — the churn loop seen in Eve's logs.
|
|
2587
2857
|
if (hm.cache.isConnected) {
|
|
2588
2858
|
try {
|
|
2589
2859
|
const modelState = await hm.cache.getModelState(agentId, sk);
|
|
2590
2860
|
const gradientBudget = modelState?.tokenBudget;
|
|
2591
2861
|
const gradientDepth = modelState?.historyDepth;
|
|
2592
|
-
|
|
2862
|
+
const inboundUserMsg = newMessages
|
|
2863
|
+
.map(m => m)
|
|
2864
|
+
.find(m => m.role === 'user');
|
|
2865
|
+
const inboundUserText = inboundUserMsg
|
|
2866
|
+
? stripMessageMetadata(extractTextFromInboundContent(inboundUserMsg.content))
|
|
2867
|
+
: '';
|
|
2868
|
+
const lifecyclePolicy = resolveAdaptiveLifecyclePolicy({
|
|
2869
|
+
usedTokens: estimateMessageArrayTokens(messages),
|
|
2870
|
+
effectiveBudget: gradientBudget,
|
|
2871
|
+
userTurnCount: messages.filter(m => m.role === 'user').length,
|
|
2872
|
+
explicitNewSession: /^\/new(?:\s|$)/i.test(inboundUserText.trim()),
|
|
2873
|
+
topicShiftConfidence: adaptiveTopicShiftConfidence,
|
|
2874
|
+
});
|
|
2875
|
+
lifecyclePolicyTelemetry({
|
|
2876
|
+
path: 'afterTurn.gradient',
|
|
2877
|
+
agentId,
|
|
2878
|
+
sessionKey: sk,
|
|
2879
|
+
band: lifecyclePolicy.band,
|
|
2880
|
+
pressurePct: lifecyclePolicy.pressurePct,
|
|
2881
|
+
topicShiftConfidence: adaptiveTopicShiftConfidence,
|
|
2882
|
+
trimSoftTarget: lifecyclePolicy.trimSoftTarget,
|
|
2883
|
+
reasons: lifecyclePolicy.reasons,
|
|
2884
|
+
});
|
|
2885
|
+
await hm.refreshRedisGradient(agentId, sk, gradientBudget, gradientDepth, lifecyclePolicy.trimSoftTarget);
|
|
2593
2886
|
}
|
|
2594
2887
|
catch (refreshErr) {
|
|
2595
2888
|
console.warn('[hypermem-plugin] afterTurn: refreshRedisGradient failed (non-fatal):', refreshErr.message);
|
|
@@ -2607,7 +2900,7 @@ ${replayRecovery.emittedText}`
|
|
|
2607
2900
|
// If a session just finished a turn at >80% pressure, the NEXT turn's
|
|
2608
2901
|
// incoming tool results (parallel web searches, large exec output, etc.)
|
|
2609
2902
|
// will hit a window with no headroom — the ingestion wave failure mode
|
|
2610
|
-
// (reported by
|
|
2903
|
+
// (reported by Eve, 2026-04-05). Pre-trim here so the tool-loop
|
|
2611
2904
|
// assemble() path starts the next turn with meaningful space.
|
|
2612
2905
|
//
|
|
2613
2906
|
// Uses modelState.tokenBudget if cached; skips if unavailable (non-fatal).
|
|
@@ -2759,7 +3052,12 @@ ${replayRecovery.emittedText}`
|
|
|
2759
3052
|
* subagentWarming config ('full' | 'light' | 'off').
|
|
2760
3053
|
* Returns a rollback handle to clean up if spawn fails.
|
|
2761
3054
|
*/
|
|
2762
|
-
async prepareSubagentSpawn(
|
|
3055
|
+
async prepareSubagentSpawn(params) {
|
|
3056
|
+
const { parentSessionKey, childSessionKey } = params;
|
|
3057
|
+
const forkParams = params;
|
|
3058
|
+
const contextMode = forkParams.contextMode;
|
|
3059
|
+
const parentSessionId = forkParams.parentSessionId;
|
|
3060
|
+
const childSessionId = forkParams.childSessionId;
|
|
2763
3061
|
if (_subagentWarming === 'off') {
|
|
2764
3062
|
return undefined;
|
|
2765
3063
|
}
|
|
@@ -2767,7 +3065,12 @@ ${replayRecovery.emittedText}`
|
|
|
2767
3065
|
const hm = await getHyperMem();
|
|
2768
3066
|
const parentAgentId = extractAgentId(parentSessionKey);
|
|
2769
3067
|
const childAgentId = extractAgentId(childSessionKey);
|
|
2770
|
-
|
|
3068
|
+
const isForkedContext = contextMode === 'fork';
|
|
3069
|
+
let parentHistoryMessages = 0;
|
|
3070
|
+
let parentUserTurnCount = 0;
|
|
3071
|
+
let parentPressureFraction;
|
|
3072
|
+
// Seed child with parent's active facts. This preserves the historical
|
|
3073
|
+
// slot for compatibility; facts still primarily come from L4 by agent id.
|
|
2771
3074
|
const facts = hm.getActiveFacts(parentAgentId, { limit: 50 });
|
|
2772
3075
|
if (facts && facts.length > 0) {
|
|
2773
3076
|
const factBlock = facts
|
|
@@ -2775,22 +3078,48 @@ ${replayRecovery.emittedText}`
|
|
|
2775
3078
|
.join('\n');
|
|
2776
3079
|
await hm.cache.setSlot(childAgentId, childSessionKey, 'parentFacts', factBlock);
|
|
2777
3080
|
}
|
|
2778
|
-
|
|
2779
|
-
if (
|
|
2780
|
-
const
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
3081
|
+
const history = await hm.cache.getHistory(parentAgentId, parentSessionKey);
|
|
3082
|
+
if (history && history.length > 0) {
|
|
3083
|
+
const maxSeededHistory = _subagentWarming === 'full' ? 25 : 12;
|
|
3084
|
+
const recentHistory = history.slice(-maxSeededHistory);
|
|
3085
|
+
parentHistoryMessages = recentHistory.length;
|
|
3086
|
+
parentUserTurnCount = recentHistory.filter(m => m.role === 'user').length;
|
|
3087
|
+
const parentTokens = estimateMessageArrayTokens(recentHistory);
|
|
3088
|
+
const parentModelState = await hm.cache.getModelState(parentAgentId, parentSessionKey).catch(() => null);
|
|
3089
|
+
const parentBudget = parentModelState?.tokenBudget && parentModelState.tokenBudget > 0
|
|
3090
|
+
? parentModelState.tokenBudget
|
|
3091
|
+
: undefined;
|
|
3092
|
+
parentPressureFraction = parentBudget ? parentTokens / parentBudget : undefined;
|
|
3093
|
+
if (isForkedContext || _subagentWarming === 'full') {
|
|
3094
|
+
await hm.cache.replaceHistory(childAgentId, childSessionKey, recentHistory, maxSeededHistory);
|
|
3095
|
+
await hm.cache.invalidateWindow(childAgentId, childSessionKey).catch(() => { });
|
|
2784
3096
|
}
|
|
3097
|
+
await hm.cache.setSlot(childAgentId, childSessionKey, 'parentHistory', JSON.stringify(recentHistory));
|
|
3098
|
+
}
|
|
3099
|
+
if (isForkedContext) {
|
|
3100
|
+
const forkedMeta = {
|
|
3101
|
+
enabled: true,
|
|
3102
|
+
parentSessionKey,
|
|
3103
|
+
parentSessionId,
|
|
3104
|
+
childSessionId,
|
|
3105
|
+
parentPressureFraction,
|
|
3106
|
+
parentUserTurnCount,
|
|
3107
|
+
parentHistoryMessages,
|
|
3108
|
+
};
|
|
3109
|
+
await hm.cache.setSlot(childAgentId, childSessionKey, FORKED_CONTEXT_META_SLOT, JSON.stringify(forkedMeta));
|
|
2785
3110
|
}
|
|
2786
3111
|
console.log(`[hypermem-plugin] prepareSubagentSpawn: seeded ${childSessionKey} ` +
|
|
2787
|
-
`from ${parentSessionKey} (warming=${_subagentWarming}
|
|
3112
|
+
`from ${parentSessionKey} (warming=${_subagentWarming}, contextMode=${contextMode ?? 'isolated'}, ` +
|
|
3113
|
+
`history=${parentHistoryMessages})`);
|
|
2788
3114
|
return {
|
|
2789
3115
|
async rollback() {
|
|
2790
3116
|
try {
|
|
2791
3117
|
const hm = await getHyperMem();
|
|
2792
3118
|
await hm.cache.setSlot(childAgentId, childSessionKey, 'parentFacts', '');
|
|
2793
3119
|
await hm.cache.setSlot(childAgentId, childSessionKey, 'parentHistory', '');
|
|
3120
|
+
await hm.cache.setSlot(childAgentId, childSessionKey, FORKED_CONTEXT_META_SLOT, '');
|
|
3121
|
+
await hm.cache.replaceHistory(childAgentId, childSessionKey, [], 0);
|
|
3122
|
+
await hm.cache.invalidateWindow(childAgentId, childSessionKey).catch(() => { });
|
|
2794
3123
|
}
|
|
2795
3124
|
catch {
|
|
2796
3125
|
// Rollback is best-effort
|
|
@@ -2816,6 +3145,7 @@ ${replayRecovery.emittedText}`
|
|
|
2816
3145
|
await Promise.all([
|
|
2817
3146
|
hm.cache.setSlot(childAgentId, childSessionKey, 'parentFacts', ''),
|
|
2818
3147
|
hm.cache.setSlot(childAgentId, childSessionKey, 'parentHistory', ''),
|
|
3148
|
+
hm.cache.setSlot(childAgentId, childSessionKey, FORKED_CONTEXT_META_SLOT, ''),
|
|
2819
3149
|
hm.cache.setSlot(childAgentId, childSessionKey, 'assemblyContextBlock', ''),
|
|
2820
3150
|
hm.cache.setSlot(childAgentId, childSessionKey, 'assemblyContextAt', '0'),
|
|
2821
3151
|
hm.cache.invalidateWindow(childAgentId, childSessionKey).catch(() => { }),
|
|
@@ -3027,6 +3357,23 @@ const hypercompositorConfigSchema = z.object({
|
|
|
3027
3357
|
timeout: z.number().int().positive().optional(),
|
|
3028
3358
|
batchSize: z.number().int().positive().optional(),
|
|
3029
3359
|
}).optional(),
|
|
3360
|
+
/**
|
|
3361
|
+
* Optional reranker config. When omitted or provider is 'none', the
|
|
3362
|
+
* compositor runs with RRF-only ordering. See INSTALL.md → Reranker.
|
|
3363
|
+
*/
|
|
3364
|
+
reranker: z.object({
|
|
3365
|
+
provider: z.enum(['zeroentropy', 'openrouter', 'local', 'none']),
|
|
3366
|
+
minCandidates: z.number().int().nonnegative().optional(),
|
|
3367
|
+
maxDocuments: z.number().int().positive().optional(),
|
|
3368
|
+
topK: z.number().int().positive().optional(),
|
|
3369
|
+
timeoutMs: z.number().int().positive().optional(),
|
|
3370
|
+
zeroEntropyApiKey: z.string().optional(),
|
|
3371
|
+
zeroEntropyModel: z.string().optional(),
|
|
3372
|
+
openrouterApiKey: z.string().optional(),
|
|
3373
|
+
openrouterModel: z.string().optional(),
|
|
3374
|
+
ollamaUrl: z.string().optional(),
|
|
3375
|
+
ollamaModel: z.string().optional(),
|
|
3376
|
+
}).optional(),
|
|
3030
3377
|
});
|
|
3031
3378
|
// ─── Plugin Entry ───────────────────────────────────────────────
|
|
3032
3379
|
const engine = createHyperMemEngine();
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@psiclawops/hypercompositor",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "HyperCompositor
|
|
3
|
+
"version": "0.9.0",
|
|
4
|
+
"description": "HyperCompositor — context engine plugin for OpenClaw",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"license": "Apache-2.0",
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"openclaw": {
|
|
19
19
|
"plugin": {
|
|
20
20
|
"id": "hypercompositor",
|
|
21
|
-
"name": "HyperCompositor
|
|
21
|
+
"name": "HyperCompositor — Context Engine",
|
|
22
22
|
"kind": "context-engine"
|
|
23
23
|
},
|
|
24
24
|
"extensions": [
|
|
@@ -38,12 +38,12 @@
|
|
|
38
38
|
"typecheck": "tsc --noEmit"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@psiclawops/hypermem": "
|
|
41
|
+
"@psiclawops/hypermem": "0.9.0",
|
|
42
|
+
"zod": "^4.0.0"
|
|
42
43
|
},
|
|
43
44
|
"devDependencies": {
|
|
44
45
|
"openclaw": "*",
|
|
45
|
-
"typescript": "^5.4.0"
|
|
46
|
-
"zod": "^4.0.0"
|
|
46
|
+
"typescript": "^5.4.0"
|
|
47
47
|
},
|
|
48
48
|
"peerDependencies": {
|
|
49
49
|
"openclaw": "*"
|