@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.
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAMH,OAAO,KAAK,EACV,cAAc,EACd,eAAe,EACf,iBAAiB,EACjB,cAAc,EACd,aAAa,EAId,MAAM,sBAAsB,CAAC;AAW9B,YAAY,EAAE,cAAc,EAAE,eAAe,EAAE,iBAAiB,EAAE,cAAc,EAAE,aAAa,EAAE,CAAC;AA6uElG;;;;GAIG;AACH,wBAAsB,iBAAiB,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAU1F;;;;;;;;AA8DD,wBA4CG"}
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.5.4',
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 pre-flight: proactively truncate the JSONL on disk if it is over
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
  *
@@ -2,6 +2,9 @@
2
2
  "id": "hypercompositor",
3
3
  "enabledByDefault": false,
4
4
  "kind": "context-engine",
5
+ "activation": {
6
+ "onCapabilities": ["context-engine"]
7
+ },
5
8
  "configSchema": {
6
9
  "type": "object",
7
10
  "additionalProperties": false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@psiclawops/hypercompositor",
3
- "version": "0.6.2",
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.5",
29
- "minGatewayVersion": "2026.4.5"
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.5"
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.5.2"
41
+ "@psiclawops/hypermem": "^0.6.3"
42
42
  },
43
43
  "devDependencies": {
44
44
  "openclaw": "*",