Skip to content

Commit cd2a469

Browse files
committed
fix(server): validate strict provider lifecycle guard
1 parent b594088 commit cd2a469

4 files changed

Lines changed: 78 additions & 3 deletions

File tree

‎apps/server/src/cli/config.ts‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,9 @@ export const serverEnvironmentConfig = {
307307
Config.option,
308308
Config.map(Option.getOrUndefined),
309309
),
310+
strictProviderLifecycleGuard: Config.Boolean("T3CODE_STRICT_PROVIDER_LIFECYCLE_GUARD").pipe(
311+
Config.withDefault(true),
312+
),
310313
telemetryEnabled: Config.Boolean("T3CODE_TELEMETRY_ENABLED").pipe(Config.withDefault(true)),
311314
telemetryFlushBatchSize: Config.Number("T3CODE_TELEMETRY_FLUSH_BATCH_SIZE").pipe(
312315
Config.withDefault(20),
@@ -557,6 +560,13 @@ export const serverEnvSpecs: ReadonlyArray<ServerEnvVarSpec> = [
557560
DevAuthTokenConfig,
558561
{ secret: true },
559562
),
563+
envSpec(
564+
"T3CODE_STRICT_PROVIDER_LIFECYCLE_GUARD",
565+
"boolean",
566+
"Require provider lifecycle events to match the active turn.",
567+
serverEnvironmentConfig.strictProviderLifecycleGuard,
568+
{ defaultText: "true" },
569+
),
560570
envSpec(
561571
"T3CODE_TELEMETRY_ENABLED",
562572
"boolean",

‎apps/server/src/cli/envValidation.test.ts‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ const runValidationFlag = (args: ReadonlyArray<string>, env: Record<string, stri
7373
);
7474

7575
const invalidStartupEnvironment: ReadonlyArray<readonly [string, string]> = [
76+
["T3CODE_STRICT_PROVIDER_LIFECYCLE_GUARD", "maybe"],
7677
["T3CODE_TELEMETRY_ENABLED", "maybe"],
7778
["T3CODE_TELEMETRY_FLUSH_BATCH_SIZE", "many"],
7879
["T3CODE_TELEMETRY_MAX_BUFFERED_EVENTS", "many"],
@@ -244,6 +245,20 @@ describe("server environment validation", () => {
244245
}),
245246
);
246247

248+
it.effect("accepts strict lifecycle guard boolean aliases", () =>
249+
Effect.gen(function* () {
250+
for (const value of ["false", "0", "off", "no"]) {
251+
const rows = yield* runValidation({
252+
T3CODE_STRICT_PROVIDER_LIFECYCLE_GUARD: value,
253+
});
254+
assert.equal(
255+
rows.find((row) => row.variable === "T3CODE_STRICT_PROVIDER_LIFECYCLE_GUARD")?.status,
256+
"ok",
257+
);
258+
}
259+
}),
260+
);
261+
247262
it.effect("accepts secure relay trace endpoints with paths", () =>
248263
Effect.gen(function* () {
249264
const rows = yield* runValidation({

‎apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts‎

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import {
2626
TurnId,
2727
} from "@t3tools/contracts";
2828
import * as Clock from "effect/Clock";
29+
import * as ConfigProvider from "effect/ConfigProvider";
2930
import * as Deferred from "effect/Deferred";
3031
import * as Effect from "effect/Effect";
3132
import * as Exit from "effect/Exit";
@@ -271,6 +272,7 @@ describe("ProviderRuntimeIngestion", () => {
271272
threadTitle?: string;
272273
workspaceSubdirectory?: string;
273274
isGitRepository?: CheckpointStore.CheckpointStore["Service"]["isGitRepository"];
275+
strictProviderLifecycleGuard?: boolean;
274276
}) {
275277
const repositoryRoot = makeTempDir("t3-provider-project-");
276278
NodeChildProcess.execFileSync("git", ["init", "--initial-branch=main"], {
@@ -342,6 +344,20 @@ describe("ProviderRuntimeIngestion", () => {
342344
),
343345
Layer.provideMerge(VcsProcess.layer),
344346
Layer.provideMerge(ServerConfig.layerTest(process.cwd(), process.cwd())),
347+
Layer.provideMerge(
348+
ConfigProvider.layer(
349+
ConfigProvider.fromEnv({
350+
env:
351+
options?.strictProviderLifecycleGuard === undefined
352+
? {}
353+
: {
354+
T3CODE_STRICT_PROVIDER_LIFECYCLE_GUARD: String(
355+
options.strictProviderLifecycleGuard,
356+
),
357+
},
358+
}),
359+
),
360+
),
345361
Layer.provideMerge(NodeServices.layer),
346362
Layer.provideMerge(Layer.succeed(Tracer.Tracer, sqlCounter.tracer)),
347363
);
@@ -1359,6 +1375,39 @@ describe("ProviderRuntimeIngestion", () => {
13591375
);
13601376
});
13611377

1378+
it("applies non-active lifecycle events when strict guarding is disabled", async () => {
1379+
const harness = await createHarness({ strictProviderLifecycleGuard: false });
1380+
const now = "2026-01-01T00:00:00.000Z";
1381+
1382+
harness.emit({
1383+
type: "turn.started",
1384+
eventId: asEventId("evt-turn-started-non-strict"),
1385+
provider: ProviderDriverKind.make("codex"),
1386+
createdAt: now,
1387+
threadId: asThreadId("thread-1"),
1388+
turnId: asTurnId("turn-non-strict-main"),
1389+
});
1390+
await waitForThread(
1391+
harness.readModel,
1392+
(thread) => thread.session?.activeTurnId === "turn-non-strict-main",
1393+
);
1394+
1395+
harness.emit({
1396+
type: "turn.completed",
1397+
eventId: asEventId("evt-turn-completed-non-strict"),
1398+
provider: ProviderDriverKind.make("codex"),
1399+
createdAt: now,
1400+
threadId: asThreadId("thread-1"),
1401+
turnId: asTurnId("turn-non-strict-other"),
1402+
status: "completed",
1403+
});
1404+
1405+
await waitForThread(
1406+
harness.readModel,
1407+
(thread) => thread.session?.status === "ready" && thread.session?.activeTurnId === null,
1408+
);
1409+
});
1410+
13621411
it("ignores provider content deltas that cannot change thread state", async () => {
13631412
const harness = await createHarness();
13641413
const initial = await harness.readModel();

‎apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import { makeDrainableWorker } from "@t3tools/shared/DrainableWorker";
3434
import { formatTokens } from "@t3tools/shared/usageFormat";
3535

3636
import { ProviderService } from "../../provider/Services/ProviderService.ts";
37+
import { serverEnvironmentConfig } from "../../cli/config.ts";
3738
import { ProjectionTurnRepository } from "../../persistence/Services/ProjectionTurns.ts";
3839
import { ProjectionTurnRepositoryLive } from "../../persistence/Layers/ProjectionTurns.ts";
3940
import { ProjectionThreadActivityRepository } from "../../persistence/Services/ProjectionThreadActivities.ts";
@@ -120,7 +121,6 @@ const MAX_BUFFERED_ASSISTANT_CHARS = 24_000;
120121
// message several times a second while still showing the first paragraph
121122
// as soon as it is done.
122123
const MIN_ASSISTANT_DELIVERY_INTERVAL_MS = 400;
123-
const STRICT_PROVIDER_LIFECYCLE_GUARD = process.env.T3CODE_STRICT_PROVIDER_LIFECYCLE_GUARD !== "0";
124124

125125
type TurnStartRequestedDomainEvent = Extract<
126126
OrchestrationEvent,
@@ -1028,6 +1028,7 @@ const make = Effect.gen(function* () {
10281028
const projectionTurnRepository = yield* ProjectionTurnRepository;
10291029
const projectionThreadActivityRepository = yield* ProjectionThreadActivityRepository;
10301030
const serverSettingsService = yield* ServerSettingsService;
1031+
const strictProviderLifecycleGuard = yield* serverEnvironmentConfig.strictProviderLifecycleGuard;
10311032
const checkpointStore = yield* CheckpointStore.CheckpointStore;
10321033
const providerCommandId = (event: ProviderRuntimeEvent, tag: string) =>
10331034
crypto.randomUUIDv4.pipe(
@@ -1807,7 +1808,7 @@ const make = Effect.gen(function* () {
18071808
: false;
18081809

18091810
const shouldApplyThreadLifecycle = (() => {
1810-
if (!STRICT_PROVIDER_LIFECYCLE_GUARD) {
1811+
if (!strictProviderLifecycleGuard) {
18111812
return true;
18121813
}
18131814
switch (event.type) {
@@ -2408,7 +2409,7 @@ const make = Effect.gen(function* () {
24082409
if (event.type === "runtime.error") {
24092410
const runtimeErrorMessage = event.payload.message;
24102411

2411-
const shouldApplyRuntimeError = !STRICT_PROVIDER_LIFECYCLE_GUARD
2412+
const shouldApplyRuntimeError = !strictProviderLifecycleGuard
24122413
? true
24132414
: activeTurnId === null || eventTurnId === undefined || sameId(activeTurnId, eventTurnId);
24142415

0 commit comments

Comments
 (0)