Skip to content

Commit 6f59228

Browse files
committed
feat(upstreams): track saved reset first-seen metadata
Store sanitized available_expirations rows with pooler-owned first_seen_at while preserving legacy available_expires_at metadata. Backfill existing legacy expiration arrays with migration-run timestamps, surface first-seen values in admin list and cockpit views, and document the compatibility/privacy contract. Verification covered focused saved-reset/admin suites, browser QA, mix precommit, mix quality, and global review/debugging gates. Routing and redemption policy behavior are unchanged.
1 parent 082bdda commit 6f59228

12 files changed

Lines changed: 1018 additions & 40 deletions

File tree

docs-site/src/content/docs/operators/upstreams.mdx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,9 @@ Manual redemption is enabled only when the upstream is not deleted or disabled,
102102

103103
Auto redemption is a per-upstream opt-in policy available from the account-card `Saved resets` dialog and the upstream cockpit. New accounts default to auto redemption off. Operators choose whether automatic redemption waits until weekly quota is blocked or may start earlier when fresh weekly quota evidence shows every eligible candidate account is near the configured `Near-limit threshold`. The blocked mode is still allowed to rescue a saved reset with a known expiration inside the next 24 hours when that same account already has weekly usage to recover and the natural weekly reset is not close. The `Natural reset buffer` prevents spending when the weekly quota will reset naturally soon, and `Keep credits` reserves banked resets from automatic use.
104104

105-
Saved reset observations and redemption attempts stay metadata-only on `upstream_identities.metadata` under `saved_resets` and `saved_reset_redemption`. The saved-reset observation may include sanitized aggregate expiration fields such as `available_expires_at`, `next_expires_at`, `expires_observed_at`, and `expires_refresh_attempted_at`; it must not include raw provider credit objects, identifiers, titles, descriptions, or redemption request ids. Policy fields live on the upstream identity as `saved_reset_auto_redeem_enabled`, `saved_reset_auto_redeem_trigger_mode`, `saved_reset_auto_redeem_quota_threshold_percent`, `saved_reset_auto_redeem_min_blocked_minutes`, and `saved_reset_auto_redeem_keep_credits`. Do not copy provider payloads, raw credit objects or identifiers, redemption request identifiers, token material, or account secrets into metadata, logs, docs, or tests.
105+
Saved reset observations and redemption attempts stay metadata-only on `upstream_identities.metadata` under `saved_resets` and `saved_reset_redemption`. The saved-reset observation may include sanitized expiration rows in `available_expirations`, where each row has an `expires_at` timestamp and a pooler-owned `first_seen_at` timestamp. Legacy `available_expires_at` stays available for compatibility, and `next_expires_at`, `expires_observed_at`, and `expires_refresh_attempted_at` remain sanitized aggregate fields. Historical rows are backfilled with the migration time as `first_seen_at`; future observations preserve `first_seen_at` only while the same expiration remains present, and disappeared expirations are not retained. Missing or invalid provider expiration fields are not guessed.
106+
107+
Policy fields live on the upstream identity as `saved_reset_auto_redeem_enabled`, `saved_reset_auto_redeem_trigger_mode`, `saved_reset_auto_redeem_quota_threshold_percent`, `saved_reset_auto_redeem_min_blocked_minutes`, and `saved_reset_auto_redeem_keep_credits`. Do not copy provider payloads, raw provider credit objects, raw credit identifiers, titles, descriptions, request ids, redemption request identifiers, token material, secrets, or account credentials into metadata, logs, docs, or tests.
106108

107109
## Readiness States
108110

lib/codex_pooler/upstreams/reconciliation/pool_reconciliation.ex

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -244,7 +244,10 @@ defmodule CodexPooler.Upstreams.Reconciliation.PoolReconciliation do
244244
metadata:
245245
identity.metadata
246246
|> Kernel.||(%{})
247-
|> Map.put("saved_resets", SavedResets.usage_snapshot(payload, observed_at, usage_url)),
247+
|> Map.put(
248+
"saved_resets",
249+
SavedResets.usage_snapshot(payload, observed_at, usage_url, identity)
250+
),
248251
updated_at: observed_at
249252
})
250253
|> Repo.update!()

lib/codex_pooler/upstreams/saved_reset_redemption.ex

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -552,23 +552,51 @@ defmodule CodexPooler.Upstreams.SavedResetRedemption do
552552
defp update_saved_reset_count!(identity, snapshot, available_count, observed_at) do
553553
metadata = identity.metadata || %{}
554554

555+
saved_reset_metadata =
556+
%{
557+
"status" => "reported",
558+
"available_count" => available_count,
559+
"source" => "codex_reset_credits_api",
560+
"path_style" => snapshot.path_style,
561+
"observed_at" => DateTime.to_iso8601(observed_at),
562+
"usage_path" => snapshot.usage_path,
563+
"reason" => nil
564+
}
565+
|> Map.merge(expiration_metadata_from_snapshot(snapshot))
566+
555567
identity
556568
|> UpstreamIdentity.changeset(%{
557-
metadata:
558-
Map.put(metadata, "saved_resets", %{
559-
"status" => "reported",
560-
"available_count" => available_count,
561-
"source" => "codex_reset_credits_api",
562-
"path_style" => snapshot.path_style,
563-
"observed_at" => DateTime.to_iso8601(observed_at),
564-
"usage_path" => snapshot.usage_path,
565-
"reason" => nil
566-
}),
569+
metadata: Map.put(metadata, "saved_resets", saved_reset_metadata),
567570
updated_at: observed_at
568571
})
569572
|> Repo.update!()
570573
end
571574

575+
@spec expiration_metadata_from_snapshot(SavedResets.snapshot_projection()) :: map()
576+
defp expiration_metadata_from_snapshot(snapshot) do
577+
%{
578+
"available_expires_at" => snapshot.available_expires_at,
579+
"available_expirations" => stored_available_expiration_rows(snapshot.available_expirations),
580+
"next_expires_at" => snapshot.next_expires_at,
581+
"expires_observed_at" => snapshot.expires_observed_at,
582+
"expires_refresh_attempted_at" => snapshot.expires_refresh_attempted_at
583+
}
584+
end
585+
586+
@spec stored_available_expiration_rows([SavedResets.available_expiration_row()]) :: [map()]
587+
defp stored_available_expiration_rows(rows) when is_list(rows) do
588+
rows
589+
|> Enum.map(fn
590+
%{expires_at: expires_at, first_seen_at: first_seen_at}
591+
when is_binary(expires_at) and is_binary(first_seen_at) ->
592+
%{"expires_at" => expires_at, "first_seen_at" => first_seen_at}
593+
594+
_row ->
595+
nil
596+
end)
597+
|> Enum.reject(&is_nil/1)
598+
end
599+
572600
defp mark_stale_redemption_failed!(identity, redemption, finished_at) do
573601
metadata = identity.metadata || %{}
574602
generation = next_generation(metadata)

0 commit comments

Comments
 (0)