Uh oh!
There was an error while loading. Please reload this page.
feat(console-ai): bind-on-create — build conversations re-key to app:X:build (ADR-0057 A1.b) - #2466
Conversation
… to app:X:build when it mints X (ADR-0057 A1.b) The magic flow (bare /ai/build) keys its conversation product-only, while Edit-with-AI keys app:X:build — so the thread that BUILT app X and the thread Edit-with-AI later opens for X were different conversations and the build history was stranded (Amendment A1 defect 3, reproduced live). - useChatConversation.rekeyScope(newScope): re-keys the CURRENT conversation (cache write + synchronous conversationScope flip) so the #2450 scope gate holds through the host's ?package= navigate — no pane remount mid-stream. - Legacy-scope fallback read (legacyScope/adoptLegacy): an app-scoped visit whose key was never cached adopts the product-only thread iff the host's predicate confirms the thread is bound to that app (pre-A1.b migration). Failing fetch degrades to a plain miss; the legacy key is never cleared (dock/FAB keep resolving through it). - AiChatPage: ChatPane reports the binding (post-URL-mirror only — single- navigator rule); the page re-keys and puts ?package= on the URL. Reload without ?package= self-heals from hydrated history. - A1.a chip is now a switcher: apps (by package) → their Edit-with-AI thread; New app → ?new=1 fresh unbound draft. Client-side only: conversation scope keying is a per-(user,scope) localStorage cache; the server needs no change. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The latest updates on your projects. Learn more about Vercel for GitHub. |
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
❌ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
…ed history on a same-conversation empty re-read Live verification caught a race the design review missed: re-keying the moment the binding draft lands MID-STREAM lets the scope-change re-resolve read the conversation before the server persists the turn (persistence is at turn COMPLETION), and a reconcile can then replace the live thread with that empty not-yet-persisted history — the pane blanked until reload. - ChatPane's bind effect now also waits for isLoading to drop: the re-key runs against a fully persisted turn (and 'bind at build completion' is truer to the named-on-first-save idiom anyway). Re-verified live: the thread stays visible through the bind, URL gains ?package=, and the Edit-with-AI entry resumes the same conversation with history. - useChatConversation's activeId branch no longer overwrites non-empty hydrated messages with an EMPTY read of the SAME conversation (a later remount would hydrate from that and render a blank thread); an empty read of a DIFFERENT conversation still replaces as before. Unit-tested. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
os-zhuang
commented
Jul 13, 2026
Live browser proof (local cloud rig, |
…schel-fc7c63 # Conflicts: # packages/app-shell/src/console/ai/AiChatPage.tsx
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
Uh oh!
There was an error while loading. Please reload this page.
…count) (#2474) The A1.b package switcher (#2466) listed every app with a _packageId, including code-delivered platform built-ins (setup / account, com.objectstack.*). Selecting one deep-linked /ai/build?package=<builtin> to a package the build agent can't author. Filter on the reserved com.objectstack.* package namespace — the only signal that reliably distinguishes built-ins on the /meta/app LIST response: _packageId is grafted onto every list item, whereas managedBy/source are sys_metadata row columns the list handler doesn't copy onto the item body, and the lock/editable envelope is single-GET only. New exported predicate isPlatformBuiltinApp; unit-tested. Co-authored-by: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Implements ADR-0057 Amendment A1.b (#2457) — part of epic #2409. Follows A1.a (#2461).
The defect (reproduced live)
A bare
/ai/buildvisit (the magic flow) keys its conversation to the product-only scopebuild, while "Edit with AI" (/ai/build?package=X) keysapp:X:build. When a build inside an unbound conversation mints package X, the thread that built X and the thread Edit-with-AI later opens for X are different conversations — the user's original build history is stranded where they'll never look for it (Amendment A1, defect 3).What this does
draftReview/builderHandoffpackage, per A1.a'sderiveBoundPackageId), the page calls the newuseChatConversation.rekeyScope('app:X:build')and puts?package=Xon the URL. A later Edit-with-AI entry for X resumes the same thread.rekeyScopeis synchronous by design: it flipsconversationScope(ref) before the?package=navigate re-render recomputes the host scope, so the AI handoff: second handoff's auto-send doesn't fire into an existing (hydrating) build conversation #2450 scope-match gate never opens a remount window — the mounted pane (and any in-flight stream) survives the re-key. Binding is deferred until the conversation id is mirrored into the URL (single-navigator rule: the URL-mirror effect is dormant once/:convIdis present, so the two effects can never fight over the URL from stale closures)./ai/build?package=Xvisit with noapp:X:buildcache entry offers the product-only thread to a predicate — adopted iff its own history is bound to X, so an unrelated thread is never hijacked into an app scope. A failing fallback fetch degrades to a plain miss (fresh conversation), never a dead surface. Pre-A1.b threads therefore stay reachable.?new=1. Trigger keepsdata-testid="ai-build-package-chip".Migration / keying semantics — read this
This mutates live conversation keying, client-side only:
(user, scope)localStorage cache (objectstack:ai-chat-conversation-id:{user}:{scope});useChatConversationnever lists by scope server-side, so no server change is needed. Corollary (pre-existing): continuity is per-browser; a different browser has no cache and the fallback/predicate handles or re-mints there./ai/buildkeep resolving through it exactly as before. Zero regression for those surfaces; the bound thread is simply also reachable under its app scope.app:X:buildentry — the thread you most recently had open bound to X becomes X's Edit-with-AI thread (matches the chip's latest-draft-wins semantics). Older threads remain in the sidebar history.useObjectChatbakesbody.contextinto the transport at first render, so turns between the bind and the next reload don't carrycontext.packageId— same as today; the server tracks the conversation's active package itself (ADR-0070).Tests
8 new cases in
useChatConversation.test.tsx(re-key writes both keys + same id; rekeyed scope change resumes without re-resolve or create; no-op before resolve; legacy adopt on match / fresh on reject / fresh on fetch failure; predicate fed the localStorage message cache when the server returns no history; fallback never runs on a primary cache hit). Existing #2450/A1.a suites green:useDeferredFirstSend,deriveBoundPackageId,AiChatPage.hydration,chatScope,useObjectChat.handoffContext— 56 tests total, plustsc --noEmitclean.Live browser proof of the continuity fix + legacy fallback to follow in a PR comment before merge.
🤖 Generated with Claude Code