@a1hvdy/cc-openclaw 0.29.0 → 0.30.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/lib/cache-parity-decide.d.ts +64 -0
- package/dist/src/lib/cache-parity-decide.js +54 -0
- package/dist/src/lib/index.d.ts +7 -0
- package/dist/src/lib/index.js +10 -0
- package/dist/src/observability/perf-telemetry.d.ts +1 -1
- package/dist/src/session-bootstrap/cwd-patch.js +61 -1
- package/package.json +1 -1
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Content-addressed cache-parity decision (v0.30.0).
|
|
3
|
+
*
|
|
4
|
+
* Problem (diagnosed 2026-05-23 from the live sysprompt-cost telemetry):
|
|
5
|
+
* cc-openclaw's cache-parity registry is keyed by sessionKey. The cache HIT
|
|
6
|
+
* path strips the role:system messages so the Claude CLI subprocess reuses its
|
|
7
|
+
* already-cached `--append-system-prompt` block; a MISS inlines the full ~7K
|
|
8
|
+
* system prompt into the user message (uncached). On the live box, 70% of all
|
|
9
|
+
* cache misses were `session_unknown` — the FIRST turn of a session, where the
|
|
10
|
+
* registry has no entry yet. Telegram conversations are short (median 1 turn /
|
|
11
|
+
* session, 23 of 35 single-turn), so cold-start dominated: the hit rate stalled
|
|
12
|
+
* at ~75% vs the terminal CLI's ~95%. The dynamic-envelope churn we originally
|
|
13
|
+
* set out to fix was only ~7.5% of turns.
|
|
14
|
+
*
|
|
15
|
+
* Key observation: every Savvy session shares the *identical* system prefix
|
|
16
|
+
* (same SOUL/USER/AGENTS/TOOLS/MEMORY + harness ⇒ same sysHash). So a brand-new
|
|
17
|
+
* session whose sysHash was already seen for some *other* session is a
|
|
18
|
+
* known-good prefix — its `--append-system-prompt` will be injected at
|
|
19
|
+
* startSession from the registry entry the route patch writes this same turn,
|
|
20
|
+
* so it is SAFE to strip the redundant inline and ride the cached path on
|
|
21
|
+
* turn 1 instead of re-billing the full prompt.
|
|
22
|
+
*
|
|
23
|
+
* Safety: the "warm-hash hit" only applies when the session is NEW (not yet in
|
|
24
|
+
* the SessionManager) — that guarantees startSession runs and appends the
|
|
25
|
+
* prompt. An EXISTING session missing its registry entry (e.g. registry wiped
|
|
26
|
+
* mid-life) keeps the legacy inline path so the model never loses its system
|
|
27
|
+
* prompt. A `hash_mismatch` (entry exists, different hash = genuine mid-session
|
|
28
|
+
* churn) also stays on the inline path: the CLI's append still holds the OLD
|
|
29
|
+
* prompt, so the new one must be delivered in-band.
|
|
30
|
+
*
|
|
31
|
+
* Pure + side-effect-free so the decision is unit-testable independent of the
|
|
32
|
+
* EmbeddedServer route closure (matches the codebase's pure-helper pattern:
|
|
33
|
+
* isPersistedSessionFresh, shouldWriteThroughResumeId).
|
|
34
|
+
*/
|
|
35
|
+
export type CacheParityAction = 'hit' | 'warm-hash-hit' | 'miss';
|
|
36
|
+
export interface CacheParityDecisionInput {
|
|
37
|
+
/** Registry entry for THIS sessionKey, if any. */
|
|
38
|
+
entry: {
|
|
39
|
+
hash: string;
|
|
40
|
+
} | undefined;
|
|
41
|
+
/** sha1 of the (stripped) system content for this turn. */
|
|
42
|
+
sysHash: string;
|
|
43
|
+
/** True if sysHash has been seen for ANY session this process (known-good prefix). */
|
|
44
|
+
knownHash: boolean;
|
|
45
|
+
/**
|
|
46
|
+
* True if the SessionManager has no live session for this key yet. A new
|
|
47
|
+
* session guarantees startSession runs and injects appendSystemPrompt from
|
|
48
|
+
* the registry, which is what makes stripping the inline safe.
|
|
49
|
+
*/
|
|
50
|
+
sessionIsNew: boolean;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Decide how the route patch should treat the system prompt this turn.
|
|
54
|
+
*
|
|
55
|
+
* - 'hit' → registry entry matches this session: strip role:system,
|
|
56
|
+
* ride the already-cached append.
|
|
57
|
+
* - 'warm-hash-hit' → new session + known-good prefix: write the registry
|
|
58
|
+
* entry (so startSession appends it), strip role:system,
|
|
59
|
+
* ride the cached path. Closes the cold-start gap.
|
|
60
|
+
* - 'miss' → inline the system prompt into the user message (the safe
|
|
61
|
+
* legacy path): first-ever prefix, genuine churn, or an
|
|
62
|
+
* existing session that lost its registry entry.
|
|
63
|
+
*/
|
|
64
|
+
export declare function decideCacheParityAction(input: CacheParityDecisionInput): CacheParityAction;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Content-addressed cache-parity decision (v0.30.0).
|
|
3
|
+
*
|
|
4
|
+
* Problem (diagnosed 2026-05-23 from the live sysprompt-cost telemetry):
|
|
5
|
+
* cc-openclaw's cache-parity registry is keyed by sessionKey. The cache HIT
|
|
6
|
+
* path strips the role:system messages so the Claude CLI subprocess reuses its
|
|
7
|
+
* already-cached `--append-system-prompt` block; a MISS inlines the full ~7K
|
|
8
|
+
* system prompt into the user message (uncached). On the live box, 70% of all
|
|
9
|
+
* cache misses were `session_unknown` — the FIRST turn of a session, where the
|
|
10
|
+
* registry has no entry yet. Telegram conversations are short (median 1 turn /
|
|
11
|
+
* session, 23 of 35 single-turn), so cold-start dominated: the hit rate stalled
|
|
12
|
+
* at ~75% vs the terminal CLI's ~95%. The dynamic-envelope churn we originally
|
|
13
|
+
* set out to fix was only ~7.5% of turns.
|
|
14
|
+
*
|
|
15
|
+
* Key observation: every Savvy session shares the *identical* system prefix
|
|
16
|
+
* (same SOUL/USER/AGENTS/TOOLS/MEMORY + harness ⇒ same sysHash). So a brand-new
|
|
17
|
+
* session whose sysHash was already seen for some *other* session is a
|
|
18
|
+
* known-good prefix — its `--append-system-prompt` will be injected at
|
|
19
|
+
* startSession from the registry entry the route patch writes this same turn,
|
|
20
|
+
* so it is SAFE to strip the redundant inline and ride the cached path on
|
|
21
|
+
* turn 1 instead of re-billing the full prompt.
|
|
22
|
+
*
|
|
23
|
+
* Safety: the "warm-hash hit" only applies when the session is NEW (not yet in
|
|
24
|
+
* the SessionManager) — that guarantees startSession runs and appends the
|
|
25
|
+
* prompt. An EXISTING session missing its registry entry (e.g. registry wiped
|
|
26
|
+
* mid-life) keeps the legacy inline path so the model never loses its system
|
|
27
|
+
* prompt. A `hash_mismatch` (entry exists, different hash = genuine mid-session
|
|
28
|
+
* churn) also stays on the inline path: the CLI's append still holds the OLD
|
|
29
|
+
* prompt, so the new one must be delivered in-band.
|
|
30
|
+
*
|
|
31
|
+
* Pure + side-effect-free so the decision is unit-testable independent of the
|
|
32
|
+
* EmbeddedServer route closure (matches the codebase's pure-helper pattern:
|
|
33
|
+
* isPersistedSessionFresh, shouldWriteThroughResumeId).
|
|
34
|
+
*/
|
|
35
|
+
/**
|
|
36
|
+
* Decide how the route patch should treat the system prompt this turn.
|
|
37
|
+
*
|
|
38
|
+
* - 'hit' → registry entry matches this session: strip role:system,
|
|
39
|
+
* ride the already-cached append.
|
|
40
|
+
* - 'warm-hash-hit' → new session + known-good prefix: write the registry
|
|
41
|
+
* entry (so startSession appends it), strip role:system,
|
|
42
|
+
* ride the cached path. Closes the cold-start gap.
|
|
43
|
+
* - 'miss' → inline the system prompt into the user message (the safe
|
|
44
|
+
* legacy path): first-ever prefix, genuine churn, or an
|
|
45
|
+
* existing session that lost its registry entry.
|
|
46
|
+
*/
|
|
47
|
+
export function decideCacheParityAction(input) {
|
|
48
|
+
const { entry, sysHash, knownHash, sessionIsNew } = input;
|
|
49
|
+
if (entry && entry.hash === sysHash)
|
|
50
|
+
return 'hit';
|
|
51
|
+
if (!entry && knownHash && sessionIsNew)
|
|
52
|
+
return 'warm-hash-hit';
|
|
53
|
+
return 'miss';
|
|
54
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export * from './register-guard.js';
|
|
2
|
+
export { registerOnce } from './register-guard.js';
|
|
3
|
+
export { stripSysprompt, isStripEnabled, type StripOptions, type StripResult } from './sysprompt-strip.js';
|
|
4
|
+
export { isCacheParityEnabled, hashPrompt, recordAttachment, readRegistry, REGISTRY_PATH, type RegistryEntry, } from './cache-parity.js';
|
|
5
|
+
export { selectEngine, isCcOpenclawEnabled, captureSessionRoute, ACTIVE_FLAG_ENV, ROUTE_FLAG_ENV, type Engine, type SessionRoute, } from './config-service.js';
|
|
6
|
+
export { isTestMode, TEST_MODE_ENV, _setTestModeForTests } from './test-mode.js';
|
|
7
|
+
export { getAggressiveStripEnabled, getCacheParityEnabled, getLogLevel, isLogLevelDebug, } from './config.js';
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export * from './register-guard.js';
|
|
2
|
+
export { registerOnce } from './register-guard.js';
|
|
3
|
+
export { stripSysprompt, isStripEnabled } from './sysprompt-strip.js';
|
|
4
|
+
export { isCacheParityEnabled, hashPrompt, recordAttachment, readRegistry, REGISTRY_PATH, } from './cache-parity.js';
|
|
5
|
+
// Engine routing — originally `./route-flag.js`; collapsed into
|
|
6
|
+
// `./config-service.js` at Cluster A step 8. Same API, same semantics,
|
|
7
|
+
// single source of truth.
|
|
8
|
+
export { selectEngine, isCcOpenclawEnabled, captureSessionRoute, ACTIVE_FLAG_ENV, ROUTE_FLAG_ENV, } from './config-service.js';
|
|
9
|
+
export { isTestMode, TEST_MODE_ENV, _setTestModeForTests } from './test-mode.js';
|
|
10
|
+
export { getAggressiveStripEnabled, getCacheParityEnabled, getLogLevel, isLogLevelDebug, } from './config.js';
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
* break the single `jq` pipeline. The `event` field makes filtering trivial.
|
|
28
28
|
*/
|
|
29
29
|
type PerfEventName = 'cache_check' | 'first_byte' | 'turn_end';
|
|
30
|
-
type CacheCheckCause = 'hit' | 'registry_empty' | 'hash_mismatch' | 'session_unknown' | 'disabled';
|
|
30
|
+
type CacheCheckCause = 'hit' | 'warm_hash' | 'registry_empty' | 'hash_mismatch' | 'session_unknown' | 'disabled';
|
|
31
31
|
interface PerfEventBase {
|
|
32
32
|
event: PerfEventName;
|
|
33
33
|
sessionKey?: string;
|
|
@@ -37,6 +37,7 @@ import { defaultRegisterGuard } from '../lib/register-guard.js';
|
|
|
37
37
|
import { isTestMode } from '../lib/test-mode.js';
|
|
38
38
|
import { writePerfEvent } from '../observability/perf-telemetry.js';
|
|
39
39
|
import { collapseSkillList } from '../lib/perf/skill-list-collapse.js';
|
|
40
|
+
import { decideCacheParityAction } from '../lib/cache-parity-decide.js';
|
|
40
41
|
import { isCacheParityTrackB, isTokenTelemetryEnabled, isSyspromptDumpEnabled, getMaxConcurrentSessions, getSessionTtlMinutes, ensureUxBridgeAllSessionsDefault, } from '../lib/config.js';
|
|
41
42
|
import { VENDOR_FILES } from '../lib/vendor-paths.js';
|
|
42
43
|
import { OpenClawConfigSchema, findMainAgent, getAgentPrimaryModel, getDefaultsPrimaryModel, isClaudeRoutedModel, } from '../types/upstream.js';
|
|
@@ -99,6 +100,7 @@ const METRICS = {
|
|
|
99
100
|
systemPromptInlined: 0,
|
|
100
101
|
uxMetaSeeded: 0,
|
|
101
102
|
cacheParityHits: 0,
|
|
103
|
+
cacheParityWarmHashHits: 0,
|
|
102
104
|
cacheParityMisses: 0,
|
|
103
105
|
cacheParityRegistryWrites: 0,
|
|
104
106
|
cacheParityAppendInjections: 0,
|
|
@@ -143,6 +145,24 @@ function _setSystemInlineCache(key, val) {
|
|
|
143
145
|
}
|
|
144
146
|
_systemInlineCache.set(key, val);
|
|
145
147
|
}
|
|
148
|
+
// ── Known sysprompt-hash set (v0.30.0 — content-addressed cache parity) ──────
|
|
149
|
+
// Cross-session record of every sysHash written to the cache-parity registry
|
|
150
|
+
// this process. Lets a brand-new session recognise the shared Savvy system
|
|
151
|
+
// prefix and ride the cached append path on turn 1 instead of inlining the full
|
|
152
|
+
// ~7K prompt — the dominant cold-start miss (see lib/cache-parity-decide.ts).
|
|
153
|
+
// Bounded FIFO: distinct prefixes are few (one per build), 64 is ample headroom.
|
|
154
|
+
const _knownSysHashes = new Set();
|
|
155
|
+
const KNOWN_SYS_HASH_MAX = 64;
|
|
156
|
+
function _rememberSysHash(hash) {
|
|
157
|
+
if (_knownSysHashes.has(hash))
|
|
158
|
+
return;
|
|
159
|
+
if (_knownSysHashes.size >= KNOWN_SYS_HASH_MAX) {
|
|
160
|
+
const oldest = _knownSysHashes.values().next().value;
|
|
161
|
+
if (oldest !== undefined)
|
|
162
|
+
_knownSysHashes.delete(oldest);
|
|
163
|
+
}
|
|
164
|
+
_knownSysHashes.add(hash);
|
|
165
|
+
}
|
|
146
166
|
// ── Tool dump hash guard (v0.6.0 — per-session-key cache + fast-skip) ──
|
|
147
167
|
// Pre-v0.6.0: a single global `_lastToolDumpHash` thrashed when multiple
|
|
148
168
|
// sessions had different tool sets. JSON.stringify + SHA1 ran on EVERY
|
|
@@ -655,11 +675,30 @@ function applyRoutePatch(EmbeddedServer) {
|
|
|
655
675
|
try {
|
|
656
676
|
const reg = _readCacheParityRegistry();
|
|
657
677
|
const entry = reg[sessionKey];
|
|
658
|
-
|
|
678
|
+
// sessionIsNew: no live session in the manager ⇒ startSession will
|
|
679
|
+
// run on this request and inject appendSystemPrompt from the
|
|
680
|
+
// registry entry we write below. That guarantee is what makes the
|
|
681
|
+
// warm-hash strip safe (see lib/cache-parity-decide.ts). Default
|
|
682
|
+
// to false on any access failure — conservative: an unproven
|
|
683
|
+
// append guarantee falls back to the safe inline path.
|
|
684
|
+
let sessionIsNew = false;
|
|
685
|
+
try {
|
|
686
|
+
const mgr = this.manager;
|
|
687
|
+
sessionIsNew = !(mgr?.sessions?.has?.('openai-' + sessionKey) ?? false);
|
|
688
|
+
}
|
|
689
|
+
catch { /* keep sessionIsNew=false (inline fallback) */ }
|
|
690
|
+
const action = decideCacheParityAction({
|
|
691
|
+
entry: entry ? { hash: entry.hash } : undefined,
|
|
692
|
+
sysHash,
|
|
693
|
+
knownHash: _knownSysHashes.has(sysHash),
|
|
694
|
+
sessionIsNew,
|
|
695
|
+
});
|
|
696
|
+
if (action === 'hit') {
|
|
659
697
|
body.messages = messages.filter(m => m?.role !== 'system');
|
|
660
698
|
METRICS.cacheParityHits++;
|
|
661
699
|
METRICS.systemPromptInlined++;
|
|
662
700
|
cacheParityHandled = true;
|
|
701
|
+
_rememberSysHash(sysHash);
|
|
663
702
|
writePerfEvent({
|
|
664
703
|
event: 'cache_check',
|
|
665
704
|
sessionKey,
|
|
@@ -668,8 +707,29 @@ function applyRoutePatch(EmbeddedServer) {
|
|
|
668
707
|
sysHash,
|
|
669
708
|
});
|
|
670
709
|
}
|
|
710
|
+
else if (action === 'warm-hash-hit') {
|
|
711
|
+
// Cold-start win: new session + known-good Savvy prefix. Write
|
|
712
|
+
// the entry so startSession appends it, strip the redundant
|
|
713
|
+
// inline, ride the cached path on turn 1.
|
|
714
|
+
_writeCacheParityEntry(sessionKey, sysHash, sysContent);
|
|
715
|
+
body.messages = messages.filter(m => m?.role !== 'system');
|
|
716
|
+
METRICS.cacheParityWarmHashHits++;
|
|
717
|
+
METRICS.cacheParityRegistryWrites++;
|
|
718
|
+
METRICS.systemPromptInlined++;
|
|
719
|
+
cacheParityHandled = true;
|
|
720
|
+
_rememberSysHash(sysHash);
|
|
721
|
+
logger.info(`${TAG} cache-parity warm-hash hit: session=${sessionKey} hash=${sysHash} sysLen=${sysContent.length} (cold-start dedup; startSession will append)`);
|
|
722
|
+
writePerfEvent({
|
|
723
|
+
event: 'cache_check',
|
|
724
|
+
sessionKey,
|
|
725
|
+
outcome: 'hit',
|
|
726
|
+
cause: 'warm_hash',
|
|
727
|
+
sysHash,
|
|
728
|
+
});
|
|
729
|
+
}
|
|
671
730
|
else {
|
|
672
731
|
_writeCacheParityEntry(sessionKey, sysHash, sysContent);
|
|
732
|
+
_rememberSysHash(sysHash);
|
|
673
733
|
METRICS.cacheParityMisses++;
|
|
674
734
|
METRICS.cacheParityRegistryWrites++;
|
|
675
735
|
logger.info(`${TAG} cache-parity miss: session=${sessionKey} oldHash=${entry?.hash || 'none'} newHash=${sysHash} sysLen=${sysContent.length} (registry updated; next session start will append)`);
|