Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
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
20 changes: 20 additions & 0 deletions packages/ai/scripts/generate-models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -427,6 +427,25 @@ function mergePrimeInferenceModels(
return Array.from(models.values());
}

function refreshPrimeInferenceAliasLimits(
snapshotModels: Model<"openai-completions">[],
catalogModels: Model<"openai-completions">[],
): Model<"openai-completions">[] {
const liveModels = new Map(catalogModels.map((model) => [model.id.toLowerCase(), model]));
return snapshotModels.map((model) => {
const canonicalId = PRIME_INFERENCE_OPENROUTER_ALIASES[model.id.toLowerCase()];
const canonical = canonicalId ? liveModels.get(canonicalId) : undefined;
if (!canonical) {
return model;
}
return {
...model,
contextWindow: canonical.contextWindow,
maxTokens: canonical.maxTokens,
};
});
}

function includesCatalogCapability(value: unknown, capabilities: readonly string[]): boolean {
if (!Array.isArray(value)) {
return false;
Expand Down Expand Up @@ -633,6 +652,7 @@ async function fetchPrimeInferenceModels(): Promise<Model<"openai-completions">[
(model) => liveIds.has(model.id.toLowerCase()) || model.id.toLowerCase().startsWith("internal/"),
);
}
snapshotModels = refreshPrimeInferenceAliasLimits(snapshotModels, catalogModels);
const models = mergePrimeInferenceModels(snapshotModels, catalogModels);
console.log(`Loaded ${models.length} Prime Inference models (${catalogModels.length} from the live catalog)`);
return models;
Expand Down
1 change: 1 addition & 0 deletions packages/coding-agent/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
- Changed automatic harness refinement to be enabled by default while keeping `autoRefine.enabled: false` as the opt-out.
- Fixed non-numeric `autoRefine.turnInterval` and `autoRefine.cooldownMs` settings falling back to defaults instead of silently enabling a noisy auto-refine loop.
- Fixed all session-resume entry points to share a searchable full-screen picker, stream results while loading, and support renaming ([ENG-4513](https://linear.app/primeintellect/issue/ENG-4513/resume-in-agents-view-is-broken)).
- Fixed self-updates losing restored daemon sessions to a socket cleanup race and leaving open session or agents-view windows disconnected.

## [0.2.7] - 2026-07-08

Expand Down
20 changes: 14 additions & 6 deletions packages/coding-agent/src/cli/daemon-launch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,28 +107,36 @@ export class StaleDaemonError extends Error {
}
}

async function waitForDaemonGone(socketPath: string, timeoutMs = 5000): Promise<boolean> {
async function waitForDaemonGone(socketPath: string, timeoutMs = 5000, requireSocketCleanup = false): Promise<boolean> {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
if (!(await canConnectToDaemon(socketPath, 250))) {
if (
!(await canConnectToDaemon(socketPath, 250)) &&
(!requireSocketCleanup || process.platform === "win32" || !existsSync(socketPath))
) {
Comment thread
cursor[bot] marked this conversation as resolved.
return true;
}
await delay(25);
}
return false;
// A daemon can exit without removing its Unix socket (for example, after a crash
// during shutdown). Once the cleanup grace has elapsed, a non-listening socket
// is safe for the replacement daemon's guarded startup path to reclaim.
return requireSocketCleanup && !(await canConnectToDaemon(socketPath, 250));
}

export async function shutdownDaemonAndWait(socketPath: string): Promise<boolean> {
export async function shutdownDaemonAndWait(socketPath: string, timeoutMs = 5000): Promise<boolean> {
const client = new DaemonClient(socketPath);
let shutdownAccepted = false;
try {
await client.connect(1000);
await client.request({ type: "shutdown" }).catch(() => undefined);
const response = await client.request({ type: "shutdown" });
shutdownAccepted = response.success;
} catch {
// A connect failure isn't treated as "gone"; waitForDaemonGone is the source of truth.
} finally {
client.close();
}
return waitForDaemonGone(socketPath);
return waitForDaemonGone(socketPath, timeoutMs, shutdownAccepted);
}

// activeSessions is undefined when the daemon is reachable but its sessions couldn't
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,12 @@ type DistributiveOmit<T, K extends keyof T> = T extends unknown ? Omit<T, K> : n
type DaemonCommandBody = DistributiveOmit<DaemonCommand, "id">;

export const DAEMON_REFINE_REQUEST_TIMEOUT_MS = 10 * 60 * 1000;
const UPDATE_RECONNECT_TIMEOUT_MS = 120000;
const UPDATE_RECONNECT_RETRY_MS = 100;

function delay(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}

export interface DaemonAgentConnectionOptions {
closeClientOnDispose?: boolean;
Expand All @@ -86,6 +92,13 @@ export class DaemonAgentConnection implements AgentConnection {
private lastEventSequence: number | undefined;
private latestSnapshot: AgentConnectionSnapshot | undefined;
private latestSnapshotIsFresh = false;
private attachedSessionId: string | undefined;
private attachedSessionFile: string | undefined;
private updateRestartPending = false;
private updateReconnectFailed = false;
private terminalCloseEmitted = false;
private updateReconnectPromise?: Promise<void>;
private disposed = false;

constructor(
private readonly client: DaemonClient,
Expand All @@ -96,6 +109,16 @@ export class DaemonAgentConnection implements AgentConnection {
void this.handleDaemonMessage(message);
});
this.unsubscribeDaemonClose = this.client.onClose((error) => {
if (this.disposed || this.terminalCloseEmitted) {
return;
}
const unannouncedUpdateCandidate = error.message === "Daemon socket closed" && !this.updateReconnectFailed;
if (this.updateRestartPending || unannouncedUpdateCandidate) {
this.updateRestartPending = true;
void this.reconnectAfterUpdate();
Comment thread
cursor[bot] marked this conversation as resolved.
return;
}
Comment thread
cursor[bot] marked this conversation as resolved.
this.terminalCloseEmitted = true;
void this.emit({ type: "closed", error: error.message });
});
}
Expand Down Expand Up @@ -132,6 +155,11 @@ export class DaemonAgentConnection implements AgentConnection {
},
});
this.activeSessionId = getAttachActiveSessionId(result);
const summary = "snapshot" in result ? result.snapshot.summary : result;
this.attachedSessionId = summary.sessionId;
this.attachedSessionFile = summary.sessionFile;
this.updateReconnectFailed = false;
this.terminalCloseEmitted = false;
this.lastEventSequence = maxEventSequence(this.lastEventSequence, getAttachLastEventSequence(result));
if ("snapshot" in result) {
this.latestSnapshot = mapDaemonAttachSnapshot(result);
Expand Down Expand Up @@ -683,6 +711,8 @@ export class DaemonAgentConnection implements AgentConnection {
}

async dispose(): Promise<void> {
this.disposed = true;
this.updateRestartPending = false;
Comment thread
cursor[bot] marked this conversation as resolved.
this.unsubscribeDaemonMessages();
this.unsubscribeDaemonClose();
await this.requestOk({ type: "detach", activeSessionId: this.activeSessionId }).catch(() => undefined);
Expand Down Expand Up @@ -732,6 +762,8 @@ export class DaemonAgentConnection implements AgentConnection {
return;
}
if (message.type === "session_replaced") {
this.attachedSessionId = message.state.sessionId;
this.attachedSessionFile = message.state.sessionFile;
const latestSnapshot: AgentConnectionSnapshot = {
state: message.state,
messages: message.messages,
Expand All @@ -756,10 +788,98 @@ export class DaemonAgentConnection implements AgentConnection {
return;
}
if (message.type === "session_closed") {
if (message.reason === "update") {
Comment thread
cursor[bot] marked this conversation as resolved.
Comment thread
macroscopeapp[bot] marked this conversation as resolved.
this.updateRestartPending = true;
void this.reconnectAfterUpdate();
return;
}
this.terminalCloseEmitted = true;
await this.emit({ type: "closed", error: message.reason });
}
}

private reconnectAfterUpdate(): Promise<void> {
if (this.updateReconnectPromise) {
return this.updateReconnectPromise;
}
const reconnectPromise = this.restoreConnectionAfterUpdate()
.catch(async (error: unknown) => {
this.updateRestartPending = false;
this.updateReconnectFailed = true;
if (!this.disposed) {
this.terminalCloseEmitted = true;
await this.emit({
type: "closed",
error: `Failed to reconnect after update: ${error instanceof Error ? error.message : String(error)}`,
});
}
Comment thread
cursor[bot] marked this conversation as resolved.
})
.finally(() => {
if (this.updateReconnectPromise === reconnectPromise) {
this.updateReconnectPromise = undefined;
}
});
this.updateReconnectPromise = reconnectPromise;
return reconnectPromise;
Comment thread
cursor[bot] marked this conversation as resolved.
}

private async restoreConnectionAfterUpdate(): Promise<void> {
const sessionId = this.attachedSessionId;
const sessionFile = this.attachedSessionFile;
Comment thread
macroscopeapp[bot] marked this conversation as resolved.
if (!sessionId && !sessionFile) {
throw new Error("the previous session identity is unavailable");
}
const deadline = Date.now() + UPDATE_RECONNECT_TIMEOUT_MS;
let lastError: unknown;
while (!this.disposed && Date.now() < deadline) {
try {
await this.client.reconnect(1000);
if (this.disposed) {
return;
}
const response = await this.client.request({ type: "list" }, 30000);
if (this.disposed) {
return;
}
if (!response.success) {
throw deserializeDaemonError(response);
}
const sessions = readSessionSummaries(response.data);
const restored = sessions.find(
(summary) =>
summary.activeSessionId !== undefined &&
((sessionFile !== undefined && summary.sessionFile === sessionFile) ||
(sessionId !== undefined && summary.sessionId === sessionId)),
);
if (restored?.activeSessionId) {
if (this.disposed) {
return;
}
this.activeSessionId = restored.activeSessionId;
this.lastEventSequence = undefined;
await this.attach();
if (this.disposed) {
return;
}
const snapshot = await this.getInitialSnapshot();
if (this.disposed) {
return;
}
this.updateRestartPending = false;
await this.emit({ type: "session_replaced", state: snapshot.state, messages: snapshot.messages });
return;
}
} catch (error) {
lastError = error;
}
await delay(UPDATE_RECONNECT_RETRY_MS);
}
if (this.disposed) {
return;
}
throw lastError ?? new Error("the restored session did not become available");
}

private isMessageForActiveSession(message: DaemonOutbound): boolean {
if (!("activeSessionId" in message)) {
return false;
Expand Down Expand Up @@ -788,6 +908,13 @@ export class DaemonAgentConnection implements AgentConnection {
}
}

function readSessionSummaries(value: unknown): SessionSummary[] {
if (!value || typeof value !== "object" || !Array.isArray((value as { sessions?: unknown }).sessions)) {
throw new Error("Daemon returned an invalid session list response");
}
return (value as { sessions: SessionSummary[] }).sessions;
}

function getAttachActiveSessionId(result: SessionSummary | DaemonAttachResult): string {
if ("snapshot" in result) {
return result.activeSessionId;
Expand Down
Loading
Loading