Skip to content
Closed
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
fc384cd
Fix steering acknowledgement and turn interruption
Quicksaver Jul 11, 2026
298f4b5
Preserve root turn interruption projection
Quicksaver Jul 11, 2026
cf6b0c8
Harden steering acknowledgement and turn interruption
Quicksaver Jul 11, 2026
3414977
Fix active Codex turn selection without timestamps
Quicksaver Jul 12, 2026
e2e05c8
Merge upstream Android support and preserve steering fixes
Quicksaver Jul 13, 2026
06e38dc
Align Expo updates with fork project
Quicksaver Jul 13, 2026
ef1f35e
Merge upstream/main into fix/repeated-steering-and-stop
Quicksaver Jul 15, 2026
dd45496
Merge remote-tracking branch 'upstream/main' into fix/repeated-steeri…
Quicksaver Jul 16, 2026
6c08e81
Remove unrelated Expo fork customization
Quicksaver Jul 16, 2026
726dd73
Document repeated steering and reliable stop behavior
Quicksaver Jul 17, 2026
0ac7499
Correct repeated steering branch documentation
Quicksaver Jul 17, 2026
195fbca
Merge remote-tracking branch 'upstream/main' into fix/repeated-steeri…
Quicksaver Jul 18, 2026
425371a
Document upstream merge impact on repeated steering
Quicksaver Jul 18, 2026
8a889a6
Isolate Codex interrupt resolution coverage
Quicksaver Jul 18, 2026
d1dca69
Keep steering acknowledgements and fallback turns current
Quicksaver Jul 18, 2026
0bf687f
Harden steering and interrupt review coverage
Quicksaver Jul 18, 2026
cb6190e
Refresh branch assessment details
Quicksaver Jul 18, 2026
4277c28
Refresh branch guidance for steering and stop fixes
Quicksaver Jul 19, 2026
e13518a
Merge remote-tracking branch 'upstream/main' into fix/repeated-steeri…
Quicksaver Jul 19, 2026
7f9ab76
Merge remote-tracking branch 'upstream/main' into fix/repeated-steeri…
Quicksaver Jul 19, 2026
e47c771
Merge remote-tracking branch 'upstream/main' into fix/repeated-steeri…
Quicksaver Jul 19, 2026
c1a0db3
Fix pnpm virtual store and Effect Vitest resolution
Quicksaver Jul 19, 2026
5384094
Merge remote-tracking branch 'upstream/main' into fix/repeated-steeri…
Quicksaver Jul 20, 2026
d193aba
Merge remote-tracking branch 'upstream/main' into fix/repeated-steeri…
Quicksaver Jul 20, 2026
4d21b97
Merge remote-tracking branch 'upstream/main' into fix/repeated-steeri…
Quicksaver Jul 20, 2026
31b31bc
Merge remote-tracking branch 'upstream/main' into fix/repeated-steeri…
Quicksaver Jul 21, 2026
e8d2dbf
Merge remote-tracking branch 'upstream/main' into fix/repeated-steeri…
Quicksaver Jul 22, 2026
811695c
Merge remote-tracking branch 'upstream/main' into fix/repeated-steeri…
Quicksaver Jul 22, 2026
bd6ddbf
Merge remote-tracking branch 'upstream/main' into fix/repeated-steeri…
Quicksaver Jul 23, 2026
75fd9ae
Merge remote-tracking branch 'upstream/main' into fix/repeated-steeri…
Quicksaver Jul 23, 2026
232edcb
Merge remote-tracking branch 'upstream/main' into fix/repeated-steeri…
Quicksaver Jul 23, 2026
ec00a93
Merge remote-tracking branch 'upstream/main' into fix/repeated-steeri…
Quicksaver Jul 23, 2026
26f8a14
Merge remote-tracking branch 'upstream/main' into fix/repeated-steeri…
Quicksaver Jul 23, 2026
05dcf7d
Merge remote-tracking branch 'upstream/main' into fix/repeated-steeri…
Quicksaver Jul 23, 2026
58f045e
Merge remote-tracking branch 'upstream/main' into fix/repeated-steeri…
Quicksaver Jul 23, 2026
bdb5944
Merge remote-tracking branch 'upstream/main' into fix/repeated-steeri…
Quicksaver Jul 23, 2026
b56532d
Acknowledge steers despite stale session state
Quicksaver Jul 23, 2026
836492d
Merge remote-tracking branch 'upstream/main' into fix/repeated-steeri…
Quicksaver Jul 24, 2026
c3852d1
Merge remote-tracking branch 'upstream/main' into fix/repeated-steeri…
Quicksaver Jul 24, 2026
d8b15e2
Recover Codex interrupts from lookup defects
Quicksaver Jul 24, 2026
935a1e3
Merge remote-tracking branch 'upstream/main' into fix/repeated-steeri…
Quicksaver Jul 24, 2026
3ac5d06
Merge remote-tracking branch 'upstream/main' into fix/repeated-steeri…
Quicksaver Jul 25, 2026
36e8ba8
Preserve interrupt fallback turn typing
Quicksaver Jul 26, 2026
d7ab3ce
Remove main-only pnpm global store pollution
Quicksaver Jul 26, 2026
5a9a1d4
Merge remote-tracking branch 'upstream/main' into fix/repeated-steeri…
Quicksaver Jul 27, 2026
b7159b2
Merge remote-tracking branch 'upstream/main' into fix/repeated-steeri…
Quicksaver Jul 27, 2026
98a4a9d
Merge remote-tracking branch 'upstream/main' into fix/repeated-steeri…
Quicksaver Jul 27, 2026
9ac6478
Merge remote-tracking branch 'upstream/main' into fix/repeated-steeri…
Quicksaver Jul 27, 2026
2e23384
Merge commit '32843c2551bd5832d436ef46bff5ab1198cdc39c' into fix/repe…
Quicksaver Jul 27, 2026
e1361f5
Merge remote-tracking branch 'upstream/main' into fix/repeated-steeri…
Jul 28, 2026
6154c47
Merge remote-tracking branch 'upstream/main' into fix/repeated-steeri…
Jul 28, 2026
b111e0a
Merge remote-tracking branch 'upstream/main' into fix/repeated-steeri…
Jul 28, 2026
5f77c89
Merge remote-tracking branch 'upstream/main' into fix/repeated-steeri…
Jul 28, 2026
8733d8b
Merge remote-tracking branch 'upstream/main' into fix/repeated-steeri…
Jul 28, 2026
0d6358b
Merge remote-tracking branch 'upstream/main' into fix/repeated-steeri…
Jul 29, 2026
798fab4
Merge remote-tracking branch 'upstream/main' into fix/repeated-steeri…
Jul 29, 2026
01f0102
Merge remote-tracking branch 'upstream/main' into fix/repeated-steeri…
Jul 29, 2026
9f31614
Merge remote-tracking branch 'upstream/main' into fix/repeated-steeri…
Jul 29, 2026
b8eecd3
Merge remote-tracking branch 'upstream/main' into fix/repeated-steeri…
Jul 29, 2026
d4627f7
Merge remote-tracking branch 'upstream/main' into fix/repeated-steeri…
Jul 29, 2026
0517393
Merge remote-tracking branch 'upstream/main' into fix/repeated-steeri…
Jul 30, 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
6 changes: 3 additions & 3 deletions apps/mobile/app.config.ts
Comment thread
macroscopeapp[bot] marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ const config: ExpoConfig = {
userInterfaceStyle: "automatic",
updates: {
enabled: true,
url: "https://u.expo.dev/d763fcb8-d37c-41ea-a773-b54a0ab4a454",
url: "https://u.expo.dev/c65ac46d-6488-49af-b61e-ab9bef78f96e",
Comment thread
Quicksaver marked this conversation as resolved.
Outdated
checkAutomatically: "ON_LOAD",
fallbackToCacheTimeout: 0,
},
Expand Down Expand Up @@ -285,10 +285,10 @@ const config: ExpoConfig = {
tracesToken: repoEnv.EXPO_PUBLIC_OTLP_TRACES_TOKEN ?? null,
},
eas: {
projectId: "d763fcb8-d37c-41ea-a773-b54a0ab4a454",
projectId: "c65ac46d-6488-49af-b61e-ab9bef78f96e",
},
},
owner: "pingdotgg",
owner: "quicksaver",
};

export default config;
168 changes: 165 additions & 3 deletions apps/server/src/provider/Layers/CodexSessionRuntime.test.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,27 @@
import * as NodeAssert from "node:assert/strict";

import { it } from "@effect/vitest";
import * as Effect from "effect/Effect";
import * as Fiber from "effect/Fiber";
import * as Schema from "effect/Schema";
import { describe } from "vite-plus/test";
import { ThreadId } from "@t3tools/contracts";
import * as TestClock from "effect/testing/TestClock";
import { describe, it } from "@effect/vitest";
import { ThreadId, TurnId } from "@t3tools/contracts";
import * as CodexErrors from "effect-codex-app-server/errors";
import * as CodexRpc from "effect-codex-app-server/rpc";
import type * as EffectCodexSchema from "effect-codex-app-server/schema";

import {
CODEX_DEFAULT_MODE_DEVELOPER_INSTRUCTIONS,
CODEX_PLAN_MODE_DEVELOPER_INSTRUCTIONS,
} from "../CodexDeveloperInstructions.ts";
import {
buildTurnStartParams,
findActiveCodexTurnId,
hasConfiguredMcpServer,
isRecoverableThreadResumeError,
openCodexThread,
resolveCodexInterruptTurnId,
shouldPreferActiveCodexTurnCandidate,
} from "./CodexSessionRuntime.ts";
const isCodexAppServerRequestError = Schema.is(CodexErrors.CodexAppServerRequestError);

Expand Down Expand Up @@ -60,6 +65,27 @@ function makeThreadOpenResponse(
} as unknown as CodexRpc.ClientRequestResponsesByMethod["thread/start"];
}

function makeThreadReadResponse(
turns: EffectCodexSchema.V2ThreadReadResponse["thread"]["turns"],
): EffectCodexSchema.V2ThreadReadResponse {
return {
thread: {
cliVersion: "0.0.0-test",
createdAt: 1,
cwd: "/tmp/project",
ephemeral: false,
id: "provider-thread-1",
modelProvider: "openai",
preview: "test thread",
sessionId: "session-1",
source: "appServer",
status: { type: "active", activeFlags: [] },
turns,
updatedAt: 2,
},
};
}

describe("buildTurnStartParams", () => {
it("keeps invalid turn values only in the schema cause", () => {
const secret = "codex-turn-input-secret-sentinel";
Expand Down Expand Up @@ -194,6 +220,142 @@ describe("buildTurnStartParams", () => {
});
});

describe("findActiveCodexTurnId", () => {
it("selects the most recently started in-progress turn", () => {
const snapshot = makeThreadReadResponse([
{ id: "turn-active-new", status: "inProgress", startedAt: 30, items: [] },
{ id: "turn-completed", status: "completed", startedAt: 20, items: [] },
{ id: "turn-active-old", status: "inProgress", startedAt: 10, items: [] },
]);

NodeAssert.equal(findActiveCodexTurnId(snapshot), "turn-active-new");
});

it("selects a later in-progress turn without a start timestamp", () => {
const snapshot = makeThreadReadResponse([
{ id: "turn-active-old", status: "inProgress", startedAt: 10, items: [] },
{ id: "turn-active-new", status: "inProgress", items: [] },
]);

NodeAssert.equal(findActiveCodexTurnId(snapshot), "turn-active-new");
});

it("selects a later timestamped turn after one without a timestamp", () => {
const snapshot = makeThreadReadResponse([
{ id: "turn-active-old", status: "inProgress", items: [] },
{ id: "turn-active-new", status: "inProgress", startedAt: 10, items: [] },
]);

NodeAssert.equal(findActiveCodexTurnId(snapshot), "turn-active-new");
});

it("returns undefined when no turn is active", () => {
const response = makeThreadReadResponse([]);
NodeAssert.equal(findActiveCodexTurnId(response), undefined);
});

it.effect("requests turns when resolving an interrupt without a projected turn id", () => {
let requestedParams: CodexRpc.ClientRequestParamsByMethod["thread/read"] | undefined;

return Effect.gen(function* () {
const turnId = yield* resolveCodexInterruptTurnId({
providerThreadId: "provider-thread-1",
requestedTurnId: undefined,
sessionActiveTurnId: undefined,
readThread: (params) => {
requestedParams = params;
return Effect.succeed(
makeThreadReadResponse([
{ id: "turn-active", status: "inProgress", startedAt: 10, items: [] },
]),
);
},
});

NodeAssert.deepStrictEqual(requestedParams, {
threadId: "provider-thread-1",
includeTurns: true,
});
NodeAssert.equal(turnId, "turn-active");
});
});

it.effect("does not revive a stale projected turn after a successful empty read", () =>
Effect.gen(function* () {
const turnId = yield* resolveCodexInterruptTurnId({
providerThreadId: "provider-thread-1",
requestedTurnId: undefined,
sessionActiveTurnId: TurnId.make("turn-stale"),
readThread: () => Effect.succeed(makeThreadReadResponse([])),
});

NodeAssert.equal(turnId, undefined);
}),
);

it.effect("falls back to the projected turn when the live lookup fails", () =>
Effect.gen(function* () {
const projectedTurnId = TurnId.make("turn-projected");
const turnId = yield* resolveCodexInterruptTurnId({
providerThreadId: "provider-thread-1",
requestedTurnId: undefined,
sessionActiveTurnId: projectedTurnId,
readThread: () => Effect.fail("lookup failed"),
});

NodeAssert.equal(turnId, projectedTurnId);
}),
);

it.effect("bounds the live lookup and falls back to the projected turn on timeout", () =>
Effect.gen(function* () {
const projectedTurnId = TurnId.make("turn-projected");
const resolution = yield* resolveCodexInterruptTurnId({
providerThreadId: "provider-thread-1",
requestedTurnId: undefined,
sessionActiveTurnId: projectedTurnId,
readThread: () => Effect.never,
}).pipe(Effect.forkScoped);

yield* Effect.yieldNow;
yield* TestClock.adjust("2 seconds");
NodeAssert.equal(yield* Fiber.join(resolution), projectedTurnId);
}),
);
});

describe("shouldPreferActiveCodexTurnCandidate", () => {
it("selects the first candidate", () => {
NodeAssert.equal(shouldPreferActiveCodexTurnCandidate({ startedAt: 10 }, undefined), true);
});

it("orders timestamped turns by start time and lets a later equal entry win", () => {
NodeAssert.equal(
shouldPreferActiveCodexTurnCandidate({ startedAt: 20 }, { startedAt: 10 }),
true,
);
NodeAssert.equal(
shouldPreferActiveCodexTurnCandidate({ startedAt: 10 }, { startedAt: 20 }),
false,
);
NodeAssert.equal(
shouldPreferActiveCodexTurnCandidate({ startedAt: 10 }, { startedAt: 10 }),
true,
);
});

it("lets the later provider entry win when either timestamp is absent", () => {
for (const [candidate, selected] of [
[{}, { startedAt: 10 }],
[{ startedAt: null }, { startedAt: 10 }],
[{ startedAt: 10 }, {}],
[{ startedAt: 10 }, { startedAt: null }],
] as const) {
NodeAssert.equal(shouldPreferActiveCodexTurnCandidate(candidate, selected), true);
}
});
});

describe("T3 browser developer instructions", () => {
it("prefers the product-native preview tools in both collaboration modes", () => {
for (const instructions of [
Expand Down
77 changes: 76 additions & 1 deletion apps/server/src/provider/Layers/CodexSessionRuntime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ const BENIGN_ERROR_LOG_SNIPPETS = [
"state db record_discrepancy: find_thread_path_by_id_str_in_subdir, falling_back",
];
const CODEX_APP_SERVER_FORCE_KILL_AFTER = "2 seconds" as const;
const CODEX_INTERRUPT_THREAD_READ_TIMEOUT = "2 seconds" as const;
const RECOVERABLE_THREAD_RESUME_ERROR_SNIPPETS = [
"not found",
"missing thread",
Expand Down Expand Up @@ -692,6 +693,75 @@ function parseThreadSnapshot(
};
}

type CodexTurnOrderingCandidate = Pick<
EffectCodexSchema.V2ThreadReadResponse["thread"]["turns"][number],
"startedAt"
>;

export function shouldPreferActiveCodexTurnCandidate(
candidate: CodexTurnOrderingCandidate,
selected: CodexTurnOrderingCandidate | undefined,
): boolean {
if (selected === undefined) {
return true;
}

// When either timestamp is absent, provider response order is authoritative.
// The caller scans in response order, so the later candidate replaces the selection.
if (candidate.startedAt == null || selected.startedAt == null) {
return true;
}

return candidate.startedAt >= selected.startedAt;
}

export function findActiveCodexTurnId(
response: EffectCodexSchema.V2ThreadReadResponse,
): TurnId | undefined {
let activeTurn: EffectCodexSchema.V2ThreadReadResponse["thread"]["turns"][number] | undefined;
for (const turn of response.thread.turns) {
if (turn.status !== "inProgress") {
continue;
}
if (shouldPreferActiveCodexTurnCandidate(turn, activeTurn)) {
activeTurn = turn;
}
Comment thread
cursor[bot] marked this conversation as resolved.
}
return activeTurn === undefined ? undefined : TurnId.make(activeTurn.id);
}

export function resolveCodexInterruptTurnId<E>(input: {
readonly providerThreadId: string;
readonly requestedTurnId: TurnId | undefined;
readonly sessionActiveTurnId: TurnId | undefined;
readonly readThread: (
params: CodexRpc.ClientRequestParamsByMethod["thread/read"],
) => Effect.Effect<CodexRpc.ClientRequestResponsesByMethod["thread/read"], E>;
}): Effect.Effect<TurnId | undefined> {
if (input.requestedTurnId !== undefined) {
return Effect.succeed(input.requestedTurnId);
}

return input
.readThread({
threadId: input.providerThreadId,
includeTurns: true,
})
.pipe(
Effect.timeout(CODEX_INTERRUPT_THREAD_READ_TIMEOUT),
Effect.map(findActiveCodexTurnId),
Effect.tapError((cause) =>
Effect.logWarning("Failed to resolve active Codex turn before interrupt.", {
providerThreadId: input.providerThreadId,
cause,
}),
),
// A failed lookup can still use the locally projected id. A successful
// lookup with no active turn must not revive a stale local id.
Effect.orElseSucceed(() => input.sessionActiveTurnId),
);
}

export const makeCodexSessionRuntime = (
options: CodexSessionRuntimeOptions,
): Effect.Effect<
Expand Down Expand Up @@ -1316,7 +1386,12 @@ export const makeCodexSessionRuntime = (
Effect.gen(function* () {
const providerThreadId = yield* readProviderThreadId;
const session = yield* Ref.get(sessionRef);
const effectiveTurnId = turnId ?? session.activeTurnId;
const effectiveTurnId = yield* resolveCodexInterruptTurnId({
providerThreadId,
requestedTurnId: turnId,
sessionActiveTurnId: session.activeTurnId,
readThread: (params) => client.request("thread/read", params),
});
if (!effectiveTurnId) {
return;
}
Expand Down
Loading
Loading