Skip to content

Commit 2da5bd4

Browse files
committed
fix: address mobile connection review feedback
1 parent b690f6a commit 2da5bd4

19 files changed

Lines changed: 292 additions & 107 deletions

apps/ade-cli/src/headlessLinearServices.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -396,7 +396,7 @@ function createHeadlessAgentChatService(projectRoot: string): HeadlessLinearServ
396396
const identitySessionIds = new Map<string, string>();
397397
const transcripts = new Map<string, HeadlessTranscriptEntry[]>();
398398

399-
const HEADLESS_MODEL_ID = "openai/gpt-5.5-codex";
399+
const HEADLESS_MODEL_ID = "openai/gpt-5.5";
400400

401401
const clipText = (value: string, maxChars: number): string => {
402402
const trimmed = value.trim();

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

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1277,6 +1277,17 @@ function validateReasoningEffort(provider: "codex" | "claude", effort: string |
12771277
return known.has(aliased) ? aliased : fallback;
12781278
}
12791279

1280+
function resolveCodexReasoningEffortForRuntime(
1281+
primary: string | null | undefined,
1282+
fallback?: string | null,
1283+
): string {
1284+
return (
1285+
validateReasoningEffort("codex", normalizeReasoningEffort(primary))
1286+
?? validateReasoningEffort("codex", normalizeReasoningEffort(fallback))
1287+
?? DEFAULT_REASONING_EFFORT
1288+
);
1289+
}
1290+
12801291
function buildClaudeV2ExecutableArgs(args: {
12811292
supportsReasoning: boolean;
12821293
effort?: string | null;
@@ -2290,11 +2301,7 @@ function applyCodexEffectiveThreadState(
22902301
),
22912302
);
22922303
if (reasoningEffort) {
2293-
const requestedReasoningEffort = validateReasoningEffort(
2294-
"codex",
2295-
normalizeReasoningEffort(managed.session.reasoningEffort),
2296-
);
2297-
managed.session.reasoningEffort = requestedReasoningEffort ?? reasoningEffort;
2304+
managed.session.reasoningEffort = reasoningEffort;
22982305
}
22992306

23002307
managed.session.permissionMode = syncLegacyPermissionMode(managed.session) ?? managed.session.permissionMode;
@@ -13741,11 +13748,16 @@ export function createAgentChatService(args: {
1374113748

1374213749
if (threadIdToResume) {
1374313750
try {
13751+
const resumeReasoningEffort = resolveCodexReasoningEffortForRuntime(
13752+
managed.session.reasoningEffort,
13753+
readPersistedState(sessionId)?.reasoningEffort,
13754+
);
13755+
managed.session.reasoningEffort = resumeReasoningEffort;
1374413756
const resumeResponse = await runtime.request<CodexThreadLifecycleResponse>("thread/resume", {
1374513757
threadId: threadIdToResume,
1374613758
model: managed.session.model,
1374713759
cwd: managed.laneWorktreePath,
13748-
reasoningEffort: managed.session.reasoningEffort ?? readPersistedState(sessionId)?.reasoningEffort ?? DEFAULT_REASONING_EFFORT,
13760+
reasoningEffort: resumeReasoningEffort,
1374913761
...codexPolicyArgs(codexPolicy),
1375013762
persistExtendedHistory: true
1375113763
});
@@ -14527,9 +14539,10 @@ export function createAgentChatService(args: {
1452714539

1452814540
if (managed.session.provider === "codex") {
1452914541
const runtime = await ensureCodexSessionRuntime(managed);
14530-
if (!managed.session.reasoningEffort) {
14531-
managed.session.reasoningEffort = persisted?.reasoningEffort ?? DEFAULT_REASONING_EFFORT;
14532-
}
14542+
managed.session.reasoningEffort = resolveCodexReasoningEffortForRuntime(
14543+
managed.session.reasoningEffort,
14544+
persisted?.reasoningEffort,
14545+
);
1453314546
const threadId = persisted?.threadId ?? managed.session.threadId;
1453414547
if (threadId) {
1453514548
const { codexPolicy } = resolveCodexThreadParams(managed);
@@ -14538,7 +14551,7 @@ export function createAgentChatService(args: {
1453814551
threadId,
1453914552
model: managed.session.model,
1454014553
cwd: managed.laneWorktreePath,
14541-
reasoningEffort: managed.session.reasoningEffort ?? DEFAULT_REASONING_EFFORT,
14554+
reasoningEffort: managed.session.reasoningEffort,
1454214555
...codexPolicyArgs(codexPolicy),
1454314556
persistExtendedHistory: true
1454414557
});

apps/desktop/src/main/services/sync/syncHostService.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1073,7 +1073,7 @@ describe.skipIf(!isCrsqliteAvailable())("syncHostService", () => {
10731073

10741074
const rebroadcast = await clientB.queue.next("changeset_batch");
10751075
const payload = rebroadcast.payload as {
1076-
batchId?: string;
1076+
batchId: string;
10771077
fromDbVersion: number;
10781078
toDbVersion: number;
10791079
changes: unknown[];
@@ -1084,9 +1084,9 @@ describe.skipIf(!isCrsqliteAvailable())("syncHostService", () => {
10841084
expect(host.getPeerStates().find((peer) => peer.deviceId === "peer-b")?.syncLag).toBeGreaterThan(0);
10851085
clientB.ws.send(encodeSyncEnvelope({
10861086
type: "changeset_ack",
1087-
requestId: payload.batchId ?? "rebroadcast-ack",
1087+
requestId: payload.batchId,
10881088
payload: {
1089-
batchId: payload.batchId ?? null,
1089+
batchId: payload.batchId,
10901090
fromDbVersion: payload.fromDbVersion,
10911091
toDbVersion: payload.toDbVersion,
10921092
appliedDbVersion: dbB.sync.getDbVersion(),

apps/desktop/src/main/services/sync/syncHostService.ts

Lines changed: 48 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ const PEER_BACKPRESSURE_BYTES = 4 * 1024 * 1024;
106106
const MOBILE_COMMAND_RESULT_CACHE_TTL_MS = 30 * 60 * 1000;
107107
const MOBILE_COMMAND_RESULT_CACHE_MAX_ENTRIES = 512;
108108
const CHANGESET_ACK_TIMEOUT_MS = 10_000;
109+
const MAX_CHANGESET_ACK_RETRIES = 6;
109110
const LANE_PRESENCE_TTL_MS = 60_000;
110111
const SYNC_MDNS_SERVICE_TYPE = "ade-sync";
111112
export const SYNC_TAILNET_DISCOVERY_SERVICE_NAME = "svc:ade-sync";
@@ -185,8 +186,6 @@ type PersistedMobileCommand = {
185186
completedAtMs: number;
186187
};
187188

188-
const mobileCommandResultCache = new Map<string, CachedMobileCommand>();
189-
190189
function stableJsonValue(value: unknown): unknown {
191190
if (value == null) return value;
192191
if (Array.isArray(value)) return value.map(stableJsonValue);
@@ -214,31 +213,6 @@ function addMobileCommandWaiter(record: CachedMobileCommand, peer: PeerState, re
214213
record.waiters.push({ peer, requestId });
215214
}
216215

217-
function pruneMobileCommandResultCache(nowMs = Date.now()): void {
218-
for (const [key, record] of mobileCommandResultCache) {
219-
const referenceMs = record.completedAtMs ?? record.acceptedAtMs;
220-
if (nowMs - referenceMs > MOBILE_COMMAND_RESULT_CACHE_TTL_MS) {
221-
mobileCommandResultCache.delete(key);
222-
}
223-
}
224-
if (mobileCommandResultCache.size <= MOBILE_COMMAND_RESULT_CACHE_MAX_ENTRIES) return;
225-
226-
const completed = [...mobileCommandResultCache.entries()]
227-
.filter(([, record]) => record.completedAtMs != null)
228-
.sort(([, left], [, right]) => (left.completedAtMs ?? left.acceptedAtMs) - (right.completedAtMs ?? right.acceptedAtMs));
229-
for (const [key] of completed) {
230-
if (mobileCommandResultCache.size <= MOBILE_COMMAND_RESULT_CACHE_MAX_ENTRIES) break;
231-
mobileCommandResultCache.delete(key);
232-
}
233-
const inFlight = [...mobileCommandResultCache.entries()]
234-
.filter(([, record]) => record.completedAtMs == null)
235-
.sort(([, left], [, right]) => left.acceptedAtMs - right.acceptedAtMs);
236-
for (const [key] of inFlight) {
237-
if (mobileCommandResultCache.size <= MOBILE_COMMAND_RESULT_CACHE_MAX_ENTRIES) break;
238-
mobileCommandResultCache.delete(key);
239-
}
240-
}
241-
242216
type SyncHostServiceArgs = {
243217
db: AdeDb;
244218
logger: Logger;
@@ -523,11 +497,30 @@ export function createSyncHostService(args: SyncHostServiceArgs) {
523497
};
524498

525499
const peers = new Set<PeerState>();
500+
const mobileCommandResultCache = new Map<string, CachedMobileCommand>();
526501
let commandReplayCount = 0;
527502
let commandConflictCount = 0;
528503
let lastCommandResultLatencyMs: number | null = null;
529504
let lastChangesetAckLatencyMs: number | null = null;
530505

506+
const pruneMobileCommandResultCache = (nowMs = Date.now()): void => {
507+
for (const [key, record] of mobileCommandResultCache) {
508+
if (record.completedAtMs == null) continue;
509+
if (nowMs - record.completedAtMs > MOBILE_COMMAND_RESULT_CACHE_TTL_MS) {
510+
mobileCommandResultCache.delete(key);
511+
}
512+
}
513+
if (mobileCommandResultCache.size <= MOBILE_COMMAND_RESULT_CACHE_MAX_ENTRIES) return;
514+
515+
const completed = [...mobileCommandResultCache.entries()]
516+
.filter(([, record]) => record.completedAtMs != null)
517+
.sort(([, left], [, right]) => (left.completedAtMs ?? left.acceptedAtMs) - (right.completedAtMs ?? right.acceptedAtMs));
518+
for (const [key] of completed) {
519+
if (mobileCommandResultCache.size <= MOBILE_COMMAND_RESULT_CACHE_MAX_ENTRIES) break;
520+
mobileCommandResultCache.delete(key);
521+
}
522+
};
523+
531524
const readPersistedCommandLedger = (): PersistedMobileCommand[] => {
532525
try {
533526
if (!fs.existsSync(commandLedgerPath)) return [];
@@ -1698,6 +1691,21 @@ export function createSyncHostService(args: SyncHostServiceArgs) {
16981691
if (peer.pendingChangesetBatch) {
16991692
if (nowMs - peer.pendingChangesetBatch.sentAtMs >= CHANGESET_ACK_TIMEOUT_MS) {
17001693
const pending = peer.pendingChangesetBatch;
1694+
if (pending.retryCount >= MAX_CHANGESET_ACK_RETRIES) {
1695+
args.logger.warn("sync_host.changeset_ack_timeout", {
1696+
peerDeviceId: peer.metadata.deviceId,
1697+
batchId: pending.batchId,
1698+
fromDbVersion: pending.fromDbVersion,
1699+
toDbVersion: pending.toDbVersion,
1700+
retryCount: pending.retryCount,
1701+
});
1702+
try {
1703+
peer.ws.close(4000, "Changeset acknowledgement timed out");
1704+
} catch {
1705+
// ignore close failures
1706+
}
1707+
continue;
1708+
}
17011709
const resent = resendPendingChangesetBatch(peer);
17021710
args.logger.debug("sync_host.changeset_ack_retry", {
17031711
peerDeviceId: peer.metadata.deviceId,
@@ -1736,19 +1744,28 @@ export function createSyncHostService(args: SyncHostServiceArgs) {
17361744
args.logger.debug("sync_host.changeset_ack_ignored", {
17371745
peerDeviceId: peer.metadata?.deviceId ?? null,
17381746
expectedBatchId: pending.batchId,
1739-
receivedBatchId: payload.batchId ?? null,
1747+
receivedBatchId: payload.batchId,
17401748
});
17411749
return;
17421750
}
17431751
if (!payload.ok) {
1752+
pending.retryCount += 1;
17441753
pending.sentAtMs = Date.now();
17451754
args.logger.warn("sync_host.changeset_ack_failed", {
17461755
peerDeviceId: peer.metadata?.deviceId ?? null,
17471756
batchId: pending.batchId,
17481757
fromDbVersion: pending.fromDbVersion,
17491758
toDbVersion: pending.toDbVersion,
1759+
retryCount: pending.retryCount,
17501760
error: payload.error?.message ?? "Changeset apply failed.",
17511761
});
1762+
if (pending.retryCount >= MAX_CHANGESET_ACK_RETRIES) {
1763+
try {
1764+
peer.ws.close(4000, "Changeset apply failed repeatedly");
1765+
} catch {
1766+
// ignore close failures
1767+
}
1768+
}
17521769
return;
17531770
}
17541771
if (payload.toDbVersion < pending.toDbVersion) return;
@@ -2306,6 +2323,7 @@ export function createSyncHostService(args: SyncHostServiceArgs) {
23062323
}
23072324
case "changeset_batch": {
23082325
const payload = (envelope.payload ?? {}) as SyncChangesetBatchPayload;
2326+
const batchId = payload.batchId || envelope.requestId || "";
23092327
const changes = Array.isArray(payload.changes) ? payload.changes as CrsqlChangeRow[] : [];
23102328
try {
23112329
let appliedCount = 0;
@@ -2318,7 +2336,7 @@ export function createSyncHostService(args: SyncHostServiceArgs) {
23182336
broadcastBrainStatus();
23192337
}
23202338
sendRequired(peer, "changeset_ack", {
2321-
batchId: payload.batchId ?? null,
2339+
batchId,
23222340
fromDbVersion: Number(payload.fromDbVersion ?? 0),
23232341
toDbVersion: Number(payload.toDbVersion ?? 0),
23242342
appliedDbVersion: args.db.sync.getDbVersion(),
@@ -2327,7 +2345,7 @@ export function createSyncHostService(args: SyncHostServiceArgs) {
23272345
} satisfies SyncChangesetAckPayload, envelope.requestId);
23282346
} catch (error) {
23292347
sendRequired(peer, "changeset_ack", {
2330-
batchId: payload.batchId ?? null,
2348+
batchId,
23312349
fromDbVersion: Number(payload.fromDbVersion ?? 0),
23322350
toDbVersion: Number(payload.toDbVersion ?? 0),
23332351
appliedDbVersion: args.db.sync.getDbVersion(),

apps/desktop/src/main/services/sync/syncPeerService.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ export function createSyncPeerService(args: SyncPeerServiceArgs) {
139139
) => {
140140
if (!ws || ws.readyState !== WebSocket.OPEN) return;
141141
const payload: SyncChangesetAckPayload = {
142-
batchId: batch.batchId ?? null,
142+
batchId: batch.batchId,
143143
fromDbVersion: Number(batch.fromDbVersion ?? 0),
144144
toDbVersion: Number(batch.toDbVersion ?? 0),
145145
appliedDbVersion,
@@ -152,7 +152,7 @@ export function createSyncPeerService(args: SyncPeerServiceArgs) {
152152
ws.send(
153153
encodeSyncEnvelope({
154154
type: "changeset_ack",
155-
requestId: batch.batchId ?? null,
155+
requestId: batch.batchId,
156156
payload,
157157
compressionThresholdBytes: DEFAULT_SYNC_COMPRESSION_THRESHOLD_BYTES,
158158
}),
@@ -250,6 +250,7 @@ export function createSyncPeerService(args: SyncPeerServiceArgs) {
250250
}
251251
}
252252
ws = null;
253+
pendingOutboundChangeset = null;
253254
latestBrainStatus = null;
254255
status.state = state;
255256
status.connectedAt = null;
@@ -329,8 +330,18 @@ export function createSyncPeerService(args: SyncPeerServiceArgs) {
329330
break;
330331
}
331332
if (payload.toDbVersion < pendingOutboundChangeset.payload.toDbVersion) break;
333+
const acknowledgedRemoteVersion = Math.max(
334+
latestRemoteDbVersion,
335+
pendingOutboundChangeset.payload.toDbVersion,
336+
Math.floor(payload.toDbVersion ?? 0),
337+
);
338+
latestRemoteDbVersion = acknowledgedRemoteVersion;
339+
if (connectionDraft) {
340+
connectionDraft.lastRemoteDbVersion = acknowledgedRemoteVersion;
341+
}
332342
outboundLocalDbVersion = Math.max(outboundLocalDbVersion, pendingOutboundChangeset.payload.toDbVersion);
333343
pendingOutboundChangeset = null;
344+
emitStatus();
334345
break;
335346
}
336347
case "brain_status": {

apps/desktop/src/main/services/usage/usageTrackingService.test.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,13 @@ describe("resolveTokenPrice", () => {
319319
it("returns mini pricing for mini OpenAI models", () => {
320320
const price = resolveTokenPrice("gpt-5.4-mini");
321321
expect(price.input).toBe(0.3 / 1_000_000);
322+
expect(price.output).toBe(1.2 / 1_000_000);
323+
});
324+
325+
it("does not treat Gemini as a mini OpenAI model", () => {
326+
const price = resolveTokenPrice("gemini-2.5-pro");
327+
expect(price.input).toBe(3 / 1_000_000);
328+
expect(price.output).toBe(15 / 1_000_000);
322329
});
323330

324331
it("returns default pricing for unknown models", () => {

apps/desktop/src/main/services/usage/usageTrackingService.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -512,10 +512,11 @@ interface TokenEntry {
512512

513513
function resolveTokenPrice(model: string): { input: number; output: number } {
514514
const lower = (model ?? "").toLowerCase();
515+
const isMiniModel = /(?:^|[\/._-])mini(?:$|[\/._-])/.test(lower);
515516
if (lower.includes("opus")) return TOKEN_PRICES["claude-opus"]!;
516517
if (lower.includes("sonnet")) return TOKEN_PRICES["claude-sonnet"]!;
517518
if (lower.includes("haiku")) return TOKEN_PRICES["claude-haiku"]!;
518-
if (lower.includes("mini")) return TOKEN_PRICES["openai-mini"]!;
519+
if (isMiniModel) return TOKEN_PRICES["openai-mini"]!;
519520
if (lower.includes("codex") || lower.includes("gpt") || lower.includes("o3") || lower.includes("o4"))
520521
return TOKEN_PRICES["codex"]!;
521522
return TOKEN_PRICES["default"]!;

apps/desktop/src/renderer/components/chat/AgentChatPane.submit.test.tsx

Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import React from "react";
44
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
5-
import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react";
5+
import { act, cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react";
66
import { MemoryRouter, Route, Routes, useLocation } from "react-router-dom";
77
import type {
88
AgentChatEventEnvelope,
@@ -1202,28 +1202,35 @@ describe("AgentChatPane submit recovery", () => {
12021202
});
12031203

12041204
it("does not hydrate hidden inactive chat tiles", async () => {
1205+
vi.useFakeTimers();
12051206
const session = buildSession("hidden-inactive-chat", {
12061207
title: "Hidden inactive chat",
12071208
});
12081209
installAdeMocks({ sessions: [session] });
12091210
const readTranscriptTail = vi.fn().mockResolvedValue("");
12101211
window.ade.sessions.readTranscriptTail = readTranscriptTail as any;
12111212

1212-
render(
1213-
<MemoryRouter>
1214-
<AgentChatPane
1215-
laneId={session.laneId}
1216-
lockSessionId={session.sessionId}
1217-
hideSessionTabs
1218-
initialSessionSummary={session}
1219-
layoutVariant="grid-tile"
1220-
isTileActive={false}
1221-
/>
1222-
</MemoryRouter>,
1223-
);
1224-
1225-
await new Promise((resolve) => window.setTimeout(resolve, 550));
1226-
expect(readTranscriptTail).not.toHaveBeenCalled();
1213+
try {
1214+
render(
1215+
<MemoryRouter>
1216+
<AgentChatPane
1217+
laneId={session.laneId}
1218+
lockSessionId={session.sessionId}
1219+
hideSessionTabs
1220+
initialSessionSummary={session}
1221+
layoutVariant="grid-tile"
1222+
isTileActive={false}
1223+
/>
1224+
</MemoryRouter>,
1225+
);
1226+
1227+
await act(async () => {
1228+
vi.advanceTimersByTime(550);
1229+
});
1230+
expect(readTranscriptTail).not.toHaveBeenCalled();
1231+
} finally {
1232+
vi.useRealTimers();
1233+
}
12271234
});
12281235

12291236
it("shows 'New chat' in the header when no session is selected", async () => {

0 commit comments

Comments
 (0)