Skip to content

Commit 122d522

Browse files
author
SqlRush
committed
Accept non-strict microcompact cache booleans
1 parent 4a9f056 commit 122d522

5 files changed

Lines changed: 80 additions & 2 deletions

File tree

docs/cc-100-roadmap.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ M7 补充:scripted permission payload、dialog expectation、event、cancel-pe
3838

3939
M6 补充:microcompact disk cache loader 和 prune 现在接受 digest 缺失但文件名已 keyed 的 cache entry,会用 `<digest>.json` 文件名作为 digest fallback,同时保留显式 digest mismatch 的 invalid-cache guard。
4040

41+
M6 补充:microcompact disk cache loader 的 `cached`/`fromCache`/`cacheHit`/`isCached` 布尔字段现在接受 JSON bool、`true`/`false``yes`/`no``on`/`off``1`/`0` 数字/字符串形态。
42+
4143
M6 补充:sidechain/subagent lifecycle state loader 现在接受 `subagent_started``agentStarted``task_failed``sidechainCompleted` 等相邻 subtype aliases,支持 `taskID`/`workerId`/`runId``agentName`/`kind``resultText`/`finalMessage` 等字段,并在 failed/cancelled subtype 没有显式 status 时自动归一状态。
4244

4345
M6 补充:transcript resume 的嵌套 content block 现在接受 `toolUseId`/`toolUseID``isError``cacheControl``cacheReference` 字段别名,并保留 cache edit 的 `cacheReference`

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,7 @@ test/parity/ # golden tests against TS/official behavior
246246
- 本轮补充: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` 等相邻实现字段别名。
247247
- 本轮补充:microcompact disk cache loader 继续接受相邻 timestamp/expiry aliases,包括 `cachedAt``cacheCreatedAt``storedAt``generatedAt``updatedAt``timestamp``expiry``expirationTime``validUntil``notAfter`,以及 `timeToLiveSeconds``validForMs` 等相对 TTL 字段。
248248
- 本轮补充:microcompact disk cache loader 和 prune 现在接受 digest 缺失但文件名已 keyed 的 cache entry,会用 `<digest>.json` 文件名作为 digest fallback,同时仍保留显式 digest mismatch 的 invalid-cache guard。
249+
- 本轮补充:microcompact disk cache loader 的 `cached`/`fromCache`/`cacheHit`/`isCached` 布尔字段现在接受 JSON bool、`true`/`false``yes`/`no``on`/`off``1`/`0` 数字/字符串形态。
249250

250251
### M7: TUI renderer 和交互体验
251252

docs/first-second-parity-audit.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ M6 progress now includes:
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 loading and pruning now use the `<digest>.json` filename as a digest fallback when a cache entry omits an internal digest, while preserving explicit digest mismatch pruning/errors.
130+
- `internal/compact`: microcompact disk cache loading now accepts non-strict cache-hit booleans for `cached`/`fromCache`/`cacheHit`/`isCached`, including `yes`/`no`, `on`/`off`, and `1`/`0` JSON number or string values.
130131
- `internal/conversation`: optional auto-compact can now run before the main request, fail open with a consecutive-failure circuit breaker, emit runtime `token_warning` events from compact warning state, persist compact boundary metadata to transcript, write a session-memory summary, inject recalled session-memory snippets into API request context when enabled, and optionally extract turn-end memory facts into session memory.
131132
- `internal/memory`: session-memory summaries can now be loaded with frontmatter field aliases, rolled up/pruned into an archive summary with prior rollup archives excluded from candidate compaction and merged across archive IDs, rune-safe rollup truncation, recalled by query with deterministic scoring and recency ordering, ranked through a model-assisted candidate/session-id selection path with fallback when selected IDs are invalid or excluded, alternate/camel response-key parsing, fenced/prose JSON extraction, scalar session-id parsing, nested/wrapped/collection-alias selection parsing, and nested selected-memory item parsing, and injected into resume context through the same optional recall agent path; deterministic and model-backed memory fact extraction can summarize user preferences, requests, decisions, and tool-use facts, including fenced/prose JSON, wrapped facts responses, alternate and structured fact field names, nested source objects, nested fact response shapes, and fact kind aliases.
132133
- `internal/session`: project session listing and pagination, prompt-history lock/buffered flush/field aliases, lightweight transcript index/title/text-preview inference, line-offset transcript indexing/window/byte-budget-window/parent-chain/resume/tail/byte-budget-tail loading, AI-title/last-prompt/task/agent/PR/worktree transcript metadata loading and type/field aliases, transcript message/session UUID field aliases, tombstone metadata loading with target/session/reason aliases and delete/relink replay, agent-scoped content replacement metadata/record field-alias loading, session-scoped metadata reappend including AI-title/last-prompt/task-summary, streaming transcript search snippets for resume/search UI, official `subagents/agent-*.jsonl` transcript layout with legacy sidechain listing, agent metadata sidecar read/write/field aliases, sidechain runtime start/append/finish/cancel/fail summary bridging plus parent-chain append/finish, sidechain manager orchestration for spawn/append/finish/cancel/fail/list/resume, sidechain state/list/resume support with content-field aliases, sidechain resume context construction, sidechain conversation and agent-scoped content-replacement reconstruction, transcript tail/window loading and byte-budget tail loading for bounded-memory resume/UI paths, lightweight transcript metadata loading, remote-history token refresh retry, page-field/event-list/records/entries/last-id/cursor/event-id/has-next aliases plus wrapped-data/links/paging/bare-array/keyed-event-map responses, before_id resume, fallback field fill, and duplicate-aware parent linking during transcript materialization.

internal/compact/compact_test.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package compact
22

33
import (
44
"context"
5+
"fmt"
56
"os"
67
"path/filepath"
78
"strings"
@@ -312,6 +313,43 @@ func TestLoadMicroResultAcceptsAdjacentCacheFieldAliases(t *testing.T) {
312313
}
313314
}
314315

316+
func TestLoadMicroResultAcceptsAdjacentBoolCacheAliases(t *testing.T) {
317+
cacheDir := filepath.Join(t.TempDir(), "micro")
318+
if err := os.MkdirAll(cacheDir, 0o755); err != nil {
319+
t.Fatal(err)
320+
}
321+
for _, tc := range []struct {
322+
digest string
323+
field string
324+
want bool
325+
}{
326+
{digest: "cache-hit-string-yes", field: `"cacheHit":"yes"`, want: true},
327+
{digest: "from-cache-number-one", field: `"fromCache":1`, want: true},
328+
{digest: "is-cached-string-no", field: `"isCached":"no"`, want: false},
329+
} {
330+
payload := fmt.Sprintf(`{
331+
"summary": %q,
332+
"digest": %q,
333+
%s,
334+
"version": "microcompact.v1",
335+
"createdAt": 100
336+
}`, tc.digest+" summary", tc.digest, tc.field)
337+
if err := os.WriteFile(microResultPath(cacheDir, tc.digest), []byte(payload), 0o600); err != nil {
338+
t.Fatal(err)
339+
}
340+
result, ok, err := LoadMicroResult(cacheDir, tc.digest)
341+
if err != nil {
342+
t.Fatalf("%s load error: %v", tc.digest, err)
343+
}
344+
if !ok {
345+
t.Fatalf("%s was not loaded", tc.digest)
346+
}
347+
if result.Cached != tc.want || result.Summary != tc.digest+" summary" {
348+
t.Fatalf("%s result = %#v", tc.digest, result)
349+
}
350+
}
351+
}
352+
315353
func TestLoadMicroResultAcceptsAdjacentCacheEntryAliases(t *testing.T) {
316354
cacheDir := filepath.Join(t.TempDir(), "micro")
317355
if err := os.MkdirAll(cacheDir, 0o755); err != nil {

internal/compact/micro.go

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -464,15 +464,51 @@ func microBoolJSONField(fields map[string]json.RawMessage, names ...string) (boo
464464
if !ok || string(raw) == "null" {
465465
continue
466466
}
467-
var value bool
468-
if err := json.Unmarshal(raw, &value); err != nil {
467+
value, err := microParseJSONBool(raw, name)
468+
if err != nil {
469469
return false, false, err
470470
}
471471
return value, true, nil
472472
}
473473
return false, false, nil
474474
}
475475

476+
func microParseJSONBool(raw json.RawMessage, field string) (bool, error) {
477+
var value bool
478+
if err := json.Unmarshal(raw, &value); err == nil {
479+
return value, nil
480+
}
481+
var text string
482+
if err := json.Unmarshal(raw, &text); err == nil {
483+
switch strings.ToLower(strings.TrimSpace(text)) {
484+
case "true", "1", "yes", "y", "on":
485+
return true, nil
486+
case "false", "0", "no", "n", "off":
487+
return false, nil
488+
default:
489+
return false, fmt.Errorf("invalid bool field %q: %q", field, text)
490+
}
491+
}
492+
decoder := json.NewDecoder(strings.NewReader(string(raw)))
493+
decoder.UseNumber()
494+
var number json.Number
495+
if err := decoder.Decode(&number); err == nil {
496+
value, err := strconv.ParseFloat(number.String(), 64)
497+
if err != nil {
498+
return false, err
499+
}
500+
switch value {
501+
case 1:
502+
return true, nil
503+
case 0:
504+
return false, nil
505+
default:
506+
return false, fmt.Errorf("invalid bool field %q: %s", field, number.String())
507+
}
508+
}
509+
return false, fmt.Errorf("invalid bool field %q", field)
510+
}
511+
476512
func microIntJSONField(fields map[string]json.RawMessage, names ...string) (int, bool, error) {
477513
for _, name := range names {
478514
raw, ok := fields[name]

0 commit comments

Comments
 (0)