@martian-engineering/lossless-claw 0.9.3 → 0.10.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.
@@ -0,0 +1,231 @@
1
+ const PLUGIN_ID = "lossless-claw";
2
+ const ENTRY_PATH = ["plugins", "entries", PLUGIN_ID];
3
+ const CONFIG_PATH = [...ENTRY_PATH, "config"];
4
+
5
+ function isRecord(value) {
6
+ return !!value && typeof value === "object" && !Array.isArray(value);
7
+ }
8
+
9
+ function readString(value) {
10
+ return typeof value === "string" && value.trim() ? value.trim() : "";
11
+ }
12
+
13
+ function readEntry(cfg) {
14
+ const plugins = isRecord(cfg) ? cfg.plugins : undefined;
15
+ const entries = isRecord(plugins) ? plugins.entries : undefined;
16
+ const entry = isRecord(entries) ? entries[PLUGIN_ID] : undefined;
17
+ return isRecord(entry) ? entry : undefined;
18
+ }
19
+
20
+ function readConfig(cfg) {
21
+ const config = readEntry(cfg)?.config;
22
+ return isRecord(config) ? config : undefined;
23
+ }
24
+
25
+ function readLlmPolicy(cfg) {
26
+ const llm = readEntry(cfg)?.llm;
27
+ if (!isRecord(llm)) {
28
+ return {
29
+ allowModelOverride: false,
30
+ allowedModels: [],
31
+ };
32
+ }
33
+ return {
34
+ allowModelOverride: llm.allowModelOverride === true,
35
+ allowedModels: Array.isArray(llm.allowedModels) ? llm.allowedModels : [],
36
+ };
37
+ }
38
+
39
+ function toModelRef(provider, model) {
40
+ const modelId = readString(model);
41
+ if (!modelId) {
42
+ return undefined;
43
+ }
44
+ const slash = modelId.indexOf("/");
45
+ if (slash > 0 && slash < modelId.length - 1) {
46
+ const directProvider = modelId.slice(0, slash).trim();
47
+ const directModel = modelId.slice(slash + 1).trim();
48
+ return directProvider && directModel ? `${directProvider}/${directModel}` : undefined;
49
+ }
50
+ const providerId = readString(provider);
51
+ return providerId ? `${providerId}/${modelId}` : undefined;
52
+ }
53
+
54
+ /** Collect configured Lossless summary model refs that doctor can safely allowlist. */
55
+ function collectLosslessRuntimeLlmModelRefs(cfg) {
56
+ const config = readConfig(cfg);
57
+ if (!config) {
58
+ return { modelRefs: [], skipped: [] };
59
+ }
60
+
61
+ const modelRefs = [];
62
+ const skipped = [];
63
+ const addConfiguredModel = (field, model, provider, configPath) => {
64
+ const modelId = readString(model);
65
+ if (!modelId) {
66
+ return;
67
+ }
68
+ const modelRef = toModelRef(provider, modelId);
69
+ if (modelRef) {
70
+ modelRefs.push({ field, modelRef, configPath });
71
+ return;
72
+ }
73
+ skipped.push({
74
+ field,
75
+ configPath,
76
+ reason: `${field} is a bare model without a provider; use provider/model or set the matching provider field so doctor can update plugins.entries.${PLUGIN_ID}.llm.allowedModels.`,
77
+ });
78
+ };
79
+
80
+ addConfiguredModel(
81
+ "summaryModel",
82
+ config.summaryModel,
83
+ config.summaryProvider,
84
+ [...CONFIG_PATH, "summaryModel"].join("."),
85
+ );
86
+ addConfiguredModel(
87
+ "largeFileSummaryModel",
88
+ config.largeFileSummaryModel,
89
+ config.largeFileSummaryProvider,
90
+ [...CONFIG_PATH, "largeFileSummaryModel"].join("."),
91
+ );
92
+
93
+ if (Array.isArray(config.fallbackProviders)) {
94
+ for (const [index, fallback] of config.fallbackProviders.entries()) {
95
+ if (!isRecord(fallback)) {
96
+ skipped.push({
97
+ field: "fallbackProviders",
98
+ configPath: `${[...CONFIG_PATH, "fallbackProviders"].join(".")}[${index}]`,
99
+ reason:
100
+ "fallbackProviders entries must be objects with provider and model before doctor can update llm.allowedModels.",
101
+ });
102
+ continue;
103
+ }
104
+ const modelRef = toModelRef(fallback.provider, fallback.model);
105
+ if (modelRef) {
106
+ modelRefs.push({
107
+ field: "fallbackProviders",
108
+ modelRef,
109
+ configPath: `${[...CONFIG_PATH, "fallbackProviders"].join(".")}[${index}]`,
110
+ });
111
+ } else if (readString(fallback.model) || readString(fallback.provider)) {
112
+ skipped.push({
113
+ field: "fallbackProviders",
114
+ configPath: `${[...CONFIG_PATH, "fallbackProviders"].join(".")}[${index}]`,
115
+ reason:
116
+ "fallbackProviders entries need both provider and model before doctor can update llm.allowedModels.",
117
+ });
118
+ }
119
+ }
120
+ }
121
+
122
+ const seen = new Set();
123
+ return {
124
+ modelRefs: modelRefs.filter((entry) => {
125
+ const key = `${entry.field}:${entry.modelRef}`;
126
+ if (seen.has(key)) {
127
+ return false;
128
+ }
129
+ seen.add(key);
130
+ return true;
131
+ }),
132
+ skipped,
133
+ };
134
+ }
135
+
136
+ function collectMissingPolicyEntries(cfg) {
137
+ const { modelRefs, skipped } = collectLosslessRuntimeLlmModelRefs(cfg);
138
+ const policy = readLlmPolicy(cfg);
139
+ const allowedStrings = new Set(policy.allowedModels.filter((entry) => typeof entry === "string"));
140
+ const missingRefs = modelRefs.filter((entry) => !allowedStrings.has(entry.modelRef));
141
+ return {
142
+ modelRefs,
143
+ skipped,
144
+ missingRefs,
145
+ missingAllowModelOverride: modelRefs.length > 0 && policy.allowModelOverride !== true,
146
+ };
147
+ }
148
+
149
+ function hasIssueForField(cfg, field) {
150
+ const issues = collectMissingPolicyEntries(cfg);
151
+ return (
152
+ issues.missingAllowModelOverride ||
153
+ issues.missingRefs.some((entry) => entry.field === field) ||
154
+ issues.skipped.some((entry) => entry.field === field)
155
+ );
156
+ }
157
+
158
+ /** Doctor warning rules for Lossless runtime LLM model override policy. */
159
+ export const legacyConfigRules = [
160
+ {
161
+ path: [...CONFIG_PATH, "summaryModel"],
162
+ message:
163
+ 'Lossless summaryModel uses api.runtime.llm.complete model overrides. Configure plugins.entries.lossless-claw.llm.allowModelOverride and allowedModels, or run "openclaw doctor --fix".',
164
+ match: (_value, root) => hasIssueForField(root, "summaryModel"),
165
+ },
166
+ {
167
+ path: [...CONFIG_PATH, "largeFileSummaryModel"],
168
+ message:
169
+ 'Lossless largeFileSummaryModel uses api.runtime.llm.complete model overrides. Configure plugins.entries.lossless-claw.llm.allowModelOverride and allowedModels, or run "openclaw doctor --fix".',
170
+ match: (_value, root) => hasIssueForField(root, "largeFileSummaryModel"),
171
+ },
172
+ {
173
+ path: [...CONFIG_PATH, "fallbackProviders"],
174
+ message:
175
+ 'Lossless fallbackProviders use api.runtime.llm.complete model overrides. Configure plugins.entries.lossless-claw.llm.allowModelOverride and allowedModels, or run "openclaw doctor --fix".',
176
+ match: (_value, root) => hasIssueForField(root, "fallbackProviders"),
177
+ },
178
+ ];
179
+
180
+ function cloneRootWithLosslessLlm(cfg) {
181
+ const root = isRecord(cfg) ? { ...cfg } : {};
182
+ const plugins = isRecord(root.plugins) ? { ...root.plugins } : {};
183
+ const entries = isRecord(plugins.entries) ? { ...plugins.entries } : {};
184
+ const entry = isRecord(entries[PLUGIN_ID]) ? { ...entries[PLUGIN_ID] } : {};
185
+ const llm = isRecord(entry.llm) ? { ...entry.llm } : {};
186
+
187
+ root.plugins = plugins;
188
+ plugins.entries = entries;
189
+ entries[PLUGIN_ID] = entry;
190
+ entry.llm = llm;
191
+
192
+ return { root, llm };
193
+ }
194
+
195
+ /** Add the minimal plugin runtime LLM policy needed for configured Lossless summary models. */
196
+ export function normalizeCompatibilityConfig({ cfg }) {
197
+ const issues = collectMissingPolicyEntries(cfg);
198
+ if (issues.modelRefs.length === 0) {
199
+ return { config: cfg, changes: [] };
200
+ }
201
+
202
+ const { root, llm } = cloneRootWithLosslessLlm(cfg);
203
+ const changes = [];
204
+
205
+ if (llm.allowModelOverride !== true) {
206
+ llm.allowModelOverride = true;
207
+ changes.push("Set plugins.entries.lossless-claw.llm.allowModelOverride = true for configured Lossless summary model overrides.");
208
+ }
209
+
210
+ const currentAllowed = Array.isArray(llm.allowedModels) ? [...llm.allowedModels] : [];
211
+ const allowedStrings = new Set(currentAllowed.filter((entry) => typeof entry === "string"));
212
+ const added = [];
213
+ for (const { modelRef } of issues.modelRefs) {
214
+ if (!allowedStrings.has(modelRef)) {
215
+ currentAllowed.push(modelRef);
216
+ allowedStrings.add(modelRef);
217
+ added.push(modelRef);
218
+ }
219
+ }
220
+
221
+ if (added.length > 0 || !Array.isArray(llm.allowedModels)) {
222
+ llm.allowedModels = currentAllowed;
223
+ changes.push(
224
+ `Added plugins.entries.lossless-claw.llm.allowedModels entries for configured Lossless summary models: ${added.join(", ")}`,
225
+ );
226
+ }
227
+
228
+ return { config: root, changes };
229
+ }
230
+
231
+ export { collectLosslessRuntimeLlmModelRefs };
@@ -1,6 +1,9 @@
1
1
  {
2
2
  "id": "lossless-claw",
3
3
  "kind": "context-engine",
4
+ "activation": {
5
+ "onStartup": true
6
+ },
4
7
  "skills": [
5
8
  "skills/lossless-claw"
6
9
  ],
@@ -21,9 +24,13 @@
21
24
  "label": "Context Threshold",
22
25
  "help": "Fraction of context window that triggers compaction (0.0–1.0)"
23
26
  },
27
+ "sweepMaxDepth": {
28
+ "label": "Sweep Max Depth",
29
+ "help": "Preferred maximum condensation source depth during routine full sweeps (0 = leaf only, -1 = unlimited). Pressure sweeps may go deeper when summarized context remains above target."
30
+ },
24
31
  "incrementalMaxDepth": {
25
- "label": "Incremental Max Depth",
26
- "help": "How deep incremental compaction goes (0 = leaf only, -1 = unlimited)"
32
+ "label": "Incremental Max Depth (Deprecated)",
33
+ "help": "Deprecated alias for sweepMaxDepth. Kept for existing configs."
27
34
  },
28
35
  "freshTailCount": {
29
36
  "label": "Fresh Tail Count",
@@ -37,9 +44,17 @@
37
44
  "label": "Prompt-Aware Eviction",
38
45
  "help": "When enabled, budget-constrained assembly keeps older context by prompt relevance instead of pure chronology. This can improve recall under tight budgets, but it can also reduce provider prompt-cache hit rates because the preserved prefix changes as prompts change."
39
46
  },
47
+ "stubLargeToolPayloads": {
48
+ "label": "Stub Large Tool Payloads",
49
+ "help": "When enabled, evictable tool-result rows whose payload was externalized via lcm-blob-migrate are replaced with a [LCM Tool Output: file_xxx | …] reference at assemble time. Fresh tail is never stubbed. The agent recovers the original output via lcm_describe(id=file_xxx, expandFile=true). Default off; requires running scripts/lcm-blob-migrate.mjs first to populate messages.large_content."
50
+ },
40
51
  "leafChunkTokens": {
41
52
  "label": "Leaf Chunk Tokens",
42
- "help": "Maximum source tokens per leaf compaction chunk before summarization"
53
+ "help": "Maximum source tokens per leaf compaction chunk before summarization (default 20000)"
54
+ },
55
+ "summaryPrefixTargetTokens": {
56
+ "label": "Summary Prefix Target Tokens",
57
+ "help": "Optional target for summarized-prefix tokens after a full sweep. When omitted, Lossless derives a pressure target from contextThreshold and leafChunkTokens."
43
58
  },
44
59
  "bootstrapMaxTokens": {
45
60
  "label": "Bootstrap Max Tokens",
@@ -111,7 +126,7 @@
111
126
  },
112
127
  "summaryModel": {
113
128
  "label": "Summary Model",
114
- "help": "Model override for LCM summarization (e.g., 'gpt-5.4' to reuse the session provider, or 'openai-resp/gpt-5.4' for a full cross-provider ref)"
129
+ "help": "Runtime LLM model override for LCM summarization (e.g., 'gpt-5.4' with summaryProvider, or 'openai-resp/gpt-5.4'); explicit overrides require plugins.entries.lossless-claw.llm policy"
115
130
  },
116
131
  "summaryProvider": {
117
132
  "label": "Summary Provider",
@@ -119,7 +134,7 @@
119
134
  },
120
135
  "largeFileSummaryModel": {
121
136
  "label": "Large File Summary Model",
122
- "help": "Model override for large-file summarization"
137
+ "help": "Runtime LLM model override for large-file summarization; explicit overrides require plugins.entries.lossless-claw.llm policy"
123
138
  },
124
139
  "largeFileSummaryProvider": {
125
140
  "label": "Large File Summary Provider",
@@ -162,40 +177,40 @@
162
177
  "help": "Cooldown before the summarization circuit breaker auto-resets"
163
178
  },
164
179
  "cacheAwareCompaction.enabled": {
165
- "label": "Cache-Aware Compaction",
166
- "help": "When enabled, hot prompt cache defers incremental compaction while cold cache allows bounded catch-up passes"
180
+ "label": "Cache-Aware Compaction (Deprecated)",
181
+ "help": "Deprecated compatibility setting. Automatic compaction is now threshold-only and does not use prompt-cache hot/cold state."
167
182
  },
168
183
  "cacheAwareCompaction.cacheTTLSeconds": {
169
- "label": "Short Cache TTL (seconds)",
170
- "help": "Treat short-lived prompt-cache entries as hot for this many seconds before deferred compaction may rewrite the prompt"
184
+ "label": "Short Cache TTL (Deprecated)",
185
+ "help": "Deprecated compatibility setting. Threshold debt no longer waits for cache TTL."
171
186
  },
172
187
  "cacheAwareCompaction.maxColdCacheCatchupPasses": {
173
- "label": "Cold Cache Catch-Up Passes",
174
- "help": "Maximum incremental leaf passes allowed in one maintenance cycle when prompt cache is cold"
188
+ "label": "Cold Cache Catch-Up (Deprecated)",
189
+ "help": "Deprecated compatibility setting. Automatic cold-cache catch-up passes were removed."
175
190
  },
176
191
  "cacheAwareCompaction.hotCachePressureFactor": {
177
- "label": "Hot Cache Pressure Factor",
178
- "help": "Multiplier applied to the hot-cache leaf trigger before raw-history pressure overrides cache preservation"
192
+ "label": "Hot Cache Pressure (Deprecated)",
193
+ "help": "Deprecated compatibility setting. Hot-cache raw-history pressure no longer drives automatic compaction."
179
194
  },
180
195
  "cacheAwareCompaction.hotCacheBudgetHeadroomRatio": {
181
- "label": "Hot Cache Budget Headroom",
182
- "help": "Fraction of the real token budget that must remain free before hot-cache incremental compaction is skipped entirely"
196
+ "label": "Hot Cache Headroom (Deprecated)",
197
+ "help": "Deprecated compatibility setting. Hot-cache budget headroom no longer defers threshold compaction."
183
198
  },
184
199
  "cacheAwareCompaction.coldCacheObservationThreshold": {
185
- "label": "Cold Cache Observation Threshold",
186
- "help": "Consecutive cold observations required before non-explicit cache misses are treated as truly cold"
200
+ "label": "Cold Cache Observations (Deprecated)",
201
+ "help": "Deprecated compatibility setting. Cold-cache streaks are diagnostic only."
187
202
  },
188
203
  "cacheAwareCompaction.criticalBudgetPressureRatio": {
189
- "label": "Critical Budget Pressure Ratio",
190
- "help": "Fraction of token budget at which deferred compaction fires regardless of prompt-cache state. Defaults to 0.70 — set to 1 to disable the override and let cache-aware throttling fully control deferral."
204
+ "label": "Critical Budget Pressure (Deprecated)",
205
+ "help": "Deprecated compatibility setting. contextThreshold is now the only automatic compaction threshold."
191
206
  },
192
207
  "dynamicLeafChunkTokens.enabled": {
193
- "label": "Dynamic Leaf Chunk Tokens",
194
- "help": "When enabled, incremental compaction uses a larger working leaf chunk in busy sessions and keeps the static floor in quieter sessions"
208
+ "label": "Dynamic Leaf Chunks (Deprecated)",
209
+ "help": "Deprecated compatibility setting. Automatic compaction now uses leafChunkTokens directly."
195
210
  },
196
211
  "dynamicLeafChunkTokens.max": {
197
- "label": "Dynamic Leaf Chunk Max",
198
- "help": "Maximum working leaf chunk target for dynamic incremental compaction. The static leafChunkTokens value remains the floor."
212
+ "label": "Dynamic Leaf Chunk Max (Deprecated)",
213
+ "help": "Deprecated compatibility setting. Automatic compaction now uses leafChunkTokens directly."
199
214
  },
200
215
  "timezone": {
201
216
  "label": "Timezone",
@@ -213,9 +228,29 @@
213
228
  "label": "Proactive Threshold Compaction Mode",
214
229
  "help": "Choose deferred compaction debt by default or keep legacy inline proactive compaction"
215
230
  },
231
+ "autoRotateSessionFiles.enabled": {
232
+ "label": "Auto-Rotate Session Files",
233
+ "help": "Automatically rotate oversized LCM-managed session JSONL files after startup and runtime checks"
234
+ },
235
+ "autoRotateSessionFiles.createBackups": {
236
+ "label": "Auto-Rotate Backups",
237
+ "help": "Create the rolling rotate-latest SQLite backup before automatic session-file rotation; disabled by default"
238
+ },
239
+ "autoRotateSessionFiles.sizeBytes": {
240
+ "label": "Auto-Rotate Size Bytes",
241
+ "help": "Session JSONL byte threshold for automatic rotation (default: 2097152)"
242
+ },
243
+ "autoRotateSessionFiles.startup": {
244
+ "label": "Startup Auto-Rotate",
245
+ "help": "Startup behavior for oversized indexed OpenClaw session files with active LCM state: rotate, warn, or off"
246
+ },
247
+ "autoRotateSessionFiles.runtime": {
248
+ "label": "Runtime Auto-Rotate",
249
+ "help": "Runtime behavior for oversized current LCM session files: rotate, warn, or off"
250
+ },
216
251
  "fallbackProviders": {
217
252
  "label": "Fallback Providers",
218
- "help": "Explicit fallback provider/model pairs for compaction summarization (e.g., [{\"provider\": \"anthropic\", \"model\": \"claude-haiku-4-5\"}])"
253
+ "help": "Explicit runtime LLM fallback provider/model pairs for compaction summarization; entries require plugins.entries.lossless-claw.llm policy"
219
254
  }
220
255
  },
221
256
  "configSchema": {
@@ -230,6 +265,10 @@
230
265
  "minimum": 0,
231
266
  "maximum": 1
232
267
  },
268
+ "sweepMaxDepth": {
269
+ "type": "integer",
270
+ "minimum": -1
271
+ },
233
272
  "incrementalMaxDepth": {
234
273
  "type": "integer",
235
274
  "minimum": -1
@@ -245,10 +284,17 @@
245
284
  "promptAwareEviction": {
246
285
  "type": "boolean"
247
286
  },
287
+ "stubLargeToolPayloads": {
288
+ "type": "boolean"
289
+ },
248
290
  "leafChunkTokens": {
249
291
  "type": "integer",
250
292
  "minimum": 1
251
293
  },
294
+ "summaryPrefixTargetTokens": {
295
+ "type": "integer",
296
+ "minimum": 1
297
+ },
252
298
  "bootstrapMaxTokens": {
253
299
  "type": "integer",
254
300
  "minimum": 1
@@ -419,6 +465,38 @@
419
465
  "inline"
420
466
  ]
421
467
  },
468
+ "autoRotateSessionFiles": {
469
+ "type": "object",
470
+ "additionalProperties": false,
471
+ "properties": {
472
+ "enabled": {
473
+ "type": "boolean"
474
+ },
475
+ "createBackups": {
476
+ "type": "boolean"
477
+ },
478
+ "sizeBytes": {
479
+ "type": "integer",
480
+ "minimum": 1
481
+ },
482
+ "startup": {
483
+ "type": "string",
484
+ "enum": [
485
+ "rotate",
486
+ "warn",
487
+ "off"
488
+ ]
489
+ },
490
+ "runtime": {
491
+ "type": "string",
492
+ "enum": [
493
+ "rotate",
494
+ "warn",
495
+ "off"
496
+ ]
497
+ }
498
+ }
499
+ },
422
500
  "databasePath": {
423
501
  "description": "Path to LCM SQLite database (preferred key; alias of dbPath, default: <OPENCLAW_STATE_DIR>/lcm.db)",
424
502
  "type": "string"
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@martian-engineering/lossless-claw",
3
- "version": "0.9.3",
4
- "description": "Lossless Context Management plugin for OpenClaw — DAG-based conversation summarization with incremental compaction",
3
+ "version": "0.10.0",
4
+ "description": "Lossless Context Management plugin for OpenClaw — DAG-based conversation summarization with threshold compaction",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "license": "MIT",
@@ -16,7 +16,7 @@
16
16
  "dag"
17
17
  ],
18
18
  "scripts": {
19
- "build": "esbuild index.ts --bundle --platform=node --target=node22 --format=esm --outfile=dist/index.js --external:openclaw --external:\"@mariozechner/*\" --minify-whitespace",
19
+ "build": "esbuild index.ts --bundle --platform=node --target=node22 --format=esm --outfile=dist/index.js --external:openclaw --external:\"@earendil-works/*\" --minify-whitespace",
20
20
  "changeset": "changeset",
21
21
  "release:verify": "npm run build && npm test && npm pack --dry-run",
22
22
  "test": "vitest run --dir test",
@@ -24,6 +24,8 @@
24
24
  },
25
25
  "files": [
26
26
  "dist/",
27
+ "doctor-contract-api.d.ts",
28
+ "doctor-contract-api.js",
27
29
  "skills/",
28
30
  "openclaw.plugin.json",
29
31
  "docs/",
@@ -31,34 +33,20 @@
31
33
  "LICENSE"
32
34
  ],
33
35
  "dependencies": {
36
+ "@earendil-works/pi-agent-core": ">=0.74 <1",
37
+ "@earendil-works/pi-ai": ">=0.74 <1",
38
+ "@earendil-works/pi-coding-agent": ">=0.74 <1",
34
39
  "@sinclair/typebox": "0.34.48"
35
40
  },
36
41
  "devDependencies": {
37
42
  "@changesets/changelog-github": "^0.6.0",
38
43
  "@changesets/cli": "^2.30.0",
39
- "@mariozechner/pi-agent-core": "0.66.1",
40
- "@mariozechner/pi-ai": "0.66.1",
41
- "@mariozechner/pi-coding-agent": "0.66.1",
42
44
  "esbuild": "^0.28.0",
43
45
  "typescript": "^5.7.0",
44
46
  "vitest": "^3.0.0"
45
47
  },
46
48
  "peerDependencies": {
47
- "@mariozechner/pi-agent-core": "*",
48
- "@mariozechner/pi-ai": "*",
49
- "@mariozechner/pi-coding-agent": "*",
50
- "openclaw": "*"
51
- },
52
- "peerDependenciesMeta": {
53
- "@mariozechner/pi-agent-core": {
54
- "optional": true
55
- },
56
- "@mariozechner/pi-ai": {
57
- "optional": true
58
- },
59
- "@mariozechner/pi-coding-agent": {
60
- "optional": true
61
- }
49
+ "openclaw": ">=2026.2.17 <2026.6.0"
62
50
  },
63
51
  "publishConfig": {
64
52
  "access": "public"
@@ -68,8 +56,11 @@
68
56
  "./dist/index.js"
69
57
  ],
70
58
  "compat": {
71
- "pluginApi": ">=2026.2.17",
72
- "minGatewayVersion": "2026.2.17"
59
+ "pluginApi": ">=2026.2.17 <2026.6.0",
60
+ "minGatewayVersion": "2026.2.17",
61
+ "tested": [
62
+ "2026.5.2"
63
+ ]
73
64
  },
74
65
  "build": {
75
66
  "openclawVersion": "2026.2.17"