Skip to content

Commit 7d2ce59

Browse files
author
SqlRush
committed
Accept collapse snapshot metadata aliases
1 parent 0d4c031 commit 7d2ce59

5 files changed

Lines changed: 59 additions & 4 deletions

File tree

docs/cc-100-roadmap.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,8 @@ M7 补充:prompt history `LogEntry` 读取现在接受 `sessionID`/`session`/`
312312

313313
本轮补充:transcript metadata loader 现在接受 `sessionID``session` 作为 session-scoped metadata ID 别名,并容忍 `prNumber``timeSavedMs``lastSpawnTokens` 等计数字段使用数字字符串。
314314

315+
本轮补充:context-collapse snapshot metadata 接受 `isArmed`/`enabled` bool 别名、`spawnTokens`/`tokenCount` 计数字段别名,以及 `stagedMessages`/`items` staged payload wrapper,full loader 和 metadata loader 保持一致。
316+
315317
本轮补充:transcript message 和嵌套 contract message 现在接受 `sessionID` 顶层别名,`LoadTranscript``LoadTranscriptIndex` 和 indexed resume 会保留该 session id(覆盖测试:`TestLoadTranscriptAcceptsSessionIDUpperAlias`)。
316318

317319
本轮补充:remote history `SDKEvent` 解码现在也接受 `sessionID` 作为事件 session id 别名,materialize 成 transcript message 时会同步填充 record 和嵌套 message 的 session id(覆盖测试:`TestRemoteHistoryTranscriptMessagesAcceptsSessionIDUpperAlias`)。

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,7 @@ test/parity/ # golden tests against TS/official behavior
195195
- 本轮补充:transcript resume 的嵌套 content block 接受 `toolUseId`/`toolUseID``isError``cacheControl``cacheReference` 字段别名,并保留 cache edit 的 `cacheReference`
196196
- 本轮补充:lightweight transcript metadata loader 在 `system`/`compact_boundary` 后清空旧 `marble-origami-commit`/`marble-origami-snapshot` 状态,和 full loader/官方 sessionStorage compact-boundary 语义一致。
197197
- 本轮补充:transcript metadata loader 接受 `sessionID`/`session` 作为 session-scoped metadata ID 别名,并容忍 `prNumber``timeSavedMs``lastSpawnTokens` 等计数字段使用数字字符串。
198+
- 本轮补充:context-collapse snapshot metadata 接受 `isArmed`/`enabled` bool 别名、`spawnTokens`/`tokenCount` 计数字段别名,以及 `stagedMessages`/`items` staged payload wrapper,full loader 和 metadata loader 保持一致。
198199
- 本轮补充:transcript message 和嵌套 contract message 接受顶层 `sessionID` 作为 session id 别名,`LoadTranscript``LoadTranscriptIndex` 和 indexed resume 会保留该 session id(覆盖测试:`TestLoadTranscriptAcceptsSessionIDUpperAlias`)。
199200
- 本轮补充:嵌套 contract message 接受 `parentUUID``parentId`/`parentID`/`parent_id``parentMessageId`/`parentMessageID`/`parent_message_id` 和 parent-message UUID 别名,transcript/remote history payload 自带 parent alias 时不会丢失嵌套 parent。
200201
- 本轮补充:嵌套 contract message 接受 `messageId`/`messageID`/`message_id``messageUuid`/`messageUUID`/`message_uuid` 作为自身 ID/UUID 别名,indexed resume 会保留 payload 自带的 nested message id。

docs/first-second-parity-audit.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@ M6 progress now includes:
158158
- `internal/session`: transcript message loading now preserves structured SerializedMessage metadata such as `userType`, `entrypoint`, `version`, and `slug`, including common alias spellings.
159159
- `internal/session`: lightweight transcript metadata loading now clears stale context-collapse commit/snapshot state after compact-boundary messages, matching the full loader and official sessionStorage restore semantics.
160160
- `internal/session`: transcript metadata loading now accepts `sessionID` and `session` as session-scoped ID aliases and tolerates numeric strings for counters such as `prNumber`, `timeSavedMs`, and `lastSpawnTokens`.
161+
- `internal/session`: context-collapse snapshot metadata now accepts alternate armed, token-count, and staged payload field names in both full and lightweight metadata loaders.
161162
- `internal/session` and `internal/contracts`: transcript records and nested contract messages now accept top-level `sessionID` as a session id alias, preserving it through `LoadTranscript`, `LoadTranscriptIndex`, and indexed resume (`TestLoadTranscriptAcceptsSessionIDUpperAlias`).
162163
- `internal/contracts`/`internal/session`: remote-history `SDKEvent` decoding now accepts top-level `sessionID` as an event session id alias and preserves it during transcript materialization (`TestRemoteHistoryTranscriptMessagesAcceptsSessionIDUpperAlias`).
163164
- `internal/contracts`/`internal/session`: remote-history `SDKEvent` decoding now accepts parent aliases such as `parentUUID`, `parentId`/`parentID`/`parent_id`, and `parentMessageId`/`parentMessageID`/`parent_message_id`, preserving parent chains during transcript materialization (`TestRemoteHistoryTranscriptMessagesAcceptsParentIDAliases`).

internal/session/transcript_metadata_fields.go

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,33 @@ func (f transcriptMetadataFields) intValue(keys ...string) int {
6262
return 0
6363
}
6464

65+
func (f transcriptMetadataFields) boolValue(keys ...string) (bool, bool) {
66+
for _, key := range keys {
67+
raw, ok := f[key]
68+
if !ok || isNullJSON(raw) {
69+
continue
70+
}
71+
var value bool
72+
if err := json.Unmarshal(raw, &value); err == nil {
73+
return value, true
74+
}
75+
var text string
76+
if err := json.Unmarshal(raw, &text); err == nil {
77+
switch strings.ToLower(strings.TrimSpace(text)) {
78+
case "1", "t", "true", "yes", "y", "on":
79+
return true, true
80+
case "0", "f", "false", "no", "n", "off":
81+
return false, true
82+
}
83+
}
84+
var number int
85+
if err := json.Unmarshal(raw, &number); err == nil {
86+
return number != 0, true
87+
}
88+
}
89+
return false, false
90+
}
91+
6592
func (f transcriptMetadataFields) rawValue(keys ...string) json.RawMessage {
6693
for _, key := range keys {
6794
raw, ok := f[key]
@@ -73,6 +100,22 @@ func (f transcriptMetadataFields) rawValue(keys ...string) json.RawMessage {
73100
return nil
74101
}
75102

103+
func (f transcriptMetadataFields) arrayValue(keys ...string) []any {
104+
raw := f.rawValue(keys...)
105+
if len(raw) == 0 {
106+
return nil
107+
}
108+
var values []any
109+
if err := json.Unmarshal(raw, &values); err == nil {
110+
return values
111+
}
112+
var value any
113+
if err := json.Unmarshal(raw, &value); err != nil || value == nil {
114+
return nil
115+
}
116+
return []any{value}
117+
}
118+
76119
func isNullJSON(raw json.RawMessage) bool {
77120
return len(bytes.TrimSpace(raw)) == 0 || bytes.Equal(bytes.TrimSpace(raw), []byte("null"))
78121
}
@@ -318,7 +361,15 @@ func parseContextCollapseSnapshotMetadata(line []byte) (ContextCollapseSnapshotE
318361
entry.SessionID = fields.sessionIDValue()
319362
}
320363
if entry.LastSpawnTokens == 0 {
321-
entry.LastSpawnTokens = fields.intValue("lastSpawnTokens", "last_spawn_tokens")
364+
entry.LastSpawnTokens = fields.intValue("lastSpawnTokens", "last_spawn_tokens", "spawnTokens", "spawn_tokens", "tokenCount", "token_count")
365+
}
366+
if !entry.Armed {
367+
if armed, ok := fields.boolValue("armed", "isArmed", "is_armed", "enabled", "ready"); ok {
368+
entry.Armed = armed
369+
}
370+
}
371+
if len(entry.Staged) == 0 {
372+
entry.Staged = fields.arrayValue("staged", "stagedMessages", "staged_messages", "pending", "entries", "items")
322373
}
323374
return entry, true
324375
}

internal/session/transcript_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -822,7 +822,7 @@ func TestLoadTranscriptMetadataAcceptsSessionIDAndNumericStrings(t *testing.T) {
822822
path := writeTranscript(t, []string{
823823
`{"type":"pr-link","sessionID":"s4","prNumber":"44","prUrl":"https://github.com/o/r/pull/44","prRepository":"o/r"}`,
824824
`{"type":"speculation-accept","createdAt":"2026-01-01T00:00:05Z","timeSavedMs":"5600"}`,
825-
`{"type":"marble-origami-snapshot","sessionID":"s4","armed":true,"lastSpawnTokens":"96"}`,
825+
`{"type":"marble-origami-snapshot","sessionID":"s4","isArmed":"true","spawnTokens":"96","items":[{"uuid":"pending_1"}]}`,
826826
})
827827
transcript, err := LoadTranscript(path)
828828
if err != nil {
@@ -834,15 +834,15 @@ func TestLoadTranscriptMetadataAcceptsSessionIDAndNumericStrings(t *testing.T) {
834834
if len(transcript.SpeculationAccepts) != 1 || transcript.SpeculationAccepts[0].TimeSavedMS != 5600 || transcript.SpeculationAccepts[0].Timestamp != "2026-01-01T00:00:05Z" {
835835
t.Fatalf("transcript speculation = %#v", transcript.SpeculationAccepts)
836836
}
837-
if transcript.ContextCollapseSnapshot == nil || transcript.ContextCollapseSnapshot.SessionID != "s4" || transcript.ContextCollapseSnapshot.LastSpawnTokens != 96 {
837+
if transcript.ContextCollapseSnapshot == nil || transcript.ContextCollapseSnapshot.SessionID != "s4" || !transcript.ContextCollapseSnapshot.Armed || transcript.ContextCollapseSnapshot.LastSpawnTokens != 96 || len(transcript.ContextCollapseSnapshot.Staged) != 1 {
838838
t.Fatalf("transcript snapshot = %#v", transcript.ContextCollapseSnapshot)
839839
}
840840

841841
metadata, err := LoadTranscriptMetadata(path)
842842
if err != nil {
843843
t.Fatal(err)
844844
}
845-
if metadata.PRLinks["s4"].PRNumber != 44 || metadata.ContextCollapseSnapshot == nil || metadata.ContextCollapseSnapshot.LastSpawnTokens != 96 {
845+
if metadata.PRLinks["s4"].PRNumber != 44 || metadata.ContextCollapseSnapshot == nil || !metadata.ContextCollapseSnapshot.Armed || metadata.ContextCollapseSnapshot.LastSpawnTokens != 96 || len(metadata.ContextCollapseSnapshot.Staged) != 1 {
846846
t.Fatalf("metadata = %#v snapshot=%#v", metadata.PRLinks, metadata.ContextCollapseSnapshot)
847847
}
848848
if len(metadata.SpeculationAccepts) != 1 || metadata.SpeculationAccepts[0].TimeSavedMS != 5600 {

0 commit comments

Comments
 (0)