You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maka.apache.org by as...@apache.org on 2026/08/20 00:49:26 UTC

(maka) branch main updated: refactor(desktop): remove superseded local authority paths (#3252)

This is an automated email from the ASF dual-hosted git repository.

Astro-Han pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/maka.git


The following commit(s) were added to refs/heads/main by this push:
     new 2bb59062e refactor(desktop): remove superseded local authority paths (#3252)
2bb59062e is described below

commit 2bb59062e66c8154fcbf2bd52ca523e6c098293e
Author: jakevin <ja...@gmail.com>
AuthorDate: Thu Aug 20 08:49:22 2026 +0800

    refactor(desktop): remove superseded local authority paths (#3252)
    
    Delete obsolete Desktop chat admission, Git Review mutation, store-backed config export, and direct ArtifactStore attachment paths now that Runtime Host owns those boundaries. Preserve the live compatibility projection and move config/attachment security coverage to the authoritative paths.
    
    Generated-by: Codex
---
 .../__tests__/attachment-ingest-resolve.test.ts    |  30 +-
 .../src/main/__tests__/attachment-ingest.test.ts   |  68 ---
 .../src/main/__tests__/chat-readiness.test.ts      | 550 ---------------------
 .../main/__tests__/config-transfer-service.test.ts |  51 +-
 .../src/main/__tests__/git-review-main.test.ts     |  92 +---
 .../__tests__/runtime-host-config-ipc-main.test.ts | 115 +++++
 apps/desktop/src/main/attachment-ingest.ts         | 113 +----
 apps/desktop/src/main/chat-readiness.ts            | 291 -----------
 apps/desktop/src/main/config-transfer-service.ts   |  64 +--
 apps/desktop/src/main/git-review-main.ts           |  76 +--
 apps/desktop/src/main/main-window.ts               |   4 +-
 .../src/main/runtime-host-config-ipc-main.ts       |   6 +-
 .../runtime-host-session-execution-ipc-main.ts     |   3 -
 .../renderer/app-shell-session-start-actions.ts    |   2 +-
 apps/desktop/src/renderer/session-health-notice.ts |  16 +-
 apps/desktop/src/renderer/task-readiness-notice.ts |   6 +
 packages/core/src/connection-readiness.ts          |  14 +-
 packages/core/src/git-review.ts                    |   9 -
 packages/core/src/session-send-projection.ts       |  59 +--
 packages/core/src/session.ts                       |   5 +-
 20 files changed, 210 insertions(+), 1364 deletions(-)

diff --git a/apps/desktop/src/main/__tests__/attachment-ingest-resolve.test.ts b/apps/desktop/src/main/__tests__/attachment-ingest-resolve.test.ts
index 28a02466c..6ce6d8062 100644
--- a/apps/desktop/src/main/__tests__/attachment-ingest-resolve.test.ts
+++ b/apps/desktop/src/main/__tests__/attachment-ingest-resolve.test.ts
@@ -1,6 +1,9 @@
 import assert from 'node:assert/strict';
+import { mkdtemp, rm, writeFile } from 'node:fs/promises';
+import { tmpdir } from 'node:os';
+import { join } from 'node:path';
 import { describe, test } from 'node:test';
-import { resolveIngestItems } from '../attachment-ingest.js';
+import { resolveAttachmentRefs, resolveIngestItems } from '../attachment-ingest.js';
 import { createAttachmentApprovalRegistry } from '../attachment-approval.js';
 
 describe('resolveIngestItems (pre-read validation)', () => {
@@ -211,3 +214,28 @@ describe('resolveIngestItems (pre-read validation)', () => {
     );
   });
 });
+
+describe('resolveAttachmentRefs', () => {
+  test('rejects a path grown beyond the cap before creating a Host artifact', async () => {
+    const dir = await mkdtemp(join(tmpdir(), 'att-cap-'));
+    const path = join(dir, 'grew.bin');
+    await writeFile(path, Buffer.alloc(11));
+    let snapshots = 0;
+    try {
+      await assert.rejects(
+        resolveAttachmentRefs({
+          files: [{ path, mimeType: 'application/octet-stream', size: 5 }],
+          maxBytes: 10,
+          snapshot: async () => {
+            snapshots += 1;
+            throw new Error('snapshot must not run');
+          },
+        }),
+        /超出大小限制/,
+      );
+      assert.equal(snapshots, 0);
+    } finally {
+      await rm(dir, { recursive: true, force: true });
+    }
+  });
+});
diff --git a/apps/desktop/src/main/__tests__/attachment-ingest.test.ts b/apps/desktop/src/main/__tests__/attachment-ingest.test.ts
deleted file mode 100644
index bb6400ec9..000000000
--- a/apps/desktop/src/main/__tests__/attachment-ingest.test.ts
+++ /dev/null
@@ -1,68 +0,0 @@
-import assert from 'node:assert/strict';
-import { mkdtemp, rm, symlink, writeFile } from 'node:fs/promises';
-import { tmpdir } from 'node:os';
-import { join } from 'node:path';
-import { describe, test } from 'node:test';
-import { createSqliteArtifactStore } from '@maka/storage';
-import { ingestAttachments } from '../attachment-ingest.js';
-
-describe('ingestAttachments', () => {
-  test('workspace symlink escaping cwd is snapshotted, not exposed as a live workspace_file', async () => {
-    const dir = await mkdtemp(join(tmpdir(), 'att-sym-'));
-    const outsideDir = await mkdtemp(join(tmpdir(), 'att-sym-out-'));
-    try {
-      const store = createSqliteArtifactStore(dir);
-      const outsideFile = join(outsideDir, 'secret.md');
-      await writeFile(outsideFile, 'secret');
-      // symlink inside the workspace that resolves to a file outside it
-      const linkPath = join(dir, 'escape.md');
-      await symlink(outsideFile, linkPath);
-      const refs = await ingestAttachments({
-        files: [{ path: linkPath, mimeType: 'text/markdown', size: 6 }],
-        cwd: dir,
-        sessionId: 's1',
-        artifactStore: store,
-      });
-      assert.equal(refs.length, 1);
-      assert.equal(
-        refs[0].ref.kind,
-        'session_file',
-        'a symlink that escapes the workspace must be snapshotted, not read live via workspace_file',
-      );
-    } finally {
-      await rm(dir, { recursive: true, force: true });
-      await rm(outsideDir, { recursive: true, force: true });
-    }
-  });
-
-  test('path attachment grown between stat and read is rejected, no artifact created', async () => {
-    const dir = await mkdtemp(join(tmpdir(), 'att-cap-'));
-    const externalPath = join(tmpdir(), `grew-${Date.now()}.bin`);
-    try {
-      const store = createSqliteArtifactStore(dir);
-      // real file is 11 bytes; files[].size lies small (TOCTOU: stat said 5)
-      await writeFile(externalPath, Buffer.alloc(11));
-      let storeCreates = 0;
-      const realCreate = store.create.bind(store);
-      store.create = async (input) => {
-        storeCreates += 1;
-        return realCreate(input);
-      };
-      await assert.rejects(
-        ingestAttachments({
-          files: [{ path: externalPath, mimeType: 'application/octet-stream', size: 5 }],
-          cwd: dir,
-          sessionId: 's1',
-          artifactStore: store,
-          resizeImage: async (b) => b,
-          maxBytes: 10,
-        }),
-        /超出大小限制/,
-      );
-      assert.equal(storeCreates, 0, 'must not create an artifact for an oversized read');
-    } finally {
-      await rm(dir, { recursive: true, force: true });
-      await rm(externalPath, { force: true });
-    }
-  });
-});
diff --git a/apps/desktop/src/main/__tests__/chat-readiness.test.ts b/apps/desktop/src/main/__tests__/chat-readiness.test.ts
deleted file mode 100644
index f43a60a0a..000000000
--- a/apps/desktop/src/main/__tests__/chat-readiness.test.ts
+++ /dev/null
@@ -1,550 +0,0 @@
-import assert from 'node:assert/strict';
-import { describe, test } from 'node:test';
-import type { LlmConnection } from '@maka/core/llm-connections';
-import type { SessionHeader } from '@maka/core/session';
-import {
-  NO_REAL_CONNECTION_CODE,
-  assertSessionCanSend,
-  ensureSessionCanSendOrRebind,
-  errorCode,
-  requireReadyConnection,
-  errorReason,
-  shouldRebindSessionToDefault,
-  type ReadyConnectionDeps,
-} from '../chat-readiness.js';
-
-describe('chat readiness guard', () => {
-  test('blocks missing, fake, missing, disabled, and secretless model references', async () => {
-    const table: Array<{
-      name: string;
-      slug: string | null | undefined;
-      deps: ReadyConnectionDeps;
-      includes: string;
-      reason: string;
-    }> = [
-      {
-        name: 'no default model',
-        slug: null,
-        deps: deps(),
-        includes: '等待配置默认模型',
-        reason: 'missing_default_connection',
-      },
-      {
-        name: 'implicit fake slug',
-        slug: 'fake',
-        deps: deps(),
-        includes: '等待配置默认模型',
-        reason: 'missing_default_connection',
-      },
-      {
-        name: 'malformed model ref',
-        slug: 'missing',
-        deps: deps(),
-        includes: '找不到模型连接 "missing"',
-        reason: 'connection_missing',
-      },
-      {
-        name: 'disabled provider',
-        slug: 'anthropic',
-        deps: deps({ connection: connection({ enabled: false }), apiKey: 'sk-test' }),
-        includes: '已禁用',
-        reason: 'connection_disabled',
-      },
-      {
-        name: 'provider requires secret but has none',
-        slug: 'anthropic',
-        deps: deps({ connection: connection(), apiKey: null }),
-        includes: '等待填写 API key',
-        reason: 'missing_api_key',
-      },
-      {
-        name: 'OAuth provider requires login token',
-        slug: 'claude-subscription',
-        deps: deps({
-          connection: connection({
-            slug: 'claude-subscription',
-            name: 'Claude OAuth',
-            providerType: 'claude-subscription',
-          }),
-          apiKey: null,
-        }),
-        includes: '等待完成 OAuth 登录',
-        reason: 'missing_api_key',
-      },
-    ];
-
-    for (const entry of table) {
-      await assertRejectsReadiness(entry.name, () => requireReadyConnection(entry.slug, entry.deps), entry.includes, entry.reason);
-    }
-  });
-
-  test('blocks connections with no usable model or model outside enabled list', async () => {
-    await assertRejectsReadiness(
-      'blank default model',
-      () => requireReadyConnection('custom', deps({
-        connection: connection({ slug: 'custom', providerType: 'openai-compatible', defaultModel: '' }),
-        apiKey: 'sk-test',
-      })),
-      '没有可用模型',
-      'missing_model',
-    );
-
-    await assertRejectsReadiness(
-      'empty model list',
-      () => requireReadyConnection('custom', deps({
-        connection: connection({ slug: 'custom', models: [] }),
-        apiKey: 'sk-test',
-      })),
-      '没有启用任何模型',
-      'empty_model_list',
-    );
-
-    await assertRejectsReadiness(
-      'requested model outside enabled list',
-      () => requireReadyConnection('custom', deps({
-        connection: connection({
-          slug: 'custom',
-          defaultModel: 'glm-4.7',
-          models: [{ id: 'glm-4.7' }],
-        }),
-        apiKey: 'sk-test',
-      }), 'gpt-4o'),
-      '不在连接 "Anthropic" 的启用模型列表中',
-      'model_not_enabled',
-    );
-
-    await assertRejectsReadiness(
-      'default model explicitly unsupported for chat',
-      () => requireReadyConnection('custom', deps({
-        connection: connection({
-          slug: 'custom',
-          defaultModel: 'gpt-image-1',
-          models: [{ id: 'gpt-image-1', capabilities: { chat: false, imageGeneration: true } }],
-        }),
-        apiKey: 'sk-test',
-      })),
-      '不能用于聊天',
-      'model_not_chat_capable',
-    );
-  });
-
-  test('send path blocks explicit fake sessions and revalidates old ai sessions', async () => {
-    await assertRejectsReadiness(
-      'explicit fake session',
-      () => assertSessionCanSend(header({ backend: 'fake', llmConnectionSlug: 'fake', model: 'fake-model' }), deps()),
-      '旧的本地模拟连接',
-      'fake_backend',
-    );
-
-    await assertRejectsReadiness(
-      'old ai session after provider deletion',
-      () => assertSessionCanSend(header({ llmConnectionSlug: 'deleted' }), deps()),
-      '找不到模型连接 "deleted"',
-      'connection_missing',
-    );
-
-    await assertRejectsReadiness(
-      'old ai session after key removal',
-      () => assertSessionCanSend(header(), deps({ connection: connection(), apiKey: null })),
-      '等待填写 API key',
-      'missing_api_key',
-    );
-
-    await assert.doesNotReject(() =>
-      assertSessionCanSend(header(), deps({ connection: connection(), apiKey: 'sk-test' })),
-    );
-  });
-
-  test('model_not_enabled names the requested model instead of the default', async () => {
-    await assert.rejects(
-      () => requireReadyConnection(
-        'anthropic',
-        deps({
-          connection: connection({
-            defaultModel: 'claude-3-5-sonnet-20241022',
-            models: [{ id: 'claude-3-5-sonnet-20241022' }],
-          }),
-          apiKey: 'sk-test',
-        }),
-        'gpt-4o-NOT-IN-LIST',
-      ),
-      (error) => {
-        const message = (error as Error).message;
-        assert.match(message, /gpt-4o-NOT-IN-LIST/, 'requested model must appear in error copy');
-        assert.doesNotMatch(message, /claude-3-5-sonnet-20241022/, 'defaultModel must NOT leak into requested-model error');
-        assert.equal(errorReason(error), 'model_not_enabled');
-        return true;
-      },
-    );
-  });
-
-  test('credential validation status does not gate a real send attempt', async () => {
-    const ready = await requireReadyConnection(
-      'anthropic',
-      deps({
-        connection: connection({ lastTestStatus: 'error' }),
-        apiKey: 'sk-test',
-      }),
-    );
-    assert.equal(ready.connection.slug, 'anthropic');
-    assert.equal(ready.model, 'claude-3-5-sonnet-20241022');
-  });
-
-  test('classifies stale sessions that can be rebound to the current default model', () => {
-    assert.equal(shouldRebindSessionToDefault('model_not_enabled'), true);
-    assert.equal(shouldRebindSessionToDefault('missing_api_key'), false);
-    assert.equal(shouldRebindSessionToDefault(undefined), false);
-  });
-
-  test('rebinds stale ai-sdk sessions to a ready default connection before send', async () => {
-    const updates: unknown[] = [];
-    const result = await ensureSessionCanSendOrRebind(
-      'session-1',
-      header({ llmConnectionSlug: 'fake-claude', model: 'fake-model' }),
-      {
-        readyConnectionDeps: keyedDeps({
-          'zai-coding-plan': {
-            connection: connection({
-              slug: 'zai-coding-plan',
-              name: 'Z.AI Coding Plan',
-              providerType: 'zai-coding-plan',
-              defaultModel: 'glm-4.7',
-              models: [{ id: 'glm-4.7' }],
-            }),
-            apiKey: 'sk-zai',
-          },
-        }),
-        async getDefaultSlug() {
-          return 'zai-coding-plan';
-        },
-        async listConnectionSlugs() {
-          return [];
-        },
-        async updateSession(_sessionId, patch) {
-          updates.push(patch);
-        },
-      },
-    );
-
-    assert.deepEqual(result, { rebound: true, connectionSlug: 'zai-coding-plan', modelId: 'glm-4.7' });
-    assert.deepEqual(updates, [{
-      backend: 'ai-sdk',
-      llmConnectionSlug: 'zai-coding-plan',
-      model: 'glm-4.7',
-      connectionLocked: true,
-    }]);
-  });
-
-  test('does not rebind locked sessions when their sticky model becomes invalid', async () => {
-    const updates: unknown[] = [];
-
-    await assertRejectsReadiness(
-      'locked sticky model outside enabled list',
-      () => ensureSessionCanSendOrRebind(
-        'session-locked',
-        header({
-          connectionLocked: true,
-          llmConnectionSlug: 'anthropic',
-          model: 'claude-old-sticky',
-        }),
-        {
-          readyConnectionDeps: keyedDeps({
-            anthropic: {
-              connection: connection({
-                slug: 'anthropic',
-                defaultModel: 'claude-new-default',
-                models: [{ id: 'claude-new-default' }],
-              }),
-              apiKey: 'sk-test',
-            },
-            'zai-coding-plan': {
-              connection: connection({
-                slug: 'zai-coding-plan',
-                name: 'Z.AI Coding Plan',
-                providerType: 'zai-coding-plan',
-                defaultModel: 'glm-4.7',
-                models: [{ id: 'glm-4.7' }],
-              }),
-              apiKey: 'sk-zai',
-            },
-          }),
-          async getDefaultSlug() {
-            return 'zai-coding-plan';
-          },
-          async listConnectionSlugs() {
-            return [];
-          },
-          async updateSession(_sessionId, patch) {
-            updates.push(patch);
-          },
-        },
-      ),
-      'claude-old-sticky',
-      'model_not_enabled',
-    );
-
-    assert.deepEqual(updates, []);
-  });
-
-  test('rebinds an unknown-provider session to the first existing ready connection', async () => {
-    const updates: unknown[] = [];
-    const rebindDeps = {
-      readyConnectionDeps: keyedDeps({
-        'branch-only-provider': {
-          connection: connection({
-            slug: 'branch-only-provider',
-            name: 'Branch-only provider',
-            providerType: 'branch-only-provider' as never,
-            defaultModel: 'branch-model',
-          }),
-          apiKey: 'gsk-test',
-        },
-        anthropic: { connection: connection(), apiKey: 'sk-test' },
-      }),
-      async getDefaultSlug() {
-        return 'branch-only-provider';
-      },
-      async listConnectionSlugs() {
-        return ['branch-only-provider', 'anthropic'];
-      },
-      async updateSession(_sessionId: string, patch: unknown) {
-        updates.push(patch);
-      },
-    };
-
-    const result = await ensureSessionCanSendOrRebind(
-      'session-unknown-provider',
-      header({ llmConnectionSlug: 'branch-only-provider', model: 'branch-model' }),
-      rebindDeps,
-    );
-
-    assert.deepEqual(result, {
-      rebound: true,
-      connectionSlug: 'anthropic',
-      modelId: 'claude-3-5-sonnet-20241022',
-    });
-    assert.equal(updates.length, 1);
-  });
-
-  test('keeps the original readiness error when no ready default exists for rebind', async () => {
-    await assertRejectsReadiness(
-      'fake session without ready default',
-      () => ensureSessionCanSendOrRebind(
-        'session-1',
-        header({ backend: 'fake', llmConnectionSlug: 'fake', model: 'fake-model' }),
-        {
-          readyConnectionDeps: keyedDeps({}),
-          async getDefaultSlug() {
-            return null;
-          },
-          async listConnectionSlugs() {
-            return ['missing'];
-          },
-          async updateSession() {
-            throw new Error('must not update');
-          },
-        },
-      ),
-      '旧的本地模拟连接',
-      'fake_backend',
-    );
-  });
-
-});
-
-async function assertRejectsReadiness(name: string, fn: () => Promise<unknown>, includes: string, reason: string): Promise<void> {
-  await assert.rejects(
-    fn,
-    (error) => {
-      assert.equal(errorCode(error), NO_REAL_CONNECTION_CODE, name);
-      assert.equal(errorReason(error), reason, name);
-      assert.match((error as Error).message, new RegExp(escapeRegExp(includes)), name);
-      return true;
-    },
-  );
-}
-
-function deps(input: { connection?: LlmConnection | null; apiKey?: string | null } = {}): ReadyConnectionDeps {
-  return {
-    async getConnection(_slug: string) {
-      return input.connection ?? null;
-    },
-    async getApiKey(_slug: string) {
-      return input.apiKey ?? null;
-    },
-  };
-}
-
-function keyedDeps(entries: Record<string, { connection: LlmConnection; apiKey?: string | null }>): ReadyConnectionDeps {
-  return {
-    async getConnection(slug: string) {
-      return entries[slug]?.connection ?? null;
-    },
-    async getApiKey(slug: string) {
-      return entries[slug]?.apiKey ?? null;
-    },
-  };
-}
-
-function connection(patch: Partial<LlmConnection> = {}): LlmConnection {
-  return {
-    slug: 'anthropic',
-    name: 'Anthropic',
-    providerType: 'anthropic',
-    defaultModel: 'claude-3-5-sonnet-20241022',
-    enabled: true,
-    createdAt: 1,
-    updatedAt: 1,
-    ...patch,
-  };
-}
-
-function header(patch: Partial<SessionHeader> = {}): Pick<SessionHeader, 'backend' | 'llmConnectionSlug' | 'model' | 'connectionLocked'> {
-  return {
-    backend: 'ai-sdk',
-    llmConnectionSlug: 'anthropic',
-    model: 'claude-3-5-sonnet-20241022',
-    connectionLocked: false,
-    ...patch,
-  };
-}
-
-describe('send-gate fact resolution stays staged', () => {
-  test('a healthy send resolves only its own connection facts', async () => {
-    const secretReads: string[] = [];
-    const result = await ensureSessionCanSendOrRebind(
-      'session-1',
-      header(),
-      {
-        readyConnectionDeps: {
-          async getConnection(slug: string) {
-            return slug === 'anthropic' ? connection() : connection({ slug });
-          },
-          async getApiKey(slug: string) {
-            secretReads.push(slug);
-            if (slug === 'unrelated') throw new Error('credential store blew up');
-            return 'sk-test';
-          },
-        },
-        async getDefaultSlug() {
-          throw new Error('default store must not be read');
-        },
-        async listConnectionSlugs() {
-          throw new Error('connection list must not be read');
-        },
-        async updateSession() {
-          throw new Error('must not rebind');
-        },
-      },
-    );
-
-    assert.deepEqual(result, { rebound: false });
-    assert.deepEqual(secretReads, ['anthropic'], 'only the session’s own connection is probed on the healthy path');
-  });
-
-  test('rebind picks the first ready candidate in persisted order, not async completion order', async () => {
-    const delays: Record<string, number> = { 'default-broken': 0, 'slow-first': 30, 'fast-second': 1 };
-    const bySlug = {
-      'default-broken': connection({ slug: 'default-broken', enabled: false }),
-      'slow-first': connection({ slug: 'slow-first' }),
-      'fast-second': connection({ slug: 'fast-second' }),
-    } as Record<string, LlmConnection>;
-    const updates: unknown[] = [];
-    const result = await ensureSessionCanSendOrRebind(
-      'session-1',
-      header({ backend: 'fake', llmConnectionSlug: 'fake', model: 'fake-model' }),
-      {
-        readyConnectionDeps: {
-          async getConnection(slug: string) {
-            await new Promise((resolve) => setTimeout(resolve, delays[slug] ?? 0));
-            return bySlug[slug] ?? null;
-          },
-          async getApiKey() {
-            return 'sk-test';
-          },
-        },
-        async getDefaultSlug() {
-          return 'default-broken';
-        },
-        async listConnectionSlugs() {
-          return ['default-broken', 'slow-first', 'fast-second'];
-        },
-        async updateSession(_id, patch) {
-          updates.push(patch);
-        },
-      },
-    );
-
-    assert.deepEqual(result, { rebound: true, connectionSlug: 'slow-first', modelId: 'claude-3-5-sonnet-20241022' });
-    assert.equal(updates.length, 1);
-  });
-
-  test('rebind walk stops probing after the first ready candidate, so a hanging later candidate cannot stall recovery', async () => {
-    const secretReads: string[] = [];
-    const updates: unknown[] = [];
-    const gate = ensureSessionCanSendOrRebind(
-      'session-1',
-      header({ backend: 'fake', llmConnectionSlug: 'fake', model: 'fake-model' }),
-      {
-        readyConnectionDeps: {
-          async getConnection(slug: string) {
-            return connection({ slug });
-          },
-          async getApiKey(slug: string) {
-            secretReads.push(slug);
-            if (slug === 'hanging-oauth') return new Promise<never>(() => {});
-            return 'sk-test';
-          },
-        },
-        async getDefaultSlug() {
-          return 'ready-default';
-        },
-        async listConnectionSlugs() {
-          return ['ready-default', 'hanging-oauth'];
-        },
-        async updateSession(_id, patch) {
-          updates.push(patch);
-        },
-      },
-    );
-    const outcome = await Promise.race([
-      gate,
-      new Promise((resolve) => setTimeout(() => resolve('timed_out'), 500)),
-    ]);
-
-    assert.deepEqual(outcome, { rebound: true, connectionSlug: 'ready-default', modelId: 'claude-3-5-sonnet-20241022' });
-    assert.equal(updates.length, 1);
-    assert.deepEqual(secretReads, ['ready-default'], 'candidates after the first ready one must never be probed');
-  });
-
-  test('rebind walk skips candidates whose facts cannot be read', async () => {
-    const result = await ensureSessionCanSendOrRebind(
-      'session-1',
-      header({ backend: 'fake', llmConnectionSlug: 'fake', model: 'fake-model' }),
-      {
-        readyConnectionDeps: {
-          async getConnection(slug: string) {
-            if (slug === 'flaky') throw new Error('read failed');
-            return slug === 'anthropic' ? connection() : null;
-          },
-          async getApiKey() {
-            return 'sk-test';
-          },
-        },
-        async getDefaultSlug() {
-          return 'flaky';
-        },
-        async listConnectionSlugs() {
-          return ['flaky', 'anthropic'];
-        },
-        async updateSession() {},
-      },
-    );
-
-    assert.deepEqual(result, { rebound: true, connectionSlug: 'anthropic', modelId: 'claude-3-5-sonnet-20241022' });
-  });
-});
-
-function escapeRegExp(value: string): string {
-  return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
-}
diff --git a/apps/desktop/src/main/__tests__/config-transfer-service.test.ts b/apps/desktop/src/main/__tests__/config-transfer-service.test.ts
index d9a17265b..d02308a32 100644
--- a/apps/desktop/src/main/__tests__/config-transfer-service.test.ts
+++ b/apps/desktop/src/main/__tests__/config-transfer-service.test.ts
@@ -3,7 +3,7 @@ import { describe, it } from 'node:test';
 import type { AppSettings } from '@maka/core/settings';
 import type { LlmConnection } from '@maka/core/llm-connections';
 import type { CredentialKind } from '@maka/storage';
-import { applyConfigImport, gatherConfigExport, type ConfigTransferDeps } from '../config-transfer-service.js';
+import { applyConfigImport, type ConfigTransferDeps } from '../config-transfer-service.js';
 
 function conn(slug: string): LlmConnection {
   return {
@@ -17,15 +17,6 @@ function conn(slug: string): LlmConnection {
   };
 }
 
-function settingsWithSecrets(): AppSettings {
-  return {
-    theme: 'dark',
-    network: { proxy: { host: '127.0.0.1', password: 'proxy-secret' } },
-    botChat: { channels: { telegram: { chatId: '42', token: 'bot-secret', appSecret: 'app-secret' } } },
-    webSearch: { providers: { tavily: { apiKey: 'tavily-secret' } } },
-  } as unknown as AppSettings;
-}
-
 function makeDeps(overrides: Partial<ConfigTransferDeps> = {}): {
   deps: ConfigTransferDeps;
   saved: LlmConnection[];
@@ -37,9 +28,7 @@ function makeDeps(overrides: Partial<ConfigTransferDeps> = {}): {
   const updatedSettings: unknown[] = [];
   const setCreds: Array<{ slug: string; kind: CredentialKind; value: string }> = [];
   const writtenMemory: string[] = [];
-  const secretsBySlugKind = new Map<string, string>([['deepseek-main::api_key', 'sk-real-key']]);
   const deps: ConfigTransferDeps = {
-    appVersion: '0.1.0',
     connectionStore: {
       list: async () => [conn('deepseek-main')],
       save: async (c) => {
@@ -48,19 +37,16 @@ function makeDeps(overrides: Partial<ConfigTransferDeps> = {}): {
       },
     },
     settingsStore: {
-      get: async () => settingsWithSecrets(),
       update: async (patch) => {
         updatedSettings.push(patch);
         return patch as unknown as AppSettings;
       },
     },
     credentialStore: {
-      getSecret: async (slug, kind) => secretsBySlugKind.get(`${slug}::${kind}`) ?? null,
       setSecret: async (slug, kind, value) => {
         setCreds.push({ slug, kind, value });
       },
     },
-    readMemory: async () => '# MEMORY\n- note',
     writeMemory: async (content) => {
       writtenMemory.push(content);
     },
@@ -70,41 +56,6 @@ function makeDeps(overrides: Partial<ConfigTransferDeps> = {}): {
 }
 
 describe('config-transfer-service', () => {
-  it('exports only selected categories', async () => {
-    const { deps } = makeDeps();
-    const bundle = await gatherConfigExport(['connections'], deps);
-    assert.deepEqual(bundle.includedData, ['connections']);
-    assert.equal(bundle.data.settings, undefined);
-    assert.equal(bundle.data.credentials, undefined);
-  });
-
-  it('omits (does not blank) settings secrets when credentials are NOT included', async () => {
-    // Secret keys must be ABSENT, not '' — mergeSettings deep-merges to the
-    // leaf, so an absent key preserves the target machine's existing secret on
-    // import, whereas '' would overwrite and wipe it.
-    const { deps } = makeDeps();
-    const bundle = await gatherConfigExport(['settings'], deps);
-    const s = bundle.data.settings as Record<string, any>;
-    assert.equal('password' in s.network.proxy, false, 'proxy password key omitted');
-    assert.equal('token' in s.botChat.channels.telegram, false, 'bot token key omitted');
-    assert.equal('appSecret' in s.botChat.channels.telegram, false, 'bot appSecret key omitted');
-    assert.equal('apiKey' in s.webSearch.providers.tavily, false, 'tavily apiKey key omitted');
-    // Non-secret fields at every level pass through untouched.
-    assert.equal(s.theme, 'dark');
-    assert.equal(s.network.proxy.host, '127.0.0.1');
-    assert.equal(s.botChat.channels.telegram.chatId, '42');
-  });
-
-  it('keeps settings secrets and enumerates credentials when credentials ARE included', async () => {
-    const { deps } = makeDeps();
-    const bundle = await gatherConfigExport(['settings', 'credentials'], deps);
-    const s = bundle.data.settings as Record<string, any>;
-    assert.equal(s.network.proxy.password, 'proxy-secret', 'secrets retained alongside credentials');
-    assert.deepEqual(bundle.data.credentials, [
-      { slug: 'deepseek-main', kind: 'api_key', value: 'sk-real-key' },
-    ]);
-  });
-
   it('applies an imported bundle to the stores and summarizes', async () => {
     const { deps, saved, updatedSettings, setCreds, writtenMemory } = makeDeps();
     const bundle = {
diff --git a/apps/desktop/src/main/__tests__/git-review-main.test.ts b/apps/desktop/src/main/__tests__/git-review-main.test.ts
index 1550398ee..8aa1c9d06 100644
--- a/apps/desktop/src/main/__tests__/git-review-main.test.ts
+++ b/apps/desktop/src/main/__tests__/git-review-main.test.ts
@@ -1,11 +1,11 @@
 import assert from 'node:assert/strict';
 import { execFile } from 'node:child_process';
-import { mkdtemp, readFile, rm, stat, writeFile } from 'node:fs/promises';
+import { mkdtemp, rm, writeFile } from 'node:fs/promises';
 import { tmpdir } from 'node:os';
 import { join } from 'node:path';
 import { promisify } from 'node:util';
 import { after, describe, it } from 'node:test';
-import { mutateGitReview, readGitReview } from '../git-review-main.js';
+import { readGitReview } from '../git-review-main.js';
 
 const execFileAsync = promisify(execFile);
 const roots = new Set<string>();
@@ -126,94 +126,6 @@ describe('Git Review snapshot authority', () => {
     );
   });
 
-  it('stages and unstages only paths admitted by the current snapshot', async () => {
-    const root = await repository();
-    await writeFile(join(root, 'base.txt'), 'base\nchanged\n', 'utf8');
-    await writeFile(join(root, 'staged.txt'), 'staged\n', 'utf8');
-    await git(root, 'add', 'staged.txt');
-
-    const unstaged = await readGitReview(root, 'unstaged');
-    assert.equal(unstaged.ok, true);
-    if (!unstaged.ok) return;
-    const stagedBase = await mutateGitReview({
-      cwd: root,
-      source: 'unstaged',
-      revision: unstaged.snapshot.revision,
-      path: 'base.txt',
-      action: 'stage',
-    });
-    assert.equal(stagedBase.ok, true);
-    if (!stagedBase.ok) return;
-    assert.equal(stagedBase.review.ok, true);
-    if (stagedBase.review.ok) {
-      assert.deepEqual(stagedBase.review.snapshot.files, []);
-    }
-
-    const staged = await readGitReview(root, 'staged');
-    assert.equal(staged.ok, true);
-    if (!staged.ok) return;
-    assert.deepEqual(
-      staged.snapshot.files.map((file) => file.path).sort(),
-      ['base.txt', 'staged.txt'],
-    );
-    const unstagedFile = await mutateGitReview({
-      cwd: root,
-      source: 'staged',
-      revision: staged.snapshot.revision,
-      path: 'staged.txt',
-      action: 'unstage',
-    });
-    assert.equal(unstagedFile.ok, true);
-    if (unstagedFile.ok && unstagedFile.review.ok) {
-      assert.deepEqual(
-        unstagedFile.review.snapshot.files.map((file) => file.path),
-        ['base.txt'],
-      );
-    }
-
-    assert.deepEqual(
-      await mutateGitReview({
-        cwd: root,
-        source: 'staged',
-        revision: '0'.repeat(64),
-        path: 'base.txt',
-        action: 'unstage',
-      }),
-      { ok: false, reason: 'stale_snapshot' },
-    );
-  });
-
-  it('reverts tracked changes and deletes an admitted untracked file', async () => {
-    const root = await repository();
-    await writeFile(join(root, 'base.txt'), 'base\nchanged\n', 'utf8');
-    await writeFile(join(root, 'untracked.txt'), 'temporary\n', 'utf8');
-
-    const snapshot = await readGitReview(root, 'unstaged');
-    assert.equal(snapshot.ok, true);
-    if (!snapshot.ok) return;
-    const reverted = await mutateGitReview({
-      cwd: root,
-      source: 'unstaged',
-      revision: snapshot.snapshot.revision,
-      path: 'base.txt',
-      action: 'revert',
-    });
-    assert.equal(reverted.ok, true);
-    assert.equal(await readFile(join(root, 'base.txt'), 'utf8'), 'base\n');
-
-    const afterTracked = await readGitReview(root, 'unstaged');
-    assert.equal(afterTracked.ok, true);
-    if (!afterTracked.ok) return;
-    const removed = await mutateGitReview({
-      cwd: root,
-      source: 'unstaged',
-      revision: afterTracked.snapshot.revision,
-      path: 'untracked.txt',
-      action: 'revert',
-    });
-    assert.equal(removed.ok, true);
-    assert.equal(await stat(join(root, 'untracked.txt')).catch(() => null), null);
-  });
 });
 
 async function repository(): Promise<string> {
diff --git a/apps/desktop/src/main/__tests__/runtime-host-config-ipc-main.test.ts b/apps/desktop/src/main/__tests__/runtime-host-config-ipc-main.test.ts
new file mode 100644
index 000000000..1b9c923ae
--- /dev/null
+++ b/apps/desktop/src/main/__tests__/runtime-host-config-ipc-main.test.ts
@@ -0,0 +1,115 @@
+import assert from 'node:assert/strict';
+import test from 'node:test';
+import type { AppSettings } from '@maka/core/settings';
+import type {
+  ConnectionCatalogSnapshot,
+  CredentialLocator,
+} from '@maka/core/runtime-policy';
+import { gatherRuntimeHostConfig } from '../runtime-host-config-ipc-main.js';
+
+const CATALOG: ConnectionCatalogSnapshot = {
+  revision: 1,
+  defaultTarget: {
+    connectionId: '00000000-0000-4000-8000-000000000001',
+    modelId: 'deepseek-v4-pro',
+  },
+  connections: [
+    {
+      connectionId: '00000000-0000-4000-8000-000000000001',
+      revision: 1,
+      slug: 'deepseek-main',
+      name: 'DeepSeek',
+      providerType: 'deepseek',
+      enabled: true,
+      enabledModelIds: ['deepseek-v4-pro'],
+      models: [{ id: 'deepseek-v4-pro' }],
+    },
+  ],
+};
+
+test('Runtime Host config export omits settings secrets unless credentials are selected', async () => {
+  let credentialExports = 0;
+  const bundle = await gatherRuntimeHostConfig(
+    ['settings'],
+    {
+      client: {
+        exportConfigurationCredentials: async () => {
+          credentialExports += 1;
+          return { credential: null };
+        },
+      },
+      appVersion: '0.1.0',
+      getSettings: async () => settingsWithSecrets(),
+    } as never,
+  );
+
+  const settings = bundle.data.settings as Record<string, any>;
+  assert.deepEqual(bundle.includedData, ['settings']);
+  assert.equal(credentialExports, 0);
+  assert.equal('password' in settings.network.proxy, false);
+  assert.equal('token' in settings.botChat.channels.telegram, false);
+  assert.equal('appSecret' in settings.botChat.channels.telegram, false);
+  assert.equal('apiKey' in settings.webSearch.providers.tavily, false);
+  assert.equal(settings.network.proxy.host, '127.0.0.1');
+});
+
+test('Runtime Host config export reads selected credentials from Host authority', async () => {
+  const bundle = await gatherRuntimeHostConfig(
+    ['connections', 'settings', 'credentials'],
+    {
+      client: {
+        loadConnectionCatalog: async () => CATALOG,
+        exportConfigurationCredentials: async ({ locator }: { locator: CredentialLocator }) => {
+          const secret = secretFor(locator);
+          return {
+            credential:
+              secret === null
+                ? null
+                : {
+                    locator,
+                    secretBase64: Buffer.from(secret).toString('base64'),
+                  },
+          };
+        },
+      },
+      appVersion: '0.1.0',
+      getSettings: async () => settingsWithSecrets(),
+    } as never,
+  );
+
+  const settings = bundle.data.settings as Record<string, any>;
+  assert.deepEqual(bundle.data.credentials, [
+    { slug: 'deepseek-main', kind: 'api_key', value: 'sk-host' },
+  ]);
+  assert.equal(settings.network.proxy.password, 'proxy-host');
+  assert.equal(settings.webSearch.providers.tavily.apiKey, 'tavily-host');
+  assert.equal(settings.botChat.channels.telegram.token, 'bot-secret');
+});
+
+function settingsWithSecrets(): AppSettings {
+  return {
+    theme: 'dark',
+    network: { proxy: { host: '127.0.0.1', password: 'local-proxy-secret' } },
+    botChat: {
+      channels: {
+        telegram: {
+          chatId: '42',
+          token: 'bot-secret',
+          appSecret: 'app-secret',
+        },
+      },
+    },
+    webSearch: {
+      providers: { tavily: { apiKey: 'local-tavily-secret' } },
+    },
+  } as unknown as AppSettings;
+}
+
+function secretFor(locator: CredentialLocator): string | null {
+  if (locator.scope === 'network_proxy') return 'proxy-host';
+  if (locator.scope === 'web_search') return 'tavily-host';
+  if (locator.scope === 'connection' && locator.kind === 'api_key') {
+    return 'sk-host';
+  }
+  return null;
+}
diff --git a/apps/desktop/src/main/attachment-ingest.ts b/apps/desktop/src/main/attachment-ingest.ts
index 3ca4143c4..f6ffa7bde 100644
--- a/apps/desktop/src/main/attachment-ingest.ts
+++ b/apps/desktop/src/main/attachment-ingest.ts
@@ -1,15 +1,14 @@
 import { Buffer } from 'node:buffer';
-import { open, realpath as fsRealpath } from 'node:fs/promises';
-import { basename, relative, sep } from 'node:path';
+import { open } from 'node:fs/promises';
+import { basename } from 'node:path';
 import {
   attachmentKindFromMimeType,
   guessMimeFromName,
   MAX_ATTACHMENT_BYTES,
   MAX_ATTACHMENT_COUNT,
 } from '@maka/core/attachments';
-import type { ArtifactKind, ArtifactSource } from '@maka/core/artifacts';
+import type { ArtifactKind } from '@maka/core/artifacts';
 import type { AttachmentRef } from '@maka/core/events';
-import type { ArtifactStore } from '@maka/storage';
 import type { AttachmentApprovalRegistry } from './attachment-approval.js';
 
 export type AttachmentIngestFile =
@@ -25,75 +24,14 @@ export interface AttachmentSnapshotInput {
 }
 
 /**
- * Resolve a selected/dropped file into an {@link AttachmentRef} the runtime
- * can consume:
- *  - image (anywhere): resize → ArtifactStore snapshot → session_file ref.
- *    Images must become provider image parts, so they are always snapshotted
- *    (an external image path could vanish or be swapped before the turn runs).
- *  - non-image inside the workspace: workspace_file ref, no copy. The model
- *    reads it on demand via the Read tool, which is already cwd-bound.
- *  - non-image outside the workspace: ArtifactStore snapshot → session_file
- *    ref. Snapshots the bytes at attach time so a symlink swap (TOCTOU) or a
- *    deleted temp file cannot change what the model later reads.
- *
- * `resizeImage` is injected because it depends on Electron's nativeImage;
- * tests pass a fake. `turnId` is not known at attach time, so the snapshot is
- * filed under the sessionId.
- */
-export async function ingestAttachments(input: {
-  files: AttachmentIngestFile[];
-  cwd: string;
-  sessionId: string;
-  artifactStore: ArtifactStore;
-  resizeImage?: (bytes: Uint8Array) => Promise<Uint8Array>;
-  realpath?: (path: string) => Promise<string>;
-  now?: () => number;
-  maxBytes?: number;
-}): Promise<AttachmentRef[]> {
-  return resolveAttachmentRefs({
-    ...input,
-    workspaceFiles: 'reference',
-    snapshot: async ({ name, mimeType, artifactKind, attachmentKind, content }) => {
-      const source: ArtifactSource = 'user_upload';
-      const record = await input.artifactStore.create({
-        sessionId: input.sessionId,
-        turnId: input.sessionId,
-        name,
-        kind: artifactKind,
-        content,
-        mimeType,
-        source,
-        ...(input.now ? { now: input.now() } : {}),
-      });
-      return {
-        kind: attachmentKind,
-        name,
-        mimeType,
-        bytes: content.byteLength,
-        ref: {
-          kind: 'session_file',
-          sessionId: input.sessionId,
-          relativePath: record.id,
-        },
-      };
-    },
-  });
-}
-
-/**
- * Resolve attachment references while making workspace-file ownership
- * explicit. Embedded execution can preserve cwd-contained files as workspace
- * references; the Runtime Host adapter snapshots every selected path because
- * hosted Turn attachments accept only canonical Session Artifacts.
+ * Snapshot selected files through Runtime Host. Hosted Turn attachments accept
+ * only canonical Session Artifacts, so every path is read once under the byte
+ * cap and handed to the Host-owned ingest boundary.
  */
 export async function resolveAttachmentRefs(input: {
   files: AttachmentIngestFile[];
-  cwd: string;
-  sessionId: string;
-  workspaceFiles: 'reference' | 'snapshot';
   snapshot: (input: AttachmentSnapshotInput) => Promise<AttachmentRef>;
   resizeImage?: (bytes: Uint8Array) => Promise<Uint8Array>;
-  realpath?: (path: string) => Promise<string>;
   maxBytes?: number;
 }): Promise<AttachmentRef[]> {
   const maxBytes = input.maxBytes ?? MAX_ATTACHMENT_BYTES;
@@ -103,24 +41,6 @@ export async function resolveAttachmentRefs(input: {
     const mimeType = file.mimeType && file.mimeType.length > 0 ? file.mimeType : guessMimeFromName(name);
     const kind = attachmentKindFromMimeType(mimeType, name);
 
-    if (
-      input.workspaceFiles === 'reference' &&
-      kind !== 'image' &&
-      isPathAttachment(file) &&
-      (await isInsideCwdReal(input.cwd, file.path, input.realpath))
-    ) {
-      const realCwd = await resolveReal(input.cwd, input.realpath);
-      const realTarget = await resolveReal(file.path, input.realpath);
-      refs.push({
-        kind,
-        name,
-        mimeType,
-        bytes: file.size,
-        ref: { kind: 'workspace_file', relativePath: relative(realCwd, realTarget) },
-      });
-      continue;
-    }
-
     let bytes: Uint8Array = isPathAttachment(file) ? await readFileCapped(file.path, maxBytes) : file.content;
     if (kind === 'image' && input.resizeImage) {
       bytes = await input.resizeImage(bytes);
@@ -166,27 +86,6 @@ function attachmentFileName(file: AttachmentIngestFile): string {
   return name || 'attachment';
 }
 
-async function resolveReal(path: string, realpath?: (path: string) => Promise<string>): Promise<string> {
-  const resolveFn = realpath ?? fsRealpath;
-  try {
-    return await resolveFn(path);
-  } catch {
-    return path;
-  }
-}
-
-async function isInsideCwdReal(cwd: string, target: string, realpath?: (path: string) => Promise<string>): Promise<boolean> {
-  const realCwd = await resolveReal(cwd, realpath);
-  const realTarget = await resolveReal(target, realpath);
-  return isInsideCwd(realCwd, realTarget);
-}
-
-function isInsideCwd(cwd: string, target: string): boolean {
-  if (target === cwd) return true;
-  const rel = relative(cwd, target);
-  return rel !== '' && !rel.startsWith('..') && rel !== '..' && !rel.includes(`..${sep}`) && !rel.startsWith(sep);
-}
-
 /** A renderer-supplied ingest item: either a main-issued approval token (for
  * user-picked files, whose path never leaves main) or inline base64 bytes (for
  * dragged/pasted blobs, which have no trustworthy path). */
diff --git a/apps/desktop/src/main/chat-readiness.ts b/apps/desktop/src/main/chat-readiness.ts
deleted file mode 100644
index 1aa161622..000000000
--- a/apps/desktop/src/main/chat-readiness.ts
+++ /dev/null
@@ -1,291 +0,0 @@
-import {
-  isConnectionReady,
-  normalizeOpenAiCodexConnection,
-  type ChatConfigurationReason,
-} from '@maka/core/connection-readiness';
-import { NO_REAL_CONNECTION_CODE } from '@maka/core/connection-error-copy';
-import {
-  projectSessionSendOutcome,
-  sessionOwnConnectionBlockReason,
-  shouldRebindSessionToDefault,
-} from '@maka/core/session-send-projection';
-import { type LlmConnection } from '@maka/core/llm-connections';
-import { type SessionHeader } from '@maka/core/session';
-import { PROVIDER_DEFAULTS } from '@maka/core/llm-connections';
-
-// The rebind-eligibility taxonomy moved to `@maka/core/session-send-projection`
-// (#1038) so the send gate and the renderer health notice share one
-// decision source. Re-exported here for back-compat.
-export { NO_REAL_CONNECTION_CODE } from '@maka/core/connection-error-copy';
-
-// The rebind-eligibility taxonomy moved to `@maka/core/session-send-projection`
-// (#1038) so the send gate and the renderer health notice share one
-// decision source. Re-exported here for back-compat.
-export { shouldRebindSessionToDefault } from '@maka/core/session-send-projection';
-
-// `ChatConfigurationReason` moved to `@maka/core/connection-readiness`
-// (PR110a) so the same taxonomy is shared between the send path and
-// onboarding. Re-exported here for back-compat — any
-// future addition belongs in core, not here.
-export type { ChatConfigurationReason };
-
-export interface ReadyConnectionDeps {
-  getConnection(slug: string): Promise<LlmConnection | null>;
-  getApiKey(slug: string): Promise<string | null | undefined>;
-}
-
-export interface ReadyConnection {
-  connection: LlmConnection;
-  apiKey: string;
-  model: string;
-}
-
-export interface SessionRebindDeps {
-  readyConnectionDeps: ReadyConnectionDeps;
-  getDefaultSlug(): Promise<string | null>;
-  listConnectionSlugs(): Promise<string[]>;
-  updateSession(
-    sessionId: string,
-    patch: Pick<SessionHeader, 'backend' | 'llmConnectionSlug' | 'model' | 'connectionLocked'>,
-  ): Promise<unknown>;
-}
-
-export interface SessionRebindResult {
-  rebound: boolean;
-  connectionSlug?: string;
-  modelId?: string;
-}
-
-export async function requireReadyConnection(
-  slug: string | null | undefined,
-  deps: ReadyConnectionDeps,
-  requestedModel?: string,
-): Promise<ReadyConnection> {
-  // Slug missing / explicit 'fake' shortcut is checked before reaching
-  // the core helper because we lack a connection object to evaluate.
-  if (!slug || slug === 'fake') {
-    throw chatConfigurationError(
-      '等待配置默认模型。请到 设置 · 模型 添加 Anthropic / OpenAI / GLM 等 API key。',
-      'missing_default_connection',
-    );
-  }
-
-  const connection = await deps.getConnection(slug);
-  if (!connection) {
-    throw chatConfigurationError(
-      `找不到模型连接 "${slug}"。请到 设置 · 模型 重新选择默认模型。`,
-      'connection_missing',
-    );
-  }
-
-  // PR110a: delegate the actual ready judgment to the pure core helper
-  // so onboarding and the send path share a single source of truth. The
-  // desktop side only owns: (1) async secret lookup, (2) Chinese error
-  // copy, (3) the throw-error API the rest of main.ts expects.
-  const normalizedConnection = normalizeOpenAiCodexConnection(connection);
-  const apiKey = await deps.getApiKey(normalizedConnection.slug);
-  const verdict = isConnectionReady({
-    connection: normalizedConnection,
-    hasSecret: typeof apiKey === 'string' && apiKey.length > 0,
-    requestedModel,
-  });
-
-  if (verdict.ready === false) {
-    throw chatConfigurationError(
-      messageForReason(verdict.reason, normalizedConnection, requestedModel),
-      verdict.reason,
-    );
-  }
-
-  return { connection: normalizedConnection, apiKey: apiKey ?? '', model: verdict.model };
-}
-
-/**
- * Map a core readiness reason to the Chinese error copy that
- * `requireReadyConnection` has historically thrown. Centralized here
- * so the copy stays close to its existing semantics (PR110a refactor
- * is behavior-preserving — only the judgment moved to core).
- */
-/**
- * Two paths reach `fake_backend`: the reason table below, and the header check
- * in `assertSessionCanSend`, which never gets far enough to look a connection
- * up. They are the same sentence to the user, so they are the same string here
- * — the rename that moved 会话 to 任务 had to be applied twice, which is what a
- * second copy costs.
- */
-const FAKE_BACKEND_MESSAGE =
-  '当前任务来自旧的本地模拟连接,不能直接发送。请到 设置 · 模型 添加真实模型后新建任务。';
-
-function messageForReason(
-  reason: ChatConfigurationReason,
-  connection: LlmConnection,
-  requestedModel: string | undefined,
-): string {
-  switch (reason) {
-    case 'connection_disabled':
-      return `模型连接 "${connection.name}" 已禁用。请到 设置 · 模型 启用或选择其他默认模型。`;
-    case 'missing_api_key':
-      if (PROVIDER_DEFAULTS[connection.providerType].authKind === 'oauth_token') {
-        return `模型连接 "${connection.name}" 等待完成 OAuth 登录。请到 设置 · 模型 重新登录后再聊天。`;
-      }
-      return `模型连接 "${connection.name}" 等待填写 API key。请到 设置 · 模型 补齐密钥后再聊天。`;
-    case 'missing_model':
-      return `模型连接 "${connection.name}" 没有可用模型。请到 设置 · 模型 选择一个默认模型。`;
-    case 'empty_model_list':
-      return `模型连接 "${connection.name}" 没有启用任何模型。请到 设置 · 模型 先添加模型。`;
-    case 'model_not_enabled': {
-      const model = requestedModel || connection.defaultModel;
-      return `模型 "${model}" 不在连接 "${connection.name}" 的启用模型列表中。请到 设置 · 模型 重新选择。`;
-    }
-    case 'model_not_chat_capable': {
-      const model = requestedModel || connection.defaultModel;
-      return `模型 "${model}" 不能用于聊天。请到 设置 · 模型 选择支持聊天的模型。`;
-    }
-    case 'fake_backend':
-      return FAKE_BACKEND_MESSAGE;
-    case 'missing_default_connection':
-    case 'connection_missing':
-      // These reasons are handled before we reach isConnectionReady,
-      // but kept here for exhaustive switch.
-      return '等待配置默认模型。请到 设置 · 模型 添加 Anthropic / OpenAI / GLM 等 API key。';
-  }
-}
-
-export async function assertSessionCanSend(
-  header: Pick<SessionHeader, 'backend' | 'llmConnectionSlug' | 'model'>,
-  deps: ReadyConnectionDeps,
-): Promise<void> {
-  if (header.backend === 'fake') {
-    throw chatConfigurationError(FAKE_BACKEND_MESSAGE, 'fake_backend');
-  }
-  await requireReadyConnection(header.llmConnectionSlug, deps, header.model);
-}
-
-export async function ensureSessionCanSendOrRebind(
-  sessionId: string,
-  header: Pick<SessionHeader, 'backend' | 'llmConnectionSlug' | 'model' | 'connectionLocked'>,
-  deps: SessionRebindDeps,
-): Promise<SessionRebindResult> {
-  // #1038: the send/rebind DECISION lives in the core projection so the
-  // renderer health notice answers "will the next send fail?" from the
-  // same facts and the same code. Main resolves the async facts,
-  // delegates the decision, then owns the side effects: the rebind
-  // mutation and the canonical error copy.
-  //
-  // Fact resolution stays staged exactly like the pre-projection send
-  // path (#1038 review): phase 1 resolves only the session's OWN
-  // connection, so a healthy session never waits on — and is never
-  // failed by — unrelated connections, the default store, or the
-  // connection list. Phase 2 gathers rebind candidates only when an
-  // unlocked session actually needs the walk.
-  const ownSlug = header.llmConnectionSlug;
-  const ownResolvable = header.backend !== 'fake' && Boolean(ownSlug) && ownSlug !== 'fake';
-  const ownConnection = ownResolvable ? await deps.readyConnectionDeps.getConnection(ownSlug) : null;
-  const ownHasSecret = ownConnection
-    ? await hasUsableSecret(deps.readyConnectionDeps, ownConnection.slug)
-    : false;
-  const ownReason = sessionOwnConnectionBlockReason(header, ownConnection, () => ownHasSecret);
-
-  if (ownReason === undefined) return { rebound: false };
-
-  if (header.connectionLocked || !shouldRebindSessionToDefault(ownReason)) {
-    // Blocked with no rebind walk. Re-run the throwing authority so the
-    // exact historical error copy surfaces unchanged; if the facts
-    // shifted underneath us (e.g. a key was just saved), the send may
-    // proceed after all.
-    await assertSessionCanSend(header, deps.readyConnectionDeps);
-    return { rebound: false };
-  }
-
-  // Phase 2: rebind candidates, resolved in deterministic order
-  // (default first, then persisted order) with the historical
-  // short-circuit: candidates are probed one at a time and the walk
-  // stops as soon as the projection can decide, so the recovery path
-  // never waits on — nor even probes — later candidates once a ready
-  // one is found (a slow or hanging OAuth refresh on an unrelated
-  // connection cannot stall the send). getDefaultSlug stays
-  // fail-closed (a default-store read error rejects the send rather
-  // than risking a rebind picked from incomplete facts), while the
-  // list read fails open to [] — the default alone can still serve.
-  const [defaultSlug, connectionSlugs] = await Promise.all([
-    deps.getDefaultSlug(),
-    deps.listConnectionSlugs().catch(() => []),
-  ]);
-  const candidateSlugs = [...new Set([defaultSlug, ...connectionSlugs])]
-    .filter((slug): slug is string => typeof slug === 'string' && slug.length > 0);
-  const connections: LlmConnection[] = [];
-  const secretPresence = new Map<string, boolean>();
-  for (const slug of candidateSlugs) {
-    try {
-      const connection = await deps.readyConnectionDeps.getConnection(slug);
-      if (connection) {
-        connections.push(connection);
-        secretPresence.set(connection.slug, await hasUsableSecret(deps.readyConnectionDeps, connection.slug));
-      }
-    } catch {
-      // Unreadable candidate: skipped, exactly like the historical
-      // walk's per-candidate catch.
-    }
-    // Re-run the projection over the resolved prefix. The winner is
-    // identical to a full assembly (the projection picks the FIRST
-    // ready candidate in this same order), but `blocked` here only
-    // means "not yet" — a later candidate may still serve, so only
-    // ready/rebind stop the walk.
-    const outcome = projectSessionSendOutcome({
-      session: header,
-      connections,
-      defaultSlug,
-      hasSecret: (candidateSlug) => secretPresence.get(candidateSlug) === true,
-    });
-
-    if (outcome.kind === 'ready') return { rebound: false }; // facts shifted mid-flight
-
-    if (outcome.kind === 'rebind') {
-      await deps.updateSession(sessionId, {
-        backend: 'ai-sdk',
-        llmConnectionSlug: outcome.connectionSlug,
-        model: outcome.model,
-        connectionLocked: true,
-      });
-      return {
-        rebound: true,
-        connectionSlug: outcome.connectionSlug,
-        modelId: outcome.model,
-      };
-    }
-  }
-
-  // Blocked after the full walk: same canonical-error re-run as above.
-  await assertSessionCanSend(header, deps.readyConnectionDeps);
-  return { rebound: false };
-}
-
-async function hasUsableSecret(deps: ReadyConnectionDeps, slug: string): Promise<boolean> {
-  const apiKey = await deps.getApiKey(slug);
-  return typeof apiKey === 'string' && apiKey.length > 0;
-}
-
-export function chatConfigurationError(message: string, reason: ChatConfigurationReason): Error {
-  const error = new Error(`${NO_REAL_CONNECTION_CODE}:${reason}: ${message}`);
-  (error as Error & { code: string; reason: ChatConfigurationReason }).code = NO_REAL_CONNECTION_CODE;
-  (error as Error & { code: string; reason: ChatConfigurationReason }).reason = reason;
-  return error;
-}
-
-export function errorCode(error: unknown): string | undefined {
-  if (error instanceof Error && 'code' in error) {
-    return String((error as { code?: unknown }).code);
-  }
-  return undefined;
-}
-
-export function errorMessage(error: unknown): string {
-  return error instanceof Error ? error.message : String(error);
-}
-
-export function errorReason(error: unknown): string | undefined {
-  if (error instanceof Error && 'reason' in error) {
-    return String((error as { reason?: unknown }).reason);
-  }
-  return undefined;
-}
diff --git a/apps/desktop/src/main/config-transfer-service.ts b/apps/desktop/src/main/config-transfer-service.ts
index 95cbd7b39..458fec554 100644
--- a/apps/desktop/src/main/config-transfer-service.ts
+++ b/apps/desktop/src/main/config-transfer-service.ts
@@ -5,23 +5,14 @@ import {
 } from '@maka/core/llm-connections';
 import {
   type ConfigBundle,
-  type ConfigCategory,
-  type ConfigData,
   type ConnectionConflictStrategy,
   type CredentialKind,
-  buildConfigBundle,
   planConnectionMerge,
 } from '@maka/storage';
-import { stripSettingsSecretsForExport } from './settings-ipc-helpers.js';
 
 /**
- * Desktop-side orchestration for config import/export. Kept store-injected and
- * Electron-free so it is unit-testable; the IPC handlers in main.ts are thin
- * wrappers that supply the real stores + file dialogs.
- *
- * The credential category (opt-in, plaintext) is gathered by walking the
- * connection list and reading each slug's connection credentials — the
- * credential store exposes no bulk-list, so enumeration is the only path.
+ * Electron-free config import orchestration. Runtime Host owns export because
+ * it is the authority for connection, credential, and memory state.
  */
 
 export interface ExportedCredential {
@@ -30,11 +21,6 @@ export interface ExportedCredential {
   value: string;
 }
 
-const CONNECTION_CREDENTIAL_KINDS: readonly CredentialKind[] = [
-  'api_key',
-  'oauth_token',
-  'request_headers',
-];
 const VALID_CREDENTIAL_KINDS: ReadonlySet<string> = new Set<CredentialKind>([
   'api_key',
   'oauth_token',
@@ -47,55 +33,11 @@ const VALID_CREDENTIAL_KINDS: ReadonlySet<string> = new Set<CredentialKind>([
 
 export interface ConfigTransferDeps {
   connectionStore: { list(): Promise<LlmConnection[]>; save(c: LlmConnection): Promise<LlmConnection> };
-  settingsStore: { get(): Promise<AppSettings>; update(patch: UpdateAppSettingsInput): Promise<AppSettings> };
+  settingsStore: { update(patch: UpdateAppSettingsInput): Promise<AppSettings> };
   credentialStore: {
-    getSecret(slug: string, kind: CredentialKind): Promise<string | null>;
     setSecret(slug: string, kind: CredentialKind, value: string): Promise<void>;
   };
-  readMemory(): Promise<string | null>;
   writeMemory(content: string): Promise<void>;
-  appVersion: string;
-}
-
-export async function gatherConfigExport(
-  categories: readonly ConfigCategory[],
-  deps: ConfigTransferDeps,
-): Promise<ConfigBundle> {
-  const selected = new Set(categories);
-  const data: ConfigData = {};
-
-  const connections = selected.has('connections') || selected.has('credentials')
-    ? await deps.connectionStore.list()
-    : [];
-
-  if (selected.has('connections')) {
-    data.connections = connections;
-  }
-
-  if (selected.has('settings')) {
-    const settings = await deps.settingsStore.get();
-    // When credentials are included, settings keeps its embedded secrets
-    // (proxy password, bot tokens, Tavily key); otherwise strip.
-    data.settings = selected.has('credentials') ? settings : stripSettingsSecretsForExport(settings);
-  }
-
-  if (selected.has('credentials')) {
-    const creds: ExportedCredential[] = [];
-    for (const connection of connections) {
-      for (const kind of CONNECTION_CREDENTIAL_KINDS) {
-        const value = await deps.credentialStore.getSecret(connection.slug, kind);
-        if (value) creds.push({ slug: connection.slug, kind, value });
-      }
-    }
-    data.credentials = creds;
-  }
-
-  if (selected.has('memory')) {
-    const memory = await deps.readMemory();
-    if (memory !== null) data.memory = memory;
-  }
-
-  return buildConfigBundle({ appVersion: deps.appVersion, data });
 }
 
 export interface ConfigImportResult {
diff --git a/apps/desktop/src/main/git-review-main.ts b/apps/desktop/src/main/git-review-main.ts
index bb46ae7ba..0d8b7dae5 100644
--- a/apps/desktop/src/main/git-review-main.ts
+++ b/apps/desktop/src/main/git-review-main.ts
@@ -1,14 +1,12 @@
 import { execFile } from 'node:child_process';
 import { createHash } from 'node:crypto';
-import { lstat, readFile, rm } from 'node:fs/promises';
+import { lstat, readFile } from 'node:fs/promises';
 import { isAbsolute, relative, resolve } from 'node:path';
 import { promisify } from 'node:util';
 import { countDiffLineStats } from '@maka/core/unified-diff';
 import {
   type GitReviewFile,
   type GitReviewFileStatus,
-  type GitReviewMutationAction,
-  type GitReviewMutationResult,
   type GitReviewReadResult,
   type GitReviewSource,
 } from '@maka/core/git-review';
@@ -128,78 +126,6 @@ export async function readGitReview(
   }
 }
 
-export async function mutateGitReview(input: {
-  cwd: string;
-  source: GitReviewSource;
-  revision: string;
-  path: string;
-  action: GitReviewMutationAction;
-  runGit?: GitReviewCommandRunner;
-}): Promise<GitReviewMutationResult> {
-  const runGit = input.runGit ?? runGitCommand;
-  try {
-    const current = await readGitReview(input.cwd, input.source, runGit);
-    if (!current.ok) return { ok: false, reason: 'git_failed' };
-    if (current.snapshot.revision !== input.revision) {
-      return { ok: false, reason: 'stale_snapshot' };
-    }
-    if (!current.snapshot.files.some((file) => file.path === input.path)) {
-      return { ok: false, reason: 'path_not_found' };
-    }
-
-    const file = current.snapshot.files.find(
-      (candidate) => candidate.path === input.path,
-    );
-    if (!file) return { ok: false, reason: 'path_not_found' };
-
-    if (input.action === 'stage') {
-      await runGit(current.snapshot.repositoryRoot, ['add', '--', input.path]);
-    } else if (input.action === 'revert') {
-      if (input.source !== 'unstaged') {
-        return { ok: false, reason: 'git_failed' };
-      }
-      if (file.status === 'untracked') {
-        const target = resolve(current.snapshot.repositoryRoot, input.path);
-        const child = relative(current.snapshot.repositoryRoot, target);
-        if (!child || child.startsWith('..') || isAbsolute(child)) {
-          return { ok: false, reason: 'path_not_found' };
-        }
-        await rm(target, { force: true });
-      } else {
-        await runGit(current.snapshot.repositoryRoot, [
-          'restore',
-          '--worktree',
-          '--',
-          input.path,
-        ]);
-      }
-    } else if (
-      await gitRefExists(current.snapshot.repositoryRoot, 'HEAD', runGit)
-    ) {
-      await runGit(current.snapshot.repositoryRoot, [
-        'restore',
-        '--staged',
-        '--',
-        input.path,
-      ]);
-    } else {
-      await runGit(current.snapshot.repositoryRoot, [
-        'rm',
-        '--cached',
-        '--',
-        input.path,
-      ]);
-    }
-
-    return {
-      ok: true,
-      review: await readGitReview(input.cwd, input.source, runGit),
-    };
-  } catch {
-    return { ok: false, reason: 'git_failed' };
-  }
-}
-
 async function readTrackedChanges(input: {
   repositoryRoot: string;
   source: GitReviewSource;
diff --git a/apps/desktop/src/main/main-window.ts b/apps/desktop/src/main/main-window.ts
index f7680f4bd..b84a22967 100644
--- a/apps/desktop/src/main/main-window.ts
+++ b/apps/desktop/src/main/main-window.ts
@@ -4,7 +4,6 @@ import { join } from 'node:path';
 import { pathToFileURL } from 'node:url';
 import type { AppSettings } from '@maka/core/settings';
 import { isExternalUrl } from './external-link-guard.js';
-import { errorMessage } from './chat-readiness.js';
 import { readSavedBounds, writeSavedBounds, SAFE_MIN_HEIGHT, SAFE_MIN_WIDTH, type SavedBounds } from './window-state.js';
 import { BrowserViewController } from './browser/controller.js';
 import { BrowserViewManager } from './browser/view-manager.js';
@@ -701,6 +700,7 @@ function emitRealWindowSmokeDiagnostic(stage: string): void {
       console.log(`[real-window-smoke] diagnostic ${JSON.stringify({ ...windowState, renderer: rendererState })}`);
     })
     .catch((err: unknown) => {
-      console.log(`[real-window-smoke] diagnostic ${JSON.stringify({ ...windowState, rendererError: errorMessage(err) })}`);
+      const rendererError = err instanceof Error ? err.message : String(err);
+      console.log(`[real-window-smoke] diagnostic ${JSON.stringify({ ...windowState, rendererError })}`);
     });
 }
diff --git a/apps/desktop/src/main/runtime-host-config-ipc-main.ts b/apps/desktop/src/main/runtime-host-config-ipc-main.ts
index 1498f2048..a8919e226 100644
--- a/apps/desktop/src/main/runtime-host-config-ipc-main.ts
+++ b/apps/desktop/src/main/runtime-host-config-ipc-main.ts
@@ -110,7 +110,7 @@ export function registerRuntimeHostConfigIpc(
   );
 }
 
-async function gatherRuntimeHostConfig(
+export async function gatherRuntimeHostConfig(
   categories: readonly ConfigCategory[],
   deps: RuntimeHostConfigIpcDeps,
 ) {
@@ -173,18 +173,14 @@ function runtimeHostTransferDeps(
       save: (connection) => saveConnection(deps.client, connection),
     },
     settingsStore: {
-      get: deps.getSettings,
       update: deps.updateSettings,
     },
     credentialStore: {
-      getSecret: async () => null,
       setSecret: (slug, kind, value) =>
         saveConnectionCredential(deps.client, slug, kind, value),
     },
-    readMemory: () => readRuntimeHostMemoryDocument(deps.client, 'memory'),
     writeMemory: (content) =>
       replaceRuntimeHostMemoryDocument(deps.client, content),
-    appVersion: deps.appVersion,
   };
 }
 
diff --git a/apps/desktop/src/main/runtime-host-session-execution-ipc-main.ts b/apps/desktop/src/main/runtime-host-session-execution-ipc-main.ts
index eec5c9cf9..d383d91ec 100644
--- a/apps/desktop/src/main/runtime-host-session-execution-ipc-main.ts
+++ b/apps/desktop/src/main/runtime-host-session-execution-ipc-main.ts
@@ -199,9 +199,6 @@ export function registerRuntimeHostSessionExecutionIpc(
         });
         attachments = await resolveAttachmentRefs({
           files,
-          cwd: session.workspace.hostCwd,
-          sessionId,
-          workspaceFiles: "snapshot",
           resizeImage: deps.resizeImage,
           snapshot: ({ name, mimeType, content }) =>
             deps.client.ingestAttachment({
diff --git a/apps/desktop/src/renderer/app-shell-session-start-actions.ts b/apps/desktop/src/renderer/app-shell-session-start-actions.ts
index 4aa513043..5f708d56e 100644
--- a/apps/desktop/src/renderer/app-shell-session-start-actions.ts
+++ b/apps/desktop/src/renderer/app-shell-session-start-actions.ts
@@ -103,7 +103,7 @@ export function createAppShellSessionStartActions(deps: {
       // is not the other one":
       //
       //   workspace_unavailable → `SESSION_WORKSPACE_UNAVAILABLE:` (project-context-root.ts)
-      //   setup_required        → `NO_REAL_CONNECTION:<reason>:`   (chat-readiness.ts)
+      //   setup_required        → `NO_REAL_CONNECTION:<reason>:`   (Runtime Host execution composition)
       //
       // Anything else is a genuine failure (storage, disk, a bug) and must
       // not be silently relabelled as "your setup is incomplete".
diff --git a/apps/desktop/src/renderer/session-health-notice.ts b/apps/desktop/src/renderer/session-health-notice.ts
index 8cce09e7e..4d5f5dbcf 100644
--- a/apps/desktop/src/renderer/session-health-notice.ts
+++ b/apps/desktop/src/renderer/session-health-notice.ts
@@ -3,12 +3,13 @@
  *
  * #1038 — the notice answers exactly one question: "will the next send
  * fail for a recoverable connection/session reason, and where should the
- * user go?". The answer comes from `projectSessionSendOutcome` — the
- * same core projection the main-process send gate delegates to, already
- * resolved by main and carried in the onboarding snapshot. The renderer
- * only maps that authoritative outcome to copy:
+ * user go?". The answer comes from `projectSessionSendOutcome`, already
+ * resolved by main and carried in the onboarding snapshot. Runtime Host
+ * remains the submission authority; the renderer only maps this
+ * compatibility projection to copy:
  *
- *   - `ready` / `rebind` → no notice (silent rebind stays silent, #1032).
+ *   - `ready` / `rebind` → no notice (`rebind` supplies a compatible
+ *     target for renderer readiness checks, #1032).
  *   - `blocked` → destructive notice whose copy names the failing
  *     connection and points at the matching Settings section.
  *
@@ -17,8 +18,9 @@
  * must never claim send is blocked either: it renders only as a
  * `warning`, only when the projection says the session's own connection
  * will serve the next send (`ready`), and its copy states plainly that
- * the send is not intercepted. When the projection rebinds away from the
- * connection, the reminder is noise and stays silent.
+ * the send is not intercepted. When the projection selects a compatibility
+ * target instead, the reminder about the stored connection is noise and
+ * stays silent.
  */
 
 import { type LlmConnection } from '@maka/core/llm-connections';
diff --git a/apps/desktop/src/renderer/task-readiness-notice.ts b/apps/desktop/src/renderer/task-readiness-notice.ts
index 93791a478..b30f7c87b 100644
--- a/apps/desktop/src/renderer/task-readiness-notice.ts
+++ b/apps/desktop/src/renderer/task-readiness-notice.ts
@@ -4,6 +4,12 @@ import type { TaskSubmissionReadinessDimension, TaskSubmissionReadinessSnapshot
 
 import type { UiLocale } from '@maka/core/ui-locale';
 
+/**
+ * Selects the model target for the renderer's readiness probe. A `rebind`
+ * projection supplies a compatible target for an empty legacy session; it
+ * does not mutate the session or admit the submission. Runtime Host owns
+ * those decisions.
+ */
 export function resolveTaskReadinessModelTarget(
   session: { llmConnectionSlug: string; model: string } | undefined,
   sendOutcome: SessionSendProjection | undefined,
diff --git a/packages/core/src/connection-readiness.ts b/packages/core/src/connection-readiness.ts
index 477329c13..4e0b4d630 100644
--- a/packages/core/src/connection-readiness.ts
+++ b/packages/core/src/connection-readiness.ts
@@ -1,7 +1,7 @@
 /**
- * Connection readiness — pure, sync judgment shared between the
- * send-path (chat-readiness.ts) and the onboarding state machine
- * (onboarding.ts). PR110a.
+ * Connection readiness — pure, sync judgment shared by task-submission
+ * readiness, the onboarding state machine, and legacy-session health
+ * projection.
  *
  * Source of truth for "is this LlmConnection ready to send a message
  * right now?". Caller is responsible for resolving async inputs
@@ -34,8 +34,8 @@ import { isModelExplicitlyUnsupportedForChat } from './model-catalog.js';
 /**
  * Canonical reasons why an LlmConnection is not ready to send.
  *
- * Moved from `apps/desktop/src/main/chat-readiness.ts` to keep the
- * taxonomy stable across the send path and onboarding surfaces.
+ * Kept in core so the taxonomy stays stable across readiness and onboarding
+ * surfaces.
  * Adding a new reason MUST update both this enum AND the matching
  * `OnboardingState` mapping in `onboarding.ts`.
  */
@@ -145,8 +145,8 @@ export function isConnectionReady(input: IsConnectionReadyInput): IsConnectionRe
  * the enabled list and the default falls back to the first servable
  * model, so the readiness gate below judges the models that would
  * actually be used. Pure; returns the input unchanged for non-Codex
- * providers. Moved from the desktop send path (#1038) so the send gate
- * and the session send projection share one normalization.
+ * providers. Moved from the former desktop send gate (#1038) so onboarding
+ * and the session compatibility projection share one normalization.
  */
 export function normalizeOpenAiCodexConnection(connection: LlmConnection): LlmConnection {
   if (connection.providerType !== 'openai-codex') return connection;
diff --git a/packages/core/src/git-review.ts b/packages/core/src/git-review.ts
index 316067629..9f7209f20 100644
--- a/packages/core/src/git-review.ts
+++ b/packages/core/src/git-review.ts
@@ -42,12 +42,3 @@ export type GitReviewReadResult =
         | 'invalid_base_branch'
         | 'git_failed';
     };
-
-export type GitReviewMutationAction = 'stage' | 'unstage' | 'revert';
-
-export type GitReviewMutationResult =
-  | { ok: true; review: GitReviewReadResult }
-  | {
-      ok: false;
-      reason: 'stale_snapshot' | 'path_not_found' | 'git_failed';
-    };
diff --git a/packages/core/src/session-send-projection.ts b/packages/core/src/session-send-projection.ts
index ae013058f..3958aa312 100644
--- a/packages/core/src/session-send-projection.ts
+++ b/packages/core/src/session-send-projection.ts
@@ -1,24 +1,22 @@
 /**
- * Session send projection — pure, sync answer to "will the next send on
- * this session succeed, silently rebind, or fail?". #1038.
+ * Session send projection — pure, sync compatibility answer for an existing
+ * session's stored model target. #1038.
  *
- * This is the single decision source shared by:
- *   - the main-process send gate (`ensureSessionCanSendOrRebind` in
- *     apps/desktop/src/main/chat-readiness.ts), which resolves the async
- *     facts (connections, secrets), calls this projection, then performs
- *     the actual rebind mutation / throws the canonical error copy;
- *   - the renderer session health notice above the composer, which maps
- *     a `blocked` outcome to actionable copy.
+ * This is the shared compatibility projection used by Desktop onboarding and
+ * the renderer session-health notice above the composer. Runtime Host owns the
+ * authoritative submission and execution path; this projection only explains
+ * whether that target looks usable or whether an empty legacy session has a
+ * compatible fallback for presentation and readiness checks.
  *
- * The logic mirrors the send path exactly:
+ * The compatibility rules are:
  *   1. The session's own connection must pass `isConnectionReady` with
  *      the sticky session model.
- *   2. A locked session (has user messages) can never rebind — any
- *      failure of its own connection blocks the send.
- *   3. An unlocked session may silently rebind only for reasons in
+ *   2. A locked session (has user messages) can never select a fallback — any
+ *      failure of its own connection projects as blocked.
+ *   3. An unlocked session may select a fallback only for reasons in
  *      `shouldRebindSessionToDefault`; the walk tries the default
  *      connection first, then every other persisted connection.
- *   4. Otherwise the send is blocked.
+ *   4. Otherwise the compatibility projection is blocked.
  *
  * `lastTestStatus` deliberately plays no part here (E4): telemetry about
  * a past credential test must not gate send, so it must not gate the
@@ -53,9 +51,8 @@ export interface SessionSendProjectionInput {
   connections: readonly LlmConnection[];
   defaultSlug: string | null;
   /**
-   * Secret presence per connection slug, resolved by the caller
-   * (credential store in main, `connections:hasSecret` IPC probe in the
-   * renderer). Only consulted for connections that exist.
+   * Secret presence per connection slug, resolved by the caller. Only
+   * consulted for connections that exist.
    */
   hasSecret(slug: string): boolean;
 }
@@ -99,18 +96,12 @@ export function projectSessionSendOutcome(
 }
 
 /**
- * Why the session's own connection cannot send, or `undefined` when it
- * can. Mirrors `assertSessionCanSend` + `requireReadyConnection` in the
- * desktop main process — keep the reason order in sync with the throwing
- * path so both surfaces report identical causes.
- *
- * Exported for the send gate's staged fact resolution (#1038 review):
- * main resolves only the session's OWN connection in phase 1 and calls
- * this directly, so a healthy session never waits on — nor is failed
- * by — unrelated connections. The full projection reuses the same
- * helper, keeping one implementation of the own-connection judgment.
+ * Why the session's own connection cannot satisfy the compatibility
+ * projection, or `undefined` when it can. Kept private so Runtime Host
+ * admission cannot accidentally grow a second dependency on this UI-facing
+ * legacy-session policy.
  */
-export function sessionOwnConnectionBlockReason(
+function sessionOwnConnectionBlockReason(
   session: SessionSendProjectionSession,
   ownConnection: LlmConnection | null,
   hasSecret: (slug: string) => boolean,
@@ -141,13 +132,13 @@ function ownConnectionBlockReason(
 }
 
 /**
- * Whether an unlocked session whose own connection failed with `reason`
- * may silently rebind to another ready connection on send. Failures not
- * listed here (e.g. `missing_api_key`, `connection_disabled`) block the
- * send even when unlocked — silently moving a session off a connection
- * the user explicitly configured would be surprising.
+ * Whether an unlocked session whose own connection failed with `reason` may
+ * project another ready connection as a compatibility target. Failures not
+ * listed here (e.g. `missing_api_key`, `connection_disabled`) stay blocked
+ * even when unlocked because masking an explicitly configured connection
+ * would make the health/readiness UI misleading.
  */
-export function shouldRebindSessionToDefault(reason: string | undefined): boolean {
+function shouldRebindSessionToDefault(reason: string | undefined): boolean {
   return (
     reason === 'fake_backend' ||
     reason === 'connection_missing' ||
diff --git a/packages/core/src/session.ts b/packages/core/src/session.ts
index 896a56389..4ea17a49c 100644
--- a/packages/core/src/session.ts
+++ b/packages/core/src/session.ts
@@ -319,9 +319,8 @@ export interface SessionSummary {
   llmConnectionSlug: string;
   /**
    * True once the session has user messages — its connection/model is
-   * sticky and the send path will never silently rebind it. Surfaced so
-   * the renderer can project send outcomes (#1038) without a main
-   * round-trip.
+   * sticky and compatibility projections never select a replacement target.
+   * Surfaced so onboarding can project existing-session health (#1038).
    */
   connectionLocked: boolean;
   /** Sticky session default model id for renderer/header display. */


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org