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
1 change: 0 additions & 1 deletion apps/desktop/src/settings/DesktopClientSettings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ const clientSettings: ClientSettings = {
confirmThreadArchive: true,
confirmThreadDelete: false,
confirmThreadUnpin: false,
continueThreadsAfterServerUpdate: true,
contextWindowMeterEnabled: false,
composerCollapseOnBlur: false,
composerCollapseOnScroll: true,
Expand Down
119 changes: 119 additions & 0 deletions apps/server/src/provider/Layers/ProviderService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -452,6 +452,125 @@ function makeProviderServiceLayer(
};
}

for (const [enabled, completed] of [
[false, false],
[true, false],
[true, true],
] as const) {
it.effect(
`persists shutdown recovery before stopping providers when enabled=${enabled}, completed=${completed}`,
() =>
Effect.gen(function* () {
const codex = makeFakeCodexAdapter();
const persistence = yield* Layer.build(
ProviderSessionDirectoryLive.pipe(
Layer.provide(
ProviderSessionRuntime.layer.pipe(Layer.provide(SqlitePersistenceMemory)),
),
),
);
const directory = yield* ProviderSessionDirectory.ProviderSessionDirectory.pipe(
Effect.provide(persistence),
);
const threadId = asThreadId("shutdown-recovery");
const turnId = asTurnId("shutdown-recovery-turn");
const scope = yield* Scope.make();
const services = yield* Layer.build(
makeProviderServiceLive().pipe(
Layer.provide(
Layer.succeed(ProviderSessionDirectory.ProviderSessionDirectory, directory),
),
Layer.provide(
Layer.succeed(
ProviderAdapterRegistry.ProviderAdapterRegistry,
makeStaticInstanceRegistry([[codexInstanceId, codex.adapter]]),
),
),
Layer.provide(ServerSettings.layerTest({ continueThreadsAfterServerUpdate: enabled })),
Layer.provide(serverConfigTestLayer),
Layer.provide(AnalyticsService.layerTest),
Layer.provide(
Layer.succeed(
ProviderEventLoggers.ProviderEventLoggers,
ProviderEventLoggers.NoOpProviderEventLoggers,
),
),
),
).pipe(Scope.provide(scope));
const provider = yield* ProviderService.ProviderService.pipe(Effect.provide(services));
const session = yield* provider.startSession(threadId, {
provider: CODEX_DRIVER,
providerInstanceId: codexInstanceId,
threadId,
runtimeMode: "full-access",
});
codex.listSessions.mockReturnValue(
Effect.succeed([
{
...session,
status: completed ? "ready" : "running",
activeTurnId: completed ? undefined : turnId,
},
]),
);
const pending = yield* directory.getBinding(threadId);
assert(Option.isSome(pending));
yield* directory.upsert({
...pending.value,
runtimePayload: { activeTurnId: null, continueAfterServerUpdate: turnId },
});
const accepted = yield* provider.sendTurn({ threadId, continuation: true });
const admitted = yield* directory.getBinding(threadId);
assert(Option.isSome(admitted));
assert.propertyVal(admitted.value.runtimePayload, "activeTurnId", accepted.turnId);
assert.propertyVal(admitted.value.runtimePayload, "continueAfterServerUpdate", null);
if (completed) {
// Updates can mark an already-admitted turn immediately before it finishes.
yield* directory.upsert({
...admitted.value,
runtimePayload: {
continueAfterServerUpdate: accepted.turnId,
continueAfterServerUpdatePrepared: null,
},
});
}
const markers: unknown[] = [];
codex.stopAll.mockImplementation(() =>
Effect.gen(function* () {
const binding = yield* directory.getBinding(threadId);
assert(Option.isSome(binding));
markers.push(binding.value.runtimePayload);
}).pipe(Effect.orDie),
);
yield* Scope.close(scope, Exit.void);
const binding = yield* directory.getBinding(threadId);
assert(Option.isSome(binding));
assert.equal(codex.stopAll.mock.calls.length, 1);
assert.deepStrictEqual(binding.value.resumeCursor, session.resumeCursor);
assert.equal(binding.value.status, "stopped");
assert.propertyVal(markers[0], "activeTurnId", completed ? null : turnId);
if (enabled && !completed) {
assert.propertyVal(markers[0], "continueAfterServerUpdate", turnId);
assert.propertyVal(binding.value.runtimePayload, "continueAfterServerUpdate", turnId);
} else if (completed) {
assert.propertyVal(
binding.value.runtimePayload,
"continueAfterServerUpdate",
accepted.turnId,
);
assert.propertyVal(
binding.value.runtimePayload,
"continueAfterServerUpdatePrepared",
null,
);
} else {
assert.propertyVal(markers[0], "continueAfterServerUpdate", null);
assert.propertyVal(binding.value.runtimePayload, "continueAfterServerUpdate", null);
}
}).pipe(Effect.provide(NodeServices.layer)),
);
}

it.effect("ProviderServiceLive catches stopAll failures during shutdown", () =>
Effect.gen(function* () {
const codex = makeFakeCodexAdapter();
Expand Down
17 changes: 17 additions & 0 deletions apps/server/src/provider/Layers/ProviderService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,7 @@ function toRuntimePayloadFromSession(
session: ProviderSession,
extra?: {
readonly modelSelection?: unknown;
readonly continueAfterServerUpdate?: TurnId;
readonly lastRuntimeEvent?: string;
readonly lastRuntimeEventAt?: string;
},
Expand All @@ -232,6 +233,9 @@ function toRuntimePayloadFromSession(
model: session.model ?? null,
activeTurnId: session.activeTurnId ?? null,
lastError: session.lastError ?? null,
...(extra?.continueAfterServerUpdate !== undefined
? { continueAfterServerUpdate: extra.continueAfterServerUpdate }
: {}),
...(extra?.modelSelection !== undefined ? { modelSelection: extra.modelSelection } : {}),
...(extra?.lastRuntimeEvent !== undefined ? { lastRuntimeEvent: extra.lastRuntimeEvent } : {}),
...(extra?.lastRuntimeEventAt !== undefined
Expand Down Expand Up @@ -831,6 +835,7 @@ const makeProviderService = Effect.fn("makeProviderService")(function* (
threadId: ThreadId,
extra?: {
readonly modelSelection?: unknown;
readonly continueAfterServerUpdate?: TurnId;
readonly lastRuntimeEvent?: string;
readonly lastRuntimeEventAt?: string;
},
Expand Down Expand Up @@ -1433,6 +1438,9 @@ const makeProviderService = Effect.fn("makeProviderService")(function* (
runtimePayload: {
...(input.modelSelection !== undefined ? { modelSelection: input.modelSelection } : {}),
activeTurnId: turn.turnId,
// Admission and marker consumption must survive the same restart.
continueAfterServerUpdate: null,
continueAfterServerUpdatePrepared: null,
lastRuntimeEvent: "provider.sendTurn",
lastRuntimeEventAt: yield* nowIso,
},
Expand Down Expand Up @@ -1733,6 +1741,8 @@ const makeProviderService = Effect.fn("makeProviderService")(function* (
status: "stopped",
runtimePayload: {
activeTurnId: null,
continueAfterServerUpdate: null,
continueAfterServerUpdatePrepared: null,
},
});
yield* analytics.record("provider.session.stopped", {
Expand Down Expand Up @@ -1942,6 +1952,10 @@ const makeProviderService = Effect.fn("makeProviderService")(function* (
);

const runStopAll = Effect.fn("runStopAll")(function* () {
const continueAfterRestart = yield* serverSettings.getSettings.pipe(
Effect.map((settings) => settings.continueThreadsAfterServerUpdate),
Effect.orElseSucceed(() => false),
);
const properties = yield* Ref.modify(turnAnalytics, (state) => {
const completed: Array<Readonly<Record<string, unknown>>> = [];
for (const [sessionKey, session] of state.sessions) {
Expand Down Expand Up @@ -1969,6 +1983,9 @@ const makeProviderService = Effect.fn("makeProviderService")(function* (
yield* Effect.forEach(activeSessions, (session) =>
Effect.flatMap(nowIso, (lastRuntimeEventAt) =>
upsertSessionBinding(session, session.threadId, {
...(continueAfterRestart && session.status === "running" && session.activeTurnId
? { continueAfterServerUpdate: session.activeTurnId }
: {}),
lastRuntimeEvent: "provider.stopAll",
lastRuntimeEventAt,
}),
Expand Down
Loading
Loading