Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
14 changes: 13 additions & 1 deletion apps/desktop/src/main/engine/local-models/execution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,18 @@ function refuseProjectConfiguration(cwd: string): void {
}
}

/**
* Codex words every HTTP 500 as a cloud service under load ("We're currently experiencing high demand"). From a
* loopback server that is not what happened: it is the person's own server failing, often out of memory. Codex's
* text stays in the detail; the turn's error says what is known (#380 AC4).
*/
function truthful(result: TurnResult): TurnResult {
if (result.status !== "failed") return result;
const detail = result.detail as { turnError?: { codexErrorInfo?: unknown } } | undefined;
if (detail?.turnError?.codexErrorInfo !== "internalServerError") return result;
return { ...result, error: "The local server returned an internal error (HTTP 500). Check its log: it may have run out of memory or crashed while loading the model. Modex didn't retry the turn or switch models." };
}

export class LocalExecution {
/** The child owning each thread's native conversation, with the key it was started for. */
private readonly children = new Map<string, { key: string; backend: CodexBackend; home: string; provenance?: LocalConfigurationProvenance }>();
Expand Down Expand Up @@ -146,7 +158,7 @@ export class LocalExecution {
this.children.set(thread.id, owned);
}
// Effort, fast mode and service tiers are cloud-model settings with no local qualification: none is sent.
return await owned.backend.runTurn(text, { ...opts, effort: undefined, fast: false }, sink, signal);
return truthful(await owned.backend.runTurn(text, { ...opts, effort: undefined, fast: false }, sink, signal));
} catch (error) {
return signal.aborted ? { status: "interrupted" } : { status: "failed", error: (error as Error).message };
} finally { release?.(); }
Expand Down
4 changes: 4 additions & 0 deletions apps/desktop/src/main/engine/local-models/run-policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ export interface LocalContextEvidence {

/** Keys a layer other than Modex's session flags may not set: the inference destination, the model, its context and the tools reaching it. */
const GOVERNED = ["model", "model_provider", "model_providers", "openai_base_url", "oss_provider", "model_catalog_json", "model_context_window", "model_auto_compact_token_limit",
// Native compaction: what it sends the provider and when it runs (#380 AC1; key names from codex-cli 0.166.0).
"compact_prompt", "model_auto_compact_token_limit_scope", "model_post_turn_compact_threshold_percent", "model_async_compact_threshold_percent",
"model_reasoning_summary", "model_reasoning_effort", "service_tier", "web_search", "features", "mcp_servers", "shell_environment_policy", "plugins", "hooks", "marketplaces",
"profile", "profiles", "projects", "notify", "otel", "tools", "developer_instructions", "model_instructions_file", "agents", "cloud"];
const MANAGED = new Set(["mdm", "system", "enterpriseManaged", "legacyManagedConfigTomlFromFile", "legacyManagedConfigTomlFromMdm"]);
Expand Down Expand Up @@ -131,6 +133,8 @@ export function admitLocalConfiguration(read: ConfigRead, requirements: Requirem
want(`model_providers.${expected.provider}.requires_openai_auth`, provider.requires_openai_auth, false);
want(`model_providers.${expected.provider}.env_key`, provider.env_key ?? null, expected.tokenEnv);
want(`model_providers.${expected.provider}.request_max_retries`, provider.request_max_retries, 0);
// A dropped stream is otherwise re-sent whole: the turn reports the drop instead (#380 AC4).
want(`model_providers.${expected.provider}.stream_max_retries`, provider.stream_max_retries, 0);
for (const name of ["experimental_bearer_token", "auth", "http_headers", "env_http_headers", "query_params"]) want(`model_providers.${expected.provider}.${name}`, provider[name] ?? null, null);
want("model_context_window", config.model_context_window, expected.contextTokens);
want("web_search", config.web_search, "disabled");
Expand Down
5 changes: 3 additions & 2 deletions apps/desktop/src/main/engine/runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1164,8 +1164,9 @@ export class ThreadRunner {
// External edits remain fenced until the renderer adopts the canonical state.
if (this.restartHold?.kind !== "owner-removal" || (!opts.fromRouter && !opts.automaticTitle)) this.assertNotRestarting();
// Resume handles and account bindings are main-owned, even if an IPC caller sends extra keys.
// A local target is chosen on this Mac: a paired desktop's patch never carries one.
const localKeys = opts.by === "paired-desktop" ? [] : ["localTarget", "localContextTokens"];
// A local target is chosen, changed and cleared on this Mac's own window only (#380 AC5): a paired desktop's,
// the phone's or an unnamed caller's patch never carries one.
const localKeys = opts.by === "desktop" ? ["localTarget", "localContextTokens"] : [];
patch = Object.fromEntries(Object.entries(patch).filter(([key]) =>
["mode", "model", "title", "backend", "plan", "effort", "auto", "approvals", ...localKeys].includes(key))) as ThreadPatch;
if (patch.approvals !== undefined && !APPROVAL_POLICIES.includes(patch.approvals)) delete patch.approvals;
Expand Down
Loading
Loading