Skip to content

Commit c2c817a

Browse files
author
SqlRush
committed
Accept remote history event payload aliases
1 parent 1770439 commit c2c817a

5 files changed

Lines changed: 186 additions & 13 deletions

File tree

docs/cc-100-roadmap.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,8 @@ M6 补充:remote history event-list 现在接受 `value`/`values`/`resources`/
8484

8585
M6 补充:remote history response parser 现在会解包 `payload`/`response`/`result`/`body` 等通用响应外壳,外壳内的 event list、pagination、links 会继续递归解析。
8686

87+
M6 补充:remote history `SDKEvent` 本体现在接受 `eventType`/`event_type`/`role` 类型别名、`createdAt`/`created_at` 时间戳别名,以及 `payload`/`data`/`body`/`serializedMessage` message payload 别名;payload 只有 `role`/`content` 时也能 materialize 成 transcript message。
88+
8789
M6 补充:remote history REST/link 风格分页现在接受 `links`/`_links``next`/`previous`/`prev`/`older` 字符串 URL 或 `{href,url,uri,link}` 对象,并从 `before_id``beforeId``cursor``pageCursor``previousCursor``prevCursor``beforeCursor``olderCursor``startCursor``endCursor` 等 query 参数提取下一页 before-id。
8890

8991
M6 补充:remote history REST/link 风格分页现在也接受 RFC/JSON:API 风格的 `links` 数组,按 `rel`/`relation`/`name`/`type` 中的 `previous`/`prev`/`older`/`next` 选择续抓 URL,并从同一组 before/cursor query 参数提取 before-id。

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,7 @@ test/parity/ # golden tests against TS/official behavior
200200
- 本轮补充:remote history response parser 会递归解包 `data.session.events``data.projectSession.eventConnection``conversation``remoteHistory``_embedded` 等 GraphQL/session/HAL wrapper,继续复用 `nodes`/`edges[].node``pageInfo` pagination 解析。
201201
- 本轮补充:remote history event-list 接受 `value`/`values`/`resources`/`collection` 别名,connection edge 也接受 `resource`/`value` 作为 node payload,覆盖 OData/HAL/resource collection 风格响应。
202202
- 本轮补充:remote history response parser 会解包 `payload`/`response`/`result`/`body` 等通用响应外壳,外壳内的 event list、pagination、links 会继续递归解析。
203+
- 本轮补充:remote history `SDKEvent` 本体接受 `eventType`/`event_type`/`role` 类型别名、`createdAt`/`created_at` 时间戳别名,以及 `payload`/`data`/`body`/`serializedMessage` message payload 别名;payload 只有 `role`/`content` 时也能 materialize 成 transcript message。
203204
- 本轮补充:transcript resume 的嵌套 content block 接受 `toolUseId`/`toolUseID``isError``cacheControl``cacheReference` 字段别名,并保留 cache edit 的 `cacheReference`
204205
- 本轮补充:transcript resume 的 nested content block `id`/`tool_use_id`/`toolUseId` 现在接受 JSON number,并保留为字符串 tool-use ID。
205206
- 本轮补充:lightweight transcript metadata loader 在 `system`/`compact_boundary` 后清空旧 `marble-origami-commit`/`marble-origami-snapshot` 状态,和 full loader/官方 sessionStorage compact-boundary 语义一致。

docs/first-second-parity-audit.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@ M6 progress now includes:
147147
- `internal/session`: remote-history parsing now also accepts `eventList`/`event_list`, `sessionEvents`/`session_events`, and connection aliases such as `connection`, `eventConnection`, and `sessionEventsConnection`.
148148
- `internal/session`: remote-history parsing now also accepts `value`, `values`, `resources`, and `collection` event-list aliases, plus `edges[].resource`/`edges[].value` node payload aliases.
149149
- `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.
150+
- `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.
150151
- `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`.
151152
- `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.
152153
- `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: 153 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package contracts
22

33
import (
4+
"bytes"
45
"encoding/json"
56
"strings"
67
)
@@ -142,26 +143,52 @@ func (e *SDKEvent) UnmarshalJSON(data []byte) error {
142143
type SDKEventJSON SDKEvent
143144
var aux struct {
144145
*SDKEventJSON
145-
EventIDSnake ID `json:"event_id"`
146-
EventIDCamel ID `json:"eventId"`
147-
SessionIDUpper ID `json:"sessionID"`
148-
SessionUUID ID `json:"sessionUuid"`
149-
SessionUUIDUpper ID `json:"sessionUUID"`
150-
SessionUUIDSnake ID `json:"session_uuid"`
151-
ParentUUIDUpper *ID `json:"parentUUID"`
152-
ParentID *ID `json:"parentId"`
153-
ParentIDUpper *ID `json:"parentID"`
154-
ParentIDSnake *ID `json:"parent_id"`
155-
ParentMessageID *ID `json:"parentMessageId"`
156-
ParentMessageIDUpper *ID `json:"parentMessageID"`
157-
ParentMessageIDSnake *ID `json:"parent_message_id"`
146+
EventTypeSnake string `json:"event_type"`
147+
EventTypeCamel string `json:"eventType"`
148+
Event string `json:"event"`
149+
Name string `json:"name"`
150+
Kind string `json:"kind"`
151+
Role string `json:"role"`
152+
MessageType string `json:"messageType"`
153+
MessageTypeSnake string `json:"message_type"`
154+
EventIDSnake ID `json:"event_id"`
155+
EventIDCamel ID `json:"eventId"`
156+
SessionIDUpper ID `json:"sessionID"`
157+
SessionUUID ID `json:"sessionUuid"`
158+
SessionUUIDUpper ID `json:"sessionUUID"`
159+
SessionUUIDSnake ID `json:"session_uuid"`
160+
ParentUUIDUpper *ID `json:"parentUUID"`
161+
ParentID *ID `json:"parentId"`
162+
ParentIDUpper *ID `json:"parentID"`
163+
ParentIDSnake *ID `json:"parent_id"`
164+
ParentMessageID *ID `json:"parentMessageId"`
165+
ParentMessageIDUpper *ID `json:"parentMessageID"`
166+
ParentMessageIDSnake *ID `json:"parent_message_id"`
167+
CreatedAt string `json:"createdAt"`
168+
CreatedAtSnake string `json:"created_at"`
169+
Time string `json:"time"`
170+
Datetime string `json:"datetime"`
171+
DateTime string `json:"dateTime"`
172+
MessagePayload json.RawMessage `json:"message_payload"`
173+
MessagePayloadCamel json.RawMessage `json:"messagePayload"`
174+
SerializedMessage json.RawMessage `json:"serialized_message"`
175+
SerializedMessageCamel json.RawMessage `json:"serializedMessage"`
176+
Payload json.RawMessage `json:"payload"`
177+
Data json.RawMessage `json:"data"`
178+
Body json.RawMessage `json:"body"`
179+
Record json.RawMessage `json:"record"`
180+
Entry json.RawMessage `json:"entry"`
181+
Item json.RawMessage `json:"item"`
158182
}
159183
base := SDKEventJSON{}
160184
aux.SDKEventJSON = &base
161185
if err := json.Unmarshal(data, &aux); err != nil {
162186
return err
163187
}
164188
*e = SDKEvent(base)
189+
if e.Type == "" {
190+
e.Type = firstSDKEventType(aux.EventTypeSnake, aux.EventTypeCamel, aux.Event, aux.Name, aux.Kind, aux.MessageType, aux.MessageTypeSnake, aux.Role)
191+
}
165192
if e.ID == "" {
166193
e.ID = aux.EventIDSnake
167194
}
@@ -180,6 +207,9 @@ func (e *SDKEvent) UnmarshalJSON(data []byte) error {
180207
if e.SessionID == "" {
181208
e.SessionID = aux.SessionUUIDSnake
182209
}
210+
if e.Timestamp == "" {
211+
e.Timestamp = firstSDKEventString(aux.CreatedAt, aux.CreatedAtSnake, aux.Time, aux.Datetime, aux.DateTime)
212+
}
183213
if e.ParentUUID == nil {
184214
e.ParentUUID = firstSDKEventIDPtr(
185215
aux.ParentUUIDUpper,
@@ -191,9 +221,55 @@ func (e *SDKEvent) UnmarshalJSON(data []byte) error {
191221
aux.ParentMessageIDSnake,
192222
)
193223
}
224+
if e.Message == nil {
225+
e.Message = firstSDKEventMessage(e.Type,
226+
aux.MessagePayload,
227+
aux.MessagePayloadCamel,
228+
aux.SerializedMessage,
229+
aux.SerializedMessageCamel,
230+
aux.Payload,
231+
aux.Data,
232+
aux.Body,
233+
aux.Record,
234+
aux.Entry,
235+
aux.Item,
236+
)
237+
}
238+
if e.Message == nil {
239+
e.Message = sdkEventTopLevelMessage(data, e.Type)
240+
}
194241
return nil
195242
}
196243

244+
func firstSDKEventType(values ...string) SDKEventType {
245+
for _, value := range values {
246+
switch strings.ToLower(strings.TrimSpace(value)) {
247+
case string(SDKEventSystem):
248+
return SDKEventSystem
249+
case string(SDKEventAssistant):
250+
return SDKEventAssistant
251+
case string(SDKEventUser):
252+
return SDKEventUser
253+
case string(SDKEventResult):
254+
return SDKEventResult
255+
case string(SDKEventError):
256+
return SDKEventError
257+
case string(SDKEventStatus):
258+
return SDKEventStatus
259+
}
260+
}
261+
return ""
262+
}
263+
264+
func firstSDKEventString(values ...string) string {
265+
for _, value := range values {
266+
if value != "" {
267+
return value
268+
}
269+
}
270+
return ""
271+
}
272+
197273
func firstSDKEventIDPtr(values ...*ID) *ID {
198274
for _, value := range values {
199275
if value != nil && *value != "" {
@@ -203,3 +279,67 @@ func firstSDKEventIDPtr(values ...*ID) *ID {
203279
}
204280
return nil
205281
}
282+
283+
func firstSDKEventMessage(eventType SDKEventType, values ...json.RawMessage) *Message {
284+
for _, value := range values {
285+
if message := sdkEventMessageFromRaw(value, eventType); message != nil {
286+
return message
287+
}
288+
}
289+
return nil
290+
}
291+
292+
func sdkEventMessageFromRaw(raw json.RawMessage, eventType SDKEventType) *Message {
293+
raw = bytes.TrimSpace(raw)
294+
if len(raw) == 0 || bytes.Equal(raw, []byte("null")) {
295+
return nil
296+
}
297+
var message Message
298+
if err := json.Unmarshal(raw, &message); err == nil && sdkEventMessageHasData(message) {
299+
if message.Type == "" {
300+
message.Type = MessageType(eventType)
301+
}
302+
return &message
303+
}
304+
var fields map[string]json.RawMessage
305+
if err := json.Unmarshal(raw, &fields); err != nil {
306+
return nil
307+
}
308+
for _, name := range []string{"message", "serialized_message", "serializedMessage", "payload", "data", "body"} {
309+
if nested := sdkEventMessageFromRaw(fields[name], eventType); nested != nil {
310+
return nested
311+
}
312+
}
313+
return nil
314+
}
315+
316+
func sdkEventTopLevelMessage(data []byte, eventType SDKEventType) *Message {
317+
var message Message
318+
if err := json.Unmarshal(data, &message); err != nil || !sdkEventTopLevelMessageHasData(message) {
319+
return nil
320+
}
321+
if message.Type == "" {
322+
message.Type = MessageType(eventType)
323+
}
324+
return &message
325+
}
326+
327+
func sdkEventMessageHasData(message Message) bool {
328+
return message.Type != "" ||
329+
message.ID != "" ||
330+
message.UUID != "" ||
331+
message.SessionID != "" ||
332+
message.ParentUUID != nil ||
333+
message.Timestamp != "" ||
334+
len(message.Content) > 0 ||
335+
message.Subtype != "" ||
336+
message.Model != "" ||
337+
message.Usage != nil
338+
}
339+
340+
func sdkEventTopLevelMessageHasData(message Message) bool {
341+
return len(message.Content) > 0 ||
342+
message.Subtype != "" ||
343+
message.Model != "" ||
344+
message.Usage != nil
345+
}

internal/session/remote_history_test.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1072,6 +1072,35 @@ func TestRemoteHistoryTranscriptMessagesAcceptsParentIDAliases(t *testing.T) {
10721072
}
10731073
}
10741074

1075+
func TestRemoteHistoryTranscriptMessagesAcceptsEventPayloadAliases(t *testing.T) {
1076+
var response sessionEventsResponse
1077+
if err := json.Unmarshal([]byte(`{"events":[
1078+
{"eventType":"user","eventId":"evt_u1","sessionID":"s_payload","createdAt":"2026-01-01T00:00:01Z","payload":{"role":"user","content":[{"type":"text","text":"hi"}]}},
1079+
{"event_type":"assistant","event_id":"evt_a1","session_id":"s_payload","parentMessageId":"evt_u1","created_at":"2026-01-01T00:00:02Z","body":{"role":"assistant","content":[{"type":"text","text":"hello"}]}}
1080+
]}`), &response); err != nil {
1081+
t.Fatal(err)
1082+
}
1083+
messages := RemoteHistoryTranscriptMessages(response.Events)
1084+
if len(messages) != 2 || messages[0].UUID != "evt_u1" || messages[1].UUID != "evt_a1" {
1085+
t.Fatalf("messages = %#v", messages)
1086+
}
1087+
if messages[0].Type != "user" || messages[0].SessionID != "s_payload" || messages[0].Timestamp != "2026-01-01T00:00:01Z" {
1088+
t.Fatalf("user payload aliases = %#v", messages[0])
1089+
}
1090+
if messages[0].Message == nil || messages[0].Message.Type != contracts.MessageUser || messages[0].Message.SessionID != "s_payload" {
1091+
t.Fatalf("user nested message = %#v", messages[0].Message)
1092+
}
1093+
if messages[1].Type != "assistant" || messages[1].SessionID != "s_payload" || messages[1].Timestamp != "2026-01-01T00:00:02Z" {
1094+
t.Fatalf("assistant payload aliases = %#v", messages[1])
1095+
}
1096+
if messages[1].ParentUUID == nil || *messages[1].ParentUUID != "evt_u1" {
1097+
t.Fatalf("assistant parent alias = %#v", messages[1].ParentUUID)
1098+
}
1099+
if messages[1].Message == nil || messages[1].Message.Type != contracts.MessageAssistant || messages[1].Message.ParentUUID == nil || *messages[1].Message.ParentUUID != "evt_u1" {
1100+
t.Fatalf("assistant nested message = %#v", messages[1].Message)
1101+
}
1102+
}
1103+
10751104
func TestRemoteHistoryTranscriptMessagesAcceptsNumericIDAliases(t *testing.T) {
10761105
var response sessionEventsResponse
10771106
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)