Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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