Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
4828d09
feat(server): index worktrees for active agents, outside the repo
ScriptedAlchemy Nov 5, 2026
9d2a037
chore: merge origin/main into feat/code-index-lifecycle
ScriptedAlchemy Nov 5, 2026
561c7d5
fix(server): harden code index cleanup and refresh
ScriptedAlchemy Nov 5, 2026
b1180b2
fix(server): cleanup follows the checked-out branch
ScriptedAlchemy Nov 5, 2026
f43caf3
fix(server): threads learn their branch when a run ends
ScriptedAlchemy Nov 5, 2026
3333f67
refactor(server): launch rebinds threads through worktree branch sync
ScriptedAlchemy Nov 5, 2026
a0c3d64
test(server): a rebound thread recreates on its new branch
ScriptedAlchemy Nov 5, 2026
43bae8c
test(server): cleanup removes a deleted owner's worktree after a rebind
ScriptedAlchemy Nov 5, 2026
2bb2e5b
fix(server): harden code indexes on Windows
ScriptedAlchemy Nov 5, 2026
548561b
fix(server): leave nothing of the code index behind in repos or T3's …
ScriptedAlchemy Nov 5, 2026
d75ea81
test(server): abort the analysis process in a way Windows supports
ScriptedAlchemy Nov 5, 2026
699446c
fix(server): branch sync leaves foreign checkouts and held branches a…
ScriptedAlchemy Nov 5, 2026
226e233
fix(server): a prunable remote branch does not protect a detached commit
ScriptedAlchemy Nov 5, 2026
e6d8036
fix(server): cleanup keeps what removal would strand and rebinds ever…
ScriptedAlchemy Nov 5, 2026
3e1b66d
Merge commit 'e6d80364d39df6be55fae51b89163bd7c692c283' into HEAD
ScriptedAlchemy Nov 5, 2026
4ca3182
feat(server): Maintenance clears code indexes that are no longer rele…
ScriptedAlchemy Nov 5, 2026
8ed1de0
fix(server): cleanup keeps commits only a reflog holds on every worktree
ScriptedAlchemy Nov 5, 2026
9abec83
Merge commit '8ed1de0027d3c700c8ed429b839af0b6620c8e4a' into HEAD
ScriptedAlchemy Nov 5, 2026
053af18
fix(server): a same-named branch in another project keeps no worktree
ScriptedAlchemy Nov 5, 2026
1e1c348
Merge branch 'main' into fix/worktree-cleanup-branch-sync
ScriptedAlchemy Nov 6, 2026
c0182b7
chore(sync): merge updated base into code index lifecycle
ScriptedAlchemy Nov 6, 2026
6a9b28b
Merge remote-tracking branch 'origin/main' into HEAD
ScriptedAlchemy Nov 6, 2026
998e975
fix(index): recheck cleanup safety after stopping checkout indexes
ScriptedAlchemy Nov 6, 2026
6fe77ad
test(server): make cleanup fixtures portable to Windows
ScriptedAlchemy Nov 6, 2026
d9eaf21
test(server): host casing test passes on case-insensitive Macs
ScriptedAlchemy Nov 6, 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
4 changes: 2 additions & 2 deletions apps/mobile/src/features/files/FileCodeRelationships.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ function coverageLine(coverage: ProjectCodeCoverage): string {
case "stale":
return "Stale · some files changed since indexing. Verify in source.";
case "unavailable":
return "The code index is unavailable for this checkout.";
return "The code index is not ready for this checkout.";
}
}

Expand Down Expand Up @@ -220,7 +220,7 @@ function Body(props: {
return (
<ScrollView className="max-h-80" nestedScrollEnabled>
<Note>{coverageLine(data.coverage)}</Note>
{data.nodes.length === 0 ? (
{data.coverage.state === "unavailable" ? null : data.nodes.length === 0 ? (
<Note>No indexed declarations in this file. Its language may be unsupported.</Note>
) : (
data.nodes.map((node) => (
Expand Down
99 changes: 70 additions & 29 deletions apps/mobile/src/features/settings/ThreadSettingsSections.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -244,40 +244,81 @@ function BetaSettingsSection() {
environment.connection.phase === "connected" &&
environment.serverConfig?.environment.capabilities.peerLinks === true,
);
const writable = useEnvironmentsWithScope(supported, AuthSettingsWriteScope);
const indexing = environments.filter(
(environment) =>
environment.connection.phase === "connected" &&
environment.serverConfig?.environment.capabilities.projectCode === true,
);
const writable = useEnvironmentsWithScope(environments, AuthSettingsWriteScope);
const updateSettings = useAtomCommand(serverEnvironment.updateSettings, {
label: "linked environments beta update",
reportFailure: true,
});
if (supported.length === 0) return null;
if (supported.length === 0 && indexing.length === 0) return null;
return (
<View className="gap-3">
<SettingsSection title="Beta: Linked environments">
{supported.map((environment) => {
const policy = environment.serverConfig?.managedPolicy;
const managed = isManagedSettingPath(policy, "linkedEnvironmentsEnabled");
return (
<SettingsSwitchRow
key={environment.environmentId}
icon="link"
label={environment.label}
{...(managed ? { subtitle: `Managed by ${policy?.organization}.` } : {})}
value={environment.serverConfig?.settings.linkedEnvironmentsEnabled === true}
disabled={managed || !writable.has(environment.environmentId)}
onValueChange={(value) =>
void updateSettings({
environmentId: environment.environmentId,
input: { patch: { linkedEnvironmentsEnabled: value } },
})
}
/>
);
})}
</SettingsSection>
<Text className="px-2 text-sm text-foreground-muted">
Let agents on a machine work on your other machines, and theirs on it. Turn it on for each
machine. While it is off, nothing new starts over a link; tasks already running finish.
</Text>
<View className="gap-6">
{supported.length === 0 ? null : (
<View className="gap-3">
<SettingsSection title="Beta: Linked environments">
{supported.map((environment) => {
const policy = environment.serverConfig?.managedPolicy;
const managed = isManagedSettingPath(policy, "linkedEnvironmentsEnabled");
return (
<SettingsSwitchRow
key={environment.environmentId}
icon="link"
label={environment.label}
{...(managed ? { subtitle: `Managed by ${policy?.organization}.` } : {})}
value={environment.serverConfig?.settings.linkedEnvironmentsEnabled === true}
disabled={managed || !writable.has(environment.environmentId)}
onValueChange={(value) =>
void updateSettings({
environmentId: environment.environmentId,
input: { patch: { linkedEnvironmentsEnabled: value } },
})
}
/>
);
})}
</SettingsSection>
<Text className="px-2 text-sm text-foreground-muted">
Let agents on a machine work on your other machines, and theirs on it. Turn it on for
each machine. While it is off, nothing new starts over a link; tasks already running
finish.
</Text>
</View>
)}
{indexing.length === 0 ? null : (
<View className="gap-3">
<SettingsSection title="Beta: Automatic code indexing">
{indexing.map((environment) => {
const policy = environment.serverConfig?.managedPolicy;
const managed = isManagedSettingPath(policy, "autoCodeIndexEnabled");
return (
<SettingsSwitchRow
key={environment.environmentId}
icon="magnifyingglass"
label={environment.label}
{...(managed ? { subtitle: `Managed by ${policy?.organization}.` } : {})}
value={environment.serverConfig?.settings.autoCodeIndexEnabled !== false}
disabled={managed || !writable.has(environment.environmentId)}
onValueChange={(value) =>
void updateSettings({
environmentId: environment.environmentId,
input: { patch: { autoCodeIndexEnabled: value } },
})
}
/>
);
})}
</SettingsSection>
<Text className="px-2 text-sm text-foreground-muted">
Index a project when you add it, and a worktree while an agent works in it. A worktree's
index is removed when its thread settles. While this is off, a checkout only indexes
when something asks it a code question.
</Text>
</View>
)}
</View>
);
}
Expand Down
3 changes: 3 additions & 0 deletions apps/server/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export interface ServerDerivedPaths {
readonly providerStatusCacheDir: string;
readonly worktreesDir: string;
readonly attachmentsDir: string;
readonly codeIndexDir: string;
readonly browserArtifactsDir: string;
readonly logsDir: string;
readonly serverTracePath: string;
Expand Down Expand Up @@ -161,6 +162,7 @@ export const deriveServerPaths = Effect.fn(function* (
providerStatusCacheDir,
worktreesDir: join(baseDir, "worktrees"),
attachmentsDir,
codeIndexDir: join(stateDir, "code-index"),
browserArtifactsDir: join(stateDir, "browser-artifacts"),
logsDir,
serverTracePath: join(logsDir, "server.trace.ndjson"),
Expand All @@ -185,6 +187,7 @@ export const ensureServerDirectories = Effect.fn(function* (derivedPaths: Server
fs.makeDirectory(derivedPaths.providerLogsDir, { recursive: true }),
fs.makeDirectory(derivedPaths.terminalLogsDir, { recursive: true }),
fs.makeDirectory(derivedPaths.attachmentsDir, { recursive: true }),
fs.makeDirectory(derivedPaths.codeIndexDir, { recursive: true }),
fs.makeDirectory(derivedPaths.worktreesDir, { recursive: true }),
fs.makeDirectory(path.dirname(derivedPaths.keybindingsConfigPath), { recursive: true }),
fs.makeDirectory(path.dirname(derivedPaths.settingsPath), { recursive: true }),
Expand Down
32 changes: 29 additions & 3 deletions apps/server/src/orchestration-v2/RunFinalizationService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
ProviderInstanceId,
RunId,
ThreadId,
VcsProcessExitError,
type OrchestrationV2ThreadShell,
} from "@t3tools/contracts";
import * as Deferred from "effect/Deferred";
Expand All @@ -29,9 +30,12 @@ import * as TestClock from "effect/testing/TestClock";

import * as ServerConfig from "../config.ts";
import * as CodeIntelligence from "../project/knowledge/CodeIntelligence.ts";
import { indexRootFor } from "../project/knowledge/codeIndexLayout.ts";
import * as ModelEvaluations from "../modelEvaluations/ModelEvaluations.ts";
import * as PullRequestService from "../pullRequest/PullRequestService.ts";
import * as ServerSettings from "../serverSettings.ts";
import * as GitVcsDriver from "../vcs/GitVcsDriver.ts";
import * as VcsDriver from "../vcs/VcsDriver.ts";
import * as VcsStatusBroadcaster from "../vcs/VcsStatusBroadcaster.ts";
import * as WorkspaceEntries from "../workspace/WorkspaceEntries.ts";
import * as WorkspacePaths from "../workspace/WorkspacePaths.ts";
Expand Down Expand Up @@ -425,6 +429,13 @@ it.effect("finalizes a run without waiting for the code index refresh its checkp
const threadId = ThreadId.make("thread_index_refresh");
const runId = RunId.make("run_index_refresh");
const scopeId = CheckpointScopeId.make("scope_index_refresh");
const configContext = yield* Layer.build(
ServerConfig.layerTest(process.cwd(), { prefix: "t3-finalize-index-config-" }).pipe(
Layer.provide(NodeServices.layer),
),
);
const config = yield* ServerConfig.ServerConfig.pipe(Effect.provide(configContext));
const indexRoot = indexRootFor(config.codeIndexDir, root);
const layer = RunFinalization.layer.pipe(
Layer.provideMerge(RunFinalization.layerObserver),
Layer.provideMerge(CodeIntelligence.layer),
Expand Down Expand Up @@ -465,6 +476,19 @@ it.effect("finalizes a run without waiting for the code index refresh its checkp
truncated: false,
}),
}),
Layer.mock(VcsDriver.VcsDriver)({
checkpoints: {} as VcsDriver.VcsDriver["Service"]["checkpoints"],
listWorkspaceFiles: () =>
Effect.fail(
new VcsProcessExitError({
operation: "test",
command: "git ls-files",
cwd: root,
exitCode: 128,
detail: "not a git repository",
}),
),
}),
Layer.mock(PullRequestService.PullRequestService)({
refreshAfterTurn: () => Effect.void,
}),
Expand All @@ -479,6 +503,8 @@ it.effect("finalizes a run without waiting for the code index refresh its checkp
workingTree: { files: [], insertions: 0, deletions: 0 },
}),
}),
ServerSettings.layerTest(),
Layer.succeed(ServerConfig.ServerConfig, config),
),
),
);
Expand All @@ -492,12 +518,12 @@ it.effect("finalizes a run without waiting for the code index refresh its checkp
const finalize = finalization.finalize({ threadId, runId, scopeId });
const published = () =>
Effect.promise(() =>
NodeFSP.readFile(NodePath.join(root, ".codegraph-t3/published"), "utf8"),
NodeFSP.readFile(NodePath.join(indexRoot, ".codegraph-t3/published"), "utf8"),
);

// A checkout nobody has queried is not indexed behind the user's back.
yield* finalize;
assert.isFalse(NodeFS.existsSync(NodePath.join(root, ".codegraph-t3")));
assert.isFalse(NodeFS.existsSync(indexRoot));

assert.deepEqual(yield* intelligence.refresh({ projectId }), {
_tag: "Published",
Expand All @@ -510,7 +536,7 @@ it.effect("finalizes a run without waiting for the code index refresh its checkp
const buildHeld = yield* Deferred.make<void>();
const releaseBuild = yield* Deferred.make<void>();
const lease = yield* withWorkspaceLease(
NodePath.join(root, ".codegraph-t3"),
indexRoot,
Deferred.succeed(buildHeld, undefined).pipe(
Effect.andThen(Deferred.await(releaseBuild)),
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ export function makeReplayServerConfig(
const providerLogsDir = path.join(logsDir, "provider");
const terminalLogsDir = path.join(logsDir, "terminals");
const attachmentsDir = path.join(stateDir, "attachments");
const codeIndexDir = path.join(stateDir, "code-index");
const environmentThemesDir = path.join(stateDir, "themes");
const worktreesDir = path.join(baseDir, "worktrees");
const providerStatusCacheDir = path.join(baseDir, "caches");
Expand All @@ -109,6 +110,7 @@ export function makeReplayServerConfig(
providerLogsDir,
terminalLogsDir,
attachmentsDir,
codeIndexDir,
environmentThemesDir,
worktreesDir,
providerStatusCacheDir,
Expand Down Expand Up @@ -152,6 +154,7 @@ export function makeReplayServerConfig(
providerStatusCacheDir,
worktreesDir,
attachmentsDir,
codeIndexDir: path.join(stateDir, "code-index"),
browserArtifactsDir: path.join(stateDir, "browser-artifacts"),
environmentThemesDir,
logsDir,
Expand Down
Loading
Loading