Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
ce6331f
feat(contracts): add agent history import window and auto-import sett…
atmikshetty Sep 12, 2026
587601b
feat(server): honor the import window and drop the message cap
atmikshetty Sep 12, 2026
38d694d
feat(web): add agent history import settings
atmikshetty Sep 12, 2026
cbb5328
docs: describe the configurable agent history import
atmikshetty Sep 12, 2026
2fd3e86
feat(contracts): add auto-import status and its RPCs
atmikshetty Sep 12, 2026
3081dfa
feat(server): import agent history in the background
atmikshetty Sep 12, 2026
9ed1212
feat(server): start the auto-importer and expose its status
atmikshetty Sep 12, 2026
27aca28
feat(web): run the history import from settings
atmikshetty Sep 12, 2026
80d190b
feat(web): import agent history from the command palette
atmikshetty Sep 12, 2026
356fab2
feat(mobile): manage agent history import from settings
atmikshetty Sep 12, 2026
0944a12
Merge upstream/main into feat/agent-session-auto-import
atmikshetty Sep 12, 2026
54cf9f7
fix(server): stop over-reporting imported conversation counts
atmikshetty Sep 12, 2026
5eaf2fa
fix(web): drop the zero-project phrasing from import status
atmikshetty Sep 12, 2026
40aa264
Merge branch 'main' into feat/agent-session-auto-import
atmikshetty Sep 12, 2026
ac5a275
fix(server): keep the auto-import drain loop terminating
atmikshetty Sep 12, 2026
e5e83a4
fix(server): report a refundable record allowance as remaining work
atmikshetty Sep 12, 2026
63e5408
fix(web): scope the import row and correct its claims
atmikshetty Sep 12, 2026
6caf97e
fix(mobile): tell the user when an import request fails
atmikshetty Sep 12, 2026
d1bb5ba
Merge remote-tracking branch 'origin/feat/agent-session-auto-import' …
atmikshetty Sep 12, 2026
f25ccc1
fix(server): keep an auth change from being buried by a stale snapshot
atmikshetty Sep 12, 2026
40f8969
fix(web): offer the manual import only for a single computer
atmikshetty Sep 12, 2026
518e86b
docs: tie the import triggers to the automatic setting
atmikshetty Sep 12, 2026
1c92660
Merge branch 'main' into feat/agent-session-auto-import
atmikshetty Sep 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { describe, expect, it } from "vite-plus/test";

import { resolveAgentAwarenessPlatformPresentation } from "./SettingsRouteScreen.logic";
import {
AGENT_SESSION_IMPORT_WINDOW_LABELS,
describeAgentSessionImportOutcome,
parseAgentSessionImportWindow,
resolveAgentAwarenessPlatformPresentation,
} from "./SettingsRouteScreen.logic";

describe("resolveAgentAwarenessPlatformPresentation", () => {
it("supports agent awareness settings on Android", () => {
Expand All @@ -17,3 +22,39 @@ describe("resolveAgentAwarenessPlatformPresentation", () => {
});
});
});

describe("parseAgentSessionImportWindow", () => {
it("accepts every import window literal", () => {
expect(parseAgentSessionImportWindow("30d")).toBe("30d");
expect(parseAgentSessionImportWindow("90d")).toBe("90d");
expect(parseAgentSessionImportWindow("1y")).toBe("1y");
expect(parseAgentSessionImportWindow("all")).toBe("all");
});

it("rejects values that are not import windows", () => {
expect(parseAgentSessionImportWindow("")).toBe(null);
expect(parseAgentSessionImportWindow("forever")).toBe(null);
});
});

describe("AGENT_SESSION_IMPORT_WINDOW_LABELS", () => {
it("labels every import window", () => {
expect(Object.keys(AGENT_SESSION_IMPORT_WINDOW_LABELS).sort()).toEqual(
["30d", "90d", "1y", "all"].sort(),
);
});
});

describe("describeAgentSessionImportOutcome", () => {
it("names the environment the import runs on", () => {
expect(describeAgentSessionImportOutcome("started", "Studio")).toBe(
"Import started on Studio. It continues in the background.",
);
});

it("says a failed request never started an import", () => {
expect(describeAgentSessionImportOutcome("failed", "Studio")).toBe(
"Import couldn't start on Studio. Try again.",
);
});
});
45 changes: 45 additions & 0 deletions apps/mobile/src/features/settings/SettingsRouteScreen.logic.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,31 @@
import type { AgentSessionImportWindow } from "@t3tools/contracts";

/** Selector copy for how far back agent history import reaches. */
export const AGENT_SESSION_IMPORT_WINDOW_LABELS: Record<AgentSessionImportWindow, string> = {
"30d": "Last 30 days",
"90d": "Last 90 days",
"1y": "Last year",
all: "All history",
};

export const AGENT_SESSION_IMPORT_WINDOWS = ["30d", "90d", "1y", "all"] as const;

/**
* Native menu action ids arrive as plain strings; narrow them back to the
* setting's literals so a stray id can never reach the server patch.
*/
export function parseAgentSessionImportWindow(value: string): AgentSessionImportWindow | null {
switch (value) {
case "30d":
case "90d":
case "1y":
case "all":
return value;
default:
return null;
}
}

export function resolveAgentAwarenessPlatformPresentation(platform: string): {
readonly supported: boolean;
readonly subtitle: string | undefined;
Expand All @@ -6,3 +34,20 @@ export function resolveAgentAwarenessPlatformPresentation(platform: string): {
? { supported: true, subtitle: undefined }
: { supported: false, subtitle: "Unavailable on this platform" };
}

/** Result of the last "Import now" tap, as of the tap itself. */
export type AgentSessionImportOutcome = "started" | "failed";

/**
* One-line status for the agent history row. Mobile has no import status
* subscription, so the copy describes only what the request itself did and
* never a run state that would go stale while the screen sits open.
*/
export function describeAgentSessionImportOutcome(
outcome: AgentSessionImportOutcome,
environmentLabel: string,
): string {
return outcome === "started"
? `Import started on ${environmentLabel}. It continues in the background.`
: `Import couldn't start on ${environmentLabel}. Try again.`;
}
132 changes: 131 additions & 1 deletion apps/mobile/src/features/settings/SettingsRouteScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
} from "@t3tools/client-runtime/state/runtime";
import { AndroidScreenHeader } from "../../components/AndroidScreenHeader";
import { AppText as Text, AppTextInput as TextInput } from "../../components/AppText";
import { ControlPillMenu } from "../../components/ControlPill";
import { supportsAgentAwarenessPush } from "../agent-awareness/capabilities";
import { setLiveActivityUpdatesEnabled } from "../agent-awareness/liveActivityPreferences";
import { requestAgentNotificationPermission } from "../agent-awareness/notificationPermissions";
Expand All @@ -33,6 +34,7 @@ import { hasCloudPublicConfig, resolveRelayClerkTokenOptions } from "../cloud/pu
import { withNativeGlassHeaderItem } from "../layout/native-glass-header-items";
import { WorkspaceSidebarToolbar } from "../layout/workspace-sidebar-toolbar";
import { runtime } from "../../lib/runtime";
import { agentSessionImportAll } from "../../state/agentSessions";
import { mobilePreferencesAtom, updateMobilePreferencesAtom } from "../../state/preferences";
import { serverEnvironment } from "../../state/server";
import { useAtomCommand } from "../../state/use-atom-command";
Expand All @@ -41,6 +43,7 @@ import {
DEFAULT_SERVER_SETTINGS,
MAX_SIDEBAR_AUTO_SETTLE_AFTER_DAYS,
MIN_SIDEBAR_AUTO_SETTLE_AFTER_DAYS,
type ServerSettingsPatch,
} from "@t3tools/contracts";
import { supportsSharedSettingsSync } from "@t3tools/client-runtime/state/shared-settings";
import { useThreadListV2Enabled } from "../threads/use-thread-list-v2-enabled";
Expand All @@ -54,7 +57,14 @@ import { useSavedRemoteConnections } from "../../state/use-remote-environment-re
import { SettingsRow } from "./components/SettingsRow";
import { SettingsSection } from "./components/SettingsSection";
import { SettingsSwitchRow } from "./components/SettingsSwitchRow";
import { resolveAgentAwarenessPlatformPresentation } from "./SettingsRouteScreen.logic";
import {
AGENT_SESSION_IMPORT_WINDOW_LABELS,
AGENT_SESSION_IMPORT_WINDOWS,
type AgentSessionImportOutcome,
describeAgentSessionImportOutcome,
parseAgentSessionImportWindow,
resolveAgentAwarenessPlatformPresentation,
} from "./SettingsRouteScreen.logic";
import { planAutoSettleSettingsSync, type AutoSettleSettings } from "./autoSettleSettingsSync";

type NotificationStatus = "checking" | "enabled" | "disabled" | "unsupported";
Expand Down Expand Up @@ -575,6 +585,7 @@ function GeneralSettingsSection() {
<SettingsSection title="General">
<SettingsRow icon="folder" label="Project Grouping" target="SettingsProjectGrouping" />
<AutoSettleSettingsRows />
<AgentHistorySettingsRows />
<SettingsRow icon="chart.bar.xaxis" label="Usage" target="SettingsUsage" />
</SettingsSection>
);
Expand Down Expand Up @@ -698,6 +709,125 @@ function AutoSettleSettingsRows() {
);
}

/**
* Agent history import is a server setting every environment has to hold,
* same as auto-settlement: the first eligible sync target provides the
* reference value and edits fan out to every eligible target. "Import now"
* runs against the reference environment only — the import walks that
* machine's disk.
*/
function AgentHistorySettingsRows() {
const { environments } = useEnvironments();
const updateSettings = useAtomCommand(serverEnvironment.updateSettings, {
label: "server settings update",
reportFailure: true,
});
const importAll = useAtomCommand(agentSessionImportAll, {
label: "environment-data:agent-sessions:import-all",
// The row reports a failed request inline; keep the log the neighbouring
// settings commands report to as well, since it carries the cause.
reportFailure: true,
});

const syncTargets = environments.filter(supportsSharedSettingsSync);
const reference = syncTargets[0] ?? null;
const referenceSettings = reference?.serverConfig?.settings ?? null;

// Mobile has no status subscription, so the row cannot follow a run. Report
// only what is true at the moment of the tap rather than showing a state
// label that would then go stale.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
const [importOutcome, setImportOutcome] = useState<AgentSessionImportOutcome | null>(null);
const importInFlight = useRef(false);

if (reference === null || referenceSettings === null) {
return null;
}

const writeToAll = (patch: ServerSettingsPatch) => {
for (const environment of syncTargets) {
void updateSettings({ environmentId: environment.environmentId, input: { patch } });
}
};

const importNow = async () => {
// `runNow` returns the status as of the request, so the round trip is
// quick — but two taps in the same frame would both get through without
// the ref.
if (importInFlight.current) return;
importInFlight.current = true;
try {
const result = await importAll({ environmentId: reference.environmentId, input: {} });
if (result._tag === "Success") {
setImportOutcome("started");
} else if (!isAtomCommandInterrupted(result)) {
// An interrupt means the request never settled, so it says nothing
// about the import; leave whatever the row already shows.
setImportOutcome("failed");
}
} finally {
importInFlight.current = false;
}
};

return (
<>
<SettingsSwitchRow
icon="arrow.clockwise"
label="Import agent history automatically"
value={referenceSettings.agentSessionAutoImport}
onValueChange={(value) => writeToAll({ agentSessionAutoImport: value })}
/>
<ControlPillMenu
title="History to import"
actions={AGENT_SESSION_IMPORT_WINDOWS.map((window) => ({
id: window,
title: AGENT_SESSION_IMPORT_WINDOW_LABELS[window],
state:
referenceSettings.agentSessionImportWindow === window
? ("on" as const)
: ("off" as const),
}))}
onPressAction={({ nativeEvent }) => {
const window = parseAgentSessionImportWindow(nativeEvent.event);
if (window !== null && window !== referenceSettings.agentSessionImportWindow) {
writeToAll({ agentSessionImportWindow: window });
}
}}
>
<View className="flex-row items-center gap-4 border-t border-border-subtle p-4">
<Text className="flex-1 text-lg text-foreground">History to import</Text>
<Text className="text-base text-foreground-muted">
{AGENT_SESSION_IMPORT_WINDOW_LABELS[referenceSettings.agentSessionImportWindow]}
</Text>
</View>
</ControlPillMenu>
<View className="flex-row items-center gap-4 border-t border-border-subtle p-4">
<View className="min-w-0 flex-1">
<Text className="text-lg text-foreground">Import agent history</Text>
{importOutcome === null ? null : (
<Text
className={
importOutcome === "failed"
? "text-sm text-danger-foreground"
: "text-sm text-foreground-muted"
}
>
{describeAgentSessionImportOutcome(importOutcome, reference.label)}
</Text>
)}
</View>
<Pressable
accessibilityRole="button"
onPress={() => void importNow()}
className="rounded-full bg-subtle px-4 py-2 active:opacity-70"
>
<Text className="text-base font-t3-medium text-foreground">Import now</Text>
</Pressable>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
</View>
</>
);
}

/**
* Device-local legacy toggles. Mobile has no client-settings sync, so this is
* the counterpart of web's Settings → General → Legacy features backed by
Expand Down
14 changes: 14 additions & 0 deletions apps/mobile/src/state/agentSessions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { WS_METHODS } from "@t3tools/contracts";
import { createEnvironmentRpcCommand } from "@t3tools/client-runtime/state/runtime";

import { connectionAtomRuntime } from "../connection/runtime";

/**
* Manual trigger for an environment's background agent session importer
* (Claude Code / Codex history). Resolves with the importer's status at the
* moment the run was requested; the run itself continues server-side.
*/
export const agentSessionImportAll = createEnvironmentRpcCommand(connectionAtomRuntime, {
label: "environment-data:agent-sessions:import-all",
tag: WS_METHODS.agentSessionsImportAll,
});
11 changes: 11 additions & 0 deletions apps/server/integration/OrchestrationEngineHarness.integration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import * as NodeServices from "@effect/platform-node/NodeServices";
import {
ApprovalRequestId,
CodexSettings,
initialAgentSessionAutoImportStatus,
ProviderDriverKind,
type OrchestrationEvent,
type OrchestrationThread,
Expand Down Expand Up @@ -88,6 +89,7 @@ import { VcsStatusBroadcaster } from "../src/vcs/VcsStatusBroadcaster.ts";
import { GitWorkflowService } from "../src/git/GitWorkflowService.ts";
import * as VcsProcess from "../src/vcs/VcsProcess.ts";
import * as AgentAwarenessRelay from "../src/relay/AgentAwarenessRelay.ts";
import * as AgentSessionAutoImporter from "../src/project/AgentSessionAutoImporter.ts";
import * as PullRequestService from "../src/pullRequest/PullRequestService.ts";

const decodeCodexSettings = Schema.decodeEffect(CodexSettings);
Expand Down Expand Up @@ -412,6 +414,15 @@ export const makeOrchestrationIntegrationHarness = (
requestSync: () => Effect.void,
}),
),
Layer.provideMerge(
Layer.succeed(AgentSessionAutoImporter.AgentSessionAutoImporter, {
start: () => Effect.void,
runNow: Effect.succeed(initialAgentSessionAutoImportStatus),
status: Effect.succeed(initialAgentSessionAutoImportStatus),
streamStatus: Stream.empty,
drain: Effect.void,
}),
),
Layer.provideMerge(
Layer.succeed(AgentAwarenessRelay.AgentAwarenessRelay, {
publishThread: () => Effect.void,
Expand Down
3 changes: 3 additions & 0 deletions apps/server/src/auth/RpcAuthorization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,9 @@ export const RPC_REQUIRED_SCOPES = {
[WS_METHODS.filesystemBrowse]: AuthOrchestrationReadScope,
[WS_METHODS.agentSessionsScan]: AuthOrchestrationReadScope,
[WS_METHODS.agentSessionsImport]: AuthOrchestrationOperateScope,
[WS_METHODS.agentSessionsImportAll]: AuthOrchestrationOperateScope,
[WS_METHODS.agentSessionsStatus]: AuthOrchestrationReadScope,
[WS_METHODS.agentSessionsSubscribeStatus]: AuthOrchestrationReadScope,
[WS_METHODS.assetsCreateUrl]: AuthOrchestrationReadScope,
[WS_METHODS.attachmentsCreateUploadUrl]: AuthOrchestrationOperateScope,
[WS_METHODS.attachmentsDelete]: AuthOrchestrationOperateScope,
Expand Down
16 changes: 16 additions & 0 deletions apps/server/src/orchestration/Layers/OrchestrationReactor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import * as Exit from "effect/Exit";
import * as Layer from "effect/Layer";
import * as ManagedRuntime from "effect/ManagedRuntime";
import * as Scope from "effect/Scope";
import * as Stream from "effect/Stream";
import { initialAgentSessionAutoImportStatus } from "@t3tools/contracts";
import { afterEach, describe, expect, it } from "vite-plus/test";

import { CheckpointReactor } from "../Services/CheckpointReactor.ts";
Expand All @@ -14,6 +16,7 @@ import * as PullRequestSyncReactor from "../PullRequestSyncReactor.ts";
import * as ThreadPullRequestReactor from "../ThreadPullRequestReactor.ts";
import { OrchestrationReactor } from "../Services/OrchestrationReactor.ts";
import { makeOrchestrationReactor } from "./OrchestrationReactor.ts";
import * as AgentSessionAutoImporter from "../../project/AgentSessionAutoImporter.ts";
import * as AgentAwarenessRelay from "../../relay/AgentAwarenessRelay.ts";

describe("OrchestrationReactor", () => {
Expand Down Expand Up @@ -95,6 +98,18 @@ describe("OrchestrationReactor", () => {
requestSync: () => Effect.void,
}),
),
Layer.provideMerge(
Layer.succeed(AgentSessionAutoImporter.AgentSessionAutoImporter, {
start: () => {
started.push("agent-session-auto-importer");
return Effect.void;
},
runNow: Effect.succeed(initialAgentSessionAutoImportStatus),
status: Effect.succeed(initialAgentSessionAutoImportStatus),
streamStatus: Stream.empty,
drain: Effect.void,
}),
),
Layer.provideMerge(
Layer.succeed(AgentAwarenessRelay.AgentAwarenessRelay, {
publishThread: () => Effect.void,
Expand All @@ -119,6 +134,7 @@ describe("OrchestrationReactor", () => {
"thread-pull-request-reactor",
"thread-settlement-reactor",
"pull-request-sync-reactor",
"agent-session-auto-importer",
"agent-awareness-relay",
]);

Expand Down
Loading
Loading