@martian-engineering/lossless-claw 0.10.0 → 0.11.1
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/README.md +3 -0
- package/dist/index.js +270 -14
- package/docs/configuration.md +1 -1
- package/docs/focus-briefs-implementation-plan.md +240 -0
- package/docs/tui.md +17 -0
- package/doctor-contract-api.d.ts +5 -0
- package/doctor-contract-api.js +158 -40
- package/package.json +11 -6
package/docs/configuration.md
CHANGED
|
@@ -179,7 +179,7 @@ Every automatic decision emits grep-able log lines prefixed with `[lcm] auto-rot
|
|
|
179
179
|
| `summaryTimeoutMs` | `integer` | `60000` | `LCM_SUMMARY_TIMEOUT_MS` | Maximum time to wait for one model-backed summarizer call. |
|
|
180
180
|
| `customInstructions` | `string` | `""` | `LCM_CUSTOM_INSTRUCTIONS` | Extra natural-language instructions injected into every summarization prompt. |
|
|
181
181
|
|
|
182
|
-
Summary calls are executed through OpenClaw's `api.runtime.llm.complete` capability. If you configure an explicit Lossless summary model (`summaryModel`, `largeFileSummaryModel`, or `fallbackProviders`), OpenClaw must allow that runtime LLM override under `plugins.entries.lossless-claw.llm.allowModelOverride` and `plugins.entries.lossless-claw.llm.allowedModels`. `openclaw doctor --fix` can add the minimal policy entries for configured Lossless summary models.
|
|
182
|
+
Summary calls are executed through OpenClaw's `api.runtime.llm.complete` capability. If you configure an explicit Lossless summary model (`summaryModel`, `largeFileSummaryModel`, or `fallbackProviders`), OpenClaw must allow that runtime LLM override under `plugins.entries.lossless-claw.llm.allowModelOverride` and `plugins.entries.lossless-claw.llm.allowedModels`. `openclaw doctor --fix` can add the minimal policy entries for configured Lossless summary models. Delegated expansion calls use OpenClaw's runtime sub-agent layer; explicit `expansionModel` values require `plugins.entries.lossless-claw.subagent.allowModelOverride` and a matching `subagent.allowedModels` entry, or `"*"` if you intentionally trust any expansion target. `openclaw doctor --fix` can add the minimal subagent policy, and `lcm_expand_query` retries once without the override if the host rejects it.
|
|
183
183
|
|
|
184
184
|
### Fallbacks, circuit breaking, and safety rails
|
|
185
185
|
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
# Focus Briefs Implementation Plan
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
Focus briefs let a user ask Lossless to build a task-oriented view of existing
|
|
6
|
+
conversation memory. A focus brief is generated by a delegated subagent that can
|
|
7
|
+
search and traverse the canonical summary DAG with Lossless recall tools. The
|
|
8
|
+
brief is not a summary DAG node, is not written to `context_items`, and must not
|
|
9
|
+
be consumed by normal compaction. Recall tools continue to operate on the DAG.
|
|
10
|
+
|
|
11
|
+
The user-facing name is **focus**. The generated artifact is a **context brief**.
|
|
12
|
+
|
|
13
|
+
## Command Surface
|
|
14
|
+
|
|
15
|
+
Use command words rather than flags so the interface works well on mobile:
|
|
16
|
+
|
|
17
|
+
```text
|
|
18
|
+
/lossless focus
|
|
19
|
+
/lossless focus <prompt>
|
|
20
|
+
/lossless refocus
|
|
21
|
+
/lossless unfocus
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
- `/lossless focus <prompt>` runs a forced full-sweep compaction, generates or
|
|
25
|
+
refocuses around the prompt from the fresh summary frontier, and activates the
|
|
26
|
+
resulting brief.
|
|
27
|
+
- `/lossless focus` reports active/draft focus state, prompt, preview, and diagnostics.
|
|
28
|
+
- `/lossless refocus` refreshes the active focus brief from post-focus summary
|
|
29
|
+
deltas, preserving the original prompt and using the old brief as the baseline.
|
|
30
|
+
- `/lossless unfocus` deactivates the active focus overlay once overlays exist,
|
|
31
|
+
then runs a forced full-sweep compaction.
|
|
32
|
+
|
|
33
|
+
## Architecture Decisions
|
|
34
|
+
|
|
35
|
+
- Focus brief generation uses a delegated subagent, not a direct summarizer call.
|
|
36
|
+
The subagent is expected to use `lcm_grep`, `lcm_describe`, and grant-scoped
|
|
37
|
+
`lcm_expand` to produce evidence-oriented content.
|
|
38
|
+
- Focus briefs are persisted outside the summary DAG. They are inspectable,
|
|
39
|
+
supersedable, and later activatable, but never replace canonical storage.
|
|
40
|
+
- The first implementation phases deliberately stop before mutating active
|
|
41
|
+
assembly. This lets us debug generation quality, command behavior, persistence,
|
|
42
|
+
and TUI inspection before introducing prompt-prefix overlays.
|
|
43
|
+
- The assembler overlay phase must be assembly-only. Canonical `context_items`
|
|
44
|
+
remain the sole compaction source.
|
|
45
|
+
- New summaries while focus is active should remain visible as post-focus delta.
|
|
46
|
+
The later overlay implementation should use a coverage watermark rather than
|
|
47
|
+
relying only on masked summary IDs.
|
|
48
|
+
- Focus, refocus, and unfocus are prompt-prefix lifecycle operations. Because
|
|
49
|
+
they break prompt caching just like compaction does, they deliberately pair
|
|
50
|
+
the prefix mutation with forced full-sweep compaction: focus and refocus
|
|
51
|
+
compact before generation, while unfocus compacts after deactivation.
|
|
52
|
+
|
|
53
|
+
## Phase 1: Slash Command Generates A Draft Brief
|
|
54
|
+
|
|
55
|
+
Build the real generation path without changing active context.
|
|
56
|
+
|
|
57
|
+
### Scope
|
|
58
|
+
|
|
59
|
+
- Extend `/lossless` parsing with `focus` and `unfocus`.
|
|
60
|
+
- Implement `/lossless focus <prompt>` using the active conversation resolved
|
|
61
|
+
from session identity.
|
|
62
|
+
- Snapshot active summary context IDs and token counts.
|
|
63
|
+
- Spawn a delegated focus subagent with a scoped expansion grant.
|
|
64
|
+
- Require structured JSON from the child containing markdown brief content,
|
|
65
|
+
cited summary IDs, expansion prompts, and diagnostics.
|
|
66
|
+
- Return a preview in the command response.
|
|
67
|
+
- Do not persist the brief yet unless Phase 2 is already present.
|
|
68
|
+
- Do not modify assembler, compaction, summaries, parents, or context items.
|
|
69
|
+
|
|
70
|
+
### Acceptance Criteria
|
|
71
|
+
|
|
72
|
+
- `/lossless focus <prompt>` returns a generated brief preview.
|
|
73
|
+
- The child session can use `lcm_grep`, `lcm_describe`, and grant-scoped
|
|
74
|
+
`lcm_expand`.
|
|
75
|
+
- The child task prompt tells it not to call `lcm_expand_query`.
|
|
76
|
+
- Malformed child JSON is handled with a useful error and raw preview.
|
|
77
|
+
- Existing `/lossless` commands keep working.
|
|
78
|
+
|
|
79
|
+
### Verification
|
|
80
|
+
|
|
81
|
+
- Parser tests cover `focus`, empty `focus`, `unfocus`, and unknown focus forms.
|
|
82
|
+
- Delegation tests cover child prompt content, grant creation, wait handling,
|
|
83
|
+
parse success, and parse failure.
|
|
84
|
+
- Command tests cover no active conversation, no prompt, and successful preview.
|
|
85
|
+
|
|
86
|
+
## Phase 2: Persist Draft Briefs And Expose Them In The TUI
|
|
87
|
+
|
|
88
|
+
Persist generated briefs outside active context so they can be inspected and
|
|
89
|
+
used as later overlay inputs. Add TUI visibility for generated focus briefs.
|
|
90
|
+
|
|
91
|
+
### Scope
|
|
92
|
+
|
|
93
|
+
- Add migrations for focus brief tables.
|
|
94
|
+
- Add a store for focus brief creation, lookup, superseding, and listing.
|
|
95
|
+
- Update `/lossless focus <prompt>` to persist generated briefs as drafts in the pre-overlay phase.
|
|
96
|
+
- Update `/lossless focus` to show latest draft/active state, prompt, preview,
|
|
97
|
+
source count, cited IDs, generation status, and stale/error diagnostics.
|
|
98
|
+
- Add `/lossless unfocus` as a no-op or draft-state deactivation command until
|
|
99
|
+
assembly overlays exist.
|
|
100
|
+
- Extend the TUI with a focus brief view so generated briefs can be browsed and
|
|
101
|
+
read from the terminal.
|
|
102
|
+
- Do not show active focus content inline in conversation yet. That belongs to
|
|
103
|
+
the later active-overlay phase.
|
|
104
|
+
|
|
105
|
+
### Suggested Tables
|
|
106
|
+
|
|
107
|
+
```sql
|
|
108
|
+
CREATE TABLE focus_briefs (
|
|
109
|
+
brief_id TEXT PRIMARY KEY,
|
|
110
|
+
conversation_id INTEGER NOT NULL,
|
|
111
|
+
session_key TEXT,
|
|
112
|
+
prompt TEXT NOT NULL,
|
|
113
|
+
content TEXT NOT NULL,
|
|
114
|
+
status TEXT NOT NULL,
|
|
115
|
+
token_count INTEGER NOT NULL DEFAULT 0,
|
|
116
|
+
target_tokens INTEGER NOT NULL DEFAULT 0,
|
|
117
|
+
covered_latest_at TEXT,
|
|
118
|
+
covered_message_seq INTEGER,
|
|
119
|
+
source_context_hash TEXT NOT NULL DEFAULT '',
|
|
120
|
+
generator_run_id TEXT,
|
|
121
|
+
generator_session_key TEXT,
|
|
122
|
+
raw_result_json TEXT,
|
|
123
|
+
error TEXT,
|
|
124
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
125
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
126
|
+
superseded_at TEXT
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
CREATE TABLE focus_brief_sources (
|
|
130
|
+
brief_id TEXT NOT NULL,
|
|
131
|
+
summary_id TEXT NOT NULL,
|
|
132
|
+
ordinal INTEGER,
|
|
133
|
+
role TEXT NOT NULL,
|
|
134
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
135
|
+
PRIMARY KEY (brief_id, summary_id, role)
|
|
136
|
+
);
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### Acceptance Criteria
|
|
140
|
+
|
|
141
|
+
- Generated briefs are durable and inspectable.
|
|
142
|
+
- Refocus supersedes the previous current draft for the conversation.
|
|
143
|
+
- Failed generations can be recorded without corrupting the latest good brief.
|
|
144
|
+
- TUI can list focus briefs for the selected conversation and display full
|
|
145
|
+
content plus prompt/metadata.
|
|
146
|
+
- No focus table rows are used by compaction or assembly yet.
|
|
147
|
+
|
|
148
|
+
### Verification
|
|
149
|
+
|
|
150
|
+
- Migration tests verify tables and indexes are created.
|
|
151
|
+
- Store tests cover create, list latest, supersede, source recording, and failed
|
|
152
|
+
generation rows.
|
|
153
|
+
- Command tests verify persisted status and preview.
|
|
154
|
+
- TUI tests cover loading and rendering focus brief rows.
|
|
155
|
+
|
|
156
|
+
## Phase 3: Brief Quality Contract
|
|
157
|
+
|
|
158
|
+
Harden the generator prompt and output schema after observing Phase 1/2 output.
|
|
159
|
+
|
|
160
|
+
### Required Brief Sections
|
|
161
|
+
|
|
162
|
+
- Focused narrative.
|
|
163
|
+
- Current working state.
|
|
164
|
+
- Evidence map with summary IDs.
|
|
165
|
+
- Expansion guide with exact future recall prompts.
|
|
166
|
+
- Risks, gaps, contradictions, stale assumptions.
|
|
167
|
+
- Likely irrelevant context.
|
|
168
|
+
- Confidence/evidence notes.
|
|
169
|
+
|
|
170
|
+
### Acceptance Criteria
|
|
171
|
+
|
|
172
|
+
- The schema captures cited IDs, expansion prompts, truncation state, and
|
|
173
|
+
confidence notes.
|
|
174
|
+
- The command response and TUI surface enough metadata to troubleshoot brief
|
|
175
|
+
quality without reading raw DB rows.
|
|
176
|
+
|
|
177
|
+
## Phase 4: Assembly-Time Overlay
|
|
178
|
+
|
|
179
|
+
Make active focus mode affect assembled context.
|
|
180
|
+
|
|
181
|
+
### Scope
|
|
182
|
+
|
|
183
|
+
- Add active-focus lookup to assembly.
|
|
184
|
+
- Replace covered summary prefix with one `<focus_brief>` user message.
|
|
185
|
+
- Preserve fresh tail and post-focus delta.
|
|
186
|
+
- Keep canonical `context_items` unchanged.
|
|
187
|
+
- Ensure compaction never reads focus brief rows as source material.
|
|
188
|
+
- Promote successful `/lossless focus <prompt>` generations to active overlays.
|
|
189
|
+
- Make `/lossless unfocus` deactivate active overlays without deleting brief history.
|
|
190
|
+
- Run forced full-sweep compaction before focus generation so the delegated
|
|
191
|
+
subagent reads the freshest balanced summary frontier.
|
|
192
|
+
- Run forced full-sweep compaction before refocus generation, then give the
|
|
193
|
+
delegated subagent the old active brief plus summaries beyond the old focus
|
|
194
|
+
watermark as delta input. A successful refocus writes a new active brief with
|
|
195
|
+
advanced coverage watermarks; a failed refocus leaves the old active brief in
|
|
196
|
+
place.
|
|
197
|
+
- Run forced full-sweep compaction after unfocus so the normal DAG view is
|
|
198
|
+
restored with the same cache break that removed the overlay.
|
|
199
|
+
|
|
200
|
+
### Acceptance Criteria
|
|
201
|
+
|
|
202
|
+
- Assembled prompts include the active focus brief.
|
|
203
|
+
- Canonical context and DAG remain unchanged.
|
|
204
|
+
- New messages/summaries after the focus watermark remain visible.
|
|
205
|
+
- Unfocus restores normal assembly.
|
|
206
|
+
|
|
207
|
+
## Phase 5: Unfocus, Refocus, And Drift Diagnostics
|
|
208
|
+
|
|
209
|
+
Complete the operational lifecycle.
|
|
210
|
+
|
|
211
|
+
### Scope
|
|
212
|
+
|
|
213
|
+
- Harden refocus atomicity: failed new generation must not destroy the old active
|
|
214
|
+
focus.
|
|
215
|
+
- Show delta since focus: message count, summary count, and estimated tokens.
|
|
216
|
+
- Warn when active focus is stale, truncated, or generated from an obsolete
|
|
217
|
+
source snapshot.
|
|
218
|
+
|
|
219
|
+
### Acceptance Criteria
|
|
220
|
+
|
|
221
|
+
- Focus lifecycle is reversible and debuggable.
|
|
222
|
+
- Drift is visible in `/lossless` status and TUI.
|
|
223
|
+
|
|
224
|
+
## Risks And Mitigations
|
|
225
|
+
|
|
226
|
+
| Risk | Impact | Mitigation |
|
|
227
|
+
| --- | --- | --- |
|
|
228
|
+
| Child calls `lcm_expand_query` recursively | High | Prompt explicitly forbids it; reuse recursion guard language; subagent should use `lcm_expand` directly. |
|
|
229
|
+
| Brief is accidentally compacted | High | Never store brief as summary or context item; later overlay is assembly-only. |
|
|
230
|
+
| Masking breaks after DAG reshaping | High | Use coverage watermarks in overlay phase; keep masked IDs as diagnostics. |
|
|
231
|
+
| Command responses are too small for full briefs | Medium | Persist full content in Phase 2; command shows preview and brief ID. |
|
|
232
|
+
| Generation quality is weak | Medium | Ship Phases 1/2 before overlay; inspect drafts in TUI; iterate prompt/schema. |
|
|
233
|
+
| Ambient subagent cleanup fails | Medium | Follow existing delegated expansion cleanup/revoke patterns. |
|
|
234
|
+
|
|
235
|
+
## Not Doing Yet
|
|
236
|
+
|
|
237
|
+
- No active prompt-prefix mutation in Phases 1 or 2.
|
|
238
|
+
- No `/compact` integration until the Lossless command path is proven.
|
|
239
|
+
- No changes to recall tool search semantics.
|
|
240
|
+
- No destructive cleanup of focus brief history.
|
package/docs/tui.md
CHANGED
|
@@ -86,6 +86,7 @@ For sessions with an LCM `conv_id`, the conversation view uses keyset-paged wind
|
|
|
86
86
|
| `]` | Load newer message window |
|
|
87
87
|
| `l` | Open **Summary DAG** view |
|
|
88
88
|
| `c` | Open **Context** view |
|
|
89
|
+
| `o` | Open **Focus Briefs** view |
|
|
89
90
|
| `f` | Open **Large Files** view |
|
|
90
91
|
| `v` | Open **Codex ↔ LCM** comparison view |
|
|
91
92
|
| `b`/`Backspace` | Back to sessions |
|
|
@@ -168,6 +169,22 @@ The status bar shows totals: how many summaries, how many messages, total items,
|
|
|
168
169
|
| `b`/`Backspace` | Back to conversation |
|
|
169
170
|
| `q` | Quit |
|
|
170
171
|
|
|
172
|
+
## Focus Briefs View
|
|
173
|
+
|
|
174
|
+
Lists focus briefs generated for the selected LCM conversation. Each row shows status, creation time, brief ID, token count, and prompt preview. The detail panel shows generator metadata, source/citation counts, post-focus drift diagnostics, cited and expanded summary IDs, the original focus prompt, and the generated brief content.
|
|
175
|
+
|
|
176
|
+
This view is read-only. When a focus brief is active, the conversation and active-context screens show a compact focus banner with the brief ID, prompt preview, token count, and stale/source-snapshot diagnostics.
|
|
177
|
+
|
|
178
|
+
| Key | Action |
|
|
179
|
+
|-----|--------|
|
|
180
|
+
| `↑`/`↓` or `k`/`j` | Move cursor |
|
|
181
|
+
| `g`/`G` | Jump to first/last |
|
|
182
|
+
| `Shift+J` | Scroll detail panel down |
|
|
183
|
+
| `Shift+K` | Scroll detail panel up |
|
|
184
|
+
| `r` | Reload focus briefs |
|
|
185
|
+
| `b`/`Backspace` | Back to conversation |
|
|
186
|
+
| `q` | Quit |
|
|
187
|
+
|
|
171
188
|
## Large Files View
|
|
172
189
|
|
|
173
190
|
Lists files that exceeded the large file threshold (default 25k tokens) and were intercepted by LCM. Shows file ID, display name, MIME type, byte size, and creation time. The detail panel shows the exploration summary that was generated as a lightweight stand-in.
|
package/doctor-contract-api.d.ts
CHANGED
|
@@ -27,3 +27,8 @@ export function collectLosslessRuntimeLlmModelRefs(cfg: unknown): {
|
|
|
27
27
|
modelRefs: LosslessRuntimeLlmModelRef[];
|
|
28
28
|
skipped: LosslessRuntimeLlmSkippedModelRef[];
|
|
29
29
|
};
|
|
30
|
+
|
|
31
|
+
export function collectLosslessSubagentModelRefs(cfg: unknown): {
|
|
32
|
+
modelRefs: LosslessRuntimeLlmModelRef[];
|
|
33
|
+
skipped: LosslessRuntimeLlmSkippedModelRef[];
|
|
34
|
+
};
|
package/doctor-contract-api.js
CHANGED
|
@@ -22,20 +22,28 @@ function readConfig(cfg) {
|
|
|
22
22
|
return isRecord(config) ? config : undefined;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
function
|
|
26
|
-
const
|
|
27
|
-
if (!isRecord(
|
|
25
|
+
function readModelOverridePolicy(cfg, policyKey) {
|
|
26
|
+
const policy = readEntry(cfg)?.[policyKey];
|
|
27
|
+
if (!isRecord(policy)) {
|
|
28
28
|
return {
|
|
29
29
|
allowModelOverride: false,
|
|
30
30
|
allowedModels: [],
|
|
31
31
|
};
|
|
32
32
|
}
|
|
33
33
|
return {
|
|
34
|
-
allowModelOverride:
|
|
35
|
-
allowedModels: Array.isArray(
|
|
34
|
+
allowModelOverride: policy.allowModelOverride === true,
|
|
35
|
+
allowedModels: Array.isArray(policy.allowedModels) ? policy.allowedModels : [],
|
|
36
36
|
};
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
function readLlmPolicy(cfg) {
|
|
40
|
+
return readModelOverridePolicy(cfg, "llm");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function readSubagentPolicy(cfg) {
|
|
44
|
+
return readModelOverridePolicy(cfg, "subagent");
|
|
45
|
+
}
|
|
46
|
+
|
|
39
47
|
function toModelRef(provider, model) {
|
|
40
48
|
const modelId = readString(model);
|
|
41
49
|
if (!modelId) {
|
|
@@ -51,6 +59,18 @@ function toModelRef(provider, model) {
|
|
|
51
59
|
return providerId ? `${providerId}/${modelId}` : undefined;
|
|
52
60
|
}
|
|
53
61
|
|
|
62
|
+
function uniqueModelRefs(modelRefs) {
|
|
63
|
+
const seen = new Set();
|
|
64
|
+
return modelRefs.filter((entry) => {
|
|
65
|
+
const key = `${entry.field}:${entry.modelRef}`;
|
|
66
|
+
if (seen.has(key)) {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
seen.add(key);
|
|
70
|
+
return true;
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
54
74
|
/** Collect configured Lossless summary model refs that doctor can safely allowlist. */
|
|
55
75
|
function collectLosslessRuntimeLlmModelRefs(cfg) {
|
|
56
76
|
const config = readConfig(cfg);
|
|
@@ -119,16 +139,42 @@ function collectLosslessRuntimeLlmModelRefs(cfg) {
|
|
|
119
139
|
}
|
|
120
140
|
}
|
|
121
141
|
|
|
122
|
-
const seen = new Set();
|
|
123
142
|
return {
|
|
124
|
-
modelRefs: modelRefs
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
143
|
+
modelRefs: uniqueModelRefs(modelRefs),
|
|
144
|
+
skipped,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Collect configured Lossless expansion model refs that doctor can safely allowlist. */
|
|
149
|
+
function collectLosslessSubagentModelRefs(cfg) {
|
|
150
|
+
const config = readConfig(cfg);
|
|
151
|
+
if (!config) {
|
|
152
|
+
return { modelRefs: [], skipped: [] };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const modelRefs = [];
|
|
156
|
+
const skipped = [];
|
|
157
|
+
const modelId = readString(config.expansionModel);
|
|
158
|
+
if (modelId) {
|
|
159
|
+
const modelRef = toModelRef(config.expansionProvider, modelId);
|
|
160
|
+
if (modelRef) {
|
|
161
|
+
modelRefs.push({
|
|
162
|
+
field: "expansionModel",
|
|
163
|
+
modelRef,
|
|
164
|
+
configPath: [...CONFIG_PATH, "expansionModel"].join("."),
|
|
165
|
+
});
|
|
166
|
+
} else {
|
|
167
|
+
skipped.push({
|
|
168
|
+
field: "expansionModel",
|
|
169
|
+
configPath: [...CONFIG_PATH, "expansionModel"].join("."),
|
|
170
|
+
reason:
|
|
171
|
+
`expansionModel is a bare model without a provider; use provider/model or set plugins.entries.${PLUGIN_ID}.config.expansionProvider so doctor can update plugins.entries.${PLUGIN_ID}.subagent.allowedModels.`,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
modelRefs: uniqueModelRefs(modelRefs),
|
|
132
178
|
skipped,
|
|
133
179
|
};
|
|
134
180
|
}
|
|
@@ -137,7 +183,24 @@ function collectMissingPolicyEntries(cfg) {
|
|
|
137
183
|
const { modelRefs, skipped } = collectLosslessRuntimeLlmModelRefs(cfg);
|
|
138
184
|
const policy = readLlmPolicy(cfg);
|
|
139
185
|
const allowedStrings = new Set(policy.allowedModels.filter((entry) => typeof entry === "string"));
|
|
140
|
-
const missingRefs =
|
|
186
|
+
const missingRefs = allowedStrings.has("*")
|
|
187
|
+
? []
|
|
188
|
+
: modelRefs.filter((entry) => !allowedStrings.has(entry.modelRef));
|
|
189
|
+
return {
|
|
190
|
+
modelRefs,
|
|
191
|
+
skipped,
|
|
192
|
+
missingRefs,
|
|
193
|
+
missingAllowModelOverride: modelRefs.length > 0 && policy.allowModelOverride !== true,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function collectMissingSubagentPolicyEntries(cfg) {
|
|
198
|
+
const { modelRefs, skipped } = collectLosslessSubagentModelRefs(cfg);
|
|
199
|
+
const policy = readSubagentPolicy(cfg);
|
|
200
|
+
const allowedStrings = new Set(policy.allowedModels.filter((entry) => typeof entry === "string"));
|
|
201
|
+
const missingRefs = allowedStrings.has("*")
|
|
202
|
+
? []
|
|
203
|
+
: modelRefs.filter((entry) => !allowedStrings.has(entry.modelRef));
|
|
141
204
|
return {
|
|
142
205
|
modelRefs,
|
|
143
206
|
skipped,
|
|
@@ -155,7 +218,20 @@ function hasIssueForField(cfg, field) {
|
|
|
155
218
|
);
|
|
156
219
|
}
|
|
157
220
|
|
|
158
|
-
|
|
221
|
+
function hasSubagentIssueForField(cfg, field) {
|
|
222
|
+
const issues = collectMissingSubagentPolicyEntries(cfg);
|
|
223
|
+
return (
|
|
224
|
+
issues.missingAllowModelOverride ||
|
|
225
|
+
issues.missingRefs.some((entry) => entry.field === field) ||
|
|
226
|
+
issues.skipped.some((entry) => entry.field === field)
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function needsPolicyRepair(issues) {
|
|
231
|
+
return issues.missingAllowModelOverride || issues.missingRefs.length > 0;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** Doctor warning rules for Lossless runtime LLM and subagent model override policy. */
|
|
159
235
|
export const legacyConfigRules = [
|
|
160
236
|
{
|
|
161
237
|
path: [...CONFIG_PATH, "summaryModel"],
|
|
@@ -175,57 +251,99 @@ export const legacyConfigRules = [
|
|
|
175
251
|
'Lossless fallbackProviders use api.runtime.llm.complete model overrides. Configure plugins.entries.lossless-claw.llm.allowModelOverride and allowedModels, or run "openclaw doctor --fix".',
|
|
176
252
|
match: (_value, root) => hasIssueForField(root, "fallbackProviders"),
|
|
177
253
|
},
|
|
254
|
+
{
|
|
255
|
+
path: [...CONFIG_PATH, "expansionModel"],
|
|
256
|
+
message:
|
|
257
|
+
'Lossless expansionModel uses delegated sub-agent model overrides. Configure plugins.entries.lossless-claw.subagent.allowModelOverride and allowedModels, or run "openclaw doctor --fix".',
|
|
258
|
+
match: (_value, root) => hasSubagentIssueForField(root, "expansionModel"),
|
|
259
|
+
},
|
|
178
260
|
];
|
|
179
261
|
|
|
180
|
-
function
|
|
262
|
+
function cloneRootWithLosslessEntry(cfg) {
|
|
181
263
|
const root = isRecord(cfg) ? { ...cfg } : {};
|
|
182
264
|
const plugins = isRecord(root.plugins) ? { ...root.plugins } : {};
|
|
183
265
|
const entries = isRecord(plugins.entries) ? { ...plugins.entries } : {};
|
|
184
266
|
const entry = isRecord(entries[PLUGIN_ID]) ? { ...entries[PLUGIN_ID] } : {};
|
|
185
|
-
const llm = isRecord(entry.llm) ? { ...entry.llm } : {};
|
|
186
267
|
|
|
187
268
|
root.plugins = plugins;
|
|
188
269
|
plugins.entries = entries;
|
|
189
270
|
entries[PLUGIN_ID] = entry;
|
|
190
|
-
entry.llm = llm;
|
|
191
271
|
|
|
192
|
-
return { root,
|
|
272
|
+
return { root, entry };
|
|
193
273
|
}
|
|
194
274
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
275
|
+
function ensurePolicy(entry, policyKey) {
|
|
276
|
+
const policy = isRecord(entry[policyKey]) ? { ...entry[policyKey] } : {};
|
|
277
|
+
entry[policyKey] = policy;
|
|
278
|
+
return policy;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function applyModelOverridePolicyRepair({ policy, issues, changes, policyPath, subject }) {
|
|
198
282
|
if (issues.modelRefs.length === 0) {
|
|
199
|
-
return
|
|
283
|
+
return;
|
|
200
284
|
}
|
|
201
285
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
changes.push("Set plugins.entries.lossless-claw.llm.allowModelOverride = true for configured Lossless summary model overrides.");
|
|
286
|
+
if (policy.allowModelOverride !== true) {
|
|
287
|
+
policy.allowModelOverride = true;
|
|
288
|
+
changes.push(
|
|
289
|
+
`Set plugins.entries.lossless-claw.${policyPath}.allowModelOverride = true for configured Lossless ${subject} model overrides.`,
|
|
290
|
+
);
|
|
208
291
|
}
|
|
209
292
|
|
|
210
|
-
const currentAllowed = Array.isArray(
|
|
293
|
+
const currentAllowed = Array.isArray(policy.allowedModels) ? [...policy.allowedModels] : [];
|
|
211
294
|
const allowedStrings = new Set(currentAllowed.filter((entry) => typeof entry === "string"));
|
|
212
295
|
const added = [];
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
296
|
+
if (!allowedStrings.has("*")) {
|
|
297
|
+
for (const { modelRef } of issues.modelRefs) {
|
|
298
|
+
if (!allowedStrings.has(modelRef)) {
|
|
299
|
+
currentAllowed.push(modelRef);
|
|
300
|
+
allowedStrings.add(modelRef);
|
|
301
|
+
added.push(modelRef);
|
|
302
|
+
}
|
|
218
303
|
}
|
|
219
304
|
}
|
|
220
305
|
|
|
221
|
-
if (added.length > 0 || !Array.isArray(
|
|
222
|
-
|
|
306
|
+
if (added.length > 0 || !Array.isArray(policy.allowedModels)) {
|
|
307
|
+
policy.allowedModels = currentAllowed;
|
|
223
308
|
changes.push(
|
|
224
|
-
`Added plugins.entries.lossless-claw.
|
|
309
|
+
`Added plugins.entries.lossless-claw.${policyPath}.allowedModels entries for configured Lossless ${subject} models: ${added.join(", ")}`,
|
|
225
310
|
);
|
|
226
311
|
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/** Add the minimal plugin policies needed for configured Lossless model overrides. */
|
|
315
|
+
export function normalizeCompatibilityConfig({ cfg }) {
|
|
316
|
+
const issues = collectMissingPolicyEntries(cfg);
|
|
317
|
+
const subagentIssues = collectMissingSubagentPolicyEntries(cfg);
|
|
318
|
+
const repairRuntimeLlmPolicy = needsPolicyRepair(issues);
|
|
319
|
+
const repairSubagentPolicy = needsPolicyRepair(subagentIssues);
|
|
320
|
+
if (!repairRuntimeLlmPolicy && !repairSubagentPolicy) {
|
|
321
|
+
return { config: cfg, changes: [] };
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const { root, entry } = cloneRootWithLosslessEntry(cfg);
|
|
325
|
+
const changes = [];
|
|
326
|
+
|
|
327
|
+
if (repairRuntimeLlmPolicy) {
|
|
328
|
+
applyModelOverridePolicyRepair({
|
|
329
|
+
policy: ensurePolicy(entry, "llm"),
|
|
330
|
+
issues,
|
|
331
|
+
changes,
|
|
332
|
+
policyPath: "llm",
|
|
333
|
+
subject: "summary",
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
if (repairSubagentPolicy) {
|
|
337
|
+
applyModelOverridePolicyRepair({
|
|
338
|
+
policy: ensurePolicy(entry, "subagent"),
|
|
339
|
+
issues: subagentIssues,
|
|
340
|
+
changes,
|
|
341
|
+
policyPath: "subagent",
|
|
342
|
+
subject: "expansion",
|
|
343
|
+
});
|
|
344
|
+
}
|
|
227
345
|
|
|
228
346
|
return { config: root, changes };
|
|
229
347
|
}
|
|
230
348
|
|
|
231
|
-
export { collectLosslessRuntimeLlmModelRefs };
|
|
349
|
+
export { collectLosslessRuntimeLlmModelRefs, collectLosslessSubagentModelRefs };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@martian-engineering/lossless-claw",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.1",
|
|
4
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",
|
|
@@ -46,7 +46,12 @@
|
|
|
46
46
|
"vitest": "^3.0.0"
|
|
47
47
|
},
|
|
48
48
|
"peerDependencies": {
|
|
49
|
-
"openclaw": ">=2026.
|
|
49
|
+
"openclaw": ">=2026.5.12"
|
|
50
|
+
},
|
|
51
|
+
"peerDependenciesMeta": {
|
|
52
|
+
"openclaw": {
|
|
53
|
+
"optional": true
|
|
54
|
+
}
|
|
50
55
|
},
|
|
51
56
|
"publishConfig": {
|
|
52
57
|
"access": "public"
|
|
@@ -56,14 +61,14 @@
|
|
|
56
61
|
"./dist/index.js"
|
|
57
62
|
],
|
|
58
63
|
"compat": {
|
|
59
|
-
"pluginApi": ">=2026.
|
|
60
|
-
"minGatewayVersion": "2026.
|
|
64
|
+
"pluginApi": ">=2026.5.12",
|
|
65
|
+
"minGatewayVersion": "2026.5.12",
|
|
61
66
|
"tested": [
|
|
62
|
-
"2026.5.
|
|
67
|
+
"2026.5.12"
|
|
63
68
|
]
|
|
64
69
|
},
|
|
65
70
|
"build": {
|
|
66
|
-
"openclawVersion": "2026.
|
|
71
|
+
"openclawVersion": "2026.5.12"
|
|
67
72
|
}
|
|
68
73
|
},
|
|
69
74
|
"repository": {
|