Sending cleared the draft and stored a request id before the server reserved usage, so a refresh left a sent question with no agent run.
Co-authored-by: Cursor <cursoragent@cursor.com>
Report pages now scroll inside the chat shell lock, and admin product save forks a draft or retires a published plan instead of rejecting with a generic constraint error.
Co-authored-by: Cursor <cursoragent@cursor.com>
The rebase also applied staging's fix-version edit to the wrong record,
because every record ends with the same boilerplate line; the follow-up
chip record is back to an uncommitted candidate.
Co-authored-by: Cursor <cursoragent@cursor.com>
Measured use of the three chips above the composer was negligible. They
were also not what they appeared to be: the server looked up a fixed
triplet by session theme and passed it as metadata that overrode
anything the model produced, so the same ten hardcoded sets served every
user regardless of question or chart. That is a plausible reason nobody
pressed them.
Both copies of the per-theme table are gone, reply metadata narrows to
the session title, and the two parse entry points collapse into one now
that they return the same shape. The write schema still tolerates a
suggestions field so a client on the previous bundle does not lose its
message mid-deploy, and stored answers containing the legacy hidden
block are still stripped rather than shown raw.
Co-authored-by: Cursor <cursoragent@cursor.com>
The home screen renders all ten domains from the consultation registry,
but the Agent only ever wrote three of them; the other seven were static
registry prompts dressed up as personalized starting points. The payload
now has to cover every domain in registry order, validated as a set
rather than per item, so a short or misordered answer is rejected whole
instead of silently leaving cards on static copy.
The greeting went the other way. Nothing has rendered it since the hero
note was removed, so it leaves the schema, the prompt, and the client
contract rather than costing tokens for text no one reads.
Ten questions take much longer to generate than three, so the route,
the server generation budget, and the client request deadline all grow
together, and the cache version bump forces existing payloads to be
regenerated once under the new shape.
Co-authored-by: Cursor <cursoragent@cursor.com>
The hero carried three lines saying the same thing: a salutation, a
question, and an Agent greeting that repeated both. Removing the third
line would also have removed the only place where accounts without a
usable birth minute are told that unsupported claims will be disclosed,
so that sentence moves to the topic section subheading, where the reader
is about to pick a question.
The starter loading copy claimed three starting points while the page
renders every domain in the registry, only three of which the Agent
writes.
Co-authored-by: Cursor <cursoragent@cursor.com>
BUG-270 and BUG-271 went out as 5caf47a4 and b5bcbaed. Both said
"本地未提交候选", which is the state that makes the ledger useless for
answering "is this on staging yet".
Co-authored-by: Cursor <cursoragent@cursor.com>
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>
The fixture removes the container as soon as startup fails, so every CI
failure reduced to one line: the container is unhealthy. The server's own
account of the refusal is deleted with it, which leaves a reproducible
staging-gate failure undiagnosable from the log alone.
Print the container log before the teardown runs.
Co-authored-by: Cursor <cursoragent@cursor.com>
The daily starlanguage card claimed to be personal but was a four-card
rotation picked by hashing the date and birth place, with the same pool
duplicated as a client fallback. It now collects chart, Vimshottari and
Narayana dasha, D9/D10 and today's transits, hands that evidence to a
dedicated Agent, and keeps the result per account per day in process. A
failed generation says so instead of printing generic advice.
The starter heading is drawn from a pool on each visit, and the
rectification card drops its fine print.
Co-authored-by: Cursor <cursoragent@cursor.com>
A zero-uncertainty exact declaration is accepted server-side as the active
minute, but the account write only answered {ok:true}. Every save path then
kept the draft it submitted, so the first consultation after initialization
asked for unverified_birth_time against an accepted profile and was rejected
with mode_changed before billing.
The account route now returns the status and active minute it derived, and
every profile save adopts that result instead of its own local guess.
Co-authored-by: Cursor <cursoragent@cursor.com>
The button was a sticky child of the scroll container, which reserves
composer-sized bottom padding. Sticky boxes are clamped by their
containing block's content box, so the 12px offset never applied and the
button parked 160px above the composer, on top of the transcript.
Anchor it to the composer wrapper's own top edge instead, so the offset
no longer depends on the --composer-reserve constant.
Co-authored-by: Cursor <cursoragent@cursor.com>
page.tsx changed on staging while this batch waited to land, so the line
numbers in the record no longer resolve. State the base explicitly and tell
future readers to locate Home by name rather than by line.
Co-authored-by: Cursor <cursoragent@cursor.com>
Second collision in one delivery: origin/staging advanced from c8d9ec64 to
e1db5762 while the test suite was running and claimed 256-259, so the three
records in this batch move to 260, 261 and 262 with their cross-references
updated. Titles differ from every remote record, so BUG-254's rule assigns new
numbers rather than merging.
BUG-260's related-records field now states the limit this exposes: searching
for the highest number before appending only holds at the moment of writing
and cannot survive the remote advancing before the push.
Co-authored-by: Cursor <cursoragent@cursor.com>
BUG-257 documents the skills/ COPY collision fixed in the previous commit, as
the workflow requires a fix to land together with its record. It notes the
missing automated regression instead of hiding it: reproducing the defect needs
a real image build, so the entry states the gap and proposes a structural guard.
BUG-258 records that node:22-alpine resolves to Node v22.15.0 while
posthog-node@5.41.0 declares ^20.20.0 || >=22.22.0. Filed as investigating,
not resolved: the version range genuinely does not match and it predates the
Next upgrade, but whether the package actually calls Node 22.22 APIs is
unconfirmed, and both remedies are out of scope for this batch.
Renumbered the React Compiler record from BUG-255 to BUG-256 while rebasing:
origin/staging had taken 254 and 255, and its 255 is an unrelated bug, so
BUG-254's rule applies and a distinct number was assigned rather than merging.
Co-authored-by: Cursor <cursoragent@cursor.com>
Next's output file tracing pulls skills/ into .next/standalone with symlinks
resolved into real directories. The final stage copies the standalone bundle
into /app first, so the later explicit `COPY skills /app/skills` tries to put
the tracked symlink skills/jyotish-vedic-astrology/assets -> ../../assets on
top of a real directory. Whether that is fatal depends on the BuildKit
snapshotter: GitHub's runners tolerate it, Docker 29.1.3 with
containerd-overlayfs refuses with "cannot replace to directory ... with file".
Dropping the traced copy after the build lets the explicit COPY own that tree
deterministically. /app/assets still comes from the standalone bundle, which
outputFileTracingIncludes guarantees, so the symlink resolves.
Verified by building the image and inspecting it: skills/ complete at 1208
files, assets still a symlink that resolves, container boots on Next.js
16.3.1 and serves / with HTTP 200. Reproduced the original failure on the
pre-upgrade commit too, so it predates the Next 16.3.1 upgrade.
Co-authored-by: Cursor <cursoragent@cursor.com>
Diagnosis was unblocked by temporarily installing
babel-plugin-react-compiler and reading its logger events, which the
Rust port swallows. Every failure in Home comes from the compiler, not
from this repository's code: 24 errors under the stable 1.0.0, all
prefixed Todo: (the compiler's own marker for unimplemented syntax) --
13 for try/finally, 9 for throw inside try/catch, 2 for a
non-reorderable MemberExpression. All 13 finally blocks do real cleanup
(clearing a timeout, releasing in-flight guards, resetting loading
flags), so deleting them to please the compiler would trade correctness
for speculative memoization.
Two paths are now closed by measurement rather than assumption. Newer
compiler builds fix the try/finally gap but Home then hits two
consecutive internal Invariant panics, which are compiler bugs. And
project-wide the version makes no difference at all: across 373 files
both versions compile exactly 134 functions, with the same 21 files
failing, so switching implementations buys nothing.
The diagnostic dependency is therefore removed and the lockfile
restored via npm ci. Deliverable is unchanged: config still rolled
back, frontend/src untouched.
Co-authored-by: Cursor <cursoragent@cursor.com>
Decision is to stay rolled back: the round's target was Home, and the
44 functions the compiler does optimize have no measured render benefit
to justify the +2.50% first-load JS. Reopening this should start by
benchmarking those 44, not by flipping the config.
Co-authored-by: Cursor <cursoragent@cursor.com>
The Rust React Compiler in Next 16.3.1 refuses to compile the 2730-line
Home component in frontend/src/app/page.tsx and reports nothing at all:
the build prints the option as enabled, exits 0, and tests stay green
while zero memoization is applied. Six configurations were tried; the
compiler optimized 44 functions elsewhere (including 2 smaller ones in
page.tsx itself) but never Home, and panicThreshold: "all_errors" is a
no-op on the Rust port, so the failing construct could not be located.
The reactCompiler config is therefore rolled back per the task's
stop-loss clause while the Next 16.3.1 upgrade is kept. BUG-255 records
the silent-bailout trap so a future attempt does not mistake "option
enabled and build green" for "compiler actually working". BLOCKED.md
records the two paths out, both of which need authorization: adding
babel-plugin-react-compiler to get diagnostics, or splitting Home.
No business code, test, or CI file was touched.
Co-authored-by: Cursor <cursoragent@cursor.com>
Prerequisite for the Rust React Compiler, which only ships from Next
16.3 onward. package.json changes one line; the 490 lockfile lines are
next itself plus the transitive versions npm re-resolved (sharp,
@img/*, @swc/helpers, and postcss hoisted out of @tailwindcss/postcss).
Verified on this commit: tsc --noEmit clean, next build exit 0, and the
199-file suite at 1592 pass / 0 fail / 0 skipped / 0 todo.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit contains zero hand edits. Running `npm install` in a fresh
worktree rewrites package-lock.json into npm's canonical form, which
produces ~8.6k changed lines of pure noise:
- all 961 `packages` keys re-sorted into npm's lexicographic order
(945 were previously out of spec order)
- 6 entries lose `dev: true` because npm recomputed their dev
reachability: @types/react-dom, escape-string-regexp, js-tokens,
loose-envify, prop-types, react-is
Verified mechanically: 961 packages before and after, no additions, no
removals, and no change to any `version`, `resolved` or `integrity`
field. Isolated into its own commit so the real dependency change that
follows is reviewable instead of being buried in the reordering.
Co-authored-by: Cursor <cursoragent@cursor.com>
The single search field asked people to type a place name and then judge
which of several near-identical results was theirs, which is the one thing
they cannot verify about their own birth record. Province, city and district
are now chosen from the dataset the app already ships, so there is nothing to
type and nothing to disambiguate.
Levels that offer no choice collapse: municipalities show one level, and
prefecture cities without districts stop at the city. A district can be left
as the city centre, which is accurate enough because the chart only needs
coordinates and a timezone.
The timezone is resolved once for the chosen place rather than on every
keystroke, through a dedicated route that both this picker and the Geoapify
path share.
Co-authored-by: Cursor <cursoragent@cursor.com>
A staging consultation asked one question about two domains, career and
wealth. The model planned both, and all four attempts failed identically with
calculation_failed. The server resolves the workflow route from the question
text — an explicit-timing check, then keyword domain_tokens, falling back to
the themes argument only when the text yields nothing — while the frontend
declares strict_workflow_route from the domain it chose. The plan contract then
requires the text-derived route to equal the declared one, and each
RouteContract allows exactly one, so the mismatch became
ConsultationPlanContractError, BadRequest, HTTP 400, workflow_bad_request.
Here "事业" is in the career token list and "财运" is not in the wealth one, so
both calls resolved to career and the wealth call was rejected every time.
Widening the token list would only move the contradiction to the next
phrasing. The frontend sends one Python call per domain with the same question
text, so text routing can agree with at most one domain of a multi-domain plan
and every other domain is refused by construction. Now that 1955ba8c caps the
plan at three domains and merges the per-domain packets into one top-level
contract, two- and three-domain plans are expected to work end to end and this
is what stops them.
Make the server-issued declaration authoritative. declared_workflow_route()
returns the route a complete, version-supported, allowlisted plan declares, and
resolve_route() honours it instead of reading the text; a caller that sends no
plan metadata keeps the text heuristics verbatim, so the MCP strict_workflow
tool and the research callers behave exactly as before. The route packet
records which rule decided, because routing now has two legitimate sources.
This is not a way to silence the 400: the whole packet comes from the declared
domain's RouteDefinition, so the sync steps, the consumer_context required
layers, the evidence packet and the frontend's themes[primary_theme] lookup all
land on the domain that was declared. A call declaring wealth can no longer
execute career and label career evidence as wealth. The contract stays
fail-closed — a route off the allowlist is refused before execution, and
themes, layers, boundary, domains, categories, depth, horizon and precision are
still checked one by one. The surviving resolved_routes check changes meaning
rather than going away: it now asserts the workflow executed what was declared.
The timing prefix "应期与阶段问题:" existed only to inject 应期 so the text
router would agree with the declared route for one domain out of ten. With the
declaration authoritative it fixes nothing and still rewrites the question the
model's answer derives from, so it goes. It influences no other server
behaviour: it matches none of the consumer-context domain regexes, and the
timing route already sets precise_timing_requested.
test_consultation_workflow_domains.py deliberately sent no plan metadata, which
is why this was never caught — its per-domain question happened to route to its
own domain. It now sends the real plan for all ten canonical domains behind one
question whose text routes to career; nine of them fail without this change.
Refs BUG-259.
Co-authored-by: Cursor <cursoragent@cursor.com>
A staging consultation submitted a three-domain plan, calculated all three
successfully in 62.9s, and returned nothing but the ensureFinalResponseText
fallback. The step budget was barely touched, so this is not the exhaustion
c8d9ec64 fixed. toModelDomainPlanContext returns two different shapes: a single
domain flattens the evidence packet to the top level, several domains return only
success, domains and consultations. Every hard output rule in jyotishInstructions
is written against those top-level paths — evidence_contract.answer_policy,
hard_blockers, rectification.boundary, status. None of them resolve in the
multi-domain shape, and under a policy that forbids stating anything the server
evidence does not support, silence is what the instructions ask for.
Merge the packets into one top-level contract shaped exactly like the single
domain one. Merging may only restrict: status takes the worst of ready >
degraded > blocked, hard_blockers and missing_route_layers take the union,
permission booleans need every domain to agree while limitation booleans need
only one, and a field the domains genuinely disagree on is reported as
unresolved rather than decided. available_layers is the one permission-shaped
union, because a layer really was computed for some domain and denying it would
deny real evidence. The natal projection is the same chart for every domain, so
it is hoisted to one copy when the domains agree and left per-domain when they
do not.
The domain cap was six, advertised as six, and could never be paid for. Domains
run sequentially at ~21s each against a cumulative 110s abort signal, so six is
~126s and four leaves nothing to write the answer with. Concurrency is not
available: the Python API is a single GIL-bound ThreadingHTTPServer whose async
work already sits behind a two-worker bounded queue that answers 503 when full.
Derive the cap from the clock instead of choosing it — 110s minus a 45s answer
reserve, divided by 21s, is three — and let the model-facing schema carry that
bound so an unpayable plan is unrepresentable. A caller that builds a plan
without that schema is truncated rather than refused, the loop stops early when
the measured pace says the next domain will not fit, and either way the dropped
domains are disclosed through omitted_domains and the receipt while status
degrades, so a partial answer cannot be presented as complete.
run.failed carried a code and nothing else, so the step durations, step budget
and workflow route recorded by c8d9ec64 were unavailable exactly when a run
needed explaining. Send the same allowlisted receipt run.completed sends,
built through publicConsultationRuntimeSteps so the internal failure code and
model loop diagnostics stay server-side, and never let building it replace the
failure event with a silent close. An agentic run that fails before
streamAgentResponse exists never reached the settle-and-log path either, so the
request-level catch now goes through the same entry point.
Refs BUG-256, BUG-257, BUG-258.
Co-authored-by: Cursor <cursoragent@cursor.com>
A staging consultation calculated the chart and then returned nothing but the
ensureFinalResponseText fallback. The model had made four calls to
run-jyotish-consultation, and two of them never reached a calculation: they set
both domains and theme, which canonicalDomainPlan rejects at execution. The
schema declared those two fields as independent optionals, the description never
mentioned the constraint, and the instructions actively told the model to use
theme for a single-domain retry. Each attempt therefore bought a rule the
contract never stated, and because the throw happens before the step-recording
try/catch, it left no trace in the receipt either.
Make the constraint unrepresentable instead of enforced. The model-facing schema
keeps only question and domains, so Mastra refuses the pair before the tool body
runs; the description states the single-array contract, and the instruction that
advertised theme is gone. canonicalDomainPlan still resolves the single-value
form for callers that build a plan without that schema, and is now exported so
that path has its own tests.
maxSteps and the abort timeout bound the same run but were hard-coded apart. One
calculation takes about 20s against a 110s budget, so time is the binding
constraint and three failed calculations exhaust it whatever the step count. The
budget only has to cover the longest useful shape, so it moves to 8 beside the
timeout with that reasoning recorded, and the recorded step list is sized to
match so an exhausted run cannot truncate its own evidence.
Step exhaustion was only ever inferable by counting events, since finishReason
was recorded nowhere and progressive-disclosure reads never reach the public
stream. Capture it as a closed enum plus a step count, normalizing anything
unrecognized, and log both as controlled fields. Neither may enter the client
receipt, whose step schema is strict and would fail a successful run.
Co-authored-by: Cursor <cursoragent@cursor.com>
A merge on 2026-07-25 (3ca30ed7) left the heading sequence and the body
sequence offset by one, so looking up a bug returned another bug's
symptom and root cause — worse than a miss, because it reads as normal.
The last body in the run was appended to BUG-043, giving that record two.
Rebind by content, not position: every reassignment was confirmed by
checking the body's 用户现象/根因 against the heading. Also drop 11
duplicate tail blocks (7 verbatim-equivalent, 4 strict subsets of the
surviving copy) and fold BUG-221~234 into BUG-020~035, which were the
same bugs recorded twice under two numbering schemes.
Structurally incomplete records 22 -> 0; records 253 -> 240 as 14
duplicates retire. Verified no body line was lost.
Co-authored-by: Cursor <cursoragent@cursor.com>
Second batch from the staging UX audit (BUG-248..253).
- css: expose the 32 palette tokens through @theme. Seven utilities
including text-ink, text-danger and text-warning compiled to no CSS
at all, so 30 call sites had been silently inert (BUG-248)
- chat: move the composer into its own component behind a draft store,
so a keystroke no longer re-renders a 2723-line component, and
persist the draft across reloads (BUG-249)
- chat: load gsap, react-markdown and thinking-orbs on demand. First
Load JS for / drops 549.5 kB to 476.3 kB gzipped (BUG-250)
- chat: route the five in-app destinations through router.push, and
keep the five auth redirects and the bootstrap retry as hard loads
on purpose (BUG-251)
- a11y: announce reply completion, and move the live region out of the
aria-busy subtree that was likely suppressing even the start
announcement (BUG-252)
- docs: give the 27 collided bug ids unique numbers and repair their
inbound references; require search rather than a full read of a
3690-line file (BUG-253)
Verified: tsc, eslint, next build, and 1592 assertions across the 199
non-database test files.
Co-authored-by: Cursor <cursoragent@cursor.com>
Framework-level UX fixes found while auditing staging (BUG-216..220).
- chat: route 44 previously discarded composer notices to sonner with
dedupe, so recovery, cancel and archive feedback is actually visible
(BUG-216)
- chat: anchor stream auto-scroll to bottom proximity and add a
jump-to-latest control, so reading history is no longer interrupted
on every token (BUG-218)
- reports: replace the silent 120s poll cutoff with an explicit
timed-out state, an 8m budget, stepped backoff and an elapsed
counter (BUG-217)
- reports: pause polling while the tab is hidden, via a shared hook
- app: add root error, global-error and not-found boundaries (BUG-219)
- admin: add antd SSR style extraction and the React 19 render adapter,
and move admin-only css out of the global stylesheet (BUG-220)
- membership: run bootstrap fetches concurrently and pause payment
polling while hidden
- build: configure optimizePackageImports
Verified on top of 2d370f2e: tsc, eslint, next build, and the related
frontend contract suites.
Co-authored-by: Cursor <cursoragent@cursor.com>
The sidebar contract read a selector with indexOf, so it returned whichever
rule appeared earliest in the file. A responsive override added above the base
rule made the gate report a missing min-height that was never removed, which
blocked staging on a change that was correct.
Share one helper that strips comments, parses rule by rule, accepts a whole
group as the query, and returns the declarations of every matching rule. The
union also makes a negative assertion mean no rule may declare the property,
which is what these contracts intend.
The helper was duplicated in two files and had no tests of its own; it now has
regressions for each way it read the wrong block.
Co-authored-by: Cursor <cursoragent@cursor.com>
Every workflow fault except abort and timeout collapsed into the single
calculation_failed code, and the upstream message was discarded, so a failing
run left no evidence of whether the API rejected the call or returned a payload
that missed the response contract.
Classify failures into a closed vocabulary carried on ConsultationWorkflowError
and record it as the failureCode of the runtime step. The observability tool
call schema gains one controlled optional field; upstream error text stays out
of logs, as that contract requires. Forward request_id to the API so a run can
be aligned with its access log.
Build public receipts from an explicit allowlist. The internal failure code
must not reach the client contract, whose step schema is strict and would
otherwise reject a successful run.
Co-authored-by: Cursor <cursoragent@cursor.com>
The runtime contract gated on total consultation tool attempts, so a single
transient calculation failure raised the counter past one and made the run
unrecoverable even after a later attempt succeeded. The already-computed chart
was discarded and the request ended as runtime_contract_incomplete.
Count only successful workflow executions against the single-calculation
boundary. Failed attempts stay in the attempt counter for observability. The
request-scoped cache still retains the successful promise, so a request can
never run more than one billable calculation.
Also record the staging-first branch delivery convention in AGENTS.md.
Co-authored-by: Cursor <cursoragent@cursor.com>
Hide the dead composer during birth-time intake, replace internal
copy with user-facing hints, and keep short-screen login, location
search, and session history reachable on phones.