diff --git a/patches-ios/0003-feat-mobile-show-stats-for-nerds-in-the-composer.patch b/patches-ios/0003-feat-mobile-show-stats-for-nerds-in-the-composer.patch index f442fc7..8ac7edc 100644 --- a/patches-ios/0003-feat-mobile-show-stats-for-nerds-in-the-composer.patch +++ b/patches-ios/0003-feat-mobile-show-stats-for-nerds-in-the-composer.patch @@ -743,7 +743,7 @@ index f3488ebd3b..3ebcd4817e 100644 onSendMessage={handleStart} onReconnectEnvironment={() => onReconnectEnvironment(selectedProject.environmentId)} diff --git a/apps/mobile/src/features/threads/ThreadComposer.tsx b/apps/mobile/src/features/threads/ThreadComposer.tsx -index 52ef705e93..6a47855b95 100644 +index ea2928f531..459bb80636 100644 --- a/apps/mobile/src/features/threads/ThreadComposer.tsx +++ b/apps/mobile/src/features/threads/ThreadComposer.tsx @@ -214,6 +214,8 @@ export interface ThreadComposerProps { @@ -764,10 +764,10 @@ index 52ef705e93..6a47855b95 100644 diff --git a/apps/mobile/src/features/threads/ThreadDetailScreen.tsx b/apps/mobile/src/features/threads/ThreadDetailScreen.tsx -index a71920c199..f1a43ef03f 100644 +index d0b08ce405..112c28c3ac 100644 --- a/apps/mobile/src/features/threads/ThreadDetailScreen.tsx +++ b/apps/mobile/src/features/threads/ThreadDetailScreen.tsx -@@ -191,6 +191,9 @@ import { +@@ -196,6 +196,9 @@ import { ThreadComposer, } from "./ThreadComposer"; import { ComposerPopoverHost } from "./ComposerPopoverHost"; @@ -775,18 +775,18 @@ index a71920c199..f1a43ef03f 100644 +import { ComposerStats, COMPOSER_STATS_ROW_HEIGHT } from "./ComposerStats"; +import { useComposerStats } from "../../state/use-composer-stats"; import { ThreadFeed, type ThreadFeedHandle, type ThreadFeedHistoryControls } from "./ThreadFeed"; + import { WorkspaceMutationIdProvider } from "./WorkspaceMutationIdProvider"; import { useThreadTurnSubagents } from "./ThreadAgentsSheet"; - import { ComposerQueuedEditBanner } from "./ComposerQueuedEdit"; -@@ -288,6 +291,8 @@ export interface ThreadDetailScreenProps { - hasAttachmentsOrContext: boolean, +@@ -295,6 +298,8 @@ export interface ThreadDetailScreenProps { ) => Promise, + onWillEnqueue?: () => void, ) => Promise; + /** Sends "/compact" without touching the draft. */ + readonly onCompactContext: () => void; readonly onReconnectEnvironment: () => void; readonly canSwitchThreadProvider: boolean; readonly onUpdateThreadInteractionMode?: (interactionMode: ProviderInteractionMode) => void; -@@ -859,10 +864,16 @@ function ThreadDetailBody(props: ThreadDetailScreenProps) { +@@ -901,10 +906,16 @@ function ThreadDetailBody( ) || (props.historyControls?.hasMoreHistory === true && props.selectedThread.latestUserMessageAt !== null); @@ -803,7 +803,7 @@ index a71920c199..f1a43ef03f 100644 composerBottomInset; // The last collapse toggle; it only applies to the request it was made on. const [userInputToggle, setUserInputToggle] = useState<{ -@@ -1703,6 +1714,32 @@ function ThreadDetailBody(props: ThreadDetailScreenProps) { +@@ -1806,6 +1817,32 @@ function ThreadDetailBody( hasCompactableConversation={ hasCompactableConversation && !props.isCompacting } @@ -1412,10 +1412,10 @@ index 0000000000..3cc2cb612f + }, [enabled, projection, environmentId, serverConfig, instanceId]); +} diff --git a/apps/mobile/src/state/use-thread-composer-state.ts b/apps/mobile/src/state/use-thread-composer-state.ts -index 288d37d9f1..7b95614225 100644 +index 700d15213f..4916186664 100644 --- a/apps/mobile/src/state/use-thread-composer-state.ts +++ b/apps/mobile/src/state/use-thread-composer-state.ts -@@ -699,6 +699,45 @@ export function useThreadComposerState() { +@@ -701,6 +701,45 @@ export function useThreadComposerState() { return messageId; }; @@ -1461,7 +1461,7 @@ index 288d37d9f1..7b95614225 100644 const onChangeDraftMessage = (value: string) => { if (!selectedThreadShell) { return; -@@ -772,6 +811,7 @@ export function useThreadComposerState() { +@@ -774,6 +813,7 @@ export function useThreadComposerState() { onNativePasteText, onRemoveDraftImage, onSendMessage, @@ -1470,10 +1470,10 @@ index 288d37d9f1..7b95614225 100644 }; } diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx -index e747718875..94a455f0a4 100644 +index 735c5cd398..f38a4f05e8 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx -@@ -314,7 +314,7 @@ import { useRightPanelSurfaceClose } from "../hooks/useRightPanelSurfaceClose"; +@@ -315,7 +315,7 @@ import { useRightPanelSurfaceClose } from "../hooks/useRightPanelSurfaceClose"; import { ComposerBannerStack, type ComposerBannerStackItem } from "./chat/ComposerBannerStack"; import { modelFallbackBannerItem } from "./chat/ComposerUsageLimits"; import { ComposerStats } from "./chat/ComposerStats"; @@ -1532,7 +1532,7 @@ index e747718875..94a455f0a4 100644 })(); const pendingBackgroundTasks = deriveThreadPendingBackgroundTasks(serverProjection); const activeWorkStartedAt = -@@ -3867,15 +3833,12 @@ export default function ChatView(props: ChatViewProps) { +@@ -3880,15 +3846,12 @@ export default function ChatView(props: ChatViewProps) { {...composerStats} onCompact={() => composerRef.current?.compactContext()} compactDisabled={compactDisabled} @@ -1555,15 +1555,15 @@ index e747718875..94a455f0a4 100644 subscriptionSwitch={{ providers: providerStatuses, diff --git a/apps/web/src/components/chat/ChatComposer.tsx b/apps/web/src/components/chat/ChatComposer.tsx -index 82b6b9617c..0c7c910bdb 100644 +index 25b7f5ce6f..95fa71d953 100644 --- a/apps/web/src/components/chat/ChatComposer.tsx +++ b/apps/web/src/components/chat/ChatComposer.tsx -@@ -253,7 +253,7 @@ import { +@@ -252,7 +252,7 @@ import { resolveComposerDispatchMode, type ComposerDispatchMode, } from "@t3tools/client-runtime/state/composer-dispatch"; --import type { ContextWindowSnapshot } from "../../lib/contextWindow"; -+import type { ContextWindowSnapshot } from "@t3tools/client-runtime/state/context-window"; +-import { deriveLatestContextWindowSnapshot } from "../../lib/contextWindow"; ++import { deriveLatestContextWindowSnapshot } from "@t3tools/client-runtime/state/context-window"; import { resolveProviderSkillsForCwd, resolveProviderSlashCommandsForCwd, @@ -2200,7 +2200,7 @@ index ff9ef9318c..b4e8bb5647 100644 -} +export * from "@t3tools/client-runtime/state/context-window"; diff --git a/packages/client-runtime/package.json b/packages/client-runtime/package.json -index d1b48e1d76..368eb01716 100644 +index 6fd4550d93..9e3e30b447 100644 --- a/packages/client-runtime/package.json +++ b/packages/client-runtime/package.json @@ -143,6 +143,14 @@ diff --git a/patches-ios/0004-feat-mobile-nest-child-threads-under-their-opener-in.patch b/patches-ios/0004-feat-mobile-nest-child-threads-under-their-opener-in.patch index b1fe095..ab6e145 100644 --- a/patches-ios/0004-feat-mobile-nest-child-threads-under-their-opener-in.patch +++ b/patches-ios/0004-feat-mobile-nest-child-threads-under-their-opener-in.patch @@ -13,7 +13,7 @@ Subject: [PATCH 4/8] feat(mobile): nest child threads under their opener in .../src/features/threads/threadListV2.test.ts | 339 +++++++++ .../src/features/threads/threadListV2.ts | 668 +++++++++++++----- .../use-thread-list-v2-shelf-preferences.ts | 70 ++ - .../src/features/threads/useThreadListV2.ts | 4 + + .../src/features/threads/useThreadListV2.ts | 6 + apps/mobile/src/lib/storage.test.ts | 20 + .../src/persistence/mobile-preferences.ts | 18 + apps/mobile/src/state/entities.ts | 5 + @@ -24,7 +24,7 @@ Subject: [PATCH 4/8] feat(mobile): nest child threads under their opener in .../src/state/childThreadFamilies.ts | 462 ++++++++++++ .../src/state/threadShell.test.ts | 37 + .../client-runtime/src/state/threadShell.ts | 24 + - 19 files changed, 2496 insertions(+), 633 deletions(-) + 19 files changed, 2498 insertions(+), 633 deletions(-) create mode 100644 packages/client-runtime/src/state/childThreadFamilies.test.ts create mode 100644 packages/client-runtime/src/state/childThreadFamilies.ts @@ -45,7 +45,7 @@ index c522da701e..f52c6718cb 100644 const { savedConnectionsById } = useSavedRemoteConnections(); const navigation = useNavigation(); diff --git a/apps/mobile/src/features/home/HomeScreen.tsx b/apps/mobile/src/features/home/HomeScreen.tsx -index 5ac49af24a..0334d98050 100644 +index 4609ecfb96..224633ba5a 100644 --- a/apps/mobile/src/features/home/HomeScreen.tsx +++ b/apps/mobile/src/features/home/HomeScreen.tsx @@ -59,6 +59,7 @@ import { threadListEnvironmentsAtom } from "../../state/presentation"; @@ -64,7 +64,7 @@ index 5ac49af24a..0334d98050 100644 } from "../threads/threadListV2"; import { useThreadListV2ShelfPreferences } from "../threads/use-thread-list-v2-shelf-preferences"; import type { HomeListFilterMenuEnvironment } from "./home-list-filter-menu"; -@@ -426,6 +428,11 @@ export function HomeScreen(props: HomeScreenProps) { +@@ -431,6 +433,11 @@ export function HomeScreen(props: HomeScreenProps) { snoozedShelfExpanded, workingShelfEnabled, workingShelfExpanded, @@ -76,7 +76,7 @@ index 5ac49af24a..0334d98050 100644 toggleSettledShelf, toggleSnoozedShelf, toggleWorkingShelf, -@@ -446,6 +453,18 @@ export function HomeScreen(props: HomeScreenProps) { +@@ -451,6 +458,18 @@ export function HomeScreen(props: HomeScreenProps) { } = listEnvironments; const resolveProviderInstance = useThreadRowProviderInstanceResolver(providersByEnvironmentId); const pendingOrder = usePendingThreadOrder(); @@ -95,7 +95,7 @@ index 5ac49af24a..0334d98050 100644 const { layout: threadListV2Layout, items: threadListV2Items, -@@ -471,6 +490,8 @@ export function HomeScreen(props: HomeScreenProps) { +@@ -476,6 +495,8 @@ export function HomeScreen(props: HomeScreenProps) { workingShelfExpanded, snoozedShelfExpanded, settledShelfExpanded, @@ -104,7 +104,7 @@ index 5ac49af24a..0334d98050 100644 selectedThreadKey, }); // Called as `props.onSelectThread(...)`, the compiler would cache this handler on the whole -@@ -531,6 +552,21 @@ export function HomeScreen(props: HomeScreenProps) { +@@ -536,6 +557,21 @@ export function HomeScreen(props: HomeScreenProps) { /> ); } @@ -126,7 +126,7 @@ index 5ac49af24a..0334d98050 100644 if (item.type === "v2-working-shelf") { return ( 0 ? ( (null); -@@ -280,6 +283,11 @@ function ThreadNavigationSidebarPane( +@@ -277,6 +280,11 @@ function ThreadNavigationSidebarPane( snoozedShelfExpanded, workingShelfEnabled, workingShelfExpanded, @@ -313,7 +313,7 @@ index 96f0a81771..8994a7bd91 100644 toggleSettledShelf, toggleSnoozedShelf, toggleWorkingShelf, -@@ -300,6 +308,18 @@ function ThreadNavigationSidebarPane( +@@ -297,6 +305,18 @@ function ThreadNavigationSidebarPane( } = listEnvironments; const resolveProviderInstance = useThreadRowProviderInstanceResolver(providersByEnvironmentId); const pendingOrder = usePendingThreadOrder(); @@ -332,7 +332,7 @@ index 96f0a81771..8994a7bd91 100644 const { layout: threadListV2Layout, items: threadListV2Items, -@@ -325,6 +345,8 @@ function ThreadNavigationSidebarPane( +@@ -322,6 +342,8 @@ function ThreadNavigationSidebarPane( workingShelfExpanded, snoozedShelfExpanded, settledShelfExpanded, @@ -341,7 +341,7 @@ index 96f0a81771..8994a7bd91 100644 selectedThreadKey: props.selectedThreadKey ?? null, }); const listItems: readonly SidebarListItem[] = -@@ -335,6 +357,7 @@ function ThreadNavigationSidebarPane( +@@ -332,6 +354,7 @@ function ThreadNavigationSidebarPane( type: "v2-show-more", key: "v2-show-more", hiddenCount: threadListV2Layout.hiddenSettledCount, @@ -349,7 +349,7 @@ index 96f0a81771..8994a7bd91 100644 ...(olderSettledShelf.loading ? { state: "loading" as const } : olderSettledShelf.failed -@@ -486,7 +509,11 @@ function ThreadNavigationSidebarPane( +@@ -483,7 +506,11 @@ function ThreadNavigationSidebarPane( return threadListV2ListItemsAreEqual(previous, item); } if (previous.type === "v2-show-more" && item.type === "v2-show-more") { @@ -362,7 +362,7 @@ index 96f0a81771..8994a7bd91 100644 } return false; }; -@@ -545,6 +572,21 @@ function ThreadNavigationSidebarPane( +@@ -542,6 +569,21 @@ function ThreadNavigationSidebarPane( /> ); } @@ -384,7 +384,7 @@ index 96f0a81771..8994a7bd91 100644 case "v2-thread": { const thread = item.item.thread; const scopeKey = scopedProjectKey(thread.environmentId, thread.projectId); -@@ -609,6 +651,12 @@ function ThreadNavigationSidebarPane( +@@ -606,6 +648,12 @@ function ThreadNavigationSidebarPane( } canMoveUp={item.canMoveUp} canMoveDown={item.canMoveDown} @@ -397,7 +397,7 @@ index 96f0a81771..8994a7bd91 100644 onSnoozeThread={snoozeThread} onUnsnoozeThread={unsnoozeThread} onUnsettleThread={unsettleThread} -@@ -657,6 +705,7 @@ function ThreadNavigationSidebarPane( +@@ -654,6 +702,7 @@ function ThreadNavigationSidebarPane( { olderSettledShelf.retry(); diff --git a/apps/mobile/src/features/threads/thread-list-v2-items.tsx b/apps/mobile/src/features/threads/thread-list-v2-items.tsx -index 74704ef116..436fb72fc4 100644 +index 84e9494759..c24738c32d 100644 --- a/apps/mobile/src/features/threads/thread-list-v2-items.tsx +++ b/apps/mobile/src/features/threads/thread-list-v2-items.tsx @@ -25,6 +25,7 @@ import { @@ -589,7 +589,7 @@ index 74704ef116..436fb72fc4 100644 : label } disabled={props.state === "loading"} -@@ -625,6 +761,15 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: { +@@ -669,6 +805,15 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: { ) => void; readonly canMoveUp?: boolean; readonly canMoveDown?: boolean; @@ -605,7 +605,7 @@ index 74704ef116..436fb72fc4 100644 readonly onSwipeableWillOpen: (methods: SwipeableMethods) => void; readonly onSwipeableClose: (methods: SwipeableMethods) => void; readonly activationKey?: string; -@@ -788,7 +933,7 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: { +@@ -832,7 +977,7 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: { ); const arrangementMenuItems = useMemo( () => [ @@ -614,7 +614,7 @@ index 74704ef116..436fb72fc4 100644 ? [ { id: "arrange", title: "Arrange threads…", image: "line.3.horizontal" }, { -@@ -817,6 +962,7 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: { +@@ -861,6 +1006,7 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: { props.canMoveDown, props.canMoveUp, props.reorderSupported, @@ -622,7 +622,7 @@ index 74704ef116..436fb72fc4 100644 props.pinningSupported, thread.pinnedAt, ], -@@ -927,9 +1073,28 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: { +@@ -971,9 +1117,28 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: { ], [arrangementMenuItems, titleMenuItems], ); @@ -651,7 +651,7 @@ index 74704ef116..436fb72fc4 100644 if (nativeEvent.event === "settle") handleSettle(); if (nativeEvent.event === "unsettle") handleUnsettle(); if (nativeEvent.event === "unsnooze") handleUnsnooze(); -@@ -978,6 +1143,8 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: { +@@ -1022,6 +1187,8 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: { }, [ onNewThreadOnBranch, @@ -660,7 +660,7 @@ index 74704ef116..436fb72fc4 100644 thread, handleArchive, handleDelete, -@@ -1304,8 +1471,101 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: { +@@ -1348,8 +1515,101 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: { cardContent ); @@ -763,7 +763,7 @@ index 74704ef116..436fb72fc4 100644 ); @@ -807,7 +807,7 @@ index 74704ef116..436fb72fc4 100644 return ( -@@ -1426,8 +1719,10 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: { +@@ -1470,8 +1763,10 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: { dormant={dormant} threadKey={`${thread.environmentId}:${thread.id}`} backgroundColor={rowAppearance.swipeBackgroundColor} @@ -820,7 +820,7 @@ index 74704ef116..436fb72fc4 100644 enableTrackpadSwipe fullSwipeAction="primary" fullSwipeWidth={props.fullSwipeWidth ?? windowWidth - 32} -@@ -1454,6 +1749,7 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: { +@@ -1498,6 +1793,7 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: { : []), { id: "copy-thread", title: "Copy thread", image: "text.bubble" }, { id: "copy-thread-id", title: "Copy thread ID", image: "doc.on.doc" }, @@ -828,7 +828,7 @@ index 74704ef116..436fb72fc4 100644 ...(snoozedRow ? snoozedMenuActions : !props.settlementSupported -@@ -1471,6 +1767,7 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: { +@@ -1515,6 +1811,7 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: { )} @@ -1196,7 +1196,7 @@ index ff438f006f..6993f4b9ca 100644 + }); +}); diff --git a/apps/mobile/src/features/threads/threadListV2.ts b/apps/mobile/src/features/threads/threadListV2.ts -index 61280bad84..89e8cee610 100644 +index 4c2b35ed6f..1f9ed4438b 100644 --- a/apps/mobile/src/features/threads/threadListV2.ts +++ b/apps/mobile/src/features/threads/threadListV2.ts @@ -16,6 +16,26 @@ import { @@ -1226,7 +1226,7 @@ index 61280bad84..89e8cee610 100644 import { createInboxReturnTracker, isThreadWorking, -@@ -254,6 +274,153 @@ export function sortThreadsForListV2< +@@ -275,6 +295,153 @@ export function sortThreadsForListV2< return sortActiveThreadsByOrderKey(threads); } @@ -1380,7 +1380,7 @@ index 61280bad84..89e8cee610 100644 export function getThreadListV2OrderedSection(input: { readonly threads: readonly EnvironmentThreadShell[]; readonly section: "pinned" | "active"; -@@ -263,24 +430,7 @@ export function getThreadListV2OrderedSection(input: { +@@ -284,24 +451,7 @@ export function getThreadListV2OrderedSection(input: { readonly snoozeEnvironmentIds?: ReadonlySet; readonly queuedThreadKeys?: ReadonlySet; }): EnvironmentThreadShell[] { @@ -1406,7 +1406,7 @@ index 61280bad84..89e8cee610 100644 const ordered = input.section === "pinned" ? sortPinnedThreadsByOrderKey(threads) -@@ -292,6 +442,43 @@ export function getThreadListV2OrderedSection(input: { +@@ -313,6 +463,43 @@ export function getThreadListV2OrderedSection(input: { return applyPendingThreadOrder(ordered, input.section, pending); } @@ -1450,7 +1450,7 @@ index 61280bad84..89e8cee610 100644 export interface ThreadListV2Item { readonly thread: EnvironmentThreadShell; readonly variant: "card" | "slim"; -@@ -299,6 +486,32 @@ export interface ThreadListV2Item { +@@ -320,6 +507,32 @@ export interface ThreadListV2Item { readonly pinned: boolean; readonly wokeAt: string | null; readonly isLast: boolean; @@ -1483,7 +1483,7 @@ index 61280bad84..89e8cee610 100644 } export interface ThreadListV2Layout { -@@ -306,6 +519,8 @@ export interface ThreadListV2Layout { +@@ -327,6 +540,8 @@ export interface ThreadListV2Layout { readonly hiddenSettledCount: number; /** The settled rows the shelf may list, before its visible limit. */ readonly listedSettledThreads: ReadonlyArray; @@ -1492,7 +1492,7 @@ index 61280bad84..89e8cee610 100644 readonly workingCount: number; readonly workingShelfHeaderIndex: number | null; readonly snoozedCount: number; -@@ -332,6 +547,24 @@ export interface ThreadListV2ThreadListItem { +@@ -353,6 +568,24 @@ export interface ThreadListV2ThreadListItem { readonly canMoveDown: boolean; } @@ -1517,7 +1517,7 @@ index 61280bad84..89e8cee610 100644 export interface ThreadListV2PendingListItem { readonly type: "v2-pending"; readonly key: string; -@@ -367,6 +600,7 @@ export interface ThreadListV2SettledShelfListItem { +@@ -388,6 +621,7 @@ export interface ThreadListV2SettledShelfListItem { export type ThreadListV2ListItem = | ThreadListV2ThreadListItem @@ -1525,7 +1525,7 @@ index 61280bad84..89e8cee610 100644 | ThreadListV2PendingListItem | ThreadListV2WorkingShelfListItem | ThreadListV2SnoozedShelfListItem -@@ -377,6 +611,7 @@ export function isThreadListV2ListItem(value: { +@@ -398,6 +632,7 @@ export function isThreadListV2ListItem(value: { }): value is ThreadListV2ListItem { return ( value.type === "v2-thread" || @@ -1533,7 +1533,7 @@ index 61280bad84..89e8cee610 100644 value.type === "v2-pending" || value.type === "v2-working-shelf" || value.type === "v2-snoozed-shelf" || -@@ -399,6 +634,13 @@ export function threadListV2ListItemsAreEqual( +@@ -420,6 +655,13 @@ export function threadListV2ListItemsAreEqual( previous.item.snoozed === item.item.snoozed && previous.item.pinned === item.item.pinned && previous.item.wokeAt === item.item.wokeAt && @@ -1547,7 +1547,7 @@ index 61280bad84..89e8cee610 100644 previous.snoozeWakeLabelText === item.snoozeWakeLabelText && previous.timeLabel === item.timeLabel && previous.snoozePresetMinute === item.snoozePresetMinute && -@@ -407,6 +649,20 @@ export function threadListV2ListItemsAreEqual( +@@ -428,6 +670,20 @@ export function threadListV2ListItemsAreEqual( previous.canMoveUp === item.canMoveUp && previous.canMoveDown === item.canMoveDown ); @@ -1568,7 +1568,7 @@ index 61280bad84..89e8cee610 100644 case "v2-pending": return ( previous.type === "v2-pending" && -@@ -478,7 +734,9 @@ export function buildThreadListV2ListItems(input: { +@@ -499,7 +755,9 @@ export function buildThreadListV2ListItems(input: { readonly shelfPreferencesLoading?: boolean; }): ThreadListV2ListItem[] { const queuedThreadKeys = input.queuedThreadKeys?.size ? input.queuedThreadKeys : undefined; @@ -1579,7 +1579,7 @@ index 61280bad84..89e8cee610 100644 const snoozeWakeLabelText = item.snoozed && item.thread.snoozedUntil != null && input.snoozeLabelNow !== undefined ? snoozeWakeLabel(item.thread.snoozedUntil, { now: input.snoozeLabelNow }) -@@ -495,7 +753,7 @@ export function buildThreadListV2ListItems(input: { +@@ -516,7 +774,7 @@ export function buildThreadListV2ListItems(input: { item.variant === "card" ? input.moveAvailability?.get(`${item.thread.environmentId}:${item.thread.id}`) : undefined; @@ -1588,7 +1588,7 @@ index 61280bad84..89e8cee610 100644 type: "v2-thread", key: `v2-thread:${item.thread.environmentId}:${item.thread.id}`, item, -@@ -508,7 +766,50 @@ export function buildThreadListV2ListItems(input: { +@@ -529,7 +787,50 @@ export function buildThreadListV2ListItems(input: { canMoveUp: move?.canMoveUp === true, canMoveDown: move?.canMoveDown === true, }; @@ -1639,7 +1639,7 @@ index 61280bad84..89e8cee610 100644 const pendingItems = input.pendingTasks.map((pendingTask, index): ThreadListV2ListItem => ({ type: "v2-pending", key: `v2-${pendingTask.key}`, -@@ -522,10 +823,10 @@ export function buildThreadListV2ListItems(input: { +@@ -543,10 +844,10 @@ export function buildThreadListV2ListItems(input: { const snoozedShelfHeaderIndex = input.snoozedShelfHeaderIndex ?? null; const settledCount = input.settledCount ?? 0; const settledShelfHeaderIndex = input.settledShelfHeaderIndex ?? null; @@ -1652,7 +1652,7 @@ index 61280bad84..89e8cee610 100644 const shelfDisabled = input.shelfPreferencesLoading === true; if (workingShelfHeaderIndex !== null && workingCount > 0) { result.push({ -@@ -535,7 +836,7 @@ export function buildThreadListV2ListItems(input: { +@@ -556,7 +857,7 @@ export function buildThreadListV2ListItems(input: { expanded: input.workingShelfExpanded === true, disabled: shelfDisabled, }); @@ -1661,7 +1661,7 @@ index 61280bad84..89e8cee610 100644 } if (snoozedShelfHeaderIndex !== null && snoozedCount > 0) { result.push({ -@@ -545,7 +846,7 @@ export function buildThreadListV2ListItems(input: { +@@ -566,7 +867,7 @@ export function buildThreadListV2ListItems(input: { expanded: input.snoozedShelfExpanded === true, disabled: shelfDisabled, }); @@ -1670,7 +1670,7 @@ index 61280bad84..89e8cee610 100644 } if (settledShelfHeaderIndex !== null && settledCount > 0) { result.push({ -@@ -555,13 +856,24 @@ export function buildThreadListV2ListItems(input: { +@@ -576,13 +877,24 @@ export function buildThreadListV2ListItems(input: { expanded: input.settledShelfExpanded !== false, disabled: shelfDisabled, }); @@ -1698,7 +1698,7 @@ index 61280bad84..89e8cee610 100644 return showTrailingDivider === entry.showTrailingDivider ? entry : { ...entry, showTrailingDivider }; -@@ -616,6 +928,12 @@ export function buildThreadListV2Items(input: { +@@ -637,6 +949,12 @@ export function buildThreadListV2Items(input: { readonly isListedSettled?: (thread: EnvironmentThreadShell) => boolean; /** Settled threads in scope that are not local (older settled threads not loaded). */ readonly unloadedSettledCount?: number; @@ -1711,7 +1711,7 @@ index 61280bad84..89e8cee610 100644 }): ThreadListV2Layout { const now = input.now; const pending = -@@ -629,76 +947,18 @@ export function buildThreadListV2Items(input: { +@@ -650,76 +968,18 @@ export function buildThreadListV2Items(input: { pendingOrder: null, }), ); @@ -1799,7 +1799,7 @@ index 61280bad84..89e8cee610 100644 if (rankedMatches !== null) { // Searching lists every match by rank across shelves, as the palette and -@@ -728,6 +988,7 @@ export function buildThreadListV2Items(input: { +@@ -749,6 +1009,7 @@ export function buildThreadListV2Items(input: { isLast: index === results.length - 1, })), hiddenSettledCount: 0, @@ -1807,7 +1807,7 @@ index 61280bad84..89e8cee610 100644 listedSettledThreads: [], workingCount: 0, workingShelfHeaderIndex: null, -@@ -762,95 +1023,170 @@ export function buildThreadListV2Items(input: { +@@ -783,95 +1044,170 @@ export function buildThreadListV2Items(input: { (thread) => `${thread.environmentId}:${thread.id}` === selectedThreadKey, ); const orderedSettled = sortSettledThreadsReusingLast(settled); @@ -2149,27 +2149,36 @@ index 4348908e53..5ad83bb485 100644 snoozedShelfExpanded, workingShelfEnabled, diff --git a/apps/mobile/src/features/threads/useThreadListV2.ts b/apps/mobile/src/features/threads/useThreadListV2.ts -index 1a0c67062f..5291af8bc2 100644 +index fcf1048d2b..68114d3cda 100644 --- a/apps/mobile/src/features/threads/useThreadListV2.ts +++ b/apps/mobile/src/features/threads/useThreadListV2.ts @@ -65,6 +65,8 @@ export function useThreadListV2(input: { readonly snoozedShelfExpanded: boolean; readonly settledShelfExpanded: boolean; readonly selectedThreadKey: string | null; -+ readonly childThreadFolds: LayoutInput["childThreadFolds"]; -+ readonly shownSettledChildThreadKeys: LayoutInput["shownSettledChildThreadKeys"]; ++ readonly childThreadFolds?: LayoutInput["childThreadFolds"]; ++ readonly shownSettledChildThreadKeys?: LayoutInput["shownSettledChildThreadKeys"]; }) { const { threads, workingShelfEnabled } = input; - // A thread can stop working when the threads change, or when its exit grace expires. -@@ -127,6 +129,8 @@ export function useThreadListV2(input: { - snoozedShelfExpanded: input.snoozedShelfExpanded, - settledShelfExpanded: input.settledShelfExpanded, - selectedThreadKey: input.selectedThreadKey, -+ childThreadFolds: input.childThreadFolds, -+ shownSettledChildThreadKeys: input.shownSettledChildThreadKeys, - isListedSettled: olderSettledShelf.isListed, - unloadedSettledCount: olderSettledShelf.unloadedCount( - threads.filter( + const layoutTime = useMemo( +@@ -152,6 +154,8 @@ export function useThreadListV2(input: { + snoozedShelfExpanded: input.snoozedShelfExpanded, + settledShelfExpanded: input.settledShelfExpanded, + selectedThreadKey: input.selectedThreadKey, ++ childThreadFolds: input.childThreadFolds, ++ shownSettledChildThreadKeys: input.shownSettledChildThreadKeys, + isListedSettled: isListed, + unloadedSettledCount: unloadedCount( + threads.filter( +@@ -184,6 +188,8 @@ export function useThreadListV2(input: { + input.snoozedShelfExpanded, + input.settledShelfExpanded, + input.selectedThreadKey, ++ input.childThreadFolds, ++ input.shownSettledChildThreadKeys, + isListed, + unloadedCount, + scopes, diff --git a/apps/mobile/src/lib/storage.test.ts b/apps/mobile/src/lib/storage.test.ts index ed95f5f142..139a029490 100644 --- a/apps/mobile/src/lib/storage.test.ts @@ -2245,10 +2254,10 @@ index 4f147667d9..855a3d971d 100644 preferences.threadListFilters = parsed.threadListFilters; } diff --git a/apps/mobile/src/state/entities.ts b/apps/mobile/src/state/entities.ts -index c5c33a5432..7924f1ca90 100644 +index 1625998fdc..629a3c7271 100644 --- a/apps/mobile/src/state/entities.ts +++ b/apps/mobile/src/state/entities.ts -@@ -177,6 +177,11 @@ export function useNavigationThreadShells(): ReadonlyArray + scopedThreadKey(scopeThreadRef(thread.environmentId, thread.id)); diff --git a/packages/client-runtime/src/state/threadShell.test.ts b/packages/client-runtime/src/state/threadShell.test.ts -index 22de36bfd3..f7528ab0f2 100644 +index f0c5458811..237065ae8c 100644 --- a/packages/client-runtime/src/state/threadShell.test.ts +++ b/packages/client-runtime/src/state/threadShell.test.ts -@@ -108,6 +108,43 @@ describe("v2 thread shell lists", () => { +@@ -112,6 +112,43 @@ describe("v2 thread shell lists", () => { registry.dispose(); }); @@ -3876,10 +3885,10 @@ index 22de36bfd3..f7528ab0f2 100644 const { registry, threads, snapshotAtom } = makeHarness(); const root = v2ThreadShell; diff --git a/packages/client-runtime/src/state/threadShell.ts b/packages/client-runtime/src/state/threadShell.ts -index 738e2e2a96..778880c0f6 100644 +index 28babf041c..237e910199 100644 --- a/packages/client-runtime/src/state/threadShell.ts +++ b/packages/client-runtime/src/state/threadShell.ts -@@ -662,6 +662,29 @@ export function createEnvironmentThreadShellAtoms(input: { +@@ -724,6 +724,29 @@ export function createEnvironmentThreadShellAtoms(input: { return next; }).pipe(Atom.withLabel("environment-navigation-thread-shells")); @@ -3909,7 +3918,7 @@ index 738e2e2a96..778880c0f6 100644 return { environmentThreadsAtom, environmentThreadIndexAtom, -@@ -673,6 +696,7 @@ export function createEnvironmentThreadShellAtoms(input: { +@@ -738,6 +761,7 @@ export function createEnvironmentThreadShellAtoms(input: { navigationThreadShellsAtom, delegatedUnderParentAtom, pendingSubagentRequestsAtom, diff --git a/patches-ios/0005-feat-mobile-register-push-tokens-directly-with-paire.patch b/patches-ios/0005-feat-mobile-register-push-tokens-directly-with-paire.patch index c3af5d0..f5667c3 100644 --- a/patches-ios/0005-feat-mobile-register-push-tokens-directly-with-paire.patch +++ b/patches-ios/0005-feat-mobile-register-push-tokens-directly-with-paire.patch @@ -819,10 +819,10 @@ index 0000000000..d0cf0bf7d9 + ); +} diff --git a/apps/mobile/src/features/settings/SettingsNotificationsRouteScreen.tsx b/apps/mobile/src/features/settings/SettingsNotificationsRouteScreen.tsx -index 4aa5cc4c70..f033972101 100644 +index cabe229f9a..f7b7da19f2 100644 --- a/apps/mobile/src/features/settings/SettingsNotificationsRouteScreen.tsx +++ b/apps/mobile/src/features/settings/SettingsNotificationsRouteScreen.tsx -@@ -20,7 +20,7 @@ import { +@@ -26,7 +26,7 @@ import { settlePromise, squashAtomCommandFailure, } from "@t3tools/client-runtime/state/runtime"; @@ -831,15 +831,15 @@ index 4aa5cc4c70..f033972101 100644 import { openAndroidLiveUpdateSettings, supportsAndroidLiveUpdateSettings, -@@ -42,6 +42,7 @@ import { SettingsRow } from "./components/SettingsRow"; +@@ -48,6 +48,7 @@ import { SettingsRow } from "./components/SettingsRow"; import { SettingsSection } from "./components/SettingsSection"; import { SettingsSwitchRow } from "./components/SettingsSwitchRow"; import { SettingsScreen } from "./components/SettingsScreen"; +import { DirectPushSettingsNotificationsRouteScreen } from "./DirectPushSettingsNotificationsRouteScreen"; import { resolveAgentAwarenessPlatformPresentation } from "./SettingsRouteScreen.logic"; - type NotificationStatus = "checking" | "enabled" | "disabled" | "unsupported"; -@@ -89,6 +90,9 @@ function useDeviceRegistered(): boolean { + /** +@@ -109,6 +110,9 @@ function useDeviceRegistered(): boolean { } export function SettingsNotificationsRouteScreen() { diff --git a/patches-ios/0007-feat-mobile-label-child-agents-as-agent-or-subagent.patch b/patches-ios/0007-feat-mobile-label-child-agents-as-agent-or-subagent.patch index 8026bc8..39e99a2 100644 --- a/patches-ios/0007-feat-mobile-label-child-agents-as-agent-or-subagent.patch +++ b/patches-ios/0007-feat-mobile-label-child-agents-as-agent-or-subagent.patch @@ -263,7 +263,7 @@ index 0000000000..52626bbb66 + }); +}); diff --git a/apps/mobile/src/lib/threadActivity.ts b/apps/mobile/src/lib/threadActivity.ts -index 91681731aa..458a5a05bb 100644 +index 8cb5ccb043..40f9bce6f5 100644 --- a/apps/mobile/src/lib/threadActivity.ts +++ b/apps/mobile/src/lib/threadActivity.ts @@ -4,7 +4,10 @@ import type { diff --git a/patches-port/0001-Stats-for-Nerds-response-throughput-and-the-composer.patch b/patches-port/0001-Stats-for-Nerds-response-throughput-and-the-composer.patch index c8bbcb6..a7849f3 100644 --- a/patches-port/0001-Stats-for-Nerds-response-throughput-and-the-composer.patch +++ b/patches-port/0001-Stats-for-Nerds-response-throughput-and-the-composer.patch @@ -5,12 +5,12 @@ Subject: [PATCH 1/5] Stats for Nerds: response throughput and the composer stats line --- - .../Adapters/ClaudeAdapterV2.test.ts | 200 +++++++ + .../Adapters/ClaudeAdapterV2.test.ts | 210 +++++++ .../Adapters/ClaudeAdapterV2.ts | 37 +- .../Adapters/ClaudeResponseThroughput.test.ts | 230 ++++++++ .../Adapters/ClaudeResponseThroughput.ts | 155 +++++ - .../Adapters/CodexAdapterV2.test.ts | 223 +++++++ - .../Adapters/CodexAdapterV2.ts | 150 +++-- + .../Adapters/CodexAdapterV2.test.ts | 233 ++++++++ + .../Adapters/CodexAdapterV2.ts | 158 +++-- .../Adapters/CodexResponseThroughput.test.ts | 203 +++++++ .../Adapters/CodexResponseThroughput.ts | 150 +++++ .../orchestration-v2/ProjectionStore.test.ts | 37 +- @@ -26,7 +26,7 @@ Subject: [PATCH 1/5] Stats for Nerds: response throughput and the composer .../contracts/src/orchestrationV2.test.ts | 78 ++- packages/contracts/src/orchestrationV2.ts | 10 + packages/contracts/src/settings.ts | 2 + - 21 files changed, 2767 insertions(+), 52 deletions(-) + 21 files changed, 2791 insertions(+), 56 deletions(-) create mode 100644 apps/server/src/orchestration-v2/Adapters/ClaudeResponseThroughput.test.ts create mode 100644 apps/server/src/orchestration-v2/Adapters/ClaudeResponseThroughput.ts create mode 100644 apps/server/src/orchestration-v2/Adapters/CodexResponseThroughput.test.ts @@ -36,7 +36,7 @@ Subject: [PATCH 1/5] Stats for Nerds: response throughput and the composer create mode 100644 apps/web/src/components/chat/ComposerStats.tsx diff --git a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts -index 8494c4cab7..5f6b80fc6e 100644 +index 45d21b5944..065f16c37c 100644 --- a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts +++ b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts @@ -3,6 +3,7 @@ import * as NodeEvents from "node:events"; @@ -47,7 +47,7 @@ index 8494c4cab7..5f6b80fc6e 100644 import * as ClaudeSdk from "@anthropic-ai/claude-agent-sdk"; import { beforeEach, vi } from "vite-plus/test"; -@@ -5067,6 +5068,205 @@ describe("ClaudeAdapterV2 background wake turns", () => { +@@ -5302,6 +5303,215 @@ describe("ClaudeAdapterV2 background wake turns", () => { ), ); @@ -135,7 +135,12 @@ index 8494c4cab7..5f6b80fc6e 100644 + ); + }).pipe( + Effect.provide( -+ Layer.mergeAll(IdAllocator.layer, McpProviderSessions.layer, NodeServices.layer), ++ Layer.mergeAll( ++ IdAllocator.layer, ++ McpProviderSessions.layer, ++ NodeServices.layer, ++ NodeCrypto.layer, ++ ), + ), + ), + ); @@ -245,7 +250,12 @@ index 8494c4cab7..5f6b80fc6e 100644 + yield* Queue.take(harness.terminalReceipts); + }).pipe( + Effect.provide( -+ Layer.mergeAll(IdAllocator.layer, McpProviderSessions.layer, NodeServices.layer), ++ Layer.mergeAll( ++ IdAllocator.layer, ++ McpProviderSessions.layer, ++ NodeServices.layer, ++ NodeCrypto.layer, ++ ), + ), + ), + ); @@ -254,7 +264,7 @@ index 8494c4cab7..5f6b80fc6e 100644 "projects Claude thinking blocks when %s", (status) => diff --git a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts -index 48ed26517c..21955d95cc 100644 +index 674f55c524..cdb826fd83 100644 --- a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts +++ b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts @@ -1,4 +1,5 @@ @@ -263,10 +273,10 @@ index 48ed26517c..21955d95cc 100644 import { makeProviderTextDeltaCoalescer } from "./ProviderTextDeltaCoalescer.ts"; import { claudeTaskOutputRoot, tailClaudeTaskOutput } from "./ClaudeTaskOutputTail.ts"; import { -@@ -1130,6 +1131,10 @@ export const layerQueryRunner: Layer.Layer< - messages: Stream.fromAsyncIterable(claudeQueryMessages(queryRuntime), (cause) => - queryRunnerError(cause, "fromAsyncIterable"), - ).pipe( +@@ -1195,6 +1196,10 @@ export const layerQueryRunner: Layer.Layer< + answered = true; + }), + ), + Stream.map((message) => { + observeClaudeFrame(message); + return message; @@ -274,7 +284,7 @@ index 48ed26517c..21955d95cc 100644 Stream.tap((message) => logProtocolEvent({ direction: "incoming", -@@ -3972,6 +3977,7 @@ function formatClaudeUsageLimitWait(waitMs: number): string { +@@ -4082,6 +4087,7 @@ function formatClaudeUsageLimitWait(waitMs: number): string { interface ActiveClaudeTurnContext { readonly queryPolicy: ClaudeRuntimeQueryPolicy; @@ -282,7 +292,7 @@ index 48ed26517c..21955d95cc 100644 readonly input: ProviderAdapter.ProviderAdapterV2TurnInput; readonly nativeThreadId: string; readonly nativeTurnId: string; -@@ -5524,6 +5530,8 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* ( +@@ -5664,6 +5670,8 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* ( return ordinal; }); @@ -291,7 +301,7 @@ index 48ed26517c..21955d95cc 100644 const providerTurnPayload = (input: { readonly context: ActiveClaudeTurnContext; readonly status: OrchestrationV2ProviderTurn["status"]; -@@ -5542,6 +5550,9 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* ( +@@ -5682,6 +5690,9 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* ( status: input.status, startedAt: input.context.startedAt, completedAt: input.completedAt, @@ -301,7 +311,7 @@ index 48ed26517c..21955d95cc 100644 }); const buildToolCallArtifacts = (input: { -@@ -7520,6 +7531,7 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* ( +@@ -7696,6 +7707,7 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* ( failure: null, threadDisposition, }; @@ -309,7 +319,7 @@ index 48ed26517c..21955d95cc 100644 yield* Effect.all( [ emitProviderEvent({ -@@ -8344,6 +8356,20 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* ( +@@ -8574,6 +8586,20 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* ( } yield* trackClaudeGoal({ nativeThreadId: liveQuery.nativeThreadId, context, message }); @@ -330,9 +340,9 @@ index 48ed26517c..21955d95cc 100644 if (message.type === "stream_event" && !message.parent_tool_use_id) { const event = message.event; const reasoning = context.reasoning; -@@ -10332,8 +10358,10 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* ( - }; - yield* Ref.set(queryContext, context); +@@ -10618,8 +10644,10 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* ( + return context; + }); yield* querySession.messages.pipe( - Stream.runForEach((message) => - sdkMessageLock.withPermits(1)( @@ -343,7 +353,7 @@ index 48ed26517c..21955d95cc 100644 Effect.suspend(() => { if ( message.type === "system" && -@@ -10353,8 +10381,8 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* ( +@@ -10649,8 +10677,8 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* ( } return handleSdkMessage({ query: querySession, message }); }), @@ -354,7 +364,7 @@ index 48ed26517c..21955d95cc 100644 Effect.exit, Effect.flatMap( Effect.fnUntraced(function* (exit: ClaudeQueryStreamExit) { -@@ -10461,6 +10489,7 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* ( +@@ -10759,6 +10787,7 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* ( ); const querySession = yield* openQuery(turnInput, nativeThreadId); const context: ActiveClaudeTurnContext = { @@ -760,10 +770,10 @@ index 0000000000..4e2ee18122 + } +} diff --git a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts -index 90ee44f3d5..939859e1df 100644 +index 4471be814b..3de8c1f867 100644 --- a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts +++ b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts -@@ -1142,6 +1142,7 @@ describe("CodexAdapterV2 post-settle continuation", () => { +@@ -1174,6 +1174,7 @@ describe("CodexAdapterV2 post-settle continuation", () => { onRequest: (method: string, params: unknown) => Effect.Effect = () => Effect.void, readChildMetadata?: Parameters[2], beforeEmitInbound?: CodexReplay.CodexAppServerReplayDriver["beforeEmitInbound"], @@ -771,15 +781,15 @@ index 90ee44f3d5..939859e1df 100644 ) => Effect.gen(function* () { const fileSystem = yield* FileSystem.FileSystem; -@@ -1193,6 +1194,7 @@ describe("CodexAdapterV2 post-settle continuation", () => { +@@ -1232,6 +1233,7 @@ describe("CodexAdapterV2 post-settle continuation", () => { environment: {}, clientFactory, - crypto: yield* Crypto.Crypto, + digest: yield* Digest.Digest, + ...hooks, fileSystem, idAllocator, serverConfig, -@@ -1368,6 +1370,75 @@ describe("CodexAdapterV2 post-settle continuation", () => { +@@ -1412,6 +1414,80 @@ describe("CodexAdapterV2 post-settle continuation", () => { ), ); @@ -846,7 +856,12 @@ index 90ee44f3d5..939859e1df 100644 + assert.deepStrictEqual(methods, ["initialize", "thread/start", "account/rateLimits/read"]); + }).pipe( + Effect.provide( -+ Layer.mergeAll(IdAllocator.layer, McpProviderSessions.layer, NodeServices.layer), ++ Layer.mergeAll( ++ IdAllocator.layer, ++ McpProviderSessions.layer, ++ NodeServices.layer, ++ NodeCrypto.layer, ++ ), + ), + ), + ), @@ -855,7 +870,7 @@ index 90ee44f3d5..939859e1df 100644 it.effect.each(["supported", "unsupported", "invalid"] as const)( "delivers native history with %s app-server protocol", (response) => -@@ -1825,6 +1896,7 @@ describe("CodexAdapterV2 post-settle continuation", () => { +@@ -1893,6 +1969,7 @@ describe("CodexAdapterV2 post-settle continuation", () => { ); assert.equal(completed?.type, "provider_turn.updated"); if (completed?.type === "provider_turn.updated") { @@ -863,7 +878,7 @@ index 90ee44f3d5..939859e1df 100644 assert.deepEqual(completed.providerTurn.turnTokenUsage, { usageStatus: "complete", usageScope: "main_agent", -@@ -2051,6 +2123,157 @@ describe("CodexAdapterV2 post-settle continuation", () => { +@@ -2129,6 +2206,162 @@ describe("CodexAdapterV2 post-settle continuation", () => { ), ); @@ -1012,7 +1027,12 @@ index 90ee44f3d5..939859e1df 100644 + ); + }).pipe( + Effect.provide( -+ Layer.mergeAll(IdAllocator.layer, McpProviderSessions.layer, NodeServices.layer), ++ Layer.mergeAll( ++ IdAllocator.layer, ++ McpProviderSessions.layer, ++ NodeServices.layer, ++ NodeCrypto.layer, ++ ), + ), + ), + ), @@ -1022,7 +1042,7 @@ index 90ee44f3d5..939859e1df 100644 Effect.scoped( Effect.gen(function* () { diff --git a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts -index 821cf051c3..1d970d3dd8 100644 +index 688de092c0..dd609a3bf4 100644 --- a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts +++ b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts @@ -1,5 +1,6 @@ @@ -1048,7 +1068,7 @@ index 821cf051c3..1d970d3dd8 100644 RuntimeMode, ThreadId, } from "@t3tools/contracts"; -@@ -1123,6 +1126,7 @@ interface ActiveCodexTurnContext { +@@ -1151,6 +1154,7 @@ interface ActiveCodexTurnContext { capacityRetryAttempt?: number; capacityRetryPending?: Deferred.Deferred; closedCommandItemIds?: ReadonlySet; @@ -1056,7 +1076,7 @@ index 821cf051c3..1d970d3dd8 100644 latestProviderFailure?: { readonly nativeMessage: string; readonly nativeCode: string | null; -@@ -1563,7 +1567,10 @@ export type CodexAdapterV2DriverEnv = +@@ -1609,7 +1613,10 @@ export type CodexAdapterV2DriverEnv = export const createCodexAdapterV2 = Effect.fn("CodexAdapterV2.createCodexAdapterV2")(function* ( { instanceId, environment, enabled, config }: ProviderAdapterDriverCreateInput, @@ -1068,7 +1088,7 @@ index 821cf051c3..1d970d3dd8 100644 ) { const clientFactory = yield* CodexAppServerClientFactory; const continuationRequests = yield* ProviderContinuationRequests.ProviderContinuationRequests; -@@ -1608,6 +1615,9 @@ export const createCodexAdapterV2 = Effect.fn("CodexAdapterV2.createCodexAdapter +@@ -1654,6 +1661,9 @@ export const createCodexAdapterV2 = Effect.fn("CodexAdapterV2.createCodexAdapter }); export interface CodexAdapterV2Options { @@ -1078,7 +1098,7 @@ index 821cf051c3..1d970d3dd8 100644 readonly instanceId: ProviderInstanceId; readonly settings: CodexSettings; readonly environment: NodeJS.ProcessEnv; -@@ -1758,6 +1768,23 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* ( +@@ -1804,6 +1814,23 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* ( ); // A failed open is discarded, so the next turn spawns the updated binary. if (adapterOptions.compatibility !== undefined) yield* ensureInitialized; @@ -1102,7 +1122,7 @@ index 821cf051c3..1d970d3dd8 100644 const now = yield* DateTime.now; const session = providerSession({ providerSessionId: input.providerSessionId, -@@ -2032,6 +2059,9 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* ( +@@ -2089,6 +2116,9 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* ( itemParentNodeId: input.turnInput.rootNodeId, rootNodeId: input.turnInput.rootNodeId, subagent: null, @@ -1112,7 +1132,7 @@ index 821cf051c3..1d970d3dd8 100644 startedAt: input.startedAt, itemPositions: new Map(), fileChangeKindsByItem: new Map(), -@@ -3382,12 +3412,15 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* ( +@@ -3439,12 +3469,15 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* ( return key; }; const appendReasoning = Effect.fn("CodexAdapterV2.appendReasoning")(function* ( @@ -1129,7 +1149,7 @@ index 821cf051c3..1d970d3dd8 100644 yield* completeProviderRetry(context, yield* DateTime.now); const itemId = reasoningPartKey(payload.turnId, payload.itemId, stream, index); // Reserve positions before delayed flushes so later tools stay after them. -@@ -4120,6 +4153,7 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* ( +@@ -4177,6 +4210,7 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* ( readonly appName?: string; readonly options?: ReadonlyArray; }) { @@ -1137,7 +1157,7 @@ index 821cf051c3..1d970d3dd8 100644 const createdAt = yield* DateTime.now; const { owner, subagent } = approvalOwnerCodexTurn(input.context); const parentNodeId = -@@ -4215,6 +4249,7 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* ( +@@ -4272,6 +4306,7 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* ( readonly questions: ReadonlyArray; readonly responseMode?: "message"; }) { @@ -1145,7 +1165,7 @@ index 821cf051c3..1d970d3dd8 100644 const createdAt = yield* DateTime.now; const requestId = yield* idAllocator.allocate.runtimeRequest({ driver: CODEX_PROVIDER, -@@ -4304,10 +4339,16 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* ( +@@ -4361,10 +4396,16 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* ( return { node, request, turnItem }; }); @@ -1164,7 +1184,7 @@ index 821cf051c3..1d970d3dd8 100644 yield* completeProviderRetry(context, yield* DateTime.now); } yield* agentMessageDeltas.append({ -@@ -4315,8 +4356,8 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* ( +@@ -4372,8 +4413,8 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* ( itemId: payload.itemId, delta: payload.delta, }); @@ -1175,7 +1195,7 @@ index 821cf051c3..1d970d3dd8 100644 yield* client.handleServerNotification("item/commandExecution/outputDelta", (payload) => Effect.gen(function* () { -@@ -4339,33 +4380,41 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* ( +@@ -4396,33 +4437,41 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* ( ); yield* client.handleServerNotification("item/reasoning/summaryTextDelta", (payload) => @@ -1225,7 +1245,7 @@ index 821cf051c3..1d970d3dd8 100644 yield* completeProviderRetry(context, yield* DateTime.now); const steps = payload.plan.map((step, index) => ({ id: `step-${index + 1}`, -@@ -4395,8 +4444,8 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* ( +@@ -4452,8 +4501,8 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* ( driver: CODEX_PROVIDER, turnItem: artifacts.turnItem, }); @@ -1236,7 +1256,7 @@ index 821cf051c3..1d970d3dd8 100644 yield* client.handleServerNotification("account/rateLimits/updated", (payload) => Effect.gen(function* () { -@@ -4428,6 +4477,29 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* ( +@@ -4485,6 +4534,29 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* ( }).pipe(Effect.orDie), ); @@ -1266,7 +1286,7 @@ index 821cf051c3..1d970d3dd8 100644 yield* client.handleServerNotification("thread/tokenUsage/updated", (payload) => Effect.gen(function* () { accumulateCodexTurnTokenUsage( -@@ -4731,13 +4803,17 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* ( +@@ -4811,13 +4883,17 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* ( }).pipe(Effect.orDie, turnTerminalizationPermit.withPermits(1)), ); @@ -1286,7 +1306,7 @@ index 821cf051c3..1d970d3dd8 100644 if (payload.item.type === "contextCompaction") { yield* emitCompactionItem( context, -@@ -4869,11 +4945,12 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* ( +@@ -4949,11 +5025,12 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* ( driver: CODEX_PROVIDER, turnItem: artifacts.turnItem, }); @@ -1303,7 +1323,7 @@ index 821cf051c3..1d970d3dd8 100644 if (payload.item.type === "contextCompaction") // Fork requests off the notification reader so it can read their responses. yield* restoreAdditionalContext(payload.threadId).pipe( -@@ -4899,6 +4976,12 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* ( +@@ -4979,6 +5056,12 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* ( return; } const { context, settled } = resolved; @@ -1316,7 +1336,7 @@ index 821cf051c3..1d970d3dd8 100644 if (payload.item.type === "reasoning") { yield* completeReasoning(payload.turnId, payload.item); -@@ -5191,8 +5274,8 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* ( +@@ -5271,8 +5354,8 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* ( result: text, }); } @@ -1327,7 +1347,7 @@ index 821cf051c3..1d970d3dd8 100644 yield* client.handleServerRequest("item/commandExecution/requestApproval", (payload) => Effect.gen(function* () { -@@ -6070,6 +6153,15 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* ( +@@ -6153,6 +6236,19 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* ( yield* emitImageWorkState(input.context, images.running.size > 0); } } @@ -1338,12 +1358,16 @@ index 821cf051c3..1d970d3dd8 100644 + ), + input.nativeTurnId, + input.status === "completed", ++ reportedTurnModel( ++ input.context.providerThread.nativeThreadRef?.nativeId ?? "", ++ input.nativeTurnId, ++ ), + ); + const responseThroughput = input.context.responseThroughput?.finalize(turnTokenUsage); const nativeThreadId = input.context.providerThread.nativeThreadRef?.nativeId; const activation = nativeThreadId == null ? undefined : goalActivations.get(nativeThreadId); -@@ -6094,14 +6186,8 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* ( +@@ -6177,18 +6273,8 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* ( status: input.status, startedAt: input.context.startedAt, completedAt: input.completedAt, @@ -1354,6 +1378,10 @@ index 821cf051c3..1d970d3dd8 100644 - ), - input.nativeTurnId, - input.status === "completed", +- reportedTurnModel( +- input.context.providerThread.nativeThreadRef?.nativeId ?? "", +- input.nativeTurnId, +- ), - ), + turnTokenUsage, + ...(responseThroughput === undefined ? {} : { responseThroughput }), @@ -1726,10 +1754,10 @@ index 0000000000..1df8c44435 + } +} diff --git a/apps/server/src/orchestration-v2/ProjectionStore.test.ts b/apps/server/src/orchestration-v2/ProjectionStore.test.ts -index 9bd3566e55..4f9c74732b 100644 +index 5b24a65a99..1d2db40308 100644 --- a/apps/server/src/orchestration-v2/ProjectionStore.test.ts +++ b/apps/server/src/orchestration-v2/ProjectionStore.test.ts -@@ -1755,6 +1755,13 @@ it.layer(layerTest)("ProjectionStoreV2", (it) => { +@@ -2123,6 +2123,13 @@ it.layer(layerTest)("ProjectionStoreV2", (it) => { outputTokens: 1_000, updatedAt: "2026-08-29T12:00:00.000Z", } as const; @@ -1743,7 +1771,7 @@ index 9bd3566e55..4f9c74732b 100644 const replacementUsage = { usedTokens: 18_000, maxTokens: 200_000, -@@ -1811,10 +1818,10 @@ it.layer(layerTest)("ProjectionStoreV2", (it) => { +@@ -2179,10 +2186,10 @@ it.layer(layerTest)("ProjectionStoreV2", (it) => { nodeId, driver, occurredAt: now, @@ -1756,7 +1784,7 @@ index 9bd3566e55..4f9c74732b 100644 type: "provider-turn.updated", threadId, nodeId, -@@ -1822,9 +1829,28 @@ it.layer(layerTest)("ProjectionStoreV2", (it) => { +@@ -2190,9 +2197,28 @@ it.layer(layerTest)("ProjectionStoreV2", (it) => { occurredAt: now, payload: { ...providerTurn, status: "completed", completedAt: now }, }); @@ -1786,7 +1814,7 @@ index 9bd3566e55..4f9c74732b 100644 assert.strictEqual(reloaded.providerTurns[0]?.status, "completed"); yield* projectionStore.apply({ -@@ -1839,11 +1865,16 @@ it.layer(layerTest)("ProjectionStoreV2", (it) => { +@@ -2207,11 +2233,16 @@ it.layer(layerTest)("ProjectionStoreV2", (it) => { status: "completed", completedAt: now, tokenUsage: replacementUsage, @@ -1804,10 +1832,10 @@ index 9bd3566e55..4f9c74732b 100644 ); diff --git a/apps/server/src/orchestration-v2/ProjectionStore.ts b/apps/server/src/orchestration-v2/ProjectionStore.ts -index 95c0f7a4ba..c2df567ea0 100644 +index 3cffce15b7..14c7dc4802 100644 --- a/apps/server/src/orchestration-v2/ProjectionStore.ts +++ b/apps/server/src/orchestration-v2/ProjectionStore.ts -@@ -701,6 +701,9 @@ function upsertProviderTurn( +@@ -717,6 +717,9 @@ function upsertProviderTurn( ...((next.tokenUsage ?? current?.tokenUsage) === undefined ? {} : { tokenUsage: next.tokenUsage ?? current?.tokenUsage }), @@ -1817,7 +1845,7 @@ index 95c0f7a4ba..c2df567ea0 100644 }); } -@@ -2318,7 +2321,8 @@ export const layer: Layer.Layer = +@@ -2357,7 +2360,8 @@ export const layer: Layer.Layer = } case "provider-turn.updated": { const existingRows = @@ -1828,10 +1856,10 @@ index 95c0f7a4ba..c2df567ea0 100644 SELECT payload_json FROM orchestration_v2_projection_provider_turns diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx -index 55d951d1bd..cc29d64c8f 100644 +index a1c1c7bc91..5c4e13eefc 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx -@@ -310,6 +310,8 @@ import { useIncrementalTimelineEntries } from "../hooks/useIncrementalTimelineEn +@@ -311,6 +311,8 @@ import { useIncrementalTimelineEntries } from "../hooks/useIncrementalTimelineEn import { useRightPanelSurfaceClose } from "../hooks/useRightPanelSurfaceClose"; import { ComposerBannerStack, type ComposerBannerStackItem } from "./chat/ComposerBannerStack"; import { modelFallbackBannerItem } from "./chat/ComposerUsageLimits"; @@ -1872,7 +1900,7 @@ index 55d951d1bd..cc29d64c8f 100644 const pendingBackgroundTasks = deriveThreadPendingBackgroundTasks(serverProjection); const activeWorkStartedAt = deriveActiveWorkStartedAt(activeActivityRun, activeRuntime, localDispatchStartedAt) ?? -@@ -3796,10 +3823,14 @@ export default function ChatView(props: ChatViewProps) { +@@ -3809,10 +3836,14 @@ export default function ChatView(props: ChatViewProps) { ) : null} @@ -3146,10 +3174,10 @@ index 0000000000..e936a4e10c + ); +} diff --git a/apps/web/src/components/settings/SettingsPanels.tsx b/apps/web/src/components/settings/SettingsPanels.tsx -index 91cb8d6613..5b4fe5042e 100644 +index 362210f96e..1e9e700ca2 100644 --- a/apps/web/src/components/settings/SettingsPanels.tsx +++ b/apps/web/src/components/settings/SettingsPanels.tsx -@@ -502,6 +502,9 @@ export function useSettingsRestore(onRestored?: () => void) { +@@ -528,6 +528,9 @@ export function useSettingsRestore(onRestored?: () => void) { ...(settings.followUpBehavior !== DEFAULT_UNIFIED_SETTINGS.followUpBehavior ? ["Follow-up behavior"] : []), @@ -3159,7 +3187,7 @@ index 91cb8d6613..5b4fe5042e 100644 ...(settings.contextWindowMeterEnabled !== DEFAULT_UNIFIED_SETTINGS.contextWindowMeterEnabled ? ["Context window indicator"] : []), -@@ -616,6 +619,7 @@ export function useSettingsRestore(onRestored?: () => void) { +@@ -642,6 +645,7 @@ export function useSettingsRestore(onRestored?: () => void) { composerRichTextEnabled: DEFAULT_UNIFIED_SETTINGS.composerRichTextEnabled, sendShortcut: DEFAULT_UNIFIED_SETTINGS.sendShortcut, followUpBehavior: DEFAULT_UNIFIED_SETTINGS.followUpBehavior, @@ -3167,7 +3195,7 @@ index 91cb8d6613..5b4fe5042e 100644 contextWindowMeterEnabled: DEFAULT_UNIFIED_SETTINGS.contextWindowMeterEnabled, environmentIdentificationMode: DEFAULT_UNIFIED_SETTINGS.environmentIdentificationMode, glassOpacity: DEFAULT_UNIFIED_SETTINGS.glassOpacity, -@@ -1131,6 +1135,31 @@ export function ChatSettingsPanel() { +@@ -1157,6 +1161,31 @@ export function ChatSettingsPanel() { } /> @@ -3200,10 +3228,10 @@ index 91cb8d6613..5b4fe5042e 100644 { diff --git a/packages/client-runtime/src/state/orchestrationV2Projection.ts b/packages/client-runtime/src/state/orchestrationV2Projection.ts -index fbba7d1bd9..448103fec9 100644 +index d9368ef405..5a79392b61 100644 --- a/packages/client-runtime/src/state/orchestrationV2Projection.ts +++ b/packages/client-runtime/src/state/orchestrationV2Projection.ts -@@ -347,22 +347,23 @@ export function applyOrchestrationV2ProjectionEvent( +@@ -348,22 +348,23 @@ export function applyOrchestrationV2ProjectionEvent( }; case "provider-thread.updated": return { ...base, providerThreads: upsertEntity(base.providerThreads, event.payload) }; @@ -3388,10 +3416,10 @@ index 3782307171..abc457cdb8 100644 + }); +}); diff --git a/packages/contracts/src/orchestrationV2.ts b/packages/contracts/src/orchestrationV2.ts -index 1c80128f8b..e651d00b25 100644 +index 785f41e3aa..769a31daa9 100644 --- a/packages/contracts/src/orchestrationV2.ts +++ b/packages/contracts/src/orchestrationV2.ts -@@ -1179,6 +1179,16 @@ export const OrchestrationV2ProviderTurn = Schema.Struct({ +@@ -1184,6 +1184,16 @@ export const OrchestrationV2ProviderTurn = Schema.Struct({ completedAt: Schema.NullOr(Schema.DateTimeUtc), tokenUsage: Schema.optional(OrchestrationV2ProviderTurnTokenUsage), turnTokenUsage: Schema.optional(TurnTokenUsage), @@ -3409,10 +3437,10 @@ index 1c80128f8b..e651d00b25 100644 export type OrchestrationV2ProviderTurn = typeof OrchestrationV2ProviderTurn.Type; diff --git a/packages/contracts/src/settings.ts b/packages/contracts/src/settings.ts -index fff25149b2..d608fc8068 100644 +index 4bb3119561..00784d571f 100644 --- a/packages/contracts/src/settings.ts +++ b/packages/contracts/src/settings.ts -@@ -465,6 +465,7 @@ export const ClientSettingsSchema = Schema.Struct({ +@@ -466,6 +466,7 @@ export const ClientSettingsSchema = Schema.Struct({ planModeEnabled: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))), contextWindowMeterEnabled: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))), @@ -3420,7 +3448,7 @@ index fff25149b2..d608fc8068 100644 composerRichTextEnabled: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(true))), sendShortcut: Schema.Literals(["enter", "mod-enter-multiline", "mod-enter"]).pipe( Schema.withDecodingDefault(Effect.succeed("enter")), -@@ -1768,6 +1769,7 @@ export const ClientSettingsPatch = Schema.Struct({ +@@ -1806,6 +1807,7 @@ export const ClientSettingsPatch = Schema.Struct({ pullRequestLinksOpenInApp: Schema.optionalKey(Schema.Boolean), planModeEnabled: Schema.optionalKey(Schema.Boolean), contextWindowMeterEnabled: Schema.optionalKey(Schema.Boolean), diff --git a/patches-port/0002-Usage-limits-background-polling-reset-aware-merging-.patch b/patches-port/0002-Usage-limits-background-polling-reset-aware-merging-.patch index ac641e3..ac8e142 100644 --- a/patches-port/0002-Usage-limits-background-polling-reset-aware-merging-.patch +++ b/patches-port/0002-Usage-limits-background-polling-reset-aware-merging-.patch @@ -78,10 +78,10 @@ index ed9e3d0cde..72cb87ef0e 100644 return buildServerProvider({ diff --git a/apps/server/src/provider/Drivers/ClaudeDriver.ts b/apps/server/src/provider/Drivers/ClaudeDriver.ts -index e1fbcd3008..d2dbd93847 100644 +index c0a9c5698d..3520fdf50e 100644 --- a/apps/server/src/provider/Drivers/ClaudeDriver.ts +++ b/apps/server/src/provider/Drivers/ClaudeDriver.ts -@@ -170,6 +170,11 @@ export const ClaudeDriver: ProviderDriver @@ -106,7 +106,7 @@ index e1fbcd3008..d2dbd93847 100644 ), ), Effect.tap((provider) => -@@ -300,6 +301,8 @@ export const ClaudeDriver: ProviderDriver diff --git a/apps/server/src/provider/Drivers/CodexDriver.ts b/apps/server/src/provider/Drivers/CodexDriver.ts -index 97ec4954ed..66d1d43889 100644 +index c34aa00a19..40f975319c 100644 --- a/apps/server/src/provider/Drivers/CodexDriver.ts +++ b/apps/server/src/provider/Drivers/CodexDriver.ts @@ -1,4 +1,8 @@ @@ -126,10 +126,10 @@ index 97ec4954ed..66d1d43889 100644 + ProviderDriverKind, + type ServerProviderUsageLimits, +} from "@t3tools/contracts"; - import * as Crypto from "effect/Crypto"; + import * as Digest from "effect/crypto/Digest"; + import * as SecureRandom from "effect/crypto/SecureRandom"; import * as Effect from "effect/Effect"; - import * as FileSystem from "effect/FileSystem"; -@@ -191,6 +195,7 @@ export const CodexDriver: ProviderDriver< +@@ -201,6 +205,7 @@ export const CodexDriver: ProviderDriver< ), ); @@ -137,7 +137,7 @@ index 97ec4954ed..66d1d43889 100644 const orchestrationAdapter = yield* createCodexAdapterV2( { instanceId, -@@ -203,6 +208,14 @@ export const CodexDriver: ProviderDriver< +@@ -213,6 +218,14 @@ export const CodexDriver: ProviderDriver< { onUsageLimits: (update) => snapshot.applyUsageLimits(update), compatibility: installation.compatibility, @@ -152,7 +152,7 @@ index 97ec4954ed..66d1d43889 100644 }, ).pipe( Effect.mapError( -@@ -244,6 +257,13 @@ export const CodexDriver: ProviderDriver< +@@ -254,6 +267,13 @@ export const CodexDriver: ProviderDriver< (draft, catalog) => stampIdentity(applyCodexModelCatalog(draft, catalog)), ), checkProvider, @@ -167,10 +167,10 @@ index 97ec4954ed..66d1d43889 100644 resolveMaintenance().pipe( Effect.flatMap((maintenanceCapabilities) => diff --git a/apps/server/src/provider/ProviderRegistry.test.ts b/apps/server/src/provider/ProviderRegistry.test.ts -index 5e1bc9e962..380cbcecc1 100644 +index 93fec081eb..c4c760a37c 100644 --- a/apps/server/src/provider/ProviderRegistry.test.ts +++ b/apps/server/src/provider/ProviderRegistry.test.ts -@@ -416,7 +416,11 @@ it.layer( +@@ -418,7 +418,11 @@ it.layer( checkCodexProviderStatus(defaultCodexSettings, () => Effect.succeed( makeCodexProbeSnapshot({ @@ -1542,7 +1542,7 @@ index d4f9c62111..ac65395da2 100644 return windows.reduce( (latest, window) => diff --git a/apps/server/src/usage/cliproxyApi.ts b/apps/server/src/usage/cliproxyApi.ts -index eb956a67a3..29bdfe118c 100644 +index daa1ea909f..0fdb4e559f 100644 --- a/apps/server/src/usage/cliproxyApi.ts +++ b/apps/server/src/usage/cliproxyApi.ts @@ -302,6 +302,7 @@ export const makeCliproxyApi = Effect.gen(function* () { @@ -2550,7 +2550,7 @@ index 1e4bcd0090..dce5f77c59 100644 + }; } diff --git a/packages/provider-cursor/src/server/usageLimits.ts b/packages/provider-cursor/src/server/usageLimits.ts -index 049e646d2c..49228632dc 100644 +index 87fcecef4e..b5a9050cf8 100644 --- a/packages/provider-cursor/src/server/usageLimits.ts +++ b/packages/provider-cursor/src/server/usageLimits.ts @@ -51,6 +51,7 @@ export function cursorUsageResponseToLimits( @@ -2574,7 +2574,7 @@ index 8036df0dd3..a5d6758897 100644 ...(Option.isSome(reset) ? { resetsAt: DateTime.formatIso(reset.value) } : {}), }; diff --git a/packages/provider-opencode/src/server/usageLimits.ts b/packages/provider-opencode/src/server/usageLimits.ts -index e691dc211f..980eb8b0c7 100644 +index 0f97ea36cf..d623b3c528 100644 --- a/packages/provider-opencode/src/server/usageLimits.ts +++ b/packages/provider-opencode/src/server/usageLimits.ts @@ -144,6 +144,7 @@ export const readOpenCodeGoUsageLimits = Effect.fn("readOpenCodeGoUsageLimits")( diff --git a/patches-port/0003-fix-web-show-compaction-settings-and-add-a-stats-com.patch b/patches-port/0003-fix-web-show-compaction-settings-and-add-a-stats-com.patch index e27dea5..65b007e 100644 --- a/patches-port/0003-fix-web-show-compaction-settings-and-add-a-stats-com.patch +++ b/patches-port/0003-fix-web-show-compaction-settings-and-add-a-stats-com.patch @@ -14,7 +14,7 @@ Subject: [PATCH 3/5] fix(web): show compaction settings and add a stats 6 files changed, 313 insertions(+), 25 deletions(-) diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx -index cc29d64c8f..e6e37e0c32 100644 +index 5c4e13eefc..57453af5b8 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -1466,20 +1466,40 @@ export default function ChatView(props: ChatViewProps) { @@ -62,7 +62,7 @@ index cc29d64c8f..e6e37e0c32 100644 ), }; })(); -@@ -3825,7 +3845,23 @@ export default function ChatView(props: ChatViewProps) { +@@ -3838,7 +3858,23 @@ export default function ChatView(props: ChatViewProps) { {/* Opaque so timeline content scrolling under the overlay can't show through the stats line. */}
diff --git a/patches-port/0004-Suggest-the-same-model-on-a-subscription-with-more-r.patch b/patches-port/0004-Suggest-the-same-model-on-a-subscription-with-more-r.patch index 6189fb4..1c0567f 100644 --- a/patches-port/0004-Suggest-the-same-model-on-a-subscription-with-more-r.patch +++ b/patches-port/0004-Suggest-the-same-model-on-a-subscription-with-more-r.patch @@ -24,7 +24,7 @@ explains why and when a started thread cannot switch. create mode 100644 apps/web/src/components/chat/SubscriptionSwitch.logic.ts diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx -index e6e37e0c32..e7e0b72af5 100644 +index 57453af5b8..0a76fc890e 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -219,6 +219,8 @@ import { @@ -54,7 +54,7 @@ index e6e37e0c32..e7e0b72af5 100644 const activeProviderName = activeProviderStatus?.displayName?.trim() || activeProviderStatus?.instanceId || ""; const composerStats = (() => { -@@ -3860,6 +3873,13 @@ export default function ChatView(props: ChatViewProps) { +@@ -3873,6 +3886,13 @@ export default function ChatView(props: ChatViewProps) { : compactDisabledReason } isCompacting={isCompacting} diff --git a/patches-port/base.txt b/patches-port/base.txt index 8fc6541..ff3e296 100644 --- a/patches-port/base.txt +++ b/patches-port/base.txt @@ -1 +1 @@ -80120b1aa34a3125c05da393c8616c2076f5888d +115739c1e02e181d6c176a4d8b43716f8f83d1d4 diff --git a/patches-tt/0001-fix-server-measure-Claude-context-against-autoCompac.patch b/patches-tt/0001-fix-server-measure-Claude-context-against-autoCompac.patch index 5f4f3de..31bebf0 100644 --- a/patches-tt/0001-fix-server-measure-Claude-context-against-autoCompac.patch +++ b/patches-tt/0001-fix-server-measure-Claude-context-against-autoCompac.patch @@ -13,10 +13,10 @@ Subject: [PATCH 01/10] fix(server): measure Claude context against 5 files changed, 157 insertions(+), 8 deletions(-) diff --git a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts -index 5f6b80fc6e..eea8894879 100644 +index 065f16c37c..7317d2f873 100644 --- a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts +++ b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts -@@ -745,6 +745,27 @@ describe("ClaudeAdapterV2 context usage", () => { +@@ -746,6 +746,27 @@ describe("ClaudeAdapterV2 context usage", () => { ); assert.strictEqual(usage.promptCacheExpiresAt, "2026-08-29T00:05:00.000Z"); }); @@ -45,10 +45,10 @@ index 5f6b80fc6e..eea8894879 100644 describe("ClaudeAdapterV2 approval presentation", () => { diff --git a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts -index 21955d95cc..a71c5edd50 100644 +index cdb826fd83..d8cb1c692d 100644 --- a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts +++ b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts -@@ -142,6 +142,7 @@ import { +@@ -150,6 +150,7 @@ import { import { claudeLaunchArgDirectories, discoverClaudeSkills, @@ -56,7 +56,7 @@ index 21955d95cc..a71c5edd50 100644 } from "../../provider/Drivers/ClaudeSkills.ts"; import { compileClaudeModelSelection, -@@ -220,6 +221,23 @@ function claudeContextWindow(modelSelection: ModelSelection): number | null { +@@ -228,6 +229,23 @@ function claudeContextWindow(modelSelection: ModelSelection): number | null { ); } @@ -80,7 +80,7 @@ index 21955d95cc..a71c5edd50 100644 interface ClaudeCacheCreation { readonly ephemeral_1h_input_tokens: number; readonly ephemeral_5m_input_tokens: number; -@@ -270,11 +288,13 @@ export function claudeProviderTurnTokenUsage( +@@ -278,11 +296,13 @@ export function claudeProviderTurnTokenUsage( updatedAt: string, autoCompactWindow = "", requestedAt?: DateTime.Utc, @@ -95,7 +95,7 @@ index 21955d95cc..a71c5edd50 100644 // The cache lifetime starts with the request, not when its response ends. const promptCacheExpiresAt = promptCacheTtlMs === undefined || requestedAt === undefined -@@ -282,13 +302,13 @@ export function claudeProviderTurnTokenUsage( +@@ -290,13 +310,13 @@ export function claudeProviderTurnTokenUsage( : DateTime.formatIso(DateTime.add(requestedAt, { milliseconds: promptCacheTtlMs })); return { usedTokens: inputTokens + outputTokens, @@ -111,7 +111,7 @@ index 21955d95cc..a71c5edd50 100644 ...(promptCacheExpiresAt === undefined ? {} : { promptCacheExpiresAt }), updatedAt, }; -@@ -3978,6 +3998,8 @@ function formatClaudeUsageLimitWait(waitMs: number): string { +@@ -4088,6 +4108,8 @@ function formatClaudeUsageLimitWait(waitMs: number): string { interface ActiveClaudeTurnContext { readonly queryPolicy: ClaudeRuntimeQueryPolicy; readonly responseThroughput: ClaudeResponseThroughput; @@ -120,7 +120,7 @@ index 21955d95cc..a71c5edd50 100644 readonly input: ProviderAdapter.ProviderAdapterV2TurnInput; readonly nativeThreadId: string; readonly nativeTurnId: string; -@@ -4691,6 +4713,16 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* ( +@@ -4808,6 +4830,16 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* ( model: input.modelSelection.model, now, }); @@ -137,7 +137,7 @@ index 21955d95cc..a71c5edd50 100644 const events = yield* Queue.unbounded(); const activeTurn = yield* Ref.make(null); const taskOutputTails = new Map< -@@ -5035,6 +5067,7 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* ( +@@ -5157,6 +5189,7 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* ( DateTime.formatIso(now), adapterOptions.settings.autoCompactWindow, context.streamUsage?.requestedAt, @@ -145,7 +145,7 @@ index 21955d95cc..a71c5edd50 100644 ); if (tokenUsage.inputTokens <= 0) return; yield* emitProviderEvent({ -@@ -8620,7 +8653,7 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* ( +@@ -8854,7 +8887,7 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* ( ? Math.round(postTokens) : undefined; if (afterTokenCount !== undefined) { @@ -154,7 +154,7 @@ index 21955d95cc..a71c5edd50 100644 yield* emitProviderEvent({ type: "provider_turn.updated", driver: CLAUDE_PROVIDER, -@@ -8641,10 +8674,10 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* ( +@@ -8875,10 +8908,10 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* ( completedAt: null, tokenUsage: { usedTokens: afterTokenCount, @@ -167,7 +167,7 @@ index 21955d95cc..a71c5edd50 100644 ), updatedAt: DateTime.formatIso(now), }, -@@ -10490,6 +10523,11 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* ( +@@ -10788,6 +10821,11 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* ( const querySession = yield* openQuery(turnInput, nativeThreadId); const context: ActiveClaudeTurnContext = { responseThroughput: new ClaudeResponseThroughput(), @@ -242,10 +242,10 @@ index 860280aa91..d6f8aedffe 100644 + ); +}); diff --git a/apps/server/src/provider/Drivers/ClaudeSkills.ts b/apps/server/src/provider/Drivers/ClaudeSkills.ts -index 6aff76d250..d3baccf2ae 100644 +index c73385374f..4954ae485d 100644 --- a/apps/server/src/provider/Drivers/ClaudeSkills.ts +++ b/apps/server/src/provider/Drivers/ClaudeSkills.ts -@@ -485,6 +485,51 @@ export function claudeLaunchArgDirectories(launchArgs?: string): ReadonlyArray { const thread = options.thread === undefined ? {} : options.thread; const scope = makeScope(options.capabilities ?? new Set(["preview", "worktree"])); @@ -76,7 +76,7 @@ index 3be8705cc0..0a715a2f89 100644 (options.dispatchGate ?? Effect.void).pipe( Effect.andThen( options.dispatchInterrupts -@@ -127,7 +132,45 @@ const makeHarness = (options: HarnessOptions = {}) => { +@@ -128,7 +133,45 @@ const makeHarness = (options: HarnessOptions = {}) => { ? Effect.die(new Error("dispatch defect")) : options.dispatchFails ? (Effect.fail("simulated dispatch failure") as never) @@ -123,7 +123,7 @@ index 3be8705cc0..0a715a2f89 100644 ), ), ); -@@ -175,22 +218,9 @@ const makeHarness = (options: HarnessOptions = {}) => { +@@ -176,22 +219,9 @@ const makeHarness = (options: HarnessOptions = {}) => { ? Effect.succeed(makeProjection(thread)) : Effect.fail(new OrchestratorProjectionError({ threadId: id })); }); @@ -149,7 +149,7 @@ index 3be8705cc0..0a715a2f89 100644 const getById = vi.fn((id: ProjectId) => options.projectReadFails ? (Effect.fail("simulated project read failure") as never) -@@ -370,19 +400,68 @@ const runStatus = (harness: ReturnType) => +@@ -372,19 +402,68 @@ const runStatus = (harness: ReturnType) => const decodeHandoffInput = Schema.decodeUnknownEffect(WorktreeMcpHandoffInput); describe("t3_worktree_handoff", () => { @@ -224,7 +224,7 @@ index 3be8705cc0..0a715a2f89 100644 it.effect("does not queue a continuation when the thread update fails", () => { const harness = makeHarness({ dispatchFails: true }); return Effect.gen(function* () { -@@ -586,6 +665,9 @@ describe("t3_worktree_handoff", () => { +@@ -588,6 +667,9 @@ describe("t3_worktree_handoff", () => { const gate = yield* Deferred.make(); const harness = makeHarness({ dispatchGate: Deferred.await(gate) }); @@ -234,7 +234,7 @@ index 3be8705cc0..0a715a2f89 100644 const fiber = yield* Effect.forkChild( runHandoff(harness, { branch: "feature/interrupted", -@@ -600,7 +682,14 @@ describe("t3_worktree_handoff", () => { +@@ -602,7 +684,14 @@ describe("t3_worktree_handoff", () => { yield* Fiber.join(interruption); expect(harness.dispatch).toHaveBeenCalledTimes(1); @@ -251,7 +251,7 @@ index 3be8705cc0..0a715a2f89 100644 }), ); diff --git a/apps/server/src/mcp/WorktreeMcpService.ts b/apps/server/src/mcp/WorktreeMcpService.ts -index 267df07f24..9624a0eb5c 100644 +index 8a2231f6e4..a0ef941dbd 100644 --- a/apps/server/src/mcp/WorktreeMcpService.ts +++ b/apps/server/src/mcp/WorktreeMcpService.ts @@ -130,7 +130,6 @@ const make = Effect.gen(function* () { @@ -341,7 +341,7 @@ index 92e1e01ad4..3fd9c675aa 100644 }), Schema.Struct({ diff --git a/apps/server/src/orchestration-v2/EffectWorker.ts b/apps/server/src/orchestration-v2/EffectWorker.ts -index 534043aeb4..6c71a7b0be 100644 +index c5b5ea9b38..443e58615f 100644 --- a/apps/server/src/orchestration-v2/EffectWorker.ts +++ b/apps/server/src/orchestration-v2/EffectWorker.ts @@ -124,6 +124,7 @@ export const layerExecutor: Layer.Layer< @@ -353,10 +353,10 @@ index 534043aeb4..6c71a7b0be 100644 ? {} : { revokeMcpCredential: effect.request.revokeMcpCredential }), diff --git a/apps/server/src/orchestration-v2/Orchestrator.ts b/apps/server/src/orchestration-v2/Orchestrator.ts -index 35e9a78c4c..0f016d17ea 100644 +index 59b708f5ba..06a2cba3a6 100644 --- a/apps/server/src/orchestration-v2/Orchestrator.ts +++ b/apps/server/src/orchestration-v2/Orchestrator.ts -@@ -3087,6 +3087,26 @@ const makeOrchestrator = Effect.fn("orchestrationV2.Orchestrator.layer")(functio +@@ -3158,6 +3158,26 @@ const makeOrchestrator = Effect.fn("orchestrationV2.Orchestrator.layer")(functio cause: `Thread ${command.threadId} branch changed before the metadata update could be applied.`, }); } @@ -383,7 +383,7 @@ index 35e9a78c4c..0f016d17ea 100644 if (command.type === "thread.metadata.update" && command.expectedEmpty === true) { const records = yield* projectionStore .getThreadRecords(command.threadId, ["runs"]) -@@ -4095,6 +4115,9 @@ const makeOrchestrator = Effect.fn("orchestrationV2.Orchestrator.layer")(functio +@@ -4179,6 +4199,9 @@ const makeOrchestrator = Effect.fn("orchestrationV2.Orchestrator.layer")(functio ? "Runtime mode changed." : "Provider or model selection changed.", // Terminal detaches revoke MCP credentials; reattachments need other detach reasons to preserve them. @@ -393,7 +393,7 @@ index 35e9a78c4c..0f016d17ea 100644 ...(command.type === "thread.archive" ? { revokeMcpCredential: true } : {}), }, } satisfies PendingOrchestrationEffectV2; -@@ -5267,6 +5290,7 @@ const makeOrchestrator = Effect.fn("orchestrationV2.Orchestrator.layer")(functio +@@ -5391,6 +5414,7 @@ const makeOrchestrator = Effect.fn("orchestrationV2.Orchestrator.layer")(functio command: Extract, events: Ref.Ref>, effects: Ref.Ref>, @@ -401,7 +401,7 @@ index 35e9a78c4c..0f016d17ea 100644 ) { let projection = yield* getProjectionWithPendingEvents(command.threadId, events); if (command.manualContinuationOfRunId !== undefined) { -@@ -5337,10 +5361,11 @@ const makeOrchestrator = Effect.fn("orchestrationV2.Orchestrator.layer")(functio +@@ -5461,10 +5485,11 @@ const makeOrchestrator = Effect.fn("orchestrationV2.Orchestrator.layer")(functio const source = projection.runs.find((run) => run.id === command.restartContinuationOfRunId); if ( !source || @@ -417,7 +417,7 @@ index 35e9a78c4c..0f016d17ea 100644 projection.thread.archivedAt !== null || projection.thread.deletedAt !== null || projection.thread.providerInstanceId !== source.providerInstanceId || -@@ -5353,6 +5378,13 @@ const makeOrchestrator = Effect.fn("orchestrationV2.Orchestrator.layer")(functio +@@ -5477,6 +5502,13 @@ const makeOrchestrator = Effect.fn("orchestrationV2.Orchestrator.layer")(functio ) || (yield* stopReachedRun(command, command.threadId, source.id)) ) { @@ -431,15 +431,15 @@ index 35e9a78c4c..0f016d17ea 100644 // Preserve the current row so stale automatic deliveries receive an // accepted receipt without changing work or repeatedly retrying. yield* emit( -@@ -12975,7 +13007,6 @@ const makeOrchestrator = Effect.fn("orchestrationV2.Orchestrator.layer")(functio - case "thread.pin.reorder": +@@ -13168,7 +13200,6 @@ const makeOrchestrator = Effect.fn("orchestrationV2.Orchestrator.layer")(functio case "thread.active.reorder": + case "thread.tag.set": case "thread.mark-unread": - case "thread.metadata.update": case "thread.pull-request.link": case "thread.pull-request.unlink": case "thread.pull-request-link.sync": -@@ -12988,6 +13019,34 @@ const makeOrchestrator = Effect.fn("orchestrationV2.Orchestrator.layer")(functio +@@ -13181,6 +13212,34 @@ const makeOrchestrator = Effect.fn("orchestrationV2.Orchestrator.layer")(functio case "provider.switch": yield* dispatchThreadMutation(command, events, effects); break; @@ -475,10 +475,10 @@ index 35e9a78c4c..0f016d17ea 100644 yield* dispatchPullRequestWatchSync(command, events, effects); break; diff --git a/apps/server/src/orchestration-v2/ProviderSessionManager.test.ts b/apps/server/src/orchestration-v2/ProviderSessionManager.test.ts -index 5bbb3926e2..1bac1bdbbe 100644 +index d8c046c018..12332c2aba 100644 --- a/apps/server/src/orchestration-v2/ProviderSessionManager.test.ts +++ b/apps/server/src/orchestration-v2/ProviderSessionManager.test.ts -@@ -1583,8 +1583,8 @@ it.effect("ProviderSessionManagerV2 interrupts event subscriptions on server shu +@@ -1680,8 +1680,8 @@ it.effect("ProviderSessionManagerV2 interrupts event subscriptions on server shu modelSelection, runtimePolicy, }); @@ -489,7 +489,7 @@ index 5bbb3926e2..1bac1bdbbe 100644 const adapterQueue = (yield* Ref.get(state)).eventQueues.get(String(providerSessionId)); assert.isDefined(adapterQueue); yield* Queue.offer(adapterQueue!, { -@@ -1626,7 +1626,7 @@ it.effect("ProviderSessionManagerV2 drains subscribers when the provider stops", +@@ -1723,7 +1723,7 @@ it.effect("ProviderSessionManagerV2 drains subscribers when the provider stops", modelSelection, runtimePolicy, }); @@ -498,7 +498,7 @@ index 5bbb3926e2..1bac1bdbbe 100644 const collected = yield* subscription.events.pipe(Stream.runCollect, Effect.forkScoped); const adapterQueue = (yield* Ref.get(state)).eventQueues.get(String(providerSessionId)); assert.isDefined(adapterQueue); -@@ -3363,8 +3363,8 @@ it.effect( +@@ -3460,8 +3460,8 @@ it.effect( const subscribe = firstRuntime.subscribeEvents; assert.isDefined(subscribe); if (subscribe === undefined) return; @@ -509,7 +509,7 @@ index 5bbb3926e2..1bac1bdbbe 100644 const queue = (yield* Ref.get(state)).eventQueues.get(String(providerSessionId)); assert.isDefined(queue); yield* Queue.offer(queue!, { -@@ -3474,7 +3474,7 @@ it.effect( +@@ -3571,7 +3571,7 @@ it.effect( const publish = Effect.fnUntraced(function* ( event: ProviderAdapter.ProviderAdapterV2Event, ) { @@ -519,7 +519,7 @@ index 5bbb3926e2..1bac1bdbbe 100644 // Observing the exact event ensures the manager has handled it before resuming. assert.deepEqual(yield* subscription.events.pipe(Stream.runHead), Option.some(event)); diff --git a/apps/server/src/orchestration-v2/ProviderSessionManager.ts b/apps/server/src/orchestration-v2/ProviderSessionManager.ts -index 03a7278774..c65d2654d4 100644 +index f06011676c..8f37107019 100644 --- a/apps/server/src/orchestration-v2/ProviderSessionManager.ts +++ b/apps/server/src/orchestration-v2/ProviderSessionManager.ts @@ -187,6 +187,7 @@ export interface ProviderSessionManagerV2Shape { @@ -724,10 +724,10 @@ index 03a7278774..c65d2654d4 100644 detached.value.attachedThreadIds.size === 0 && !detached.value.supportsMultipleProviderThreads diff --git a/apps/server/src/orchestration-v2/RunExecutionService.test.ts b/apps/server/src/orchestration-v2/RunExecutionService.test.ts -index dee246855b..12430742f4 100644 +index f3f1542146..24e85b857f 100644 --- a/apps/server/src/orchestration-v2/RunExecutionService.test.ts +++ b/apps/server/src/orchestration-v2/RunExecutionService.test.ts -@@ -2190,98 +2190,99 @@ it.effect( +@@ -2214,98 +2214,99 @@ it.effect( providerSessionId: ProviderSessionId.make("session:subagent-interrupt-cascade"), session: { events: Stream.empty, @@ -915,7 +915,7 @@ index dee246855b..12430742f4 100644 startTurn: () => Effect.void, } as unknown as ProviderAdapter.ProviderAdapterV2SessionRuntime, run: { -@@ -2618,6 +2619,126 @@ it.effect("does not refresh pull requests for auxiliary or stale provider termin +@@ -2663,6 +2664,126 @@ it.effect("does not refresh pull requests for auxiliary or stale provider termin }), ); @@ -1042,7 +1042,7 @@ index dee246855b..12430742f4 100644 it.effect("refreshes pull requests after a provider stream exits with an error", () => Effect.gen(function* () { const { written, observed } = yield* captureRootRunTermination({ -@@ -3092,6 +3213,7 @@ function captureRootRunTermination(input: { +@@ -3167,6 +3288,7 @@ function captureRootRunTermination(input: { }); const writtenItems = yield* Ref.make>([]); const finalEvents = yield* Ref.make>([]); @@ -1050,7 +1050,7 @@ index dee246855b..12430742f4 100644 const observed = yield* Ref.make>([]); const submittedEffects = yield* Ref.make>([]); const committedEffects = yield* Ref.make>([]); -@@ -3153,7 +3275,10 @@ function captureRootRunTermination(input: { +@@ -3228,7 +3350,10 @@ function captureRootRunTermination(input: { }), IdAllocator.layer, Layer.mock(ProviderEventIngestor.ProviderEventIngestorV2)({ @@ -1062,7 +1062,7 @@ index dee246855b..12430742f4 100644 }), ServerSettings.layerTest(), Layer.succeed(RunFinalizationService.RunFinalizationObserver, { -@@ -3176,37 +3301,38 @@ function captureRootRunTermination(input: { +@@ -3251,37 +3376,38 @@ function captureRootRunTermination(input: { providerSessionId: ProviderSessionId.make(`session:${input.key}`), session: { events: Stream.empty, @@ -1131,7 +1131,7 @@ index dee246855b..12430742f4 100644 startTurn: input.startTurn ?? (() => Effect.void), interruptTurn: input.interruptTurn ?? (() => Effect.void), hasPendingBackgroundWorkForThread: input.hasPendingBackgroundWorkForThread, -@@ -3279,6 +3405,7 @@ function captureRootRunTermination(input: { +@@ -3357,6 +3483,7 @@ function captureRootRunTermination(input: { previewActivityEnded, written: yield* Ref.get(writtenItems), finalEvents: yield* Ref.get(finalEvents), @@ -1139,7 +1139,7 @@ index dee246855b..12430742f4 100644 observed: yield* Ref.get(observed), submittedEffects: yield* Ref.get(submittedEffects), committedEffects: yield* Ref.get(committedEffects), -@@ -3354,7 +3481,7 @@ function childBackgroundTurnItemEvent( +@@ -3432,7 +3559,7 @@ function childBackgroundTurnItemEvent( function backgroundTurnItemEvent( ids: BackgroundScenarioIds, type: "command_execution" | "dynamic_tool" | "subagent", @@ -1148,7 +1148,7 @@ index dee246855b..12430742f4 100644 ordinal: number, itemId?: TurnItemId, ): ProviderAdapter.ProviderAdapterV2Event { -@@ -3689,26 +3816,27 @@ function runBackgroundItemScenario( +@@ -3767,26 +3894,27 @@ function runBackgroundItemScenario( ...(options?.hasPendingBackgroundWorkForThread === undefined ? {} : { hasPendingBackgroundWorkForThread: options.hasPendingBackgroundWorkForThread }), @@ -1197,7 +1197,7 @@ index dee246855b..12430742f4 100644 } as unknown as ProviderAdapter.ProviderAdapterV2SessionRuntime, run: { diff --git a/apps/server/src/orchestration-v2/RunExecutionService.ts b/apps/server/src/orchestration-v2/RunExecutionService.ts -index b9b2b09bd2..8b0817865a 100644 +index 6a52fac447..c726b55d2e 100644 --- a/apps/server/src/orchestration-v2/RunExecutionService.ts +++ b/apps/server/src/orchestration-v2/RunExecutionService.ts @@ -5,6 +5,8 @@ import { @@ -1209,7 +1209,7 @@ index b9b2b09bd2..8b0817865a 100644 type NodeId, type OrchestrationV2AppThread, type OrchestrationV2CheckpointScope, -@@ -630,6 +632,7 @@ export const layer: Layer.Layer< +@@ -632,6 +634,7 @@ export const layer: Layer.Layer< // Root turn snapshot written with the run when T3 ends the run itself. readonly abandonedProviderTurn?: OrchestrationV2ProviderTurn; readonly terminal: ProviderTerminalEvent; @@ -1217,7 +1217,7 @@ index b9b2b09bd2..8b0817865a 100644 readonly completedAt?: DateTime.Utc; readonly failureItemPersisted: boolean; readonly refreshAfterTurn: Effect.Effect; -@@ -796,6 +799,9 @@ export const layer: Layer.Layer< +@@ -800,6 +803,9 @@ export const layer: Layer.Layer< providerThread: input.providerThread, reason, completedAt, @@ -1227,17 +1227,17 @@ index b9b2b09bd2..8b0817865a 100644 }), }, ] -@@ -995,6 +1001,9 @@ export const layer: Layer.Layer< - const latestTurnItemOrdinal = yield* Ref.make(input.providerTurnOrdinal * 100); - const latestProviderThread = yield* Ref.make(input.providerThread); - const latestRootProviderTurn = yield* Ref.make(null); +@@ -1024,6 +1030,9 @@ export const layer: Layer.Layer< + context: { ...analyticsContext, segment: "initial" }, + }); + } + // A planned detach closes ingestion before the adapter can end its open root items. + const openRootTurnItems = new Map(); + const streamingRootMessages = new Map(); const routeIdentity: ProviderEventRouteIdentity = { threadId: input.run.threadId, runId: input.run.id, -@@ -1004,7 +1013,7 @@ export const layer: Layer.Layer< +@@ -1033,7 +1042,7 @@ export const layer: Layer.Layer< const eventSubscription = input.session.subscribeEvents === undefined ? { events: input.session.events, close: Effect.void } @@ -1246,7 +1246,7 @@ index b9b2b09bd2..8b0817865a 100644 const inheritedBackgroundTurnItems = yield* ( input.loadInheritedBackgroundTurnItems?.() ?? Effect.succeed([]) ).pipe( -@@ -1036,6 +1045,7 @@ export const layer: Layer.Layer< +@@ -1065,6 +1074,7 @@ export const layer: Layer.Layer< ); const rootTerminalSeen = yield* Ref.make(false); const rootRunFinalized = yield* Ref.make(false); @@ -1254,7 +1254,7 @@ index b9b2b09bd2..8b0817865a 100644 const providerThreadOwnerLost = yield* Ref.make(false); const pendingBackgroundProviderTurns = new Set(); const activeChildProviderTurns = yield* Ref.make>(new Set()); -@@ -1052,6 +1062,7 @@ export const layer: Layer.Layer< +@@ -1081,6 +1091,7 @@ export const layer: Layer.Layer< } const providerThread = yield* Ref.get(latestProviderThread); const openSubagents = yield* Ref.get(openRunOwnedSubagents); @@ -1262,7 +1262,7 @@ index b9b2b09bd2..8b0817865a 100644 yield* writeFinalRunEvents({ run: input.run, rootNode: input.rootNode, -@@ -1079,6 +1090,7 @@ export const layer: Layer.Layer< +@@ -1108,6 +1119,7 @@ export const layer: Layer.Layer< : { loadRunInterruptReason: input.loadRunInterruptReason }), openRunOwnedSubagents: openSubagents, terminal, @@ -1270,7 +1270,7 @@ index b9b2b09bd2..8b0817865a 100644 failureItemPersisted: terminal.status === "failed", refreshAfterTurn, }).pipe( -@@ -1294,6 +1306,86 @@ export const layer: Layer.Layer< +@@ -1323,6 +1335,86 @@ export const layer: Layer.Layer< // as a dev server keep this consumer alive afterwards and must not pin preview focus. const endPreviewActivity = yield* previewAgentActivity.begin(input.run.threadId); const providerEventFiber = yield* eventSubscription.events.pipe( @@ -1357,7 +1357,7 @@ index b9b2b09bd2..8b0817865a 100644 Stream.mapEffect((event) => Effect.gen(function* () { const previous = yield* Ref.get(eventRouting); -@@ -1384,6 +1476,16 @@ export const layer: Layer.Layer< +@@ -1413,6 +1505,16 @@ export const layer: Layer.Layer< ) { yield* Ref.set(latestRootProviderTurn, event.providerTurn); } @@ -1374,7 +1374,7 @@ index b9b2b09bd2..8b0817865a 100644 let storedEventCount = 0; const deliveredEvent = filterAssistantEvent( event, -@@ -1464,6 +1566,15 @@ export const layer: Layer.Layer< +@@ -1503,6 +1605,15 @@ export const layer: Layer.Layer< yield* Ref.update(latestTurnItemOrdinal, (current) => Math.max(current, event.turnItem.ordinal), ); @@ -1390,15 +1390,15 @@ index b9b2b09bd2..8b0817865a 100644 } if (event.type === "turn.terminal") { yield* Ref.set(terminalEvent, event); -@@ -1778,6 +1889,7 @@ export function makeInterruptResultTurnItem(input: { - /** The Stop's own reason, from its run_interrupt_request item. */ +@@ -1816,6 +1927,7 @@ export function makeInterruptResultTurnItem(input: { + readonly providerThread: OrchestrationV2ProviderThread; readonly reason: string | undefined; readonly completedAt: DateTime.Utc; + readonly message?: string; }): OrchestrationV2TurnItem { return { id: input.idAllocator.derive.runSignalTurnItem({ -@@ -1801,6 +1913,6 @@ export function makeInterruptResultTurnItem(input: { +@@ -1839,6 +1951,6 @@ export function makeInterruptResultTurnItem(input: { completedAt: input.completedAt, updatedAt: input.completedAt, type: "run_interrupt_result", @@ -1407,10 +1407,10 @@ index b9b2b09bd2..8b0817865a 100644 }; } diff --git a/apps/server/src/orchestration-v2/SelectionRestart.integration.test.ts b/apps/server/src/orchestration-v2/SelectionRestart.integration.test.ts -index d059c4c20e..a7f4e3e90f 100644 +index 4352d8b2d6..b0300960df 100644 --- a/apps/server/src/orchestration-v2/SelectionRestart.integration.test.ts +++ b/apps/server/src/orchestration-v2/SelectionRestart.integration.test.ts -@@ -17,6 +17,7 @@ import { +@@ -18,6 +18,7 @@ import { ThreadId, } from "@t3tools/contracts"; import * as DateTime from "effect/DateTime"; @@ -1418,7 +1418,7 @@ index d059c4c20e..a7f4e3e90f 100644 import * as Effect from "effect/Effect"; import * as Fiber from "effect/Fiber"; import * as Layer from "effect/Layer"; -@@ -24,10 +25,12 @@ import * as Queue from "effect/Queue"; +@@ -25,10 +26,12 @@ import * as Queue from "effect/Queue"; import * as Ref from "effect/Ref"; import * as Stream from "effect/Stream"; @@ -1431,7 +1431,7 @@ index d059c4c20e..a7f4e3e90f 100644 import * as ProviderAdapterRegistry from "./ProviderAdapterRegistry.ts"; import * as ProviderReplayHarness from "./testkit/ProviderReplayHarness.ts"; import { checkpointWorkspace } from "@t3tools/provider-testing/replayWorkspace"; -@@ -81,6 +84,11 @@ function makeRestartAdapter( +@@ -86,6 +89,11 @@ function makeRestartAdapter( state: Ref.Ref, sessionCapabilities: OrchestrationV2ProviderCapabilities = pooledCapabilities, providerInstanceId = initialSelection.instanceId, @@ -1443,7 +1443,7 @@ index d059c4c20e..a7f4e3e90f 100644 ): ProviderAdapter.ProviderAdapterV2["Service"] { return { instanceId: providerInstanceId, -@@ -221,11 +229,21 @@ function makeRestartAdapter( +@@ -226,11 +234,21 @@ function makeRestartAdapter( }, ], })); @@ -1466,7 +1466,7 @@ index d059c4c20e..a7f4e3e90f 100644 const occurredAt = yield* DateTime.now; yield* Ref.update(state, (current) => ({ ...current, activeTurn: active })); yield* Queue.offer(events, { -@@ -249,6 +267,48 @@ function makeRestartAdapter( +@@ -254,6 +272,48 @@ function makeRestartAdapter( }); return; } @@ -1515,7 +1515,7 @@ index d059c4c20e..a7f4e3e90f 100644 yield* publishTerminal(active, "completed"); }), steerTurn: () => Effect.void, -@@ -886,3 +946,397 @@ it.live.each(["active", "idle", "selection-command", "pooled", "separate-home"] +@@ -897,3 +957,397 @@ it.live.each(["active", "idle", "selection-command", "pooled", "separate-home"] }), ), ); @@ -1914,10 +1914,10 @@ index d059c4c20e..a7f4e3e90f 100644 + ), +); diff --git a/apps/server/src/orchestration-v2/testkit/ProviderReplayHarness.ts b/apps/server/src/orchestration-v2/testkit/ProviderReplayHarness.ts -index afd11a9ab7..0530eacf0a 100644 +index 746ff0f844..48507aee25 100644 --- a/apps/server/src/orchestration-v2/testkit/ProviderReplayHarness.ts +++ b/apps/server/src/orchestration-v2/testkit/ProviderReplayHarness.ts -@@ -268,6 +268,7 @@ export function layerWithRegistry( +@@ -276,6 +276,7 @@ export function layerWithRegistry( SqlClient.SqlClient, MigrationError | PlatformError.PlatformError | SqlError >; @@ -1925,7 +1925,7 @@ index afd11a9ab7..0530eacf0a 100644 readonly runEffectWorker?: boolean; readonly runContinuationWorker?: boolean; readonly recoverOnStartup?: boolean; -@@ -383,7 +384,7 @@ export function layerWithRegistry( +@@ -402,7 +403,7 @@ export function layerWithRegistry( const layerRunExecutionServiceProvided = RunExecutionService.layer.pipe( Layer.provide( Layer.mergeAll( @@ -1950,10 +1950,10 @@ index 9e5cf6c9d8..2a5c5f7cc4 100644 startup as well as live RPC traffic. Compatibility work must account for stored history, not just what the newest client sends. diff --git a/docs/user/composer.md b/docs/user/composer.md -index 49f49a87e3..b4839da8e8 100644 +index a3e06c9aed..183b580b93 100644 --- a/docs/user/composer.md +++ b/docs/user/composer.md -@@ -65,6 +65,10 @@ If your **Send shortcut** requires `Cmd+Enter` or `Ctrl+Enter`, that shortcut us +@@ -66,6 +66,10 @@ If your **Send shortcut** requires `Cmd+Enter` or `Ctrl+Enter`, that shortcut us your default follow-up behavior; add **Shift** to use the opposite action. This applies only to multiline messages when you choose the multiline send option. @@ -1965,10 +1965,10 @@ index 49f49a87e3..b4839da8e8 100644 the oldest queued message as a steer. This leaves the current draft intact and requires an active turn that supports steering. If that shortcut sends the opposite diff --git a/packages/contracts/src/orchestrationV2.ts b/packages/contracts/src/orchestrationV2.ts -index e651d00b25..36c096a333 100644 +index 769a31daa9..8a054d175a 100644 --- a/packages/contracts/src/orchestrationV2.ts +++ b/packages/contracts/src/orchestrationV2.ts -@@ -3037,6 +3037,10 @@ export const OrchestrationV2Command = Schema.Union([ +@@ -3062,6 +3062,10 @@ export const OrchestrationV2Command = Schema.Union([ worktreePath: Schema.optional(Schema.NullOr(TrimmedNonEmptyString)), expectedWorktreePath: Schema.optional(Schema.NullOr(TrimmedNonEmptyString)), expectedBranch: Schema.optional(Schema.NullOr(TrimmedNonEmptyString)), @@ -1980,10 +1980,10 @@ index e651d00b25..36c096a333 100644 expectedEmpty: Schema.optional(Schema.Boolean), limitRecovery: Schema.optional(Schema.NullOr(OrchestrationV2LimitRecoveryUpdate)), diff --git a/packages/provider-core/src/server/ProviderAdapter.ts b/packages/provider-core/src/server/ProviderAdapter.ts -index cadf4173a6..6103bd1728 100644 +index 9d83da489b..41832004e1 100644 --- a/packages/provider-core/src/server/ProviderAdapter.ts +++ b/packages/provider-core/src/server/ProviderAdapter.ts -@@ -382,6 +382,15 @@ const ProviderAdapterFailureReason = Schema.Literals([ +@@ -390,6 +390,15 @@ const ProviderAdapterFailureReason = Schema.Literals([ "attachment_unreadable", ]); export type ProviderAdapterFailureReason = typeof ProviderAdapterFailureReason.Type; @@ -1999,7 +1999,7 @@ index cadf4173a6..6103bd1728 100644 export class ProviderAdapterProtocolError extends Schema.TaggedError()( "ProviderAdapterProtocolError", -@@ -414,6 +423,7 @@ export const ProviderAdapterV2Error = Schema.Union([ +@@ -422,6 +431,7 @@ export const ProviderAdapterV2Error = Schema.Union([ ProviderAdapterInterruptTimeoutError, ProviderAdapterRuntimeRequestResponseError, ProviderAdapterEventStreamError, @@ -2007,7 +2007,7 @@ index cadf4173a6..6103bd1728 100644 ProviderAdapterProtocolError, ]); export type ProviderAdapterV2Error = typeof ProviderAdapterV2Error.Type; -@@ -570,8 +580,10 @@ export interface ProviderAdapterV2SessionRuntime { +@@ -579,8 +589,10 @@ export interface ProviderAdapterV2SessionRuntime { readonly providerSessionId: ProviderSessionId; readonly providerSession: OrchestrationV2ProviderSession; readonly events: Stream.Stream; diff --git a/patches-tt/0003-feat-nest-agent-opened-child-threads-under-their-ope.patch b/patches-tt/0003-feat-nest-agent-opened-child-threads-under-their-ope.patch index 03d1bf7..10ccf33 100644 --- a/patches-tt/0003-feat-nest-agent-opened-child-threads-under-their-ope.patch +++ b/patches-tt/0003-feat-nest-agent-opened-child-threads-under-their-ope.patch @@ -10,7 +10,7 @@ a child nests under its opener only within the same inbox section, with a guide line, context-menu collapse and a summary stub. --- apps/server/src/mcp/OrchestratorMcpService.ts | 1 + - .../src/mcp/toolkits/project/handlers.test.ts | 78 +++ + .../src/mcp/toolkits/project/handlers.test.ts | 79 ++- .../src/mcp/toolkits/project/handlers.ts | 1 + .../src/orchestration-v2/Orchestrator.ts | 10 +- .../src/orchestration-v2/ProjectionStore.ts | 15 + @@ -38,7 +38,7 @@ guide line, context-menu collapse and a summary stub. .../src/state/threadCommands.ts | 14 +- .../src/state/threadShell.test.ts | 37 ++ packages/contracts/src/orchestrationV2.ts | 25 + - 29 files changed, 2471 insertions(+), 49 deletions(-) + 29 files changed, 2471 insertions(+), 50 deletions(-) create mode 100644 apps/web/src/components/Sidebar.childThreads.browser.test.tsx create mode 100644 apps/web/src/components/Sidebar.childThreads.test.ts create mode 100644 apps/web/src/components/Sidebar.childThreads.tsx @@ -46,10 +46,10 @@ guide line, context-menu collapse and a summary stub. create mode 100644 apps/web/src/components/Sidebar.families.ts diff --git a/apps/server/src/mcp/OrchestratorMcpService.ts b/apps/server/src/mcp/OrchestratorMcpService.ts -index b124a03ea2..c64ff7af9d 100644 +index b937453f39..2a2b7c37f5 100644 --- a/apps/server/src/mcp/OrchestratorMcpService.ts +++ b/apps/server/src/mcp/OrchestratorMcpService.ts -@@ -2803,6 +2803,7 @@ const make = Effect.gen(function* () { +@@ -2827,6 +2827,7 @@ const make = Effect.gen(function* () { ...(parent.thread.linkOrigin === undefined ? {} : { linkOrigin: parent.thread.linkOrigin }), @@ -58,10 +58,10 @@ index b124a03ea2..c64ff7af9d 100644 .pipe( Effect.mapError((error) => diff --git a/apps/server/src/mcp/toolkits/project/handlers.test.ts b/apps/server/src/mcp/toolkits/project/handlers.test.ts -index 748234387c..b907eeda24 100644 +index 8d7369bc85..0e36856678 100644 --- a/apps/server/src/mcp/toolkits/project/handlers.test.ts +++ b/apps/server/src/mcp/toolkits/project/handlers.test.ts -@@ -6,6 +6,7 @@ import { +@@ -6,11 +6,11 @@ import { ProviderInstanceId, ProviderDriverKind, type ServerProvider, @@ -69,7 +69,12 @@ index 748234387c..b907eeda24 100644 type Project as ProjectRecord, type ModelSelection, type RuntimeMode, -@@ -38,6 +39,82 @@ import * as McpToolAccess from "../../McpToolAccess.ts"; + type OrchestrationV2ThreadShell, +- ThreadId, + } from "@t3tools/contracts"; + import * as NodeCrypto from "@effect/platform-node/NodeCrypto"; + import * as NodeServices from "@effect/platform-node/NodeServices"; +@@ -40,6 +40,82 @@ import * as McpToolAccess from "../../McpToolAccess.ts"; import * as ProjectHandlers from "./handlers.ts"; import { ProjectToolkit } from "./tools.ts"; @@ -152,7 +157,7 @@ index 748234387c..b907eeda24 100644 const clientLaunchHarness = (input: { readonly runtimeModeCeiling: RuntimeMode; readonly defaultRuntimeMode?: RuntimeMode; -@@ -203,6 +280,7 @@ it.effect( +@@ -225,6 +301,7 @@ it.effect( }); expect(belowCeiling.launched[0]?.runtimeMode).toBe("approval-required"); expect(belowCeiling.launched[0]?.initialMessage?.senderThreadId).toBeUndefined(); @@ -161,10 +166,10 @@ index 748234387c..b907eeda24 100644 const aboveCeiling = yield* launchWith("auto-accept-edits", "full-access", { title: "Fix", diff --git a/apps/server/src/mcp/toolkits/project/handlers.ts b/apps/server/src/mcp/toolkits/project/handlers.ts -index 7253bcedb0..d56e0f262c 100644 +index 253964cc55..dff7f00b70 100644 --- a/apps/server/src/mcp/toolkits/project/handlers.ts +++ b/apps/server/src/mcp/toolkits/project/handlers.ts -@@ -238,6 +238,7 @@ export const layer = McpToolAccess.toLayer(ProjectToolkit, { +@@ -242,6 +242,7 @@ export const layer = McpToolAccess.toLayer(ProjectToolkit, { attachments, }, }), @@ -173,10 +178,10 @@ index 7253bcedb0..d56e0f262c 100644 creationSource: "mcp", ...(linkOrigin === undefined ? {} : { linkOrigin }), diff --git a/apps/server/src/orchestration-v2/Orchestrator.ts b/apps/server/src/orchestration-v2/Orchestrator.ts -index 0f016d17ea..71d89f22a3 100644 +index 06a2cba3a6..3bd5e44104 100644 --- a/apps/server/src/orchestration-v2/Orchestrator.ts +++ b/apps/server/src/orchestration-v2/Orchestrator.ts -@@ -2704,6 +2704,9 @@ const makeOrchestrator = Effect.fn("orchestrationV2.Orchestrator.layer")(functio +@@ -2774,6 +2774,9 @@ const makeOrchestrator = Effect.fn("orchestrationV2.Orchestrator.layer")(functio relationshipToParent: null, rootThreadId: command.threadId, }, @@ -186,7 +191,7 @@ index 0f016d17ea..71d89f22a3 100644 forkedFrom: null, createdAt: now, updatedAt: now, -@@ -3435,8 +3438,12 @@ const makeOrchestrator = Effect.fn("orchestrationV2.Orchestrator.layer")(functio +@@ -3506,8 +3509,12 @@ const makeOrchestrator = Effect.fn("orchestrationV2.Orchestrator.layer")(functio const wasPinned = thread.pinnedAt != null; const alreadySettled = thread.settledOverride === "settled" && thread.settledAt !== null && !wasPinned; @@ -200,7 +205,7 @@ index 0f016d17ea..71d89f22a3 100644 settledOverride: "settled", settledAt: alreadySettled ? thread.settledAt : (command.settledAt ?? now), pullRequests: thread.pullRequests?.map((link) => withPullRequestWatch(link, undefined)), -@@ -12928,6 +12935,7 @@ const makeOrchestrator = Effect.fn("orchestrationV2.Orchestrator.layer")(functio +@@ -13120,6 +13127,7 @@ const makeOrchestrator = Effect.fn("orchestrationV2.Orchestrator.layer")(functio type: "thread.settle", commandId: command.commandId, threadId: command.threadId, @@ -209,10 +214,10 @@ index 0f016d17ea..71d89f22a3 100644 }, events, diff --git a/apps/server/src/orchestration-v2/ProjectionStore.ts b/apps/server/src/orchestration-v2/ProjectionStore.ts -index c2df567ea0..240239cd17 100644 +index 14c7dc4802..1f92db8b04 100644 --- a/apps/server/src/orchestration-v2/ProjectionStore.ts +++ b/apps/server/src/orchestration-v2/ProjectionStore.ts -@@ -1512,6 +1512,12 @@ export function threadShellFromProjection( +@@ -1549,6 +1549,12 @@ export function threadShellFromProjection( ? {} : { activeOrderKey: projection.thread.activeOrderKey }), lineage: projection.thread.lineage, @@ -225,7 +230,7 @@ index c2df567ea0..240239cd17 100644 forkedFrom: projection.thread.forkedFrom, activeProviderThreadId: projection.thread.activeProviderThreadId, ...(projection.thread.historyOrigin === undefined -@@ -1572,6 +1578,7 @@ export function threadShellFromProjection( +@@ -1608,6 +1614,7 @@ export function threadShellFromProjection( unsettledAt: projection.thread.unsettledAt ?? null, snoozedUntil: projection.thread.snoozedUntil ?? null, snoozedAt: projection.thread.snoozedAt ?? null, @@ -233,7 +238,7 @@ index c2df567ea0..240239cd17 100644 pinnedAt: projection.thread.pinnedAt ?? null, autoSettleDisabledAt: projection.thread.autoSettleDisabledAt ?? null, -@@ -1793,6 +1800,12 @@ function shellFromState(input: { +@@ -1830,6 +1837,12 @@ function shellFromState(input: { ? {} : { activeOrderKey: input.state.thread.activeOrderKey }), lineage: input.state.thread.lineage, @@ -246,7 +251,7 @@ index c2df567ea0..240239cd17 100644 forkedFrom: input.state.thread.forkedFrom, activeProviderThreadId: input.state.thread.activeProviderThreadId, ...(input.state.thread.historyOrigin === undefined -@@ -1837,6 +1850,7 @@ function shellFromState(input: { +@@ -1874,6 +1887,7 @@ function shellFromState(input: { unsettledAt: input.state.thread.unsettledAt ?? null, snoozedUntil: input.state.thread.snoozedUntil ?? null, snoozedAt: input.state.thread.snoozedAt ?? null, @@ -254,7 +259,7 @@ index c2df567ea0..240239cd17 100644 pinnedAt: input.state.thread.pinnedAt ?? null, autoSettleDisabledAt: input.state.thread.autoSettleDisabledAt ?? null, -@@ -6335,6 +6349,7 @@ export const layer: Layer.Layer = +@@ -6455,6 +6469,7 @@ export const layer: Layer.Layer = autoSettleDisabledAt: thread.autoSettleDisabledAt ?? null, snoozedUntil: thread.snoozedUntil ?? null, snoozedAt: thread.snoozedAt ?? null, @@ -263,10 +268,10 @@ index c2df567ea0..240239cd17 100644 latestRunId, latestRunRequestedAt: diff --git a/apps/server/src/orchestration-v2/ThreadLaunchService.test.ts b/apps/server/src/orchestration-v2/ThreadLaunchService.test.ts -index d3c62a7e13..ad0cab72a4 100644 +index ec0e370565..0b3e98e43d 100644 --- a/apps/server/src/orchestration-v2/ThreadLaunchService.test.ts +++ b/apps/server/src/orchestration-v2/ThreadLaunchService.test.ts -@@ -509,6 +509,25 @@ it.effect("retains automation and sender attribution while a message waits in th +@@ -513,6 +513,25 @@ it.effect("retains automation and sender attribution while a message waits in th }).pipe(Effect.provide(harness.layer)); }); @@ -293,7 +298,7 @@ index d3c62a7e13..ad0cab72a4 100644 Effect.gen(function* () { const setupCount = yield* Ref.make(0); diff --git a/apps/server/src/orchestration-v2/ThreadLaunchService.ts b/apps/server/src/orchestration-v2/ThreadLaunchService.ts -index b68eef4b2a..2583ca5c6d 100644 +index 4ab38c4b30..821f81ae4f 100644 --- a/apps/server/src/orchestration-v2/ThreadLaunchService.ts +++ b/apps/server/src/orchestration-v2/ThreadLaunchService.ts @@ -89,6 +89,8 @@ export interface ThreadLaunchInput { @@ -305,7 +310,7 @@ index b68eef4b2a..2583ca5c6d 100644 readonly importedNativeThread?: { readonly ref: { readonly driver: ProviderDriverKind; -@@ -913,6 +915,9 @@ const make = Effect.gen(function* () { +@@ -916,6 +918,9 @@ const make = Effect.gen(function* () { interactionMode: input.interactionMode, branch: initialBranch, worktreePath: initialWorktreePath, @@ -343,10 +348,10 @@ index 9285f72d46..671b0107f8 100644 ); diff --git a/apps/server/src/orchestration-v2/ThreadManagementService.ts b/apps/server/src/orchestration-v2/ThreadManagementService.ts -index c1727567f9..3f31307558 100644 +index f0346b2bec..cb3e063550 100644 --- a/apps/server/src/orchestration-v2/ThreadManagementService.ts +++ b/apps/server/src/orchestration-v2/ThreadManagementService.ts -@@ -1037,6 +1037,7 @@ const make = Effect.gen(function* () { +@@ -1038,6 +1038,7 @@ const make = Effect.gen(function* () { type: "thread.settle", commandId: CommandId.make(`server:settle-after-run:${input.runId}`), threadId: input.threadId, @@ -354,7 +359,7 @@ index c1727567f9..3f31307558 100644 }); }); -@@ -1056,6 +1057,7 @@ const make = Effect.gen(function* () { +@@ -1057,6 +1058,7 @@ const make = Effect.gen(function* () { type: "thread.settle", commandId: input.commandId, threadId: input.threadId, @@ -363,17 +368,17 @@ index c1727567f9..3f31307558 100644 return { sequence: result.sequence }; }); diff --git a/apps/server/src/orchestration-v2/ThreadSettlementService.test.ts b/apps/server/src/orchestration-v2/ThreadSettlementService.test.ts -index e55d585c30..33bb2da429 100644 +index 1d8e514942..e2263fad9e 100644 --- a/apps/server/src/orchestration-v2/ThreadSettlementService.test.ts +++ b/apps/server/src/orchestration-v2/ThreadSettlementService.test.ts -@@ -464,6 +464,7 @@ describe("autoSettlementSettingsKey", () => { +@@ -466,6 +466,7 @@ describe("autoSettlementSettingsKey", () => { }); type AutoSettleCommand = Extract; +type SettleCommand = Extract; - const testCrypto = Crypto.make({ - randomBytes: (size) => new Uint8Array(size).fill(1), + const testCrypto = SecureRandom.make((size) => new Uint8Array(size).fill(1)); + @@ -562,6 +563,8 @@ interface HarnessOptions { ) => Effect.Effect; readonly onCloseIdle?: () => Effect.Effect; @@ -428,7 +433,7 @@ index e55d585c30..33bb2da429 100644 getThread: (threadId) => { const thread = options.currentThreads?.find((candidate) => candidate.id === threadId); return thread -@@ -717,6 +744,7 @@ const makeHarness = Effect.fn("makeThreadSettlementHarness")(function* (options: +@@ -728,6 +755,7 @@ const makeHarness = Effect.fn("makeThreadSettlementHarness")(function* (options: snapshotReads, candidateReads, commands, @@ -436,7 +441,7 @@ index e55d585c30..33bb2da429 100644 branchCalls, summaryCalls, summaryRecovery, -@@ -1355,6 +1383,43 @@ describe("ThreadSettlementServiceV2 terminals", () => { +@@ -1477,6 +1505,43 @@ describe("ThreadSettlementServiceV2 terminals", () => { ), ); @@ -480,7 +485,7 @@ index e55d585c30..33bb2da429 100644 it.effect("runs the settle script in the thread's own worktree", () => Effect.scoped( Effect.gen(function* () { -@@ -1487,3 +1552,56 @@ describe("isSnoozed", () => { +@@ -1609,3 +1674,56 @@ describe("isSnoozed", () => { ).toBeNull(); }); }); @@ -538,7 +543,7 @@ index e55d585c30..33bb2da429 100644 + }); +}); diff --git a/apps/server/src/orchestration-v2/ThreadSettlementService.ts b/apps/server/src/orchestration-v2/ThreadSettlementService.ts -index fa0bf3dff4..4411978353 100644 +index 8c2dcfe886..243f19a9e1 100644 --- a/apps/server/src/orchestration-v2/ThreadSettlementService.ts +++ b/apps/server/src/orchestration-v2/ThreadSettlementService.ts @@ -7,6 +7,7 @@ import { visibleThreadPullRequests } from "@t3tools/shared/threadPullRequests"; @@ -549,7 +554,7 @@ index fa0bf3dff4..4411978353 100644 type ThreadId, type OrchestrationV2DomainEvent, type OrchestrationV2ThreadShell, -@@ -158,6 +159,29 @@ export function isAutoSettlementCandidate( +@@ -159,6 +160,29 @@ export function isAutoSettlementCandidate( return !isSnoozed(thread, nowMs); } @@ -579,7 +584,7 @@ index fa0bf3dff4..4411978353 100644 /** Match client effectiveSnoozed so agents and sidebar agree. */ export function isSnoozed( thread: Pick< -@@ -621,10 +645,62 @@ export const make = Effect.gen(function* () { +@@ -625,10 +649,62 @@ export const make = Effect.gen(function* () { ), ); @@ -643,7 +648,7 @@ index fa0bf3dff4..4411978353 100644 case "thread.pull-request-link-updated": case "thread.pull-request-synced": case "provider-session.detached": -@@ -705,6 +781,9 @@ export const make = Effect.gen(function* () { +@@ -709,6 +785,9 @@ export const make = Effect.gen(function* () { return { start, @@ -655,10 +660,10 @@ index fa0bf3dff4..4411978353 100644 } satisfies ThreadSettlementServiceV2["Service"]; }); diff --git a/apps/server/src/orchestration-v2/runtimeLayer.test.ts b/apps/server/src/orchestration-v2/runtimeLayer.test.ts -index 8e1ca75fca..604a3f5a1b 100644 +index b38fb737c9..869a744a97 100644 --- a/apps/server/src/orchestration-v2/runtimeLayer.test.ts +++ b/apps/server/src/orchestration-v2/runtimeLayer.test.ts -@@ -2565,6 +2565,12 @@ it.layer(layerTest)("OrchestrationV2LayerLive lifecycle", (it) => { +@@ -2603,6 +2603,12 @@ it.layer(layerTest)("OrchestrationV2LayerLive lifecycle", (it) => { const autoSettledProjection = yield* orchestrator.getThreadProjection(threadId); assert.equal(autoSettledProjection.thread.settledOverride, "settled"); assert.isNull(autoSettledProjection.thread.activeOrderKey); @@ -671,7 +676,7 @@ index 8e1ca75fca..604a3f5a1b 100644 yield* orchestrator.dispatch({ type: "thread.unsettle", commandId: CommandId.make("runtime-layer-lifecycle-auto-unsettle"), -@@ -2590,6 +2596,15 @@ it.layer(layerTest)("OrchestrationV2LayerLive lifecycle", (it) => { +@@ -2628,6 +2634,15 @@ it.layer(layerTest)("OrchestrationV2LayerLive lifecycle", (it) => { const settledProjection = yield* orchestrator.getThreadProjection(threadId); assert.equal(settledProjection.thread.settledOverride, "settled"); assert.isNotNull(settledProjection.thread.settledAt); @@ -687,7 +692,7 @@ index 8e1ca75fca..604a3f5a1b 100644 yield* orchestrator.dispatch({ type: "thread.unsettle", -@@ -7007,6 +7022,11 @@ it.layer(layerSharedApplicationDataPlaneTest)("snooze projection", (it) => { +@@ -7145,6 +7160,11 @@ it.layer(layerSharedApplicationDataPlaneTest)("snooze projection", (it) => { ); const [candidate] = yield* projections.getSettlementCandidates(threadId); assert.deepEqual(candidate?.lastSnoozeWakeAt, yield* DateTime.now); @@ -697,8 +702,8 @@ index 8e1ca75fca..604a3f5a1b 100644 + ); + assert.deepEqual(wokenShell?.lastSnoozeWakeAt, yield* DateTime.now); yield* orchestrator.dispatch({ - type: "thread.snooze", - commandId: CommandId.make("runtime-layer-snoozed-thread-snooze-after-wake"), + type: "thread.settle", + commandId: CommandId.make("runtime-layer-snoozed-thread-settle"), diff --git a/apps/web/src/components/Sidebar.childThreads.browser.test.tsx b/apps/web/src/components/Sidebar.childThreads.browser.test.tsx new file mode 100644 index 0000000000..64c05d7c0d @@ -2629,7 +2634,7 @@ index 0000000000..ec6adb8b89 + ].join(", "); +} diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx -index deede26763..915d280e2b 100644 +index 868aa85cec..44d826dbac 100644 --- a/apps/web/src/components/Sidebar.tsx +++ b/apps/web/src/components/Sidebar.tsx @@ -214,6 +214,7 @@ import { @@ -2790,7 +2795,7 @@ index deede26763..915d280e2b 100644 case "copy-path": if (!threadWorkspacePath) { toastManager.add( -@@ -3438,6 +3452,13 @@ export default function Sidebar({ routeReveal }: { routeReveal: SidebarRouteReve +@@ -3439,6 +3453,13 @@ export default function Sidebar({ routeReveal }: { routeReveal: SidebarRouteReve // Folders draw active rows in their own place below. if (projectFolders !== null && item.section === "active") continue; items.push(renderThreadRow(threadByKey.get(item.key)!, item.section)); @@ -2804,7 +2809,7 @@ index deede26763..915d280e2b 100644 continue; } if (projectFolders !== null && item.marker === "active-placeholder") { -@@ -3453,7 +3474,8 @@ export default function Sidebar({ routeReveal }: { routeReveal: SidebarRouteReve +@@ -3454,7 +3475,8 @@ export default function Sidebar({ routeReveal }: { routeReveal: SidebarRouteReve onContextMenu={handleProjectFolderContextMenu} />, ); @@ -2814,7 +2819,7 @@ index deede26763..915d280e2b 100644 items.push( renderThreadRow( thread, -@@ -3461,6 +3483,13 @@ export default function Sidebar({ routeReveal }: { routeReveal: SidebarRouteReve +@@ -3462,6 +3484,13 @@ export default function Sidebar({ routeReveal }: { routeReveal: SidebarRouteReve sidebarFolderRowId(folder.group.projectKey), ), ); @@ -2828,7 +2833,7 @@ index deede26763..915d280e2b 100644 } } continue; -@@ -3598,7 +3627,7 @@ export default function Sidebar({ routeReveal }: { routeReveal: SidebarRouteReve +@@ -3597,7 +3626,7 @@ export default function Sidebar({ routeReveal }: { routeReveal: SidebarRouteReve ? "Loading" : olderSettledShelf.failed ? "Could not load older threads. Retry" @@ -2838,7 +2843,7 @@ index deede26763..915d280e2b 100644 ) : null} diff --git a/apps/web/src/components/sidebar/SidebarThreadRow.tsx b/apps/web/src/components/sidebar/SidebarThreadRow.tsx -index fc47fd8e21..b50791ae0c 100644 +index ed3a8276be..1826eeecef 100644 --- a/apps/web/src/components/sidebar/SidebarThreadRow.tsx +++ b/apps/web/src/components/sidebar/SidebarThreadRow.tsx @@ -102,6 +102,7 @@ import { @@ -3046,10 +3051,10 @@ index 196b5ff39e..271ba3004d 100644 pinnedAt: nullableIso(thread.pinnedAt ?? null), autoSettleDisabledAt: nullableIso(thread.autoSettleDisabledAt ?? null), diff --git a/packages/client-runtime/src/state/shell-sync.test.ts b/packages/client-runtime/src/state/shell-sync.test.ts -index 9d1df33d1f..415a6c6396 100644 +index c74ecd82fe..7924c6a31d 100644 --- a/packages/client-runtime/src/state/shell-sync.test.ts +++ b/packages/client-runtime/src/state/shell-sync.test.ts -@@ -686,14 +686,12 @@ describe("environment shell synchronization", () => { +@@ -870,14 +870,12 @@ describe("environment shell synchronization", () => { const stateWhere = (predicate: (state: EnvironmentShellState) => boolean) => SubscriptionRef.changes(shellState).pipe(Stream.filter(predicate), Stream.runHead); @@ -3070,10 +3075,10 @@ index 9d1df33d1f..415a6c6396 100644 const loaded = Option.getOrThrow(yield* stateWhere((state) => state.generation > 0)); expect(loaded.removedAt.size).toBe(0); diff --git a/packages/client-runtime/src/state/shell.ts b/packages/client-runtime/src/state/shell.ts -index bce13d315b..cafcf2e583 100644 +index 442e99b6f2..928b56cc44 100644 --- a/packages/client-runtime/src/state/shell.ts +++ b/packages/client-runtime/src/state/shell.ts -@@ -365,7 +365,9 @@ export const makeEnvironmentShellState = Effect.fn("EnvironmentShellState.make") +@@ -380,7 +380,9 @@ export const makeEnvironmentShellState = Effect.fn("EnvironmentShellState.make") yield* Ref.set(activeSubscriptionSession, session); const { omitIdleSubagents, omitOlderSettled } = yield* session.initialConfig.pipe( Effect.map((config) => ({ @@ -3085,10 +3090,10 @@ index bce13d315b..cafcf2e583 100644 config.shellOlderSettled === true && config.shellThreadFamilies === true, })), diff --git a/packages/client-runtime/src/state/threadCommands.test.ts b/packages/client-runtime/src/state/threadCommands.test.ts -index 2b1b1162d6..9c0de4ebce 100644 +index bc210e18ac..ad09cac0c7 100644 --- a/packages/client-runtime/src/state/threadCommands.test.ts +++ b/packages/client-runtime/src/state/threadCommands.test.ts -@@ -258,7 +258,11 @@ describe("remote thread lifecycle commands", () => { +@@ -255,7 +255,11 @@ describe("remote thread lifecycle commands", () => { ["settle", {}, { settledOverride: "settled", pinnedAt: null, snoozedUntil: null }], ["unsettle", { reason: "user" }, { settledOverride: "active", settledAt: null }], ["snooze", { snoozedUntil: "2099-01-01T00:00:00.000Z" }, { snoozedUntil: FUTURE }], @@ -3102,7 +3107,7 @@ index 2b1b1162d6..9c0de4ebce 100644 ["unpin", {}, { pinnedAt: null, pinOrderKey: null }], ["setAutoSettle", { enabled: false }, { autoSettleDisabledAt: expect.any(Object) }], diff --git a/packages/client-runtime/src/state/threadCommands.ts b/packages/client-runtime/src/state/threadCommands.ts -index e0aae375ec..9873724a3b 100644 +index 88a5759c5e..ee82af9813 100644 --- a/packages/client-runtime/src/state/threadCommands.ts +++ b/packages/client-runtime/src/state/threadCommands.ts @@ -68,6 +68,13 @@ export type LoadEarlierThreadHistoryInput = { @@ -3117,7 +3122,7 @@ index e0aae375ec..9873724a3b 100644 +} + export function createThreadEnvironmentAtoms( - runtime: Atom.AtomRuntime, + runtime: Atom.AtomRuntime, snapshotAtom: (environmentId: EnvironmentId) => Atom.Atom, @@ -384,7 +391,8 @@ export function createThreadEnvironmentAtoms( ["preparing", "queued", "starting", "running", "waiting"].includes(thread.status)) @@ -3144,10 +3149,10 @@ index e0aae375ec..9873724a3b 100644 setAutoSettle: optimistic.wrap(commands.setAutoSettle, (thread, input, now) => ({ ...thread, diff --git a/packages/client-runtime/src/state/threadShell.test.ts b/packages/client-runtime/src/state/threadShell.test.ts -index 7eb329320a..16652b51b0 100644 +index 3960757562..236a466081 100644 --- a/packages/client-runtime/src/state/threadShell.test.ts +++ b/packages/client-runtime/src/state/threadShell.test.ts -@@ -108,6 +108,43 @@ describe("v2 thread shell lists", () => { +@@ -112,6 +112,43 @@ describe("v2 thread shell lists", () => { registry.dispose(); }); @@ -3192,10 +3197,10 @@ index 7eb329320a..16652b51b0 100644 const { registry, threads, snapshotAtom } = makeHarness(); const root = v2ThreadShell; diff --git a/packages/contracts/src/orchestrationV2.ts b/packages/contracts/src/orchestrationV2.ts -index 36c096a333..96909b7ab2 100644 +index 8a054d175a..c73ae68d3a 100644 --- a/packages/contracts/src/orchestrationV2.ts +++ b/packages/contracts/src/orchestrationV2.ts -@@ -457,6 +457,13 @@ export const OrchestrationV2AppThread = Schema.Struct({ +@@ -460,6 +460,13 @@ export const OrchestrationV2AppThread = Schema.Struct({ linkOrigin: Schema.optional(OrchestrationV2LinkOrigin), delegatedFrom: Schema.optional(OrchestrationV2DelegatedFrom), lineage: OrchestrationV2AppThreadLineage, @@ -3209,7 +3214,7 @@ index 36c096a333..96909b7ab2 100644 forkedFrom: Schema.NullOr( Schema.Union([ Schema.Struct({ type: Schema.Literal("run"), threadId: ThreadId, runId: RunId }), -@@ -582,6 +589,17 @@ export function idleCompactionOf( +@@ -587,6 +594,17 @@ export function idleCompactionOf( : undefined; } @@ -3227,7 +3232,7 @@ index 36c096a333..96909b7ab2 100644 export const OrchestrationV2RunStatus = Schema.Literals([ "preparing", "queued", -@@ -2080,6 +2098,8 @@ export const OrchestrationV2ThreadShell = Schema.Struct({ +@@ -2094,6 +2112,8 @@ export const OrchestrationV2ThreadShell = Schema.Struct({ pullRequests: Schema.optional(Schema.Array(ThreadPullRequestLink)), branchPullRequest: Schema.optional(Schema.NullOr(ThreadLinkedPullRequest)), lineage: OrchestrationV2AppThreadLineage, @@ -3236,7 +3241,7 @@ index 36c096a333..96909b7ab2 100644 forkedFrom: Schema.NullOr(OrchestrationV2AppThread.fields.forkedFrom), activeProviderThreadId: Schema.NullOr(ProviderThreadId), historyOrigin: Schema.optional(OrchestrationV2ThreadHistoryOrigin), -@@ -2126,6 +2146,8 @@ export const OrchestrationV2ThreadShell = Schema.Struct({ +@@ -2140,6 +2160,8 @@ export const OrchestrationV2ThreadShell = Schema.Struct({ unsettledAt: Schema.optional(Schema.NullOr(Schema.DateTimeUtc)), snoozedUntil: Schema.optional(Schema.NullOr(Schema.DateTimeUtc)), snoozedAt: Schema.optional(Schema.NullOr(Schema.DateTimeUtc)), @@ -3245,7 +3250,7 @@ index 36c096a333..96909b7ab2 100644 limitRecovery: Schema.optional(Schema.NullOr(OrchestrationV2LimitRecovery)), pinnedAt: Schema.optional(Schema.NullOr(Schema.DateTimeUtc)), autoSettleDisabledAt: Schema.optional(Schema.NullOr(Schema.DateTimeUtc)), -@@ -2912,6 +2934,7 @@ export const OrchestrationV2Command = Schema.Union([ +@@ -2930,6 +2952,7 @@ export const OrchestrationV2Command = Schema.Union([ linkOrigin: Schema.optional(OrchestrationV2LinkOrigin), /** Only the server sets this, with `linkOrigin`, for a linked environment's delegated task. */ delegatedFrom: Schema.optional(OrchestrationV2DelegatedFrom), @@ -3253,7 +3258,7 @@ index 36c096a333..96909b7ab2 100644 importedNativeThread: Schema.optional( Schema.Struct({ ref: Schema.Struct({ -@@ -2956,6 +2979,8 @@ export const OrchestrationV2Command = Schema.Union([ +@@ -2974,6 +2997,8 @@ export const OrchestrationV2Command = Schema.Union([ commandId: CommandId, threadId: ThreadId, settledAt: Schema.optional(Schema.DateTimeUtc), diff --git a/patches-tt/0004-feat-server-push-agent-activity-directly-to-APNs.patch b/patches-tt/0004-feat-server-push-agent-activity-directly-to-APNs.patch index 1dfe9a1..4ec5937 100644 --- a/patches-tt/0004-feat-server-push-agent-activity-directly-to-APNs.patch +++ b/patches-tt/0004-feat-server-push-agent-activity-directly-to-APNs.patch @@ -34,10 +34,10 @@ Activity it filled itself. Without a key the server behaves as upstream. create mode 100644 apps/server/src/directPush/agentActivityDelivery.ts diff --git a/apps/server/src/auth/RpcAuthorization.ts b/apps/server/src/auth/RpcAuthorization.ts -index 6049121b41..29faf8f433 100644 +index f040dbc459..431de7df97 100644 --- a/apps/server/src/auth/RpcAuthorization.ts +++ b/apps/server/src/auth/RpcAuthorization.ts -@@ -110,6 +110,9 @@ export const RPC_REQUIRED_SCOPES = { +@@ -111,6 +111,9 @@ export const RPC_REQUIRED_SCOPES = { [WS_METHODS.serverRefreshUsageRates]: AuthDiagnosticsReadScope, [WS_METHODS.serverSignalProcess]: AuthEnvironmentMaintainScope, [WS_METHODS.serverReportClientActivity]: AuthOrchestrationReadScope, @@ -1635,7 +1635,7 @@ index 0000000000..288496e494 + ); +} diff --git a/apps/server/src/environment/ServerEnvironment.ts b/apps/server/src/environment/ServerEnvironment.ts -index d542b4ee55..27a9374071 100644 +index 0787a3702c..b67d7d780f 100644 --- a/apps/server/src/environment/ServerEnvironment.ts +++ b/apps/server/src/environment/ServerEnvironment.ts @@ -17,6 +17,7 @@ import packageJson from "../../package.json" with { type: "json" }; @@ -1661,10 +1661,10 @@ index d542b4ee55..27a9374071 100644 ...descriptor, capabilities: { ...descriptor.capabilities, agentActivityPublishing }, diff --git a/apps/server/src/observability/RpcInstrumentation.ts b/apps/server/src/observability/RpcInstrumentation.ts -index 1e3388e413..818e95f19b 100644 +index 17940b8371..e95b4d9a1a 100644 --- a/apps/server/src/observability/RpcInstrumentation.ts +++ b/apps/server/src/observability/RpcInstrumentation.ts -@@ -87,6 +87,9 @@ const RPC_AGGREGATES = { +@@ -88,6 +88,9 @@ const RPC_AGGREGATES = { [WS_METHODS.serverRefreshUsageRates]: "server", [WS_METHODS.serverSignalProcess]: "server", [WS_METHODS.serverReportClientActivity]: "server", @@ -1675,7 +1675,7 @@ index 1e3388e413..818e95f19b 100644 [WS_METHODS.scheduledTasksUpsert]: "scheduledTasks", [WS_METHODS.scheduledTasksSetEnabled]: "scheduledTasks", diff --git a/apps/server/src/relay/AgentAwarenessRelay.test.ts b/apps/server/src/relay/AgentAwarenessRelay.test.ts -index 4bbaa85fc3..d1ddfbd9b6 100644 +index 87838fd5a5..03aca1ae12 100644 --- a/apps/server/src/relay/AgentAwarenessRelay.test.ts +++ b/apps/server/src/relay/AgentAwarenessRelay.test.ts @@ -35,6 +35,7 @@ import { @@ -1694,7 +1694,7 @@ index 4bbaa85fc3..d1ddfbd9b6 100644 } = {}, ) { const values = new Map( -@@ -288,6 +290,10 @@ const makeTestRelay = Effect.fnUntraced(function* ( +@@ -289,6 +291,10 @@ const makeTestRelay = Effect.fnUntraced(function* ( }), Effect.provideService(FetchHttpClient.Fetch, fetch), Effect.provide(NodeCrypto.layer), @@ -1705,7 +1705,7 @@ index 4bbaa85fc3..d1ddfbd9b6 100644 ); return { relay, secrets, secretReads, currentShell, shellReads, publications, catchUp }; }); -@@ -340,6 +346,67 @@ describe("AgentAwarenessRelay", () => { +@@ -341,6 +347,67 @@ describe("AgentAwarenessRelay", () => { }), ); @@ -1774,7 +1774,7 @@ index 4bbaa85fc3..d1ddfbd9b6 100644 Effect.gen(function* () { const { relay, currentShell, publications } = yield* makeTestRelay(); diff --git a/apps/server/src/relay/AgentAwarenessRelay.ts b/apps/server/src/relay/AgentAwarenessRelay.ts -index f3c7b893af..7fb1c7df34 100644 +index 6a235911d4..251dfb8f0c 100644 --- a/apps/server/src/relay/AgentAwarenessRelay.ts +++ b/apps/server/src/relay/AgentAwarenessRelay.ts @@ -47,6 +47,7 @@ import { @@ -1785,7 +1785,7 @@ index f3c7b893af..7fb1c7df34 100644 import * as ServerEnvironment from "../environment/ServerEnvironment.ts"; import * as ThreadManagement from "../orchestration-v2/ThreadManagementService.ts"; import * as ProjectService from "../project/ProjectService.ts"; -@@ -371,6 +372,12 @@ export const make = Effect.gen(function* () { +@@ -383,6 +384,12 @@ export const make = Effect.gen(function* () { const startedAt = (yield* DateTime.now).epochMilliseconds; const catchUpRequests = yield* Queue.dropping(1); const publishedStateByThreadRef = yield* Ref.make(new Map()); @@ -1798,7 +1798,7 @@ index f3c7b893af..7fb1c7df34 100644 const readSecretString = (name: string) => secrets -@@ -435,8 +442,9 @@ export const make = Effect.gen(function* () { +@@ -447,8 +454,9 @@ export const make = Effect.gen(function* () { }); const publishThreadUnsafe = Effect.fn("publishThreadUnsafe")(function* (threadId: ThreadId) { @@ -1809,7 +1809,7 @@ index f3c7b893af..7fb1c7df34 100644 yield* cancelPublishRetries; yield* resetPublishedConnection; yield* Effect.logDebug("agent activity publish skipped; publication disabled", { -@@ -444,8 +452,8 @@ export const make = Effect.gen(function* () { +@@ -456,8 +464,8 @@ export const make = Effect.gen(function* () { }); return; } @@ -1820,7 +1820,7 @@ index f3c7b893af..7fb1c7df34 100644 yield* cancelPublishRetries; yield* resetPublishedConnection; yield* Effect.logDebug("agent activity publish skipped; relay link credentials unavailable", { -@@ -454,9 +462,10 @@ export const make = Effect.gen(function* () { +@@ -466,9 +474,10 @@ export const make = Effect.gen(function* () { return; } if ( @@ -1834,7 +1834,7 @@ index f3c7b893af..7fb1c7df34 100644 ) { yield* resetPublishedConnection; publishedRelayConfig = relayConfig; -@@ -467,7 +476,16 @@ export const make = Effect.gen(function* () { +@@ -479,7 +488,16 @@ export const make = Effect.gen(function* () { readonly projectId: string | null; readonly state: RelayAgentActivityState | null; readonly reason: string; @@ -1851,7 +1851,7 @@ index f3c7b893af..7fb1c7df34 100644 if (cachedRelayClient === undefined) { cachedRelayClient = yield* makeRelayClient(relayConfig); } -@@ -605,6 +623,7 @@ export const make = Effect.gen(function* () { +@@ -618,6 +636,7 @@ export const make = Effect.gen(function* () { projectId: unsettled.projectId, state: completion, reason: unsettled.reason, @@ -1859,7 +1859,7 @@ index f3c7b893af..7fb1c7df34 100644 }).pipe(withRelayClientTracing); heldCompletions.delete(threadId); publishConfirmDeadlines.set(threadId, nowMs + 5_000); -@@ -636,6 +655,7 @@ export const make = Effect.gen(function* () { +@@ -649,6 +668,7 @@ export const make = Effect.gen(function* () { projectId: snapshot.projectId, state: snapshot.state, reason: snapshot.reason, @@ -1867,7 +1867,7 @@ index f3c7b893af..7fb1c7df34 100644 }).pipe(withRelayClientTracing); publishConfirmDeadlines.delete(threadId); heldCompletions.delete(threadId); -@@ -707,24 +727,33 @@ export const make = Effect.gen(function* () { +@@ -714,24 +734,33 @@ export const make = Effect.gen(function* () { }); const publishActiveThreadsUnsafe = Effect.gen(function* () { @@ -1920,18 +1920,18 @@ index f3c7b893af..7fb1c7df34 100644 const environmentId = yield* serverEnvironment.getEnvironmentId; const projectSnapshot = yield* projects.snapshot; diff --git a/apps/server/src/server.ts b/apps/server/src/server.ts -index 452addfb4a..a655bfaf3f 100644 +index a4433b59e8..1c40e402d7 100644 --- a/apps/server/src/server.ts +++ b/apps/server/src/server.ts -@@ -92,6 +92,7 @@ import * as Keybindings from "./keybindings.ts"; +@@ -94,6 +94,7 @@ import * as Keybindings from "./keybindings.ts"; import * as ServerRuntimeStartup from "./serverRuntimeStartup.ts"; import * as ServerOwnership from "./serverOwnership.ts"; import * as AgentAwarenessRelay from "./relay/AgentAwarenessRelay.ts"; +import * as DirectPush from "./directPush/DirectPush.ts"; import { hasCloudPublicConfig } from "./cloud/publicConfig.ts"; import * as ServerSettings from "./serverSettings.ts"; - import * as ProjectEnrichmentService from "./project/ProjectEnrichmentService.ts"; -@@ -683,6 +684,9 @@ const layerRuntimeCoreDependencies = layerRuntimeCoreDependenciesBase.pipe( + import * as ManagedLimits from "./policy/managedLimits.ts"; +@@ -725,6 +726,9 @@ const layerRuntimeCoreDependencies = layerRuntimeCoreDependenciesBase.pipe( Layer.provideMerge(CodexManagedTokenBroker.layer), Layer.provideMerge(layerServerEnvironment), Layer.provideMerge(layerAuth), @@ -1942,10 +1942,10 @@ index 452addfb4a..a655bfaf3f 100644 Layer.provideMerge( Layer.mergeAll( diff --git a/apps/server/src/ws.ts b/apps/server/src/ws.ts -index abc7c71f4b..581e059325 100644 +index 492764c6ae..41767aa26b 100644 --- a/apps/server/src/ws.ts +++ b/apps/server/src/ws.ts -@@ -228,6 +228,7 @@ import * as RemoteOpenTargets from "./environment/RemoteOpenTargets.ts"; +@@ -226,6 +226,7 @@ import * as RemoteOpenTargets from "./environment/RemoteOpenTargets.ts"; import * as DefectReporter from "./observability/DefectReporter.ts"; import * as EventLoopMonitor from "./observability/EventLoopMonitor.ts"; import * as BackgroundPolicy from "./background/BackgroundPolicy.ts"; @@ -1953,7 +1953,7 @@ index abc7c71f4b..581e059325 100644 import * as EnvironmentAuth from "./auth/EnvironmentAuth.ts"; import { requiredScopeForDeviceList, rpcAuthorizationError } from "./auth/RpcAuthorization.ts"; import * as RpcAuthorization from "./auth/RpcAuthorization.ts"; -@@ -1387,6 +1388,7 @@ export const layerWsRpc = ( +@@ -1424,6 +1425,7 @@ export const layerWsRpc = ( const workspaceFileSystem = yield* WorkspaceFileSystem.WorkspaceFileSystem; const serverEnvironment = yield* ServerEnvironment.ServerEnvironment; const backgroundPolicy = yield* BackgroundPolicy.BackgroundPolicy; @@ -1961,7 +1961,7 @@ index abc7c71f4b..581e059325 100644 const rpcClientIds = yield* Ref.make(new Set()); yield* Effect.addFinalizer(() => Ref.get(rpcClientIds).pipe( -@@ -2602,6 +2604,18 @@ export const layerWsRpc = ( +@@ -2625,6 +2627,18 @@ export const layerWsRpc = ( ), ), ), @@ -1981,7 +1981,7 @@ index abc7c71f4b..581e059325 100644 [WS_METHODS.cloudInstallRelayClient]: (_input) => Stream.callback((queue) => diff --git a/packages/contracts/src/rpc.ts b/packages/contracts/src/rpc.ts -index 37ea5866ae..86a26933b2 100644 +index 737059e855..b3eb84fa68 100644 --- a/packages/contracts/src/rpc.ts +++ b/packages/contracts/src/rpc.ts @@ -38,6 +38,11 @@ import * as Rpc from "effect/rpc/Rpc"; @@ -1996,7 +1996,7 @@ index 37ea5866ae..86a26933b2 100644 import { ProviderAuthCancelInput, ProviderAuthCompleteInput, -@@ -534,6 +539,11 @@ export const WS_METHODS = { +@@ -535,6 +540,11 @@ export const WS_METHODS = { serverPrepareTriage: "server.prepareTriage", serverSignalProcess: "server.signalProcess", serverReportClientActivity: "server.reportClientActivity", @@ -2008,7 +2008,7 @@ index 37ea5866ae..86a26933b2 100644 serverGetUsageSummary: "server.getUsageSummary", serverRefreshUsageRates: "server.refreshUsageRates", -@@ -992,6 +1002,30 @@ const WsServerReportClientActivityRpc = Rpc.make(WS_METHODS.serverReportClientAc +@@ -994,6 +1004,30 @@ const WsServerReportClientActivityRpc = Rpc.make(WS_METHODS.serverReportClientAc error: EnvironmentAuthorizationError, }); @@ -2039,7 +2039,7 @@ index 37ea5866ae..86a26933b2 100644 const PullRequestRpcError = Schema.Union([ PullRequestUnavailableError, PullRequestOperationError, -@@ -2036,6 +2070,9 @@ export const WsRpcGroup = RpcGroup.make( +@@ -2054,6 +2088,9 @@ export const WsRpcGroup = RpcGroup.make( WsScheduledTasksListWebhookDeliveriesRpc, WsScheduledTasksGetWebhookDeliveryRpc, WsServerReportClientActivityRpc, diff --git a/patches-tt/0005-feat-show-the-customized-build-next-to-the-logo-and-.patch b/patches-tt/0005-feat-show-the-customized-build-next-to-the-logo-and-.patch index c69d806..0b8e610 100644 --- a/patches-tt/0005-feat-show-the-customized-build-next-to-the-logo-and-.patch +++ b/patches-tt/0005-feat-show-the-customized-build-next-to-the-logo-and-.patch @@ -218,10 +218,10 @@ index 0000000000..607215f524 + ); +} diff --git a/apps/web/src/components/settings/SettingsPanels.tsx b/apps/web/src/components/settings/SettingsPanels.tsx -index 5b4fe5042e..50b27ed187 100644 +index 1e9e700ca2..c2104a7754 100644 --- a/apps/web/src/components/settings/SettingsPanels.tsx +++ b/apps/web/src/components/settings/SettingsPanels.tsx -@@ -26,6 +26,7 @@ import { createModelSelection } from "@t3tools/shared/model"; +@@ -29,6 +29,7 @@ import { resolvePastedPath } from "@t3tools/shared/path"; import * as Equal from "effect/Equal"; import * as Schema from "effect/Schema"; import { APP_VERSION, HOSTED_APP_CHANNEL, HOSTED_APP_CHANNEL_LABEL } from "../../branding"; @@ -229,7 +229,7 @@ index 5b4fe5042e..50b27ed187 100644 import { AboutVersionTitle } from "./AboutVersionTitle"; import { CliCommandSettingsRow } from "./CliCommandSettingsRow"; import { MobileAppSettingsRow } from "./MobileAppSettingsRow"; -@@ -2020,6 +2021,7 @@ export function AboutSettingsPanel() { +@@ -2068,6 +2069,7 @@ export function AboutSettingsPanel() { /> )} diff --git a/patches-tt/0006-feat-desktop-download-nightlies-and-install-them-on-.patch b/patches-tt/0006-feat-desktop-download-nightlies-and-install-them-on-.patch index 2bfc8c0..6fa054a 100644 --- a/patches-tt/0006-feat-desktop-download-nightlies-and-install-them-on-.patch +++ b/patches-tt/0006-feat-desktop-download-nightlies-and-install-them-on-.patch @@ -138,18 +138,18 @@ index 0000000000..feeb28e3dd + { defaultValue: () => resolveDesktopUpdateAutomation() }, +); diff --git a/apps/desktop/src/updates/DesktopUpdates.ts b/apps/desktop/src/updates/DesktopUpdates.ts -index 3a0b41cd9e..3cad45d4e1 100644 +index 6279e8b614..f92b5bdb00 100644 --- a/apps/desktop/src/updates/DesktopUpdates.ts +++ b/apps/desktop/src/updates/DesktopUpdates.ts -@@ -34,6 +34,7 @@ import * as ElectronUpdater from "../electron/ElectronUpdater.ts"; - import * as ElectronWindow from "../electron/ElectronWindow.ts"; +@@ -35,6 +35,7 @@ import * as ElectronWindow from "../electron/ElectronWindow.ts"; import * as IpcChannels from "../ipc/channels.ts"; import * as DesktopAppSettings from "../settings/DesktopAppSettings.ts"; + import * as DesktopUpdatePolicy from "./DesktopUpdatePolicy.ts"; +import { DesktopUpdateAutomation } from "./DesktopUpdateAutomation.ts"; import { normalizeDesktopUpdateReleaseNotes } from "./releaseNotes.ts"; import { desktopUpdateChannelOf } from "@t3tools/shared/cliRelease"; import { compareSemverVersions } from "@t3tools/shared/semver"; -@@ -348,6 +349,7 @@ export const make = Effect.gen(function* () { +@@ -366,6 +367,7 @@ export const make = Effect.gen(function* () { const pool = yield* DesktopBackendPool.DesktopBackendPool; const desktopState = yield* DesktopState.DesktopState; const electronUpdater = yield* ElectronUpdater.ElectronUpdater; @@ -157,7 +157,7 @@ index 3a0b41cd9e..3cad45d4e1 100644 const electronWindow = yield* ElectronWindow.ElectronWindow; const environment = yield* DesktopEnvironment.DesktopEnvironment; const fileSystem = yield* FileSystem.FileSystem; -@@ -540,12 +542,22 @@ export const make = Effect.gen(function* () { +@@ -579,12 +581,22 @@ export const make = Effect.gen(function* () { ); }); @@ -186,7 +186,7 @@ index 3a0b41cd9e..3cad45d4e1 100644 }); const downloadAvailableUpdate = Effect.gen(function* () { -@@ -1098,8 +1110,10 @@ export const make = Effect.gen(function* () { +@@ -1155,8 +1167,10 @@ export const make = Effect.gen(function* () { } yield* Ref.set(updaterConfiguredRef, true); @@ -199,18 +199,18 @@ index 3a0b41cd9e..3cad45d4e1 100644 yield* electronUpdater.setDisableDifferentialDownload( isArm64HostRunningIntelBuild(environment.runtimeInfo), diff --git a/apps/desktop/src/updates/updatesTestHarness.ts b/apps/desktop/src/updates/updatesTestHarness.ts -index d8d16b7dbf..6da10ecdc5 100644 +index 226f00f8cb..ca8b71bc54 100644 --- a/apps/desktop/src/updates/updatesTestHarness.ts +++ b/apps/desktop/src/updates/updatesTestHarness.ts -@@ -14,6 +14,7 @@ import * as ElectronWindow from "../electron/ElectronWindow.ts"; - import * as DesktopAppSettings from "../settings/DesktopAppSettings.ts"; +@@ -16,6 +16,7 @@ import * as DesktopAppSettings from "../settings/DesktopAppSettings.ts"; import * as DesktopState from "../app/DesktopState.ts"; + import * as DesktopUpdatePolicy from "./DesktopUpdatePolicy.ts"; import * as DesktopUpdates from "./DesktopUpdates.ts"; +import * as DesktopUpdateAutomation from "./DesktopUpdateAutomation.ts"; export const flushCallbacks = Effect.yieldNow; -@@ -40,6 +41,7 @@ export interface UpdatesHarnessOptions { +@@ -43,6 +44,7 @@ export interface UpdatesHarnessOptions { readonly savedChannel?: DesktopAppSettings.DesktopSettings["updateChannel"]; /** Adds a WSL backend beside the primary, as on a Windows host with WSL enabled. */ readonly wslBackend?: boolean; @@ -218,7 +218,7 @@ index d8d16b7dbf..6da10ecdc5 100644 } export function makeHarness(options: UpdatesHarnessOptions = {}) { -@@ -49,6 +51,8 @@ export function makeHarness(options: UpdatesHarnessOptions = {}) { +@@ -52,6 +54,8 @@ export function makeHarness(options: UpdatesHarnessOptions = {}) { let allowDowngrade = false; const checkAllowedDowngrade: boolean[] = []; let fullChangelog = false; @@ -227,7 +227,7 @@ index d8d16b7dbf..6da10ecdc5 100644 const feedUrls: ElectronUpdater.ElectronUpdaterFeedUrl[] = []; const listeners = new Map void>>(); const sentStates: DesktopUpdateState[] = []; -@@ -76,8 +80,14 @@ export function makeHarness(options: UpdatesHarnessOptions = {}) { +@@ -79,8 +83,14 @@ export function makeHarness(options: UpdatesHarnessOptions = {}) { Effect.sync(() => { feedUrls.push(options); }), @@ -244,10 +244,10 @@ index d8d16b7dbf..6da10ecdc5 100644 setChannel: () => Effect.void, setAllowPrerelease: () => Effect.void, allowDowngrade: Effect.sync(() => allowDowngrade), -@@ -271,6 +281,12 @@ export function makeHarness(options: UpdatesHarnessOptions = {}) { - +@@ -275,6 +285,12 @@ export function makeHarness(options: UpdatesHarnessOptions = {}) { const layer = DesktopUpdates.layer.pipe( Layer.provide(layerFileSystem), + Layer.provide(DesktopUpdatePolicy.layerTest(options.policy)), + Layer.provide( + Layer.succeed( + DesktopUpdateAutomation.DesktopUpdateAutomation, @@ -257,7 +257,7 @@ index d8d16b7dbf..6da10ecdc5 100644 Layer.provideMerge(layerUpdater), Layer.provideMerge(layerWindow), Layer.provideMerge(layerBackend), -@@ -302,6 +318,8 @@ export function makeHarness(options: UpdatesHarnessOptions = {}) { +@@ -306,6 +322,8 @@ export function makeHarness(options: UpdatesHarnessOptions = {}) { /** Whether each feed check could offer an older release, and the flag now. */ checkAllowedDowngrade, allowDowngrade: () => allowDowngrade, diff --git a/patches-tt/0007-feat-web-label-child-agents-as-agent-or-subagent.patch b/patches-tt/0007-feat-web-label-child-agents-as-agent-or-subagent.patch index 3879369..6f57f91 100644 --- a/patches-tt/0007-feat-web-label-child-agents-as-agent-or-subagent.patch +++ b/patches-tt/0007-feat-web-label-child-agents-as-agent-or-subagent.patch @@ -9,23 +9,23 @@ Subject: [PATCH 07/10] feat(web): label child agents as agent or subagent .../chat/MessagesTimeline.browser.test.tsx | 22 +++ .../src/components/chat/MessagesTimeline.tsx | 6 +- .../chat/SubagentTooltipContent.tsx | 5 + - .../components/chat/ThreadActivityBanners.tsx | 66 +++++-- + .../components/chat/ThreadActivityBanners.tsx | 63 +++++-- ...RelationshipsControl.kind.browser.test.tsx | 163 ++++++++++++++++ .../chat/ThreadRelationshipsControl.logic.ts | 39 ++++ - .../chat/ThreadRelationshipsControl.tsx | 43 +++-- + .../chat/ThreadRelationshipsControl.tsx | 42 +++-- .../components/chat/TimelineV2EventRow.tsx | 4 +- - .../chat/V2LifecycleRow.kind.test.tsx | 85 ++++++++ - .../src/components/chat/V2LifecycleRow.tsx | 181 ++++++++++++++---- + .../chat/V2LifecycleRow.kind.test.tsx | 85 +++++++++ + .../src/components/chat/V2LifecycleRow.tsx | 175 ++++++++++++++---- .../src/state/subagentDisplay.kind.test.ts | 57 ++++++ .../src/state/subagentDisplay.ts | 40 +++- - .../src/state/threadExecution.kind.test.ts | 95 +++++++++ - .../src/state/threadExecution.ts | 83 ++++---- + .../src/state/threadExecution.kind.test.ts | 95 ++++++++++ + .../src/state/threadExecution.ts | 83 +++++---- .../src/state/threadShell.kind.test.ts | 23 +++ .../src/state/threadShell.test.ts | 2 +- .../client-runtime/src/state/threadShell.ts | 22 ++- packages/contracts/src/orchestrationV2.ts | 1 + .../orchestrationV2PendingBackgroundWork.ts | 2 + - 21 files changed, 832 insertions(+), 119 deletions(-) + 21 files changed, 824 insertions(+), 117 deletions(-) create mode 100644 apps/web/src/components/chat/ThreadRelationshipsControl.kind.browser.test.tsx create mode 100644 apps/web/src/components/chat/ThreadRelationshipsControl.logic.ts create mode 100644 apps/web/src/components/chat/V2LifecycleRow.kind.test.tsx @@ -34,10 +34,10 @@ Subject: [PATCH 07/10] feat(web): label child agents as agent or subagent create mode 100644 packages/client-runtime/src/state/threadShell.kind.test.ts diff --git a/apps/server/src/orchestration-v2/ProjectionStore.test.ts b/apps/server/src/orchestration-v2/ProjectionStore.test.ts -index 4f9c74732b..f28706499b 100644 +index 1d2db40308..76c08c9efe 100644 --- a/apps/server/src/orchestration-v2/ProjectionStore.test.ts +++ b/apps/server/src/orchestration-v2/ProjectionStore.test.ts -@@ -636,7 +636,13 @@ it.effect("SQL shells use the latest subagent owner for duplicate provider-threa +@@ -893,7 +893,13 @@ it.effect("SQL shells use the latest subagent owner for duplicate provider-threa } }); const owned = [ @@ -53,7 +53,7 @@ index 4f9c74732b..f28706499b 100644 yield* assertOwners(threadId, owned); yield* assertOwners(otherThreadId, owned); diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx -index e7e0b72af5..e747718875 100644 +index 0a76fc890e..735c5cd398 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -97,6 +97,7 @@ import { @@ -75,10 +75,10 @@ index e7e0b72af5..e747718875 100644 const threadError = isServerThread ? (localServerError ?? serverRuntime?.lastError ?? null) diff --git a/apps/web/src/components/chat/MessagesTimeline.browser.test.tsx b/apps/web/src/components/chat/MessagesTimeline.browser.test.tsx -index 2fcaa596a5..146eeb4988 100644 +index 6b1df4ca16..b86774b8af 100644 --- a/apps/web/src/components/chat/MessagesTimeline.browser.test.tsx +++ b/apps/web/src/components/chat/MessagesTimeline.browser.test.tsx -@@ -1440,3 +1440,25 @@ it("keeps concurrent approvals actionable outside collapsed work and restores se +@@ -1549,3 +1549,25 @@ it("keeps concurrent approvals actionable outside collapsed work and restores se .element(screen.getByRole("button", { name: "Open the current tab", exact: true })) .toBeVisible(); }); @@ -105,7 +105,7 @@ index 2fcaa596a5..146eeb4988 100644 + }); +}); diff --git a/apps/web/src/components/chat/MessagesTimeline.tsx b/apps/web/src/components/chat/MessagesTimeline.tsx -index 4c28163766..d1334aff65 100644 +index cea2daa111..2ad05ea523 100644 --- a/apps/web/src/components/chat/MessagesTimeline.tsx +++ b/apps/web/src/components/chat/MessagesTimeline.tsx @@ -268,6 +268,8 @@ interface MessagesTimelineProps { @@ -117,7 +117,7 @@ index 4c28163766..d1334aff65 100644 } | null; workflowOpening?: WorkflowOpening | null; onForkFromRun?: (input: { -@@ -1109,7 +1111,9 @@ const ConversationTimeline = memo(function ConversationTimeline({ +@@ -1096,7 +1098,9 @@ const ConversationTimeline = memo(function ConversationTimeline({
diff --git a/apps/web/src/components/chat/ThreadActivityBanners.tsx b/apps/web/src/components/chat/ThreadActivityBanners.tsx -index 5342bd09b3..99b521919f 100644 +index 368931833d..3ecf6bec99 100644 --- a/apps/web/src/components/chat/ThreadActivityBanners.tsx +++ b/apps/web/src/components/chat/ThreadActivityBanners.tsx -@@ -3,7 +3,10 @@ import type { +@@ -4,7 +4,10 @@ import type { OrchestrationV2ProviderGoal, ThreadId, } from "@t3tools/contracts"; @@ -174,7 +174,7 @@ index 5342bd09b3..99b521919f 100644 import { presentPendingBackgroundWork, presentProviderGoal, -@@ -75,7 +78,10 @@ export function goalNowItem(input: { +@@ -77,7 +80,10 @@ export function goalNowItem(input: { readonly threadId: ThreadId; readonly goal: OrchestrationV2ProviderGoal; readonly isWorking: boolean; @@ -186,7 +186,7 @@ index 5342bd09b3..99b521919f 100644 }): ThreadNowItem { const { isWorking, onSendCommand } = input; const presentation = presentProviderGoal(input.goal, isWorking); -@@ -93,7 +99,9 @@ export function goalNowItem(input: { +@@ -95,7 +101,9 @@ export function goalNowItem(input: { {presentation.canResume && !isWorking ? ( <> Resume -@@ -102,7 +110,9 @@ export function goalNowItem(input: { +@@ -104,7 +112,9 @@ export function goalNowItem(input: { ) : null} Clear -@@ -167,13 +177,16 @@ export function backgroundWorkNowItem(input: { +@@ -169,7 +179,8 @@ export function backgroundWorkNowItem(input: { const rows = presentation.items.map((task) => { // Same key derivePendingBackgroundWork gives turn-item tasks; subagent items carry progress. const item = input.turnItems.findLast( @@ -218,7 +218,9 @@ index 5342bd09b3..99b521919f 100644 ); const subagent = item?.type === "subagent" ? item : undefined; return { - task, +@@ -185,7 +196,9 @@ export function backgroundWorkNowItem(input: { + task.label + ), activity: subagent?.progress?.replace(/\s+/gu, " ").trim() || null, - startedAtMs: item?.startedAt ? DateTime.toEpochMillis(item.startedAt) : null, + startedAtMs: item?.startedAt @@ -227,38 +229,22 @@ index 5342bd09b3..99b521919f 100644 updatedAtMs: subagent ? DateTime.toEpochMillis(subagent.updatedAt) : 0, childThreadId: task.childThreadId ?? subagent?.childThreadId ?? undefined, }; -@@ -182,7 +195,8 @@ export function backgroundWorkNowItem(input: { - const newest = rows - .filter((row) => row.activity) - .toSorted((left, right) => right.updatedAtMs - left.updatedAtMs)[0]; -- const isCommand = (row: (typeof rows)[number]) => row.task.kind !== "subagent"; -+ const isCommand = (row: (typeof rows)[number]) => -+ row.task.kind !== "subagent"; - const verb = presentation.waiting ? "Waiting on" : "Running"; - const onlyThreadId = only?.childThreadId; - const startedAtMs = only?.startedAtMs ?? null; -@@ -192,6 +206,9 @@ export function backgroundWorkNowItem(input: { - title: only ? ( - <> - {verb}{" "} -+ {only.task.kind === "subagent" && only.task.label !== only.task.noun -+ ? `${only.task.noun} ` -+ : null} - {rows.map((row) => row.task.label).join(", ")} -+ -+ {rows.map((row) => row.task.label).join(", ")} -+ - ) : ( - rows.map((row) => row.task.label).join(", ") - ), -@@ -224,11 +243,18 @@ export function backgroundWorkNowItem(input: { +@@ -201,9 +214,12 @@ export function backgroundWorkNowItem(input: { + id: `background-work:${input.threadId}`, + icon: rows.every(isCommand) ? : , + title: +- only && typeof only.label !== "string" && only.task.label !== only.task.kind ? ( ++ only && typeof only.label !== "string" && only.task.label !== only.task.noun ? ( + <> +- {presentation.waiting ? "Waiting" : "Running"} · {only.label} ++ {presentation.waiting && only.task.kind === "subagent" ++ ? `Waiting on ${only.task.noun} ` ++ : `${presentation.waiting ? "Waiting" : "Running"} · `} ++ {only.label} + + ) : ( + presentation.title +@@ -234,11 +250,18 @@ export function backgroundWorkNowItem(input: { /> )} {onlyThreadId === undefined ? null : ( @@ -279,7 +265,7 @@ index 5342bd09b3..99b521919f 100644 {input.stopping ? "Stopping..." : "Stop"} -@@ -239,7 +265,10 @@ export function backgroundWorkNowItem(input: { +@@ -249,7 +272,10 @@ export function backgroundWorkNowItem(input: { /** The local checkout moved off the branch this thread last ran on. */ export function branchMismatchBannerItem(input: { readonly mismatchKey: string; @@ -291,7 +277,7 @@ index 5342bd09b3..99b521919f 100644 readonly restoreDisabled: boolean; readonly restoring: boolean; readonly onRestore: () => void; -@@ -252,7 +281,9 @@ export function branchMismatchBannerItem(input: { +@@ -262,7 +288,9 @@ export function branchMismatchBannerItem(input: { icon: , title: ( @@ -302,7 +288,7 @@ index 5342bd09b3..99b521919f 100644 @@ -540,17 +526,18 @@ index 0000000000..deeab95cdf + return edge.sourceThreadId === currentThreadId ? "Fork" : "Parent thread"; +} diff --git a/apps/web/src/components/chat/ThreadRelationshipsControl.tsx b/apps/web/src/components/chat/ThreadRelationshipsControl.tsx -index a3eaf6e787..b060e46fbf 100644 +index c91d5acebc..1b52035aff 100644 --- a/apps/web/src/components/chat/ThreadRelationshipsControl.tsx +++ b/apps/web/src/components/chat/ThreadRelationshipsControl.tsx -@@ -1,5 +1,6 @@ +@@ -1,6 +1,7 @@ import { useAtomValue } from "@effect/atom-react"; + import { threadKey } from "@t3tools/client-runtime/state/entities"; import { ThreadDetailsControl } from "./ThreadDetailsControl"; +import { relationshipLabel } from "./ThreadRelationshipsControl.logic"; import { ThreadHoverCardPopup } from "../ThreadHoverCard"; import { ThreadDetailsSection } from "./ThreadDetailsSection"; import { SectionHeaderStatus } from "../ui/collapsible-section-header"; -@@ -11,7 +12,11 @@ import { +@@ -12,7 +13,11 @@ import { projectedSubagentsToRuntime, type RuntimeSubagent, } from "@t3tools/client-runtime/state/subagentRuntime"; @@ -563,15 +550,7 @@ index a3eaf6e787..b060e46fbf 100644 import { deriveProviderSubagentStatus } from "@t3tools/client-runtime/state/thread-execution"; import { deriveThreadRelationshipGraph, -@@ -20,7 +25,6 @@ import { - threadRelationshipRowStatus, - orderWebThreadLineageRows, - resolveMergeBackTargetThreadId, -- type ThreadRelationshipEdge, - type ThreadRelationshipWalkRow, - } from "@t3tools/client-runtime/state/thread-relationships"; - import { -@@ -181,14 +185,6 @@ function ThreadLineageGroup(props: { +@@ -184,14 +189,6 @@ function ThreadLineageGroup(props: { ); } @@ -586,7 +565,7 @@ index a3eaf6e787..b060e46fbf 100644 function relationshipThreadTitle(input: { readonly title: string; readonly isSubagent: boolean; -@@ -571,7 +567,7 @@ export function ThreadRelationshipsPanel(props: { +@@ -609,7 +606,7 @@ export function ThreadRelationshipsPanel(props: { const stopSubagent = async ( childThreadId: ThreadId, @@ -595,7 +574,7 @@ index a3eaf6e787..b060e46fbf 100644 ) => { if (stoppingId !== null) return; setStoppingId(childThreadId); -@@ -581,7 +577,10 @@ export function ThreadRelationshipsPanel(props: { +@@ -619,7 +616,10 @@ export function ThreadRelationshipsPanel(props: { }); setStoppingId(null); if (result._tag === "Failure") { @@ -607,7 +586,7 @@ index a3eaf6e787..b060e46fbf 100644 } }; -@@ -619,14 +618,15 @@ export function ThreadRelationshipsPanel(props: { +@@ -659,14 +659,15 @@ export function ThreadRelationshipsPanel(props: { : node?.thread?.lineage.sideChat === true ? MessagesSquareIcon : GitForkIcon; @@ -627,7 +606,7 @@ index a3eaf6e787..b060e46fbf 100644 const failed = rowStatus === "failed" || rowStatus === "error"; const canStop = agent && -@@ -691,10 +691,13 @@ export function ThreadRelationshipsPanel(props: { +@@ -731,10 +732,13 @@ export function ThreadRelationshipsPanel(props: { fallbackIcon={RelationshipIcon} status={rowStatus} /> @@ -643,7 +622,7 @@ index a3eaf6e787..b060e46fbf 100644 {agent ? null : ( -@@ -824,7 +827,7 @@ export function ThreadRelationshipsPanel(props: { +@@ -864,7 +868,7 @@ export function ThreadRelationshipsPanel(props: { variant="ghost" part="icon" tone="destructive" @@ -652,7 +631,7 @@ index a3eaf6e787..b060e46fbf 100644 disabled={stoppingId !== null} onClick={() => void stopSubagent(threadId, agent)} /> -@@ -836,7 +839,7 @@ export function ThreadRelationshipsPanel(props: { +@@ -876,7 +880,7 @@ export function ThreadRelationshipsPanel(props: { )} @@ -781,7 +760,7 @@ index 0000000000..8403303c63 + }); +}); diff --git a/apps/web/src/components/chat/V2LifecycleRow.tsx b/apps/web/src/components/chat/V2LifecycleRow.tsx -index d9320d7148..de813a8b8d 100644 +index c0dd0e0900..f44ba2747c 100644 --- a/apps/web/src/components/chat/V2LifecycleRow.tsx +++ b/apps/web/src/components/chat/V2LifecycleRow.tsx @@ -8,7 +8,10 @@ import { buildThreadRouteParams } from "../../threadRoutes"; @@ -796,9 +775,9 @@ index d9320d7148..de813a8b8d 100644 import { environmentThreadDetails } from "../../state/threads"; import { EMPTY_SERVER_PROVIDERS, serverEnvironment } from "../../state/server"; import { MiddleTruncate } from "../ui/middle-truncate"; -@@ -19,7 +22,11 @@ import { - type HandoffEndpoints, +@@ -20,7 +23,11 @@ import { type HandoffTimelineRun, + resolveModelSwitchChange, } from "@t3tools/client-runtime/handoff"; -import { formatSubagentDisplayTitle } from "@t3tools/client-runtime/state/subagent-display"; +import { @@ -808,8 +787,8 @@ index d9320d7148..de813a8b8d 100644 +} from "@t3tools/client-runtime/state/subagent-display"; import { ProviderDriverKind, - type OrchestrationV2Notification, -@@ -54,7 +61,10 @@ import { + type ModelSelection, +@@ -57,7 +64,10 @@ import { } from "../../providerInstances"; import { formatShortTimestamp } from "../../timestampFormat"; import { getTriggerDisplayModelName } from "./providerIconUtils"; @@ -821,20 +800,7 @@ index d9320d7148..de813a8b8d 100644 import { cn } from "~/lib/utils"; import { TimelineSystemDivider } from "./TimelineSystemDivider"; import { subagentResumedSince } from "./V2LifecycleRow.logic"; -@@ -73,7 +83,11 @@ export function V2LifecycleRow(props: { - const { item } = props; - if (item.type === "run_interrupt_result") { - return ( -- -+ - ); - } - if (item.type === "handoff") { -@@ -86,12 +100,19 @@ export function V2LifecycleRow(props: { +@@ -94,12 +104,19 @@ export function V2LifecycleRow(props: { ); } if (item.type === "fork") { @@ -857,7 +823,7 @@ index d9320d7148..de813a8b8d 100644 onAction={() => props.onOpenThread(relatedThreadId)} /> ); -@@ -104,7 +125,10 @@ export function V2LifecycleRow(props: { +@@ -112,7 +129,10 @@ export function V2LifecycleRow(props: { className="flex min-w-0 items-center gap-3 rounded-lg border border-border/60 p-3" > @@ -869,7 +835,7 @@ index d9320d7148..de813a8b8d 100644 {item.title ?? "Created thread"} -@@ -145,7 +169,9 @@ export function V2LifecycleRow(props: { +@@ -153,7 +173,9 @@ export function V2LifecycleRow(props: { origin={item.origin} status={item.status} driver={item.driver} @@ -880,7 +846,7 @@ index d9320d7148..de813a8b8d 100644 result={item.result} progress={item.progress} startedAt={item.startedAt} -@@ -199,7 +225,11 @@ function isoOrNull(value: DateTime.Utc | null | undefined): string | null { +@@ -207,7 +229,11 @@ function isoOrNull(value: DateTime.Utc | null | undefined): string | null { return value ? DateTime.formatIso(value) : null; } @@ -893,7 +859,7 @@ index d9320d7148..de813a8b8d 100644 return ; } -@@ -280,7 +310,8 @@ export function EnvironmentSubagentAvatar({ +@@ -288,7 +314,8 @@ export function EnvironmentSubagentAvatar({ const providerDriver = useAtomValue( providersAtom, (providers) => @@ -903,7 +869,7 @@ index d9320d7148..de813a8b8d 100644 ); const displayName = useAtomValue( providersAtom, -@@ -300,7 +331,8 @@ export function EnvironmentSubagentAvatar({ +@@ -308,7 +335,8 @@ export function EnvironmentSubagentAvatar({ const iconUrl = useAtomValue( providersAtom, (providers) => @@ -913,7 +879,7 @@ index d9320d7148..de813a8b8d 100644 ); return ( -@@ -409,7 +449,10 @@ function SubagentTimelineLink(props: { +@@ -417,7 +453,10 @@ function SubagentTimelineLink(props: { }) { const agent = useAtomValue( environmentThreadDetails.threadAtom(props.parentRef), @@ -960,7 +926,7 @@ index d9320d7148..de813a8b8d 100644 ); const remote = props.remote ?? agent?.remoteChild; const remoteEnvironment = useEnvironment(remote?.environmentId ?? null); -@@ -423,18 +466,27 @@ function SubagentTimelineLink(props: { +@@ -431,18 +470,27 @@ function SubagentTimelineLink(props: { ? () => void navigate({ to: "/$environmentId/$threadId", @@ -991,7 +957,7 @@ index d9320d7148..de813a8b8d 100644 const failed = status === "failed"; const timing = { status: liveStatus, -@@ -456,7 +508,17 @@ function SubagentTimelineLink(props: { +@@ -464,7 +512,17 @@ function SubagentTimelineLink(props: { {props.title} {remote !== undefined ? ( @@ -1010,7 +976,7 @@ index d9320d7148..de813a8b8d 100644 ) : null} {detail === null ? ( @@ -1019,7 +985,7 @@ index d9320d7148..de813a8b8d 100644 ) : detail.includes("/") && !detail.includes(" ") ? ( ) : ( -@@ -474,15 +536,23 @@ function SubagentTimelineLink(props: { +@@ -482,15 +540,23 @@ function SubagentTimelineLink(props: { )} @@ -1047,7 +1013,7 @@ index d9320d7148..de813a8b8d 100644 {open !== null ? ( ) : ( -@@ -508,7 +582,7 @@ function SubagentTimelineLink(props: { +@@ -516,7 +586,7 @@ function SubagentTimelineLink(props: { type="button" data-v2-item-type="subagent" aria-label={`Open ${props.title}`} @@ -1069,7 +1035,7 @@ index d9320d7148..de813a8b8d 100644 onClick={open} className={cn( className, -@@ -523,7 +597,11 @@ function SubagentTimelineLink(props: { +@@ -531,7 +601,11 @@ function SubagentTimelineLink(props: { 0 ? (