Skip to content

Commit 59fdbb0

Browse files
juliusmarmingecodex
andcommitted
Preflight archive session cleanup
Co-authored-by: codex <[email protected]>
1 parent 83b005e commit 59fdbb0

2 files changed

Lines changed: 94 additions & 11 deletions

File tree

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

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3043,6 +3043,84 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
30433043
}).pipe(Effect.provide(NodeHttpServer.layerTest)),
30443044
);
30453045

3046+
it.effect("checks session status before archiving removes the thread from active lookups", () =>
3047+
Effect.gen(function* () {
3048+
const threadId = ThreadId.make("thread-archive-precheck");
3049+
const effects: string[] = [];
3050+
const dispatchedCommands: Array<OrchestrationCommand> = [];
3051+
const now = new Date().toISOString();
3052+
let archived = false;
3053+
3054+
yield* buildAppUnderTest({
3055+
layers: {
3056+
terminalManager: {
3057+
close: (input) =>
3058+
Effect.sync(() => {
3059+
effects.push(`terminal.close:${input.threadId}`);
3060+
}),
3061+
},
3062+
orchestrationEngine: {
3063+
dispatch: (command) =>
3064+
Effect.sync(() => {
3065+
dispatchedCommands.push(command);
3066+
effects.push(`dispatch:${command.type}`);
3067+
if (command.type === "thread.archive") {
3068+
archived = true;
3069+
}
3070+
return { sequence: dispatchedCommands.length };
3071+
}),
3072+
},
3073+
projectionSnapshotQuery: {
3074+
getThreadShellById: () =>
3075+
Effect.sync(() => {
3076+
effects.push(`query:thread-shell:${archived ? "archived" : "active"}`);
3077+
return archived
3078+
? Option.none()
3079+
: Option.some(
3080+
makeDefaultOrchestrationThreadShell({
3081+
id: threadId,
3082+
updatedAt: now,
3083+
session: {
3084+
threadId,
3085+
status: "ready",
3086+
providerName: "claudeAgent",
3087+
runtimeMode: "full-access",
3088+
activeTurnId: null,
3089+
lastError: null,
3090+
updatedAt: now,
3091+
},
3092+
}),
3093+
);
3094+
}),
3095+
},
3096+
},
3097+
});
3098+
3099+
const wsUrl = yield* getWsServerUrl("/ws");
3100+
const dispatchResult = yield* Effect.scoped(
3101+
withWsRpcClient(wsUrl, (client) =>
3102+
client[ORCHESTRATION_WS_METHODS.dispatchCommand]({
3103+
type: "thread.archive",
3104+
commandId: CommandId.make("cmd-thread-archive-precheck"),
3105+
threadId,
3106+
}),
3107+
),
3108+
);
3109+
3110+
assert.equal(dispatchResult.sequence, 1);
3111+
assert.deepEqual(effects, [
3112+
"query:thread-shell:active",
3113+
"dispatch:thread.archive",
3114+
"dispatch:thread.session.stop",
3115+
`terminal.close:${threadId}`,
3116+
]);
3117+
assert.deepEqual(
3118+
dispatchedCommands.map((command) => command.type),
3119+
["thread.archive", "thread.session.stop"],
3120+
);
3121+
}).pipe(Effect.provide(NodeHttpServer.layerTest)),
3122+
);
3123+
30463124
it.effect("archives without dispatching session stop when the thread has no session", () =>
30473125
Effect.gen(function* () {
30483126
const threadId = ThreadId.make("thread-archive-no-session");

‎apps/server/src/ws.ts‎

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -550,19 +550,24 @@ const makeWsRpcLayer = (currentSessionId: AuthSessionId) =>
550550
ORCHESTRATION_WS_METHODS.dispatchCommand,
551551
Effect.gen(function* () {
552552
const normalizedCommand = yield* normalizeDispatchCommand(command);
553+
const shouldStopSessionAfterArchive =
554+
normalizedCommand.type === "thread.archive"
555+
? yield* projectionSnapshotQuery
556+
.getThreadShellById(normalizedCommand.threadId)
557+
.pipe(
558+
Effect.map(
559+
Option.match({
560+
onNone: () => false,
561+
onSome: (thread) =>
562+
thread.session !== null && thread.session.status !== "stopped",
563+
}),
564+
),
565+
Effect.catch(() => Effect.succeed(false)),
566+
)
567+
: false;
553568
const result = yield* dispatchNormalizedCommand(normalizedCommand);
554569
if (normalizedCommand.type === "thread.archive") {
555-
const existingSessionStatus = yield* projectionSnapshotQuery
556-
.getThreadShellById(normalizedCommand.threadId)
557-
.pipe(
558-
Effect.map(Option.flatMapNullishOr((thread) => thread.session?.status)),
559-
Effect.catch(() => Effect.succeedNone),
560-
);
561-
562-
if (
563-
Option.isSome(existingSessionStatus) &&
564-
existingSessionStatus.value !== "stopped"
565-
) {
570+
if (shouldStopSessionAfterArchive) {
566571
yield* Effect.gen(function* () {
567572
const stopCommand = yield* normalizeDispatchCommand({
568573
type: "thread.session.stop",

0 commit comments

Comments
 (0)