Skip to content

fix: improve Kitchen AI reliability and conversation scrolling - #33

Draft
1395490241lhj-code wants to merge 2 commits into
claude/hopeful-volhard-9272e9from
claude/kitchen-ai-reliability
Draft

1395490241lhj-code wants to merge 2 commits into
claude/hopeful-volhard-9272e9from
claude/kitchen-ai-reliability

Conversation

@1395490241lhj-code

Copy link
Copy Markdown
Owner

Kitchen AI Reliability & Motion is stacked on PR #32: base claude/hopeful-volhard-9272e9, head claude/kitchen-ai-reliability. It fixes problems found during the first real use on an iPhone 17 Pro. It does not fix the production "AI 服务暂时不可用" outage. That outage has no proven root cause yet.

Commits:

  • 20a0de2 fix(server): improve Kitchen AI upstream failure diagnostics
  • 329cc50 fix(ios): stabilize Kitchen AI conversation and scrolling

11 files, +694/−11. There is no change to the six content blocks, the five proposals, the coordinator, the interpreter, the orchestrator, persistence, confirmation or undo.

⚠️ Read first

Server: upstream failure diagnostics (20a0de2)

Problem. /api/ai-conversation caught every provider exception, logged only resultCode: provider_unavailable, and dropped the upstream error. 429, 413, timeouts and 5xx were indistinguishable in production logs.

Change. server.js and test/ai-conversation-stream.test.mjs only.

  • ai_conversation_failed / ai_conversation_completed now also record:

    • primaryProvider, timeoutMs and requestSizeBytes (messages only, see above);
    • on failure: upstream status, reason (the upstream error code), timeoutSource and upstreamRequestId;
    • when a provider switch happens: fallbackReason (the first provider's code).
  • Field values are validated, not only field names. The logger allowlist does not vet values, so:

    • status must be an integer from 100 to 599;
    • codes must be safe identifiers (at most 64 chars of [A-Za-z0-9_.-]), otherwise unrecognized;
    • request ids must be 8–128 chars of [A-Za-z0-9_-], otherwise they are dropped.

    Upstream error text, keys and conversation content are never logged. Tests assert this with hostile upstream values, for example a code that echoes the prompt and a request id containing spaces and a key fragment.

  • Unchanged: provider routing, timeouts (45 s primary, 20 s fallback), the fallback rule, retries (maxRetries: 0, no client auto-retry), rate limiting, and /api/ai-chat.

How to read the logs after deploy:

Log shape Meaning
durationMs ≈ 45000 primary timed out
attempt=2 a transient primary failure fell back to Groq
1–2 s duration immediate rejection
no log line at the failure time the request never reached the app (Render-level 5xx)

status, reason and fallbackReason then name the upstream cause.

iOS: conversation reliability and scrolling (329cc50)

Fixes for what the device run showed:

  • A meal follow-up got prose instead of cards. 「今晚做什么」, sent after 「用快过期的食材做饭」, read only tonight's plan.

    • A whole short follow-up now inherits the meal intent, matched against the entire sentence: 换一个 / 换一批 / 再来一个 / 换个清淡的 / 推荐点简单的 / 今晚做什么 / 那明天做啥 / 还有别的吗 / 有没有其他的 / Anything else? / What else?
    • It only does so when one of the last three user questions was an explicit meal question.
    • Home only. Excluding inventory still withholds the shortlist. Planner is unchanged.
  • System instructions. When recommending dishes, the model uses present_recipe_card, cites shortlist recipeIDs, keeps prose to one or two sentences, and does not claim freshness or quantity. When nothing fits, it says why briefly and gives one next step. The length rule applies only to dish recommendations; plan changes and other questions are answered in full.

  • Failed retries stacked identical error rows. An older attempt of a retried turn is hidden once a newer attempt exists, if it carried only an error or was stopped empty (AIConversationTranscriptPresentation). This is presentation only: persisted history is unchanged, and a failed attempt with partial text, a card or an action outcome always stays.

  • The transcript yanked readers back to the bottom on every streamed delta. Three onChange handlers could scroll in one update. Now (AIConversationAutoScroll):

    • one trigger drives at most one scroll per update;
    • a drag up to reread stops following, and later deltas leave the position alone;
    • a drag that dismisses the keyboard keeps the previous follow state;
    • a new user message, a running retry, or a different conversation resumes following. This does not depend on observing .preparingContext, so it still works if SwiftUI coalesces the send with .requesting or .streaming.

    No animation was added, and the existing scroll animation and Reduce Motion handling are unchanged (D-043).

Privacy regression checks

These are assembler tests that assert the actual domain reads, not just the intent function:

  • New negatives. In a conversation that already asked what to cook, these read only tonight's plan, with no inventory and no shortlist: 我想换手机 / 推荐电影院 / 今晚做什么运动 / 今晚做什么运动好 / 换个话题 / 推荐一部电影 / 还有别的事吗 / 帮我推荐个电影 / "Anything else about my phone?". 谢谢 / 明天几点下班 / 今晚会下雨吗 inside a meal conversation stay unread too.
  • Follow-up words without an earlier user meal question read nothing extra. A meal question older than the last three user turns does not carry over.
  • The 24 earlier negatives (饭店推荐, 饭卡, 吃药, 菜馆, 菜单, 早餐店, 午餐会议, weather/theater, …) still hold.
  • The instructions test asserts the full text reaches the model untruncated, and that the old blanket wording 「正文最多两三句」 is gone.

Validation

All iOS runs used xcodebuild through dev-sim-slot on the reserved simulator, iOS 27.2, with parallel testing off. Xcode MCP was held by another worktree. The tested tree is byte-identical to 329cc50; nothing changed after the final runs.

Suite Result
iOS unit (ContextAssembler 58, Controller 74, AutoScroll 7, TranscriptPresentation 5) 144/144
iOS UI (AIConversationWorkspaceUITests 79 incl. test47 and all 4 StructuredBlocks variants, AIConversationAcceptanceUITests 4) 83/83
Server (ai-conversation-stream, -lifecycle, -turn-limit, ai-server-routes) 181/181

No new compiler warnings in the changed files.

How the UI result was reached:

About test47:

  • test47_ScrollingUpDuringStreamingIsNotPulledBackAndASendFollowsAgain uses a DEBUG-only incremental stream of 40 pieces over about 10 s. It covers: dismiss the keyboard, scroll up mid-stream, position held after the stream ends, then send with the keyboard up and follow the new reply.
  • The corrected test47 also passed on the pre-fix code, so the coalesced-update failure was not reproduced in real UI. The latestUserMessageID resume is a defensive fix.

Screenshots. test47 captured three screens, kept as local review evidence and not attached here:

  • AutoScroll-1-ScrolledUpMidStream
  • AutoScroll-2-StreamFinishedPositionHeld
  • AutoScroll-3-SendFollowsWithKeyboard

CI's iOS tests workflow runs the whole unit target plus its default UI selection. That selection does not include AIConversationWorkspaceUITests. To recapture these screens in CI, dispatch it with ui_tests: KitchenManagerUITests/AIConversationWorkspaceUITests.

Not verified

  • The production AI outage root cause. Getting it needs deploy approval and Render logs.
  • Any physical device run of this PR, including scroll feel, keyboard and real streaming.
  • The new instructions against the real models, i.e. whether cards appear reliably.
  • Real VoiceOver listening and Reduce Motion runtime capture.
  • The full iOS and full server suites locally. CI on this push is pending.
  • /api/ai-chat's existing log values are not value-validated. That is pre-existing and out of scope.

Proposed, not implemented (needs decisions)

  • The 库存 · 今晚计划 · 菜谱 chips read as "always read".
  • 400/413 show as "unavailable" even though a retry cannot succeed.
  • A failed turn cannot be retried after an app relaunch.
  • Card and empty-state transitions (D-043 scope).
  • Trimming tool definitions per entry point, which changes what the AI can do.

Merge order

  1. Merge iOS: fix bugs and logic issues found in the UX and functional reviews #31 (which carries de14ffd) and Kitchen AI Phase 1a: tonight recipe recommendations with deterministic kitchen facts #32 into main in whatever order the owner picks. Keep de14ffd.
  2. Retarget this PR to main and merge it.
  3. Deploying the server diagnostics to Render needs explicit owner approval.

🤖 Generated with Claude Code

1395490241lhj-code and others added 2 commits October 20, 2026 19:41
/api/ai-conversation logged every provider failure as provider_unavailable
and dropped the upstream error, so 429, 413, timeouts and 5xx could not be
told apart in production logs.

- ai_conversation_failed and ai_conversation_completed now carry the safe
  classification: primaryProvider, timeoutMs, requestSizeBytes, and, on
  failure, upstream status, reason, timeoutSource and upstreamRequestId; a
  provider switch records the first provider's fallbackReason.
- Field values are validated, not just field names: status must be an
  integer 100-599, codes must be short safe identifiers (otherwise
  "unrecognized"), request ids must be 8-128 safe characters (otherwise
  dropped). Upstream error text, keys and conversation content are never
  logged.

This adds diagnostics only. It does not change provider routing,
timeouts, fallback or retries, and it does not identify the production
failure; that needs the redacted logs after deploy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On device, a meal follow-up got prose instead of recipe cards, failed
retries stacked identical error rows, and the transcript pulled a reader
back to the bottom on every streamed delta.

- Meal follow-ups: a whole short follow-up (换一个 / 今晚做什么 / 还有别的吗 /
  推荐点简单的 / Anything else?) inherits the meal intent when one of the last
  three user questions was an explicit meal question. Home only. Excluding
  inventory still withholds the shortlist. Sentences that only share a word
  (我想换手机 / 推荐电影院 / 今晚做什么运动) read only tonight's plan.
- Instructions: dish recommendations use present_recipe_card, cite recipeID
  and keep prose to one or two sentences, without claiming freshness or
  quantity. The length rule applies only to recommendations; plan changes
  and other questions are answered in full.
- Transcript: an older attempt of a retried turn that carried only an error
  (or was stopped empty) is hidden once a newer attempt exists. Presentation
  only; history is unchanged and partial content always stays.
- Auto-scroll (AIConversationAutoScroll): one trigger, at most one scroll per
  update. A drag up to reread stops following; a drag that dismisses the
  keyboard keeps the previous state. A new user message, a running retry or
  a different conversation resumes following, without relying on observing
  .preparingContext. No animation added.
- Tests: assembler privacy negatives assert the actual reads; test47 covers
  scrolling up mid-stream and sending with the keyboard up, with a DEBUG-only
  incremental stream script. The structured-blocks AXXXL helper now scrolls
  both ways (that Light AXXXL red also reproduces on bcca20a).

No change to the six content blocks, five proposals, coordinator,
persistence, confirmation or undo.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1395490241lhj-code added a commit that referenced this pull request Oct 22, 2026
…s end to end

- ConversationContextAssembler.isMealQuestion now reads a question clause
  by clause. A clause about food someone else cooks (食堂, 餐厅, 外卖, 老板,
  附近 ...) or about a medicine (药, not 药膳) does not read inventory or
  the shortlist, unless the same clause names the family's own kitchen
  (家里, 在家, 冰箱 ...). Other clauses still count, so 「不想点外卖,在家
  做什么菜」 stays a cooking question. New pattern (f) covers 家里有鸡蛋能
  做什么 / 冰箱里的东西能做点什么. The inventory exclusion, the Home
  tonightPlan read and the context budget are unchanged.
- Tests check the domain reads and the final request context: eating-out
  and medicine questions read only tonightPlan and carry no inventory facts
  or missing counts; home-cooking questions read inventory, tonightPlan and
  the shortlist; excluding inventory still withholds both.
- ConversationOrchestratorTests: four end-to-end cases from a provider tool
  call through the interpreter, coordinator and store: tonight for four,
  no or null count keeps the existing rule, 0/13/2.5/"4" is refused with
  nothing written, and a replacement for four previews "4 份" and writes
  it on confirm. The helper keeps the orchestrator alive for the whole
  stream, since its turn task holds it weakly.
- AIConversationWorkspaceUITests.bringIntoView: when the target sits above
  the visible band (at AX sizes 继续调整 pushes the last card up), scroll
  back in small steps; otherwise scroll down as before. Same direction
  check as PR #33's 329cc50; nothing else from that PR.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant