chisle 3.6.0 → 3.6.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/.claude-plugin/plugin.json +1 -1
- package/.opencode/plugins/chisle.mjs +18 -5
- package/bin/install.js +1 -1
- package/hooks/chisle-compress-output.js +193 -31
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "chisle",
|
|
3
|
-
"version": "3.6.
|
|
3
|
+
"version": "3.6.1",
|
|
4
4
|
"description": "Maximum-efficiency dev mode. Zero-fluff prose combined with YAGNI/ladder-first code decisions. One persona: the senior dev who deletes code for fun and bills by the syllable.",
|
|
5
5
|
"hooks": {
|
|
6
6
|
"SessionStart": [
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
// Tunables (shared with the Claude/Pi/Copilot compressor, see chisle-config):
|
|
23
23
|
// CHISLE_COMPRESS=0 kill switch
|
|
24
24
|
// CHISLE_DEFAULT_MODE=off disable by default (env or ~/.config/chisle)
|
|
25
|
-
// CHISLE_COMPRESS_MAX_CHARS
|
|
25
|
+
// CHISLE_COMPRESS_MAX_CHARS elision threshold and final OpenCode bound
|
|
26
26
|
// CHISLE_COMPRESS_TOOLS=bash,… override the tool allowlist
|
|
27
27
|
|
|
28
28
|
import { createRequire } from 'module';
|
|
@@ -52,12 +52,16 @@ const config = loadFrom([
|
|
|
52
52
|
]) || { getDefaultMode: () => 'on' };
|
|
53
53
|
|
|
54
54
|
const compressForOpencode = core && core.compressForOpencode;
|
|
55
|
+
const recordSavings = core && core.recordSavings;
|
|
55
56
|
const getDefaultMode = config.getDefaultMode || (() => 'on');
|
|
57
|
+
const getOpencodeDir = config.getOpencodeDir || (() => path.dirname(__dirname));
|
|
56
58
|
|
|
57
59
|
// Already compressed: our elision marker. Skip such text so a second pass
|
|
58
60
|
// (messages.transform after tool.execute.after already ran) never nests a
|
|
59
61
|
// marker or eats an earlier salvage block. Keeps compression idempotent.
|
|
60
62
|
const CHISLE_MARK = '[chisle:';
|
|
63
|
+
const getOpencodeMaxChars = () =>
|
|
64
|
+
(core && core.limitsFor && core.limitsFor().maxChars) || 8000;
|
|
61
65
|
|
|
62
66
|
export default async () => {
|
|
63
67
|
return {
|
|
@@ -75,10 +79,19 @@ export default async () => {
|
|
|
75
79
|
// MCP tools can hand back a content[] array rather than a string;
|
|
76
80
|
// built-ins give a string. Only the string case is ours to touch.
|
|
77
81
|
if (!output || typeof output.output !== 'string') return;
|
|
78
|
-
if (output.output.includes(CHISLE_MARK)) return;
|
|
79
|
-
const
|
|
80
|
-
|
|
82
|
+
if (output.output.includes(CHISLE_MARK) && output.output.length <= getOpencodeMaxChars()) return;
|
|
83
|
+
const original = output.output;
|
|
84
|
+
const updated = compressForOpencode(input && input.tool, original, {
|
|
85
|
+
mode: getDefaultMode(),
|
|
86
|
+
sessionId: input && input.sessionID,
|
|
87
|
+
callId: input && input.callID,
|
|
88
|
+
});
|
|
89
|
+
if (updated != null && updated.length < original.length) {
|
|
81
90
|
output.output = updated;
|
|
91
|
+
if (recordSavings) {
|
|
92
|
+
recordSavings(original.length - updated.length,
|
|
93
|
+
process.env.CHISLE_STATE_DIR || getOpencodeDir());
|
|
94
|
+
}
|
|
82
95
|
}
|
|
83
96
|
} catch (e) { /* keep the original output */ }
|
|
84
97
|
},
|
|
@@ -102,7 +115,7 @@ export default async () => {
|
|
|
102
115
|
if (!part || part.type !== 'tool') continue;
|
|
103
116
|
const st = part.state;
|
|
104
117
|
if (!st || st.status !== 'completed' || typeof st.output !== 'string') continue;
|
|
105
|
-
if (st.output.includes(CHISLE_MARK)) continue;
|
|
118
|
+
if (st.output.includes(CHISLE_MARK) && st.output.length <= getOpencodeMaxChars()) continue;
|
|
106
119
|
const updated = compressForOpencode(part.tool, st.output, { mode });
|
|
107
120
|
if (updated != null && updated.length < st.output.length) {
|
|
108
121
|
st.output = updated;
|
package/bin/install.js
CHANGED
|
@@ -709,7 +709,7 @@ function uninstall(ctx) {
|
|
|
709
709
|
const dst = path.join(ocPlugins, rel);
|
|
710
710
|
if (fs.existsSync(dst)) { if (!opts.dryRun) fs.rmSync(dst, { recursive: true, force: true }); note(' removed ' + dst); touched++; }
|
|
711
711
|
}
|
|
712
|
-
for (const f of ['chisle-spill', '.chisle-compress-stats.json', '.chisle-compress-last.json']) {
|
|
712
|
+
for (const f of ['chisle-spill', 'chisle-dedup', '.chisle-compress-stats.json', '.chisle-compress-last.json']) {
|
|
713
713
|
const p = path.join(homeDir(), '.config', 'opencode', f);
|
|
714
714
|
if (fs.existsSync(p)) { if (!opts.dryRun) fs.rmSync(p, { recursive: true, force: true }); note(' removed ' + p); touched++; }
|
|
715
715
|
}
|
|
@@ -88,6 +88,46 @@ const SALVAGE_RE = /\b(error|err!|fail(ed|ure|ing)?|exception|traceback|panic|fa
|
|
|
88
88
|
const MAX_SALVAGED = 12; // error lines rescued from the elided middle
|
|
89
89
|
const MAX_SALVAGE_LINE = 300; // per-line char cap on salvaged lines
|
|
90
90
|
|
|
91
|
+
function splitsSurrogatePair(text, index) {
|
|
92
|
+
if (index <= 0 || index >= text.length) return false;
|
|
93
|
+
const before = text.charCodeAt(index - 1);
|
|
94
|
+
const after = text.charCodeAt(index);
|
|
95
|
+
return before >= 0xD800 && before <= 0xDBFF && after >= 0xDC00 && after <= 0xDFFF;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function safePrefix(text, maxChars) {
|
|
99
|
+
let end = Math.max(0, Math.min(text.length, maxChars));
|
|
100
|
+
if (splitsSurrogatePair(text, end)) end -= 1;
|
|
101
|
+
return text.slice(0, end);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function safeSuffix(text, maxChars) {
|
|
105
|
+
let start = Math.max(0, text.length - Math.max(0, maxChars));
|
|
106
|
+
if (splitsSurrogatePair(text, start)) start += 1;
|
|
107
|
+
return text.slice(start);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function safeSlice(text, start, end) {
|
|
111
|
+
let safeStart = Math.max(0, Math.min(text.length, start));
|
|
112
|
+
let safeEnd = Math.max(safeStart, Math.min(text.length, end));
|
|
113
|
+
if (splitsSurrogatePair(text, safeStart)) safeStart += 1;
|
|
114
|
+
if (splitsSurrogatePair(text, safeEnd)) safeEnd -= 1;
|
|
115
|
+
return text.slice(safeStart, Math.max(safeStart, safeEnd));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function salvageLine(line) {
|
|
119
|
+
const match = SALVAGE_RE.exec(line.replace(/error-like/gi, 'diagnostic'));
|
|
120
|
+
if (!match) return null;
|
|
121
|
+
if (line.length <= MAX_SALVAGE_LINE) return line;
|
|
122
|
+
const contentLimit = MAX_SALVAGE_LINE - 2;
|
|
123
|
+
const start = Math.max(0, Math.min(
|
|
124
|
+
match.index - Math.floor((contentLimit - match[0].length) / 2),
|
|
125
|
+
line.length - contentLimit,
|
|
126
|
+
));
|
|
127
|
+
const end = start + contentLimit;
|
|
128
|
+
return (start > 0 ? '…' : '') + safeSlice(line, start, end) + (end < line.length ? '…' : '');
|
|
129
|
+
}
|
|
130
|
+
|
|
91
131
|
function toolAllowed(name) {
|
|
92
132
|
if (!name || typeof name !== 'string') return false;
|
|
93
133
|
if (NEVER_COMPRESS.includes(name.toLowerCase())) return false;
|
|
@@ -107,28 +147,54 @@ function copilotToolAllowed(name) {
|
|
|
107
147
|
return list.includes(name);
|
|
108
148
|
}
|
|
109
149
|
|
|
110
|
-
// OpenCode runtime tool names are lowercase (bash, grep, glob, webfetch, ...)
|
|
111
|
-
//
|
|
150
|
+
// OpenCode runtime tool names are lowercase (bash, grep, glob, webfetch, ...).
|
|
151
|
+
// MCP tools use `server_tool`, not Claude's mcp__ prefix; their names do not
|
|
152
|
+
// establish read-only safety, so they require explicit allowlisting.
|
|
112
153
|
// read/edit/write/patch are excluded on purpose: their output feeds exact-match
|
|
113
154
|
// edits, exactly as SAFE_TOOLS omits Read/Edit/Write.
|
|
114
|
-
const SAFE_TOOLS_OPENCODE = ['bash', 'grep', 'glob', 'webfetch', 'websearch', '
|
|
115
|
-
const UNSAFE_TOOLS_OPENCODE = [
|
|
155
|
+
const SAFE_TOOLS_OPENCODE = ['bash', 'grep', 'glob', 'webfetch', 'websearch', 'list'];
|
|
156
|
+
const UNSAFE_TOOLS_OPENCODE = [
|
|
157
|
+
'read', 'edit', 'write', 'patch', 'apply_patch', 'multiedit',
|
|
158
|
+
'notebookedit', 'notebookread', 'notebook_write', 'notebook_edit',
|
|
159
|
+
'create', 'str_replace', 'insert', 'todowrite', 'todoread',
|
|
160
|
+
];
|
|
116
161
|
|
|
117
162
|
function opencodeToolAllowed(name) {
|
|
118
163
|
if (!name || typeof name !== 'string') return false;
|
|
119
164
|
const lower = name.toLowerCase();
|
|
120
|
-
//
|
|
121
|
-
// their output feeds exact-match edits
|
|
165
|
+
// Source-bearing and mutating tools are never compressed, even via
|
|
166
|
+
// CHISLE_COMPRESS_TOOLS: their output feeds exact-match edits, an invariant
|
|
167
|
+
// INSTALL.md promises rather than an overridable default.
|
|
122
168
|
if (UNSAFE_TOOLS_OPENCODE.includes(lower)) return false;
|
|
123
169
|
if (process.env.CHISLE_COMPRESS_TOOLS) {
|
|
124
170
|
return process.env.CHISLE_COMPRESS_TOOLS
|
|
125
171
|
.split(',').map(s => s.trim()).filter(Boolean).includes(name);
|
|
126
172
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
173
|
+
return SAFE_TOOLS_OPENCODE.includes(lower);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function boundForOpencode(text, spillPath, maxChars) {
|
|
177
|
+
const outputLimit = maxChars || THRESHOLDS.maxChars;
|
|
178
|
+
if (text.length <= outputLimit) return text;
|
|
179
|
+
if (outputLimit < 7) return safePrefix(text, outputLimit);
|
|
180
|
+
const recover = spillPath ? ' Full output: ' + spillPath + ' (grep it, do not re-run)' : '';
|
|
181
|
+
let marker = '... [chisle: rebound compressed output to ' +
|
|
182
|
+
outputLimit.toLocaleString('en-US') + ' chars.' + recover + '] ...';
|
|
183
|
+
if (marker.length > outputLimit - 2) marker = safePrefix(marker, outputLimit - 7) + '] ...';
|
|
184
|
+
const middle = [marker];
|
|
185
|
+
// Keep head/tail context without excluding diagnostics under small bounds.
|
|
186
|
+
const contextReserve = Math.min(2050, Math.floor(outputLimit / 4));
|
|
187
|
+
for (const line of text.split('\n')) {
|
|
188
|
+
if (middle.length > MAX_SALVAGED) break;
|
|
189
|
+
const candidate = salvageLine(line);
|
|
190
|
+
if (!candidate) continue;
|
|
191
|
+
if (middle.join('\n').length + candidate.length + 1 > outputLimit - contextReserve) break;
|
|
192
|
+
if (!middle.includes(candidate)) middle.push(candidate);
|
|
193
|
+
}
|
|
194
|
+
const center = middle.join('\n');
|
|
195
|
+
const remaining = Math.max(0, outputLimit - center.length - 2);
|
|
196
|
+
const headChars = Math.ceil(remaining / 2);
|
|
197
|
+
return safePrefix(text, headChars) + '\n' + center + '\n' + safeSuffix(text, Math.floor(remaining / 2));
|
|
132
198
|
}
|
|
133
199
|
|
|
134
200
|
// One OpenCode tool_result → replacement string, or null to keep the original.
|
|
@@ -149,7 +215,19 @@ function compressForOpencode(toolName, text, opts) {
|
|
|
149
215
|
if (!opencodeToolAllowed(toolName)) return null;
|
|
150
216
|
if (typeof text !== 'string' || !text) return null;
|
|
151
217
|
const stateDir = opts.stateDir || process.env.CHISLE_STATE_DIR || getOpencodeDir();
|
|
152
|
-
|
|
218
|
+
const limits = limitsFor();
|
|
219
|
+
const dup = dedupCheck(toolName, text, opts.sessionId, opts.callId, stateDir, true);
|
|
220
|
+
if (dup != null && dup.length < text.length) return boundForOpencode(dup, null, limits.maxChars);
|
|
221
|
+
if (text.length <= SCRUB_MIN && text.length <= limits.maxChars) return null;
|
|
222
|
+
const spillPath = text.length > limits.maxChars && toolName ? spill(text, toolName, stateDir) : null;
|
|
223
|
+
let updated = process.env.CHISLE_COMPRESS_SCRUB === '0' ? text : scrub(text);
|
|
224
|
+
if (updated.length > limits.maxChars && updated.split('\n').length > limits.headLines + limits.tailLines) {
|
|
225
|
+
const elided = compress(updated, limits, spillPath);
|
|
226
|
+
if (elided != null) updated = elided;
|
|
227
|
+
}
|
|
228
|
+
updated = boundForOpencode(updated, spillPath, limits.maxChars);
|
|
229
|
+
if (text.length > limits.maxChars) return updated;
|
|
230
|
+
return text.length - updated.length >= MIN_WIN ? updated : null;
|
|
153
231
|
}
|
|
154
232
|
|
|
155
233
|
// Copilot CLI's postToolUse payload is flat and camelCase:
|
|
@@ -240,26 +318,103 @@ function duplicateMarker(toolName, text, reference = 'previous') {
|
|
|
240
318
|
const lines = text.split('\n');
|
|
241
319
|
const prior = reference === 'previous' ? 'the previous' : 'an earlier';
|
|
242
320
|
const preview = lines.slice(0, 5)
|
|
243
|
-
.map(l => l.length > MAX_SALVAGE_LINE ? l
|
|
321
|
+
.map(l => l.length > MAX_SALVAGE_LINE ? safePrefix(l, MAX_SALVAGE_LINE - 1) + '…' : l);
|
|
244
322
|
return '[chisle: output byte-identical to ' + prior + ' ' + toolName + ' result — ' +
|
|
245
323
|
text.length.toLocaleString('en-US') + ' chars / ' + lines.length +
|
|
246
324
|
' lines, unchanged. First lines:]\n' + preview.join('\n');
|
|
247
325
|
}
|
|
248
326
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
function
|
|
327
|
+
const DEDUP_SESSION_KEEP = 20;
|
|
328
|
+
const DEDUP_DIR = 'chisle-dedup';
|
|
329
|
+
|
|
330
|
+
function readDedupFile(p) {
|
|
331
|
+
let st;
|
|
332
|
+
try { st = fs.lstatSync(p); }
|
|
333
|
+
catch (e) { return { state: null, unsafe: e.code !== 'ENOENT' }; }
|
|
334
|
+
if (st.isSymbolicLink() || !st.isFile()) return { state: null, unsafe: true };
|
|
335
|
+
const noFollow = typeof fs.constants.O_NOFOLLOW === 'number' ? fs.constants.O_NOFOLLOW : 0;
|
|
336
|
+
let fd;
|
|
337
|
+
let raw;
|
|
338
|
+
try {
|
|
339
|
+
fd = fs.openSync(p, fs.constants.O_RDONLY | noFollow);
|
|
340
|
+
raw = fs.readFileSync(fd, 'utf8');
|
|
341
|
+
} catch (e) {
|
|
342
|
+
return { state: null, unsafe: true };
|
|
343
|
+
} finally {
|
|
344
|
+
if (fd !== undefined) { try { fs.closeSync(fd); } catch (e) {} }
|
|
345
|
+
}
|
|
346
|
+
try { return { state: JSON.parse(raw) || null, unsafe: false }; }
|
|
347
|
+
catch (e) { return { state: null, unsafe: false }; }
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function dedupStateDir(stateDir) {
|
|
351
|
+
const dir = path.join(stateDir || getClaudeDir(), DEDUP_DIR);
|
|
352
|
+
try { fs.mkdirSync(dir, { recursive: true, mode: 0o700 }); } catch (e) { return null; }
|
|
353
|
+
try {
|
|
354
|
+
const st = fs.lstatSync(dir);
|
|
355
|
+
return st.isDirectory() && !st.isSymbolicLink() ? dir : null;
|
|
356
|
+
} catch (e) { return null; }
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function writeDedupState(p, state) {
|
|
360
|
+
const tmp = p + '.' + process.pid + '.' + crypto.randomBytes(6).toString('hex') + '.tmp';
|
|
361
|
+
const noFollow = typeof fs.constants.O_NOFOLLOW === 'number' ? fs.constants.O_NOFOLLOW : 0;
|
|
362
|
+
const flags = fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL | noFollow;
|
|
363
|
+
let fd;
|
|
364
|
+
try {
|
|
365
|
+
fd = fs.openSync(tmp, flags, 0o600);
|
|
366
|
+
fs.writeFileSync(fd, JSON.stringify(state));
|
|
367
|
+
try { fs.fchmodSync(fd, 0o600); } catch (e) {}
|
|
368
|
+
fs.closeSync(fd);
|
|
369
|
+
fd = undefined;
|
|
370
|
+
fs.renameSync(tmp, p);
|
|
371
|
+
} catch (e) {
|
|
372
|
+
if (fd !== undefined) { try { fs.closeSync(fd); } catch (closeError) {} }
|
|
373
|
+
try { fs.unlinkSync(tmp); } catch (unlinkError) {}
|
|
374
|
+
throw e;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function pruneDedupSessions(dir) {
|
|
379
|
+
try {
|
|
380
|
+
const files = fs.readdirSync(dir)
|
|
381
|
+
.filter(name => /^[a-f0-9]{64}\.json$/.test(name))
|
|
382
|
+
.map(name => {
|
|
383
|
+
const p = path.join(dir, name);
|
|
384
|
+
const st = fs.lstatSync(p);
|
|
385
|
+
return st.isFile() || st.isSymbolicLink() ? { p, t: st.mtimeMs } : null;
|
|
386
|
+
})
|
|
387
|
+
.filter(Boolean)
|
|
388
|
+
.sort((a, b) => b.t - a.t || a.p.localeCompare(b.p));
|
|
389
|
+
for (const file of files.slice(DEDUP_SESSION_KEEP)) {
|
|
390
|
+
try { fs.unlinkSync(file.p); } catch (e) {}
|
|
391
|
+
}
|
|
392
|
+
} catch (e) {}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// `stateDir` defaults to getClaudeDir() so existing Claude/Pi callers keep
|
|
396
|
+
// their prior storage path. Copilot passes getCopilotDir(); OpenCode passes
|
|
397
|
+
// getOpencodeDir() and opts into per-session files for interleaved sessions.
|
|
398
|
+
function dedupCheck(toolName, text, sessionId, toolUseId, stateDir, perSession) {
|
|
253
399
|
if (process.env.CHISLE_COMPRESS_DEDUP === '0') return null;
|
|
254
400
|
if (!sessionId || typeof sessionId !== 'string') return null;
|
|
255
401
|
if (text.length < DEDUP_MIN) return null;
|
|
256
402
|
try {
|
|
257
|
-
const dir = stateDir || getClaudeDir();
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
403
|
+
const dir = perSession ? dedupStateDir(stateDir) : stateDir || getClaudeDir();
|
|
404
|
+
if (!dir) return null;
|
|
405
|
+
const p = path.join(dir, perSession
|
|
406
|
+
? crypto.createHash('sha256').update(sessionId).digest('hex') + '.json'
|
|
407
|
+
: '.chisle-compress-last.json');
|
|
408
|
+
let state;
|
|
409
|
+
if (perSession) {
|
|
410
|
+
const loaded = readDedupFile(p);
|
|
411
|
+
if (loaded.unsafe) return null;
|
|
412
|
+
state = loaded.state;
|
|
413
|
+
} else {
|
|
414
|
+
try { if (fs.lstatSync(p).isSymbolicLink()) return null; } catch (e) { if (e.code !== 'ENOENT') return null; }
|
|
415
|
+
try { state = JSON.parse(fs.readFileSync(p, 'utf8')) || {}; } catch (e) {}
|
|
416
|
+
}
|
|
417
|
+
if (!state || state.session !== sessionId) state = { session: sessionId, tools: {} };
|
|
263
418
|
if (!state.tools || typeof state.tools !== 'object') state.tools = {};
|
|
264
419
|
const hash = crypto.createHash('sha256').update(text).digest('hex');
|
|
265
420
|
// A second hook invocation for the SAME tool call is not the model seeing
|
|
@@ -274,10 +429,15 @@ function dedupCheck(toolName, text, sessionId, toolUseId, stateDir) {
|
|
|
274
429
|
const sameCall = !!(toolUseId && rec.id && rec.id === toolUseId);
|
|
275
430
|
const dup = rec.hash === hash && !sameCall;
|
|
276
431
|
state.tools[toolName] = { hash, id: toolUseId || rec.id || null };
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
432
|
+
if (perSession) {
|
|
433
|
+
writeDedupState(p, state);
|
|
434
|
+
pruneDedupSessions(dir);
|
|
435
|
+
} else {
|
|
436
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
437
|
+
const tmp = p + '.' + process.pid + '.tmp';
|
|
438
|
+
fs.writeFileSync(tmp, JSON.stringify(state), { mode: 0o600 });
|
|
439
|
+
fs.renameSync(tmp, p);
|
|
440
|
+
}
|
|
281
441
|
return dup ? duplicateMarker(toolName, text) : null;
|
|
282
442
|
} catch (e) { return null; }
|
|
283
443
|
}
|
|
@@ -336,9 +496,9 @@ function compress(text, limits, spillPath) {
|
|
|
336
496
|
const keep = Math.floor(maxChars / 2);
|
|
337
497
|
const elided = text.length - 2 * keep;
|
|
338
498
|
if (elided <= 0) return null;
|
|
339
|
-
return text
|
|
499
|
+
return safePrefix(text, keep) +
|
|
340
500
|
'\n... [chisle: elided ' + elided.toLocaleString('en-US') + ' chars from the middle.' + recover + '] ...\n' +
|
|
341
|
-
text
|
|
501
|
+
safeSuffix(text, keep);
|
|
342
502
|
}
|
|
343
503
|
|
|
344
504
|
const head = lines.slice(0, headLines);
|
|
@@ -348,7 +508,8 @@ function compress(text, limits, spillPath) {
|
|
|
348
508
|
const salvaged = [];
|
|
349
509
|
for (const line of middle) {
|
|
350
510
|
if (salvaged.length >= MAX_SALVAGED) break;
|
|
351
|
-
|
|
511
|
+
const candidate = salvageLine(line);
|
|
512
|
+
if (candidate) salvaged.push(candidate);
|
|
352
513
|
}
|
|
353
514
|
|
|
354
515
|
const marker = '... [chisle: elided ' + middle.length.toLocaleString('en-US') +
|
|
@@ -513,5 +674,6 @@ module.exports = {
|
|
|
513
674
|
isCopilotPayload, copilotToolAllowed, SAFE_TOOLS_COPILOT, NEVER_COMPRESS_COPILOT,
|
|
514
675
|
// OpenCode-specific additions (plugin uses tool.execute.after +
|
|
515
676
|
// experimental.chat.messages.transform):
|
|
516
|
-
opencodeToolAllowed, compressForOpencode, SAFE_TOOLS_OPENCODE,
|
|
677
|
+
opencodeToolAllowed, compressForOpencode, SAFE_TOOLS_OPENCODE, UNSAFE_TOOLS_OPENCODE,
|
|
678
|
+
boundForOpencode, recordSavings,
|
|
517
679
|
};
|