Skip to content

Commit 4258802

Browse files
author
SqlRush
committed
Accept resource attribute remote history events
1 parent 66d1746 commit 4258802

5 files changed

Lines changed: 61 additions & 2 deletions

File tree

docs/cc-100-roadmap.md

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

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

491+
本轮补充:remote history 普通事件数组现在也接受 JSON:API/resource-style 元素,事件 payload 可放在 `attributes``properties` 里,并使用外层 resource `id` 作为 SDK event ID fallback。
492+
491493
本轮补充:remote history event-list 字段现在接受 keyed event map,例如 `events: {"evt_1": {...}}`,会按 key 稳定排序并在事件缺 ID 时用 map key 作为 fallback event ID。
492494

493495
本轮补充:contract content block `type` 解码现在会归一化 `toolUse`/`tool-result`/`cacheEdits`/`inputImage`/`chain-of-thought` 等 camel/kebab/compact 别名,transcript resume 可保留为规范 block type。

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 pagination 现在也接受 `previousPageToken`/`prevPageToken`/`olderPageToken``previousToken`/`prevToken`/`olderToken` 及 snake_case 形式,响应字段、link object 和 link URL query 参数都会归一到续抓 before-id。
201201
- 本轮补充:remote history pagination 现在也接受相邻 before-cursor aliases,包括 `before``beforeID``olderThan``endingBefore``untilId`,响应字段、link object 和 link URL query 参数都会归一到续抓 before-id。
202202
- 本轮补充:remote history pagination 现在也接受 OData next-link 字段 `@odata.nextLink``odata.nextLink``__next`,并从 `$skiptoken`/`skipToken` link query 参数提取续抓 cursor。
203+
- 本轮补充:remote history 普通事件数组现在也接受 JSON:API/resource-style 元素,事件 payload 可放在 `attributes``properties` 里,并使用外层 resource `id` 作为 SDK event ID fallback。
203204
- 本轮补充:remote history fetch 把 HTTP 204 和 200 空 body 视为空的终止页,避免空历史响应被标成 incomplete 或触发 JSON EOF。
204205
- 本轮补充:contract `ID` JSON 读取现在接受 JSON number/null,remote history event/message/session/parent ID alias 可继承数字 ID 兼容面并在 transcript materialization 中保留为字符串。
205206
- 本轮补充:remote history response parser 会递归解包 `data.session.events``data.projectSession.eventConnection``data.viewer.session.events``data.node.eventConnection``conversation``remoteHistory``_embedded` 等 GraphQL/session/HAL wrapper,继续复用 `nodes`/`edges[].node``pageInfo` pagination 解析。

docs/first-second-parity-audit.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,7 @@ M7 progress now includes:
347347
- `internal/tui`: interaction script loading now recurses through outer API/fixture wrappers such as `data`/`payload`/`body`/`result`/`response`/`recording`/`session`/`run`.
348348
- `internal/contracts`/`internal/session`: remote history `SDKEvent` message extraction now recurses through nested payload wrappers such as `record`/`entry`/`item`/`event`/`result`/`response`/`output`.
349349
- `internal/session`: remote-history event arrays now unwrap element-level wrappers such as `event`/`record`/`entry`/`item`/`resource`/`value` and wrapper-only `data`/`payload`/`body`, using element cursors as event ID fallback outside GraphQL edges.
350+
- `internal/session`: remote-history event arrays now accept JSON:API/resource-style elements with event payloads inside `attributes` or `properties`, preserving the outer resource `id` as the SDK event ID fallback.
350351
- `internal/contracts`/`internal/session`: content block `type` decoding now normalizes camel/kebab/compact aliases such as `toolUse`, `tool-result`, `cacheEdits`, `inputImage`, and `chain-of-thought`.
351352
- `internal/session`: image history dimensions now default display size to original size when only `width`/`height` or original dimensions are present, preserving source metadata for single-size image fixtures.
352353
- `internal/session`: pasted-content history type decoding now normalizes aliases such as `inputImage`/`pasted-image`/`input_text`/`pasted-text` to canonical `image`/`text` for runtime and stored history.

internal/session/remote_history.go

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -742,11 +742,13 @@ func decodeRemoteHistoryEventElement(name string, index int, data json.RawMessag
742742
return contracts.SDKEvent{}, nil
743743
}
744744
cursor := ""
745+
resourceID := ""
745746
rawEvent := data
746747
if data[0] == '{' {
747748
var fields map[string]json.RawMessage
748749
if err := json.Unmarshal(data, &fields); err == nil {
749750
cursor = remoteHistoryStringField(fields, "cursor")
751+
resourceID = remoteHistoryStringField(fields, "id", "event_id", "eventId", "uuid")
750752
if nested := remoteHistoryWrappedEventRaw(fields); nested != nil {
751753
rawEvent = nested
752754
}
@@ -757,16 +759,21 @@ func decodeRemoteHistoryEventElement(name string, index int, data json.RawMessag
757759
return contracts.SDKEvent{}, fmt.Errorf("%s[%d]: %w", name, index, err)
758760
}
759761
if event.ID == "" {
760-
event.ID = contracts.ID(cursor)
762+
event.ID = contracts.ID(firstNonEmpty(resourceID, cursor))
761763
}
762764
return event, nil
763765
}
764766

765767
func remoteHistoryWrappedEventRaw(fields map[string]json.RawMessage) json.RawMessage {
768+
if remoteHistoryRecognizedEventType(fields) == "" {
769+
if nested := firstObjectRawField(fields, "attributes", "properties"); nested != nil {
770+
return nested
771+
}
772+
}
766773
if remoteHistoryHasDirectEventFields(fields) {
767774
return nil
768775
}
769-
return firstObjectRawField(fields, "node", "event", "record", "entry", "item", "resource", "value", "data", "payload", "body", "result", "response", "output")
776+
return firstObjectRawField(fields, "node", "event", "record", "entry", "item", "resource", "value", "attributes", "properties", "data", "payload", "body", "result", "response", "output")
770777
}
771778

772779
func remoteHistoryHasDirectEventFields(fields map[string]json.RawMessage) bool {

internal/session/remote_history_test.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -888,6 +888,54 @@ func TestFetchRemoteHistoryAcceptsWrappedEventArrayItems(t *testing.T) {
888888
}
889889
}
890890

891+
func TestFetchRemoteHistoryAcceptsResourceAttributeEvents(t *testing.T) {
892+
var seen []url.Values
893+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
894+
seen = append(seen, r.URL.Query())
895+
w.Header().Set("Content-Type", "application/json")
896+
switch r.URL.Query().Get("before_id") {
897+
case "":
898+
_, _ = w.Write([]byte(`{
899+
"data": [
900+
{"id":"evt_attr_1","type":"session-events","attributes":{"eventType":"status","sessionID":"s","status":"latest"}},
901+
{"id":"evt_attr_2","type":"session-events","attributes":{"event_type":"assistant","session_id":"s","parentMessageID":"evt_attr_1","createdAt":"2026-01-01T00:00:02Z","message":{"type":"assistant","content":[{"type":"text","text":"hello"}]}}}
902+
],
903+
"links": {"prev": "/v1/sessions/s/events?before_id=evt_attr_2"}
904+
}`))
905+
case "evt_attr_2":
906+
_, _ = w.Write([]byte(`{
907+
"data": [
908+
{"id":"evt_attr_3","type":"session-events","properties":{"role":"user","sessionId":"s","createdAt":"2026-01-01T00:00:03Z","message":{"type":"user","content":[{"type":"text","text":"older"}]}}}
909+
]
910+
}`))
911+
default:
912+
t.Fatalf("unexpected before_id = %q", r.URL.Query().Get("before_id"))
913+
}
914+
}))
915+
defer server.Close()
916+
917+
authCtx := NewRemoteHistoryAuthContext("s", "token", "", auth.OAuthConfig{BaseAPIURL: server.URL})
918+
events, err := FetchRemoteHistory(context.Background(), server.Client(), authCtx, RemoteHistoryFetchOptions{Limit: 3})
919+
if err != nil {
920+
t.Fatal(err)
921+
}
922+
if !events.Complete || events.Pages != 2 || len(events.Events) != 3 || events.NextBeforeID != "" {
923+
t.Fatalf("events = %#v", events)
924+
}
925+
if events.Events[0].ID != "evt_attr_1" || events.Events[0].Status != "latest" || events.Events[0].SessionID != "s" {
926+
t.Fatalf("status resource event = %#v", events.Events[0])
927+
}
928+
if events.Events[1].ID != "evt_attr_2" || events.Events[1].Type != contracts.SDKEventAssistant || events.Events[1].Message == nil || len(events.Events[1].Message.Content) != 1 || events.Events[1].Message.Content[0].Text != "hello" {
929+
t.Fatalf("assistant resource event = %#v", events.Events[1])
930+
}
931+
if events.Events[2].ID != "evt_attr_3" || events.Events[2].Type != contracts.SDKEventUser || events.Events[2].Message == nil || len(events.Events[2].Message.Content) != 1 || events.Events[2].Message.Content[0].Text != "older" {
932+
t.Fatalf("properties resource event = %#v", events.Events[2])
933+
}
934+
if len(seen) != 2 || seen[1].Get("before_id") != "evt_attr_2" {
935+
t.Fatalf("queries = %#v", seen)
936+
}
937+
}
938+
891939
func TestFetchRemoteHistoryAcceptsKeyedEventMaps(t *testing.T) {
892940
var seen []url.Values
893941
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {

0 commit comments

Comments
 (0)