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