Skip to content

Commit c059e1e

Browse files
authored
Iphone 17 Handoff Failure (#673)
* Fix Codex handoff goal ordering * Address Codex handoff review feedback * Keep deferred Codex goal seeding best effort
1 parent bd5409f commit c059e1e

4 files changed

Lines changed: 145 additions & 13 deletions

File tree

apps/desktop/src/main/services/chat/agentChatService.test.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3181,6 +3181,83 @@ describe("createAgentChatService", () => {
31813181
}, { timeout: 2000, interval: 50 });
31823182
});
31833183

3184+
it("sends Codex brief handoff text before syncing the inherited goal", async () => {
3185+
const { service, sessionService } = createService();
3186+
const source = await service.createSession({
3187+
laneId: "lane-1",
3188+
provider: "codex",
3189+
model: "gpt-5.5",
3190+
modelId: "openai/gpt-5.5",
3191+
});
3192+
sessionService.updateMeta({
3193+
sessionId: source.id,
3194+
goal: "No Machine State Polish",
3195+
});
3196+
const sourceRow = mockState.sessions.get(source.id);
3197+
if (sourceRow) {
3198+
sourceRow.summary = "Fix the iPhone 17 simulator chat layout handoff.";
3199+
}
3200+
3201+
const handoffStart = mockState.codexRequestPayloads.length;
3202+
const result = await service.handoffSession({
3203+
sourceSessionId: source.id,
3204+
targetModelId: "openai/gpt-5.5",
3205+
});
3206+
3207+
expect(result.session.provider).toBe("codex");
3208+
expect(mockState.sessions.get(result.session.id)?.goal).toBe("No Machine State Polish");
3209+
3210+
const handoffPayloads = mockState.codexRequestPayloads.slice(handoffStart);
3211+
const requestMethods = handoffPayloads.map((payload) => String(payload.method ?? ""));
3212+
const turnStartIndex = requestMethods.indexOf("turn/start");
3213+
const goalSetIndex = requestMethods.indexOf("thread/goal/set");
3214+
expect(turnStartIndex).toBeGreaterThanOrEqual(0);
3215+
expect(goalSetIndex).toBeGreaterThan(turnStartIndex);
3216+
3217+
const turnStartRequest = handoffPayloads[turnStartIndex] as {
3218+
params?: { input?: Array<{ text?: unknown }> };
3219+
};
3220+
const inputText = turnStartRequest.params?.input?.map((entry) => String(entry.text ?? "")).join("\n") ?? "";
3221+
expect(inputText).toContain("This message was injected automatically by ADE during a chat handoff.");
3222+
expect(inputText).toContain("No Machine State Polish");
3223+
3224+
const goalSetRequest = handoffPayloads[goalSetIndex] as {
3225+
params?: { objective?: unknown };
3226+
};
3227+
expect(goalSetRequest.params?.objective).toBe("No Machine State Polish");
3228+
});
3229+
3230+
it("keeps Codex brief handoff successful when deferred goal seeding throws", async () => {
3231+
const { service, sessionService } = createService();
3232+
const source = await service.createSession({
3233+
laneId: "lane-1",
3234+
provider: "codex",
3235+
model: "gpt-5.5",
3236+
modelId: "openai/gpt-5.5",
3237+
});
3238+
sessionService.updateMeta({
3239+
sessionId: source.id,
3240+
goal: "No Machine State Polish",
3241+
});
3242+
mockState.codexResponseOverrides.set("thread/goal/set", () => {
3243+
throw new Error("goal seed unavailable");
3244+
});
3245+
3246+
const handoffStart = mockState.codexRequestPayloads.length;
3247+
const result = await service.handoffSession({
3248+
sourceSessionId: source.id,
3249+
targetModelId: "openai/gpt-5.5",
3250+
});
3251+
3252+
expect(result.session.provider).toBe("codex");
3253+
expect(mockState.sessions.get(result.session.id)?.goal).toBe("No Machine State Polish");
3254+
const handoffMethods = mockState.codexRequestPayloads
3255+
.slice(handoffStart)
3256+
.map((payload) => String(payload.method ?? ""));
3257+
expect(handoffMethods).toContain("turn/start");
3258+
expect(handoffMethods).toContain("thread/goal/set");
3259+
});
3260+
31843261
it("uses the selected Claude handoff permission instead of the source interaction mode", async () => {
31853262
const send = vi.fn().mockResolvedValue(undefined);
31863263
const setPermissionMode = vi.fn().mockResolvedValue(undefined);

apps/desktop/src/main/services/chat/agentChatService.ts

Lines changed: 36 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19970,27 +19970,51 @@ export function createAgentChatService(args: {
1997019970
const inheritedGoal = trimLine(sourceSession.goal)
1997119971
?? trimLine(sourceSession.summary)
1997219972
?? trimLine(sourceSession.title);
19973-
if (inheritedGoal) {
19973+
const applyInheritedGoal = (): void => {
19974+
if (!inheritedGoal) return;
1997419975
createdManaged.session.goal = inheritedGoal;
1997519976
sessionService.updateMeta({
1997619977
sessionId: created.id,
1997719978
goal: inheritedGoal,
1997819979
});
19980+
};
19981+
const deferInheritedGoalUntilHandoffDispatch =
19982+
handoffMode === "brief" && createdManaged.session.provider === "codex";
19983+
if (!deferInheritedGoalUntilHandoffDispatch) {
19984+
applyInheritedGoal();
1997919985
}
1998019986
persistChatState(createdManaged);
1998119987

1998219988
if (handoffMode === "brief") {
19983-
await sendMessage({
19984-
sessionId: created.id,
19985-
text: buildHandoffPrompt(brief),
19986-
displayText: "Chat handoff from previous session",
19987-
metadata: { kind: "handoff", hideFullPrompt: true },
19988-
reasoningEffort: targetReasoningEffort,
19989-
executionMode: createdManaged.session.executionMode ?? null,
19990-
interactionMode: createdManaged.session.interactionMode ?? null,
19991-
}, {
19992-
awaitDispatch: true,
19993-
});
19989+
try {
19990+
await sendMessage({
19991+
sessionId: created.id,
19992+
text: buildHandoffPrompt(brief),
19993+
displayText: "Chat handoff from previous session",
19994+
metadata: { kind: "handoff", hideFullPrompt: true },
19995+
reasoningEffort: targetReasoningEffort,
19996+
executionMode: createdManaged.session.executionMode ?? null,
19997+
interactionMode: createdManaged.session.interactionMode ?? null,
19998+
}, {
19999+
awaitDispatch: true,
20000+
});
20001+
} finally {
20002+
if (deferInheritedGoalUntilHandoffDispatch) {
20003+
applyInheritedGoal();
20004+
persistChatState(createdManaged);
20005+
if (createdManaged.runtime?.kind === "codex") {
20006+
try {
20007+
await seedCodexThreadGoalFromSessionGoal(createdManaged, createdManaged.runtime);
20008+
} catch (error) {
20009+
logger.warn("agent_chat.codex_goal_seed_after_handoff_failed", {
20010+
sessionId: createdManaged.session.id,
20011+
error: error instanceof Error ? error.message : String(error),
20012+
});
20013+
persistChatState(createdManaged);
20014+
}
20015+
}
20016+
}
20017+
}
1999420018
}
1999520019

1999620020
return {

apps/desktop/src/renderer/components/files/v2/EditorGroup.test.tsx

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ const registry = {
2727
} as unknown as MonacoModelRegistry;
2828

2929
const tabId = editorTabId("workspace-1", "src/file.ts");
30+
const otherLaneTabId = editorTabId("workspace-2", "src/other.ts");
3031

3132
const baseProps: EditorGroupProps = {
3233
group: {
@@ -108,6 +109,36 @@ describe("EditorGroup", () => {
108109
expect(screen.getByTestId("viewer-button")).toBeTruthy();
109110
});
110111

112+
it("marks the visible fallback tab active when lane scope hides the stored active tab", () => {
113+
render(
114+
<EditorGroup
115+
{...baseProps}
116+
tabScope="lane"
117+
group={{
118+
...baseProps.group,
119+
activeTabId: otherLaneTabId,
120+
tabs: [
121+
...baseProps.group.tabs,
122+
{
123+
id: otherLaneTabId,
124+
workspaceId: "workspace-2",
125+
laneId: "lane-2",
126+
path: "src/other.ts",
127+
title: "other.ts",
128+
viewerKind: "code",
129+
languageId: "typescript",
130+
preview: false,
131+
pinned: false,
132+
},
133+
],
134+
}}
135+
/>,
136+
);
137+
138+
expect(screen.getByRole("tab", { name: /file\.ts/i }).getAttribute("aria-selected")).toBe("true");
139+
expect(screen.queryByRole("tab", { name: /other\.ts/i })).toBeNull();
140+
});
141+
111142
it("does not steal Cmd+S from focused text inputs", () => {
112143
render(<EditorGroup {...baseProps} />);
113144
const input = screen.getByTestId("viewer-input");

apps/desktop/src/renderer/components/files/v2/EditorGroup.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@ export function EditorGroup(props: EditorGroupProps) {
178178
<TabButton
179179
key={tab.id}
180180
tab={tab}
181-
active={tab.id === group.activeTabId}
181+
active={tab.id === activeTab?.id}
182182
dirty={dirtyTabIds.has(tab.id)}
183183
laneAccent={props.tabScope === "all" ? laneAccentForTab(tab, props.lanes) : undefined}
184184
showLaneDivider={props.tabScope === "all" && isLaneGroupBoundary(displayTabs, index)}

0 commit comments

Comments
 (0)