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
4 changes: 2 additions & 2 deletions apps/desktop/src/app/DesktopObservability.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import * as Ref from "effect/Ref";
import * as Schema from "effect/Schema";
import * as Semaphore from "effect/Semaphore";
import * as Tracer from "effect/Tracer";
import { OtlpSerialization, OtlpTracer } from "effect/unstable/observability";
import { OtlpExporter, OtlpSerialization, OtlpTracer } from "effect/unstable/observability";

import * as DesktopDataMigration from "./DesktopDataMigration.ts";
import * as DesktopEnvironment from "./DesktopEnvironment.ts";
Expand Down Expand Up @@ -415,7 +415,7 @@ const tracerLayer = Layer.unwrap(

return Layer.succeed(Tracer.Tracer, tracer);
}),
).pipe(Layer.provideMerge(OtlpSerialization.layerJson));
).pipe(Layer.provideMerge(Layer.mergeAll(OtlpSerialization.layerJson, OtlpExporter.layerFlusher)));

export const layer = Layer.mergeAll(
backendOutputLogLayer,
Expand Down
16 changes: 7 additions & 9 deletions apps/desktop/src/backend/DesktopBackendManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -366,15 +366,13 @@ const makeDesktopBackendManager = Effect.fn("makeDesktopBackendManager")(functio
Ref.update(state, withActiveRun(runId, f));

const snapshot = Ref.get(state).pipe(
Effect.map(
(current): DesktopBackendSnapshot => ({
desiredRunning: current.desiredRunning,
ready: current.ready,
activePid: activePid(current.active),
restartAttempt: current.restartAttempt,
restartScheduled: Option.isSome(current.restartFiber),
}),
),
Effect.map((current): DesktopBackendSnapshot => ({
desiredRunning: current.desiredRunning,
ready: current.ready,
activePid: activePid(current.active),
restartAttempt: current.restartAttempt,
restartScheduled: Option.isSome(current.restartFiber),
})),
);
const currentConfig = Ref.get(state).pipe(Effect.map((current) => current.config));

Expand Down
4 changes: 2 additions & 2 deletions apps/desktop/src/preview/PreviewAutomation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ const DRAG_MOVE_STEPS = 10;

const MAX_NETWORK_FAILURES = 100;

export class PreviewTargetMissingError extends Schema.TaggedErrorClass<PreviewTargetMissingError>()(
export class PreviewTargetMissingError extends Schema.TaggedError<PreviewTargetMissingError>()(
"PreviewTargetMissingError",
{ webContentsId: Schema.Number },
) {
Expand All @@ -82,7 +82,7 @@ export class PreviewTargetMissingError extends Schema.TaggedErrorClass<PreviewTa
}
}

export class PreviewCommandError extends Schema.TaggedErrorClass<PreviewCommandError>()(
export class PreviewCommandError extends Schema.TaggedError<PreviewCommandError>()(
"PreviewCommandError",
{ webContentsId: Schema.Number, method: Schema.String, cause: Schema.Defect() },
) {
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/preview/PreviewSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ const ALLOWED_PREVIEW_PERMISSIONS: ReadonlySet<string> = new Set([
"notifications",
]);

export class PreviewSessionCreationError extends Schema.TaggedErrorClass<PreviewSessionCreationError>()(
export class PreviewSessionCreationError extends Schema.TaggedError<PreviewSessionCreationError>()(
"PreviewSessionCreationError",
{ partition: Schema.String, cause: Schema.Defect() },
) {
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/relay/DesktopRelay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ const decodeRelayCreateSessionResult = Schema.decodeUnknownEffect(RelayCreateSes
const decodeRelayRenewSessionResult = Schema.decodeUnknownEffect(RelayRenewSessionResult);
const decodeAuthBearerBootstrapResult = Schema.decodeUnknownEffect(AuthBearerBootstrapResult);
const decodeAuthWebSocketTokenResult = Schema.decodeUnknownEffect(AuthWebSocketTokenResult);
const decodeUnknownJsonString = Schema.decodeUnknownEffect(Schema.UnknownFromJsonString);
const decodeUnknownJsonString = Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.Unknown));
const encodeBootstrapBearerRequestBody = Schema.encodeEffect(
Schema.fromJsonString(BootstrapBearerRequestBody),
);
Expand Down
2 changes: 1 addition & 1 deletion apps/marketing/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
},
"dependencies": {
"@vercel/analytics": "^2.0.1",
"astro": "^7.2.4"
"astro": "^7.2.9"
},
"devDependencies": {
"@astrojs/check": "^0.9.10",
Expand Down
2 changes: 1 addition & 1 deletion apps/relay-worker/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,6 @@
"vite": "catalog:",
"vite-plus": "catalog:",
"vitest": "catalog:",
"wrangler": "^4.125.0"
"wrangler": "^4.127.0"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -116,12 +116,9 @@ export function gitShowFileAtRef(cwd: string, ref: string, filePath: string): st
return runGit(cwd, ["show", `${ref}:${filePath}`]);
}

class WaitForTimeoutError extends Schema.TaggedErrorClass<WaitForTimeoutError>()(
"WaitForTimeoutError",
{
description: Schema.String,
},
) {}
class WaitForTimeoutError extends Schema.TaggedError<WaitForTimeoutError>()("WaitForTimeoutError", {
description: Schema.String,
}) {}

function waitFor<A, E>(
read: Effect.Effect<A, E>,
Expand Down Expand Up @@ -160,7 +157,7 @@ function waitFor<A, E>(
);
}

class OrchestrationHarnessRuntimeError extends Schema.TaggedErrorClass<OrchestrationHarnessRuntimeError>()(
class OrchestrationHarnessRuntimeError extends Schema.TaggedError<OrchestrationHarnessRuntimeError>()(
"OrchestrationHarnessRuntimeError",
{
operation: Schema.String,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ function nowIso() {
return "2026-05-01T00:00:00.000Z";
}

class IntegrationWaitTimeoutError extends Schema.TaggedErrorClass<IntegrationWaitTimeoutError>()(
class IntegrationWaitTimeoutError extends Schema.TaggedError<IntegrationWaitTimeoutError>()(
"IntegrationWaitTimeoutError",
{
description: Schema.String,
Expand Down
2 changes: 1 addition & 1 deletion apps/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
"@effect/platform-node": "catalog:",
"@effect/platform-node-shared": "catalog:",
"@effect/sql-sqlite-bun": "catalog:",
"@opencode-ai/sdk": "^1.18.20",
"@opencode-ai/sdk": "^1.18.23",
"@pierre/diffs": "catalog:",
"effect": "catalog:",
"node-pty": "^1.1.0"
Expand Down
26 changes: 11 additions & 15 deletions apps/server/src/auth/Layers/ServerAuth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,15 +197,13 @@ export const makeServerAuth = Effect.gen(function* () {
},
})
.pipe(
Effect.map(
(session): AuthenticatedSession => ({
sessionId: session.sessionId,
subject: TRUSTED_LOOPBACK_BROWSER_DEV_SUBJECT,
method: session.method,
role: session.role,
expiresAt: session.expiresAt,
}),
),
Effect.map((session): AuthenticatedSession => ({
sessionId: session.sessionId,
subject: TRUSTED_LOOPBACK_BROWSER_DEV_SUBJECT,
method: session.method,
role: session.role,
expiresAt: session.expiresAt,
})),
Effect.mapError(
(cause) =>
new AuthError({
Expand Down Expand Up @@ -418,12 +416,10 @@ export const makeServerAuth = Effect.gen(function* () {
),
Effect.map((clientSessions) =>
clientSessions
.map(
(clientSession): AuthClientSession => ({
...clientSession,
current: clientSession.sessionId === currentSessionId,
}),
)
.map((clientSession): AuthClientSession => ({
...clientSession,
current: clientSession.sessionId === currentSessionId,
}))
.filter(
(clientSession) => clientSession.current || !isInternalClientSession(clientSession),
),
Expand Down
4 changes: 2 additions & 2 deletions apps/server/src/checkpointing/Errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type { VcsError } from "@threadlines/contracts";
/**
* CheckpointUnavailableError - Expected checkpoint does not exist.
*/
export class CheckpointUnavailableError extends Schema.TaggedErrorClass<CheckpointUnavailableError>()(
export class CheckpointUnavailableError extends Schema.TaggedError<CheckpointUnavailableError>()(
"CheckpointUnavailableError",
{
threadId: Schema.String,
Expand All @@ -22,7 +22,7 @@ export class CheckpointUnavailableError extends Schema.TaggedErrorClass<Checkpoi
/**
* CheckpointInvariantError - Inconsistent provider/filesystem/catalog state.
*/
export class CheckpointInvariantError extends Schema.TaggedErrorClass<CheckpointInvariantError>()(
export class CheckpointInvariantError extends Schema.TaggedError<CheckpointInvariantError>()(
"CheckpointInvariantError",
{
operation: Schema.String,
Expand Down
2 changes: 1 addition & 1 deletion apps/server/src/cli/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -584,7 +584,7 @@ export const DurationFromString = Schema.String.pipe(
return Effect.succeed(duration);
}
return Effect.fail(
new SchemaIssue.InvalidValue(Option.some(value), {
new SchemaIssue.InvalidValue({
message: "Invalid duration. Use values like 5m, 1h, 30d, or 15 minutes.",
}),
);
Expand Down
2 changes: 1 addition & 1 deletion apps/server/src/diagnostics/ProcessDiagnostics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ export class ProcessSnapshot extends Context.Service<ProcessSnapshot, ProcessSna
"threadlines/diagnostics/ProcessSnapshot",
) {}

export class ProcessDiagnosticsError extends Schema.TaggedErrorClass<ProcessDiagnosticsError>()(
export class ProcessDiagnosticsError extends Schema.TaggedError<ProcessDiagnosticsError>()(
"ProcessDiagnosticsError",
{
message: Schema.String,
Expand Down
30 changes: 14 additions & 16 deletions apps/server/src/git/GitAuthRemediationService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,14 +125,13 @@ export const make = Effect.fn("makeGitAuthRemediationService")(function* () {
timeoutMs: GH_PROBE_TIMEOUT_MS,
})
.pipe(
Effect.map(
(result): RemediationProbe =>
result.exitCode === 0
? { applicable: true, reason: null }
: {
applicable: false,
reason: `GitHub CLI is installed but not logged in to ${host}. Run "gh auth login" in a terminal first.`,
},
Effect.map((result): RemediationProbe =>
result.exitCode === 0
? { applicable: true, reason: null }
: {
applicable: false,
reason: `GitHub CLI is installed but not logged in to ${host}. Run "gh auth login" in a terminal first.`,
},
),
Effect.catch(() =>
Effect.succeed<RemediationProbe>({
Expand All @@ -153,14 +152,13 @@ export const make = Effect.fn("makeGitAuthRemediationService")(function* () {
timeoutMs: SSH_PROBE_TIMEOUT_MS,
})
.pipe(
Effect.map(
(result): RemediationProbe =>
result.exitCode === 0
? { applicable: true, reason: null }
: {
applicable: false,
reason: `SSH access to ${host} is not set up on this machine (no usable key).`,
},
Effect.map((result): RemediationProbe =>
result.exitCode === 0
? { applicable: true, reason: null }
: {
applicable: false,
reason: `SSH access to ${host} is not set up on this machine (no usable key).`,
},
),
Effect.catch(() =>
Effect.succeed<RemediationProbe>({
Expand Down
5 changes: 2 additions & 3 deletions apps/server/src/keybindings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ import * as Exit from "effect/Exit";
import * as FileSystem from "effect/FileSystem";
import * as Path from "effect/Path";
import * as Layer from "effect/Layer";
import * as Option from "effect/Option";
import * as Predicate from "effect/Predicate";
import * as PubSub from "effect/PubSub";
import * as Schema from "effect/Schema";
Expand Down Expand Up @@ -67,7 +66,7 @@ export const ResolvedKeybindingFromConfig = KeybindingRule.pipe(
Effect.filterOrFail(
Predicate.isNotNull,
() =>
new SchemaIssue.InvalidValue(Option.some(rule), {
new SchemaIssue.InvalidValue({
message: "Invalid keybinding rule",
}),
),
Expand All @@ -79,7 +78,7 @@ export const ResolvedKeybindingFromConfig = KeybindingRule.pipe(
const key = encodeShortcut(resolved.shortcut);
if (!key) {
return yield* Effect.fail(
new SchemaIssue.InvalidValue(Option.some(resolved), {
new SchemaIssue.InvalidValue({
message: "Resolved shortcut cannot be encoded to key string",
}),
);
Expand Down
27 changes: 2 additions & 25 deletions apps/server/src/mcp/McpHttpServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import * as Sink from "effect/Sink";
import * as Stream from "effect/Stream";
import * as Layer from "effect/Layer";
import type * as Types from "effect/Types";
import { McpSchema, McpServer, Tool } from "effect/unstable/ai";
import { McpSchema, McpProtocol, McpServer, Tool } from "effect/unstable/ai";
import { HttpRouter, HttpServerRequest, HttpServerResponse } from "effect/unstable/http";

import {
Expand Down Expand Up @@ -138,29 +138,6 @@ const AuthenticationLive = HttpRouter.middleware<{
provides: McpInvocationContext;
}>()(authenticate).layer;

/**
* The stream this server does not offer, said properly.
*
* Streamable HTTP lets a client open a GET stream for server-initiated
* messages, and a server that has none must answer 405. Ours had no GET route
* at all, so the request fell through to the web app's catch-all and came back
* a 302 to an HTML page -- which a client reads as neither "here is the stream"
* nor "there is no stream", and sits in connecting on.
*/
const noServerStreamRoute = HttpRouter.add(
"GET",
MCP_ROUTE_PATH,
Effect.succeed(
HttpServerResponse.jsonUnsafe(
{
jsonrpc: "2.0",
error: { code: -32000, message: "This server does not offer an event stream." },
},
{ status: 405, headers: { allow: "POST" } },
),
),
);

/**
* The screenshot tool, registered by hand so it can answer with a picture.
*
Expand Down Expand Up @@ -272,7 +249,7 @@ export const layer = McpServer.toolkit(BrowserStandardToolkit).pipe(
name: "threadlines-browser",
version: "1",
path: MCP_ROUTE_PATH,
protocols: [McpProtocol.v2025_06_18],
}).pipe(Layer.provide(AuthenticationLive)),
),
Layer.provideMerge(noServerStreamRoute),
);
13 changes: 11 additions & 2 deletions apps/server/src/observability/Layers/Observability.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,22 @@ import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import * as References from "effect/References";
import * as Tracer from "effect/Tracer";
import { OtlpMetrics, OtlpSerialization, OtlpTracer } from "effect/unstable/observability";
import {
OtlpExporter,
OtlpMetrics,
OtlpSerialization,
OtlpTracer,
} from "effect/unstable/observability";

import { ServerConfig } from "../../config.ts";
import { ServerLoggerLive } from "../../serverLogger.ts";
import { BrowserTraceCollector } from "../Services/BrowserTraceCollector.ts";

const otlpSerializationLayer = OtlpSerialization.layerJson;
// Exporters need the JSON codec plus the shared flush registry (one per layer build).
const otlpSerializationLayer = Layer.mergeAll(
OtlpSerialization.layerJson,
OtlpExporter.layerFlusher,
);
const SLOW_TRACE_RECORD_THRESHOLD_MS = 1_000;
const SQL_TRACE_SAMPLE_MODULO = 100;
const PROJECTION_TRACE_SAMPLE_MODULO = 20;
Expand Down
Loading
Loading