Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
767954b
Unify launch env resolution on the server.
tarik02 Jun 8, 2026
42247da
Fix terminal launch env resolution to stop server crash-loops.
tarik02 Jun 9, 2026
192e37e
Fix terminal open for draft threads missing from server projection.
tarik02 Jun 9, 2026
d971efe
Merge branch 'main' of github.com:pingdotgg/t3code into unify-launch-env
tarik02 Jun 9, 2026
44f48a3
Fix provider launch env leakage and tighten terminal env binding.
tarik02 Jun 9, 2026
a089db8
next part of wip, more coming...
tarik02 Jun 9, 2026
d0e8dd0
refactor: simplify LaunchEnv layer composition for cleaner architecture
tarik02 Jun 10, 2026
64edec7
refactor: remove redundant LaunchEnvLive layer file and fix duplicate…
tarik02 Jun 10, 2026
b62eb31
refactor: split LaunchEnv service into contract and implementation
tarik02 Jun 10, 2026
d8c18c8
refactor: remove unnecessary comments
tarik02 Jun 10, 2026
21d2d35
Merge branch 'main' of https://github.com/pingdotgg/t3code into unify…
tarik02 Jun 10, 2026
fb4e646
refactor
tarik02 Jun 10, 2026
acb44a5
fix launch env review and test failures
tarik02 Jun 10, 2026
a2bde53
Merge branch 'main' of github.com:pingdotgg/t3code into unify-launch-env
tarik02 Jun 10, 2026
0aa17fd
revert unrelated cloud test fixes
tarik02 Jun 10, 2026
fafe970
clean up launch env review fixes
tarik02 Jun 10, 2026
4d5d682
final fixes (hopefully)
tarik02 Jun 10, 2026
d362892
preserve launch env in provider sessions
tarik02 Jun 12, 2026
f189f37
Merge remote-tracking branch 'upstream/main' into HEAD
tarik02 Jun 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
7 changes: 1 addition & 6 deletions apps/mobile/src/features/threads/ThreadRouteScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import * as Arr from "effect/Array";
import * as Option from "effect/Option";
import { pipe } from "effect/Function";
import { EnvironmentId, type ProjectScript } from "@t3tools/contracts";
import { projectScriptCwd, projectScriptRuntimeEnv } from "@t3tools/shared/projectScripts";
import { projectScriptCwd } from "@t3tools/shared/projectScripts";
import { Pressable, ScrollView, Text as RNText, View, useColorScheme } from "react-native";
import { useThemeColor } from "../../lib/useThemeColor";
import { useVcsStatus } from "../../state/use-vcs-status";
Expand Down Expand Up @@ -198,10 +198,6 @@ export function ThreadRouteScreen() {
project: { cwd: selectedThreadProject.workspaceRoot },
worktreePath: preferredWorktreePath,
});
const env = projectScriptRuntimeEnv({
project: { cwd: selectedThreadProject.workspaceRoot },
worktreePath: preferredWorktreePath,
});
stagePendingTerminalLaunch({
target: {
environmentId: selectedThread.environmentId,
Expand All @@ -211,7 +207,6 @@ export function ThreadRouteScreen() {
launch: {
cwd,
worktreePath: preferredWorktreePath,
env,
initialInput: `${script.command}\r`,
},
});
Expand Down
21 changes: 7 additions & 14 deletions apps/mobile/src/state/use-terminal-session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,9 @@ import {
terminalSessionStateAtom,
type TerminalSessionTarget,
type TerminalSessionState,
type TerminalAttachSessionInput,
} from "@t3tools/client-runtime";
import type {
EnvironmentId,
TerminalAttachInput,
TerminalAttachStreamEvent,
TerminalMetadataStreamEvent,
TerminalSessionSnapshot,
} from "@t3tools/contracts";
import type { EnvironmentId, TerminalMetadataStreamEvent } from "@t3tools/contracts";
import { useMemo } from "react";

import { appAtomRegistry } from "./atom-registry";
Expand All @@ -39,13 +34,11 @@ export function subscribeTerminalMetadata(input: {
return terminalSessionManager.subscribeMetadata(input);
}

export function attachTerminalSession(input: {
readonly environmentId: EnvironmentId;
readonly client: Parameters<typeof terminalSessionManager.attach>[0]["client"];
readonly terminal: TerminalAttachInput;
readonly onSnapshot?: (snapshot: TerminalSessionSnapshot) => void;
readonly onEvent?: (event: TerminalAttachStreamEvent) => void;
}) {
export function attachTerminalSession(
input: TerminalAttachSessionInput & {
readonly environmentId: EnvironmentId;
},
) {
return terminalSessionManager.attach({
environmentId: input.environmentId,
client: input.client,
Expand Down
28 changes: 21 additions & 7 deletions apps/server/integration/OrchestrationEngineHarness.integration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import { makeAdapterRegistryMock } from "../src/provider/testUtils/providerAdapt
import { ProviderAdapterRegistry } from "../src/provider/Services/ProviderAdapterRegistry.ts";
import { makeProviderRegistryLayer } from "../src/provider/testUtils/providerRegistryMock.ts";
import { ProviderSessionDirectoryLive } from "../src/provider/Layers/ProviderSessionDirectory.ts";
import { LaunchEnvLive } from "../src/launchEnv/Layers/LaunchEnvLive.ts";
import { ServerSettingsService } from "../src/serverSettings.ts";
import { makeProviderServiceLive } from "../src/provider/Layers/ProviderService.ts";
import { makeCodexAdapter } from "../src/provider/Layers/CodexAdapter.ts";
Expand Down Expand Up @@ -259,12 +260,16 @@ export const makeOrchestrationIntegrationHarness = (

const persistenceLayer = makeSqlitePersistenceLive(dbPath);
const orchestrationLayer = OrchestrationEngineLive.pipe(
Layer.provide(OrchestrationProjectionSnapshotQueryLive),
Layer.provide(OrchestrationProjectionPipelineLive),
Layer.provide(OrchestrationEventStoreLive),
Layer.provide(OrchestrationCommandReceiptRepositoryLive),
Layer.provide(RepositoryIdentityResolverLive),
Layer.provide(persistenceLayer),
);
const providerSessionDirectoryLayer = ProviderSessionDirectoryLive.pipe(
Layer.provide(ProviderSessionRuntimeRepositoryLive),
Layer.provide(persistenceLayer),
);
const realCodexRegistry = Layer.effect(
ProviderAdapterRegistry,
Expand Down Expand Up @@ -297,12 +302,15 @@ export const makeOrchestrationIntegrationHarness = (
const providerRegistryLayer = makeProviderRegistryLayer();

const checkpointStoreLayer = CheckpointStoreLive.pipe(Layer.provide(VcsDriverRegistry.layer));
const projectionSnapshotQueryLayer = OrchestrationProjectionSnapshotQueryLive;
const projectionSnapshotQueryLayer = OrchestrationProjectionSnapshotQueryLive.pipe(
Layer.provide(RepositoryIdentityResolverLive),
Layer.provide(persistenceLayer),
);
const runtimeServicesLayer = Layer.mergeAll(
projectionSnapshotQueryLayer,
orchestrationLayer.pipe(Layer.provide(projectionSnapshotQueryLayer)),
ProjectionCheckpointRepositoryLive,
ProjectionPendingApprovalRepositoryLive,
orchestrationLayer,
ProjectionCheckpointRepositoryLive.pipe(Layer.provide(persistenceLayer)),
ProjectionPendingApprovalRepositoryLive.pipe(Layer.provide(persistenceLayer)),
checkpointStoreLayer,
providerLayer,
RuntimeReceiptBusTest,
Expand Down Expand Up @@ -374,14 +382,20 @@ export const makeOrchestrationIntegrationHarness = (
}),
),
);
const serverConfigLayer = ServerConfig.layerTest(workspaceDir, rootDir);
const layer = Layer.empty.pipe(
Layer.provideMerge(runtimeServicesLayer),
Layer.provideMerge(orchestrationReactorLayer),
Layer.provideMerge(providerRegistryLayer),
Layer.provide(persistenceLayer),
Layer.provideMerge(RepositoryIdentityResolverLive),
Layer.provideMerge(ServerSettingsService.layerTest()),
Layer.provideMerge(ServerConfig.layerTest(workspaceDir, rootDir)),
Layer.provideMerge(serverConfigLayer),
Layer.provide(persistenceLayer),
Layer.provideMerge(
LaunchEnvLive.pipe(
Layer.provide(serverConfigLayer),
Layer.provide(projectionSnapshotQueryLayer),
),
),
Layer.provideMerge(NodeServices.layer),
);

Expand Down
116 changes: 116 additions & 0 deletions apps/server/src/launchEnv/Layers/LaunchEnvLive.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { ThreadId } from "@t3tools/contracts";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import * as Option from "effect/Option";

import { ServerConfig } from "../../config.ts";
import { ProjectionSnapshotQuery } from "../../orchestration/Services/ProjectionSnapshotQuery.ts";
import { LaunchEnv, type LaunchEnvShape } from "../Services/LaunchEnv.ts";
import { mergeResolvedLaunchEnv } from "../launchEnvUtils.ts";
import {
LaunchEnvProjectLookupError,
LaunchEnvThreadLookupError,
} from "../Services/LaunchEnvErrors.ts";

export const makeLaunchEnv = Effect.fn("makeLaunchEnv")(function* () {
const serverConfig = yield* ServerConfig;
const projectionSnapshotQuery = yield* ProjectionSnapshotQuery;

const resolve: LaunchEnvShape["resolve"] = (input) =>
Effect.succeed(
mergeResolvedLaunchEnv({
t3Home: serverConfig.baseDir,
...(input.extraEnv !== undefined ? { extraEnv: input.extraEnv } : {}),
context: {
projectRoot: input.projectRoot,
projectId: String(input.projectId),
threadId: String(input.threadId),
worktreePath: input.worktreePath ?? undefined,
},
}),
);

const resolveForThread: LaunchEnvShape["resolveForThread"] = Effect.fn(
"LaunchEnv.resolveForThread",
)(function* (input) {
const threadOption = yield* projectionSnapshotQuery
.getThreadShellById(ThreadId.make(input.threadId))
.pipe(
Effect.mapError(
(cause) =>
new LaunchEnvThreadLookupError({
threadId: input.threadId,
terminalId: input.terminalId,
cause,
}),
),
);

const { projectId, worktreePath } = yield* Option.match(threadOption, {
onSome: (thread) =>
Effect.succeed({
projectId: thread.projectId,
worktreePath: input.worktreePath !== undefined ? input.worktreePath : thread.worktreePath,
}),
onNone: () => {
if (input.projectId === undefined) {
return Effect.fail(
new LaunchEnvThreadLookupError({
threadId: input.threadId,
terminalId: input.terminalId,
}),
);
}

return Effect.succeed({
projectId: input.projectId,
...(input.worktreePath !== undefined ? { worktreePath: input.worktreePath } : {}),
});
},
});

const projectOption = yield* projectionSnapshotQuery.getProjectShellById(projectId).pipe(
Effect.mapError(
(cause) =>
new LaunchEnvProjectLookupError({
projectId: String(projectId),
reason: "statFailed",
cause,
}),
),
);

const project = yield* Option.match(projectOption, {
onSome: Effect.succeed,
onNone: () =>
Effect.fail(
new LaunchEnvProjectLookupError({
projectId: String(projectId),
reason: "notFound",
}),
),
});

const env: Record<string, string> = yield* resolve({
...(input.extraEnv !== undefined ? { extraEnv: input.extraEnv } : {}),
projectRoot: project.workspaceRoot,
projectId: project.id,
threadId: input.threadId,
...(worktreePath !== undefined ? { worktreePath } : {}),
});

const result = {
projectId,
worktreePath,
env,
};
return result;
});

return {
resolve,
resolveForThread,
} satisfies LaunchEnvShape;
});

export const LaunchEnvLive = Layer.effect(LaunchEnv, makeLaunchEnv());
Loading
Loading