| author | |
| committer | |
| log | 981cf405612932afa8a0b98b8c98474ef7e1015d |
| tree | 77f358d2b39ba5f0104d2f6d8bf9e7cfaf9a0388 |
| parent | 689dda59e997337103c76ed93af0574dcc554d26 |
- add structured lifecycle logs for Codex thread open/resume and provider session restarts
- introduce persistent server file logging layer and tighten WebSocket server startup/finalizer ordering
- preserve env-based boolean overrides in dev runner and stop forcing WS event logging in dev
- enrich provider session listing from persisted bindings and fix Claude rate-limit/task-id event mapping
- update orchestration and dev-runner tests for runtime-mode restart and env-flag behavior12 files changed, 386 insertions(+), 74 deletions(-)
apps/server/scripts/logger-scope-repro.ts created+66| ... | @@ -0,0 +1,66 @@ | ||
| 1 | import * as NodeRuntime from "@effect/platform-node/NodeRuntime"; | ||
| 2 | import * as NodeServices from "@effect/platform-node/NodeServices"; | ||
| 3 | import path from "node:path"; | ||
| 4 | |||
| 5 | import { Effect, FileSystem, Layer, Logger, ServiceMap } from "effect"; | ||
| 6 | |||
| 7 | import { makeEventNdjsonLogger } from "../src/provider/Layers/EventNdjsonLogger.ts"; | ||
| 8 | |||
| 9 | class LogDir extends ServiceMap.Service<LogDir, string>()("t3/scripts/logger-scope-repro/LogDir") {} | ||
| 10 | |||
| 11 | const main = Effect.gen(function* () { | ||
| 12 | const logdir = yield* LogDir; | ||
| 13 | const providerLogPath = path.join(logdir, "provider"); | ||
| 14 | |||
| 15 | yield* Effect.logInfo(`providerLogPath=${providerLogPath}`); | ||
| 16 | |||
| 17 | const providerLogger = yield* makeEventNdjsonLogger(providerLogPath, { | ||
| 18 | stream: "native", | ||
| 19 | batchWindowMs: 10, | ||
| 20 | }); | ||
| 21 | |||
| 22 | yield* Effect.logInfo("before provider write"); | ||
| 23 | |||
| 24 | if (providerLogger) { | ||
| 25 | yield* providerLogger.write( | ||
| 26 | { | ||
| 27 | kind: "probe", | ||
| 28 | message: "provider-only event", | ||
| 29 | }, | ||
| 30 | "thread-123" as never, | ||
| 31 | ); | ||
| 32 | } | ||
| 33 | |||
| 34 | yield* Effect.logInfo("after provider write"); | ||
| 35 | yield* Effect.sleep("50 millis"); | ||
| 36 | |||
| 37 | if (providerLogger) { | ||
| 38 | yield* providerLogger.close(); | ||
| 39 | } | ||
| 40 | yield* Effect.logInfo("after provider close"); | ||
| 41 | |||
| 42 | const fs = yield* FileSystem.FileSystem; | ||
| 43 | const logContents = yield* fs | ||
| 44 | .readDirectory(logdir, { recursive: true }) | ||
| 45 | .pipe( | ||
| 46 | Effect.flatMap((entries) => | ||
| 47 | Effect.all(entries.map((entry) => fs.readFileString(path.join(logdir, entry)))), | ||
| 48 | ), | ||
| 49 | ); | ||
| 50 | yield* Effect.logInfo(`logContents=${logContents}`); | ||
| 51 | }); | ||
| 52 | |||
| 53 | Effect.gen(function* () { | ||
| 54 | const fs = yield* FileSystem.FileSystem; | ||
| 55 | const logdir = path.join(process.cwd(), "logtest"); | ||
| 56 | yield* fs.makeDirectory(logdir); | ||
| 57 | |||
| 58 | const fileLogger = yield* Logger.formatSimple.pipe( | ||
| 59 | Logger.toFile(path.join(logdir, "global.log")), | ||
| 60 | ); | ||
| 61 | const dualLogger = Logger.layer([fileLogger, Logger.consolePretty()]); | ||
| 62 | |||
| 63 | const mainLayer = Layer.mergeAll(dualLogger, Layer.succeed(LogDir, logdir)); | ||
| 64 | |||
| 65 | yield* main.pipe(Effect.provide(mainLayer)); | ||
| 66 | }).pipe(Effect.scoped, Effect.provide(NodeServices.layer), NodeRuntime.runMain); | ||
apps/server/src/codexAppServerManager.ts+42| ... | @@ -18,6 +18,7 @@ import { | ... | @@ -18,6 +18,7 @@ import { |
| 18 | type ProviderSessionStartInput, | 18 | type ProviderSessionStartInput, |
| 19 | type ProviderTurnStartResult, | 19 | type ProviderTurnStartResult, |
| 20 | } from "@t3tools/contracts"; | 20 | } from "@t3tools/contracts"; |
| 21 | import { Effect } from "effect"; | ||
| 21 | 22 | ||
| 22 | type PendingRequestKey = string; | 23 | type PendingRequestKey = string; |
| 23 | 24 | ||
| ... | @@ -265,6 +266,20 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve | ... | @@ -265,6 +266,20 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve |
| 265 | experimentalRawEvents: false, | 266 | experimentalRawEvents: false, |
| 266 | }; | 267 | }; |
| 267 | const resumeThreadId = readResumeThreadId(input); | 268 | const resumeThreadId = readResumeThreadId(input); |
| 269 | this.emitLifecycleEvent( | ||
| 270 | context, | ||
| 271 | "session/threadOpenRequested", | ||
| 272 | resumeThreadId | ||
| 273 | ? `Attempting to resume thread ${resumeThreadId}.` | ||
| 274 | : "Starting a new Codex thread.", | ||
| 275 | ); | ||
| 276 | await Effect.logInfo("codex app-server opening thread", { | ||
| 277 | sessionId, | ||
| 278 | requestedRuntimeMode: input.runtimeMode, | ||
| 279 | requestedModel: normalizedModel ?? null, | ||
| 280 | requestedCwd: resolvedCwd, | ||
| 281 | resumeThreadId: resumeThreadId ?? null, | ||
| 282 | }).pipe(Effect.runPromise); | ||
| 268 | 283 | ||
| 269 | let threadOpenMethod: "thread/start" | "thread/resume" = "thread/start"; | 284 | let threadOpenMethod: "thread/start" | "thread/resume" = "thread/start"; |
| 270 | let threadOpenResponse: unknown; | 285 | let threadOpenResponse: unknown; |
| ... | @@ -277,6 +292,18 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve | ... | @@ -277,6 +292,18 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve |
| 277 | }); | 292 | }); |
| 278 | } catch (error) { | 293 | } catch (error) { |
| 279 | if (!isRecoverableThreadResumeError(error)) { | 294 | if (!isRecoverableThreadResumeError(error)) { |
| 295 | this.emitErrorEvent( | ||
| 296 | context, | ||
| 297 | "session/threadResumeFailed", | ||
| 298 | error instanceof Error ? error.message : "Codex thread resume failed.", | ||
| 299 | ); | ||
| 300 | await Effect.logWarning("codex app-server thread resume failed", { | ||
| 301 | sessionId, | ||
| 302 | requestedRuntimeMode: input.runtimeMode, | ||
| 303 | resumeThreadId, | ||
| 304 | recoverable: false, | ||
| 305 | cause: error instanceof Error ? error.message : String(error), | ||
| 306 | }).pipe(Effect.runPromise); | ||
| 280 | throw error; | 307 | throw error; |
| 281 | } | 308 | } |
| 282 | 309 | ||
| ... | @@ -286,6 +313,13 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve | ... | @@ -286,6 +313,13 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve |
| 286 | "session/threadResumeFallback", | 313 | "session/threadResumeFallback", |
| 287 | `Could not resume thread ${resumeThreadId}; started a new thread instead.`, | 314 | `Could not resume thread ${resumeThreadId}; started a new thread instead.`, |
| 288 | ); | 315 | ); |
| 316 | await Effect.logWarning("codex app-server thread resume fell back to fresh start", { | ||
| 317 | sessionId, | ||
| 318 | requestedRuntimeMode: input.runtimeMode, | ||
| 319 | resumeThreadId, | ||
| 320 | recoverable: true, | ||
| 321 | cause: error instanceof Error ? error.message : String(error), | ||
| 322 | }).pipe(Effect.runPromise); | ||
| 289 | threadOpenResponse = await this.sendRequest(context, "thread/start", threadStartParams); | 323 | threadOpenResponse = await this.sendRequest(context, "thread/start", threadStartParams); |
| 290 | } | 324 | } |
| 291 | } else { | 325 | } else { |
| ... | @@ -307,6 +341,14 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve | ... | @@ -307,6 +341,14 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve |
| 307 | threadId, | 341 | threadId, |
| 308 | resumeCursor: { threadId }, | 342 | resumeCursor: { threadId }, |
| 309 | }); | 343 | }); |
| 344 | this.emitLifecycleEvent(context, "session/threadOpenResolved", `Codex ${threadOpenMethod} resolved.`); | ||
| 345 | await Effect.logInfo("codex app-server thread open resolved", { | ||
| 346 | sessionId, | ||
| 347 | threadOpenMethod, | ||
| 348 | requestedResumeThreadId: resumeThreadId ?? null, | ||
| 349 | resolvedThreadId: threadId, | ||
| 350 | requestedRuntimeMode: input.runtimeMode, | ||
| 351 | }).pipe(Effect.runPromise); | ||
| 310 | this.emitLifecycleEvent(context, "session/ready", `Connected to thread ${threadId}`); | 352 | this.emitLifecycleEvent(context, "session/ready", `Connected to thread ${threadId}`); |
| 311 | return { ...context.session }; | 353 | return { ...context.session }; |
| 312 | } catch (error) { | 354 | } catch (error) { |
apps/server/src/main.ts+21-26| ... | @@ -24,6 +24,7 @@ import * as SqlitePersistence from "./persistence/Layers/Sqlite"; | ... | @@ -24,6 +24,7 @@ import * as SqlitePersistence from "./persistence/Layers/Sqlite"; |
| 24 | import { makeServerProviderLayer, makeServerRuntimeServicesLayer } from "./serverLayers"; | 24 | import { makeServerProviderLayer, makeServerRuntimeServicesLayer } from "./serverLayers"; |
| 25 | import { ProviderHealthLive } from "./provider/Layers/ProviderHealth"; | 25 | import { ProviderHealthLive } from "./provider/Layers/ProviderHealth"; |
| 26 | import { Server } from "./wsServer"; | 26 | import { Server } from "./wsServer"; |
| 27 | import { ServerLoggerLive } from "./serverLogger"; | ||
| 27 | 28 | ||
| 28 | export class StartupError extends Data.TaggedError("StartupError")<{ | 29 | export class StartupError extends Data.TaggedError("StartupError")<{ |
| 29 | readonly message: string; | 30 | readonly message: string; |
| ... | @@ -120,6 +121,9 @@ const CliEnvConfig = Config.all({ | ... | @@ -120,6 +121,9 @@ const CliEnvConfig = Config.all({ |
| 120 | ), | 121 | ), |
| 121 | }); | 122 | }); |
| 122 | 123 | ||
| 124 | const resolveBooleanFlag = (flag: Option.Option<boolean>, envValue: boolean) => | ||
| 125 | Option.getOrElse(Option.filter(flag, Boolean), () => envValue); | ||
| 126 | |||
| 123 | const ServerConfigLive = (input: CliInput) => | 127 | const ServerConfigLive = (input: CliInput) => |
| 124 | Layer.effect( | 128 | Layer.effect( |
| 125 | ServerConfig, | 129 | ServerConfig, |
| ... | @@ -151,25 +155,16 @@ const ServerConfigLive = (input: CliInput) => | ... | @@ -151,25 +155,16 @@ const ServerConfigLive = (input: CliInput) => |
| 151 | Option.getOrUndefined(input.stateDir) ?? env.stateDir, | 155 | Option.getOrUndefined(input.stateDir) ?? env.stateDir, |
| 152 | ); | 156 | ); |
| 153 | const devUrl = Option.getOrElse(input.devUrl, () => env.devUrl); | 157 | const devUrl = Option.getOrElse(input.devUrl, () => env.devUrl); |
| 154 | const noBrowser = Option.match(input.noBrowser, { | 158 | const noBrowser = resolveBooleanFlag(input.noBrowser, env.noBrowser ?? mode === "desktop"); |
| 155 | // effect/cli boolean flags parse to `false` when absent; in that case | ||
| 156 | // we still want env/mode fallbacks to apply. | ||
| 157 | onSome: (value) => (value ? true : (env.noBrowser ?? mode === "desktop")), | ||
| 158 | onNone: () => env.noBrowser ?? mode === "desktop", | ||
| 159 | }); | ||
| 160 | const authToken = Option.getOrUndefined(input.authToken) ?? env.authToken; | 159 | const authToken = Option.getOrUndefined(input.authToken) ?? env.authToken; |
| 161 | const autoBootstrapProjectFromCwd = Option.match(input.autoBootstrapProjectFromCwd, { | 160 | const autoBootstrapProjectFromCwd = resolveBooleanFlag( |
| 162 | // effect/cli boolean flags parse to `false` when absent; in that case | 161 | input.autoBootstrapProjectFromCwd, |
| 163 | // we still want env/mode fallbacks to apply. | 162 | env.autoBootstrapProjectFromCwd ?? mode === "web", |
| 164 | onSome: (value) => (value ? true : (env.autoBootstrapProjectFromCwd ?? mode === "web")), | 163 | ); |
| 165 | onNone: () => env.autoBootstrapProjectFromCwd ?? mode === "web", | 164 | const logWebSocketEvents = resolveBooleanFlag( |
| 166 | }); | 165 | input.logWebSocketEvents, |
| 167 | const logWebSocketEvents = Option.match(input.logWebSocketEvents, { | 166 | env.logWebSocketEvents ?? Boolean(devUrl), |
| 168 | // effect/cli boolean flags parse to `false` when absent; in that case | 167 | ); |
| 169 | // we still want env/dev fallbacks to apply. | ||
| 170 | onSome: (value) => (value ? true : (env.logWebSocketEvents ?? Boolean(devUrl))), | ||
| 171 | onNone: () => env.logWebSocketEvents ?? Boolean(devUrl), | ||
| 172 | }); | ||
| 173 | const staticDir = devUrl ? undefined : yield* cliConfig.resolveStaticDir; | 168 | const staticDir = devUrl ? undefined : yield* cliConfig.resolveStaticDir; |
| 174 | const { join } = yield* Path.Path; | 169 | const { join } = yield* Path.Path; |
| 175 | const keybindingsConfigPath = join(stateDir, "keybindings.json"); | 170 | const keybindingsConfigPath = join(stateDir, "keybindings.json"); |
| ... | @@ -178,7 +173,7 @@ const ServerConfigLive = (input: CliInput) => | ... | @@ -178,7 +173,7 @@ const ServerConfigLive = (input: CliInput) => |
| 178 | env.host ?? | 173 | env.host ?? |
| 179 | (mode === "desktop" ? "127.0.0.1" : undefined); | 174 | (mode === "desktop" ? "127.0.0.1" : undefined); |
| 180 | 175 | ||
| 181 | return { | 176 | const config: ServerConfigShape = { |
| 182 | mode, | 177 | mode, |
| 183 | port, | 178 | port, |
| 184 | cwd: cliConfig.cwd, | 179 | cwd: cliConfig.cwd, |
| ... | @@ -192,6 +187,8 @@ const ServerConfigLive = (input: CliInput) => | ... | @@ -192,6 +187,8 @@ const ServerConfigLive = (input: CliInput) => |
| 192 | autoBootstrapProjectFromCwd, | 187 | autoBootstrapProjectFromCwd, |
| 193 | logWebSocketEvents, | 188 | logWebSocketEvents, |
| 194 | } satisfies ServerConfigShape; | 189 | } satisfies ServerConfigShape; |
| 190 | |||
| 191 | return config; | ||
| 195 | }), | 192 | }), |
| 196 | ); | 193 | ); |
| 197 | 194 | ||
| ... | @@ -201,6 +198,7 @@ const LayerLive = (input: CliInput) => | ... | @@ -201,6 +198,7 @@ const LayerLive = (input: CliInput) => |
| 201 | Layer.provideMerge(makeServerProviderLayer()), | 198 | Layer.provideMerge(makeServerProviderLayer()), |
| 202 | Layer.provideMerge(ProviderHealthLive), | 199 | Layer.provideMerge(ProviderHealthLive), |
| 203 | Layer.provideMerge(SqlitePersistence.layerConfig), | 200 | Layer.provideMerge(SqlitePersistence.layerConfig), |
| 201 | Layer.provideMerge(ServerLoggerLive), | ||
| 204 | Layer.provideMerge(ServerConfigLive(input)), | 202 | Layer.provideMerge(ServerConfigLive(input)), |
| 205 | ); | 203 | ); |
| 206 | 204 | ||
| ... | @@ -235,14 +233,11 @@ const makeServerProgram = (input: CliInput) => | ... | @@ -235,14 +233,11 @@ const makeServerProgram = (input: CliInput) => |
| 235 | config.host && !isWildcardHost(config.host) | 233 | config.host && !isWildcardHost(config.host) |
| 236 | ? `http://${formatHostForUrl(config.host)}:${config.port}` | 234 | ? `http://${formatHostForUrl(config.host)}:${config.port}` |
| 237 | : localUrl; | 235 | : localUrl; |
| 236 | const { authToken, devUrl, ...safeConfig } = config; | ||
| 238 | yield* Effect.logInfo("T3 Code running", { | 237 | yield* Effect.logInfo("T3 Code running", { |
| 239 | url: bindUrl, | 238 | ...safeConfig, |
| 240 | localUrl, | 239 | devUrl: devUrl?.toString(), |
| 241 | bindHost: config.host ?? "default", | 240 | authEnabled: Boolean(authToken), |
| 242 | cwd: config.cwd, | ||
| 243 | mode: config.mode, | ||
| 244 | stateDir: config.stateDir, | ||
| 245 | authEnabled: Boolean(config.authToken), | ||
| 246 | }); | 241 | }); |
| 247 | 242 | ||
| 248 | if (!config.noBrowser) { | 243 | if (!config.noBrowser) { |
apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts+30| ... | @@ -509,6 +509,16 @@ describe("ProviderCommandReactor", () => { | ... | @@ -509,6 +509,16 @@ describe("ProviderCommandReactor", () => { |
| 509 | const harness = await createHarness(); | 509 | const harness = await createHarness(); |
| 510 | const now = new Date().toISOString(); | 510 | const now = new Date().toISOString(); |
| 511 | 511 | ||
| 512 | await Effect.runPromise( | ||
| 513 | harness.engine.dispatch({ | ||
| 514 | type: "thread.runtime-mode.set", | ||
| 515 | commandId: CommandId.makeUnsafe("cmd-runtime-mode-set-initial-full-access"), | ||
| 516 | threadId: ThreadId.makeUnsafe("thread-1"), | ||
| 517 | runtimeMode: "full-access", | ||
| 518 | createdAt: now, | ||
| 519 | }), | ||
| 520 | ); | ||
| 521 | |||
| 512 | await Effect.runPromise( | 522 | await Effect.runPromise( |
| 513 | harness.engine.dispatch({ | 523 | harness.engine.dispatch({ |
| 514 | type: "thread.turn.start", | 524 | type: "thread.turn.start", |
| ... | @@ -538,6 +548,11 @@ describe("ProviderCommandReactor", () => { | ... | @@ -538,6 +548,11 @@ describe("ProviderCommandReactor", () => { |
| 538 | }), | 548 | }), |
| 539 | ); | 549 | ); |
| 540 | 550 | ||
| 551 | await waitFor(async () => { | ||
| 552 | const readModel = await Effect.runPromise(harness.engine.getReadModel()); | ||
| 553 | const thread = readModel.threads.find((entry) => entry.id === ThreadId.makeUnsafe("thread-1")); | ||
| 554 | return thread?.runtimeMode === "approval-required"; | ||
| 555 | }); | ||
| 541 | await waitFor(() => harness.startSession.mock.calls.length === 2); | 556 | await waitFor(() => harness.startSession.mock.calls.length === 2); |
| 542 | await waitFor(() => harness.stopSession.mock.calls.length === 1); | 557 | await waitFor(() => harness.stopSession.mock.calls.length === 1); |
| 543 | 558 | ||
| ... | @@ -636,6 +651,16 @@ describe("ProviderCommandReactor", () => { | ... | @@ -636,6 +651,16 @@ describe("ProviderCommandReactor", () => { |
| 636 | const harness = await createHarness(); | 651 | const harness = await createHarness(); |
| 637 | const now = new Date().toISOString(); | 652 | const now = new Date().toISOString(); |
| 638 | 653 | ||
| 654 | await Effect.runPromise( | ||
| 655 | harness.engine.dispatch({ | ||
| 656 | type: "thread.runtime-mode.set", | ||
| 657 | commandId: CommandId.makeUnsafe("cmd-runtime-mode-set-initial-full-access-2"), | ||
| 658 | threadId: ThreadId.makeUnsafe("thread-1"), | ||
| 659 | runtimeMode: "full-access", | ||
| 660 | createdAt: now, | ||
| 661 | }), | ||
| 662 | ); | ||
| 663 | |||
| 639 | await Effect.runPromise( | 664 | await Effect.runPromise( |
| 640 | harness.engine.dispatch({ | 665 | harness.engine.dispatch({ |
| 641 | type: "thread.turn.start", | 666 | type: "thread.turn.start", |
| ... | @@ -669,6 +694,11 @@ describe("ProviderCommandReactor", () => { | ... | @@ -669,6 +694,11 @@ describe("ProviderCommandReactor", () => { |
| 669 | }), | 694 | }), |
| 670 | ); | 695 | ); |
| 671 | 696 | ||
| 697 | await waitFor(async () => { | ||
| 698 | const readModel = await Effect.runPromise(harness.engine.getReadModel()); | ||
| 699 | const thread = readModel.threads.find((entry) => entry.id === ThreadId.makeUnsafe("thread-1")); | ||
| 700 | return thread?.runtimeMode === "approval-required"; | ||
| 701 | }); | ||
| 672 | await waitFor(() => harness.startSession.mock.calls.length === 2); | 702 | await waitFor(() => harness.startSession.mock.calls.length === 2); |
| 673 | await Effect.runPromise(Effect.sleep("30 millis")); | 703 | await Effect.runPromise(Effect.sleep("30 millis")); |
| 674 | 704 |
apps/server/src/orchestration/Layers/ProviderCommandReactor.ts+21| ... | @@ -283,10 +283,31 @@ const make = Effect.gen(function* () { | ... | @@ -283,10 +283,31 @@ const make = Effect.gen(function* () { |
| 283 | providerChanged || shouldRestartForModelChange | 283 | providerChanged || shouldRestartForModelChange |
| 284 | ? undefined | 284 | ? undefined |
| 285 | : (activeSession?.resumeCursor ?? undefined); | 285 | : (activeSession?.resumeCursor ?? undefined); |
| 286 | yield* Effect.logInfo("provider command reactor restarting provider session", { | ||
| 287 | threadId, | ||
| 288 | existingSessionId, | ||
| 289 | currentProvider, | ||
| 290 | desiredProvider: options?.provider ?? currentProvider, | ||
| 291 | currentRuntimeMode: thread.session?.runtimeMode, | ||
| 292 | desiredRuntimeMode: thread.runtimeMode, | ||
| 293 | runtimeModeChanged, | ||
| 294 | providerChanged, | ||
| 295 | modelChanged, | ||
| 296 | shouldRestartForModelChange, | ||
| 297 | hasResumeCursor: resumeCursor !== undefined, | ||
| 298 | }); | ||
| 286 | const restartedSession = yield* startProviderSession({ | 299 | const restartedSession = yield* startProviderSession({ |
| 287 | ...(resumeCursor !== undefined ? { resumeCursor } : {}), | 300 | ...(resumeCursor !== undefined ? { resumeCursor } : {}), |
| 288 | ...(options?.provider !== undefined ? { provider: options.provider } : {}), | 301 | ...(options?.provider !== undefined ? { provider: options.provider } : {}), |
| 289 | }); | 302 | }); |
| 303 | yield* Effect.logInfo("provider command reactor restarted provider session", { | ||
| 304 | threadId, | ||
| 305 | previousSessionId: existingSessionId, | ||
| 306 | restartedSessionId: restartedSession.sessionId, | ||
| 307 | provider: restartedSession.provider, | ||
| 308 | providerThreadId: restartedSession.threadId, | ||
| 309 | runtimeMode: restartedSession.runtimeMode, | ||
| 310 | }); | ||
| 290 | yield* bindSessionToThread(restartedSession); | 311 | yield* bindSessionToThread(restartedSession); |
| 291 | yield* providerService.stopSession({ sessionId: existingSessionId }).pipe( | 312 | yield* providerService.stopSession({ sessionId: existingSessionId }).pipe( |
| 292 | Effect.catchCause((cause) => | 313 | Effect.catchCause((cause) => |
apps/server/src/orchestration/decider.projectScripts.test.ts+6-4| ... | @@ -251,10 +251,12 @@ describe("decider project scripts", () => { | ... | @@ -251,10 +251,12 @@ describe("decider project scripts", () => { |
| 251 | if (singleResult === null) { | 251 | if (singleResult === null) { |
| 252 | throw new Error("Expected a single runtime-mode-set event."); | 252 | throw new Error("Expected a single runtime-mode-set event."); |
| 253 | } | 253 | } |
| 254 | expect(singleResult.type).toBe("thread.runtime-mode-set"); | 254 | expect(singleResult).toMatchObject({ |
| 255 | expect(singleResult.payload).toMatchObject({ | 255 | type: "thread.runtime-mode-set", |
| 256 | threadId: ThreadId.makeUnsafe("thread-1"), | 256 | payload: { |
| 257 | runtimeMode: "approval-required", | 257 | threadId: ThreadId.makeUnsafe("thread-1"), |
| 258 | runtimeMode: "approval-required", | ||
| 259 | }, | ||
| 258 | }); | 260 | }); |
| 259 | }); | 261 | }); |
| 260 | }); | 262 | }); |
apps/server/src/provider/Layers/ClaudeCodeAdapter.ts+6-5| ... | @@ -34,6 +34,7 @@ import { | ... | @@ -34,6 +34,7 @@ import { |
| 34 | RuntimeItemId, | 34 | RuntimeItemId, |
| 35 | RuntimeRequestId, | 35 | RuntimeRequestId, |
| 36 | RuntimeSessionId, | 36 | RuntimeSessionId, |
| 37 | RuntimeTaskId, | ||
| 37 | ThreadId, | 38 | ThreadId, |
| 38 | TurnId, | 39 | TurnId, |
| 39 | } from "@t3tools/contracts"; | 40 | } from "@t3tools/contracts"; |
| ... | @@ -1130,7 +1131,7 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { | ... | @@ -1130,7 +1131,7 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { |
| 1130 | ...base, | 1131 | ...base, |
| 1131 | type: "task.started", | 1132 | type: "task.started", |
| 1132 | payload: { | 1133 | payload: { |
| 1133 | taskId: message.task_id, | 1134 | taskId: RuntimeTaskId.makeUnsafe(message.task_id), |
| 1134 | description: message.description, | 1135 | description: message.description, |
| 1135 | ...(message.task_type ? { taskType: message.task_type } : {}), | 1136 | ...(message.task_type ? { taskType: message.task_type } : {}), |
| 1136 | }, | 1137 | }, |
| ... | @@ -1141,7 +1142,7 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { | ... | @@ -1141,7 +1142,7 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { |
| 1141 | ...base, | 1142 | ...base, |
| 1142 | type: "task.progress", | 1143 | type: "task.progress", |
| 1143 | payload: { | 1144 | payload: { |
| 1144 | taskId: message.task_id, | 1145 | taskId: RuntimeTaskId.makeUnsafe(message.task_id), |
| 1145 | description: message.description, | 1146 | description: message.description, |
| 1146 | ...(message.usage ? { usage: message.usage } : {}), | 1147 | ...(message.usage ? { usage: message.usage } : {}), |
| 1147 | ...(message.last_tool_name ? { lastToolName: message.last_tool_name } : {}), | 1148 | ...(message.last_tool_name ? { lastToolName: message.last_tool_name } : {}), |
| ... | @@ -1153,7 +1154,7 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { | ... | @@ -1153,7 +1154,7 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { |
| 1153 | ...base, | 1154 | ...base, |
| 1154 | type: "task.completed", | 1155 | type: "task.completed", |
| 1155 | payload: { | 1156 | payload: { |
| 1156 | taskId: message.task_id, | 1157 | taskId: RuntimeTaskId.makeUnsafe(message.task_id), |
| 1157 | status: message.status, | 1158 | status: message.status, |
| 1158 | ...(message.summary ? { summary: message.summary } : {}), | 1159 | ...(message.summary ? { summary: message.summary } : {}), |
| 1159 | ...(message.usage ? { usage: message.usage } : {}), | 1160 | ...(message.usage ? { usage: message.usage } : {}), |
| ... | @@ -1259,7 +1260,7 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { | ... | @@ -1259,7 +1260,7 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { |
| 1259 | return; | 1260 | return; |
| 1260 | } | 1261 | } |
| 1261 | 1262 | ||
| 1262 | if (message.type === "rate_limit") { | 1263 | if (message.type === "rate_limit_event") { |
| 1263 | yield* offerRuntimeEvent({ | 1264 | yield* offerRuntimeEvent({ |
| 1264 | ...base, | 1265 | ...base, |
| 1265 | type: "account.rate-limits.updated", | 1266 | type: "account.rate-limits.updated", |
| ... | @@ -1295,7 +1296,7 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { | ... | @@ -1295,7 +1296,7 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { |
| 1295 | case "tool_progress": | 1296 | case "tool_progress": |
| 1296 | case "tool_use_summary": | 1297 | case "tool_use_summary": |
| 1297 | case "auth_status": | 1298 | case "auth_status": |
| 1298 | case "rate_limit": | 1299 | case "rate_limit_event": |
| 1299 | yield* handleSdkTelemetryMessage(context, message); | 1300 | yield* handleSdkTelemetryMessage(context, message); |
| 1300 | return; | 1301 | return; |
| 1301 | default: | 1302 | default: |
apps/server/src/provider/Layers/ProviderService.ts+44-3| ... | @@ -539,9 +539,50 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) => | ... | @@ -539,9 +539,50 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) => |
| 539 | }); | 539 | }); |
| 540 | 540 | ||
| 541 | const listSessions: ProviderServiceShape["listSessions"] = () => | 541 | const listSessions: ProviderServiceShape["listSessions"] = () => |
| 542 | Effect.forEach(adapters, (adapter) => adapter.listSessions()).pipe( | 542 | Effect.gen(function* () { |
| 543 | Effect.map((sessionsByProvider) => sessionsByProvider.flatMap((sessions) => sessions)), | 543 | const sessionsByProvider = yield* Effect.forEach(adapters, (adapter) => adapter.listSessions()); |
| 544 | ); | 544 | const activeSessions = sessionsByProvider.flatMap((sessions) => sessions); |
| 545 | const persistedBindings = yield* directory | ||
| 546 | .listSessionIds() | ||
| 547 | .pipe( | ||
| 548 | Effect.flatMap((sessionIds) => | ||
| 549 | Effect.forEach( | ||
| 550 | sessionIds, | ||
| 551 | (sessionId) => | ||
| 552 | directory.getBinding(sessionId).pipe( | ||
| 553 | Effect.orElseSucceed(() => Option.none<ProviderSessionBinding>()), | ||
| 554 | ), | ||
| 555 | { concurrency: "unbounded" }, | ||
| 556 | ), | ||
| 557 | ), | ||
| 558 | Effect.orElseSucceed(() => [] as Array<Option.Option<ProviderSessionBinding>>), | ||
| 559 | ); | ||
| 560 | const bindingsBySessionId = new Map<ProviderSessionId, ProviderSessionBinding>(); | ||
| 561 | for (const bindingOption of persistedBindings) { | ||
| 562 | const binding = Option.getOrUndefined(bindingOption); | ||
| 563 | if (binding) { | ||
| 564 | bindingsBySessionId.set(binding.sessionId, binding); | ||
| 565 | } | ||
| 566 | } | ||
| 567 | |||
| 568 | return activeSessions.map((session) => { | ||
| 569 | const binding = bindingsBySessionId.get(session.sessionId); | ||
| 570 | if (!binding) { | ||
| 571 | return session; | ||
| 572 | } | ||
| 573 | |||
| 574 | return { | ||
| 575 | ...session, | ||
| 576 | ...(session.threadId === undefined && binding.providerThreadId | ||
| 577 | ? { threadId: binding.providerThreadId } | ||
| 578 | : {}), | ||
| 579 | ...(session.resumeCursor === undefined && binding.resumeCursor !== undefined | ||
| 580 | ? { resumeCursor: binding.resumeCursor } | ||
| 581 | : {}), | ||
| 582 | ...(binding.runtimeMode !== undefined ? { runtimeMode: binding.runtimeMode } : {}), | ||
| 583 | }; | ||
| 584 | }); | ||
| 585 | }); | ||
| 545 | 586 | ||
| 546 | const getCapabilities: ProviderServiceShape["getCapabilities"] = (provider) => | 587 | const getCapabilities: ProviderServiceShape["getCapabilities"] = (provider) => |
| 547 | registry.getByProvider(provider).pipe(Effect.map((adapter) => adapter.capabilities)); | 588 | registry.getByProvider(provider).pipe(Effect.map((adapter) => adapter.capabilities)); |
apps/server/src/serverLogger.ts created+27| ... | @@ -0,0 +1,27 @@ | ||
| 1 | import fs from "node:fs"; | ||
| 2 | import path from "node:path"; | ||
| 3 | |||
| 4 | import { Effect, Logger, Option } from "effect"; | ||
| 5 | import * as Layer from "effect/Layer"; | ||
| 6 | |||
| 7 | import { ServerConfig } from "./config"; | ||
| 8 | |||
| 9 | export const ServerLoggerLive = Effect.gen(function* () { | ||
| 10 | const config = yield* Effect.serviceOption(ServerConfig); | ||
| 11 | if (Option.isNone(config)) { | ||
| 12 | return Logger.layer([Logger.defaultLogger]); | ||
| 13 | } | ||
| 14 | |||
| 15 | const logDir = path.join(config.value.stateDir, "logs"); | ||
| 16 | const logPath = path.join(logDir, "server.log"); | ||
| 17 | |||
| 18 | yield* Effect.sync(() => { | ||
| 19 | fs.mkdirSync(logDir, { recursive: true }); | ||
| 20 | }); | ||
| 21 | |||
| 22 | const fileLogger = Logger.formatSimple.pipe(Logger.toFile(logPath)); | ||
| 23 | |||
| 24 | return Logger.layer([Logger.defaultLogger, fileLogger], { | ||
| 25 | mergeWithExisting: false, | ||
| 26 | }); | ||
| 27 | }).pipe(Layer.unwrap); | ||
apps/server/src/wsServer.ts+32-30| ... | @@ -613,6 +613,33 @@ export const createServer = Effect.fn(function* (): Effect.fn.Return< | ... | @@ -613,6 +613,33 @@ export const createServer = Effect.fn(function* (): Effect.fn.Return< |
| 613 | ); | 613 | ); |
| 614 | } | 614 | } |
| 615 | 615 | ||
| 616 | const runPromise = yield* Effect.map(Effect.services<never>(), Effect.runPromiseWith); | ||
| 617 | yield* Effect.addFinalizer(() => | ||
| 618 | Effect.catch(liveProviderService.stopAll(), (cause) => | ||
| 619 | Effect.logWarning("failed to stop provider service", { cause }), | ||
| 620 | ), | ||
| 621 | ); | ||
| 622 | |||
| 623 | const unsubscribeTerminalEvents = yield* terminalManager.subscribe( | ||
| 624 | (event) => void Effect.runPromise(onTerminalEvent(event)), | ||
| 625 | ); | ||
| 626 | yield* Effect.addFinalizer(() => Effect.sync(() => unsubscribeTerminalEvents())); | ||
| 627 | |||
| 628 | yield* NodeHttpServer.make(() => httpServer, listenOptions).pipe( | ||
| 629 | Effect.mapError((cause) => new ServerLifecycleError({ operation: "httpServerListen", cause })), | ||
| 630 | ); | ||
| 631 | |||
| 632 | yield* Effect.addFinalizer(() => | ||
| 633 | Effect.all([ | ||
| 634 | closeAllClients, | ||
| 635 | closeWebSocketServer.pipe( | ||
| 636 | Effect.catch((error) => | ||
| 637 | Effect.logWarning("failed to close web socket server", { cause: error }), | ||
| 638 | ), | ||
| 639 | ), | ||
| 640 | ]), | ||
| 641 | ); | ||
| 642 | |||
| 616 | const routeRequest = Effect.fnUntraced(function* (request: WebSocketRequest) { | 643 | const routeRequest = Effect.fnUntraced(function* (request: WebSocketRequest) { |
| 617 | switch (request.body._tag) { | 644 | switch (request.body._tag) { |
| 618 | case ORCHESTRATION_WS_METHODS.getSnapshot: | 645 | case ORCHESTRATION_WS_METHODS.getSnapshot: |
| ... | @@ -800,6 +827,7 @@ export const createServer = Effect.fn(function* (): Effect.fn.Return< | ... | @@ -800,6 +827,7 @@ export const createServer = Effect.fn(function* (): Effect.fn.Return< |
| 800 | id: request.value.id, | 827 | id: request.value.id, |
| 801 | result: result.value, | 828 | result: result.value, |
| 802 | }); | 829 | }); |
| 830 | |||
| 803 | ws.send(response); | 831 | ws.send(response); |
| 804 | }); | 832 | }); |
| 805 | 833 | ||
| ... | @@ -828,7 +856,7 @@ export const createServer = Effect.fn(function* (): Effect.fn.Return< | ... | @@ -828,7 +856,7 @@ export const createServer = Effect.fn(function* (): Effect.fn.Return< |
| 828 | }); | 856 | }); |
| 829 | 857 | ||
| 830 | wss.on("connection", (ws) => { | 858 | wss.on("connection", (ws) => { |
| 831 | void Effect.runPromise(Ref.update(clients, (clients) => clients.add(ws))); | 859 | void runPromise(Ref.update(clients, (clients) => clients.add(ws))); |
| 832 | 860 | ||
| 833 | const segments = cwd.split(/[/\\]/).filter(Boolean); | 861 | const segments = cwd.split(/[/\\]/).filter(Boolean); |
| 834 | const projectName = segments[segments.length - 1] ?? "project"; | 862 | const projectName = segments[segments.length - 1] ?? "project"; |
| ... | @@ -847,7 +875,7 @@ export const createServer = Effect.fn(function* (): Effect.fn.Return< | ... | @@ -847,7 +875,7 @@ export const createServer = Effect.fn(function* (): Effect.fn.Return< |
| 847 | ws.send(JSON.stringify(welcome)); | 875 | ws.send(JSON.stringify(welcome)); |
| 848 | 876 | ||
| 849 | ws.on("message", (raw) => { | 877 | ws.on("message", (raw) => { |
| 850 | void Effect.runPromise( | 878 | void runPromise( |
| 851 | handleMessage(ws, raw).pipe( | 879 | handleMessage(ws, raw).pipe( |
| 852 | Effect.catch((error) => Effect.logError("Error handling message", error)), | 880 | Effect.catch((error) => Effect.logError("Error handling message", error)), |
| 853 | ), | 881 | ), |
| ... | @@ -855,7 +883,7 @@ export const createServer = Effect.fn(function* (): Effect.fn.Return< | ... | @@ -855,7 +883,7 @@ export const createServer = Effect.fn(function* (): Effect.fn.Return< |
| 855 | }); | 883 | }); |
| 856 | 884 | ||
| 857 | ws.on("close", () => { | 885 | ws.on("close", () => { |
| 858 | void Effect.runPromise( | 886 | void runPromise( |
| 859 | Ref.update(clients, (clients) => { | 887 | Ref.update(clients, (clients) => { |
| 860 | clients.delete(ws); | 888 | clients.delete(ws); |
| 861 | return clients; | 889 | return clients; |
| ... | @@ -864,7 +892,7 @@ export const createServer = Effect.fn(function* (): Effect.fn.Return< | ... | @@ -864,7 +892,7 @@ export const createServer = Effect.fn(function* (): Effect.fn.Return< |
| 864 | }); | 892 | }); |
| 865 | 893 | ||
| 866 | ws.on("error", () => { | 894 | ws.on("error", () => { |
| 867 | void Effect.runPromise( | 895 | void runPromise( |
| 868 | Ref.update(clients, (clients) => { | 896 | Ref.update(clients, (clients) => { |
| 869 | clients.delete(ws); | 897 | clients.delete(ws); |
| 870 | return clients; | 898 | return clients; |
| ... | @@ -873,32 +901,6 @@ export const createServer = Effect.fn(function* (): Effect.fn.Return< | ... | @@ -873,32 +901,6 @@ export const createServer = Effect.fn(function* (): Effect.fn.Return< |
| 873 | }); | 901 | }); |
| 874 | }); | 902 | }); |
| 875 | 903 | ||
| 876 | yield* Effect.addFinalizer(() => | ||
| 877 | Effect.catch(liveProviderService.stopAll(), (cause) => | ||
| 878 | Effect.logWarning("failed to stop provider service", { cause }), | ||
| 879 | ), | ||
| 880 | ); | ||
| 881 | |||
| 882 | const unsubscribeTerminalEvents = yield* terminalManager.subscribe( | ||
| 883 | (event) => void Effect.runPromise(onTerminalEvent(event)), | ||
| 884 | ); | ||
| 885 | yield* Effect.addFinalizer(() => Effect.sync(() => unsubscribeTerminalEvents())); | ||
| 886 | |||
| 887 | yield* NodeHttpServer.make(() => httpServer, listenOptions).pipe( | ||
| 888 | Effect.mapError((cause) => new ServerLifecycleError({ operation: "httpServerListen", cause })), | ||
| 889 | ); | ||
| 890 | |||
| 891 | yield* Effect.addFinalizer(() => | ||
| 892 | Effect.all([ | ||
| 893 | closeAllClients, | ||
| 894 | closeWebSocketServer.pipe( | ||
| 895 | Effect.catch((error) => | ||
| 896 | Effect.logWarning("failed to close web socket server", { cause: error }), | ||
| 897 | ), | ||
| 898 | ), | ||
| 899 | ]), | ||
| 900 | ); | ||
| 901 | |||
| 902 | return httpServer; | 904 | return httpServer; |
| 903 | }); | 905 | }); |
| 904 | 906 |
scripts/dev-runner.test.ts+45| ... | @@ -97,6 +97,51 @@ it.layer(NodeServices.layer)("dev-runner", (it) => { | ... | @@ -97,6 +97,51 @@ it.layer(NodeServices.layer)("dev-runner", (it) => { |
| 97 | assert.equal(env.VITE_DEV_SERVER_URL, "http://localhost:7331/"); | 97 | assert.equal(env.VITE_DEV_SERVER_URL, "http://localhost:7331/"); |
| 98 | }), | 98 | }), |
| 99 | ); | 99 | ); |
| 100 | |||
| 101 | it.effect("does not force websocket logging on in dev mode when unset", () => | ||
| 102 | Effect.gen(function* () { | ||
| 103 | const env = yield* createDevRunnerEnv({ | ||
| 104 | mode: "dev", | ||
| 105 | baseEnv: { | ||
| 106 | T3CODE_LOG_WS_EVENTS: "keep-me-out", | ||
| 107 | }, | ||
| 108 | serverOffset: 0, | ||
| 109 | webOffset: 0, | ||
| 110 | stateDir: undefined, | ||
| 111 | authToken: undefined, | ||
| 112 | noBrowser: undefined, | ||
| 113 | autoBootstrapProjectFromCwd: undefined, | ||
| 114 | logWebSocketEvents: undefined, | ||
| 115 | host: undefined, | ||
| 116 | port: undefined, | ||
| 117 | devUrl: undefined, | ||
| 118 | }); | ||
| 119 | |||
| 120 | assert.equal(env.T3CODE_MODE, "web"); | ||
| 121 | assert.equal(env.T3CODE_LOG_WS_EVENTS, undefined); | ||
| 122 | }), | ||
| 123 | ); | ||
| 124 | |||
| 125 | it.effect("forwards explicit websocket logging false without coercing it away", () => | ||
| 126 | Effect.gen(function* () { | ||
| 127 | const env = yield* createDevRunnerEnv({ | ||
| 128 | mode: "dev", | ||
| 129 | baseEnv: {}, | ||
| 130 | serverOffset: 0, | ||
| 131 | webOffset: 0, | ||
| 132 | stateDir: undefined, | ||
| 133 | authToken: undefined, | ||
| 134 | noBrowser: undefined, | ||
| 135 | autoBootstrapProjectFromCwd: undefined, | ||
| 136 | logWebSocketEvents: false, | ||
| 137 | host: undefined, | ||
| 138 | port: undefined, | ||
| 139 | devUrl: undefined, | ||
| 140 | }); | ||
| 141 | |||
| 142 | assert.equal(env.T3CODE_LOG_WS_EVENTS, "0"); | ||
| 143 | }), | ||
| 144 | ); | ||
| 100 | }); | 145 | }); |
| 101 | 146 | ||
| 102 | describe("findFirstAvailableOffset", () => { | 147 | describe("findFirstAvailableOffset", () => { |
scripts/dev-runner.ts+46-6| ... | @@ -189,9 +189,6 @@ export function createDevRunnerEnv({ | ... | @@ -189,9 +189,6 @@ export function createDevRunnerEnv({ |
| 189 | 189 | ||
| 190 | if (mode === "dev") { | 190 | if (mode === "dev") { |
| 191 | output.T3CODE_MODE = "web"; | 191 | output.T3CODE_MODE = "web"; |
| 192 | if (logWebSocketEvents === undefined) { | ||
| 193 | output.T3CODE_LOG_WS_EVENTS = "1"; | ||
| 194 | } | ||
| 195 | delete output.T3CODE_DESKTOP_WS_URL; | 192 | delete output.T3CODE_DESKTOP_WS_URL; |
| 196 | } | 193 | } |
| 197 | 194 | ||
| ... | @@ -350,6 +347,35 @@ interface DevRunnerCliInput { | ... | @@ -350,6 +347,35 @@ interface DevRunnerCliInput { |
| 350 | readonly turboArgs: ReadonlyArray<string>; | 347 | readonly turboArgs: ReadonlyArray<string>; |
| 351 | } | 348 | } |
| 352 | 349 | ||
| 350 | const readOptionalBooleanEnv = (name: string): boolean | undefined => { | ||
| 351 | const value = process.env[name]; | ||
| 352 | if (value === undefined) { | ||
| 353 | return undefined; | ||
| 354 | } | ||
| 355 | if (value === "1" || value.toLowerCase() === "true") { | ||
| 356 | return true; | ||
| 357 | } | ||
| 358 | if (value === "0" || value.toLowerCase() === "false") { | ||
| 359 | return false; | ||
| 360 | } | ||
| 361 | return undefined; | ||
| 362 | }; | ||
| 363 | |||
| 364 | const resolveOptionalBooleanOverride = ( | ||
| 365 | explicitValue: boolean | undefined, | ||
| 366 | envValue: boolean | undefined, | ||
| 367 | ): boolean | undefined => { | ||
| 368 | if (explicitValue === true) { | ||
| 369 | return true; | ||
| 370 | } | ||
| 371 | |||
| 372 | if (explicitValue === false) { | ||
| 373 | return envValue; | ||
| 374 | } | ||
| 375 | |||
| 376 | return envValue; | ||
| 377 | }; | ||
| 378 | |||
| 353 | export function runDevRunnerWithInput(input: DevRunnerCliInput) { | 379 | export function runDevRunnerWithInput(input: DevRunnerCliInput) { |
| 354 | return Effect.gen(function* () { | 380 | return Effect.gen(function* () { |
| 355 | const { portOffset, devInstance } = yield* OffsetConfig.asEffect().pipe( | 381 | const { portOffset, devInstance } = yield* OffsetConfig.asEffect().pipe( |
| ... | @@ -371,6 +397,14 @@ export function runDevRunnerWithInput(input: DevRunnerCliInput) { | ... | @@ -371,6 +397,14 @@ export function runDevRunnerWithInput(input: DevRunnerCliInput) { |
| 371 | }), | 397 | }), |
| 372 | }); | 398 | }); |
| 373 | 399 | ||
| 400 | const envOverrides = { | ||
| 401 | noBrowser: readOptionalBooleanEnv("T3CODE_NO_BROWSER"), | ||
| 402 | autoBootstrapProjectFromCwd: readOptionalBooleanEnv( | ||
| 403 | "T3CODE_AUTO_BOOTSTRAP_PROJECT_FROM_CWD", | ||
| 404 | ), | ||
| 405 | logWebSocketEvents: readOptionalBooleanEnv("T3CODE_LOG_WS_EVENTS"), | ||
| 406 | }; | ||
| 407 | |||
| 374 | const { serverOffset, webOffset } = yield* resolveModePortOffsets({ | 408 | const { serverOffset, webOffset } = yield* resolveModePortOffsets({ |
| 375 | mode: input.mode, | 409 | mode: input.mode, |
| 376 | startOffset: offset, | 410 | startOffset: offset, |
| ... | @@ -385,9 +419,15 @@ export function runDevRunnerWithInput(input: DevRunnerCliInput) { | ... | @@ -385,9 +419,15 @@ export function runDevRunnerWithInput(input: DevRunnerCliInput) { |
| 385 | webOffset, | 419 | webOffset, |
| 386 | stateDir: input.stateDir, | 420 | stateDir: input.stateDir, |
| 387 | authToken: input.authToken, | 421 | authToken: input.authToken, |
| 388 | noBrowser: input.noBrowser, | 422 | noBrowser: resolveOptionalBooleanOverride(input.noBrowser, envOverrides.noBrowser), |
| 389 | autoBootstrapProjectFromCwd: input.autoBootstrapProjectFromCwd, | 423 | autoBootstrapProjectFromCwd: resolveOptionalBooleanOverride( |
| 390 | logWebSocketEvents: input.logWebSocketEvents, | 424 | input.autoBootstrapProjectFromCwd, |
| 425 | envOverrides.autoBootstrapProjectFromCwd, | ||
| 426 | ), | ||
| 427 | logWebSocketEvents: resolveOptionalBooleanOverride( | ||
| 428 | input.logWebSocketEvents, | ||
| 429 | envOverrides.logWebSocketEvents, | ||
| 430 | ), | ||
| 391 | host: input.host, | 431 | host: input.host, |
| 392 | port: input.port, | 432 | port: input.port, |
| 393 | devUrl: input.devUrl, | 433 | devUrl: input.devUrl, |