mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-06-13 03:33:15 +08:00
fix: context-size /compact trigger, Codex marketplace plugin path, live README badges (#2237)
- suggest-compact hook now reads the latest usage record from the session transcript and suggests /compact at a window-scaled token threshold (160k/200k window, 250k/1M window; COMPACT_CONTEXT_THRESHOLD and COMPACT_CONTEXT_INTERVAL overridable), re-firing per 60k-token growth bucket; tool-call count stays as the secondary signal (#2155) - Codex repo marketplace now points at ./plugins/ecc instead of ./ — Codex never discovers plugins whose local marketplace source.path is the marketplace root (verified on Codex CLI 0.137.0); plugins/ecc is a thin folder referencing root skills/.mcp.json per maintainer direction on #2097; docs flag plugin mode as experimental with the upstream blocker openai/codex#26037 linked (#2128) - README badges for installs/stars/forks now use shields endpoint badges backed by api.ecc.tools (live install count 3,712 vs the stale static 150), which also eliminates shields' 'Unable to select next GitHub token from pool' render in the stars badge Closes #2155 Closes #2128
This commit is contained in:
@@ -33,10 +33,18 @@ function test(name, fn) {
|
||||
* Returns { code, stdout, stderr }.
|
||||
*/
|
||||
function runCompact(envOverrides = {}) {
|
||||
return runCompactWithInput('{}', envOverrides);
|
||||
}
|
||||
|
||||
/**
|
||||
* Run suggest-compact.js with a custom stdin payload (hook input JSON).
|
||||
* Returns { code, stdout, stderr }.
|
||||
*/
|
||||
function runCompactWithInput(input, envOverrides = {}) {
|
||||
const env = { ...process.env, ...envOverrides };
|
||||
const result = spawnSync('node', [compactScript], {
|
||||
encoding: 'utf8',
|
||||
input: '{}',
|
||||
input: typeof input === 'string' ? input : JSON.stringify(input),
|
||||
timeout: 10000,
|
||||
env,
|
||||
});
|
||||
@@ -637,6 +645,252 @@ function runTests() {
|
||||
})) passed++;
|
||||
else failed++;
|
||||
|
||||
// ── Context-size trigger (#2155) ──
|
||||
// Tool count is a weak proxy for window pressure. The hook now also reads
|
||||
// the latest `usage` record from the session transcript (transcript_path in
|
||||
// the hook stdin payload) and suggests /compact at a window-scaled token
|
||||
// threshold, re-firing only after another interval of context growth.
|
||||
console.log('\nContext-size trigger (#2155):');
|
||||
|
||||
function getBucketFilePath(sessionId) {
|
||||
return path.join(os.tmpdir(), `claude-context-bucket-${sessionId}`);
|
||||
}
|
||||
|
||||
let transcriptSeq = 0;
|
||||
|
||||
function writeTranscriptFixture(tokens, model = 'claude-sonnet-4-6') {
|
||||
transcriptSeq += 1;
|
||||
const filePath = path.join(os.tmpdir(), `compact-transcript-${process.pid}-${transcriptSeq}.jsonl`);
|
||||
writeTranscriptTokens(filePath, tokens, model);
|
||||
return filePath;
|
||||
}
|
||||
|
||||
function writeTranscriptTokens(filePath, tokens, model = 'claude-sonnet-4-6') {
|
||||
const record = JSON.stringify({
|
||||
type: 'assistant',
|
||||
message: {
|
||||
model,
|
||||
usage: {
|
||||
input_tokens: tokens,
|
||||
cache_read_input_tokens: 0,
|
||||
cache_creation_input_tokens: 0,
|
||||
output_tokens: 50
|
||||
}
|
||||
}
|
||||
});
|
||||
fs.writeFileSync(filePath, record + '\n');
|
||||
}
|
||||
|
||||
function createContextContext() {
|
||||
const base = createCounterContext('test-context');
|
||||
const bucketFile = getBucketFilePath(base.sessionId);
|
||||
return {
|
||||
...base,
|
||||
bucketFile,
|
||||
cleanup() {
|
||||
base.cleanup();
|
||||
try { fs.unlinkSync(bucketFile); } catch (_err) { /* ignore */ }
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (test('suggests compact when context exceeds the 200k-window threshold', () => {
|
||||
const ctx = createContextContext();
|
||||
const transcript = writeTranscriptFixture(170000);
|
||||
try {
|
||||
const result = runCompactWithInput({ session_id: ctx.sessionId, transcript_path: transcript });
|
||||
assert.strictEqual(result.code, 0, 'Should exit 0');
|
||||
assert.ok(result.stdout.trim().length > 0, `Expected stdout payload. Got: "${result.stdout}"`);
|
||||
const parsed = JSON.parse(result.stdout);
|
||||
const context = parsed.hookSpecificOutput.additionalContext;
|
||||
assert.ok(context.includes('Context ~170k tokens'), `Expected token estimate. Got: ${context}`);
|
||||
assert.ok(context.includes('85% of 200k window'), `Expected window percentage. Got: ${context}`);
|
||||
} finally {
|
||||
try { fs.unlinkSync(transcript); } catch (_err) { /* ignore */ }
|
||||
ctx.cleanup();
|
||||
}
|
||||
})) passed++;
|
||||
else failed++;
|
||||
|
||||
if (test('stays silent below the context threshold', () => {
|
||||
const ctx = createContextContext();
|
||||
const transcript = writeTranscriptFixture(100000);
|
||||
try {
|
||||
const result = runCompactWithInput({ session_id: ctx.sessionId, transcript_path: transcript });
|
||||
assert.strictEqual(result.code, 0);
|
||||
assert.strictEqual(result.stdout.trim(), '', `Expected silent run below threshold. Got: "${result.stdout}"`);
|
||||
} finally {
|
||||
try { fs.unlinkSync(transcript); } catch (_err) { /* ignore */ }
|
||||
ctx.cleanup();
|
||||
}
|
||||
})) passed++;
|
||||
else failed++;
|
||||
|
||||
if (test('honours COMPACT_CONTEXT_THRESHOLD override', () => {
|
||||
const ctx = createContextContext();
|
||||
const transcript = writeTranscriptFixture(1500);
|
||||
try {
|
||||
const result = runCompactWithInput(
|
||||
{ session_id: ctx.sessionId, transcript_path: transcript },
|
||||
{ COMPACT_CONTEXT_THRESHOLD: '1000' }
|
||||
);
|
||||
assert.ok(result.stdout.includes('Context ~2k tokens'), `Expected context suggestion with overridden threshold. Got: "${result.stdout}"`);
|
||||
} finally {
|
||||
try { fs.unlinkSync(transcript); } catch (_err) { /* ignore */ }
|
||||
ctx.cleanup();
|
||||
}
|
||||
})) passed++;
|
||||
else failed++;
|
||||
|
||||
if (test('does not re-fire within the same context bucket', () => {
|
||||
const ctx = createContextContext();
|
||||
const transcript = writeTranscriptFixture(170000);
|
||||
try {
|
||||
const first = runCompactWithInput({ session_id: ctx.sessionId, transcript_path: transcript });
|
||||
assert.ok(first.stdout.includes('Context ~170k tokens'), 'First run should fire');
|
||||
const second = runCompactWithInput({ session_id: ctx.sessionId, transcript_path: transcript });
|
||||
assert.strictEqual(second.stdout.trim(), '', `Second run in the same bucket must be silent. Got: "${second.stdout}"`);
|
||||
} finally {
|
||||
try { fs.unlinkSync(transcript); } catch (_err) { /* ignore */ }
|
||||
ctx.cleanup();
|
||||
}
|
||||
})) passed++;
|
||||
else failed++;
|
||||
|
||||
if (test('re-fires after the context grows by another interval', () => {
|
||||
const ctx = createContextContext();
|
||||
const transcript = writeTranscriptFixture(170000);
|
||||
try {
|
||||
runCompactWithInput({ session_id: ctx.sessionId, transcript_path: transcript });
|
||||
// Default interval is 60k: 160k threshold + 60k => next bucket at 220k.
|
||||
writeTranscriptTokens(transcript, 230000, 'claude-sonnet-4-6[1m]');
|
||||
const result = runCompactWithInput(
|
||||
{ session_id: ctx.sessionId, transcript_path: transcript },
|
||||
// Pin the threshold so window detection (230k > 200k => 1M window,
|
||||
// 250k default threshold) does not silence the growth re-fire.
|
||||
{ COMPACT_CONTEXT_THRESHOLD: '160000' }
|
||||
);
|
||||
assert.ok(result.stdout.includes('Context ~230k tokens'), `Expected re-fire after interval growth. Got: "${result.stdout}"`);
|
||||
} finally {
|
||||
try { fs.unlinkSync(transcript); } catch (_err) { /* ignore */ }
|
||||
ctx.cleanup();
|
||||
}
|
||||
})) passed++;
|
||||
else failed++;
|
||||
|
||||
if (test('uses the 250k default threshold for [1m] models', () => {
|
||||
const ctx = createContextContext();
|
||||
const transcript = writeTranscriptFixture(230000, 'claude-opus-4-5[1m]');
|
||||
try {
|
||||
const silent = runCompactWithInput({ session_id: ctx.sessionId, transcript_path: transcript });
|
||||
assert.strictEqual(silent.stdout.trim(), '', `230k on a 1M window must stay silent. Got: "${silent.stdout}"`);
|
||||
writeTranscriptTokens(transcript, 260000, 'claude-opus-4-5[1m]');
|
||||
const fired = runCompactWithInput({ session_id: ctx.sessionId, transcript_path: transcript });
|
||||
assert.ok(fired.stdout.includes('26% of 1M window'), `260k on a 1M window should fire. Got: "${fired.stdout}"`);
|
||||
} finally {
|
||||
try { fs.unlinkSync(transcript); } catch (_err) { /* ignore */ }
|
||||
ctx.cleanup();
|
||||
}
|
||||
})) passed++;
|
||||
else failed++;
|
||||
|
||||
if (test('treats >200k observed tokens as a 1M window even without the [1m] marker', () => {
|
||||
const ctx = createContextContext();
|
||||
const transcript = writeTranscriptFixture(230000, 'claude-opus-4-5');
|
||||
try {
|
||||
const result = runCompactWithInput({ session_id: ctx.sessionId, transcript_path: transcript });
|
||||
// 230k would exceed the 160k standard threshold, but the observed size
|
||||
// implies a 1M window whose 250k default threshold is not reached yet.
|
||||
assert.strictEqual(result.stdout.trim(), '', `Expected 1M-window inference to keep run silent. Got: "${result.stdout}"`);
|
||||
} finally {
|
||||
try { fs.unlinkSync(transcript); } catch (_err) { /* ignore */ }
|
||||
ctx.cleanup();
|
||||
}
|
||||
})) passed++;
|
||||
else failed++;
|
||||
|
||||
if (test('COMPACT_CONTEXT_THRESHOLD=0 disables the context signal', () => {
|
||||
const ctx = createContextContext();
|
||||
const transcript = writeTranscriptFixture(170000);
|
||||
try {
|
||||
const result = runCompactWithInput(
|
||||
{ session_id: ctx.sessionId, transcript_path: transcript },
|
||||
{ COMPACT_CONTEXT_THRESHOLD: '0' }
|
||||
);
|
||||
assert.strictEqual(result.stdout.trim(), '', `Disabled signal must stay silent. Got: "${result.stdout}"`);
|
||||
} finally {
|
||||
try { fs.unlinkSync(transcript); } catch (_err) { /* ignore */ }
|
||||
ctx.cleanup();
|
||||
}
|
||||
})) passed++;
|
||||
else failed++;
|
||||
|
||||
if (test('survives a malformed transcript (exit 0, silent)', () => {
|
||||
const ctx = createContextContext();
|
||||
const transcript = path.join(os.tmpdir(), `compact-transcript-broken-${Date.now()}.jsonl`);
|
||||
fs.writeFileSync(transcript, 'this is not json\n{broken');
|
||||
try {
|
||||
const result = runCompactWithInput({ session_id: ctx.sessionId, transcript_path: transcript });
|
||||
assert.strictEqual(result.code, 0, 'Must exit 0 on malformed transcript');
|
||||
assert.strictEqual(result.stdout.trim(), '');
|
||||
} finally {
|
||||
try { fs.unlinkSync(transcript); } catch (_err) { /* ignore */ }
|
||||
ctx.cleanup();
|
||||
}
|
||||
})) passed++;
|
||||
else failed++;
|
||||
|
||||
if (test('survives a missing transcript path (exit 0, count signal intact)', () => {
|
||||
const ctx = createContextContext();
|
||||
try {
|
||||
fs.writeFileSync(ctx.counterFile, '49');
|
||||
const result = runCompactWithInput({
|
||||
session_id: ctx.sessionId,
|
||||
transcript_path: path.join(os.tmpdir(), `missing-${Date.now()}.jsonl`)
|
||||
});
|
||||
assert.strictEqual(result.code, 0);
|
||||
assert.ok(result.stdout.includes('50 tool calls reached'), `Count signal must still work. Got: "${result.stdout}"`);
|
||||
} finally {
|
||||
ctx.cleanup();
|
||||
}
|
||||
})) passed++;
|
||||
else failed++;
|
||||
|
||||
if (test('emits a single stdout JSON payload when both signals fire', () => {
|
||||
const ctx = createContextContext();
|
||||
const transcript = writeTranscriptFixture(170000);
|
||||
try {
|
||||
fs.writeFileSync(ctx.counterFile, '49');
|
||||
const result = runCompactWithInput({ session_id: ctx.sessionId, transcript_path: transcript });
|
||||
const lines = result.stdout.trim().split('\n');
|
||||
assert.strictEqual(lines.length, 1, `Hook must emit exactly one stdout JSON line. Got: "${result.stdout}"`);
|
||||
const parsed = JSON.parse(lines[0]);
|
||||
const context = parsed.hookSpecificOutput.additionalContext;
|
||||
assert.ok(context.includes('Context ~170k tokens'), `Expected context signal. Got: ${context}`);
|
||||
assert.ok(context.includes('50 tool calls reached'), `Expected count signal. Got: ${context}`);
|
||||
} finally {
|
||||
try { fs.unlinkSync(transcript); } catch (_err) { /* ignore */ }
|
||||
ctx.cleanup();
|
||||
}
|
||||
})) passed++;
|
||||
else failed++;
|
||||
|
||||
if (test('sweeps stale context bucket state files', () => {
|
||||
const ctx = createContextContext();
|
||||
const stale = getBucketFilePath(`stale-bucket-${Date.now()}`);
|
||||
fs.writeFileSync(stale, '2');
|
||||
setMtimeDaysAgo(stale, 30);
|
||||
try {
|
||||
const result = runCompact({ CLAUDE_SESSION_ID: ctx.sessionId });
|
||||
assert.strictEqual(result.code, 0);
|
||||
assert.ok(!fs.existsSync(stale), `Stale bucket state file should have been swept. Path: ${stale}`);
|
||||
} finally {
|
||||
try { fs.unlinkSync(stale); } catch (_err) { /* ignore */ }
|
||||
ctx.cleanup();
|
||||
}
|
||||
})) passed++;
|
||||
else failed++;
|
||||
|
||||
// Summary
|
||||
console.log(`
|
||||
Results: Passed: ${passed}, Failed: ${failed}`);
|
||||
|
||||
Reference in New Issue
Block a user