Skip to content

Commit 7869365

Browse files
author
SqlRush
committed
Accept remote history single event pages
1 parent f7b2847 commit 7869365

4 files changed

Lines changed: 73 additions & 0 deletions

File tree

docs/cc-100-roadmap.md

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

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

91+
M6 补充:remote history `data`/`payload`/`response`/`result`/`body` 等 event-list 字段现在也接受单个 SDK event 对象,避免非数组单事件页被当作普通 wrapper 后丢失。
92+
9193
M6 补充:remote history `SDKEvent` 本体现在接受 `eventType`/`event_type`/`role` 类型别名、`createdAt`/`created_at` 时间戳别名,以及 `payload`/`data`/`body`/`serializedMessage` message payload 别名;payload 只有 `role`/`content` 时也能 materialize 成 transcript message。
9294

9395
M6 补充:remote history REST/link 风格分页现在接受 `links`/`_links``next`/`previous`/`prev`/`older` 字符串 URL、`{href,url,uri,link}` 对象,或直接携带 `cursor`/`beforeId`/`lastEvaluatedKey` 等 cursor 字段的 link object,并从 `before_id``beforeId``cursor``pageCursor``previousCursor``prevCursor``beforeCursor``olderCursor``startCursor``endCursor` 等 query 参数提取下一页 before-id。
@@ -390,6 +392,8 @@ M7 补充:prompt history `LogEntry` 读取现在接受 `sessionID`/`session`/`
390392

391393
本轮补充:remote history response parser 会递归解包 `data.session.events``data.projectSession.eventConnection``conversation``remoteHistory` 等 GraphQL/session wrapper,继续复用 `nodes`/`edges[].node``pageInfo` pagination 解析。
392394

395+
本轮补充:remote history event-list 字段现在可直接承载单个 SDK event 对象;`data`/`result` 等字段不再必须是数组或 wrapper 才能进入分页结果。
396+
393397
本轮补充:remote history REST/link 风格分页接受 `links.next`/`links.previous`/`links.prev`/`links.older` 的字符串 URL、`{href,url,uri,link}` 对象,或直接携带 cursor 字段的 link object,并从 before/cursor query 参数或 direct cursor fields 提取续抓 before-id。
394398

395399
本轮补充:remote history HTTP `Link` header 分页接受 `previous`/`prev`/`older`/`next` rel URL,并以 body cursor 优先、header cursor fallback 的方式继续抓取。

docs/first-second-parity-audit.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@ M6 progress now includes:
149149
- `internal/session`: remote-history parsing now also accepts `eventList`/`event_list`, `sessionEvents`/`session_events`, and connection aliases such as `connection`, `eventConnection`, and `sessionEventsConnection`.
150150
- `internal/session`: remote-history parsing now also accepts `value`, `values`, `resources`, and `collection` event-list aliases, plus `edges[].resource`/`edges[].value` node payload aliases.
151151
- `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.
152+
- `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.
152153
- `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.
153154
- `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`.
154155
- `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.

internal/session/remote_history.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,17 @@ func (r *sessionEventsResponse) mergeEventListField(name string, target *[]contr
336336
return nil
337337
}
338338
if data[0] == '{' {
339+
var fields map[string]json.RawMessage
340+
if err := json.Unmarshal(data, &fields); err == nil && remoteHistoryLooksLikeSingleEvent(fields) {
341+
event, err := decodeRemoteHistoryEventElement(name, 0, data)
342+
if err != nil {
343+
return err
344+
}
345+
if *target == nil {
346+
*target = []contracts.SDKEvent{event}
347+
}
348+
return nil
349+
}
339350
var nested sessionEventsResponse
340351
if err := nested.mergeJSON(data); err != nil {
341352
return fmt.Errorf("%s: %w", name, err)
@@ -702,6 +713,31 @@ func remoteHistoryHasDirectEventFields(fields map[string]json.RawMessage) bool {
702713
return false
703714
}
704715

716+
func remoteHistoryLooksLikeSingleEvent(fields map[string]json.RawMessage) bool {
717+
if remoteHistoryRecognizedEventType(fields) != "" {
718+
return true
719+
}
720+
status := remoteHistoryStringField(fields, "status")
721+
if status == "" {
722+
return false
723+
}
724+
return firstNonEmpty(
725+
remoteHistoryStringField(fields, "id", "event_id", "eventId", "uuid"),
726+
remoteHistoryStringField(fields, "session_id", "sessionId", "sessionID", "session_uuid", "sessionUuid", "sessionUUID"),
727+
remoteHistoryStringField(fields, "timestamp", "created_at", "createdAt", "time", "datetime", "dateTime"),
728+
) != ""
729+
}
730+
731+
func remoteHistoryRecognizedEventType(fields map[string]json.RawMessage) string {
732+
for _, name := range []string{"type", "event_type", "eventType", "event", "name", "kind", "role", "messageType", "message_type"} {
733+
switch strings.ToLower(strings.TrimSpace(remoteHistoryStringField(fields, name))) {
734+
case "system", "assistant", "user", "result", "error", "status":
735+
return name
736+
}
737+
}
738+
return ""
739+
}
740+
705741
func firstObjectRawField(raw map[string]json.RawMessage, names ...string) json.RawMessage {
706742
for _, name := range names {
707743
value, ok := raw[name]

internal/session/remote_history_test.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -920,6 +920,38 @@ func TestFetchRemoteHistoryAcceptsGenericResponseWrappers(t *testing.T) {
920920
}
921921
}
922922

923+
func TestFetchRemoteHistoryAcceptsSingleObjectEventPages(t *testing.T) {
924+
var seen []url.Values
925+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
926+
seen = append(seen, r.URL.Query())
927+
w.Header().Set("Content-Type", "application/json")
928+
switch r.URL.Query().Get("before_id") {
929+
case "":
930+
_, _ = w.Write([]byte(`{"data":{"eventType":"status","eventId":"evt_single","session_id":"s","status":"single"},"hasNext":true,"nextCursor":"evt_single"}`))
931+
case "evt_single":
932+
_, _ = w.Write([]byte(`{"result":{"type":"status","id":"evt_result","session_id":"s","status":"result"},"more":false}`))
933+
default:
934+
t.Fatalf("unexpected before_id = %q", r.URL.Query().Get("before_id"))
935+
}
936+
}))
937+
defer server.Close()
938+
939+
authCtx := NewRemoteHistoryAuthContext("s", "token", "", auth.OAuthConfig{BaseAPIURL: server.URL})
940+
events, err := FetchRemoteHistory(context.Background(), server.Client(), authCtx, RemoteHistoryFetchOptions{Limit: 2})
941+
if err != nil {
942+
t.Fatal(err)
943+
}
944+
if !events.Complete || events.Pages != 2 || len(events.Events) != 2 || events.NextBeforeID != "" {
945+
t.Fatalf("events = %#v", events)
946+
}
947+
if events.Events[0].ID != "evt_single" || events.Events[0].Status != "single" || events.Events[1].ID != "evt_result" || events.Events[1].Status != "result" {
948+
t.Fatalf("events = %#v", events.Events)
949+
}
950+
if len(seen) != 2 || seen[1].Get("before_id") != "evt_single" {
951+
t.Fatalf("queries = %#v", seen)
952+
}
953+
}
954+
923955
func TestFetchRemoteHistoryUsesEdgeCursorWhenNodeIDMissing(t *testing.T) {
924956
var seen []url.Values
925957
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {

0 commit comments

Comments
 (0)