Skip to content

Commit f5e4667

Browse files
author
SqlRush
committed
Recover remote history metadata payloads
1 parent 0903445 commit f5e4667

5 files changed

Lines changed: 58 additions & 3 deletions

File tree

docs/cc-100-roadmap.md

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

559559
本轮补充:contract image source 解码现在接受 `mediaType`/`mimeType`/`contentType``base64`/`payload` 等 source 字段别名,并支持顶层 image block 直接携带媒体类型和 base64 数据,transcript resume 会保留为规范 `ImageSource`
560560

561-
本轮补充:remote history `SDKEvent` payload materialization 现在会递归解包 `payload`/`data`/`body` 内的 `record`/`entry`/`item`/`event`/`result`/`response`/`output` wrapper,减少远端事件多层包装导致的消息丢失。
561+
本轮补充:remote history `SDKEvent` payload materialization 现在会递归解包 `payload`/`data`/`body`/`metadata`/`meta`/`attributes`/`properties` 内的 `record`/`entry`/`item`/`event`/`result`/`response`/`output` wrapper,减少远端事件多层包装导致的消息丢失。
562562

563563
本轮补充:remote history 普通事件数组现在也会解包元素级 `event`/`record`/`entry`/`item`/`resource`/`value` 以及无事件本体字段时的 `data`/`payload`/`body` wrapper,并用元素 `cursor` 作为事件 ID fallback,覆盖非 GraphQL edges 的 wrapper item 响应。
564564

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,7 @@ test/parity/ # golden tests against TS/official behavior
215215
- 本轮补充:remote history response parser 现在也接受 JSON:API `included` collection,会过滤非事件资源,并递归解包 `resource`/`attributes`/`properties` 后保留外层 resource id 作为事件 ID fallback。
216216
- 本轮补充:remote history response parser 会解包 `payload`/`response`/`result`/`body` 等通用响应外壳,外壳内的 event list、pagination、links 会继续递归解析。
217217
- 本轮补充:remote history response parser 现在也接受 provider-style `choices`/`outputs`/`candidates`/`generations` wrapper,可从 `message.content`、content-block array 和 `content.parts[].text` 中恢复 event page JSON(包括 fenced `json` code block),并保留 pagination 继续驱动 `before_id` 续抓。
218-
- 本轮补充:remote history `SDKEvent` 本体接受 `eventType`/`event_type`/`role` 类型别名、`createdAt`/`created_at` 时间戳别名,以及 `payload`/`data`/`body`/`serializedMessage` message payload 别名;payload 只有 `role`/`content` 时也能 materialize 成 transcript message。
218+
- 本轮补充:remote history `SDKEvent` 本体接受 `eventType`/`event_type`/`role` 类型别名、`createdAt`/`created_at` 时间戳别名,以及 `payload`/`data`/`body`/`metadata`/`meta`/`attributes`/`properties`/`serializedMessage` message payload 别名;payload 只有 `role`/`content` 时也能 materialize 成 transcript message。
219219
- 本轮补充:transcript resume 的嵌套 content block 接受 `toolUseId`/`toolUseID``isError``cacheControl``cacheReference` 字段别名,并保留 cache edit 的 `cacheReference`
220220
- 本轮补充:transcript resume 的 nested content block `id`/`tool_use_id`/`toolUseId` 现在接受 JSON number,并保留为字符串 tool-use ID。
221221
- 本轮补充:嵌套 contract message 的 `content` 接受字符串、单个 content-block 对象,以及混合字符串/content-block 数组;字符串会归一为 text block,并接受 `text`/`body`/`message`/`value`/`output` 正文字段和 `role`/`messageType` 类型别名,提升 transcript/remote history payload 恢复率。

docs/first-second-parity-audit.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ M6 progress now includes:
169169
- `internal/session`: remote-history parsing now also accepts `value`, `values`, `resources`, and `collection` event-list aliases, plus `edges[].resource`/`edges[].value` node payload aliases.
170170
- `internal/session`: remote-history parsing now unwraps generic response envelopes such as `payload`, `response`, `result`, and `body` when they contain event lists and pagination fields.
171171
- `internal/session`: remote-history event-list fields such as `data` and `result` now accept a single SDK event object directly, preserving non-array single-event pages instead of treating them only as wrappers.
172-
- `internal/contracts`/`internal/session`: remote-history event materialization now accepts `eventType`/`event_type`/`role` type aliases, `createdAt`/`created_at` timestamp aliases, and message payload aliases such as `payload`, `data`, `body`, and `serializedMessage`, including role/content-only payloads.
172+
- `internal/contracts`/`internal/session`: remote-history event materialization now accepts `eventType`/`event_type`/`role` type aliases, `createdAt`/`created_at` timestamp aliases, and message payload aliases such as `payload`, `data`, `body`, `metadata`, `meta`, `attributes`, `properties`, and `serializedMessage`, including role/content-only payloads.
173173
- `internal/session`: remote-history pageInfo parsing now accepts previous/older pagination signals such as `hasPrevious`/`hasPreviousPage`, `hasOlder`/`more`, and before-id cursors such as `previousCursor`/`prevCursor`/`beforeCursor`/`olderCursor`.
174174
- `internal/session`: remote-history pagination bool parsing now accepts numeric and yes/no-style bool values in addition to JSON bools and true/false strings.
175175
- `internal/session`: remote-history pagination cursor/id parsing now accepts JSON numbers and preserves them as string cursors for page fields and `edges[].cursor`.

internal/contracts/session.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,18 @@ func (e *SDKEvent) UnmarshalJSON(data []byte) error {
235235
aux.Item,
236236
)
237237
}
238+
if e.Message == nil {
239+
fields := map[string]json.RawMessage{}
240+
if err := json.Unmarshal(data, &fields); err == nil {
241+
e.Message = firstSDKEventSidecarMessage(e.Type,
242+
fields["metadata"],
243+
fields["meta"],
244+
fields["attributes"],
245+
fields["properties"],
246+
fields["attrs"],
247+
)
248+
}
249+
}
238250
if e.Message == nil {
239251
e.Message = sdkEventTopLevelMessage(data, e.Type)
240252
}
@@ -326,6 +338,26 @@ func sdkEventMessageFromRaw(raw json.RawMessage, eventType SDKEventType) *Messag
326338
return nested
327339
}
328340
}
341+
for _, name := range []string{
342+
"metadata",
343+
"meta",
344+
"attributes",
345+
"properties",
346+
"attrs",
347+
} {
348+
if nested := sdkEventMessageFromRaw(fields[name], eventType); nested != nil && sdkEventTopLevelMessageHasData(*nested) {
349+
return nested
350+
}
351+
}
352+
return nil
353+
}
354+
355+
func firstSDKEventSidecarMessage(eventType SDKEventType, values ...json.RawMessage) *Message {
356+
for _, value := range values {
357+
if message := sdkEventMessageFromRaw(value, eventType); message != nil && sdkEventTopLevelMessageHasData(*message) {
358+
return message
359+
}
360+
}
329361
return nil
330362
}
331363

internal/session/remote_history_test.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1749,6 +1749,29 @@ func TestRemoteHistoryTranscriptMessagesAcceptsNestedEventPayloadWrappers(t *tes
17491749
}
17501750
}
17511751

1752+
func TestRemoteHistoryTranscriptMessagesAcceptsMetadataPayloadWrappers(t *testing.T) {
1753+
var response sessionEventsResponse
1754+
if err := json.Unmarshal([]byte(`{"events":[
1755+
{"eventType":"user","eventId":"evt_u_meta","sessionID":"s_meta","createdAt":"2026-01-01T00:00:01Z","metadata":{"role":"user","text":"metadata hi"}},
1756+
{"eventType":"assistant","eventId":"evt_a_props","sessionID":"s_meta","parentMessageID":"evt_u_meta","createdAt":"2026-01-01T00:00:02Z","properties":{"message":{"role":"assistant","message":"properties hello"}}}
1757+
]}`), &response); err != nil {
1758+
t.Fatal(err)
1759+
}
1760+
messages := RemoteHistoryTranscriptMessages(response.Events)
1761+
if len(messages) != 2 || messages[0].UUID != "evt_u_meta" || messages[1].UUID != "evt_a_props" {
1762+
t.Fatalf("messages = %#v", messages)
1763+
}
1764+
if messages[0].Message == nil || len(messages[0].Message.Content) != 1 || messages[0].Message.Content[0].Text != "metadata hi" {
1765+
t.Fatalf("metadata payload = %#v", messages[0].Message)
1766+
}
1767+
if messages[1].Message == nil || len(messages[1].Message.Content) != 1 || messages[1].Message.Content[0].Text != "properties hello" {
1768+
t.Fatalf("properties payload = %#v", messages[1].Message)
1769+
}
1770+
if messages[1].ParentUUID == nil || *messages[1].ParentUUID != "evt_u_meta" || messages[1].Message.ParentUUID == nil || *messages[1].Message.ParentUUID != "evt_u_meta" {
1771+
t.Fatalf("parent linkage = %#v message=%#v", messages[1].ParentUUID, messages[1].Message)
1772+
}
1773+
}
1774+
17521775
func TestRemoteHistoryTranscriptMessagesAcceptsNumericIDAliases(t *testing.T) {
17531776
var response sessionEventsResponse
17541777
if err := json.Unmarshal([]byte(`{"events":[{"type":"user","id":101,"sessionId":7,"timestamp":"2026-01-01T00:00:01Z","message":{"type":"user","content":[{"type":"text","text":"hi"}]}},{"type":"assistant","eventId":102,"sessionId":7,"parentMessageID":101,"timestamp":"2026-01-01T00:00:02Z","message":{"type":"assistant","content":[{"type":"text","text":"hello"}]}}]}`), &response); err != nil {

0 commit comments

Comments
 (0)