Skip to content

Commit e6c851b

Browse files
author
SqlRush
committed
Accept whole-number microcompact counts
1 parent 801a939 commit e6c851b

5 files changed

Lines changed: 96 additions & 10 deletions

File tree

‎docs/cc-100-roadmap.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -592,7 +592,7 @@ M7 补充:prompt history `LogEntry` 读取现在接受 `sessionID`/`session`/`
592592

593593
本轮补充:remote history response parser 现在也接受 provider-style `choices`/`outputs`/`candidates`/`generations` wrapper 以及顶层 `message`/`content`/`text` envelope,可从 `message.content`、content-block array 和 `content.parts[].text` 中恢复 event page JSON(包括 fenced `json` code block),并保留 pagination 继续驱动 `before_id` 续抓。
594594

595-
本轮补充:microcompact disk cache loader 现在读取 Go 默认、camelCase、snake_case 和相邻实现常见字段别名/包装形态,包括 `result`/`data`/`cache`/`value` wrapper、`content`/`text` summary、`cacheKey`/`key`/`hash` digest、cache-hit 别名、计数字段别名/数字字符串、RFC3339/Unix 秒/Unix 毫秒时间字段,以及 `createdAt` + `ttlSeconds`/`ttlMs`/`expiresIn`/`maxAge` 等相对 TTL 推导,提升 cached microcompact 文件在不同实现/版本间的恢复率。
595+
本轮补充:microcompact disk cache loader 现在读取 Go 默认、camelCase、snake_case 和相邻实现常见字段别名/包装形态,包括 `result`/`data`/`cache`/`value` wrapper、`content`/`text` summary、`cacheKey`/`key`/`hash` digest、cache-hit 别名、计数字段别名/数字字符串/whole-number JSON number、RFC3339/Unix 秒/Unix 毫秒时间字段,以及 `createdAt` + `ttlSeconds`/`ttlMs`/`expiresIn`/`maxAge` 等相对 TTL 推导,提升 cached microcompact 文件在不同实现/版本间的恢复率;fractional count 仍会被拒绝。
596596

597597
本轮补充:microcompact disk cache loader 现在也接受 `cacheEntry`/`cache_entry`、`micro_compact`/`micro_compact_result` wrapper,以及 `summaryMarkdown`/`resultSummary`/`compressedText`、`cacheDigest`/`digestHash`/`fingerprint`、`summarizedCount`/`retainedCount`、`formatVersion` 和 `ttlMilliseconds`/`expiresInMilliseconds`/`maxAgeMilliseconds` 等相邻实现字段别名。
598598

‎docs/claude-code-go-rewrite-plan.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -314,7 +314,7 @@ test/parity/ # golden tests against TS/official behavior
314314
- 本轮补充:compact runner 的 summary 响应现在也接受 provider-style `choices`/`outputs`/`candidates`/`generations` wrapper,可在构建 compact plan 前从 `message.content`、content-block array、`content.parts[].text` 和 fenced `json` code block 中恢复 visible summary text。
315315
- 本轮补充:remote history `SDKEvent` type 现在会把 provider-style aliases 归一化为现有 canonical 事件类型,包括 `assistant_message`/`assistant_delta`、`userMessage`/`humanMessage`、`system-event`、`result_event`/`finalResult`/`response.completed`、`errorEvent`/`failureEvent` 和 `status_update`/`statusMessage`/`progress`,single-object page 与 transcript materialization 不再因事件类型拼写相邻而丢消息。
316316
- 本轮补充:conversation runner 会在用户消息入队后基于 compact window 计算 token warning state,并在达到 warning/error/auto-compact/blocking 阈值时发出 `token_warning` event;warning state 接入 blocking-limit env override,auto-compact 判断接入既有 `CLAUDE_AUTOCOMPACT_PCT_OVERRIDE`,避免 warning 和 compact 阈值来源分叉。
317-
- 本轮补充:microcompact disk cache loader 现在读取 Go 默认、camelCase 和 snake_case 字段别名,并容忍计数字段的数字字符串,以及 RFC3339、Unix 秒、Unix 毫秒时间字段,提升 cached microcompact 文件在不同实现/版本间的恢复率。
317+
- 本轮补充:microcompact disk cache loader 现在读取 Go 默认、camelCase 和 snake_case 字段别名,并容忍计数字段的数字字符串、whole-number JSON number 和 whole-number 数字字符串,同时继续拒绝 fractional count,以及 RFC3339、Unix 秒、Unix 毫秒时间字段,提升 cached microcompact 文件在不同实现/版本间的恢复率。
318318
- 本轮补充:microcompact disk cache loader 现在接受 `cacheEntry`/`cache_entry`、`micro_compact`/`micro_compact_result` wrapper,以及 `summaryMarkdown`/`resultSummary`/`compressedText`、`cacheDigest`/`digestHash`/`fingerprint`、`summarizedCount`/`retainedCount`、`formatVersion` 和 `ttlMilliseconds`/`expiresInMilliseconds`/`maxAgeMilliseconds` 等相邻实现字段别名。
319319
- 本轮补充:microcompact disk cache loader 继续接受相邻 timestamp/expiry aliases,包括 `cachedAt`、`cacheCreatedAt`、`storedAt`、`generatedAt`、`updatedAt`、`timestamp`、`expiry`、`expirationTime`、`validUntil`、`notAfter`,以及 `timeToLiveSeconds`、`validForMs` 等相对 TTL 字段。
320320
- 本轮补充:microcompact disk cache loader 的相对 TTL 现在也接受分钟、小时和天级别名,例如 `ttlMinutes`、`expiresInHours`、`validForDays` 及 snake/camel 相邻形式,恢复 cached microcompact 过期时间时不再局限于秒/毫秒。

‎docs/first-second-parity-audit.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ M6 progress now includes:
123123

124124
- `internal/memory`: recursive `.md` memory scanning, frontmatter parsing, newest-first capped manifests, root-to-leaf `CLAUDE.md` discovery/loading, and team-memory secret detection.
125125
- `internal/compact`: effective context window calculations, auto-compact thresholds, token warning state, compact summary prompt construction, compact API runner, compact boundary/summary plan generation, auto-compact window env override application, microcompact/cache primitives, persistent cached microcompact storage, structural/rich-content microcompact cache digests, cache version/TTL/prune handling for disk and in-memory caches, memory-cache write-through to disk, atomic cache writes, corrupt-cache fail-open behavior, and filename/digest mismatch pruning.
126-
- `internal/compact`: microcompact disk cache loading now accepts Go default, camelCase, snake_case, and adjacent cache field aliases/wrappers such as `result`/`data`/`cache`/`value` objects, `content`/`text` summary, `cacheKey`/`key`/`hash` digest, cache-hit aliases, numeric strings for count fields, RFC3339/Unix-seconds/Unix-millis time fields, and relative TTL fields such as `ttlSeconds`, `ttlMs`, `expiresIn`, and `maxAge`, so cached microcompact records from adjacent implementations remain usable.
126+
- `internal/compact`: microcompact disk cache loading now accepts Go default, camelCase, snake_case, and adjacent cache field aliases/wrappers such as `result`/`data`/`cache`/`value` objects, `content`/`text` summary, `cacheKey`/`key`/`hash` digest, cache-hit aliases, numeric strings and whole-number JSON/string numbers for count fields, RFC3339/Unix-seconds/Unix-millis time fields, and relative TTL fields such as `ttlSeconds`, `ttlMs`, `expiresIn`, and `maxAge`, so cached microcompact records from adjacent implementations remain usable.
127127
- `internal/compact`: microcompact disk cache loading also accepts adjacent cache-entry wrappers and aliases such as `cacheEntry`/`cache_entry`, `micro_compact`/`micro_compact_result`, `summaryMarkdown`/`resultSummary`/`compressedText`, `cacheDigest`/`digestHash`/`fingerprint`, `summarizedCount`/`retainedCount`, `formatVersion`, and millisecond TTL aliases such as `ttlMilliseconds`, `expiresInMilliseconds`, and `maxAgeMilliseconds`.
128128
- `internal/compact`: microcompact disk cache loading now also accepts adjacent timestamp aliases such as `cachedAt`, `cacheCreatedAt`, `storedAt`, `generatedAt`, `updatedAt`, `timestamp`, `expiry`, `expirationTime`, `validUntil`, `notAfter`, and relative TTL aliases such as `timeToLiveSeconds` and `validForMs`.
129129
- `internal/compact`: microcompact disk cache relative TTL loading now also accepts minute, hour, and day aliases such as `ttlMinutes`, `expiresInHours`, and `validForDays`, including adjacent snake/camel forms.
@@ -551,6 +551,7 @@ M7 progress now includes:
551551
- `internal/session`: prompt-history pasted image loading now accepts content-block-style `source`/`imageSource` objects, nested JSON:API/GraphQL-style source wrappers, data URL aliases, and source URL/URI/cache-path aliases; stored image entries with inline source data or a current-session image-cache file now restore base64 content into prompt image blocks instead of keeping only metadata, including same-ID cache fallback, media-type inference when history metadata omits the original MIME type, symlink escape guards before reading cached image bytes, and restored path re-indexing for later image metadata/source lookup.
552552
- `internal/compact`: microcompact disk cache loading now accepts broader summary-like direct and provider aliases including `body`, `markdown`, `description`/`details`, `finalSummary`, `summaryContent`, `plainText`/`displayText`, `answerText`, `refusal`, `resultText`, `completionText`, `responseText`, and `messageText`; provider summary recovery also follows wrapper-only `value`/`payload`/`data` chains without failing on non-summary metadata, preserving visible summaries through nested cache entries, JSON:API/resource envelopes, and provider-style response wrappers.
553553
- `internal/compact`: microcompact provider and cache summary extraction now unwraps inline fenced JSON payloads where the fence language and JSON object are on the same line, so SDK/model outputs no longer preserve the whole code fence as visible summary text.
554+
- `internal/compact`: microcompact count field loading now accepts whole-number JSON numbers and whole-number numeric strings, while fractional count values still fail validation.
554555

555556
Still missing for full M6/M7 parity:
556557

‎internal/compact/compact_test.go‎

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,56 @@ func TestLoadMicroResultAcceptsAdjacentCacheFieldAliases(t *testing.T) {
314314
}
315315
}
316316

317+
func TestLoadMicroResultAcceptsWholeNumberCountFields(t *testing.T) {
318+
cacheDir := filepath.Join(t.TempDir(), "micro")
319+
if err := os.MkdirAll(cacheDir, 0o755); err != nil {
320+
t.Fatal(err)
321+
}
322+
digest := "whole-number-counts"
323+
data := `{
324+
"summary": "whole number counts summary",
325+
"digest": "whole-number-counts",
326+
"messagesSummarized": 7.0,
327+
"messagesKept": "2.0",
328+
"version": "microcompact.v1",
329+
"createdAt": 100
330+
}`
331+
if err := os.WriteFile(microResultPath(cacheDir, digest), []byte(data), 0o600); err != nil {
332+
t.Fatal(err)
333+
}
334+
result, ok, err := LoadMicroResult(cacheDir, digest)
335+
if err != nil {
336+
t.Fatal(err)
337+
}
338+
if !ok {
339+
t.Fatal("expected whole-number count cache result")
340+
}
341+
if result.MessagesSummarized != 7 || result.MessagesKept != 2 {
342+
t.Fatalf("result = %#v", result)
343+
}
344+
}
345+
346+
func TestLoadMicroResultRejectsFractionalCountFields(t *testing.T) {
347+
cacheDir := filepath.Join(t.TempDir(), "micro")
348+
if err := os.MkdirAll(cacheDir, 0o755); err != nil {
349+
t.Fatal(err)
350+
}
351+
digest := "fractional-counts"
352+
data := `{
353+
"summary": "fractional counts summary",
354+
"digest": "fractional-counts",
355+
"messagesSummarized": 7.5,
356+
"version": "microcompact.v1",
357+
"createdAt": 100
358+
}`
359+
if err := os.WriteFile(microResultPath(cacheDir, digest), []byte(data), 0o600); err != nil {
360+
t.Fatal(err)
361+
}
362+
if _, _, err := LoadMicroResult(cacheDir, digest); err == nil {
363+
t.Fatal("expected fractional count field to fail")
364+
}
365+
}
366+
317367
func TestLoadMicroResultAcceptsSummaryTextAliases(t *testing.T) {
318368
cacheDir := filepath.Join(t.TempDir(), "micro")
319369
if err := os.MkdirAll(cacheDir, 0o755); err != nil {

‎internal/compact/micro.go‎

Lines changed: 42 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"encoding/hex"
77
"encoding/json"
88
"fmt"
9+
"math"
910
"os"
1011
"path/filepath"
1112
"sort"
@@ -859,23 +860,57 @@ func microParseJSONBool(raw json.RawMessage, field string) (bool, error) {
859860
}
860861

861862
func microIntJSONField(fields map[string]json.RawMessage, names ...string) (int, bool, error) {
862-
raw, _, ok := microRawJSONField(fields, names...)
863+
raw, name, ok := microRawJSONField(fields, names...)
863864
if !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
864865
return 0, false, nil
865866
}
867+
value, err := microParseJSONInt(raw, name)
868+
if err != nil {
869+
return 0, false, err
870+
}
871+
return value, true, nil
872+
}
873+
874+
func microParseJSONInt(raw json.RawMessage, field string) (int, error) {
866875
var value int
867876
if err := json.Unmarshal(raw, &value); err == nil {
868-
return value, true, nil
877+
return value, nil
869878
}
870879
var text string
871-
if err := json.Unmarshal(raw, &text); err != nil {
872-
return 0, false, err
880+
if err := json.Unmarshal(raw, &text); err == nil {
881+
return microParseIntText(text, field)
873882
}
874-
parsed, err := strconv.Atoi(text)
883+
decoder := json.NewDecoder(strings.NewReader(string(raw)))
884+
decoder.UseNumber()
885+
var number json.Number
886+
if err := decoder.Decode(&number); err == nil {
887+
return microParseIntText(number.String(), field)
888+
}
889+
return 0, fmt.Errorf("invalid int field %q", field)
890+
}
891+
892+
func microParseIntText(text string, field string) (int, error) {
893+
text = strings.TrimSpace(text)
894+
if text == "" {
895+
return 0, fmt.Errorf("empty int field %q", field)
896+
}
897+
parsed, err := strconv.ParseInt(text, 10, 0)
898+
if err == nil {
899+
return int(parsed), nil
900+
}
901+
value, err := strconv.ParseFloat(text, 64)
875902
if err != nil {
876-
return 0, false, err
903+
return 0, err
904+
}
905+
if math.IsInf(value, 0) || math.IsNaN(value) || value != math.Trunc(value) {
906+
return 0, fmt.Errorf("invalid int field %q: %q", field, text)
907+
}
908+
maxInt := int64(^uint(0) >> 1)
909+
minInt := -maxInt - 1
910+
if value > float64(maxInt) || value < float64(minInt) {
911+
return 0, fmt.Errorf("int field %q out of range: %q", field, text)
877912
}
878-
return parsed, true, nil
913+
return int(value), nil
879914
}
880915

881916
func microTimeJSONField(fields map[string]json.RawMessage, names ...string) (time.Time, bool, error) {

0 commit comments

Comments
 (0)