From db46d873dddf264144e6ca909a060131e227a184 Mon Sep 17 00:00:00 2001 From: SaladDay <1203511142@qq.com> Date: Sat, 24 Oct 2026 16:37:15 +0000 Subject: [PATCH] Accept deleted Sessions as list cursors --- contracts/agents-api/index.md | 2 +- contracts/agents-api/wire-semantics.md | 2 +- contracts/agents-api/zh/index.md | 4 +-- contracts/agents-api/zh/wire-semantics.md | 4 +-- .../core/internal/db/queries/sessions.sql | 3 ++ .../core/internal/db/sqlc/sessions.sql.go | 21 ++++++++++++ .../postgres/sessionpg/session_reads.go | 3 +- .../postgres/sessionpg/session_reads_test.go | 34 +++++++++++++++++++ 8 files changed, 66 insertions(+), 7 deletions(-) create mode 100644 services/core/internal/persistence/postgres/sessionpg/session_reads_test.go diff --git a/contracts/agents-api/index.md b/contracts/agents-api/index.md index a1b26c7d9..0c42e051b 100644 --- a/contracts/agents-api/index.md +++ b/contracts/agents-api/index.md @@ -61,7 +61,7 @@ Each item is Core's deliberate or native behavior where the official service beh **Lists** ([lists](./wire-semantics.md#lists)) -- A deleted Agent or Session used as a cursor returns 404; the official service still pages from it. +- A deleted Agent used as a cursor returns 404; the official service still pages from it. - A Turn cursor from another Session, a Credential cursor equal to its Vault ID, and a Skills cursor that is not a Skill ID return 404. - Vault and Credential lists clamp a negative `limit`, as the pinned SDK describes; the official service returns 400. diff --git a/contracts/agents-api/wire-semantics.md b/contracts/agents-api/wire-semantics.md index 5b961639e..3146bff2b 100644 --- a/contracts/agents-api/wire-semantics.md +++ b/contracts/agents-api/wire-semantics.md @@ -126,7 +126,7 @@ A `limit` that is not a decimal integer, including an empty value, returns 400 ` ### Cursors -`after` names a resource of the same list, inside its already resolved parent and tenant. The parent is resolved first: a missing or foreign parent returns its 404 before the cursor is read. A cursor that does not resolve, whether random, malformed, of another type, of another parent, deleted or of another tenant, returns: +`after` names a resource of the same list, inside its already resolved parent and tenant. The parent is resolved first: a missing or foreign parent returns its 404 before the cursor is read. A cursor that does not resolve, whether random, malformed, of another type, of another parent, deleted or of another tenant, returns the response below. A deleted Session still resolves as a Sessions cursor and the page continues from its position, as on the official service; a deleted Agent does not resolve: | Lists | Response | | --- | --- | diff --git a/contracts/agents-api/zh/index.md b/contracts/agents-api/zh/index.md index 880190c94..816473583 100644 --- a/contracts/agents-api/zh/index.md +++ b/contracts/agents-api/zh/index.md @@ -1,7 +1,7 @@ --- title: "OpenAI 兼容性" source: contracts/agents-api/index.md -source_hash: 685e26e496e369c1eb2b1b093ec2e26e6fb6fbdf082ec1b877d9c638b0ac17e6 +source_hash: d3d542a34c7cf38c798a2ec85373f9d554f83892f7c47b6773783b7dcf230c62 --- Core 实现下方固定版本的 OpenAI Agents API。本台账列出各项资源的覆盖情况、相对于官方服务的所有差异以及已知缺口。 @@ -64,7 +64,7 @@ Core 自身字段位于 `x_agents_core` 中([Core extensions](../../../docs/zh **列表**([lists](wire-semantics.md#lists)) -- 将已删除的 Agent 或 Session 用作游标时会返回 404;官方服务仍可从该游标继续分页。 +- 将已删除的 Agent 用作游标时会返回 404;官方服务仍可从该游标继续分页。 - 使用来自其他 Session 的 Turn 游标、与 Vault ID 相等的 Credential 游标,或不是 Skill ID 的 Skills 游标时,都会返回 404。 - Vault 和 Credential 列表会按照固定版本 SDK 的描述钳制负数 `limit`;官方服务返回 400。 diff --git a/contracts/agents-api/zh/wire-semantics.md b/contracts/agents-api/zh/wire-semantics.md index 2c6c53efe..4c807eddd 100644 --- a/contracts/agents-api/zh/wire-semantics.md +++ b/contracts/agents-api/zh/wire-semantics.md @@ -1,7 +1,7 @@ --- title: "API 行为" source: contracts/agents-api/wire-semantics.md -source_hash: ba9c6565567c1b4dcffb0c8f18731372b9e3c95e25f84dc0c3c2782c94161eb2 +source_hash: 1d087e0f29e20f091416cc7bd3293e754c610f2335e2d66456f5205706d9983a --- 已锁定版本的 OpenAI Python SDK([upstream.json](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/contracts/agents-api/upstream.json))定义了 `/v1` 路由、字段和类型。本页面说明这些类型未作规定之处 Core 的行为,例如状态码、错误字段、默认值和列表边界,以及 Core 与官方服务存在差异的地方。[OpenAI 兼容性](index.md) 列出了这些差异和尚存缺口;[Sessions, events and history](sessions-events.md)、[message content](message-content.md)、[Vaults](vaults.md)、[source Files and Skills](source-files.md) 和 [Environment files and Artifacts](environment-files.md) 分别负责各自资源的规则。 @@ -128,7 +128,7 @@ Vault 和 Credential 列表接受标量 `status`、`status[]` 条目或两者, ### 游标 {#cursors} -`after` 指定同一列表中的某个资源,并且该资源必须位于已经解析出的父资源和租户内。首先解析父资源:父资源缺失或属于外部租户时,会先返回其 404,然后才读取游标。无法解析的游标,无论是随机值、格式错误、类型不同、父资源不同、已删除还是属于其他租户,均返回: +`after` 指定同一列表中的某个资源,并且该资源必须位于已经解析出的父资源和租户内。首先解析父资源:父资源缺失或属于外部租户时,会先返回其 404,然后才读取游标。无法解析的游标,无论是随机值、格式错误、类型不同、父资源不同、已删除还是属于其他租户,均返回下表中的响应。已删除的 Session 仍可作为 Sessions 游标解析,并从其位置继续分页,与官方服务一致;已删除的 Agent 无法解析: | 列表 | 响应 | | --- | --- | diff --git a/services/core/internal/db/queries/sessions.sql b/services/core/internal/db/queries/sessions.sql index 2dc88c67e..173d1ea31 100644 --- a/services/core/internal/db/queries/sessions.sql +++ b/services/core/internal/db/queries/sessions.sql @@ -13,6 +13,9 @@ RETURNING *; -- name: GetSession :one SELECT * FROM sessions WHERE tenant_id = $1 AND id = $2 AND deleted_at IS NULL; +-- name: GetSessionCursor :one +SELECT created_at, id FROM sessions WHERE tenant_id = $1 AND id = $2; + -- name: ListSessions :many SELECT * FROM sessions WHERE tenant_id = sqlc.arg(tenant_id) AND deleted_at IS NULL diff --git a/services/core/internal/db/sqlc/sessions.sql.go b/services/core/internal/db/sqlc/sessions.sql.go index 00b4db5e8..5d986c08a 100644 --- a/services/core/internal/db/sqlc/sessions.sql.go +++ b/services/core/internal/db/sqlc/sessions.sql.go @@ -130,6 +130,27 @@ func (q *Queries) GetSession(ctx context.Context, arg GetSessionParams) (Session return i, err } +const getSessionCursor = `-- name: GetSessionCursor :one +SELECT created_at, id FROM sessions WHERE tenant_id = $1 AND id = $2 +` + +type GetSessionCursorParams struct { + TenantID pgtype.UUID `json:"tenant_id"` + ID pgtype.UUID `json:"id"` +} + +type GetSessionCursorRow struct { + CreatedAt pgtype.Timestamptz `json:"created_at"` + ID pgtype.UUID `json:"id"` +} + +func (q *Queries) GetSessionCursor(ctx context.Context, arg GetSessionCursorParams) (GetSessionCursorRow, error) { + row := q.db.QueryRow(ctx, getSessionCursor, arg.TenantID, arg.ID) + var i GetSessionCursorRow + err := row.Scan(&i.CreatedAt, &i.ID) + return i, err +} + const listSessions = `-- name: ListSessions :many SELECT id, tenant_id, engine, metadata, idempotency_key, request_hash, created_at, configuration, event_sequence, creation_request_hash, deleted_at, creator_kind, creator_id FROM sessions WHERE tenant_id = $1 AND deleted_at IS NULL diff --git a/services/core/internal/persistence/postgres/sessionpg/session_reads.go b/services/core/internal/persistence/postgres/sessionpg/session_reads.go index e38c2bd20..dc0109247 100644 --- a/services/core/internal/persistence/postgres/sessionpg/session_reads.go +++ b/services/core/internal/persistence/postgres/sessionpg/session_reads.go @@ -39,7 +39,8 @@ func (s *Store) ListSessions(ctx context.Context, tenantID, cursor string, limit err = s.units.Snapshot(ctx, func(ctx context.Context, tx pgx.Tx) error { q := sqlc.New(tx) if cursor != "" { - after, err := q.GetSession(ctx, sqlc.GetSessionParams{TenantID: tenant, ID: pgunit.PathID(cursor)}) + // A deleted Session still anchors the page position. + after, err := q.GetSessionCursor(ctx, sqlc.GetSessionCursorParams{TenantID: tenant, ID: pgunit.PathID(cursor)}) if err != nil { return err } diff --git a/services/core/internal/persistence/postgres/sessionpg/session_reads_test.go b/services/core/internal/persistence/postgres/sessionpg/session_reads_test.go new file mode 100644 index 000000000..beddd9087 --- /dev/null +++ b/services/core/internal/persistence/postgres/sessionpg/session_reads_test.go @@ -0,0 +1,34 @@ +package sessionpg + +import ( + "errors" + "testing" + + "github.com/google/uuid" + + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/pgtest" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/pgunit" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" +) + +func TestListSessionsPagesFromDeletedCursor(t *testing.T) { + pool := pgtest.Open(t) + store := New(pgunit.NewPool(pool), pgtest.CredentialKey(t)) + tenant := uuid.New() + var ids []string + for i := range 3 { + id := uuid.New() + exec(t, pool, `INSERT INTO sessions(id, tenant_id, engine, idempotency_key, request_hash, configuration, created_at) + VALUES ($1, $2, 'codex', $3, 'hash', '{}', now() + $4 * interval '1 second')`, id, tenant, id.String(), i) + ids = append(ids, id.String()) + } + exec(t, pool, `UPDATE sessions SET deleted_at = now() WHERE id = $1`, ids[1]) + + page, err := store.ListSessions(t.Context(), tenant.String(), ids[1], 10, false, nil) + if err != nil || len(page.Sessions) != 1 || page.Sessions[0].ID != ids[0] { + t.Fatalf("deleted cursor: %+v %v", page, err) + } + if _, err := store.ListSessions(t.Context(), uuid.NewString(), ids[1], 10, false, nil); !errors.Is(err, sessions.ErrNotFound) { + t.Fatalf("foreign tenant cursor: %v", err) + } +}