# Files changed · PR \#23901 · pingdotgg/t3code

[View on GitCafe](https://git.cafe/pingdotgg/t3code/pull/23901/diffs)

Repository: [pingdotgg/t3code](https://git.cafe/pingdotgg/t3code)

Visibility: public

[feat\(pi\): show extension status in existing controls](https://git.cafe/pingdotgg/t3code/pull/23901.md)

Version: 5

Head commit: c07e70680d2c1f678b48a79ea6a9b3f47fb73fd8

Base commit: 23bd753806eb9db5a315fbf9ac6563d355f20b3a

Comparison base: 23bd753806eb9db5a315fbf9ac6563d355f20b3a

Showing up to 20 entries from this revision. If the pull request changes, reopen the first page before continuing.

## apps/mobile/src/components/ui/Dialog.android.tsx

Status: modified; +4; −1

```diff
@@ -10,6 +10,7 @@
   Text,
   useNativeState,
 } from "@expo/ui/jetpack-compose";
+import { verticalScroll } from "@expo/ui/jetpack-compose/modifiers";
 
 import { useAppearancePreferences } from "../../features/settings/appearance/AppearancePreferencesProvider";
 import { useScaledTextRole } from "../../features/settings/appearance/useScaledTextRole";
@@ -240,7 +241,9 @@
           </AlertDialog.Text>
         ) : props.request.message ? (
           <AlertDialog.Text>
+            <Text modifiers={[verticalScroll()]} style={bodyTypography}>
+              {props.request.message}
-            <Text style={bodyTypography}>{props.request.message}</Text>
+            </Text>
           </AlertDialog.Text>
         ) : null}
         {actions.length > 1 ? (
```

## apps/mobile/src/features/threads/ThreadDetailScreen.tsx

Status: modified; +22; −0

```diff
@@ -173,6 +173,7 @@
 import type { ThreadContentPresentation } from "./threadContentPresentation";
 import { resolveThreadFeedSubmissionAnchor } from "./thread-feed-live-follow";
 import { useGlobalVoiceInput } from "../voice-input/VoiceInputProvider";
+import type { OrchestrationV2ProviderSession } from "@t3tools/contracts";
 
 export interface ThreadDetailScreenProps {
   readonly header?: React.ReactNode;
@@ -208,6 +209,7 @@
   readonly activeWorkStartedAt: string | null;
   readonly runlessWorkActive?: boolean;
   readonly providerSubagentStatus?: ProviderSubagentStatus | null;
+  readonly extensionStatuses?: NonNullable<OrchestrationV2ProviderSession["extensionStatuses"]>;
   readonly isCompacting: boolean;
   readonly creationState:
     | {
@@ -675,6 +677,13 @@
   const pendingBackgroundWork = presentPendingBackgroundWork(
     props.selectedThread.pendingBackgroundTasks,
   );
+  const extensionStatus = props.extensionStatuses?.length
+    ? {
+        label: `Pi · ${props.extensionStatuses.map((status) => status.text).join(" · ")}`,
+        onPress: () =>
+          Dialog.alert("Pi", props.extensionStatuses?.map((status) => status.text).join("\n\n")),
+      }
+    : null;
   const floatingStatus = ((): FloatingWorkingStatus | null => {
     const connectionStatus = connectionFloatingStatus({
       connectionError: props.connectionError,
@@ -782,6 +791,9 @@
         accessibilityLabel: `${goal.title}: ${goal.objective}`,
       };
     }
+    if (extensionStatus !== null && contentPresentationKind === "ready") {
+      return { kind: "extension", ...extensionStatus };
+    }
     return null;
   })();
   const showWorkingControl = floatingStatus !== null;
@@ -1447,6 +1459,16 @@
                 <FloatingWorkingControl
                   colorScheme={isDarkMode ? "dark" : "light"}
                   status={floatingStatus}
+                  extensionStatus={
+                    floatingStatus !== null &&
+                    (floatingStatus.kind === "working" ||
+                      floatingStatus.kind === "compacting" ||
+                      floatingStatus.kind === "background" ||
+                      floatingStatus.kind === "goal" ||
+                      floatingStatus.kind === "lifecycle")
+                      ? extensionStatus
+                      : null
+                  }
                   lift={floatingControlLift}
                   devicePreview={
                     devicePreviews.length > 0
```

## apps/mobile/src/features/threads/ThreadRouteScreen.tsx

Status: modified; +1; −0

```diff
@@ -936,6 +936,7 @@
           }
           isCompacting={composer.isCompacting}
           runlessWorkActive={composer.runlessWorkActive}
+          extensionStatuses={composer.extensionStatuses}
           providerSubagentStatus={composer.providerSubagentStatus}
           creationState={creationState}
           setupWorkingStartedAt={
```

## apps/mobile/src/features/threads/floating-working-control.tsx

Status: modified; +59; −9

```diff
@@ -71,6 +71,7 @@
 export function FloatingWorkingControl(props: {
   readonly colorScheme: "light" | "dark";
   readonly status: FloatingWorkingStatus | null;
+  readonly extensionStatus?: { readonly label: string; readonly onPress: () => void } | null;
   readonly devicePreview: { readonly count: number; readonly onPress: () => void } | null;
   readonly browserPreview: { readonly count: number; readonly onPress: () => void } | null;
   readonly showScrollToEnd: boolean;
@@ -85,6 +86,10 @@
   const [overlayWidth, setOverlayWidth] = useState(windowWidth);
   const [queueWidth, setQueueWidth] = useState(0);
   const [actionWidth, setActionWidth] = useState(0);
+  const [extensionWidth, setExtensionWidth] = useState(0);
+  const extensionStatus = props.extensionStatus ?? null;
+  const extensionOnly = props.status?.kind === "extension";
+  const useGlass = NATIVE_LIQUID_GLASS_SUPPORTED && !extensionOnly;
   const action =
     props.status?.kind === "background" || props.status?.kind === "lifecycle"
       ? props.status.action
@@ -95,7 +100,7 @@
   const [previewWidth, setPreviewWidth] = useState(0);
   const agents = props.agents;
   const hasAgents = agents !== null;
-  const labelWidth = Math.max(
+  const availableLabelWidth = Math.max(
     0,
     Math.min(overlayWidth, windowWidth) -
       CONTROL_HEIGHT -
@@ -105,6 +110,10 @@
       (hasAgents ? agentsWidth : 0) -
       (hasPreview ? previewWidth : 0),
   );
+  const labelWidth = Math.max(
+    0,
+    availableLabelWidth - (extensionStatus === null ? 0 : extensionWidth),
+  );
   const separationProgress = useSharedValue(props.showScrollToEnd ? 1 : 0);
 
   useEffect(() => {
@@ -163,10 +172,16 @@
     (props.devicePreview !== null && props.browserPreview !== null);
   const statusInteractive =
     props.status?.kind === "connection" ||
+    props.status?.kind === "extension" ||
     props.status?.kind === "child-input" ||
     props.status?.kind === "background";
   const capsuleInteractive =
-    statusInteractive || action !== null || hasQueue || hasAgents || hasPreview;
+    statusInteractive ||
+    extensionStatus !== null ||
+    action !== null ||
+    hasQueue ||
+    hasAgents ||
+    hasPreview;
   const statusContent =
     props.status !== null ? (
       <View
@@ -181,7 +196,9 @@
         >
           <FloatingStatusLabel
             key={
+              props.status.kind === "working" ||
+              props.status.kind === "compacting" ||
+              props.status.kind === "extension"
-              props.status.kind === "working" || props.status.kind === "compacting"
                 ? props.status.kind
                 : `${props.status.kind}:${props.status.label}`
             }
@@ -195,6 +212,21 @@
   const capsuleContent = (
     <View className="flex-row items-center">
       {statusContent}
+      {extensionStatus !== null ? (
+        <Pressable
+          accessibilityRole="button"
+          accessibilityLabel={extensionStatus.label}
+          accessibilityHint="Shows the full Pi status"
+          onPress={extensionStatus.onPress}
+          onLayout={(event) => setExtensionWidth(settleWidth(event.nativeEvent.layout.width))}
+          style={{ maxWidth: availableLabelWidth * 0.5 }}
+          className="h-11 flex-row items-center bg-black px-3 active:opacity-70"
+        >
+          <Text className="shrink font-t3-medium text-xs text-white" numberOfLines={1}>
+            {extensionStatus.label}
+          </Text>
+        </Pressable>
+      ) : null}
       {action !== null ? (
         <Pressable
           accessibilityRole="button"
@@ -269,10 +301,10 @@
       className="absolute left-0 right-0 z-20 items-center"
       style={[{ top: -CONTROL_OVERLAY_OFFSET }, liftStyle]}
       onLayout={(event) => setOverlayWidth(event.nativeEvent.layout.width)}
+      entering={useGlass || extensionOnly ? undefined : CONTROL_ENTERING}
-      entering={NATIVE_LIQUID_GLASS_SUPPORTED ? undefined : CONTROL_ENTERING}
-      exiting={NATIVE_LIQUID_GLASS_SUPPORTED ? undefined : CONTROL_EXITING}
+      exiting={useGlass || extensionOnly ? undefined : CONTROL_EXITING}
     >
+      {hasCapsule && useGlass ? (
-      {hasCapsule && NATIVE_LIQUID_GLASS_SUPPORTED ? (
         <UniwindGlassContainer
           spacing={GLASS_MERGE_SPACING}
           pointerEvents="box-none"
@@ -308,7 +340,7 @@
         <View pointerEvents="box-none" className="flex-row items-center gap-4">
           <Animated.View
             pointerEvents={capsuleInteractive ? "box-none" : "none"}
-            className="h-11 items-center justify-center overflow-hidden rounded-full border border-border bg-glass-fallback shadow-md shadow-black/10"
+            className={`h-11 items-center justify-center overflow-hidden rounded-full border border-border shadow-md shadow-black/10 ${extensionOnly ? "bg-black" : "bg-glass-fallback"}`}
             style={capsuleStyle}
           >
             {capsuleContent}
@@ -370,6 +402,23 @@
   readonly status: FloatingWorkingStatus;
   readonly onLayout: (event: LayoutChangeEvent) => void;
 }) {
+  if (props.status.kind === "extension") {
+    return (
+      <StatusLabelRow
+        accessibilityLabel={props.status.label}
+        accessibilityRole="button"
+        onPress={props.status.onPress}
+        onLayout={props.onLayout}
+        className="gap-1.5"
+        animated={false}
+      >
+        <Text className="shrink font-t3-medium text-xs text-white" numberOfLines={1}>
+          {props.status.label}
+        </Text>
+        <SymbolView name="chevron.down" size={10} tintColor="#fff" />
+      </StatusLabelRow>
+    );
+  }
   if (props.status.kind === "syncing") {
     return (
       <StatusLabelRow
@@ -518,6 +567,7 @@
 }
 
 function StatusLabelRow(props: {
+  readonly animated?: boolean;
   readonly accessibilityLabel?: string;
   readonly accessibilityRole?: "button";
   readonly className?: string;
@@ -529,8 +579,8 @@
   return (
     <Animated.View
       className="absolute max-w-full"
-      entering={LABEL_ENTERING}
-      exiting={LABEL_EXITING}
+      entering={props.animated === false ? undefined : LABEL_ENTERING}
+      exiting={props.animated === false ? undefined : LABEL_EXITING}
       onLayout={props.onLayout}
     >
       {props.onPress ? (
```

## apps/mobile/src/features/threads/floating-working-status.ts

Status: modified; +1; −0

```diff
@@ -32,6 +32,7 @@
       readonly action: FloatingStatusAction;
     }
   | { readonly kind: "goal"; readonly label: string; readonly accessibilityLabel: string }
+  | { readonly kind: "extension"; readonly label: string; readonly onPress: () => void }
   | { readonly kind: "preparing"; readonly label: string }
   | {
       readonly kind: "connection";
```

## apps/mobile/src/state/use-thread-composer-state.ts

Status: modified; +4; −0

```diff
@@ -8,6 +8,7 @@
 import { resolvePendingHandoff } from "@t3tools/client-runtime/handoff";
 import {
   deriveProviderSubagentStatus,
+  deriveProviderExtensionStatuses,
   deriveRunlessWorkStartedAt,
   deriveThreadActivityRun,
   deriveThreadRuntime,
@@ -887,6 +888,9 @@
     dispatchingQueuedMessageId,
     activeWorkStartedAt,
     runlessWorkActive,
+    extensionStatuses: deriveProviderExtensionStatuses(
+      selectedThreadProjection?.projection ?? null,
+    ),
     providerSubagentStatus,
     isCompacting,
     composerDraftKey,
```

## apps/server/src/orchestration-v2/ProjectionRecovery.test.ts

Status: modified; +98; −0

```diff
@@ -20,6 +20,7 @@
 import * as DateTime from "effect/DateTime";
 import * as Effect from "effect/Effect";
 import * as Layer from "effect/Layer";
+import * as Stream from "effect/Stream";
 import * as SqlClient from "effect/sql/SqlClient";
 
 import * as SqlitePersistence from "../persistence/Sqlite.ts";
@@ -260,6 +261,103 @@
   }).pipe(Effect.provide(layerRecoveryTest)),
 );
 
+it.effect.each(["attached", "detached", "replaced"] as const)(
+  "recovers attached sessions without restoring detached errors, session=%s",
+  (state) =>
+    Effect.gen(function* () {
+      const projections = yield* ProjectionStore.ProjectionStoreV2;
+      const recovery = yield* ProviderRuntimeRecovery.ProviderRuntimeRecoveryService;
+      const now = yield* DateTime.now;
+      const threadId = yield* createThread("failed-status-session");
+      const providerSessionId = ProviderSessionId.make("session:recovery:failed-status");
+      const session = {
+        id: providerSessionId,
+        driver,
+        providerInstanceId,
+        status: "error" as const,
+        cwd: "/workspace",
+        model: modelSelection.model,
+        capabilities: CodexProviderCapabilitiesV2,
+        extensionStatuses: [{ key: "watch", text: "Watching" }],
+        createdAt: now,
+        updatedAt: now,
+        lastError: "Rate limited",
+      };
+      yield* projections.apply({
+        id: EventId.make("event:recovery:failed-status:attached"),
+        type: "provider-session.attached",
+        threadId,
+        driver,
+        providerInstanceId,
+        occurredAt: now,
+        payload: session,
+      });
+      if (state !== "attached") {
+        yield* projections.apply({
+          id: EventId.make("event:recovery:failed-status:detached"),
+          type: "provider-session.detached",
+          threadId,
+          driver,
+          providerInstanceId,
+          occurredAt: now,
+          payload: { providerSessionId, detachedAt: now, reason: "Thread settled." },
+        });
+      }
+      const replacementSessionId = ProviderSessionId.make("session:recovery:replacement");
+      if (state === "replaced") {
+        yield* projections.apply({
+          id: EventId.make("event:recovery:replacement:attached"),
+          type: "provider-session.attached",
+          threadId,
+          driver,
+          providerInstanceId,
+          occurredAt: now,
+          payload: {
+            ...session,
+            id: replacementSessionId,
+            status: "ready",
+            extensionStatuses: [],
+            lastError: null,
+          },
+        });
+        yield* createRun(threadId, "completed");
+      }
+      assert.deepEqual(
+        yield* projections.getRecoveryThreadIds("runtime"),
+        state === "detached" ? [] : [threadId],
+      );
+      yield* recovery.reconcile("startup");
+      const eventStore = yield* EventStore.EventStoreV2;
+      const stored = yield* eventStore.read({ threadId }).pipe(Stream.runCollect);
+      const sessionUpdates = stored.flatMap(({ event }) =>
+        event.type === "provider-session.updated"
+          ? [
+              {
+                id: event.payload.id,
+                status: event.payload.status,
+                lastError: event.payload.lastError,
+                extensionStatuses: event.payload.extensionStatuses,
+              },
+            ]
+          : [],
+      );
+      assert.deepEqual(
+        sessionUpdates,
+        state === "detached"
+          ? []
+          : [
+              {
+                id: state === "attached" ? providerSessionId : replacementSessionId,
+                status: state === "attached" ? "error" : "stopped",
+                lastError: state === "attached" ? "Rate limited" : null,
+                extensionStatuses: [],
+              },
+            ],
+      );
+      assert.deepEqual(yield* projections.getRecoveryThreadIds("runtime"), []);
+    }).pipe(Effect.provide(layerRecoveryTest)),
+);
+
 it.effect("selects unfinished recovery work without reading settled thread histories", () =>
   Effect.gen(function* () {
     const projections = yield* ProjectionStore.ProjectionStoreV2;
```

## apps/server/src/orchestration-v2/ProjectionStore.ts

Status: modified; +9; −1

```diff
@@ -653,7 +653,9 @@
             ["pending", "running", "waiting"].includes(subagent.status),
         ) ||
         projection.providerSessions.some(
+          (session) =>
+            (session.status !== "stopped" && session.status !== "error") ||
+            (session.status === "error" && (session.extensionStatuses?.length ?? 0) > 0),
-          (session) => session.status !== "stopped" && session.status !== "error",
         ) ||
         projection.providerThreads.some(
           (thread) =>
@@ -3963,6 +3965,9 @@
                 CROSS JOIN orchestration_v2_projection_provider_session_bindings AS bindings
                   ON bindings.provider_session_id = sessions.provider_session_id
                 WHERE sessions.status NOT IN ('stopped', 'error')
+                  OR (sessions.status = 'error' AND CASE WHEN json_valid(sessions.payload_json)
+                    THEN json_array_length(sessions.payload_json, '$.extensionStatuses') > 0
+                    ELSE 0 END)
                 UNION
                 -- A detach drops the binding but keeps the session's status.
                 SELECT sessions.thread_id
@@ -4446,6 +4451,9 @@
                   )
                   AND (
                     session.status NOT IN ('stopped', 'error')
+                    OR (session.status = 'error' AND CASE WHEN json_valid(session.payload_json)
+                      THEN json_array_length(session.payload_json, '$.extensionStatuses') > 0
+                      ELSE 0 END)
                     OR session.provider_session_id IN (
                       SELECT provider_thread.provider_session_id
                       FROM orchestration_v2_projection_provider_threads AS provider_thread
```

## apps/server/src/orchestration-v2/ProviderRuntimeRecoveryService.test.ts

Status: modified; +8; −0

```diff
@@ -547,6 +547,7 @@
           driver: ProviderDriverKind.make("codex"),
           providerInstanceId: ProviderInstanceId.make("codex"),
           status: "ready",
+          extensionStatuses: [{ key: "watch", text: "Watching" }],
         },
       ],
       providerThreads: [
@@ -650,6 +651,13 @@
       );
       const messageEvent = events.find((event) => event.type === "message.updated");
       assert.isFalse(messageEvent?.type === "message.updated" && messageEvent.payload.streaming);
+      const stoppedSession = events.find((event) => event.type === "provider-session.updated");
+      assert.deepEqual(
+        stoppedSession?.type === "provider-session.updated"
+          ? stoppedSession.payload.extensionStatuses
+          : undefined,
+        [],
+      );
     }).pipe(Effect.provide(layer));
   },
 );
```

## apps/server/src/orchestration-v2/ProviderRuntimeRecoveryService.ts

Status: modified; +10; −2

```diff
@@ -588,7 +588,9 @@
         });
       }
       for (const session of projection.providerSessions.filter(
+        (candidate) =>
+          (candidate.status !== "stopped" && candidate.status !== "error") ||
-        (candidate) => candidate.status !== "stopped" && candidate.status !== "error",
+          (candidate.status === "error" && (candidate.extensionStatuses?.length ?? 0) > 0),
       )) {
         events.push({
           id: yield* allocateEventId(),
@@ -597,7 +599,13 @@
           driver: session.driver,
           providerInstanceId: session.providerInstanceId,
           occurredAt: now,
+          payload: {
-          payload: { ...session, status: "stopped", updatedAt: now, lastError: null },
+            ...session,
+            status: session.status === "error" ? "error" : "stopped",
+            ...(session.extensionStatuses === undefined ? {} : { extensionStatuses: [] }),
+            updatedAt: now,
+            lastError: session.status === "error" ? session.lastError : null,
+          },
         });
       }
       for (const [providerThreadId, work] of cancelledBackgroundWork) {
```

## apps/server/src/orchestration-v2/ProviderSessionManager.test.ts

Status: modified; +261; −7

```diff
@@ -321,6 +321,7 @@
   options: {
     readonly failEventStream?: boolean;
     readonly capabilities?: OrchestrationV2ProviderCapabilities;
+    readonly extensionStatuses?: OrchestrationV2ProviderSession["extensionStatuses"];
     readonly mcpConfigs?: Ref.Ref<
       ReadonlyArray<McpProviderSession.McpProviderSessionConfig | undefined>
     >;
@@ -378,11 +379,16 @@
         }
         const now = yield* DateTime.now;
         const events = yield* Queue.unbounded<ProviderAdapter.ProviderAdapterV2Event, Cause.Done>();
+        let session: OrchestrationV2ProviderSession = {
+          ...makeProviderSession({
+            providerSessionId: input.providerSessionId,
+            now,
+            ...(options.capabilities === undefined ? {} : { capabilities: options.capabilities }),
+          }),
+          ...(options.extensionStatuses === undefined
+            ? {}
-        const session = makeProviderSession({
-          providerSessionId: input.providerSessionId,
-          now,
-          ...(options.capabilities === undefined ? {} : { capabilities: options.capabilities }),
-        });
+            : { extensionStatuses: options.extensionStatuses }),
+        };
         yield* Ref.update(state, (current) => {
           const eventQueues = new Map(current.eventQueues);
           eventQueues.set(String(input.providerSessionId), events);
@@ -401,7 +407,9 @@
           instanceId: ProviderInstanceId.make("codex"),
           driver: CODEX_DRIVER,
           providerSessionId: input.providerSessionId,
-          providerSession: session,
+          get providerSession() {
+            return session;
+          },
           events: options.failEventStream
             ? Stream.fail(
                 new ProviderAdapter.ProviderAdapterEventStreamError({
@@ -410,7 +418,13 @@
                   cause: "process exited",
                 }),
               )
+            : Stream.fromQueue(events).pipe(
+                Stream.tap((event) =>
+                  Effect.sync(() => {
+                    if (event.type === "provider_session.updated") session = event.providerSession;
+                  }),
+                ),
-            : Stream.fromQueue(events),
+              ),
           ...(options.backgroundTasksEndWithProcess === false
             ? {}
             : { backgroundTasksEndWithProcess: true as const }),
@@ -462,6 +476,7 @@
   readonly maxIdlePinMs?: number;
   readonly failEventStream?: boolean;
   readonly capabilities?: OrchestrationV2ProviderCapabilities;
+  readonly extensionStatuses?: OrchestrationV2ProviderSession["extensionStatuses"];
   readonly mcpConfigs?: Ref.Ref<
     ReadonlyArray<McpProviderSession.McpProviderSessionConfig | undefined>
   >;
@@ -502,6 +517,9 @@
     makeProviderAdapter(input.state, {
       failEventStream: input.failEventStream ?? false,
       ...(input.capabilities === undefined ? {} : { capabilities: input.capabilities }),
+      ...(input.extensionStatuses === undefined
+        ? {}
+        : { extensionStatuses: input.extensionStatuses }),
       ...(input.mcpConfigs === undefined ? {} : { mcpConfigs: input.mcpConfigs }),
       ...(input.beforeOpen === undefined ? {} : { beforeOpen: input.beforeOpen }),
       ...(input.hasPendingBackgroundWork === undefined
@@ -1390,6 +1408,242 @@
   }),
 );
 
+it.effect(
+  "ProviderSessionManagerV2 projects idle extension statuses and rejects a retired runtime's updates",
+  () =>
+    Effect.gen(function* () {
+      const state = yield* Ref.make(emptyState);
+      yield* Effect.gen(function* () {
+        const eventSink = yield* EventSink.EventSinkV2;
+        const idAllocator = yield* IdAllocator.IdAllocatorV2;
+        const manager = yield* ProviderSessionManager.ProviderSessionManagerV2;
+        const projectionStore = yield* ProjectionStore.ProjectionStoreV2;
+        const threadId = ThreadId.make("thread-extension-status");
+        const providerSessionId = yield* idAllocator.allocate.providerSession({
+          providerInstanceId: modelSelection.instanceId,
+          threadId,
+        });
+        yield* eventSink.write({
+          events: [
+            yield* makeThreadCreatedEvent({ idAllocator, threadId, now: yield* DateTime.now }),
+          ],
+        });
+        const runtime = yield* manager.open({
+          threadId,
+          providerSessionId,
+          modelSelection,
+          runtimePolicy,
+        });
+        const firstClient = yield* runtime.subscribeEvents!;
+        const secondClient = yield* runtime.subscribeEvents!;
+        const oldQueue = (yield* Ref.get(state)).eventQueues.get(String(providerSessionId));
+        assert.isDefined(oldQueue);
+        const statuses = [{ key: "watch", text: "Watching 4 files" }];
+        yield* Queue.offer(oldQueue!, {
+          type: "provider_session.updated",
+          driver: CODEX_DRIVER,
+          providerSession: { ...runtime.providerSession, extensionStatuses: statuses },
+        });
+        yield* Effect.all([
+          firstClient.events.pipe(Stream.runHead),
+          secondClient.events.pipe(Stream.runHead),
+        ]);
+        assert.deepEqual(
+          (yield* projectionStore.getThreadProjection(threadId)).providerSessions.at(-1)
+            ?.extensionStatuses,
+          statuses,
+        );
+        const reused = yield* manager.open({
+          threadId,
+          providerSessionId,
+          modelSelection,
+          runtimePolicy,
+        });
+        assert.deepEqual(reused.providerSession.extensionStatuses, statuses);
+        yield* eventSink.write({
+          events: [
+            {
+              id: yield* idAllocator.allocate.event({ threadId, providerSessionId }),
+              type: "provider-session.updated",
+              threadId,
+              driver: CODEX_DRIVER,
+              providerInstanceId: modelSelection.instanceId,
+              occurredAt: yield* DateTime.now,
+              payload: reused.providerSession,
+            },
+          ],
+        });
+        assert.deepEqual(
+          (yield* projectionStore.getThreadProjection(threadId)).providerSessions.at(-1)
+            ?.extensionStatuses,
+          statuses,
+        );
+        yield* manager.close(providerSessionId);
+        assert.deepEqual(
+          (yield* projectionStore.getThreadProjection(threadId)).providerSessions.at(-1)
+            ?.extensionStatuses,
+          [],
+        );
+        const replacement = yield* manager.open({
+          threadId,
+          providerSessionId,
+          modelSelection,
+          runtimePolicy,
+        });
+        const replacementClient = yield* replacement.subscribeEvents!;
+        const newQueue = (yield* Ref.get(state)).eventQueues.get(String(providerSessionId));
+        assert.isDefined(newQueue);
+        yield* Queue.offer(oldQueue!, {
+          type: "provider_session.updated",
+          driver: CODEX_DRIVER,
+          providerSession: {
+            ...runtime.providerSession,
+            extensionStatuses: [{ key: "stale", text: "Old session" }],
+          },
+        });
+        yield* Queue.offer(newQueue!, {
+          type: "provider_session.updated",
+          driver: CODEX_DRIVER,
+          providerSession: {
+            ...replacement.providerSession,
+            extensionStatuses: [{ key: "new", text: "New session" }],
+          },
+        });
+        yield* replacementClient.events.pipe(Stream.runHead);
+        assert.deepEqual(
+          (yield* projectionStore.getThreadProjection(threadId)).providerSessions.at(-1)
+            ?.extensionStatuses,
+          [{ key: "new", text: "New session" }],
+        );
+        assert.lengthOf((yield* projectionStore.getThreadProjection(threadId)).messages, 0);
+        assert.lengthOf((yield* projectionStore.getThreadProjection(threadId)).turnItems, 0);
+      }).pipe(
+        Effect.provide(
+          layerTest({
+            state,
+            idleTimeoutMs: 60_000,
+            extensionStatuses: [{ key: "initial", text: "Watching" }],
+          }),
+        ),
+      );
+    }),
+);
+
+it.effect.each(["session metadata", "session status changes", "provider work"] as const)(
+  "ProviderSessionManagerV2 expires idle sessions according to %s",
+  (activity) =>
+    Effect.gen(function* () {
+      const state = yield* Ref.make(emptyState);
+      yield* Effect.gen(function* () {
+        const eventSink = yield* EventSink.EventSinkV2;
+        const idAllocator = yield* IdAllocator.IdAllocatorV2;
+        const manager = yield* ProviderSessionManager.ProviderSessionManagerV2;
+        const projectionStore = yield* ProjectionStore.ProjectionStoreV2;
+        const threadId = ThreadId.make("thread-session-metadata-idle");
+        const providerSessionId = yield* idAllocator.allocate.providerSession({
+          providerInstanceId: modelSelection.instanceId,
+          threadId,
+        });
+        yield* eventSink.write({
+          events: [
+            yield* makeThreadCreatedEvent({ idAllocator, threadId, now: yield* DateTime.now }),
+          ],
+        });
+        const runtime = yield* manager.open({
+          threadId,
+          providerSessionId,
+          modelSelection,
+          runtimePolicy,
+        });
+        const subscriber = yield* runtime.subscribeEvents!;
+        const observed = yield* Queue.unbounded<ProviderAdapter.ProviderAdapterV2Event>();
+        yield* subscriber.events.pipe(
+          Stream.runForEach((event) => Queue.offer(observed, event)),
+          Effect.ignore,
+          Effect.forkScoped,
+        );
+        const queue = (yield* Ref.get(state)).eventQueues.get(String(providerSessionId));
+        assert.isDefined(queue);
+        const waitingSession = { ...runtime.providerSession, status: "waiting" as const };
+        yield* Queue.offer(queue!, {
+          type: "provider_session.updated",
+          driver: CODEX_DRIVER,
+          providerSession: waitingSession,
+        });
+        yield* Queue.take(observed);
+
+        for (const text of ["Watching 1 file", "Watching 2 files"]) {
+          yield* TestClock.adjust("300 millis");
+          yield* Queue.offer(queue!, {
+            type: "provider_session.updated",
+            driver: CODEX_DRIVER,
+            providerSession: {
+              ...waitingSession,
+              extensionStatuses: [{ key: "watch", text }],
+            },
+          });
+          yield* Queue.take(observed);
+        }
+        yield* TestClock.adjust("300 millis");
+        const statuses = [{ key: "watch", text: "Watching 3 files" }];
+        yield* Queue.offer(
+          queue!,
+          activity === "provider work"
+            ? {
+                type: "provider_thread.updated",
+                driver: CODEX_DRIVER,
+                providerThread: makeProviderThread({
+                  idAllocator,
+                  threadId,
+                  providerSessionId,
+                  now: yield* DateTime.now,
+                }),
+              }
+            : {
+                type: "provider_session.updated",
+                driver: CODEX_DRIVER,
+                providerSession: {
+                  ...waitingSession,
+                  status: activity === "session status changes" ? "ready" : "waiting",
+                  extensionStatuses: statuses,
+                },
+              },
+        );
+        yield* Queue.take(observed);
+        if (activity !== "provider work") {
+          assert.deepEqual(
+            (yield* projectionStore.getThreadProjection(threadId)).providerSessions.at(-1)
+              ?.extensionStatuses,
+            statuses,
+          );
+        }
+        const afterSequence = yield* eventSink.latestSequence({ threadId });
+        yield* TestClock.adjust("100 millis");
+        assert.equal(yield* manager.isLive(providerSessionId), activity !== "session metadata");
+        if (activity !== "session metadata") {
+          yield* TestClock.adjust("900 millis");
+        }
+        assert.isTrue(Option.isNone(yield* manager.get(providerSessionId)));
+        yield* eventSink
+          .stream({ threadId, afterSequence, eventType: "provider-session.updated" })
+          .pipe(
+            Stream.filter(
+              (stored) =>
+                stored.event.type === "provider-session.updated" &&
+                stored.event.payload.status === "stopped",
+            ),
+            Stream.runHead,
+          );
+        assert.deepEqual(
+          (yield* projectionStore.getThreadProjection(threadId)).providerSessions.at(-1)
+            ?.extensionStatuses,
+          [],
+        );
+        assert.equal((yield* Ref.get(state)).closeCount, 1);
+      }).pipe(Effect.provide(layerTest({ state, idleTimeoutMs: 1_000 })));
+    }),
+);
+
 it.effect("ProviderSessionManagerV2 interrupts event subscriptions on server shutdown", () =>
   Effect.gen(function* () {
     const state = yield* Ref.make(emptyState);
```

## apps/server/src/orchestration-v2/ProviderSessionManager.ts

Status: modified; +19; −7

```diff
@@ -615,6 +615,9 @@
         const payload: OrchestrationV2ProviderSession = {
           ...input.entry.runtime.providerSession,
           status: releaseStatusFor(input.reason),
+          ...(input.entry.runtime.providerSession.extensionStatuses === undefined
+            ? {}
+            : { extensionStatuses: [] }),
           updatedAt: now,
           lastError:
             input.reason === "runtime_error" ? (input.detail ?? "Provider runtime failed.") : null,
@@ -1203,8 +1206,8 @@
 
       // Provider activity restarts the pin budget, so background work that keeps
       // reporting progress (a long Claude workflow) is never cut off; only work
+      // that went silent for maxIdlePinMs is released. Progress events can be
-      // that went silent for maxIdlePinMs is released. This runs for every
+      // frequent, so keep this untraced.
-      // provider event, so keep it untraced.
       const scheduleIdleReleaseInternal = Effect.fnUntraced(function* (
         providerSessionId: ProviderSessionId,
         options?: { readonly providerActivity?: boolean },
@@ -1732,6 +1735,10 @@
           });
         return {
           ...runtime,
+          // Pi replaces its session snapshot as metadata changes.
+          get providerSession() {
+            return runtime.providerSession;
+          },
           subscribeEvents,
           events: Stream.unwrap(
             subscribeEvents.pipe(Effect.map((subscription) => subscription.events)),
@@ -1928,6 +1935,7 @@
 
       const startEventPump = (entry: LiveSessionEntry) => {
         let stoppedByProvider = false;
+        let lastSessionStatus = entry.runtime.providerSession.status;
         return entry.runtime.events.pipe(
           Stream.runForEach((event) => {
             if (shutdownSignal.received) return Effect.void;
@@ -1946,11 +1954,13 @@
                     itemId: event.turnItem.id,
                   })
                 : Effect.void;
-            if (
+            // Metadata can change while idle without extending the runtime's lifetime.
+            const metadataOnly =
               event.type === "provider_session.updated" &&
+              event.providerSession.status === lastSessionStatus;
+            if (event.type === "provider_session.updated") {
+              lastSessionStatus = event.providerSession.status;
+              if (lastSessionStatus === "stopped") stoppedByProvider = true;
-              event.providerSession.status === "stopped"
-            ) {
-              stoppedByProvider = true;
             }
             return observeActivity(
               entry.runtime.providerSessionId,
@@ -1960,7 +1970,9 @@
                     event.providerThreadId,
                     event.runOrdinal,
                   )
-                : touchActivity(entry.runtime.providerSessionId, { providerActivity: true }),
+                : metadataOnly
+                  ? Effect.void
+                  : touchActivity(entry.runtime.providerSessionId, { providerActivity: true }),
             ).pipe(
               Effect.andThen(
                 event.type === "provider_session.updated"
```

## apps/server/src/storageCleanup.test.ts

Status: modified; +107; −1

```diff
@@ -6,6 +6,7 @@
 import * as FileSystem from "effect/FileSystem";
 import * as Layer from "effect/Layer";
 import * as Option from "effect/Option";
+import * as Queue from "effect/Queue";
 import type * as Path from "effect/Path";
 import type * as Scope from "effect/Scope";
 import * as Stream from "effect/Stream";
@@ -33,7 +34,10 @@
 import { describe, expect, it } from "@effect/vitest";
 import {
   ProjectId,
+  EventId,
+  ProviderDriverKind,
   ProviderInstanceId,
+  ProviderSessionId,
   RunId,
   RuntimeRequestId,
   ThreadId,
@@ -267,6 +271,7 @@
     let editBeforeRemoval = false;
     let openSessionCwd: string | null = null;
     let sessionLive = true;
+    let sessionLiveChecks = 0;
     let fullStatusCalls = 0;
     let snapshotReads = 0;
     let events: Stream.Stream<OrchestrationV2DomainEvent> = Stream.empty;
@@ -334,7 +339,11 @@
           Effect.sync(() => threads.find((thread) => thread.id === threadId) ?? null),
       } as unknown as ProjectionStore.ProjectionStoreV2["Service"]),
       Effect.provideService(ProviderSessionManager.ProviderSessionManagerV2, {
+        isLive: () =>
-        isLive: () => Effect.sync(() => sessionLive),
+          Effect.sync(() => {
+            sessionLiveChecks++;
+            return sessionLive;
+          }),
       } as unknown as ProviderSessionManager.ProviderSessionManagerV2["Service"]),
       Effect.provideService(Orchestrator.OrchestratorV2, {
         streamDomainEvents: Stream.suspend(() => events),
@@ -429,6 +438,7 @@
       },
       fullStatusCalls: () => fullStatusCalls,
       snapshotReads: () => snapshotReads,
+      sessionLiveChecks: () => sessionLiveChecks,
       activate: (domainEvents: Stream.Stream<OrchestrationV2DomainEvent>) => {
         events = domainEvents;
         return Deferred.succeed(activated, undefined);
@@ -1157,6 +1167,102 @@
 });
 
 describe("storage cleanup reports and local file policies", () => {
+  it.effect.each([true, false])(
+    "sweeps on runtime retirement rather than live error metadata, worktreeOnDelete=%s",
+    (worktreeOnDelete) =>
+      runCleanupTest(
+        Effect.gen(function* () {
+          const fixture = yield* cleanupFixture({
+            ...DEFAULT_SERVER_SETTINGS.storageCleanup,
+            worktreeAfterDays: 8,
+            worktreeOnDelete,
+          });
+          fixture.setThreads([]);
+          const events = yield* Queue.unbounded<{
+            readonly event: OrchestrationV2DomainEvent;
+            readonly processed: Deferred.Deferred<void>;
+          }>();
+          yield* fixture.activate(
+            Stream.fromQueue(events).pipe(
+              Stream.flatMap(({ event, processed }) =>
+                Stream.succeed(event).pipe(
+                  Stream.concat(
+                    // The next pull acknowledges that the listener handled this event.
+                    Stream.fromEffect(Deferred.succeed(processed, undefined)).pipe(Stream.drain),
+                  ),
+                ),
+              ),
+            ),
+          );
+          yield* fixture.service.reports.pipe(
+            Stream.filter((report) => report !== null),
+            Stream.take(1),
+            Stream.runDrain,
+          );
+          yield* fixture.service.drain;
+          const startupReads = fixture.snapshotReads();
+          const publish = Effect.fn("StorageCleanupTest.publish")(function* (
+            event: OrchestrationV2DomainEvent,
+          ) {
+            const processed = yield* Deferred.make<void>();
+            yield* Queue.offer(events, { event, processed });
+            yield* Deferred.await(processed);
+            yield* fixture.service.drain;
+          });
+          const sessionUpdated = (
+            status: "ready" | "error" | "stopped",
+            text: string,
+          ): OrchestrationV2DomainEvent => ({
+            id: EventId.make(`event:session:${status}:${text}`),
+            type: "provider-session.updated",
+            threadId: ThreadId.make("thread-1"),
+            occurredAt: at(0),
+            payload: {
+              id: ProviderSessionId.make("session-1"),
+              driver: ProviderDriverKind.make("pi"),
+              providerInstanceId: ProviderInstanceId.make("pi"),
+              status,
+              cwd: fixture.worktree,
+              model: null,
+              capabilities: CodexProviderCapabilitiesV2,
+              extensionStatuses: text === "" ? [] : [{ key: "watch", text }],
+              createdAt: at(-DAY_MS),
+              updatedAt: at(0),
+              lastError: status === "error" ? "Rate limited" : null,
+            },
+          });
+          yield* publish(sessionUpdated("ready", "Watching"));
+          expect(fixture.snapshotReads()).toBe(startupReads);
+          expect(fixture.sessionLiveChecks()).toBe(0);
+          for (let index = 0; index < 5; index++) {
+            yield* publish(sessionUpdated("error", `Watching ${index} files`));
+            expect(fixture.snapshotReads()).toBe(startupReads);
+          }
+          expect(fixture.sessionLiveChecks()).toBe(worktreeOnDelete ? 5 : 0);
+
+          // Release removes the live entry before publishing its final error update.
+          fixture.setSessionLive(false);
+          yield* publish(sessionUpdated("error", ""));
+          expect(fixture.snapshotReads()).toBe(startupReads + Number(worktreeOnDelete));
+          expect(fixture.sessionLiveChecks()).toBe(worktreeOnDelete ? 6 : 0);
+
+          yield* publish(sessionUpdated("stopped", ""));
+          expect(fixture.snapshotReads()).toBe(startupReads + 2 * Number(worktreeOnDelete));
+          expect(fixture.sessionLiveChecks()).toBe(worktreeOnDelete ? 7 : 0);
+
+          fixture.setSessionLive(true);
+          yield* publish({
+            id: EventId.make("event:thread:deleted"),
+            type: "thread.deleted",
+            threadId: ThreadId.make("thread-1"),
+            occurredAt: at(0),
+            payload: { ...shell({ deletedAt: at(0) }), lastVisitedAt: null },
+          });
+          expect(fixture.snapshotReads()).toBe(startupReads + 3 * Number(worktreeOnDelete));
+          expect(fixture.sessionLiveChecks()).toBe(worktreeOnDelete ? 7 : 0);
+        }),
+      ),
+  );
   it.effect("automatically removes an eligible archived thread checkout", () =>
     runCleanupTest(
       Effect.gen(function* () {
```

## apps/server/src/storageCleanup.ts

Status: modified; +17; −8

```diff
@@ -1260,14 +1260,23 @@
       }),
     );
     yield* forkParked(
+      Stream.runForEach(events, (event) => {
+        if (
+          !(
+            event.type === "thread.deleted" ||
+            (event.type === "provider-session.updated" &&
+              (event.payload.status === "stopped" || event.payload.status === "error"))
+          ) ||
+          !anyWorktreePolicy(lastSettings, (rules) => rules.worktreeOnDelete)
+        )
+          return Effect.void;
+        // A turn error can leave the runtime alive and emitting metadata.
+        return event.type === "provider-session.updated"
+          ? providerSessions
-      Stream.runForEach(events, (event) =>
-        (event.type === "thread.deleted" ||
-          (event.type === "provider-session.updated" &&
-            (event.payload.status === "stopped" || event.payload.status === "error"))) &&
-        anyWorktreePolicy(lastSettings, (rules) => rules.worktreeOnDelete)
-          ? requestSweep
-          : Effect.void,
+              .isLive(event.payload.id)
-      ).pipe(
+              .pipe(Effect.flatMap((live) => (live ? Effect.void : requestSweep)))
+          : requestSweep;
+      }).pipe(
         Effect.catchCause((cause) =>
           Effect.logWarning("Storage cleanup event stream failed", { cause }),
         ),
```

## apps/web/src/components/ChatView.tsx

Status: modified; +8; −4

```diff
@@ -71,6 +71,7 @@
 } from "@t3tools/contracts";
 import {
   deriveProviderSubagentStatus,
+  deriveProviderExtensionStatuses,
   deriveReportedModelSelection,
   formatModelSelectionEffort,
   deriveRunlessWorkStartedAt,
@@ -346,7 +347,7 @@
 import { ChatTerminalDrawers } from "./chat/ChatTerminalDrawers";
 import { ChatRightPanel } from "./chat/ChatRightPanel";
 import { useDraftEnvironmentRouting } from "./chat/useDraftEnvironmentRouting";
+import { branchMismatchBannerItem, extensionStatusBannerItem } from "./chat/ThreadActivityBanners";
-import { branchMismatchBannerItem } from "./chat/ThreadActivityBanners";
 import { useTimelineFollow } from "./chat/useTimelineFollow";
 import {
   type EnvironmentUnavailableState,
@@ -2602,9 +2603,12 @@
       : null;
   const composerBannerItems = (() => {
     const limitRecoveryItems = limitRecoveryBanner === null ? [] : [limitRecoveryBanner];
-    const backgroundWorkItems = [childInputBanner, goalBanner, backgroundWorkBanner].filter(
-      (item) => item !== null,
-    );
+    const backgroundWorkItems = [
+      childInputBanner,
+      goalBanner,
+      backgroundWorkBanner,
+      extensionStatusBannerItem(deriveProviderExtensionStatuses(serverProjection)),
+    ].filter((item) => item !== null);
     const usageLimitsItems = usageLimitsBanner === null ? [] : [usageLimitsBanner];
     const projectCloneItems = projectCloneBannerItem === null ? [] : [projectCloneBannerItem];
     if (!localCheckoutBranchMismatch || !showBranchMismatchBanner || !activeBranchMismatchKey) {
```

## apps/web/src/components/chat/ComposerBanner.tsx

Status: modified; +6; −1

```diff
@@ -7,7 +7,7 @@
 import { Button } from "../ui/button";
 import { ScrollArea } from "../ui/scroll-area";
 
-export type ComposerBannerVariant = "default" | "error" | "info" | "success" | "warning";
+export type ComposerBannerVariant = "default" | "plain" | "error" | "info" | "success" | "warning";
 
 const surfaceColors = cn(
   "[--chat-composer-attached-surface:var(--chat-composer-glass-surface,var(--card))]",
@@ -23,8 +23,12 @@
   "dark:[html[data-theme-id=t3-chat]_&]:[--chat-composer-attached-outline:#241e28]",
 );
 
+const plainSurfaceColors =
+  "text-white [--glass-opacity:100%] [--glass-blur:0px] [--chat-composer-attached-surface:#000] dark:[--chat-composer-attached-surface:#000] [html[data-theme-id]_&]:[--chat-composer-attached-surface:#000]";
+
 const variantColors: Record<ComposerBannerVariant, string> = {
   default: neutralOutline,
+  plain: cn(neutralOutline, plainSurfaceColors),
   error:
     "[--chat-composer-attached-outline:color-mix(in_srgb,var(--error)_32%,transparent)] [--chat-composer-attached-tint:color-mix(in_srgb,var(--error)_8%,transparent)]",
   info: neutralOutline,
@@ -70,6 +74,7 @@
 
 const peekBorder: Record<ComposerBannerVariant, string> = {
   default: "border-(--chat-composer-attached-outline)",
+  plain: cn("border-(--chat-composer-attached-outline)", plainSurfaceColors),
   error: "border-destructive/24",
   info: "border-(--chat-composer-attached-outline)",
   success: "border-(--chat-composer-attached-outline)",
```

## apps/web/src/components/chat/ThreadActivityBanners.tsx

Status: modified; +13; −0

```diff
@@ -1,6 +1,7 @@
 import type {
   OrchestrationV2PendingBackgroundTask,
   OrchestrationV2ProviderGoal,
+  OrchestrationV2ProviderSession,
   ThreadId,
 } from "@t3tools/contracts";
 import type { EnvironmentThreadShell } from "@t3tools/client-runtime/state/shell";
@@ -16,6 +17,18 @@
 import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";
 import type { ComposerBannerStackItem } from "./ComposerBannerStack";
 
+export function extensionStatusBannerItem(
+  statuses: NonNullable<OrchestrationV2ProviderSession["extensionStatuses"]>,
+): ComposerBannerStackItem | null {
+  if (statuses.length === 0) return null;
+  return {
+    id: "pi-extension-status",
+    variant: "plain",
+    icon: <span className="text-white">▸</span>,
+    title: `Pi · ${statuses.map((status) => status.text).join(" · ")}`,
+  };
+}
+
 /** Subagents waiting on the user, unless the parent is itself waiting on them. */
 export function childInputBannerItem(input: {
   readonly childThreadInputs: ReadonlyArray<EnvironmentThreadShell>;
```

## docs/user/providers-pi.md

Status: modified; +2; −2

```diff
@@ -48,8 +48,8 @@
 loads for the current workspace; selecting one uses Pi's native skill expansion.
 
 Pi loads its normal user and project extensions. Blocking `select`, `confirm`, `input`, and `editor`
-dialogs work in T3 Code. Notifications appear in the work log. Pi terminal decoration such as
-titles, status lines, and widgets does not have a T3 Code equivalent.
+dialogs work in T3 Code. Notifications appear in the work log. Extensions can publish status text
+near the composer. Terminal titles and generic widgets do not have a T3 Code equivalent.
 
 ## Permission Modes
 
```

## packages/client-runtime/src/state/threadExecution.test.ts

Status: modified; +72; −0

```diff
@@ -22,6 +22,7 @@
   deriveReportedModelSelection,
   deriveLatestThreadRun,
   deriveProviderSubagentStatus,
+  deriveProviderExtensionStatuses,
   deriveRunlessWorkStartedAt,
   deriveThreadActivityRun,
   deriveThreadRuntime,
@@ -51,6 +52,77 @@
 }
 
 describe("thread execution presentation", () => {
+  it("shows extension statuses only from the selected provider's current live session", () => {
+    const instanceId = v2Projection.thread.modelSelection.instanceId;
+    const providerThreadId = ProviderThreadId.make("native-status-thread");
+    const sessionId = ProviderSessionId.make("native-status-session");
+    const statuses = [{ key: "watch", text: "Watching" }];
+    const projection = {
+      thread: { ...v2Projection.thread, activeProviderThreadId: providerThreadId },
+      providerThreads: [
+        { id: providerThreadId, providerSessionId: sessionId, providerInstanceId: instanceId },
+      ],
+      providerSessions: [
+        {
+          id: sessionId,
+          providerInstanceId: instanceId,
+          status: "ready" as const,
+          extensionStatuses: statuses,
+        },
+      ],
+    };
+    expect(deriveProviderExtensionStatuses(projection)).toEqual(statuses);
+    expect(
+      deriveProviderExtensionStatuses({
+        ...projection,
+        providerSessions: [
+          ...projection.providerSessions,
+          {
+            ...projection.providerSessions[0]!,
+            id: ProviderSessionId.make("unrelated"),
+            extensionStatuses: [{ key: "wrong", text: "Wrong session" }],
+          },
+        ],
+      }),
+    ).toEqual(statuses);
+    expect(
+      deriveProviderExtensionStatuses({
+        ...projection,
+        providerSessions: [{ ...projection.providerSessions[0]!, status: "stopped" }],
+      }),
+    ).toEqual([]);
+    expect(
+      deriveProviderExtensionStatuses({
+        ...projection,
+        providerSessions: [{ ...projection.providerSessions[0]!, status: "error" }],
+      }),
+    ).toEqual(statuses);
+    expect(
+      deriveProviderExtensionStatuses({
+        ...projection,
+        providerSessions: [
+          { ...projection.providerSessions[0]!, status: "error", extensionStatuses: [] },
+        ],
+      }),
+    ).toEqual([]);
+    expect(
+      deriveProviderExtensionStatuses({
+        ...projection,
+        providerSessions: [{ ...projection.providerSessions[0]!, extensionStatuses: undefined }],
+      }),
+    ).toEqual([]);
+    expect(
+      deriveProviderExtensionStatuses({
+        ...projection,
+        thread: {
+          ...projection.thread,
+          modelSelection: { instanceId: ProviderInstanceId.make("other"), model: "default" },
+        },
+      }),
+    ).toEqual([]);
+    expect(deriveProviderExtensionStatuses(null)).toEqual([]);
+  });
+
   it("derives the current root failure without inheriting errors from children or previous runs", () => {
     const failed = { ...run("limited", 1, "failed"), rootNodeId: NodeId.make("root") };
     const item = {
```

## packages/client-runtime/src/state/threadExecution.ts

Status: modified; +40; −0

```diff
@@ -12,6 +12,7 @@
   type OrchestrationV2NotificationSource,
   type OrchestrationV2PendingBackgroundTask,
   type OrchestrationV2ProviderGoal,
+  type OrchestrationV2ProviderSession,
   type ServerProviderModel,
   type OrchestrationV2ExecutionNode,
   type OrchestrationV2ThreadProjection,
@@ -38,6 +39,45 @@
 const ACTIVITY_RUN_STATUSES = new Set(["preparing", "starting", "running", "waiting"]);
 const INTERRUPTIBLE_RUN_STATUSES = new Set(["preparing", "starting", "running"]);
 
+const EMPTY_EXTENSION_STATUSES: NonNullable<OrchestrationV2ProviderSession["extensionStatuses"]> =
+  [];
+
+/** Only the selected provider's current session can supply transient extension status. */
+export function deriveProviderExtensionStatuses(
+  projection:
+    | (Pick<ThreadStructure, "thread"> & {
+        readonly providerThreads: ReadonlyArray<
+          Pick<
+            ThreadStructure["providerThreads"][number],
+            "id" | "providerSessionId" | "providerInstanceId"
+          >
+        >;
+        readonly providerSessions: ReadonlyArray<
+          Pick<
+            OrchestrationV2ProviderSession,
+            "id" | "providerInstanceId" | "status" | "extensionStatuses"
+          >
+        >;
+      })
+    | null,
+) {
+  if (projection === null) return EMPTY_EXTENSION_STATUSES;
+  const instanceId = projection.thread.modelSelection.instanceId;
+  const activeThread = projection.providerThreads.find(
+    (thread) => thread.id === projection.thread.activeProviderThreadId,
+  );
+  if (activeThread !== undefined && activeThread.providerInstanceId !== instanceId)
+    return EMPTY_EXTENSION_STATUSES;
+  const session = projection.providerSessions.findLast(
+    (session) =>
+      session.providerInstanceId === instanceId &&
+      (activeThread === undefined || session.id === activeThread.providerSessionId),
+  );
+  return session === undefined || session.status === "stopped"
+    ? EMPTY_EXTENSION_STATUSES
+    : (session.extensionStatuses ?? EMPTY_EXTENSION_STATUSES);
+}
+
 function latestMatchingRun(
   projection: ThreadStructure,
   predicate: (run: OrchestrationV2ThreadProjection["runs"][number]) => boolean,
```

[Next page](https://git.cafe/pingdotgg/t3code/pull/23901/diffs?after=InBhY2thZ2VzL2NsaWVudC1ydW50aW1lL3NyYy9zdGF0ZS90aHJlYWRFeGVjdXRpb24udHMi&expectedVersion=5&format=markdown)
