Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
aade036
fix(server): provider sessions clean up when their start is interrupted
juliusmarminge Oct 4, 2026
be865b2
fix(server): keep a Pi turn whose prompt was sent, clean up sessions …
juliusmarminge Oct 4, 2026
e7741ec
fix(server): a kept Pi turn always has the response that settles it
juliusmarminge Oct 4, 2026
5beedc5
fix(server): a stuck session close no longer holds up an interrupted …
juliusmarminge Oct 5, 2026
b1c31ee
fix(server): a stopped turn start undoes only its own busy mark
juliusmarminge Oct 5, 2026
c32cef8
fix(server): a session tracks busy turns, not a count
juliusmarminge Oct 5, 2026
47a46bc
Merge remote-tracking branch 'origin/main' into t3code/session-scope-…
juliusmarminge Oct 5, 2026
53a6b7e
fix(server): OpenCode turns end on the provider thread they started on
juliusmarminge Oct 5, 2026
30f5e82
Merge branch 'main' into t3code/session-scope-interrupt-cleanup-2
juliusmarminge Oct 5, 2026
113f25e
Merge remote-tracking branch 'origin/main' into t3code/session-scope-…
juliusmarminge Oct 6, 2026
b084738
Merge remote-tracking branch 'origin/main' into t3code/session-scope-…
juliusmarminge Oct 6, 2026
fb47723
fix(server): a device hub stopped mid-start is always cleaned up
juliusmarminge Oct 6, 2026
850d5dd
fix(server): an interrupted attach no longer undoes a later attach of…
juliusmarminge Oct 6, 2026
35b7b0b
fix(server): an interrupted attach leaves a replacement session alone
juliusmarminge Oct 6, 2026
3a82d44
fix(server): a resume stopped while checking its credential drops the…
juliusmarminge Oct 6, 2026
5cc6a98
Merge remote-tracking branch 'origin/main' into t3code/session-scope-…
juliusmarminge Oct 6, 2026
b0a9ce3
Merge branch 'main' into t3code/session-scope-interrupt-cleanup-2
juliusmarminge Oct 6, 2026
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
107 changes: 54 additions & 53 deletions apps/server/src/device/LocalDeviceHost.ts
Original file line number Diff line number Diff line change
Expand Up @@ -358,59 +358,60 @@ export const make = Effect.fn("LocalDeviceHost.make")(function* () {
);
const origin = `http://127.0.0.1:${port}`;
const scope = yield* Scope.make("sequential");
const child = yield* spawner
.spawn(
ChildProcess.make(
nodePath,
[
hubTool.entryPath,
"--port",
String(port),
"--host",
"127.0.0.1",
"--hide-sidebar",
"--hide-boot-device",
],
{
detached: false,
shell: false,
stdout: "pipe",
stderr: "pipe",
env: hubEnvironment(),
},
),
)
.pipe(
Effect.provideService(Scope.Scope, scope),
Effect.mapError(
(cause) =>
new DeviceHost.DeviceHostError({
hostId,
step: "starting the device hub",
cause,
}),
),
);
const startedAtMillis = yield* Clock.currentTimeMillis;
const hub: HubProcess = { child, scope, origin, startedAtMillis, nodePath };
yield* Effect.forkIn(observeHubOutput(hub), scope);
yield* waitForHttpReady({
baseUrl: origin,
path: "/readyz",
timeoutMs: HUB_READY_TIMEOUT_MS,
makeError: (info) =>
new DeviceHost.DeviceHostError({
hostId,
step: "waiting for the device hub to answer",
cause: info.cause,
}),
}).pipe(
Effect.provideService(HttpClient.HttpClient, httpClient),
Effect.tapError(() => stopHub(hub)),
);
yield* recordHub(hub, hubTool);
yield* Effect.logInfo("Device hub started", { pid: Number(child.pid), port });
return hub;
// On failure or interrupt, from the spawn on: the hub is not recorded yet,
// so nothing else stops it.
return yield* Effect.gen(function* () {
const child = yield* spawner
.spawn(
ChildProcess.make(
nodePath,
[
hubTool.entryPath,
"--port",
String(port),
"--host",
"127.0.0.1",
"--hide-sidebar",
"--hide-boot-device",
],
{
detached: false,
shell: false,
stdout: "pipe",
stderr: "pipe",
env: hubEnvironment(),
},
),
)
.pipe(
Effect.provideService(Scope.Scope, scope),
Effect.mapError(
(cause) =>
new DeviceHost.DeviceHostError({
hostId,
step: "starting the device hub",
cause,
}),
),
);
const startedAtMillis = yield* Clock.currentTimeMillis;
const hub: HubProcess = { child, scope, origin, startedAtMillis, nodePath };
yield* Effect.forkIn(observeHubOutput(hub), scope);
yield* waitForHttpReady({
baseUrl: origin,
path: "/readyz",
timeoutMs: HUB_READY_TIMEOUT_MS,
makeError: (info) =>
new DeviceHost.DeviceHostError({
hostId,
step: "waiting for the device hub to answer",
cause: info.cause,
}),
}).pipe(Effect.provideService(HttpClient.HttpClient, httpClient));
yield* recordHub(hub, hubTool);
yield* Effect.logInfo("Device hub started", { pid: Number(child.pid), port });
return hub;
}).pipe(Effect.onError(() => Scope.close(scope, Exit.void).pipe(Effect.ignore)));
Comment thread
coderabbitai[bot] marked this conversation as resolved.
});

const observeHubOutput = (hub: HubProcess) =>
Expand Down
3 changes: 2 additions & 1 deletion apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6962,7 +6962,8 @@ export function makeClaudeAdapterV2(
options: queryOptions,
})
.pipe(
Effect.tapError(() =>
// An interrupted open leaves the old process just as dead.
Effect.onError(() =>
// Same-native-thread replacement: the old process is already
// dead, so its process-scoped roster is not authoritative.
// First-ever failed open (no prior live query) must not invent
Expand Down
4 changes: 3 additions & 1 deletion apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5495,8 +5495,10 @@ export function makeCodexAdapterV2(adapterOptions: CodexAdapterV2Options): Provi
yield* Ref.update(pendingRootTurns, (current) =>
new Map(current).set(threadId, turnInput),
);
// Cleared on interrupt too, as startTurn does: an interrupted start
// must not adopt the native turn that a late turn/started reports.
yield* client.request("thread/compact/start", { threadId }).pipe(
Effect.tapError(() =>
Effect.onError(() =>
Ref.update(pendingRootTurns, (current) => {
const next = new Map(current);
next.delete(threadId);
Expand Down
11 changes: 6 additions & 5 deletions apps/server/src/orchestration-v2/Adapters/OpenCode2AdapterV2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2813,12 +2813,13 @@ export const make = Effect.fn("OpenCode2Adapter.make")(function* (instanceId: Pr
* reconciles. Retried a few times; the caller fails everything after that.
*/
const reconnect = Effect.gen(function* () {
// Returned on any failure, including the interrupt a closing session
// sends a reconnect still in flight, so a spawned server can idle-stop.
const scope = yield* Scope.make();
const next = yield* borrow.pipe(
Effect.provideService(Scope.Scope, scope),
Effect.tapError(() => Scope.close(scope, Exit.void)),
);
const stream = yield* next.events.pipe(Effect.tapError(() => Scope.close(scope, Exit.void)));
const { next, stream } = yield* Effect.gen(function* () {
const next = yield* borrow.pipe(Effect.provideService(Scope.Scope, scope));
return { next, stream: yield* next.events };
}).pipe(Effect.onError(() => Scope.close(scope, Exit.void)));
const previous = currentScope;
connection = next;
client = next.client;
Expand Down
3 changes: 2 additions & 1 deletion apps/server/src/orchestration-v2/Adapters/PiAdapterV2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2669,8 +2669,9 @@ export function makePiAdapterV2(
}
// Pi fork replaces the session file, including for rollback. Persist
// its new identity before any later request can fail or restart.
// An interrupted read leaves the identity just as unknown as a failed one.
const forkState = yield* request({ type: "get_state" }).pipe(
Effect.tapError(() =>
Effect.onError(() =>
Effect.sync(() => {
threadState = null;
}),
Expand Down
Loading
Loading