Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .downstream/codex-quality-diagnostics.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ grant 与 trial header 在 handler 开始时移入私有执行上下文,不转

账本存于 `sub2api_plugin_state`:plugin key `codexrip.codex-runtime`,namespace `codex-routing-private`,key `quality-run.<run_id>`。新记录显式保存 `model`、`reasoning_effort` 和凭据主体 `owner_identity`;账本不保存 grant、Key、题面、Cookie 或 STATE 值。

路由资格诊断时期写入的旧记录(无 `model`)按 `gpt-6-astra`/`high` 显示且只读:GET 和 close 仍可用,同一 `run_id` 不能再创建、恢复或发送(409,需换用新 run id)。旧记录的 `scope`、`qualification`、`route_generation`、`route_runtime_generation` 及旧 attempt 字段在重写(如 close)时原样保留;视图只对旧记录只读显示 `route_generation`、`route_expires_at` 和 `connection_fingerprint`,旧 attempt 可能带 `acquire`/`verify` 阶段。关闭旧记录不再清理其遗留的私有 Cookie 状态。
路由资格诊断时期写入的旧记录(无 `model`)按 `gpt-6-astra`/`high` 显示且只读:GET 和 close 仍可用,同一 `run_id` 不能再创建、恢复或发送(409,需换用新 run id)。旧记录的 `scope`、`qualification`、`route_generation`、`route_runtime_generation` 及旧 attempt 字段在重写(如 close)时原样保留;视图只对旧记录只读显示 `route_generation`、`route_expires_at` 和 `connection_fingerprint`,旧 attempt 可能带 `acquire`/`verify` 阶段。旧记录引用的路由 bundle 与 Cookie 状态(`bundle.*`、`clock.*`、`seen.*`)已由迁移 263 删除,账本本身原样保留。

## 运维执行器

Expand Down
43 changes: 29 additions & 14 deletions .downstream/native-domains.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,12 @@ and not stored again; any other key, or a `request_zstd` that is not a boolean

The route acquisition endpoints (`/admin/accounts/codex-tickets/*`,
`/admin/accounts/:id/codex-tickets/*`, `/admin/accounts/:id/codex-routing/validate`
and `/admin/settings/openai-codex-ticket/*`) are removed. Jobs of the retired
kinds `codex_ticket_harvest`, `codex_ticket_stop` and `extension_operation`
still list and read with `retry_eligible=false` and
`retry_unavailable_reason=kind_unsupported`; a retry returns HTTP 409, and their
items still pending fail with `kind_unsupported`. Ordinary account bulk editing
and `/admin/settings/openai-codex-ticket/*`) are removed. Migration 263 deletes
the job rows of the retired kinds `codex_ticket_harvest`, `codex_ticket_stop`
and `extension_operation`; their display names are gone. A job whose kind this
version has no executor for still lists and reads with `retry_eligible=false`
and `retry_unavailable_reason=kind_unsupported`; a retry returns HTTP 409, and
its items still pending fail with `kind_unsupported`. Ordinary account bulk editing
remains HTTP 202; this change does not replace background tasks with synchronous
editing. Existing API-key reveal and account-field protections remain.

Expand Down Expand Up @@ -94,15 +95,23 @@ the first start advances `runtime_generation` once and records the new
configuration hash and version; the encrypted source keeps its old bytes until
the next save.

Data written by the retired route acquisition stays in place and is no longer
read: the `openai_codex_ticket_*` settings, the `tickets`, `routing-demand` and
`proxy-trust` state namespaces, the route keys in `codex-routing-private`
(`bundle.*`, `clock.*`, `seen.*`, `spent.*`, `validation.*`; `quality-run.*` and
`wire.*` stay in use), `sub2api_plugin_leases` rows, the account `extra` keys
`codex_turn_ticket:*`, `codex_ticket_runtime:*`, `codex_harvest_proxy_url` and
`plugin_account_projections`, and job rows of the retired kinds. Migrations 244
and 245 and their triggers are unchanged. A later release removes this data with
a forward migration.
Migration `263_purge_retired_codex_ticket_data.sql` deletes the data written by
the retired route acquisition: the triggers, trigger functions and tables of
migrations 244 and 245; the settings `openai_codex_ticket_enabled` and
`openai_codex_ticket_harvest_proxy_url`; the `codexrip.codex-runtime` state
namespaces `tickets`, `routing-demand` and `proxy-trust` and its
`routing-account` and `tickets` leases; the route and Cookie material in
`codex-routing-private` (`bundle.*`, `clock.*`, `seen.*`, `validation-result.*`);
the account `extra` keys `codex_turn_ticket:*`, `codex_ticket_runtime:*` and
`codex_harvest_proxy_url` and the `codexrip.codex-runtime` entry of
`plugin_account_projections` (the map goes once it is empty); and job rows of
the retired kinds. `codex-routing-private` keeps the ledgers `quality-run.*`,
`validation.*` and `spent.*`, and the `wire.*` observations without the raw
Cookie values (`cookies`) they stored. Accounts without these keys are not
rewritten. Account create, update and bulk edit drop these `extra` keys and that
projection entry, so importing an account export taken before this release does
not bring them back. The encrypted Codex runtime source keeps its bytes until
the next save, as described above.

Migration `254_plugin_job_action_digest.sql` preserves full 64-character historical
task action digests. History and retries continue to use saved targets; unsupported
Expand All @@ -125,6 +134,12 @@ runtime generation or delete its native anchor**. A plain image change does not
perform this reverse conversion. Such a legacy-host rollback has not been run as
part of the native migration and is not an automatic failure path.

Migration 263 has no down path; the data it deletes (listed above) comes back
only from a database dump taken before it. An image that still has the route
acquisition starts without its tickets and routes. Images from
`v0.2.8-codexrip.5` (source `9f7b08dfc`) on never read the 244/245 tables; older
images still query them, so returning to one of those needs that dump.

## Verification

Normal required PR checks validate the native domain packages, account scope,
Expand Down
11 changes: 6 additions & 5 deletions DOWNSTREAM.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,12 @@ request route gates, and their admin endpoints, settings and job kinds. OpenAI
OAuth accounts are scheduled without route qualification and send through their
ordinary proxy and transport. An OAuth Codex `/responses` reply over HTTP that
declares a different model than the request is still rejected. The Codex
runtime setting keeps only request compression. Old jobs of the retired kinds
stay readable but cannot be retried; data the feature wrote stays in place,
unread, until a later release purges it. Quality diagnostics bind an explicit
model and reasoning effort and send through the account's ordinary path; see
[quality diagnostics](.downstream/codex-quality-diagnostics.md).
runtime setting keeps only request compression. Migration 263 deletes the data
the feature wrote, old jobs of the retired kinds included; quality ledgers and
the observed outbound requests stay, without stored Cookie values (see
[native domains](.downstream/native-domains.md)). Quality diagnostics bind an
explicit model and reasoning effort and send through the account's ordinary
path; see [quality diagnostics](.downstream/codex-quality-diagnostics.md).

## Official behavior and downstream contracts

Expand Down
27 changes: 0 additions & 27 deletions backend/internal/repository/account_plugin_projection.go

This file was deleted.

50 changes: 46 additions & 4 deletions backend/internal/repository/account_repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,48 @@ func stripCodexFingerprintSeedFromExtraUpdate(extra map[string]any) map[string]a
return stripped
}

// isRetiredCodexTicketExtraKey matches the account extra keys of the retired
// Codex route acquisition that migration 263 purged.
func isRetiredCodexTicketExtraKey(key string) bool {
return strings.HasPrefix(key, "codex_turn_ticket:") ||
strings.HasPrefix(key, "codex_ticket_runtime:") ||
key == "codex_harvest_proxy_url"
}

// withoutRetiredCodexTicketExtra keeps account writes from storing the data
// migration 263 purged, so importing an account export taken before it cannot
// bring that data back. Like 263 it also drops the codexrip.codex-runtime entry
// of plugin_account_projections, and the map only once no other entry is left.
// extra is returned as is when it holds none of this.
func withoutRetiredCodexTicketExtra(extra map[string]any) map[string]any {
projections, _ := extra["plugin_account_projections"].(map[string]any)
_, projected := projections["codexrip.codex-runtime"]
stripped := make(map[string]any, len(extra))
for key, value := range extra {
if !isRetiredCodexTicketExtraKey(key) {
stripped[key] = value
}
}
if !projected {
if len(stripped) == len(extra) {
return extra
}
return stripped
}
rest := make(map[string]any, len(projections))
for key, value := range projections {
if key != "codexrip.codex-runtime" {
rest[key] = value
}
}
if len(rest) == 0 {
delete(stripped, "plugin_account_projections")
} else {
stripped["plugin_account_projections"] = rest
}
return stripped
}

// NewAccountRepository 创建账户仓储实例。
// 这是对外暴露的构造函数,返回接口类型以便于依赖注入。
func NewAccountRepository(client *dbent.Client, sqlDB *sql.DB, schedulerCache service.SchedulerCache) service.AccountRepository {
Expand Down Expand Up @@ -156,7 +198,7 @@ func createAccountRecord(ctx context.Context, client *dbent.Client, account *ser
SetPlatform(account.Platform).
SetType(account.Type).
SetCredentials(normalizeJSONMap(account.Credentials)).
SetExtra(normalizeJSONMap(withoutPluginAccountProjection(account.Extra))).
SetExtra(normalizeJSONMap(withoutRetiredCodexTicketExtra(account.Extra))).
SetConcurrency(account.Concurrency).
SetPriority(account.Priority).
SetStatus(account.Status).
Expand Down Expand Up @@ -740,7 +782,7 @@ func lockAndMergeAccountProbeExtra(
currentExtra = nil
}
}
extra = preservePluginAccountProjection(extra, currentExtra)
extra = withoutRetiredCodexTicketExtra(extra)
// Preserve the system prompt binding read under the row lock. A stale
// general account editor cannot overwrite the dedicated binding endpoint.
delete(extra, service.AccountExtraSystemPromptKey)
Expand Down Expand Up @@ -2878,7 +2920,7 @@ func (r *accountRepository) AutoPauseExpiredAccounts(ctx context.Context, now ti
}

func (r *accountRepository) UpdateExtra(ctx context.Context, id int64, updates map[string]any) error {
updates = withoutPluginAccountProjection(updates)
updates = withoutRetiredCodexTicketExtra(updates)
updates = stripCodexFingerprintSeedFromExtraUpdate(updates)
if len(updates) == 0 {
return nil
Expand Down Expand Up @@ -3234,7 +3276,7 @@ func (r *accountRepository) BulkUpdate(ctx context.Context, ids []int64, updates
return 0, nil
}
updates.Extra = stripCodexFingerprintSeedFromExtraUpdate(updates.Extra)
updates.Extra = withoutPluginAccountProjection(updates.Extra)
updates.Extra = withoutRetiredCodexTicketExtra(updates.Extra)

setClauses := make([]string, 0, 8)
args := make([]any, 0, 8)
Expand Down
22 changes: 18 additions & 4 deletions backend/internal/repository/account_repo_model_context_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -183,8 +183,8 @@ func TestLockAndMergeAccountProbeExtraUsesLockedModelContextValues(t *testing.T)
}

// This path serves every account update: a non-object stored extra must not
// fail the edit, only skip carrying locked values over. A dormant scheduling
// projection supplied by the editor is still never written.
// fail the edit, only skip carrying locked values over. Data of the retired
// Codex route acquisition supplied by the editor or an import is never written.
func TestLockAndMergeAccountExtraDegradesOnUnparsableExtra(t *testing.T) {
db, mock, err := sqlmock.New()
require.NoError(t, err)
Expand All @@ -195,14 +195,28 @@ func TestLockAndMergeAccountExtraDegradesOnUnparsableExtra(t *testing.T) {
WithArgs(int64(41), service.PlatformOpenAI, service.AccountTypeOAuth, `{"access_token":"test"}`, nil).
WillReturnRows(sqlmock.NewRows([]string{"identity_unchanged", "ollama_group_unchanged", "ollama_proxy_unchanged", "enabled", "rate_sync_enabled", "snapshot", "ollama_session", "ollama_auto", "ollama_snapshot", "model_context_overrides", "upstream_model_metadata", "current_extra", "opencode_group_unchanged", "opencode_auto", "opencode_snapshot"}).
AddRow(true, false, true, nil, nil, nil, nil, nil, nil, nil, nil, []byte(`[1,2,3]`), false, nil, nil))
account := &service.Account{ID: 41, Platform: service.PlatformOpenAI, Type: service.AccountTypeOAuth, Credentials: map[string]any{"access_token": "test"}, Extra: map[string]any{service.NativeCodexAccountProjectionKey: map[string]any{"forged": true}, "new_admin_setting": true}}
account := &service.Account{ID: 41, Platform: service.PlatformOpenAI, Type: service.AccountTypeOAuth, Credentials: map[string]any{"access_token": "test"}, Extra: map[string]any{"plugin_account_projections": map[string]any{"codexrip.codex-runtime": map[string]any{"forged": true}}, "codex_turn_ticket:gpt-6-astra": map[string]any{"state": "forged"}, "codex_harvest_proxy_url": "http://proxy.example.test:8080", "new_admin_setting": true}}
extra, err := lockAndMergeAccountProbeExtra(context.Background(), client, account, nil, nil)
require.NoError(t, err, "unparsable extra must not fail the account update")
require.Equal(t, true, extra["new_admin_setting"])
require.NotContains(t, extra, service.NativeCodexAccountProjectionKey)
for _, key := range []string{"plugin_account_projections", "codex_turn_ticket:gpt-6-astra", "codex_harvest_proxy_url"} {
require.NotContains(t, extra, key)
}
require.NoError(t, mock.ExpectationsWereMet())
}

// Like migration 263, only the codexrip.codex-runtime projection goes; another
// plugin's entry and a key that merely shares a name stay.
func TestWithoutRetiredCodexTicketExtraKeepsOtherEntries(t *testing.T) {
kept := map[string]any{"codex_turn_ticket_summary": "not the retired prefix", "plugin_account_projections": map[string]any{"acme.other-plugin": true}}
require.Equal(t, kept, withoutRetiredCodexTicketExtra(kept))
require.Equal(t, kept, withoutRetiredCodexTicketExtra(map[string]any{
"codex_turn_ticket_summary": "not the retired prefix",
"codex_ticket_runtime:gpt-6-astra": map[string]any{"phase": "ready"},
"plugin_account_projections": map[string]any{"codexrip.codex-runtime": map[string]any{"identity": "owner"}, "acme.other-plugin": true},
}))
}

func TestShouldEnqueueSchedulerOutboxForExtraUpdatesModelContextKeys(t *testing.T) {
for _, key := range []string{
service.ModelContextOverridesExtraKey,
Expand Down
Loading
Loading