diff --git a/apps/mobile/src/features/settings/SettingsServerControlsRouteScreen.tsx b/apps/mobile/src/features/settings/SettingsServerControlsRouteScreen.tsx
index 595c083638c6d..8a395284296fa 100644
--- a/apps/mobile/src/features/settings/SettingsServerControlsRouteScreen.tsx
+++ b/apps/mobile/src/features/settings/SettingsServerControlsRouteScreen.tsx
@@ -472,6 +472,20 @@ function ServerSettingsDetail(props: { readonly page: SettingsPage }) {
))}
) : null}
+
+ write({ freshSessionOnColdReturn: value })}
+ />
+
{
}
return `/${parts.join("/")}`;
};
-const layerWorktreeLocation = (worktreesDir = managedWorktreesDir) =>
+const layerWorktreeLocation = (
+ worktreesDir = managedWorktreesDir,
+ settings?: Parameters[0],
+) =>
Layer.mergeAll(
Path.layer,
Layer.mock(ServerConfig.ServerConfig)({ worktreesDir } as ServerConfig.ServerConfig["Service"]),
- ServerSettings.layerTest(),
+ ServerSettings.layerTest(settings),
);
function startWithMissingWorktree(
@@ -286,6 +290,7 @@ it.each([
});
function makeLocalCommandHarness(input: {
+ readonly settings?: Parameters[0];
readonly pstack?: PstackPack.PstackPack["Service"];
readonly text: string;
readonly knowledgeStore?: Effect.Success;
@@ -307,6 +312,8 @@ function makeLocalCommandHarness(input: {
readonly ensureThreadFailure?: unknown;
readonly historyReadFailureAfterFallback?: unknown;
readonly resumeFailure?: ProviderAdapter.ProviderAdapterV2Error;
+ readonly pendingBackgroundWorkOnResume?: boolean;
+ readonly pendingBackgroundWorkAfterHandoff?: boolean;
readonly interruptOpen?: boolean;
readonly interruptRunBeforeOpenFailure?: boolean;
readonly writeFailure?: unknown;
@@ -786,7 +793,20 @@ function makeLocalCommandHarness(input: {
const nativeStart = vi.fn(
() => Effect.void,
);
- const fallbackEnsureThread = vi.fn(() => Effect.succeed(providerThread));
+ const fallbackEnsureThread = vi.fn(() =>
+ Effect.succeed(
+ input.recoveryNativeId === undefined
+ ? providerThread
+ : {
+ ...providerThread,
+ nativeThreadRef: {
+ driver: providerThread.driver,
+ nativeId: input.recoveryNativeId,
+ strength: "strong" as const,
+ },
+ },
+ ),
+ );
const resumeFallbackSession = {
driver: providerThread.driver,
resumeThread: () =>
@@ -832,13 +852,19 @@ function makeLocalCommandHarness(input: {
forkThread,
startTurn,
};
+ let resumedNativeThread = false;
+ let preparedRecoveryHandoff = false;
const open = vi.fn(() =>
input.sideChat !== undefined
? Effect.succeed(sideChatSession as never)
: input.interruptOpen === true
? Effect.interrupt
: "historyReadFailureAfterFallback" in input || "resumeFailure" in input
- ? Effect.succeed(resumeFallbackSession as never)
+ ? Effect.succeed({
+ ...resumeFallbackSession,
+ providerSession,
+ startTurn: nativeStart,
+ } as never)
: "ensureThreadFailure" in input
? Effect.succeed({ driver: providerThread.driver, ensureThread } as never)
: "openFailure" in input
@@ -870,7 +896,18 @@ function makeLocalCommandHarness(input: {
strength: "strong" as const,
},
}),
- resumeThread: () => Effect.succeed(projection.providerThreads.at(-1)),
+ resumeThread: () =>
+ Effect.sync(() => {
+ resumedNativeThread = true;
+ return projection.providerThreads.at(-1);
+ }),
+ hasPendingBackgroundWorkForThread: () =>
+ Effect.sync(
+ () =>
+ (input.pendingBackgroundWorkOnResume === true && resumedNativeThread) ||
+ (input.pendingBackgroundWorkAfterHandoff === true &&
+ preparedRecoveryHandoff),
+ ),
} as never)
: Effect.die("A local command must not open a native session."),
);
@@ -1062,7 +1099,13 @@ function makeLocalCommandHarness(input: {
summaryText: input.sideChat ? "PARENT_HISTORY" : "Recovered history",
createdByProviderInstanceId: input.sideChat ? request.toProviderInstanceId : null,
updatedAt: request.createdAt,
- } as OrchestrationV2ContextHandoff),
+ } as OrchestrationV2ContextHandoff).pipe(
+ Effect.tap(() =>
+ Effect.sync(() => {
+ preparedRecoveryHandoff = true;
+ }),
+ ),
+ ),
}),
Layer.mock(EventSink.EventSinkV2)({
writeIfRunCurrent,
@@ -1076,7 +1119,7 @@ function makeLocalCommandHarness(input: {
}),
}),
IdAllocator.layer,
- layerWorktreeLocation(input.worktreesDir),
+ layerWorktreeLocation(input.worktreesDir, input.settings),
input.fileSystem === undefined
? FileSystem.layerNoop({ exists: () => Effect.succeed(true) })
: Layer.succeed(FileSystem.FileSystem, input.fileSystem),
@@ -2434,6 +2477,157 @@ effectIt.effect(
}),
);
+effectIt.effect.each([
+ { name: "a long idle thread", enabled: true, usedTokens: 120_000, idleMinutes: 61, fresh: true },
+ { name: "the setting off", enabled: false, usedTokens: 120_000, idleMinutes: 61, fresh: false },
+ { name: "a recent turn", enabled: true, usedTokens: 120_000, idleMinutes: 59, fresh: false },
+ { name: "a small thread", enabled: true, usedTokens: 99_999, idleMinutes: 61, fresh: false },
+ {
+ name: "recent completion after an old token report",
+ enabled: true,
+ usedTokens: 120_000,
+ idleMinutes: 61,
+ turnCompletedMinutes: 60,
+ fresh: false,
+ },
+ {
+ name: "insufficient room for a fresh handoff",
+ enabled: true,
+ usedTokens: 120_000,
+ idleMinutes: 61,
+ modelContextWindow: 16_000,
+ fresh: false,
+ },
+ {
+ name: "a native thread held by another client",
+ enabled: true,
+ usedTokens: 120_000,
+ idleMinutes: 61,
+ threadInUse: true,
+ fresh: false,
+ },
+ {
+ name: "background work discovered while resuming",
+ enabled: true,
+ usedTokens: 120_000,
+ idleMinutes: 61,
+ pendingBackgroundWorkOnResume: true,
+ fresh: false,
+ },
+ {
+ name: "background work discovered while preparing the handoff",
+ enabled: true,
+ usedTokens: 120_000,
+ idleMinutes: 61,
+ pendingBackgroundWorkAfterHandoff: true,
+ fresh: false,
+ },
+ {
+ name: "a command message",
+ enabled: true,
+ usedTokens: 120_000,
+ idleMinutes: 61,
+ text: "/compact",
+ fresh: false,
+ },
+ {
+ name: "a message not from a person",
+ enabled: true,
+ usedTokens: 120_000,
+ idleMinutes: 61,
+ createdBy: "system" as const,
+ fresh: false,
+ },
+] as const)("starting a turn with $name picks fresh session: $fresh", (scenario) =>
+ Effect.gen(function* () {
+ const harness = makeLocalCommandHarness({
+ settings: { freshSessionOnColdReturn: scenario.enabled },
+ text: "text" in scenario ? scenario.text : "Keep going on the migration",
+ previousTurn: { runStatus: "completed", turnStatus: "completed" },
+ recoveryNativeId: "fresh-native-conversation",
+ ...("modelContextWindow" in scenario
+ ? { modelContextWindow: scenario.modelContextWindow }
+ : {}),
+ ...("threadInUse" in scenario
+ ? {
+ resumeFailure: new ProviderAdapter.ProviderAdapterThreadInUseError({
+ driver: ProviderDriverKind.make("codex"),
+ providerThreadId: ProviderThreadId.make("new-provider-thread"),
+ detail: "This conversation is open in another Codex client.",
+ cause: new Error("thread existing-native-conversation already has an active writer"),
+ }),
+ }
+ : {}),
+ ...("pendingBackgroundWorkOnResume" in scenario
+ ? { pendingBackgroundWorkOnResume: scenario.pendingBackgroundWorkOnResume }
+ : {}),
+ ...("pendingBackgroundWorkAfterHandoff" in scenario
+ ? { pendingBackgroundWorkAfterHandoff: scenario.pendingBackgroundWorkAfterHandoff }
+ : {}),
+ });
+ const projection = harness.projection();
+ const previousAt = "2026-09-04T12:00:00.000Z";
+ const previousAttemptId = RunAttemptId.make("previous-failed-attempt");
+ harness.setProjection({
+ ...projection,
+ messages: projection.messages.map((message) => ({
+ ...message,
+ ...("createdBy" in scenario ? { createdBy: scenario.createdBy } : {}),
+ })),
+ attempts: [
+ ...projection.attempts,
+ {
+ ...projection.attempts[0]!,
+ id: previousAttemptId,
+ runId: RunId.make("previous-failed-run"),
+ rootNodeId: NodeId.make("previous-root"),
+ nativeThreadId: "existing-native-conversation",
+ providerThreadId: projection.providerThreads[0]!.id,
+ status: "completed",
+ },
+ ],
+ providerTurns: projection.providerTurns.map((turn) => ({
+ ...turn,
+ tokenUsage: { usedTokens: scenario.usedTokens, updatedAt: previousAt },
+ ...("turnCompletedMinutes" in scenario
+ ? {
+ completedAt: DateTime.makeUnsafe(
+ Date.parse(previousAt) + scenario.turnCompletedMinutes * 60_000,
+ ),
+ }
+ : {}),
+ })),
+ });
+ yield* TestClock.setTime(Date.parse(previousAt) + scenario.idleMinutes * 60_000);
+
+ yield* harness.start;
+
+ if ("threadInUse" in scenario) {
+ expect(harness.fallbackEnsureThread).not.toHaveBeenCalled();
+ expect(harness.startRootRun).not.toHaveBeenCalled();
+ expect(harness.projection().runs.at(-1)).toMatchObject({ status: "failed", startedAt: null });
+ }
+ // Commands run without a root run, which also means no fresh session.
+ const nativeId =
+ harness.startRootRun.mock.calls[0]?.[0].providerThread.nativeThreadRef?.nativeId;
+ expect(nativeId === "fresh-native-conversation").toBe(scenario.fresh);
+ expect(harness.events.some((event) => event.type === "context-handoff.updated")).toBe(
+ scenario.fresh,
+ );
+ if (
+ "pendingBackgroundWorkOnResume" in scenario ||
+ "pendingBackgroundWorkAfterHandoff" in scenario
+ ) {
+ expect(nativeId).toBe("existing-native-conversation");
+ }
+ if ("modelContextWindow" in scenario) {
+ yield* dispatchKnowledge(harness.startRootRun.mock.calls[0]![0]);
+ expect(nativeId).toBe("existing-native-conversation");
+ expect(harness.nativeStart).toHaveBeenCalledOnce();
+ }
+ }),
+);
+
effectIt.effect.each(["prepared", "uncertain"] as const)(
"recovers a %s knowledge receipt through the native handoff fallback without resuming the ambiguous conversation",
(status) =>
diff --git a/apps/server/src/orchestration-v2/ProviderTurnStartService.ts b/apps/server/src/orchestration-v2/ProviderTurnStartService.ts
index 73029a242d6df..22127f89a506a 100644
--- a/apps/server/src/orchestration-v2/ProviderTurnStartService.ts
+++ b/apps/server/src/orchestration-v2/ProviderTurnStartService.ts
@@ -136,6 +136,11 @@ export class ProviderTurnStartServiceV2 extends Context.Service<
ProviderTurnStartServiceV2Shape
>()("t3/orchestration-v2/ProviderTurnStartService/ProviderTurnStartServiceV2") {}
+// Past both limits, a replay of real threads shows a fresh session repaying its uncached first
+// request: the cache has already expired, and the smaller prompt is cheaper on every later call.
+const COLD_RETURN_IDLE_MS = 60 * 60_000;
+const COLD_RETURN_MIN_CONTEXT_TOKENS = 100_000;
+
export const layer: Layer.Layer<
ProviderTurnStartServiceV2,
never,
@@ -159,6 +164,7 @@ export const layer: Layer.Layer<
ProviderTurnStartServiceV2,
Effect.gen(function* () {
const pstack = yield* PstackPack.PstackPack;
+ const serverSettings = yield* ServerSettings.ServerSettingsService;
const eventSink = yield* EventSink.EventSinkV2;
const turnAnalytics = yield* ProviderUsageAnalytics.ProviderTurnAnalytics;
const contextHandoffService = yield* ContextHandoffService.ContextHandoffServiceV2;
@@ -1033,6 +1039,47 @@ export const layer: Layer.Layer<
return retryHandoff;
},
);
+ const isColdReturn = Effect.fn("ProviderTurnStartService.isColdReturn")(function* (
+ resumedThread: OrchestrationV2ProviderThread,
+ ) {
+ const enabled = yield* serverSettings.getSettings.pipe(
+ Effect.map((settings) => settings.freshSessionOnColdReturn),
+ Effect.orElseSucceed(() => false),
+ );
+ if (!enabled) return false;
+ const measured = latestNativeContextUsage(projection, resumedThread);
+ if (measured === undefined) return false;
+ if (measured.usage.usedTokens < COLD_RETURN_MIN_CONTEXT_TOKENS) return false;
+ const reportedAt = DateTime.make(measured.reportedAt);
+ if (Option.isNone(reportedAt)) return false;
+ // Token telemetry can arrive before a long tool run finishes. Count accepted root
+ // turn activity on this native conversation, including turns without usage reports.
+ let lastActivityMs = DateTime.toEpochMillis(reportedAt.value);
+ const attempts = new Map(projection.attempts.map((attempt) => [attempt.id, attempt]));
+ for (const turn of projection.providerTurns) {
+ if (turn.providerThreadId !== resumedThread.id || turn.runAttemptId === null) continue;
+ const attempt = attempts.get(turn.runAttemptId);
+ if (
+ attempt === undefined ||
+ attempt.nativeThreadId !== resumedThread.nativeThreadRef?.nativeId ||
+ attempt.providerThreadId !== resumedThread.id ||
+ attempt.rootNodeId !== turn.nodeId
+ )
+ continue;
+ const activityAt = turn.completedAt ?? turn.startedAt;
+ if (activityAt !== null) {
+ lastActivityMs = Math.max(lastActivityMs, DateTime.toEpochMillis(activityAt));
+ }
+ }
+ const idleMs = DateTime.toEpochMillis(yield* DateTime.now) - lastActivityMs;
+ if (idleMs < COLD_RETURN_IDLE_MS) return false;
+ const hasPendingWork = yield* (
+ session.hasPendingBackgroundWorkForThread?.(resumedThread, { includeSubagents: true }) ??
+ session.hasPendingBackgroundWork ??
+ Effect.succeed(false)
+ );
+ return !hasPendingWork;
+ });
const prepareRecoveryHandoff = Effect.fn("ProviderTurnStartService.prepareRecoveryHandoff")(
function* () {
const transferId = yield* idAllocator.allocate.contextTransfer({
@@ -1269,6 +1316,13 @@ export const layer: Layer.Layer<
runtimePolicy: resolvedRuntimePolicy,
}),
);
+ // Resume checks native ownership and refreshes background work before a fresh session.
+ const coldReturn =
+ resumed._tag === "Success" &&
+ message.createdBy === "user" &&
+ !isCompactionCommandMessage(message) &&
+ !goalCommand &&
+ (yield* isColdReturn(resumed.success));
if (resumed._tag === "Success") {
const context = handoffContextForThread(resumed.success);
context.budget = yield* Effect.cached(context.budget);
@@ -1285,7 +1339,7 @@ export const layer: Layer.Layer<
Effect.as(true),
Effect.catchTags({ ContextHandoffBudgetError: () => Effect.succeed(false) }),
);
- if (fits) return resumed.success;
+ if (fits && !coldReturn) return resumed.success;
const freshThread = { ...resumed.success, nativeThreadRef: null };
const freshContext = handoffContextForThread(freshThread);
const { handoff } = yield* recoveryHandoff;
@@ -1308,6 +1362,7 @@ export const layer: Layer.Layer<
Effect.succeed(false)
);
if (hasPendingWork) {
+ if (fits) return resumed.success;
return yield* loadFromProvider(
Effect.fail(
new ProviderAdapter.ProviderAdapterTurnStartError({
@@ -1337,12 +1392,19 @@ export const layer: Layer.Layer<
driver: session.driver,
providerThreadId: providerThread.id,
runId,
- reason: uncertainDelivery
- ? "uncertain_history_delivery"
- : resumed._tag === "Success"
- ? "handoff_budget_exhausted"
- : "resume_failed",
- errorTag: resumed._tag === "Failure" ? resumed.failure._tag : "ContextHandoffBudgetError",
+ reason: coldReturn
+ ? "cold_return"
+ : uncertainDelivery
+ ? "uncertain_history_delivery"
+ : resumed._tag === "Success"
+ ? "handoff_budget_exhausted"
+ : "resume_failed",
+ ...(coldReturn
+ ? {}
+ : {
+ errorTag:
+ resumed._tag === "Failure" ? resumed.failure._tag : "ContextHandoffBudgetError",
+ }),
});
const replacement = yield* loadFromProvider(
session.ensureThread({
diff --git a/apps/web/src/components/settings/SettingsPanels.tsx b/apps/web/src/components/settings/SettingsPanels.tsx
index 6ac9ab54c26b8..605fc641f0c77 100644
--- a/apps/web/src/components/settings/SettingsPanels.tsx
+++ b/apps/web/src/components/settings/SettingsPanels.tsx
@@ -523,6 +523,9 @@ export function useSettingsRestore(onRestored?: () => void) {
? ["Show skills in slash menu"]
: []),
...(settings.pstackEnabled !== DEFAULT_UNIFIED_SETTINGS.pstackEnabled ? ["pstack"] : []),
+ ...(settings.freshSessionOnColdReturn !== DEFAULT_UNIFIED_SETTINGS.freshSessionOnColdReturn
+ ? ["Fresh session after a long break"]
+ : []),
...(settings.composerRichTextEnabled !== DEFAULT_UNIFIED_SETTINGS.composerRichTextEnabled
? ["Rich text composer"]
: []),
@@ -645,6 +648,7 @@ export function useSettingsRestore(onRestored?: () => void) {
proactivePanelsEnabled: DEFAULT_UNIFIED_SETTINGS.proactivePanelsEnabled,
showSkillsInSlashMenu: DEFAULT_UNIFIED_SETTINGS.showSkillsInSlashMenu,
pstackEnabled: DEFAULT_UNIFIED_SETTINGS.pstackEnabled,
+ freshSessionOnColdReturn: DEFAULT_UNIFIED_SETTINGS.freshSessionOnColdReturn,
composerRichTextEnabled: DEFAULT_UNIFIED_SETTINGS.composerRichTextEnabled,
sendShortcut: DEFAULT_UNIFIED_SETTINGS.sendShortcut,
followUpBehavior: DEFAULT_UNIFIED_SETTINGS.followUpBehavior,
diff --git a/apps/web/src/components/settings/ZackSettings.tsx b/apps/web/src/components/settings/ZackSettings.tsx
index ed41da6afbfee..1b7bcb4295756 100644
--- a/apps/web/src/components/settings/ZackSettings.tsx
+++ b/apps/web/src/components/settings/ZackSettings.tsx
@@ -27,6 +27,7 @@ export function ZackSettings() {
+
@@ -155,6 +156,41 @@ function ExternalHistoryRow() {
);
}
+function FreshSessionOnColdReturnRow() {
+ const settings = useScopedSettings();
+ const updateSettings = useUpdateScopedSettings();
+ return (
+
+ updateSettings({
+ freshSessionOnColdReturn: DEFAULT_UNIFIED_SETTINGS.freshSessionOnColdReturn,
+ })
+ }
+ />
+ ) : null
+ }
+ control={
+
+ updateSettings({ freshSessionOnColdReturn: Boolean(checked) })
+ }
+ aria-label="Fresh session after a long break"
+ />
+ }
+ />
+ );
+}
+
function PstackRow() {
const settings = useScopedSettings();
const updateSettings = useUpdateScopedSettings();
diff --git a/apps/web/src/components/settings/settingsSearch.test.ts b/apps/web/src/components/settings/settingsSearch.test.ts
index 21b130fcd3b8a..fb7aa7c78664e 100644
--- a/apps/web/src/components/settings/settingsSearch.test.ts
+++ b/apps/web/src/components/settings/settingsSearch.test.ts
@@ -234,6 +234,12 @@ describe("settings search targets", () => {
expect(isSettingsSearchScopeAvailable(fallback.scope, "environment")).toBe(true);
expect(isSettingsSearchScopeAvailable(fallback.scope, "all")).toBe(true);
expect(isSettingsSearchScopeAvailable(fallback.scope, "project")).toBe(false);
+ const coldReturn = getSettingsSearchTargetScope("fresh-session-on-cold-return")!;
+ expect(coldReturn.scope).toBe("environment-defaults");
+ expect(isSettingsSearchScopeAvailable(coldReturn.scope, "environment")).toBe(true);
+ expect(isSettingsSearchScopeAvailable(coldReturn.scope, "all")).toBe(true);
+ expect(isSettingsSearchScopeAvailable(coldReturn.scope, "project")).toBe(false);
+ expect(isSettingsSearchScopeAvailable(coldReturn.scope, "checkout")).toBe(false);
for (const id of ["response-streaming", "speech-project-dictionary"]) {
expect(getSettingsSearchTargetScope(id)?.scope).toBe("project-defaults");
}
diff --git a/apps/web/src/components/settings/settingsSearch.ts b/apps/web/src/components/settings/settingsSearch.ts
index 9c0f5bd012a83..5217ee97b5a9a 100644
--- a/apps/web/src/components/settings/settingsSearch.ts
+++ b/apps/web/src/components/settings/settingsSearch.ts
@@ -714,6 +714,13 @@ export const SETTINGS_SEARCH_ITEMS = [
to: "/settings/labs",
searchTerms: ["zack lauren tan skills workflows plugin pack architect setup models"],
},
+ {
+ id: "fresh-session-on-cold-return",
+ title: "Fresh session after a long break",
+ to: "/settings/labs",
+ scope: "environment-defaults",
+ searchTerms: ["zack cached input cost tokens long thread idle context handoff trim"],
+ },
{
id: "composer-rich-text",
title: "Rich text composer",
diff --git a/docs/user/portable-handoffs.md b/docs/user/portable-handoffs.md
index cfefeeab32ea2..856b405537d82 100644
--- a/docs/user/portable-handoffs.md
+++ b/docs/user/portable-handoffs.md
@@ -21,6 +21,12 @@ to retrieve saved messages and activity, including the remainder of a long item.
constraint, you can still repeat it in your next message. Unless the project has conversation
summaries turned on, a handoff is a budgeted selection, not a summary.
+## Fresh sessions after a long break
+
+Long threads get more expensive with every message, because the provider re-reads the whole history each time. With **Settings > Labs > Zack > Fresh session after a long break** turned on, a message you send to a thread over about 100,000 tokens, after an hour or more idle, continues in a new provider session. It receives the same handoff described above, and your thread and its history in T3 Code stay as they were.
+
+The first request costs more because nothing is cached, and the agent may re-read some files, so this suits threads you keep returning to. Runs with background work still in progress are left alone. The setting is off by default.
+
## Conversation summaries
With conversation summaries on, older history that would not fit in a handoff can arrive as short
diff --git a/packages/contracts/src/settings.ts b/packages/contracts/src/settings.ts
index 527b4cb25f7c5..235fa712cffaf 100644
--- a/packages/contracts/src/settings.ts
+++ b/packages/contracts/src/settings.ts
@@ -1349,6 +1349,7 @@ export const ServerSettings = Schema.Struct({
enableAgentDeviceAccess: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))),
/** Offer the bundled pstack skills in every provider composer. */
pstackEnabled: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))),
+ freshSessionOnColdReturn: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))),
/** Off by default: turning it on moves skill folders out of the user's Claude and Codex homes. */
skillsLibraryEnabled: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))),
/** Off by default: each settled run in an opted-in project costs a text generation call. */
@@ -1808,6 +1809,7 @@ export const ServerSettingsPatch = Schema.Struct({
),
enableAgentDeviceAccess: Schema.optionalKey(Schema.Boolean),
pstackEnabled: Schema.optionalKey(Schema.Boolean),
+ freshSessionOnColdReturn: Schema.optionalKey(Schema.Boolean),
skillsLibraryEnabled: Schema.optionalKey(Schema.Boolean),
enableAssistedLearning: Schema.optionalKey(Schema.Boolean),
enableConversationSummaries: Schema.optionalKey(Schema.Boolean),
diff --git a/packages/shared/src/testing/providerEnv.ts b/packages/shared/src/testing/providerEnv.ts
index ac07867c20473..50db357a45f9e 100644
--- a/packages/shared/src/testing/providerEnv.ts
+++ b/packages/shared/src/testing/providerEnv.ts
@@ -10,6 +10,8 @@ const KEPT_ENV = new Set([
"T3CODE_TRANSFER_BUDGET_RESULT_PATH",
// Opt-in: points the HtmlRender tests at a headless browser, which they skip without.
"T3CODE_TEST_HEADLESS_SHELL",
+ // The mobile test script sets this; tests must see the platform their code was compiled for.
+ "T3_MOBILE_TEST_PLATFORM",
]);
if (process.env.T3CODE_TEST_KEEP_PROVIDER_ENV !== "1") {