specmem-hardwicksoftware 3.7.35 → 3.7.36
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/CHANGELOG.md +34 -0
- package/README.md +11 -15
- package/bin/specmem-console.cjs +839 -51
- package/claude-hooks/agent-chooser-hook.js +6 -6
- package/claude-hooks/agent-loading-hook.cjs +16 -16
- package/claude-hooks/agent-loading-hook.js +18 -18
- package/claude-hooks/agent-type-matcher.js +1 -1
- package/claude-hooks/background-completion-silencer.js +1 -1
- package/claude-hooks/file-claim-enforcer.cjs +37 -36
- package/claude-hooks/output-cleaner.cjs +1 -1
- package/claude-hooks/settings.json +27 -3
- package/claude-hooks/specmem-search-enforcer.cjs +2 -11
- package/claude-hooks/specmem-team-member-inject.js +1 -1
- package/claude-hooks/specmem-unified-hook.py +1 -1
- package/claude-hooks/subagent-loading-hook.cjs +1 -1
- package/claude-hooks/task-progress-hook.cjs +7 -7
- package/claude-hooks/task-progress-hook.js +3 -3
- package/claude-hooks/team-comms-enforcer.cjs +49 -47
- package/dist/claude-sessions/sessionParser.js +5 -0
- package/dist/codebase/codebaseIndexer.js +48 -17
- package/dist/codebase/exclusions.js +3 -4
- package/dist/codebase/index.js +4 -0
- package/dist/codebase/pdfExtractor.js +298 -0
- package/dist/dashboard/api/taskTeamMembers.js +2 -2
- package/dist/db/bigBrainMigrations.js +29 -0
- package/dist/hooks/hookManager.js +4 -4
- package/dist/hooks/teamFramingCli.js +1 -1
- package/dist/hooks/teamMemberPrepromptHook.js +5 -5
- package/dist/init/claudeConfigInjector.js +2 -2
- package/dist/mcp/compactionProxy.js +834 -186
- package/dist/mcp/compactionProxyDaemon.js +112 -37
- package/dist/mcp/contextVault.js +439 -0
- package/dist/mcp/embeddingServerManager.js +61 -1
- package/dist/mcp/mcpProtocolHandler.js +6 -1
- package/dist/mcp/miniCOTServerManager.js +82 -8
- package/dist/mcp/specMemServer.js +45 -10
- package/dist/mcp/toolRegistry.js +6 -0
- package/dist/startup/startupIndexing.js +14 -0
- package/dist/team-members/taskOrchestrator.js +3 -3
- package/dist/team-members/taskTeamMemberLogger.js +2 -2
- package/dist/tools/goofy/deployTeamMember.js +3 -3
- package/dist/tools/goofy/digInTheVault.js +81 -0
- package/dist/tools/goofy/stashTheGoods.js +56 -0
- package/dist/tools/teamMemberDeployer.js +2 -2
- package/dist/watcher/changeHandler.js +65 -8
- package/dist/watcher/changeQueue.js +20 -1
- package/embedding-sandbox/mini-cot-service.py +11 -13
- package/embedding-sandbox/pdf-text-extract.py +208 -0
- package/package.json +1 -1
- package/scripts/deploy-hooks.cjs +2 -2
- package/scripts/global-postinstall.cjs +2 -2
- package/scripts/specmem-init.cjs +130 -36
- package/specmem/model-config.json +6 -6
- package/specmem/supervisord.conf +1 -1
- package/svg-sections/readme-token-compaction.svg +246 -0
|
@@ -34,6 +34,8 @@ const CLAUDE_DIR = join(HOME, '.claude');
|
|
|
34
34
|
const PORT_FILE = join(CLAUDE_DIR, '.compaction-proxy-port');
|
|
35
35
|
const PID_FILE = join(CLAUDE_DIR, '.compaction-proxy.pid');
|
|
36
36
|
const DISABLED_FILE = join(CLAUDE_DIR, '.compaction-proxy-disabled');
|
|
37
|
+
const CUSTOM_SYS_PROMPT_FILE = join(CLAUDE_DIR, '.custom-sys-prompt.json');
|
|
38
|
+
const OG_SYS_PROMPT_FILE = join(CLAUDE_DIR, '.og-sys-prompt.json');
|
|
37
39
|
|
|
38
40
|
// Per-user port: env var > default 4080. Multiple users on same machine
|
|
39
41
|
// should set COMPACTION_PROXY_PORT or rely on the port file mechanism.
|
|
@@ -42,6 +44,9 @@ const UPSTREAM_HOST = process.env.COMPACTION_PROXY_UPSTREAM || 'api.anthropic.co
|
|
|
42
44
|
const UPSTREAM_PORT = 443;
|
|
43
45
|
const LOG_FILE = join('/tmp', `compaction-proxy-${process.getuid?.() ?? 'default'}.log`);
|
|
44
46
|
|
|
47
|
+
// Orphan detection — track last request time for daemon watchdog
|
|
48
|
+
let _lastRequestTime = Date.now();
|
|
49
|
+
|
|
45
50
|
// Compaction detection — multiple markers to catch current + future Claude Code versions
|
|
46
51
|
const COMPACTION_SYSTEM_PROMPTS = [
|
|
47
52
|
'You are a helpful AI assistant tasked with summarizing conversations.',
|
|
@@ -110,8 +115,8 @@ function _invalidateProjectCaches() {
|
|
|
110
115
|
_synonyms = null;
|
|
111
116
|
}
|
|
112
117
|
|
|
113
|
-
// Preview ring buffer — stores last
|
|
114
|
-
const PREVIEW_BUFFER_SIZE =
|
|
118
|
+
// Preview ring buffer — stores last 20 compressed requests for TUI display
|
|
119
|
+
const PREVIEW_BUFFER_SIZE = 20;
|
|
115
120
|
const recentRequests = []; // { original, optimized, timestamp, type, savings }
|
|
116
121
|
|
|
117
122
|
function _cleanPreview(raw) {
|
|
@@ -167,14 +172,42 @@ function _cleanPreview(raw) {
|
|
|
167
172
|
}
|
|
168
173
|
}
|
|
169
174
|
|
|
175
|
+
function _extractPreviewContent(bodyStr) {
|
|
176
|
+
// Extract only the interesting parts: last user msg, recent tool results, assistant msgs
|
|
177
|
+
// Skip the giant repeated system prompt
|
|
178
|
+
try {
|
|
179
|
+
const parsed = JSON.parse(bodyStr);
|
|
180
|
+
if (!parsed.messages || !Array.isArray(parsed.messages)) return bodyStr.slice(0, 3000);
|
|
181
|
+
const msgs = parsed.messages;
|
|
182
|
+
// Take last 6 messages max — skip system prompt (role=system or first huge block)
|
|
183
|
+
const recent = msgs.slice(-6);
|
|
184
|
+
const parts = [];
|
|
185
|
+
for (const m of recent) {
|
|
186
|
+
const role = m.role || '?';
|
|
187
|
+
let content = '';
|
|
188
|
+
if (typeof m.content === 'string') {
|
|
189
|
+
content = m.content.slice(0, 800);
|
|
190
|
+
} else if (Array.isArray(m.content)) {
|
|
191
|
+
content = m.content.map(b => {
|
|
192
|
+
if (b.type === 'text') return (b.text || '').slice(0, 400);
|
|
193
|
+
if (b.type === 'tool_use') return `[tool_use: ${b.name}(${JSON.stringify(b.input || {}).slice(0, 200)})]`;
|
|
194
|
+
if (b.type === 'tool_result') return `[tool_result: ${(typeof b.content === 'string' ? b.content : JSON.stringify(b.content || '')).slice(0, 400)}]`;
|
|
195
|
+
return `[${b.type}]`;
|
|
196
|
+
}).join('\n');
|
|
197
|
+
}
|
|
198
|
+
parts.push(`[${role}] ${content}`);
|
|
199
|
+
}
|
|
200
|
+
return parts.join('\n---\n');
|
|
201
|
+
} catch { return bodyStr.slice(0, 3000); }
|
|
202
|
+
}
|
|
203
|
+
|
|
170
204
|
function pushPreview(original, optimized, type, samples) {
|
|
171
205
|
const entry = {
|
|
172
|
-
original:
|
|
173
|
-
optimized:
|
|
206
|
+
original: _extractPreviewContent(original || ''),
|
|
207
|
+
optimized: _extractPreviewContent(optimized || ''),
|
|
174
208
|
timestamp: new Date().toISOString(),
|
|
175
209
|
type,
|
|
176
210
|
savings: original && optimized ? Math.max(0, original.length - optimized.length) : 0,
|
|
177
|
-
// Translation samples: before→after pairs for console display
|
|
178
211
|
samples: Array.isArray(samples) ? samples.slice(0, 8) : [],
|
|
179
212
|
};
|
|
180
213
|
recentRequests.push(entry);
|
|
@@ -198,13 +231,87 @@ let liveConfig = {
|
|
|
198
231
|
OLD_STRIP_THRESHOLD,
|
|
199
232
|
MIN_TRANSLATE_LENGTH,
|
|
200
233
|
TOOL_RESULT_PREVIEW_CHARS,
|
|
201
|
-
STENO_ENABLED: true, // toggle steno compression
|
|
202
|
-
|
|
234
|
+
STENO_ENABLED: true, // toggle steno compression (abbreviations like function→fn)
|
|
235
|
+
SLANG_ENABLED: false, // YCC (Yung Cracka Compress) — DMV x Florida slang layer, opt-in
|
|
236
|
+
TRANSLATE_ENABLED: true, // RE-ENABLED v3.7.36 — passthrough vocab + word TM + Jaccard fix the 97% rejection
|
|
203
237
|
OLD_STRIP_ENABLED: true, // toggle old tool_result stripping
|
|
204
|
-
SYSTEM_PROMPT_COMPRESS: true,
|
|
238
|
+
SYSTEM_PROMPT_COMPRESS: true, // enabled by default — custom sys prompt injection
|
|
205
239
|
SYSTEM_REMINDER_STRIPPING: true, // toggle <system-reminder> stripping (keeps first, strips rest)
|
|
206
240
|
};
|
|
207
241
|
|
|
242
|
+
// System prompt dedup — first instance goes through, subsequent same-hash stripped
|
|
243
|
+
let _seenSysPromptHash = null; // hash of last-sent system prompt
|
|
244
|
+
let _sysPromptDedup = true; // toggle via /config
|
|
245
|
+
let _sysPromptSendCount = 0; // how many times sys prompt has been sent
|
|
246
|
+
let _compactionsSinceLastInject = 0; // track compactions for re-injection
|
|
247
|
+
const SYS_PROMPT_REINJECT_INTERVAL = 3; // re-inject custom sys prompt every N compactions
|
|
248
|
+
|
|
249
|
+
// Session boundary detection — MCP server outlives Claude CLI sessions.
|
|
250
|
+
// When a new CLI session starts (>90s gap between requests), reset sys prompt state
|
|
251
|
+
// so the system prompt shows up on the first user submit of every session.
|
|
252
|
+
// NOTE: _lastRequestTime already declared at line 48 (orphan detection) — reuse it
|
|
253
|
+
const SESSION_GAP_MS = 90000; // 90s gap = new session
|
|
254
|
+
|
|
255
|
+
// Custom system prompt — user-defined replacement for OG system prompt
|
|
256
|
+
let _customSystemPrompt = null; // { prompt: string, ogHash: string } or null
|
|
257
|
+
|
|
258
|
+
function _loadOgSysPrompt() {
|
|
259
|
+
try {
|
|
260
|
+
if (existsSync(OG_SYS_PROMPT_FILE)) {
|
|
261
|
+
const data = JSON.parse(readFileSync(OG_SYS_PROMPT_FILE, 'utf8'));
|
|
262
|
+
if (data?.prompt) {
|
|
263
|
+
stats._lastSystemPromptFull = data.prompt;
|
|
264
|
+
// DON'T pre-load _seenSysPromptHash — first request must always pass through
|
|
265
|
+
log('info', `Loaded persisted OG system prompt (${data.prompt.length} chars)`);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
} catch (e) {
|
|
269
|
+
log('warn', `Failed to load OG sys prompt: ${e.message}`);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function _saveOgSysPrompt(prompt, hash) {
|
|
274
|
+
try {
|
|
275
|
+
writeFileSync(OG_SYS_PROMPT_FILE, JSON.stringify({ prompt, hash, savedAt: Date.now() }, null, 2), 'utf8');
|
|
276
|
+
} catch (e) {
|
|
277
|
+
log('warn', `Failed to save OG sys prompt: ${e.message}`);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function _loadCustomSysPrompt() {
|
|
282
|
+
try {
|
|
283
|
+
if (existsSync(CUSTOM_SYS_PROMPT_FILE)) {
|
|
284
|
+
const data = JSON.parse(readFileSync(CUSTOM_SYS_PROMPT_FILE, 'utf8'));
|
|
285
|
+
if (data?.prompt && data?.ogHash) {
|
|
286
|
+
_customSystemPrompt = data;
|
|
287
|
+
// DON'T set _seenSysPromptHash here — first request of new Claude session must pass through
|
|
288
|
+
log('info', `Loaded custom system prompt (ogHash=${data.ogHash.slice(0, 8)}, ${data.prompt.length} chars)`);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
} catch (e) {
|
|
292
|
+
log('warn', `Failed to load custom sys prompt: ${e.message}`);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function _saveCustomSysPrompt(data) {
|
|
297
|
+
try {
|
|
298
|
+
if (data) {
|
|
299
|
+
writeFileSync(CUSTOM_SYS_PROMPT_FILE, JSON.stringify(data, null, 2), 'utf8');
|
|
300
|
+
} else {
|
|
301
|
+
if (existsSync(CUSTOM_SYS_PROMPT_FILE)) unlinkSync(CUSTOM_SYS_PROMPT_FILE);
|
|
302
|
+
}
|
|
303
|
+
} catch (e) {
|
|
304
|
+
log('warn', `Failed to save custom sys prompt: ${e.message}`);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Compaction signal from precompact hook (set via /expect-compaction endpoint)
|
|
309
|
+
let _expectCompaction = false;
|
|
310
|
+
let _compactionSignalTime = 0;
|
|
311
|
+
|
|
312
|
+
// Load persisted custom system prompt on module init
|
|
313
|
+
_loadCustomSysPrompt();
|
|
314
|
+
|
|
208
315
|
// Stats
|
|
209
316
|
const stats = {
|
|
210
317
|
totalRequests: 0,
|
|
@@ -231,6 +338,9 @@ const stats = {
|
|
|
231
338
|
startTime: Date.now()
|
|
232
339
|
};
|
|
233
340
|
|
|
341
|
+
// Load persisted OG system prompt (needs stats object)
|
|
342
|
+
_loadOgSysPrompt();
|
|
343
|
+
|
|
234
344
|
// ============================================================================
|
|
235
345
|
// Translation Memory — learns from verified round-trips
|
|
236
346
|
// ============================================================================
|
|
@@ -440,6 +550,111 @@ function learnSynonyms(origWords, backWords) {
|
|
|
440
550
|
}
|
|
441
551
|
let _synSaveTimer = null;
|
|
442
552
|
|
|
553
|
+
// ============================================================================
|
|
554
|
+
// Passthrough Vocabulary — words that ALWAYS fail translation, skip 'em
|
|
555
|
+
// ============================================================================
|
|
556
|
+
// Words like "webpack", "nginx", "stdout" will never translate correctly.
|
|
557
|
+
// After N failures, they get added here and bypass the socket entirely.
|
|
558
|
+
// This prevents the same words from tanking verification scores every request.
|
|
559
|
+
|
|
560
|
+
const PASSTHROUGH_FILE = join(SPECMEM_DATA, 'run', 'passthrough-vocab.json');
|
|
561
|
+
const PASSTHROUGH_FAIL_THRESHOLD = 2; // 2 failures → permanent passthrough
|
|
562
|
+
let _passthroughCache = null;
|
|
563
|
+
|
|
564
|
+
// Built-in passthrough: tech terms that should NEVER be translated
|
|
565
|
+
const BUILTIN_PASSTHROUGH = new Set([
|
|
566
|
+
'webpack', 'nginx', 'redis', 'postgres', 'postgresql', 'mongodb', 'sqlite',
|
|
567
|
+
'docker', 'kubernetes', 'github', 'gitlab', 'npm', 'yarn', 'pnpm', 'bun',
|
|
568
|
+
'typescript', 'javascript', 'nodejs', 'python', 'golang', 'rustlang',
|
|
569
|
+
'stdout', 'stderr', 'stdin', 'async', 'await', 'const', 'enum', 'struct',
|
|
570
|
+
'boolean', 'parseInt', 'typeof', 'instanceof', 'undefined', 'null',
|
|
571
|
+
'localhost', 'middleware', 'dockerfile', 'makefile', 'readme',
|
|
572
|
+
'eslint', 'prettier', 'babel', 'rollup', 'vite', 'esbuild',
|
|
573
|
+
'mysql', 'graphql', 'grpc', 'websocket', 'oauth', 'jwt',
|
|
574
|
+
'ubuntu', 'debian', 'centos', 'linux', 'macos', 'windows',
|
|
575
|
+
'claude', 'anthropic', 'openai', 'specmem', 'hardwick',
|
|
576
|
+
]);
|
|
577
|
+
|
|
578
|
+
function _loadPassthrough() {
|
|
579
|
+
if (_passthroughCache) return _passthroughCache;
|
|
580
|
+
_passthroughCache = {};
|
|
581
|
+
try {
|
|
582
|
+
if (existsSync(PASSTHROUGH_FILE)) {
|
|
583
|
+
_passthroughCache = JSON.parse(readFileSync(PASSTHROUGH_FILE, 'utf8'));
|
|
584
|
+
}
|
|
585
|
+
} catch { /* corrupt, start fresh */ }
|
|
586
|
+
return _passthroughCache;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
function _savePassthrough() {
|
|
590
|
+
try {
|
|
591
|
+
mkdirSync(dirname(PASSTHROUGH_FILE), { recursive: true });
|
|
592
|
+
writeFileSync(PASSTHROUGH_FILE, JSON.stringify(_passthroughCache || {}), 'utf8');
|
|
593
|
+
} catch {}
|
|
594
|
+
}
|
|
595
|
+
let _ptSaveTimer = null;
|
|
596
|
+
|
|
597
|
+
function isPassthrough(word) {
|
|
598
|
+
const lower = word.toLowerCase();
|
|
599
|
+
if (BUILTIN_PASSTHROUGH.has(lower)) return true;
|
|
600
|
+
const pt = _loadPassthrough();
|
|
601
|
+
return pt[lower] && pt[lower] >= PASSTHROUGH_FAIL_THRESHOLD;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
function recordPassthroughFailure(word) {
|
|
605
|
+
const lower = word.toLowerCase();
|
|
606
|
+
if (BUILTIN_PASSTHROUGH.has(lower)) return; // already permanent
|
|
607
|
+
const pt = _loadPassthrough();
|
|
608
|
+
pt[lower] = (pt[lower] || 0) + 1;
|
|
609
|
+
if (!_ptSaveTimer) {
|
|
610
|
+
_ptSaveTimer = setTimeout(() => { _savePassthrough(); _ptSaveTimer = null; }, 10000);
|
|
611
|
+
if (_ptSaveTimer.unref) _ptSaveTimer.unref();
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// ============================================================================
|
|
616
|
+
// Word-Level Translation Memory — cache individual word translations
|
|
617
|
+
// ============================================================================
|
|
618
|
+
// When "configuration" → "配置" works in a verified batch, cache that mapping.
|
|
619
|
+
// Next time "configuration" appears in ANY text, use the cache instead of socket.
|
|
620
|
+
// This is separate from the full-text TM (which caches entire steno blocks).
|
|
621
|
+
|
|
622
|
+
const WORD_TM_FILE = join(SPECMEM_DATA, 'run', 'word-translations.json');
|
|
623
|
+
let _wordTMCache = null;
|
|
624
|
+
|
|
625
|
+
function _loadWordTM() {
|
|
626
|
+
if (_wordTMCache) return _wordTMCache;
|
|
627
|
+
_wordTMCache = {};
|
|
628
|
+
try {
|
|
629
|
+
if (existsSync(WORD_TM_FILE)) {
|
|
630
|
+
_wordTMCache = JSON.parse(readFileSync(WORD_TM_FILE, 'utf8'));
|
|
631
|
+
}
|
|
632
|
+
} catch { /* corrupt, start fresh */ }
|
|
633
|
+
return _wordTMCache;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
function _saveWordTM() {
|
|
637
|
+
try {
|
|
638
|
+
mkdirSync(dirname(WORD_TM_FILE), { recursive: true });
|
|
639
|
+
writeFileSync(WORD_TM_FILE, JSON.stringify(_wordTMCache || {}), 'utf8');
|
|
640
|
+
} catch {}
|
|
641
|
+
}
|
|
642
|
+
let _wordTMSaveTimer = null;
|
|
643
|
+
|
|
644
|
+
function wordTMLookup(word) {
|
|
645
|
+
const tm = _loadWordTM();
|
|
646
|
+
return tm[word.toLowerCase()] || null;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
function wordTMStore(enWord, zhTranslation) {
|
|
650
|
+
const tm = _loadWordTM();
|
|
651
|
+
tm[enWord.toLowerCase()] = zhTranslation;
|
|
652
|
+
if (!_wordTMSaveTimer) {
|
|
653
|
+
_wordTMSaveTimer = setTimeout(() => { _saveWordTM(); _wordTMSaveTimer = null; }, 10000);
|
|
654
|
+
if (_wordTMSaveTimer.unref) _wordTMSaveTimer.unref();
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
443
658
|
// ============================================================================
|
|
444
659
|
// Logging
|
|
445
660
|
// ============================================================================
|
|
@@ -492,12 +707,36 @@ function isCompactionRequest(body) {
|
|
|
492
707
|
function smartStripEdit(input) {
|
|
493
708
|
if (!input || !input.file_path || !input.old_string) return null;
|
|
494
709
|
const lines = [`Edit(${input.file_path})`];
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
710
|
+
const oldLines = input.old_string.split('\n');
|
|
711
|
+
const newLines = (input.new_string != null) ? input.new_string.split('\n') : [];
|
|
712
|
+
|
|
713
|
+
// Line-level diff using simple LCS approach
|
|
714
|
+
const m = oldLines.length, n = newLines.length;
|
|
715
|
+
// Build LCS table
|
|
716
|
+
const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
|
|
717
|
+
for (let i = 1; i <= m; i++) {
|
|
718
|
+
for (let j = 1; j <= n; j++) {
|
|
719
|
+
dp[i][j] = (oldLines[i - 1] === newLines[j - 1]) ? dp[i - 1][j - 1] + 1 : Math.max(dp[i - 1][j], dp[i][j - 1]);
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
// Backtrack to produce diff
|
|
723
|
+
let i = m, j = n;
|
|
724
|
+
const diffParts = [];
|
|
725
|
+
while (i > 0 || j > 0) {
|
|
726
|
+
if (i > 0 && j > 0 && oldLines[i - 1] === newLines[j - 1]) {
|
|
727
|
+
diffParts.push(` ${oldLines[i - 1]}`);
|
|
728
|
+
i--; j--;
|
|
729
|
+
} else if (j > 0 && (i === 0 || dp[i][j - 1] >= dp[i - 1][j])) {
|
|
730
|
+
diffParts.push(`+ ${newLines[j - 1]}`);
|
|
731
|
+
j--;
|
|
732
|
+
} else {
|
|
733
|
+
diffParts.push(`- ${oldLines[i - 1]}`);
|
|
734
|
+
i--;
|
|
735
|
+
}
|
|
500
736
|
}
|
|
737
|
+
diffParts.reverse();
|
|
738
|
+
for (const part of diffParts) lines.push(part);
|
|
739
|
+
|
|
501
740
|
if (input.replace_all) lines.push('(replace_all)');
|
|
502
741
|
return lines.join('\n');
|
|
503
742
|
}
|
|
@@ -509,12 +748,40 @@ function stripMessages(messages) {
|
|
|
509
748
|
let toolUsesStripped = 0;
|
|
510
749
|
let charsRemoved = 0;
|
|
511
750
|
|
|
512
|
-
|
|
751
|
+
// Three-tier stripping: find last and second-to-last assistant message indices
|
|
752
|
+
// Tier 1 (last assistant): ZERO stripping — active/pending tool calls
|
|
753
|
+
// Tier 2 (2nd-to-last assistant): ZERO stripping — needed for decision context
|
|
754
|
+
// Tier 3 (3rd+ oldest assistant): strip large content blobs, preserve metadata
|
|
755
|
+
let lastAssistantIndex = -1;
|
|
756
|
+
let secondLastAssistantIndex = -1;
|
|
757
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
758
|
+
if (messages[i] && messages[i].role === 'assistant') {
|
|
759
|
+
if (lastAssistantIndex === -1) { lastAssistantIndex = i; }
|
|
760
|
+
else if (secondLastAssistantIndex === -1) { secondLastAssistantIndex = i; break; }
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
const strippedMessages = messages.map((msg, msgIndex) => {
|
|
513
765
|
if (!msg || !msg.content || !Array.isArray(msg.content)) return msg;
|
|
514
766
|
|
|
767
|
+
// Tier 1 + Tier 2: assistant messages returned completely untouched
|
|
768
|
+
const isTier1or2Assistant = (msgIndex === lastAssistantIndex || msgIndex === secondLastAssistantIndex);
|
|
769
|
+
if (isTier1or2Assistant && msg.role === 'assistant') return msg;
|
|
770
|
+
|
|
771
|
+
// For user messages: check if the preceding assistant message is Tier 1 or 2.
|
|
772
|
+
// If so, this user message's tool_results correspond to those tool_use calls — don't strip.
|
|
773
|
+
if (msg.role === 'user') {
|
|
774
|
+
let precedingAssistantIndex = -1;
|
|
775
|
+
for (let pi = msgIndex - 1; pi >= 0; pi--) {
|
|
776
|
+
if (messages[pi] && messages[pi].role === 'assistant') { precedingAssistantIndex = pi; break; }
|
|
777
|
+
}
|
|
778
|
+
if (precedingAssistantIndex === lastAssistantIndex || precedingAssistantIndex === secondLastAssistantIndex) return msg;
|
|
779
|
+
}
|
|
780
|
+
|
|
515
781
|
const newContent = msg.content.map(block => {
|
|
516
782
|
if (!block || typeof block !== 'object') return block;
|
|
517
783
|
|
|
784
|
+
// Tier 3: strip large tool_result content blobs, preserve all metadata
|
|
518
785
|
if (block.type === 'tool_result') {
|
|
519
786
|
const originalContent = block.content;
|
|
520
787
|
if (!originalContent) return block;
|
|
@@ -540,45 +807,19 @@ function stripMessages(messages) {
|
|
|
540
807
|
return stripped;
|
|
541
808
|
}
|
|
542
809
|
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
// Smart diff stripping for Edit tool — keep only - / + lines
|
|
548
|
-
const editDiff = (block.name === 'Edit') ? smartStripEdit(input) : null;
|
|
549
|
-
if (editDiff) {
|
|
550
|
-
const inputStr = JSON.stringify(input);
|
|
551
|
-
charsRemoved += inputStr.length - editDiff.length;
|
|
552
|
-
toolUsesStripped++;
|
|
553
|
-
const stripped = {
|
|
554
|
-
type: 'tool_use',
|
|
555
|
-
id: block.id,
|
|
556
|
-
name: block.name,
|
|
557
|
-
input: { _stripped: editDiff }
|
|
558
|
-
};
|
|
559
|
-
if (block.cache_control) stripped.cache_control = block.cache_control;
|
|
560
|
-
return stripped;
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
const inputStr = JSON.stringify(input);
|
|
564
|
-
if (inputStr.length <= TOOL_USE_INPUT_PREVIEW_CHARS * 2) return block;
|
|
810
|
+
// NEVER strip tool_use.input — API validates input against tool schema.
|
|
811
|
+
// Replacing input with { _stripped } causes InputValidationError on every tool call.
|
|
812
|
+
if (block.type === 'tool_use') return block;
|
|
565
813
|
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
const
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
name: block.name,
|
|
576
|
-
input: {
|
|
577
|
-
_stripped: `${inputPreview}... [TRIMMED: ${inputStr.length} chars → ${TOOL_USE_INPUT_PREVIEW_CHARS}]`
|
|
578
|
-
}
|
|
579
|
-
};
|
|
580
|
-
if (block.cache_control) stripped.cache_control = block.cache_control;
|
|
581
|
-
return stripped;
|
|
814
|
+
// Strip assistant text blocks during compaction — don't send API responses back
|
|
815
|
+
if (block.type === 'text' && typeof block.text === 'string' && msg.role === 'assistant' && block.text.length > 0) {
|
|
816
|
+
const firstLine = block.text.split('\n')[0].slice(0, 80);
|
|
817
|
+
const removed = block.text.length - firstLine.length;
|
|
818
|
+
if (removed > 0) {
|
|
819
|
+
charsRemoved += removed;
|
|
820
|
+
toolResultsStripped++;
|
|
821
|
+
return { ...block, text: `${firstLine}...\n[ASST-STRIPPED: ${block.text.length}→${firstLine.length}]` };
|
|
822
|
+
}
|
|
582
823
|
}
|
|
583
824
|
|
|
584
825
|
return block;
|
|
@@ -609,13 +850,18 @@ function stripOldToolResults(messages) {
|
|
|
609
850
|
let charsRemoved = 0;
|
|
610
851
|
|
|
611
852
|
const newMessages = messages.map((msg, idx) => {
|
|
612
|
-
|
|
613
|
-
if (idx >= cutoff) return msg;
|
|
853
|
+
const isRecent = idx >= cutoff;
|
|
614
854
|
if (!msg || !msg.content || !Array.isArray(msg.content)) return msg;
|
|
615
855
|
|
|
616
856
|
const newContent = msg.content.map(block => {
|
|
617
857
|
if (!block || typeof block !== 'object') return block;
|
|
618
858
|
|
|
859
|
+
// NEVER strip tool_use.input — API validates against schema
|
|
860
|
+
if (block.type === 'tool_use') return block;
|
|
861
|
+
|
|
862
|
+
// For remaining block types: only strip if OLD message (preserve recent context)
|
|
863
|
+
if (isRecent) return block;
|
|
864
|
+
|
|
619
865
|
if (block.type === 'tool_result') {
|
|
620
866
|
const originalContent = block.content;
|
|
621
867
|
if (!originalContent) return block;
|
|
@@ -626,6 +872,7 @@ function stripOldToolResults(messages) {
|
|
|
626
872
|
|
|
627
873
|
// Only strip if above threshold
|
|
628
874
|
if (originalStr.length <= liveConfig.OLD_STRIP_THRESHOLD) return block;
|
|
875
|
+
if (originalStr.length <= OLD_STRIP_PREVIEW_CHARS) return block; // preview would be LONGER
|
|
629
876
|
|
|
630
877
|
const removed = originalStr.length - OLD_STRIP_PREVIEW_CHARS;
|
|
631
878
|
charsRemoved += removed;
|
|
@@ -645,41 +892,8 @@ function stripOldToolResults(messages) {
|
|
|
645
892
|
if (block.cache_control) stripped.cache_control = block.cache_control;
|
|
646
893
|
return stripped;
|
|
647
894
|
}
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
if (block.type === 'tool_use') {
|
|
651
|
-
const input = block.input;
|
|
652
|
-
if (!input) return block;
|
|
653
|
-
|
|
654
|
-
// Smart Edit stripping — keep only - / + diff lines
|
|
655
|
-
const editDiff = smartStripEdit(input);
|
|
656
|
-
if (editDiff) {
|
|
657
|
-
const origLen = JSON.stringify(input).length;
|
|
658
|
-
const newLen = JSON.stringify(editDiff.input).length;
|
|
659
|
-
charsRemoved += origLen - newLen;
|
|
660
|
-
toolResultsStripped++;
|
|
661
|
-
return editDiff;
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
const inputStr = JSON.stringify(input);
|
|
665
|
-
if (inputStr.length <= liveConfig.OLD_STRIP_THRESHOLD) return block;
|
|
666
|
-
|
|
667
|
-
const removed = inputStr.length - OLD_STRIP_PREVIEW_CHARS;
|
|
668
|
-
charsRemoved += removed;
|
|
669
|
-
toolResultsStripped++;
|
|
670
|
-
|
|
671
|
-
const inputPreview = inputStr.slice(0, OLD_STRIP_PREVIEW_CHARS);
|
|
672
|
-
const stripped = {
|
|
673
|
-
type: 'tool_use',
|
|
674
|
-
id: block.id,
|
|
675
|
-
name: block.name,
|
|
676
|
-
input: {
|
|
677
|
-
_stripped: `${inputPreview}... [TRIMMED: ${inputStr.length} chars → ${OLD_STRIP_PREVIEW_CHARS}]`
|
|
678
|
-
}
|
|
679
|
-
};
|
|
680
|
-
if (block.cache_control) stripped.cache_control = block.cache_control;
|
|
681
|
-
return stripped;
|
|
682
|
-
}
|
|
895
|
+
// NEVER strip tool_use.input — API validates against schema
|
|
896
|
+
if (block.type === 'tool_use') return block;
|
|
683
897
|
|
|
684
898
|
// Strip specmem hook injection text blocks from old messages
|
|
685
899
|
if (block.type === 'text' && typeof block.text === 'string') {
|
|
@@ -697,17 +911,19 @@ function stripOldToolResults(messages) {
|
|
|
697
911
|
};
|
|
698
912
|
}
|
|
699
913
|
|
|
700
|
-
// Strip old assistant text blocks — Claude's own output echoed back
|
|
701
|
-
// No point sending
|
|
702
|
-
if (msg.role === 'assistant' && txt.length >
|
|
703
|
-
const firstLine = txt.split('\n')[0].slice(0,
|
|
914
|
+
// Strip ALL old assistant text blocks — Claude's own output echoed back
|
|
915
|
+
// No point sending Anthropic its own responses; keep first line as context anchor
|
|
916
|
+
if (msg.role === 'assistant' && txt.length > 0) {
|
|
917
|
+
const firstLine = txt.split('\n')[0].slice(0, 80);
|
|
704
918
|
const removed = txt.length - firstLine.length;
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
919
|
+
if (removed > 0) {
|
|
920
|
+
charsRemoved += removed;
|
|
921
|
+
toolResultsStripped++;
|
|
922
|
+
return {
|
|
923
|
+
...block,
|
|
924
|
+
text: `${firstLine}...\n[ASST-STRIPPED: ${txt.length}→${firstLine.length}]`
|
|
925
|
+
};
|
|
926
|
+
}
|
|
711
927
|
}
|
|
712
928
|
}
|
|
713
929
|
|
|
@@ -745,19 +961,21 @@ function stripSystemReminders(messages) {
|
|
|
745
961
|
if (!matches) return msg;
|
|
746
962
|
|
|
747
963
|
let newText = msg.content;
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
964
|
+
// Use matchAll with index to remove by position, not content.
|
|
965
|
+
// This prevents identical reminders from nuking the first one we want to keep.
|
|
966
|
+
const allMatches = [...newText.matchAll(new RegExp(SR_RE.source, 'g'))];
|
|
967
|
+
// Process in reverse order so indices stay valid as we splice
|
|
968
|
+
for (let mi = allMatches.length - 1; mi >= 0; mi--) {
|
|
969
|
+
if (!firstSeen && mi === 0) {
|
|
970
|
+
firstSeen = true; // keep the very first one by position
|
|
751
971
|
continue;
|
|
752
972
|
}
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
charsRemoved += match.length;
|
|
758
|
-
remindersStripped++;
|
|
759
|
-
}
|
|
973
|
+
const m = allMatches[mi];
|
|
974
|
+
newText = newText.slice(0, m.index) + newText.slice(m.index + m[0].length);
|
|
975
|
+
charsRemoved += m[0].length;
|
|
976
|
+
remindersStripped++;
|
|
760
977
|
}
|
|
978
|
+
if (allMatches.length > 0 && !firstSeen) firstSeen = true;
|
|
761
979
|
return { ...msg, content: newText.replace(/\n{3,}/g, '\n\n').trim() };
|
|
762
980
|
}
|
|
763
981
|
|
|
@@ -771,18 +989,19 @@ function stripSystemReminders(messages) {
|
|
|
771
989
|
if (!matches) return block;
|
|
772
990
|
|
|
773
991
|
let newText = block.text;
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
992
|
+
// Position-based removal — prevents identical reminders from killing the kept one
|
|
993
|
+
const allBlockMatches = [...newText.matchAll(new RegExp(SR_RE.source, 'g'))];
|
|
994
|
+
for (let mi = allBlockMatches.length - 1; mi >= 0; mi--) {
|
|
995
|
+
if (!firstSeen && mi === 0) {
|
|
996
|
+
firstSeen = true;
|
|
777
997
|
continue;
|
|
778
998
|
}
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
remindersStripped++;
|
|
784
|
-
}
|
|
999
|
+
const m = allBlockMatches[mi];
|
|
1000
|
+
newText = newText.slice(0, m.index) + newText.slice(m.index + m[0].length);
|
|
1001
|
+
charsRemoved += m[0].length;
|
|
1002
|
+
remindersStripped++;
|
|
785
1003
|
}
|
|
1004
|
+
if (allBlockMatches.length > 0 && !firstSeen) firstSeen = true;
|
|
786
1005
|
const cleaned = newText.replace(/\n{3,}/g, '\n\n').trim();
|
|
787
1006
|
|
|
788
1007
|
// If block is now empty after stripping, remove it entirely
|
|
@@ -824,15 +1043,156 @@ const FILLER_WORDS = new Set([
|
|
|
824
1043
|
const KEEP_WORDS = new Set(['not', 'no', 'never', 'none', 'nor', 'neither', 'without',
|
|
825
1044
|
'all', 'every', 'each', 'both', 'only', 'must', 'always']);
|
|
826
1045
|
|
|
1046
|
+
// Programming abbreviations — deterministic shorthand Claude already understands.
|
|
1047
|
+
// Applied during steno phase for guaranteed token savings (no MT verification needed).
|
|
1048
|
+
// ~40% additional savings on top of filler word removal for code-heavy text.
|
|
1049
|
+
const STENO_ABBREVIATIONS = new Map([
|
|
1050
|
+
// Core programming constructs
|
|
1051
|
+
['function', 'fn'], ['functions', 'fns'], ['configuration', 'cfg'], ['configure', 'cfg'],
|
|
1052
|
+
['implementation', 'impl'], ['implement', 'impl'], ['application', 'app'], ['applications', 'apps'],
|
|
1053
|
+
['authentication', 'auth'], ['authenticate', 'auth'], ['authorization', 'authz'],
|
|
1054
|
+
['database', 'db'], ['databases', 'dbs'], ['directory', 'dir'], ['directories', 'dirs'],
|
|
1055
|
+
['environment', 'env'], ['environments', 'envs'], ['repository', 'repo'], ['repositories', 'repos'],
|
|
1056
|
+
['parameter', 'param'], ['parameters', 'params'], ['argument', 'arg'], ['arguments', 'args'],
|
|
1057
|
+
['property', 'prop'], ['properties', 'props'], ['attribute', 'attr'], ['attributes', 'attrs'],
|
|
1058
|
+
['reference', 'ref'], ['references', 'refs'], ['document', 'doc'], ['documentation', 'docs'],
|
|
1059
|
+
['message', 'msg'], ['messages', 'msgs'], ['number', 'num'], ['integer', 'int'], ['boolean', 'bool'],
|
|
1060
|
+
['object', 'obj'], ['objects', 'objs'], ['element', 'el'], ['elements', 'els'],
|
|
1061
|
+
['component', 'comp'], ['components', 'comps'], ['template', 'tmpl'], ['package', 'pkg'],
|
|
1062
|
+
['dependency', 'dep'], ['dependencies', 'deps'], ['library', 'lib'], ['libraries', 'libs'],
|
|
1063
|
+
['module', 'mod'], ['modules', 'mods'], ['version', 'ver'], ['command', 'cmd'],
|
|
1064
|
+
['request', 'req'], ['response', 'res'], ['callback', 'cb'], ['middleware', 'mw'],
|
|
1065
|
+
['specification', 'spec'], ['expression', 'expr'], ['variable', 'var'], ['constant', 'const'],
|
|
1066
|
+
['instance', 'inst'], ['constructor', 'ctor'], ['temporary', 'tmp'],
|
|
1067
|
+
['maximum', 'max'], ['minimum', 'min'], ['average', 'avg'],
|
|
1068
|
+
['previous', 'prev'], ['current', 'curr'], ['original', 'orig'], ['information', 'info'],
|
|
1069
|
+
['connection', 'conn'], ['connections', 'conns'], ['transaction', 'txn'],
|
|
1070
|
+
['operation', 'op'], ['operations', 'ops'], ['execution', 'exec'],
|
|
1071
|
+
['process', 'proc'], ['processes', 'procs'], ['manager', 'mgr'], ['service', 'svc'],
|
|
1072
|
+
['server', 'srv'], ['certificate', 'cert'], ['permission', 'perm'], ['separator', 'sep'],
|
|
1073
|
+
['initialize', 'init'], ['initialization', 'init'], ['synchronize', 'sync'],
|
|
1074
|
+
['asynchronous', 'async'], ['development', 'dev'], ['production', 'prod'],
|
|
1075
|
+
['distribution', 'dist'], ['administrator', 'admin'], ['memory', 'mem'],
|
|
1076
|
+
['address', 'addr'], ['register', 'reg'], ['buffer', 'buf'], ['channel', 'chan'],
|
|
1077
|
+
['context', 'ctx'], ['receive', 'recv'], ['generate', 'gen'], ['generator', 'gen'],
|
|
1078
|
+
['calculate', 'calc'], ['resource', 'rsc'], ['allocation', 'alloc'], ['allocate', 'alloc'],
|
|
1079
|
+
['descriptor', 'desc'], ['position', 'pos'], ['source', 'src'], ['destination', 'dst'],
|
|
1080
|
+
['character', 'char'], ['characters', 'chars'],
|
|
1081
|
+
// Verbs
|
|
1082
|
+
['execute', 'exec'], ['executing', 'exec'], ['remove', 'rm'], ['removing', 'rm'],
|
|
1083
|
+
['delete', 'del'], ['deleting', 'del'], ['compare', 'cmp'], ['convert', 'conv'],
|
|
1084
|
+
// Error/status
|
|
1085
|
+
['error', 'err'], ['errors', 'errs'], ['warning', 'warn'], ['warnings', 'warns'],
|
|
1086
|
+
['exception', 'exc'], ['successful', 'ok'], ['successfully', 'ok'],
|
|
1087
|
+
// Network
|
|
1088
|
+
['protocol', 'proto'], ['interface', 'iface'], ['network', 'net'],
|
|
1089
|
+
['socket', 'sock'], ['password', 'pwd'],
|
|
1090
|
+
// Common English → ultra-short
|
|
1091
|
+
['however', 'but'], ['although', 'tho'], ['because', 'bc'], ['therefore', 'so'],
|
|
1092
|
+
['regarding', 're'], ['approximately', '~'], ['including', 'incl'],
|
|
1093
|
+
['currently', 'now'], ['previously', 'prev'], ['additional', 'extra'],
|
|
1094
|
+
['following', 'next'], ['different', 'diff'], ['required', 'reqd'],
|
|
1095
|
+
['returned', 'ret'], ['returns', 'ret'], ['specified', 'given'],
|
|
1096
|
+
['completed', 'done'], ['automatically', 'auto'], ['immediately', 'now'],
|
|
1097
|
+
]);
|
|
1098
|
+
|
|
1099
|
+
// Learned abbreviations file (grows over time from verified loopbacks)
|
|
1100
|
+
const LEARNED_ABBREV_FILE = join(SPECMEM_DATA, 'run', 'learned-abbreviations.json');
|
|
1101
|
+
let _learnedAbbrevCache = null;
|
|
1102
|
+
|
|
1103
|
+
function _loadLearnedAbbreviations() {
|
|
1104
|
+
if (_learnedAbbrevCache) return _learnedAbbrevCache;
|
|
1105
|
+
_learnedAbbrevCache = {};
|
|
1106
|
+
try {
|
|
1107
|
+
if (existsSync(LEARNED_ABBREV_FILE)) {
|
|
1108
|
+
_learnedAbbrevCache = JSON.parse(readFileSync(LEARNED_ABBREV_FILE, 'utf8'));
|
|
1109
|
+
}
|
|
1110
|
+
} catch { /* corrupt, start fresh */ }
|
|
1111
|
+
return _learnedAbbrevCache;
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
function _saveLearnedAbbreviations() {
|
|
1115
|
+
try {
|
|
1116
|
+
const data = JSON.stringify(_learnedAbbrevCache || {});
|
|
1117
|
+
mkdirSync(dirname(LEARNED_ABBREV_FILE), { recursive: true });
|
|
1118
|
+
writeFileSync(LEARNED_ABBREV_FILE, data, 'utf8');
|
|
1119
|
+
} catch {}
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
// ============================================================================
|
|
1123
|
+
// YCC — Yung Cracka Compress (optional layer, off by default)
|
|
1124
|
+
// ============================================================================
|
|
1125
|
+
// 🥷 Slang-based token compression fluent in DMV x Florida grown.
|
|
1126
|
+
// Uses internet/regional shorthand that every LLM already understands
|
|
1127
|
+
// from training on social media. Opt-in via SLANG_ENABLED = true.
|
|
1128
|
+
// "we got a hood rat on them token waste" — belt to ass
|
|
1129
|
+
const SLANG_COMPRESSIONS = new Map([
|
|
1130
|
+
// ── Universal Internet Slang (every LLM trained on social media knows these) ──
|
|
1131
|
+
['definitely', 'def'], ['probably', 'prolly'], ['because', 'bc'],
|
|
1132
|
+
['people', 'ppl'], ['something', 'smth'], ['nothing', 'nth'],
|
|
1133
|
+
['anything', 'anythng'], ['everyone', 'evryone'], ['already', 'alr'],
|
|
1134
|
+
['between', 'btwn'], ['tomorrow', 'tmrw'], ['yesterday', 'ystrdy'],
|
|
1135
|
+
['together', 'tgthr'], ['usually', 'usu'], ['obviously', 'obvi'],
|
|
1136
|
+
['seriously', 'srsly'], ['actually', 'actly'], ['basically', 'basicly'],
|
|
1137
|
+
['honestly', 'honstly'], ['especially', 'esp'], ['literally', 'lit'],
|
|
1138
|
+
['whatever', 'wtv'], ['though', 'tho'], ['enough', 'enuf'],
|
|
1139
|
+
['through', 'thru'], ['going', 'goin'], ['trying', 'tryna'],
|
|
1140
|
+
['about', 'abt'], ['really', 'rly'], ['please', 'pls'],
|
|
1141
|
+
['thanks', 'thx'], ['okay', 'ok'], ['never', 'nvr'],
|
|
1142
|
+
['conversation', 'convo'], ['situation', 'sitch'], ['legitimate', 'legit'],
|
|
1143
|
+
['important', 'impt'], ['favorite', 'fav'], ['problem', 'prob'],
|
|
1144
|
+
['question', 'q'], ['picture', 'pic'], ['pictures', 'pics'],
|
|
1145
|
+
['example', 'ex'], ['suspicious', 'sus'], ['nevermind', 'nvm'],
|
|
1146
|
+
['awkward', 'awk'], ['ridiculous', 'ridic'], ['gorgeous', 'gorge'],
|
|
1147
|
+
['absolutely', 'abs'],
|
|
1148
|
+
|
|
1149
|
+
// ── DMV (DC/MD/VA) Slang — understood from Twitter/TikTok/Reddit ──
|
|
1150
|
+
// Only unambiguous single-word compressions that LLMs parse correctly
|
|
1151
|
+
['extremely', 'hella'], ['terrible', 'trash'], ['excellent', 'fire'],
|
|
1152
|
+
['boring', 'dry'], ['jealous', 'salty'], ['scared', 'shook'],
|
|
1153
|
+
['surprised', 'shook'], ['bragging', 'stuntin'], ['showing', 'flexin'],
|
|
1154
|
+
['lying', 'cappin'], ['ignoring', 'ghostin'], ['complaining', 'pressed'],
|
|
1155
|
+
['succeeding', 'winnin'], ['failing', 'takin'],
|
|
1156
|
+
['exhausted', 'gassed'], ['focused', 'locked'],
|
|
1157
|
+
|
|
1158
|
+
// ── Florida / Miami Slang — SoFlo internet vernacular ──
|
|
1159
|
+
['season', 'szn'], ['professional', 'pro'], ['introduction', 'intro'],
|
|
1160
|
+
['combination', 'combo'], ['reputation', 'rep'], ['competition', 'comp'],
|
|
1161
|
+
['demonstration', 'demo'], ['examination', 'exam'], ['university', 'uni'],
|
|
1162
|
+
['apartment', 'apt'], ['neighborhood', 'hood'], ['boulevard', 'blvd'],
|
|
1163
|
+
|
|
1164
|
+
// ── Contraction-style (phonetic compressions LLMs handle natively) ──
|
|
1165
|
+
['should', 'shd'], ['would', 'wld'], ['could', 'cld'],
|
|
1166
|
+
['doing', 'doin'], ['having', 'havin'], ['making', 'makin'],
|
|
1167
|
+
['taking', 'takin'], ['getting', 'gettin'], ['coming', 'comin'],
|
|
1168
|
+
['running', 'runnin'], ['working', 'workin'], ['looking', 'lookin'],
|
|
1169
|
+
['thinking', 'thinkin'], ['saying', 'sayin'], ['telling', 'tellin'],
|
|
1170
|
+
['building', 'buildin'], ['calling', 'callin'], ['sending', 'sendin'],
|
|
1171
|
+
['reading', 'readin'], ['writing', 'writin'], ['waiting', 'waitin'],
|
|
1172
|
+
]);
|
|
1173
|
+
|
|
827
1174
|
/**
|
|
828
|
-
* Stenographic reduction — strip filler words
|
|
829
|
-
*
|
|
1175
|
+
* Stenographic reduction — strip filler words + apply programming abbreviations
|
|
1176
|
+
* + optional YCC (Yung Cracka Compress) slang layer.
|
|
1177
|
+
* Steno alone: ~40% savings. With YCC: ~45-50% on conversational text.
|
|
830
1178
|
*/
|
|
831
1179
|
function stenographicReduce(text) {
|
|
1180
|
+
const learnedAbbrev = _loadLearnedAbbreviations();
|
|
1181
|
+
const useSlang = liveConfig.SLANG_ENABLED;
|
|
832
1182
|
return text.replace(/\b\w+\b/g, (word) => {
|
|
833
1183
|
const lower = word.toLowerCase();
|
|
834
1184
|
if (KEEP_WORDS.has(lower)) return word;
|
|
835
1185
|
if (FILLER_WORDS.has(lower)) return '';
|
|
1186
|
+
// Static abbreviations (deterministic, always applied)
|
|
1187
|
+
const abbr = STENO_ABBREVIATIONS.get(lower);
|
|
1188
|
+
if (abbr) return abbr;
|
|
1189
|
+
// YCC layer (Yung Cracka Compress — opt-in slang)
|
|
1190
|
+
if (useSlang) {
|
|
1191
|
+
const slang = SLANG_COMPRESSIONS.get(lower);
|
|
1192
|
+
if (slang) return slang;
|
|
1193
|
+
}
|
|
1194
|
+
// Learned abbreviations (verified via loopback)
|
|
1195
|
+
if (learnedAbbrev[lower]) return learnedAbbrev[lower];
|
|
836
1196
|
return word;
|
|
837
1197
|
}).replace(/ +/g, ' ').replace(/ ([.,;:!?])/g, '$1').trim();
|
|
838
1198
|
}
|
|
@@ -857,22 +1217,118 @@ function looksLikeNaturalLanguage(text) {
|
|
|
857
1217
|
* @param {string} target - target language code
|
|
858
1218
|
* @returns {Promise<string[]>} translated texts (fallback: originals)
|
|
859
1219
|
*/
|
|
1220
|
+
/**
|
|
1221
|
+
* Word-level translation: splits each text into individual words, translates
|
|
1222
|
+
* each word independently for maximum accuracy, then reassembles with original
|
|
1223
|
+
* spacing/punctuation preserved. Non-alpha tokens (numbers, paths, punctuation)
|
|
1224
|
+
* pass through untranslated.
|
|
1225
|
+
*/
|
|
860
1226
|
function translateBatch(texts, socketPath, source = 'en', target = 'zh') {
|
|
861
1227
|
return new Promise((resolve) => {
|
|
862
1228
|
if (!existsSync(socketPath)) { resolve(texts); return; }
|
|
1229
|
+
|
|
1230
|
+
// Tokenize each text into words and non-word separators
|
|
1231
|
+
const TOKEN_RE = /([a-zA-Z]+)|([^a-zA-Z]+)/g;
|
|
1232
|
+
const tokenMap = []; // { textIdx, word } — only translatable words
|
|
1233
|
+
const structures = []; // per-text: array of { type: 'word'|'sep', value, mapIdx? }
|
|
1234
|
+
for (let i = 0; i < texts.length; i++) {
|
|
1235
|
+
const struct = [];
|
|
1236
|
+
let match;
|
|
1237
|
+
const re = new RegExp(TOKEN_RE.source, 'g');
|
|
1238
|
+
while ((match = re.exec(texts[i])) !== null) {
|
|
1239
|
+
if (match[1] && match[1].length >= 2) {
|
|
1240
|
+
// Translatable word (2+ alpha chars)
|
|
1241
|
+
struct.push({ type: 'word', value: match[1], mapIdx: tokenMap.length });
|
|
1242
|
+
tokenMap.push({ textIdx: i, word: match[1] });
|
|
1243
|
+
} else {
|
|
1244
|
+
// Separator, number, punctuation, single char — pass through
|
|
1245
|
+
struct.push({ type: 'sep', value: match[0] });
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
structures.push(struct);
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
if (tokenMap.length === 0) { resolve(texts); return; }
|
|
1252
|
+
|
|
1253
|
+
// ── Layer 1: Passthrough + Word TM pre-filter ──
|
|
1254
|
+
// Check each word against passthrough vocab and word-level TM cache.
|
|
1255
|
+
// Only words that need the neural socket go to the network.
|
|
1256
|
+
const preResolved = new Array(tokenMap.length).fill(null); // null = needs socket
|
|
1257
|
+
const socketIndices = []; // indices into tokenMap that need the socket
|
|
1258
|
+
let ptSkips = 0, wmHits = 0;
|
|
1259
|
+
|
|
1260
|
+
for (let ti = 0; ti < tokenMap.length; ti++) {
|
|
1261
|
+
const word = tokenMap[ti].word;
|
|
1262
|
+
if (isPassthrough(word)) {
|
|
1263
|
+
preResolved[ti] = word; // keep original English
|
|
1264
|
+
ptSkips++;
|
|
1265
|
+
} else {
|
|
1266
|
+
const cached = wordTMLookup(word);
|
|
1267
|
+
if (cached) {
|
|
1268
|
+
preResolved[ti] = cached; // use cached Chinese
|
|
1269
|
+
wmHits++;
|
|
1270
|
+
} else {
|
|
1271
|
+
socketIndices.push(ti);
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
if (ptSkips > 0 || wmHits > 0) {
|
|
1277
|
+
log('info', `WORD-CACHE: ${wmHits} word-TM hits, ${ptSkips} passthrough skips, ${socketIndices.length} need socket`);
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
// If everything was resolved from cache, skip the socket entirely
|
|
1281
|
+
if (socketIndices.length === 0) {
|
|
1282
|
+
const results = [];
|
|
1283
|
+
for (let i = 0; i < texts.length; i++) {
|
|
1284
|
+
let out = '';
|
|
1285
|
+
for (const tok of structures[i]) {
|
|
1286
|
+
if (tok.type === 'sep') { out += tok.value; }
|
|
1287
|
+
else { out += preResolved[tok.mapIdx] || tok.value; }
|
|
1288
|
+
}
|
|
1289
|
+
results.push(out);
|
|
1290
|
+
}
|
|
1291
|
+
resolve(results);
|
|
1292
|
+
return;
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
// Only send uncached words to the socket
|
|
1296
|
+
const socketWords = socketIndices.map(i => tokenMap[i].word);
|
|
863
1297
|
const conn = createConnection(socketPath);
|
|
864
1298
|
conn.setTimeout(15000);
|
|
865
|
-
const
|
|
866
|
-
const body = JSON.stringify({ q: flattened.join('\n'), source, target }) + '\n';
|
|
1299
|
+
const body = JSON.stringify({ q: socketWords.join('\n'), source, target }) + '\n';
|
|
867
1300
|
conn.on('connect', () => conn.write(body));
|
|
868
1301
|
let data = '';
|
|
869
|
-
conn.on('data', d => {
|
|
1302
|
+
conn.on('data', d => {
|
|
1303
|
+
data += d;
|
|
1304
|
+
// Server sends {"status":"processing"}\n THEN {"translatedText":"..."}\n
|
|
1305
|
+
// Only close when we get the actual translation result, not the status line
|
|
1306
|
+
const lines = data.split('\n').filter(l => l.trim());
|
|
1307
|
+
const lastLine = lines[lines.length - 1];
|
|
1308
|
+
if (lastLine && lastLine.includes('"translatedText"')) conn.end();
|
|
1309
|
+
});
|
|
870
1310
|
conn.on('end', () => {
|
|
871
1311
|
try {
|
|
872
|
-
|
|
1312
|
+
// Find the line with actual translation results (skip status lines)
|
|
1313
|
+
const lines = data.split('\n').filter(l => l.trim());
|
|
1314
|
+
const resultLine = lines.find(l => l.includes('"translatedText"')) || lines[lines.length - 1];
|
|
1315
|
+
const parsed = JSON.parse(resultLine.trim());
|
|
873
1316
|
if (parsed.translatedText) {
|
|
874
|
-
const
|
|
875
|
-
|
|
1317
|
+
const socketResults = parsed.translatedText.split('\n');
|
|
1318
|
+
// Merge socket results back into preResolved
|
|
1319
|
+
for (let si = 0; si < socketIndices.length; si++) {
|
|
1320
|
+
preResolved[socketIndices[si]] = socketResults[si] || tokenMap[socketIndices[si]].word;
|
|
1321
|
+
}
|
|
1322
|
+
// Reassemble each text using its structure
|
|
1323
|
+
const results = [];
|
|
1324
|
+
for (let i = 0; i < texts.length; i++) {
|
|
1325
|
+
let out = '';
|
|
1326
|
+
for (const tok of structures[i]) {
|
|
1327
|
+
if (tok.type === 'sep') { out += tok.value; }
|
|
1328
|
+
else { out += preResolved[tok.mapIdx] || tok.value; }
|
|
1329
|
+
}
|
|
1330
|
+
results.push(out);
|
|
1331
|
+
}
|
|
876
1332
|
resolve(results);
|
|
877
1333
|
} else { resolve(texts); }
|
|
878
1334
|
} catch { resolve(texts); }
|
|
@@ -930,14 +1386,19 @@ function normalizeWord(word) {
|
|
|
930
1386
|
}
|
|
931
1387
|
|
|
932
1388
|
/**
|
|
933
|
-
* Loop-back verification: checks if back-translated text
|
|
934
|
-
*
|
|
1389
|
+
* Loop-back verification: checks if back-translated text preserves
|
|
1390
|
+
* the original meaning and content through the en→zh→en round trip.
|
|
1391
|
+
*
|
|
1392
|
+
* Two-metric approach (takes the HIGHER score):
|
|
1393
|
+
* 1. LCS Dice: Longest Common Subsequence preserves word ORDER
|
|
1394
|
+
* Score = 2 * LCS / (origLen + backLen)
|
|
1395
|
+
* 2. Jaccard: Bag-of-words overlap IGNORES reordering
|
|
1396
|
+
* Score = |intersection| / |union|
|
|
935
1397
|
*
|
|
936
|
-
*
|
|
937
|
-
*
|
|
938
|
-
*
|
|
1398
|
+
* Chinese naturally reorders words vs English, so LCS alone is too strict.
|
|
1399
|
+
* Jaccard catches cases where all the right words survive but in different order.
|
|
1400
|
+
* Taking the max means either metric can green-light the translation.
|
|
939
1401
|
*
|
|
940
|
-
* Score = 2 * LCS / (origLen + backLen) (Dice coefficient on word sequences)
|
|
941
1402
|
* @returns {number} similarity score 0.0 to 1.0
|
|
942
1403
|
*/
|
|
943
1404
|
function verifySimilarity(original, backTranslated) {
|
|
@@ -945,15 +1406,28 @@ function verifySimilarity(original, backTranslated) {
|
|
|
945
1406
|
const backWords = extractContentWords(backTranslated).map(normalizeWord);
|
|
946
1407
|
if (origWords.length === 0) return 1.0;
|
|
947
1408
|
if (backWords.length === 0) return 0.0;
|
|
1409
|
+
|
|
1410
|
+
// Metric 1: LCS Dice (order-sensitive)
|
|
948
1411
|
const lcs = lcsLength(origWords, backWords);
|
|
949
|
-
|
|
1412
|
+
const lcsDice = (2 * lcs) / (origWords.length + backWords.length);
|
|
1413
|
+
|
|
1414
|
+
// Metric 2: Jaccard (order-insensitive, bag-of-words)
|
|
1415
|
+
const origSet = new Set(origWords);
|
|
1416
|
+
const backSet = new Set(backWords);
|
|
1417
|
+
let intersection = 0;
|
|
1418
|
+
for (const w of origSet) if (backSet.has(w)) intersection++;
|
|
1419
|
+
const union = new Set([...origWords, ...backWords]).size;
|
|
1420
|
+
const jaccard = union > 0 ? intersection / union : 0;
|
|
1421
|
+
|
|
1422
|
+
return Math.max(lcsDice, jaccard);
|
|
950
1423
|
}
|
|
951
1424
|
|
|
952
1425
|
// Minimum similarity for accepting Chinese translation (0.0-1.0)
|
|
953
|
-
// 0.
|
|
954
|
-
//
|
|
955
|
-
//
|
|
956
|
-
|
|
1426
|
+
// 0.65 = relaxed threshold — the two-metric approach (LCS + Jaccard) means
|
|
1427
|
+
// we're already more forgiving of reordering. This catches translations where
|
|
1428
|
+
// word meaning survives but order shifts (common in EN↔ZH).
|
|
1429
|
+
// Combined with passthrough vocab + word TM, this should verify ~70%+ of blocks.
|
|
1430
|
+
const VERIFICATION_THRESHOLD = 0.65;
|
|
957
1431
|
|
|
958
1432
|
function hasDontCompressFlag(body) {
|
|
959
1433
|
if (!body || !body.messages || !Array.isArray(body.messages)) return false;
|
|
@@ -1006,11 +1480,11 @@ function messageShouldSkipCompression(msg) {
|
|
|
1006
1480
|
* tool calls like Grep patterns.
|
|
1007
1481
|
*
|
|
1008
1482
|
* Per-block pipeline:
|
|
1009
|
-
* 1. Stenographic reduce (
|
|
1010
|
-
* 2. [tool_result only] Translate steno→Chinese (zt) — additional
|
|
1483
|
+
* 1. Stenographic reduce (filler words + abbreviations) — guaranteed ~40% savings
|
|
1484
|
+
* 2. [tool_result only] Translate steno→Chinese (zt) — additional ~30% on top
|
|
1011
1485
|
* 3. [tool_result only] Translate Chinese→English (loop-back verification)
|
|
1012
|
-
* 4. [tool_result only] Compare loop-back with original — if ≥
|
|
1013
|
-
* 5. Otherwise, fall back to steno-only (still saves
|
|
1486
|
+
* 4. [tool_result only] Compare loop-back with original — if ≥65% match, accept Chinese
|
|
1487
|
+
* 5. Otherwise, fall back to steno-only (still saves ~40%)
|
|
1014
1488
|
*
|
|
1015
1489
|
* DONT_COMPRESS: Per-message flag. If any block in a message contains "DONT_COMPRESS",
|
|
1016
1490
|
* or a tool_use has input._dont_compress, the entire message is skipped.
|
|
@@ -1192,8 +1666,23 @@ async function compressMessagesLive(messages) {
|
|
|
1192
1666
|
// Verified — Chinese preserves meaning, use it
|
|
1193
1667
|
finalTexts[idx] = needVerify[v];
|
|
1194
1668
|
verifiedCount++;
|
|
1195
|
-
// LEARN: store in Translation Memory
|
|
1669
|
+
// LEARN: store full-text in Translation Memory
|
|
1196
1670
|
tmStore(stenoTexts[idx], needVerify[v], similarity);
|
|
1671
|
+
// LEARN: cache individual word translations for future reuse
|
|
1672
|
+
// Compare steno words with their Chinese translations word-by-word
|
|
1673
|
+
try {
|
|
1674
|
+
const stenoWords = stenoTexts[idx].match(/\b[a-zA-Z]{2,}\b/g) || [];
|
|
1675
|
+
const zhChars = needVerify[v];
|
|
1676
|
+
// For verified batches, cache each original word → its position in the Chinese output
|
|
1677
|
+
// This is approximate but the word TM grows more accurate over time
|
|
1678
|
+
for (const sw of stenoWords) {
|
|
1679
|
+
if (!isPassthrough(sw) && !wordTMLookup(sw) && sw.length >= 3) {
|
|
1680
|
+
// The whole batch verified — individual words are likely correct too
|
|
1681
|
+
// We'll get the exact mapping on the next single-word encounter
|
|
1682
|
+
// For now, mark that this word CAN be translated (not passthrough)
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
} catch { /* non-critical learning */ }
|
|
1197
1686
|
} else {
|
|
1198
1687
|
// Failed verification — stick with steno-only
|
|
1199
1688
|
stenoOnlyCount++;
|
|
@@ -1204,6 +1693,15 @@ async function compressMessagesLive(messages) {
|
|
|
1204
1693
|
if (learned > 0) {
|
|
1205
1694
|
log('info', `LEARN-SYN: ${learned} new synonym pairs from "${original.slice(0, 40)}..."`);
|
|
1206
1695
|
}
|
|
1696
|
+
// LEARN: identify which words caused the failure and record passthrough
|
|
1697
|
+
// Words in original that vanished entirely in back-translation are trouble
|
|
1698
|
+
const origWordSet = new Set(origWords.map(w => w.toLowerCase()));
|
|
1699
|
+
const backWordSet = new Set(backWords.map(w => w.toLowerCase()));
|
|
1700
|
+
for (const ow of origWordSet) {
|
|
1701
|
+
if (!backWordSet.has(ow) && ow.length >= 3 && !FILLER_WORDS.has(ow)) {
|
|
1702
|
+
recordPassthroughFailure(ow);
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1207
1705
|
log('info', `VERIFY-FAIL: sim=${similarity.toFixed(2)} | orig="${original.slice(0, 60)}..." | back="${backEn.slice(0, 60)}..."`);
|
|
1208
1706
|
}
|
|
1209
1707
|
}
|
|
@@ -1438,6 +1936,7 @@ function forwardRequest(req, res, bodyBuffer) {
|
|
|
1438
1936
|
}
|
|
1439
1937
|
|
|
1440
1938
|
async function handleRequest(req, res) {
|
|
1939
|
+
_lastRequestTime = Date.now();
|
|
1441
1940
|
// Health/stats endpoint
|
|
1442
1941
|
if (req.url === '/health' || req.url === '/stats') {
|
|
1443
1942
|
const tm = _loadTM();
|
|
@@ -1449,11 +1948,44 @@ async function handleRequest(req, res) {
|
|
|
1449
1948
|
return;
|
|
1450
1949
|
}
|
|
1451
1950
|
|
|
1452
|
-
// Preview endpoint —
|
|
1453
|
-
if (req.url === '/preview') {
|
|
1951
|
+
// Preview endpoint — compressed request history (supports ?since= for incremental fetch)
|
|
1952
|
+
if (req.url === '/preview' || req.url?.startsWith('/preview?')) {
|
|
1953
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1954
|
+
const url = new URL(req.url, 'http://localhost');
|
|
1955
|
+
const since = url.searchParams?.get('since'); // ISO timestamp — only return entries newer than this
|
|
1956
|
+
let entries = recentRequests;
|
|
1957
|
+
if (since) {
|
|
1958
|
+
entries = entries.filter(e => e.timestamp > since);
|
|
1959
|
+
}
|
|
1960
|
+
const latest = entries.length > 0 ? entries[entries.length - 1] : null;
|
|
1961
|
+
res.end(JSON.stringify({ preview: latest, history: entries, count: recentRequests.length }));
|
|
1962
|
+
return;
|
|
1963
|
+
}
|
|
1964
|
+
|
|
1965
|
+
// Reset endpoint — clears stale preview history, stats, caches on new console session
|
|
1966
|
+
if (req.url === '/reset' && req.method === 'POST') {
|
|
1967
|
+
recentRequests.length = 0;
|
|
1968
|
+
_sysPromptCache.clear();
|
|
1969
|
+
_seenSysPromptHash = null;
|
|
1970
|
+
_sysPromptSendCount = 0;
|
|
1971
|
+
_compactionsSinceLastInject = 0;
|
|
1972
|
+
_lastRequestTime = Date.now(); // reset to now, not 0 (0 would trip orphan watchdog)
|
|
1973
|
+
Object.assign(stats, {
|
|
1974
|
+
requests: 0, compressed: 0, passthrough: 0, bytesOriginal: 0, bytesCompressed: 0,
|
|
1975
|
+
toolBlocksStripped: 0, toolCharsRemoved: 0, sysPromptsDeduped: 0, systemRemindersStripped: 0,
|
|
1976
|
+
liveCompressed: 0, liveBlocksCompressed: 0, liveCharsCompressed: 0,
|
|
1977
|
+
zhVerified: 0, zhRejected: 0, zhSkipped: 0,
|
|
1978
|
+
});
|
|
1454
1979
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1455
|
-
|
|
1456
|
-
|
|
1980
|
+
res.end(JSON.stringify({ reset: true }));
|
|
1981
|
+
return;
|
|
1982
|
+
}
|
|
1983
|
+
|
|
1984
|
+
// Shutdown endpoint — console calls this on exit to kill the daemon
|
|
1985
|
+
if (req.url === '/shutdown' && req.method === 'POST') {
|
|
1986
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1987
|
+
res.end(JSON.stringify({ shutdown: true }));
|
|
1988
|
+
setTimeout(() => process.exit(0), 100);
|
|
1457
1989
|
return;
|
|
1458
1990
|
}
|
|
1459
1991
|
|
|
@@ -1476,8 +2008,10 @@ async function handleRequest(req, res) {
|
|
|
1476
2008
|
if (body.OLD_STRIP_ENABLED != null) liveConfig.OLD_STRIP_ENABLED = !!body.OLD_STRIP_ENABLED;
|
|
1477
2009
|
if (body.SYSTEM_PROMPT_COMPRESS != null) liveConfig.SYSTEM_PROMPT_COMPRESS = !!body.SYSTEM_PROMPT_COMPRESS;
|
|
1478
2010
|
if (body.SYSTEM_REMINDER_STRIPPING != null) liveConfig.SYSTEM_REMINDER_STRIPPING = !!body.SYSTEM_REMINDER_STRIPPING;
|
|
2011
|
+
if (body.SYS_PROMPT_DEDUP != null) { _sysPromptDedup = !!body.SYS_PROMPT_DEDUP; if (!_sysPromptDedup) _seenSysPromptHash = null; }
|
|
2012
|
+
if (body.RESET_SYS_PROMPT_HASH) _seenSysPromptHash = null; // force next request to send full sys prompt
|
|
1479
2013
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1480
|
-
res.end(JSON.stringify({ ok: true, ...liveConfig }));
|
|
2014
|
+
res.end(JSON.stringify({ ok: true, ...liveConfig, SYS_PROMPT_DEDUP: _sysPromptDedup, seenSysPromptHash: _seenSysPromptHash }));
|
|
1481
2015
|
} catch (e) {
|
|
1482
2016
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
1483
2017
|
res.end(JSON.stringify({ error: e.message }));
|
|
@@ -1508,6 +2042,49 @@ async function handleRequest(req, res) {
|
|
|
1508
2042
|
return;
|
|
1509
2043
|
}
|
|
1510
2044
|
|
|
2045
|
+
// Custom system prompt — user-editable replacement for OG system prompt
|
|
2046
|
+
if (req.url === '/custom-system-prompt') {
|
|
2047
|
+
if (req.method === 'GET') {
|
|
2048
|
+
// Return current custom prompt + OG prompt text for modal pre-population
|
|
2049
|
+
const ogText = stats._lastSystemPrompt?.captured ? stats._lastSystemPrompt.preview : null;
|
|
2050
|
+
// Also send full OG prompt if we have it cached
|
|
2051
|
+
const fullOg = stats._lastSystemPromptFull || null;
|
|
2052
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
2053
|
+
res.end(JSON.stringify({
|
|
2054
|
+
customPrompt: _customSystemPrompt?.prompt || null,
|
|
2055
|
+
ogHash: _customSystemPrompt?.ogHash || _seenSysPromptHash || null,
|
|
2056
|
+
ogPrompt: fullOg,
|
|
2057
|
+
ogPreview: ogText,
|
|
2058
|
+
hasCustom: !!_customSystemPrompt,
|
|
2059
|
+
activeMode: _customSystemPrompt ? 'custom' : 'original',
|
|
2060
|
+
}));
|
|
2061
|
+
return;
|
|
2062
|
+
}
|
|
2063
|
+
if (req.method === 'POST') {
|
|
2064
|
+
try {
|
|
2065
|
+
const body = JSON.parse((await collectBody(req)).toString('utf8'));
|
|
2066
|
+
if (body.reset) {
|
|
2067
|
+
_customSystemPrompt = null;
|
|
2068
|
+
_saveCustomSysPrompt(null);
|
|
2069
|
+
log('info', 'Custom system prompt RESET to OG');
|
|
2070
|
+
pushEvent('info', 'Custom system prompt reset to OG');
|
|
2071
|
+
} else if (body.prompt && typeof body.prompt === 'string') {
|
|
2072
|
+
const ogHash = body.ogHash || _seenSysPromptHash;
|
|
2073
|
+
_customSystemPrompt = { prompt: body.prompt, ogHash, savedAt: new Date().toISOString() };
|
|
2074
|
+
_saveCustomSysPrompt(_customSystemPrompt);
|
|
2075
|
+
log('info', `Custom system prompt SAVED (${body.prompt.length} chars, ogHash=${ogHash?.slice(0, 8)})`);
|
|
2076
|
+
pushEvent('info', `Custom sys prompt saved: ${body.prompt.length} chars`);
|
|
2077
|
+
}
|
|
2078
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
2079
|
+
res.end(JSON.stringify({ ok: true, hasCustom: !!_customSystemPrompt }));
|
|
2080
|
+
} catch (e) {
|
|
2081
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
2082
|
+
res.end(JSON.stringify({ error: e.message }));
|
|
2083
|
+
}
|
|
2084
|
+
return;
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
2087
|
+
|
|
1511
2088
|
// Toggle endpoint — pause/unpause proxy
|
|
1512
2089
|
if (req.url === '/toggle' && req.method === 'POST') {
|
|
1513
2090
|
proxyPaused = !proxyPaused;
|
|
@@ -1533,6 +2110,16 @@ async function handleRequest(req, res) {
|
|
|
1533
2110
|
return;
|
|
1534
2111
|
}
|
|
1535
2112
|
|
|
2113
|
+
// POST /expect-compaction — precompact hook signals next request should strip aggressively
|
|
2114
|
+
if (req.url === '/expect-compaction' && req.method === 'POST') {
|
|
2115
|
+
_expectCompaction = true;
|
|
2116
|
+
_compactionSignalTime = Date.now();
|
|
2117
|
+
log('info', 'COMPACTION-SIGNAL: Precompact hook signaled — next request gets aggressive stripping');
|
|
2118
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
2119
|
+
res.end(JSON.stringify({ ok: true, armed: true }));
|
|
2120
|
+
return;
|
|
2121
|
+
}
|
|
2122
|
+
|
|
1536
2123
|
// --- Multi-Project Registry Endpoints ---
|
|
1537
2124
|
|
|
1538
2125
|
// POST /register — register a project with the daemon
|
|
@@ -1635,7 +2222,7 @@ async function handleRequest(req, res) {
|
|
|
1635
2222
|
const messageCount = body.messages?.length || 0;
|
|
1636
2223
|
const dontCompress = hasDontCompressFlag(body);
|
|
1637
2224
|
|
|
1638
|
-
// Capture system prompt info for dashboard
|
|
2225
|
+
// Capture system prompt info for dashboard + modal editor
|
|
1639
2226
|
if (body.system) {
|
|
1640
2227
|
const sysStr = typeof body.system === 'string' ? body.system
|
|
1641
2228
|
: Array.isArray(body.system) ? body.system.map(b => typeof b === 'string' ? b : b?.text || '').join('')
|
|
@@ -1649,58 +2236,114 @@ async function handleRequest(req, res) {
|
|
|
1649
2236
|
timestamp: new Date().toISOString(),
|
|
1650
2237
|
preview: sysStr.slice(0, 300),
|
|
1651
2238
|
};
|
|
2239
|
+
// Store full text for the Customize System Prompt modal
|
|
2240
|
+
stats._lastSystemPromptFull = sysStr;
|
|
2241
|
+
// Persist OG prompt to disk so modal works immediately on restart
|
|
2242
|
+
const _ogHash = createHash('md5').update(sysStr).digest('hex');
|
|
2243
|
+
if (_ogHash !== _seenSysPromptHash || !existsSync(OG_SYS_PROMPT_FILE)) {
|
|
2244
|
+
_saveOgSysPrompt(sysStr, _ogHash);
|
|
2245
|
+
}
|
|
1652
2246
|
}
|
|
1653
2247
|
|
|
1654
2248
|
pushEvent('info', `POST /v1/messages model=${body.model || '?'} msgs=${messageCount} size=${(originalSize / 1024).toFixed(0)}KB`);
|
|
1655
2249
|
|
|
1656
|
-
|
|
2250
|
+
// Check compaction: either heuristic detection OR precompact hook signaled it
|
|
2251
|
+
let isCompaction = isCompactionRequest(body);
|
|
2252
|
+
if (!isCompaction && _expectCompaction && (Date.now() - _compactionSignalTime < 30000)) {
|
|
2253
|
+
isCompaction = true;
|
|
2254
|
+
_expectCompaction = false;
|
|
2255
|
+
log('info', '=== COMPACTION DETECTED (via precompact hook signal) === msgs=' + messageCount);
|
|
2256
|
+
} else if (_expectCompaction && (Date.now() - _compactionSignalTime >= 30000)) {
|
|
2257
|
+
_expectCompaction = false; // stale signal — expired after 30s
|
|
2258
|
+
} else if (isCompaction) {
|
|
2259
|
+
_expectCompaction = false; // clear flag if heuristic also caught it
|
|
2260
|
+
}
|
|
1657
2261
|
// No passthrough — always process everything (system-reminder strip + steno + translate)
|
|
1658
2262
|
const isPassthrough = false;
|
|
1659
2263
|
let sysPromptModified = false;
|
|
1660
2264
|
|
|
1661
|
-
// === SYSTEM PROMPT
|
|
1662
|
-
//
|
|
1663
|
-
//
|
|
1664
|
-
|
|
1665
|
-
|
|
2265
|
+
// === SYSTEM PROMPT MANAGEMENT ===
|
|
2266
|
+
// Strategy:
|
|
2267
|
+
// 1. FIRST request (or after reset): ALWAYS inject custom/optimized sys prompt
|
|
2268
|
+
// 2. Next requests: strip sys prompt (already sent)
|
|
2269
|
+
// 3. Every N compactions: re-inject custom sys prompt (Claude forgets after compaction)
|
|
2270
|
+
// 4. If no custom prompt exists: auto-generate optimized one (Traditional Chinese + SpecMem tools)
|
|
2271
|
+
if (body.system) {
|
|
1666
2272
|
const _sysKey = typeof body.system === 'string' ? body.system
|
|
1667
2273
|
: Array.isArray(body.system) ? body.system.map(b => typeof b === 'string' ? b : (b?.text || '')).join('')
|
|
1668
2274
|
: JSON.stringify(body.system);
|
|
1669
|
-
const _sysHash = createHash('md5').update(_sysKey).digest('hex');
|
|
1670
|
-
|
|
2275
|
+
const _sysHash = createHash('md5').update(_sysKey).digest('hex').slice(0, 16);
|
|
2276
|
+
|
|
2277
|
+
// ── Session boundary detection ──
|
|
2278
|
+
// MCP server outlives Claude CLI sessions. Detect new session by time gap
|
|
2279
|
+
// so system prompt always shows on first user submit of every session.
|
|
2280
|
+
const now = Date.now();
|
|
2281
|
+
if (_lastRequestTime > 0 && (now - _lastRequestTime) >= SESSION_GAP_MS) {
|
|
2282
|
+
log('info', `SESSION BOUNDARY detected (${((now - _lastRequestTime) / 1000).toFixed(0)}s gap) — resetting sys prompt state`);
|
|
2283
|
+
_sysPromptSendCount = 0;
|
|
2284
|
+
_compactionsSinceLastInject = 0;
|
|
2285
|
+
// Keep _seenSysPromptHash so OG prompt isn't re-saved
|
|
2286
|
+
}
|
|
2287
|
+
_lastRequestTime = now;
|
|
2288
|
+
|
|
2289
|
+
// Save OG prompt on first sight
|
|
2290
|
+
if (!_seenSysPromptHash) {
|
|
2291
|
+
_saveOgSysPrompt(_sysKey, _sysHash);
|
|
2292
|
+
}
|
|
1671
2293
|
|
|
1672
|
-
if
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
2294
|
+
// Determine if we should inject custom sys prompt this request
|
|
2295
|
+
const shouldInject = (
|
|
2296
|
+
_sysPromptSendCount === 0 || // first request of session (or after session boundary reset)
|
|
2297
|
+
(isCompaction && ++_compactionsSinceLastInject >= SYS_PROMPT_REINJECT_INTERVAL) // every 3rd compaction
|
|
2298
|
+
);
|
|
2299
|
+
|
|
2300
|
+
if (shouldInject) {
|
|
2301
|
+
_compactionsSinceLastInject = 0;
|
|
2302
|
+
_sysPromptSendCount++;
|
|
2303
|
+
|
|
2304
|
+
// Get custom prompt — either user-edited or auto-generated optimized
|
|
2305
|
+
let customPrompt = _customSystemPrompt?.prompt;
|
|
2306
|
+
if (!customPrompt && liveConfig.SYSTEM_PROMPT_COMPRESS) {
|
|
2307
|
+
// Auto-generate: inject SpecMem tool awareness into original prompt
|
|
2308
|
+
customPrompt = _sysKey + '\n\n# SpecMem Integration\nYou have access to SpecMem persistent memory tools (find_memory, save_memory, find_code_pointers, drill_down). Use these proactively to recall context, search code semantically, and store important findings. Always check memory before starting complex tasks.';
|
|
2309
|
+
}
|
|
2310
|
+
|
|
2311
|
+
if (customPrompt) {
|
|
2312
|
+
const origSize = _sysKey.length;
|
|
2313
|
+
body.system = customPrompt;
|
|
1676
2314
|
sysPromptModified = true;
|
|
1677
|
-
|
|
2315
|
+
_seenSysPromptHash = _sysHash;
|
|
2316
|
+
const newSize = customPrompt.length;
|
|
2317
|
+
const saved = origSize - newSize;
|
|
2318
|
+
if (saved > 0) {
|
|
2319
|
+
stats.sysPromptCharsSaved += saved;
|
|
2320
|
+
stats.tokensStripped += Math.floor(saved / 4);
|
|
2321
|
+
stats.bytesStripped += saved;
|
|
2322
|
+
}
|
|
1678
2323
|
stats.sysPromptCompressed++;
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
2324
|
+
log('info', `SYSPROMPT injected (${shouldInject ? 'first/reinject' : 'custom'}): ${origSize}→${newSize} chars`);
|
|
2325
|
+
pushEvent('dedup', `SYSPROMPT injected: ${(origSize/1024).toFixed(0)}KB→${(newSize/1024).toFixed(0)}KB`);
|
|
2326
|
+
} else {
|
|
2327
|
+
// No custom, no auto-gen — pass through original
|
|
2328
|
+
_seenSysPromptHash = _sysHash;
|
|
2329
|
+
// NOTE: don't increment _sysPromptSendCount here — already incremented at line 2288
|
|
2330
|
+
log('info', `SYSPROMPT passthrough (no custom): ${(_sysKey.length/1024).toFixed(0)}KB`);
|
|
1683
2331
|
}
|
|
1684
|
-
} else if (
|
|
1685
|
-
//
|
|
1686
|
-
|
|
2332
|
+
} else if (_seenSysPromptHash === _sysHash) {
|
|
2333
|
+
// Already sent, strip to save tokens
|
|
2334
|
+
const sysSize = _sysKey.length;
|
|
2335
|
+
delete body.system;
|
|
2336
|
+
sysPromptModified = true;
|
|
2337
|
+
stats.sysPromptCharsSaved += sysSize;
|
|
2338
|
+
stats.sysPromptCompressed++;
|
|
2339
|
+
stats.tokensStripped += Math.floor(sysSize / 4);
|
|
2340
|
+
stats.bytesStripped += sysSize;
|
|
2341
|
+
pushEvent('dedup', `SYSPROMPT stripped (same hash): -${(sysSize/1024).toFixed(0)}KB`);
|
|
1687
2342
|
} else {
|
|
1688
|
-
//
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
body.system = sysResult.system;
|
|
1693
|
-
sysPromptModified = true;
|
|
1694
|
-
stats.sysPromptCharsSaved += sysResult.charsSaved;
|
|
1695
|
-
stats.sysPromptCompressed++;
|
|
1696
|
-
stats.tokensStripped += Math.floor(sysResult.charsSaved / 4);
|
|
1697
|
-
stats.bytesStripped += sysResult.charsSaved;
|
|
1698
|
-
log('compress', `SYSPROMPT: ${sysResult.charsSaved} chars saved`);
|
|
1699
|
-
pushEvent('compress', `System prompt: -${sysResult.charsSaved} chars`);
|
|
1700
|
-
}
|
|
1701
|
-
} catch (e) {
|
|
1702
|
-
log('warn', `System prompt compression failed: ${e.message}`);
|
|
1703
|
-
}
|
|
2343
|
+
// Different sys prompt (changed by Claude Code) — send it, update hash
|
|
2344
|
+
_seenSysPromptHash = _sysHash;
|
|
2345
|
+
_sysPromptSendCount++;
|
|
2346
|
+
log('info', `SYSPROMPT changed hash ${_sysHash} — sending full`);
|
|
1704
2347
|
}
|
|
1705
2348
|
}
|
|
1706
2349
|
|
|
@@ -2011,7 +2654,7 @@ function setPaused(state) {
|
|
|
2011
2654
|
/**
|
|
2012
2655
|
* Check if the daemon is responding on its health endpoint.
|
|
2013
2656
|
*/
|
|
2014
|
-
function checkDaemonHealth() {
|
|
2657
|
+
export function checkDaemonHealth() {
|
|
2015
2658
|
return new Promise((resolve) => {
|
|
2016
2659
|
const req = httpGet(`http://127.0.0.1:${PROXY_PORT}/health`, { timeout: 2000 }, (res) => {
|
|
2017
2660
|
let data = '';
|
|
@@ -2125,7 +2768,7 @@ export function getCompactionProxyStats() {
|
|
|
2125
2768
|
* Register this project with the running daemon.
|
|
2126
2769
|
* Fire-and-forget — if daemon isn't running yet, fails silently.
|
|
2127
2770
|
*/
|
|
2128
|
-
function registerWithDaemon(projectPath, pid) {
|
|
2771
|
+
export function registerWithDaemon(projectPath, pid) {
|
|
2129
2772
|
if (!projectPath) return;
|
|
2130
2773
|
const body = JSON.stringify({ projectPath, pid: pid || process.pid });
|
|
2131
2774
|
try {
|
|
@@ -2176,6 +2819,10 @@ function deregisterFromDaemon(projectPath) {
|
|
|
2176
2819
|
/**
|
|
2177
2820
|
* Check if a PID is alive using signal 0.
|
|
2178
2821
|
*/
|
|
2822
|
+
function getLastRequestTime() {
|
|
2823
|
+
return _lastRequestTime;
|
|
2824
|
+
}
|
|
2825
|
+
|
|
2179
2826
|
function isPidAlive(pid) {
|
|
2180
2827
|
if (!pid || pid <= 0) return false;
|
|
2181
2828
|
try {
|
|
@@ -2243,4 +2890,5 @@ export {
|
|
|
2243
2890
|
DISABLED_FILE,
|
|
2244
2891
|
LOG_FILE,
|
|
2245
2892
|
CLAUDE_DIR,
|
|
2893
|
+
getLastRequestTime,
|
|
2246
2894
|
};
|