fix(home): request the daily card on the same condition that renders it
Independent Staging Quality Gate / validate (pull_request) Successful in 9m16s
Independent Staging Quality Gate / publish (pull_request) Has been skipped
Independent Staging Quality Gate / validate (push) Successful in 9m44s
Independent Staging Quality Gate / publish (push) Successful in 8m41s

The daily starlanguage card sat on "正在结合你的星盘写今天的星语。" forever for
every account whose birth time was usable. Its effect bailed out on
birthTimeDisplayState(profile), which returns a value precisely when the
birth time is candidate, accepted or confirmed, so the request went out
only for accounts that had nothing to read. The guard predates the Agent
rewrite and was masked by the written-in client fallback that rewrite
deleted. It now gates on personalChartAvailable, the same fact the card
uses to render personal content, and retries once before admitting that
today has no card.

The route stops letting one engine call take the whole card down
silently: /api/chart fails into a named reason like the other four
layers, and the engine and agent budgets leave room for a cold chart and
an observed 30s generation inside the 60s ceiling.

The home also had three greeting implementations. The hero heading drew
from a static pool while the time-aware greeting lived elsewhere and the
Agent's own greeting was overwritten client-side into a field nothing
rendered. createStartGreeting now exposes its salutation and question
halves, the hero uses both, and the served greeting reaches the hero note.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jesse_Chen
2026-08-18 12:00:53 +08:00
co-authored by Cursor
parent 10ae149c58
commit a12f57970b
8 changed files with 199 additions and 106 deletions
@@ -20,15 +20,19 @@ export const maxDuration = 60;
type Profile = GlobalBirthProfile & { birthTimeStatus?: string };
type BirthPayload = NonNullable<Awaited<ReturnType<typeof dailyProfilePayload>>>;
type CacheEntry = { readonly day: string; readonly card: DailyStarlanguageCard };
type GenerationFailure = "chart_unavailable" | "model_unavailable" | "agent_generation_failed";
type Generated =
| { readonly kind: "card"; readonly card: DailyStarlanguageCard }
| { readonly kind: "failed"; readonly reason: GenerationFailure };
const jyotishApiBase = process.env.JYOTISH_API_BASE ?? "http://127.0.0.1:5200";
const engineTimeoutMs = 8_000;
const agentTimeoutMs = 30_000;
const engineTimeoutMs = 20_000;
const agentTimeoutMs = 45_000;
const cacheLimit = 500;
const state = globalThis as typeof globalThis & {
jyotishaDailyStarlanguageCache?: Map<string, CacheEntry>;
jyotishaDailyStarlanguagePending?: Map<string, Promise<DailyStarlanguageCard | null>>;
jyotishaDailyStarlanguagePending?: Map<string, Promise<Generated>>;
};
function cache() {
@@ -85,9 +89,12 @@ function chartPoints(chart: Record<string, unknown>) {
}
async function collectLayers(payload: BirthPayload, today: string) {
const chart = await fetchEngine("/api/chart", payload);
const points = chartPoints(chart);
if (!points) return null;
const chart = await fetchEngine("/api/chart", payload).catch((error: unknown) => {
console.warn("daily_starlanguage_chart_unavailable", error);
return null;
});
const points = chart === null ? null : chartPoints(chart);
if (chart === null || !points) return null;
const tomorrow = new Date(`${today}T00:00:00.000Z`);
tomorrow.setUTCDate(tomorrow.getUTCDate() + 1);
const withPoints = { ...payload, planets: points.planets, ascendant: points.ascendant };
@@ -106,21 +113,22 @@ async function collectLayers(payload: BirthPayload, today: string) {
return { chart, vimshottari, narayana, varga, transit };
}
async function generateCard(payload: BirthPayload, profile: Profile, today: string) {
async function generateCard(payload: BirthPayload, profile: Profile, today: string): Promise<Generated> {
const layers = await collectLayers(payload, today);
if (!layers) return null;
if (!layers) return { kind: "failed", reason: "chart_unavailable" };
const modelId = process.env.DAILY_STARLANGUAGE_MODEL_ID?.trim() || "deepseek-v4-flash";
const catalog = await loadLanguageModelCatalog();
const model = catalog.models.find((entry) => entry.id === modelId)
?? catalog.models.find((entry) => entry.id === catalog.defaultModelId)
?? null;
if (!model) return null;
if (!model) return { kind: "failed", reason: "model_unavailable" };
const evidence = dailyStarlanguageEvidence(layers, today, profile.birthTimeStatus === "confirmed");
const result = await getDailyStarlanguageAgent(model).generate(
[{ role: "user", content: dailyStarlanguagePrompt(evidence) }],
{ abortSignal: AbortSignal.timeout(agentTimeoutMs) },
);
return parseDailyStarlanguageText(result.text);
const card = parseDailyStarlanguageText(result.text);
return card ? { kind: "card", card } : { kind: "failed", reason: "agent_generation_failed" };
}
function unavailable(reason: string) {
@@ -156,19 +164,19 @@ export async function POST(request: Request) {
}
const inFlight = pending().get(key) ?? generateCard(payload, profile, today)
.catch((error: unknown) => {
.catch((error: unknown): Generated => {
console.warn("daily_starlanguage_generation_failed", error);
return null;
return { kind: "failed", reason: "agent_generation_failed" };
})
.finally(() => pending().delete(key));
pending().set(key, inFlight);
const card = await inFlight;
if (!card) return unavailable("agent_generation_failed");
writeCache(key, today, card);
const generated = await inFlight;
if (generated.kind === "failed") return unavailable(generated.reason);
writeCache(key, today, generated.card);
return NextResponse.json({
status: "ok",
card,
card: generated.card,
source: "agent",
claim_status: "exploratory_unvalidated",
boundary: "not_deterministic_prediction",