Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
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
2 changes: 1 addition & 1 deletion .docs/scripts.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
- `bun run dev:web` — Starts just the Vite dev server for the web app.
- Dev commands default `T3CODE_STATE_DIR` to `~/.t3/dev` to keep dev state isolated from desktop/prod state.
- Override server CLI-equivalent flags from root dev commands with `--`, for example:
`bun run dev -- --state-dir ~/.t3/another-dev-state`
`bun run dev -- --base-dir ~/.t3-2`
Comment on lines 6 to +8
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if T3CODE_STATE_DIR is still used anywhere in the codebase
echo "=== Searching for T3CODE_STATE_DIR usage ==="
rg -n "T3CODE_STATE_DIR" --type-add 'config:*.{json,md,ts,js}' -t config

echo ""
echo "=== Searching for T3CODE_HOME usage ==="
rg -n "T3CODE_HOME" --type-add 'config:*.{json,md,ts,js}' -t config

Repository: aaditagrawal/t3code

Length of output: 1631


Update T3CODE_STATE_DIR to T3CODE_HOME on line 6.

The environment variable was renamed to T3CODE_HOME throughout the codebase. Line 6 still references the obsolete T3CODE_STATE_DIR, which no longer exists in the project. Update to match the current variable name and be consistent with line 8's correct usage of --base-dir.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.docs/scripts.md around lines 6 - 8, Update the docs to use the new
environment variable name: replace the obsolete T3CODE_STATE_DIR with
T3CODE_HOME in the sentence that describes defaulting dev commands; specifically
change the phrase "Dev commands default `T3CODE_STATE_DIR` to `~/.t3/dev`" to
use `T3CODE_HOME` so it matches the renamed variable used throughout the
codebase and remains consistent with the `--base-dir` example.

- `bun run start` — Runs the production server (serves built web app as static files).
- `bun run build` — Builds contracts, web app, and server through Turbo.
- `bun run typecheck` — Strict TypeScript checks for all packages.
Expand Down
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,5 @@ release/
apps/web/.playwright
apps/web/playwright-report
apps/web/src/components/__screenshots__
.vitest-*
.vitest-*
__screenshots__/
2 changes: 1 addition & 1 deletion REMOTE.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ The T3 Code CLI accepts the following configuration options, available either as
| `--mode <web\|desktop>` | `T3CODE_MODE` | Runtime mode. |
| `--port <number>` | `T3CODE_PORT` | HTTP/WebSocket port. |
| `--host <address>` | `T3CODE_HOST` | Bind interface/address. |
| `--state-dir <path>` | `T3CODE_STATE_DIR` | State directory. |
| `--base-dir <path>` | `T3CODE_HOME` | Base directory. |
| `--dev-url <url>` | `VITE_DEV_SERVER_URL` | Dev web URL redirect/proxy target. |
| `--no-browser` | `T3CODE_NO_BROWSER` | Disable auto-open browser. |
| `--auth-token <token>` | `T3CODE_AUTH_TOKEN` | WebSocket auth token. |
Expand Down
6 changes: 3 additions & 3 deletions apps/desktop/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,8 @@ const LOG_DIR_CHANNEL = "desktop:log-dir";
const LOG_LIST_CHANNEL = "desktop:log-list";
const LOG_READ_CHANNEL = "desktop:log-read";
const LOG_OPEN_DIR_CHANNEL = "desktop:log-open-dir";
const STATE_DIR =
process.env.T3CODE_STATE_DIR?.trim() || Path.join(OS.homedir(), ".t3", "userdata");
const BASE_DIR = process.env.T3CODE_HOME?.trim() || Path.join(OS.homedir(), ".t3");
const STATE_DIR = Path.join(BASE_DIR, "userdata");
Comment on lines +64 to +65
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Resolve BASE_DIR to an absolute path to avoid desktop/backend path drift.

If T3CODE_HOME is relative, desktop log paths can diverge from backend paths (different CWDs). Normalize once at startup.

Suggested fix
-const BASE_DIR = process.env.T3CODE_HOME?.trim() || Path.join(OS.homedir(), ".t3");
+const BASE_DIR = Path.resolve(process.env.T3CODE_HOME?.trim() || Path.join(OS.homedir(), ".t3"));
 const STATE_DIR = Path.join(BASE_DIR, "userdata");

Also applies to: 932-933

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/main.ts` around lines 64 - 65, BASE_DIR is currently taken
directly from process.env.T3CODE_HOME which can be relative and cause path
drift; normalize it to an absolute path at startup (e.g., compute BASE_DIR from
process.env.T3CODE_HOME if present, otherwise default to Path.join(OS.homedir(),
".t3"), then pass that value through Path.resolve or Path.normalize to produce
an absolute BASE_DIR) and then derive STATE_DIR from that resolved BASE_DIR so
both desktop and backend use the same absolute path; update the BASE_DIR and
STATE_DIR assignments (symbols: BASE_DIR, STATE_DIR, T3CODE_HOME) accordingly.

const DESKTOP_SCHEME = "t3";
const ROOT_DIR = Path.resolve(__dirname, "../../..");
const isDevelopment = Boolean(process.env.VITE_DEV_SERVER_URL);
Expand Down Expand Up @@ -929,7 +929,7 @@ function backendEnv(): NodeJS.ProcessEnv {
T3CODE_MODE: "desktop",
T3CODE_NO_BROWSER: "1",
T3CODE_PORT: String(backendPort),
T3CODE_STATE_DIR: STATE_DIR,
T3CODE_HOME: BASE_DIR,
T3CODE_AUTH_TOKEN: backendAuthToken,
};
}
Expand Down
48 changes: 24 additions & 24 deletions apps/server/integration/OrchestrationEngineHarness.integration.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { execFileSync } from "node:child_process";

import * as NodeServices from "@effect/platform-node/NodeServices";
Expand All @@ -13,9 +10,11 @@ import {
import {
Effect,
Exit,
FileSystem,
Layer,
ManagedRuntime,
Option,
Path,
Ref,
Schedule,
Schema,
Expand Down Expand Up @@ -66,7 +65,7 @@ import {
makeTestProviderAdapterHarness,
type TestProviderAdapterHarness,
} from "./TestProviderAdapter.integration.ts";
import { ServerConfig } from "../src/config.ts";
import { deriveServerPaths, ServerConfig } from "../src/config.ts";

function runGit(cwd: string, args: ReadonlyArray<string>) {
return execFileSync("git", args, {
Expand All @@ -76,14 +75,16 @@ function runGit(cwd: string, args: ReadonlyArray<string>) {
});
}

function initializeGitWorkspace(cwd: string) {
const initializeGitWorkspace = Effect.fn(function* (cwd: string) {
runGit(cwd, ["init", "--initial-branch=main"]);
runGit(cwd, ["config", "user.email", "test@example.com"]);
runGit(cwd, ["config", "user.name", "Test User"]);
fs.writeFileSync(path.join(cwd, "README.md"), "v1\n", "utf8");
const fileSystem = yield* FileSystem.FileSystem;
const { join } = yield* Path.Path;
yield* fileSystem.writeFileString(join(cwd, "README.md"), "v1\n");
runGit(cwd, ["add", "."]);
runGit(cwd, ["commit", "-m", "Initial"]);
}
});

export function gitRefExists(cwd: string, ref: string): boolean {
try {
Expand Down Expand Up @@ -214,7 +215,9 @@ export const makeOrchestrationIntegrationHarness = (
options?: MakeOrchestrationIntegrationHarnessOptions,
) =>
Effect.gen(function* () {
const sleep = (ms: number) => Effect.sleep(ms);
const path = yield* Path.Path;
const fileSystem = yield* FileSystem.FileSystem;

const provider = options?.provider ?? "codex";
const useRealCodex = options?.realCodex === true;
const adapterHarness = useRealCodex
Expand All @@ -231,13 +234,16 @@ export const makeOrchestrationIntegrationHarness = (
listProviders: () => Effect.succeed([adapterHarness.provider]),
} as typeof ProviderAdapterRegistry.Service)
: null;
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "t3-orchestration-integration-"));
const rootDir = yield* fileSystem.makeTempDirectoryScoped({
prefix: "t3-orchestration-integration-",
});
const workspaceDir = path.join(rootDir, "workspace");
const stateDir = path.join(rootDir, "state");
const dbPath = path.join(stateDir, "state.sqlite");
fs.mkdirSync(workspaceDir, { recursive: true });
fs.mkdirSync(stateDir, { recursive: true });
initializeGitWorkspace(workspaceDir);
const { stateDir, dbPath } = yield* deriveServerPaths(rootDir, undefined).pipe(
Effect.provideService(Path.Path, path),
);
yield* fileSystem.makeDirectory(workspaceDir, { recursive: true });
yield* fileSystem.makeDirectory(stateDir, { recursive: true });
yield* initializeGitWorkspace(workspaceDir);
Comment on lines +237 to +246
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n -C3 'makeTempDirectoryScoped|Scope\.make|Scope\.close|dispose = Effect\.gen' apps/server/integration/OrchestrationEngineHarness.integration.ts

Repository: aaditagrawal/t3code

Length of output: 1233


Tie the temp directory to the harness-owned lifecycle.

rootDir is acquired with makeTempDirectoryScoped, but dispose only closes the reactor scope (line 353 and 464). The temp directory lifetime is now decoupled from the harness lifecycle: it will be cleaned up by the outer generator scope, not by dispose. Create rootDir under a harness-owned Scope and close that same scope in dispose, or restore explicit cleanup.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/server/integration/OrchestrationEngineHarness.integration.ts` around
lines 237 - 246, rootDir is created with makeTempDirectoryScoped but its scope
is not closed in the harness, so the temp directory outlives the harness;
allocate a harness-owned Scope (e.g., harnessScope) and call
makeTempDirectoryScoped within that scope to produce rootDir (or capture the
returned disposal handle) and store the scope/handle on the harness; then update
dispose to close that same harnessScope (or call the temp-directory disposal) so
the temporary directory is reliably cleaned up when dispose runs; refer to
makeTempDirectoryScoped, rootDir, deriveServerPaths, and dispose to locate where
to attach and close the scope.


const persistenceLayer = makeSqlitePersistenceLive(dbPath);
const orchestrationLayer = OrchestrationEngineLive.pipe(
Expand All @@ -262,7 +268,7 @@ export const makeOrchestrationIntegrationHarness = (
}),
).pipe(
Layer.provide(makeCodexAdapterLive()),
Layer.provideMerge(ServerConfig.layerTest(workspaceDir, stateDir)),
Layer.provideMerge(ServerConfig.layerTest(workspaceDir, rootDir)),
Layer.provideMerge(NodeServices.layer),
Layer.provideMerge(providerSessionDirectoryLayer),
);
Expand Down Expand Up @@ -312,7 +318,7 @@ export const makeOrchestrationIntegrationHarness = (
);
const layer = orchestrationReactorLayer.pipe(
Layer.provide(persistenceLayer),
Layer.provideMerge(ServerConfig.layerTest(workspaceDir, stateDir)),
Layer.provideMerge(ServerConfig.layerTest(workspaceDir, rootDir)),
Layer.provideMerge(NodeServices.layer),
);

Expand Down Expand Up @@ -352,7 +358,7 @@ export const makeOrchestrationIntegrationHarness = (
yield* Stream.runForEach(runtimeReceiptBus.stream, (receipt) =>
Ref.update(receiptHistory, (history) => [...history, receipt]).pipe(Effect.asVoid),
).pipe(Effect.forkIn(scope));
yield* sleep(10);
yield* Effect.sleep(10);

const waitForThread: OrchestrationIntegrationHarness["waitForThread"] = (
threadId,
Expand Down Expand Up @@ -469,13 +475,7 @@ export const makeOrchestrationIntegrationHarness = (
}
});

yield* shutdown.pipe(
Effect.ensuring(
Effect.sync(() => {
fs.rmSync(rootDir, { recursive: true, force: true });
}),
),
);
yield* shutdown;
});

return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import type {
CheckpointDiffFinalizedReceipt,
TurnProcessingQuiescedReceipt,
} from "../src/orchestration/Services/RuntimeReceiptBus.ts";
import * as NodeServices from "@effect/platform-node/NodeServices";

const asMessageId = (value: string): MessageId => MessageId.makeUnsafe(value);
const asProjectId = (value: string): ProjectId => ProjectId.makeUnsafe(value);
Expand All @@ -51,8 +52,6 @@ class IntegrationWaitTimeoutError extends Schema.TaggedErrorClass<IntegrationWai
},
) {}

const sleep = (ms: number) => Effect.sleep(ms);

function waitForSync<A>(
read: () => A,
predicate: (value: A) => boolean,
Expand All @@ -70,7 +69,7 @@ function waitForSync<A>(
if (Date.now() >= deadline) {
return yield* Effect.die(new IntegrationWaitTimeoutError({ description }));
}
yield* sleep(10);
yield* Effect.sleep(10);
}
});
}
Expand All @@ -91,7 +90,7 @@ function withHarness<A, E>(
makeOrchestrationIntegrationHarness({ provider }),
use,
(harness) => harness.dispose,
);
).pipe(Effect.provide(NodeServices.layer));
}

function withRealCodexHarness<A, E>(
Expand All @@ -101,7 +100,7 @@ function withRealCodexHarness<A, E>(
makeOrchestrationIntegrationHarness({ provider: "codex", realCodex: true }),
use,
(harness) => harness.dispose,
);
).pipe(Effect.provide(NodeServices.layer));
}

const seedProjectAndThread = (harness: OrchestrationIntegrationHarness) =>
Expand Down
4 changes: 2 additions & 2 deletions apps/server/src/attachmentPaths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,15 @@ export function normalizeAttachmentRelativePath(rawRelativePath: string): string
}

export function resolveAttachmentRelativePath(input: {
readonly stateDir: string;
readonly attachmentsDir: string;
readonly relativePath: string;
}): string | null {
const normalizedRelativePath = normalizeAttachmentRelativePath(input.relativePath);
if (!normalizedRelativePath) {
return null;
}

const attachmentsRoot = path.resolve(path.join(input.stateDir, "attachments"));
const attachmentsRoot = path.resolve(input.attachmentsDir);
const filePath = path.resolve(path.join(attachmentsRoot, normalizedRelativePath));
if (!filePath.startsWith(`${attachmentsRoot}${path.sep}`)) {
return null;
Expand Down
14 changes: 6 additions & 8 deletions apps/server/src/attachmentStore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,34 +44,32 @@ describe("attachmentStore", () => {
});

it("resolves attachment path by id using the extension that exists on disk", () => {
const stateDir = fs.mkdtempSync(path.join(os.tmpdir(), "t3code-attachment-store-"));
const attachmentsDir = fs.mkdtempSync(path.join(os.tmpdir(), "t3code-attachment-store-"));
try {
const attachmentId = "thread-1-attachment";
const attachmentsDir = path.join(stateDir, "attachments");
fs.mkdirSync(attachmentsDir, { recursive: true });
const pngPath = path.join(attachmentsDir, `${attachmentId}.png`);
fs.writeFileSync(pngPath, Buffer.from("hello"));

const resolved = resolveAttachmentPathById({
stateDir,
attachmentsDir,
attachmentId,
});
expect(resolved).toBe(pngPath);
} finally {
fs.rmSync(stateDir, { recursive: true, force: true });
fs.rmSync(attachmentsDir, { recursive: true, force: true });
}
});

it("returns null when no attachment file exists for the id", () => {
const stateDir = fs.mkdtempSync(path.join(os.tmpdir(), "t3code-attachment-store-"));
const attachmentsDir = fs.mkdtempSync(path.join(os.tmpdir(), "t3code-attachment-store-"));
try {
const resolved = resolveAttachmentPathById({
stateDir,
attachmentsDir,
attachmentId: "thread-1-missing",
});
expect(resolved).toBeNull();
} finally {
fs.rmSync(stateDir, { recursive: true, force: true });
fs.rmSync(attachmentsDir, { recursive: true, force: true });
}
});
});
8 changes: 4 additions & 4 deletions apps/server/src/attachmentStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,17 +66,17 @@ export function attachmentRelativePath(attachment: ChatAttachment): string {
}

export function resolveAttachmentPath(input: {
readonly stateDir: string;
readonly attachmentsDir: string;
readonly attachment: ChatAttachment;
}): string | null {
return resolveAttachmentRelativePath({
stateDir: input.stateDir,
attachmentsDir: input.attachmentsDir,
relativePath: attachmentRelativePath(input.attachment),
});
}

export function resolveAttachmentPathById(input: {
readonly stateDir: string;
readonly attachmentsDir: string;
readonly attachmentId: string;
}): string | null {
const normalizedId = normalizeAttachmentRelativePath(input.attachmentId);
Expand All @@ -85,7 +85,7 @@ export function resolveAttachmentPathById(input: {
}
for (const extension of ATTACHMENT_FILENAME_EXTENSIONS) {
const maybePath = resolveAttachmentRelativePath({
stateDir: input.stateDir,
attachmentsDir: input.attachmentsDir,
relativePath: `${normalizedId}${extension}`,
});
if (maybePath && existsSync(maybePath)) {
Expand Down
Loading
Loading