@psiclawops/hypercompositor 0.6.2 → 0.7.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/dist/index.d.ts.map +1 -1
- package/dist/index.js +173 -29
- package/openclaw.plugin.json +3 -0
- package/package.json +5 -5
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;
|
|
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;AAW9B,YAAY,EAAE,cAAc,EAAE,eAAe,EAAE,iBAAiB,EAAE,cAAc,EAAE,aAAa,EAAE,CAAC;AAq5ElG;;;;GAIG;AACH,wBAAsB,iBAAiB,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAU1F;;;;;;;;AA8DD,wBA4CG"}
|
package/dist/index.js
CHANGED
|
@@ -718,7 +718,7 @@ function createHyperMemEngine() {
|
|
|
718
718
|
info: {
|
|
719
719
|
id: 'hypermem',
|
|
720
720
|
name: 'hypermem context engine',
|
|
721
|
-
version: '0.
|
|
721
|
+
version: '0.6.3',
|
|
722
722
|
// We own compaction — assemble() trims to budget via the compositor safety
|
|
723
723
|
// valve, so runtime compaction is never needed. compact() handles any
|
|
724
724
|
// explicit calls by trimming the Redis history window directly.
|
|
@@ -742,34 +742,7 @@ function createHyperMemEngine() {
|
|
|
742
742
|
const hm = await getHyperMem();
|
|
743
743
|
const sk = resolveSessionKey(sessionId, sessionKey);
|
|
744
744
|
const agentId = extractAgentId(sk);
|
|
745
|
-
// EC1
|
|
746
|
-
// the safe replay threshold. Fires BEFORE warm() so the next restart
|
|
747
|
-
// (not this one) loads a clean file. Combined with the preflight script
|
|
748
|
-
// run before each gateway restart, this closes the EC1 loop entirely.
|
|
749
|
-
//
|
|
750
|
-
// Why this doesn't help the CURRENT session:
|
|
751
|
-
// OpenClaw has already replayed the JSONL into memory by the time
|
|
752
|
-
// bootstrap() is called. Disk truncation here is forward-looking only.
|
|
753
|
-
//
|
|
754
|
-
// Why it still matters:
|
|
755
|
-
// Without this, a session that saturates in operation (no preflight ran)
|
|
756
|
-
// would restart saturated on the next boot. This ensures at least one
|
|
757
|
-
// restart cycle later the session comes up clean.
|
|
758
|
-
try {
|
|
759
|
-
const sessionDir = path.join(os.homedir(), '.openclaw', 'agents', agentId, 'sessions');
|
|
760
|
-
const jsonlPath = path.join(sessionDir, `${sessionId}.jsonl`);
|
|
761
|
-
// EC1 threshold: 60 conversation messages (token-capped at 40% of 128k)
|
|
762
|
-
const EC1_MAX_MESSAGES = 60;
|
|
763
|
-
const EC1_TOKEN_BUDGET = Math.floor(128_000 * 0.40);
|
|
764
|
-
const truncated = await truncateJsonlIfNeeded(jsonlPath, EC1_MAX_MESSAGES, false, EC1_TOKEN_BUDGET);
|
|
765
|
-
if (truncated) {
|
|
766
|
-
console.log(`[hypermem-plugin] bootstrap: proactive JSONL trim for ${agentId} ` +
|
|
767
|
-
`(EC1 guard — next restart will load clean)`);
|
|
768
|
-
}
|
|
769
|
-
}
|
|
770
|
-
catch {
|
|
771
|
-
// Non-fatal — JSONL truncation is best-effort
|
|
772
|
-
}
|
|
745
|
+
// EC1 JSONL truncation moved to maintain() — bootstrap stays fast.
|
|
773
746
|
// Fast path: if session already has history in Redis, skip warm entirely.
|
|
774
747
|
// sessionExists() is a single EXISTS call — sub-millisecond cost.
|
|
775
748
|
const alreadyWarm = await hm.cache.sessionExists(agentId, sk);
|
|
@@ -895,6 +868,67 @@ function createHyperMemEngine() {
|
|
|
895
868
|
return { bootstrapped: false, reason: err.message };
|
|
896
869
|
}
|
|
897
870
|
},
|
|
871
|
+
/**
|
|
872
|
+
* Transcript maintenance — runs after bootstrap, successful turns, or compaction.
|
|
873
|
+
*
|
|
874
|
+
* Moved from bootstrap: proactive JSONL truncation is forward-looking (helps
|
|
875
|
+
* next restart, not current session), so it belongs in maintenance, not init.
|
|
876
|
+
* Also runs tool pair repair on Redis history to fix orphaned pairs from
|
|
877
|
+
* trim/compaction passes.
|
|
878
|
+
*/
|
|
879
|
+
async maintain({ sessionId, sessionKey, sessionFile }) {
|
|
880
|
+
let changed = false;
|
|
881
|
+
let bytesFreed = 0;
|
|
882
|
+
let rewrittenEntries = 0;
|
|
883
|
+
try {
|
|
884
|
+
const hm = await getHyperMem();
|
|
885
|
+
const sk = resolveSessionKey(sessionId, sessionKey);
|
|
886
|
+
const agentId = extractAgentId(sk);
|
|
887
|
+
// 1. Proactive JSONL truncation (EC1 guard — next restart loads clean)
|
|
888
|
+
try {
|
|
889
|
+
const EC1_MAX_MESSAGES = 60;
|
|
890
|
+
const EC1_TOKEN_BUDGET = Math.floor(128_000 * 0.40);
|
|
891
|
+
const truncated = await truncateJsonlIfNeeded(sessionFile, EC1_MAX_MESSAGES, false, EC1_TOKEN_BUDGET);
|
|
892
|
+
if (truncated) {
|
|
893
|
+
console.log(`[hypermem-plugin] maintain: proactive JSONL trim for ${agentId} ` +
|
|
894
|
+
`(EC1 guard — next restart will load clean)`);
|
|
895
|
+
changed = true;
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
catch {
|
|
899
|
+
// Non-fatal — JSONL truncation is best-effort
|
|
900
|
+
}
|
|
901
|
+
// 2. Redis history tool pair repair
|
|
902
|
+
// Compaction and trim passes can orphan tool_call/tool_result pairs.
|
|
903
|
+
// Anthropic and Gemini reject orphaned pairs with 400 errors.
|
|
904
|
+
try {
|
|
905
|
+
const history = await hm.cache.getHistory(agentId, sk);
|
|
906
|
+
if (history && history.length > 0) {
|
|
907
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
908
|
+
const repairedHistory = repairToolPairs(history);
|
|
909
|
+
const removedCount = history.length - repairedHistory.length;
|
|
910
|
+
if (removedCount > 0) {
|
|
911
|
+
await hm.cache.replaceHistory(agentId, sk, repairedHistory);
|
|
912
|
+
await hm.cache.invalidateWindow(agentId, sk);
|
|
913
|
+
console.log(`[hypermem-plugin] maintain: repaired tool pairs in Redis history ` +
|
|
914
|
+
`for ${agentId} (removed ${removedCount} orphaned messages)`);
|
|
915
|
+
changed = true;
|
|
916
|
+
rewrittenEntries += removedCount;
|
|
917
|
+
// Rough estimate: ~500 bytes per removed message
|
|
918
|
+
bytesFreed += removedCount * 500;
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
catch {
|
|
923
|
+
// Non-fatal
|
|
924
|
+
}
|
|
925
|
+
return { changed, bytesFreed, rewrittenEntries };
|
|
926
|
+
}
|
|
927
|
+
catch (err) {
|
|
928
|
+
console.warn('[hypermem-plugin] maintain failed:', err.message);
|
|
929
|
+
return { changed, bytesFreed, rewrittenEntries, reason: err.message };
|
|
930
|
+
}
|
|
931
|
+
},
|
|
898
932
|
/**
|
|
899
933
|
* Ingest a single message into hypermem's message store.
|
|
900
934
|
* Skip heartbeats — they're noise in the memory store.
|
|
@@ -983,6 +1017,41 @@ function createHyperMemEngine() {
|
|
|
983
1017
|
return { ingested: false };
|
|
984
1018
|
}
|
|
985
1019
|
},
|
|
1020
|
+
/**
|
|
1021
|
+
* Batch ingest: process multiple messages in a single call.
|
|
1022
|
+
*
|
|
1023
|
+
* Note: when afterTurn() is defined (which it is), the runtime calls
|
|
1024
|
+
* afterTurn instead of ingest/ingestBatch. This is here for interface
|
|
1025
|
+
* completeness and forward compatibility.
|
|
1026
|
+
*/
|
|
1027
|
+
async ingestBatch({ sessionId, sessionKey, messages, isHeartbeat }) {
|
|
1028
|
+
if (isHeartbeat) {
|
|
1029
|
+
return { ingestedCount: 0 };
|
|
1030
|
+
}
|
|
1031
|
+
let ingestedCount = 0;
|
|
1032
|
+
try {
|
|
1033
|
+
const hm = await getHyperMem();
|
|
1034
|
+
const sk = resolveSessionKey(sessionId, sessionKey);
|
|
1035
|
+
const agentId = extractAgentId(sk);
|
|
1036
|
+
for (const message of messages) {
|
|
1037
|
+
const msg = message;
|
|
1038
|
+
if (msg.role === 'system')
|
|
1039
|
+
continue;
|
|
1040
|
+
const neutral = toNeutralMessage(msg);
|
|
1041
|
+
if (neutral.role === 'user' && !neutral.toolResults?.length) {
|
|
1042
|
+
await hm.recordUserMessage(agentId, sk, stripMessageMetadata(neutral.textContent ?? ''));
|
|
1043
|
+
}
|
|
1044
|
+
else {
|
|
1045
|
+
await hm.recordAssistantMessage(agentId, sk, neutral);
|
|
1046
|
+
}
|
|
1047
|
+
ingestedCount++;
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
catch (err) {
|
|
1051
|
+
console.warn('[hypermem-plugin] ingestBatch failed:', err.message);
|
|
1052
|
+
}
|
|
1053
|
+
return { ingestedCount };
|
|
1054
|
+
},
|
|
986
1055
|
/**
|
|
987
1056
|
* Assemble model context from all four hypermem layers.
|
|
988
1057
|
*
|
|
@@ -1954,6 +2023,81 @@ function createHyperMemEngine() {
|
|
|
1954
2023
|
console.warn('[hypermem-plugin] afterTurn failed:', err.message);
|
|
1955
2024
|
}
|
|
1956
2025
|
},
|
|
2026
|
+
/**
|
|
2027
|
+
* Prepare context for a subagent session before it starts.
|
|
2028
|
+
*
|
|
2029
|
+
* Seeds the child session's Redis with parent context based on the
|
|
2030
|
+
* subagentWarming config ('full' | 'light' | 'off').
|
|
2031
|
+
* Returns a rollback handle to clean up if spawn fails.
|
|
2032
|
+
*/
|
|
2033
|
+
async prepareSubagentSpawn({ parentSessionKey, childSessionKey }) {
|
|
2034
|
+
if (_subagentWarming === 'off') {
|
|
2035
|
+
return undefined;
|
|
2036
|
+
}
|
|
2037
|
+
try {
|
|
2038
|
+
const hm = await getHyperMem();
|
|
2039
|
+
const parentAgentId = extractAgentId(parentSessionKey);
|
|
2040
|
+
const childAgentId = extractAgentId(childSessionKey);
|
|
2041
|
+
// Seed child with parent's active facts
|
|
2042
|
+
const facts = hm.getActiveFacts(parentAgentId, { limit: 50 });
|
|
2043
|
+
if (facts && facts.length > 0) {
|
|
2044
|
+
const factBlock = facts
|
|
2045
|
+
.map(f => f.content)
|
|
2046
|
+
.join('\n');
|
|
2047
|
+
await hm.cache.setSlot(childAgentId, childSessionKey, 'parentFacts', factBlock);
|
|
2048
|
+
}
|
|
2049
|
+
// For 'full' warming, also seed recent history context
|
|
2050
|
+
if (_subagentWarming === 'full') {
|
|
2051
|
+
const history = await hm.cache.getHistory(parentAgentId, parentSessionKey);
|
|
2052
|
+
if (history && history.length > 0) {
|
|
2053
|
+
const recentHistory = history.slice(-10);
|
|
2054
|
+
await hm.cache.setSlot(childAgentId, childSessionKey, 'parentHistory', JSON.stringify(recentHistory));
|
|
2055
|
+
}
|
|
2056
|
+
}
|
|
2057
|
+
console.log(`[hypermem-plugin] prepareSubagentSpawn: seeded ${childSessionKey} ` +
|
|
2058
|
+
`from ${parentSessionKey} (warming=${_subagentWarming})`);
|
|
2059
|
+
return {
|
|
2060
|
+
async rollback() {
|
|
2061
|
+
try {
|
|
2062
|
+
const hm = await getHyperMem();
|
|
2063
|
+
await hm.cache.setSlot(childAgentId, childSessionKey, 'parentFacts', '');
|
|
2064
|
+
await hm.cache.setSlot(childAgentId, childSessionKey, 'parentHistory', '');
|
|
2065
|
+
}
|
|
2066
|
+
catch {
|
|
2067
|
+
// Rollback is best-effort
|
|
2068
|
+
}
|
|
2069
|
+
},
|
|
2070
|
+
};
|
|
2071
|
+
}
|
|
2072
|
+
catch (err) {
|
|
2073
|
+
console.warn('[hypermem-plugin] prepareSubagentSpawn failed (non-fatal):', err.message);
|
|
2074
|
+
return undefined;
|
|
2075
|
+
}
|
|
2076
|
+
},
|
|
2077
|
+
/**
|
|
2078
|
+
* Clean up after a subagent session ends.
|
|
2079
|
+
*
|
|
2080
|
+
* Removes Redis slots and invalidates caches for the dead session
|
|
2081
|
+
* to prevent stale data accumulation.
|
|
2082
|
+
*/
|
|
2083
|
+
async onSubagentEnded({ childSessionKey, reason }) {
|
|
2084
|
+
try {
|
|
2085
|
+
const hm = await getHyperMem();
|
|
2086
|
+
const childAgentId = extractAgentId(childSessionKey);
|
|
2087
|
+
await Promise.all([
|
|
2088
|
+
hm.cache.setSlot(childAgentId, childSessionKey, 'parentFacts', ''),
|
|
2089
|
+
hm.cache.setSlot(childAgentId, childSessionKey, 'parentHistory', ''),
|
|
2090
|
+
hm.cache.setSlot(childAgentId, childSessionKey, 'assemblyContextBlock', ''),
|
|
2091
|
+
hm.cache.setSlot(childAgentId, childSessionKey, 'assemblyContextAt', '0'),
|
|
2092
|
+
hm.cache.invalidateWindow(childAgentId, childSessionKey).catch(() => { }),
|
|
2093
|
+
]);
|
|
2094
|
+
_overheadCache.delete(childSessionKey);
|
|
2095
|
+
console.log(`[hypermem-plugin] onSubagentEnded: cleaned up ${childSessionKey} (reason=${reason})`);
|
|
2096
|
+
}
|
|
2097
|
+
catch (err) {
|
|
2098
|
+
console.warn('[hypermem-plugin] onSubagentEnded failed (non-fatal):', err.message);
|
|
2099
|
+
}
|
|
2100
|
+
},
|
|
1957
2101
|
/**
|
|
1958
2102
|
* Dispose: intentionally a no-op.
|
|
1959
2103
|
*
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@psiclawops/hypercompositor",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "HyperCompositor — context engine plugin for OpenClaw",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -25,12 +25,12 @@
|
|
|
25
25
|
"./dist/index.js"
|
|
26
26
|
],
|
|
27
27
|
"compat": {
|
|
28
|
-
"pluginApi": ">=2026.4.
|
|
29
|
-
"minGatewayVersion": "2026.4.
|
|
28
|
+
"pluginApi": ">=2026.4.12",
|
|
29
|
+
"minGatewayVersion": "2026.4.12"
|
|
30
30
|
},
|
|
31
31
|
"build": {
|
|
32
32
|
"openclawVersion": "2026.4.9",
|
|
33
|
-
"pluginSdkVersion": "2026.4.
|
|
33
|
+
"pluginSdkVersion": "2026.4.12"
|
|
34
34
|
}
|
|
35
35
|
},
|
|
36
36
|
"scripts": {
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"typecheck": "tsc --noEmit"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@psiclawops/hypermem": "^0.
|
|
41
|
+
"@psiclawops/hypermem": "^0.6.3"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"openclaw": "*",
|