Skip to content

refactor: retire 'fake' from the live backend surface - #3249

Merged
Astro-Han merged 7 commits into
mainfrom
refactor/3211-retire-fake-backend-kind
Aug 20, 2026
Merged

refactor: retire 'fake' from the live backend surface#3249
Astro-Han merged 7 commits into
mainfrom
refactor/3211-retire-fake-backend-kind

Conversation

@Astro-Han

@Astro-Han Astro-Han commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Summary

Second half of #3211. #3226 stopped shipping FakeBackend; this retires 'fake' from the live surface.

The value was carrying three jobs: a selector for a runnable backend, a literal in durable records, and a sentinel meaning "not real / not known". #3226 killed the first. The second is permanent. The third is what this removes — so 'fake' now appears only in decode guards and in the one function translating it to the fake_backend product reason. Everything else reads that reason.

  • BackendKind narrows to 'ai-sdk'; PersistedBackendKind = BackendKind | 'fake' types everything durable (session header/summary, catalog wire projection, run header, Automation template, workspace defaults, and the registry dispatching off them).
  • No caller chooses a backend. CreateSessionInput.backend is gone rather than narrowed: a live build has exactly one backend, so the field carried no choice — only the chance of writing the retired value. The store stamps every new header. Two backend-kind guards go with it: the sessions:create IPC check, now unrepresentable, and the T1 tool-boundary gate on header.backend — that one was reachable, but all it withheld was the protocol marker on the initial event of a run that dies at reserveRun before any tool dispatch, and every marker consumer treats a marked run with zero tool operations the same as an unmarked one (thanks @me2seeks for correcting my original framing).
  • No data migration. Legacy rows keep 'fake' on disk. Narrowing the decode guards makes them read back as malformed; rewriting them to 'ai-sdk' makes an unrunnable task look runnable, since llmConnectionSlug still points at nothing. Activation refuses them with the product reason, as of fix(release): stop shipping FakeBackend and desktop E2E material in production artifacts #3226.
  • backendKindOf now throws for an unrecognized providerType instead of answering 'fake'. This changes @maka/core's public contract; it has no caller in this repo.

Per-commit messages carry the reasoning for each change.

Closes #3211

Behavior change

shouldRebindSessionToDefault listed fake_backend, but nothing performs that rebind — the only executor was in the deleted module, and activation dispatches off the header's own backend anyway, so no connection swap could have helped. That false promise is why the rail and composer bypassed the projection and read session.backend directly.

Removing it makes the projection answer blocked, both surfaces drop their workaround, and the existing '任务已过期 · 请先配置真实模型' notice — suppressed for unlocked rows until now — appears. That is the one user-visible change beyond the type work.

A session derived from a legacy row (branch, revision, subagent, conversation copy) no longer inherits 'fake'. It is a real session whose connection slug resolves to nothing, so the projection reports connection_missing — which is what that row actually is.

Verification

  • npm run format / npm run lint clean; typecheck clean across all affected workspaces.
  • Focused suites: core 55, storage 32, runtime 308, runtime-host 179, cli 46, desktop main 29 — all pass, including the fix(release): stop shipping FakeBackend and desktop E2E material in production artifacts #3226 regression test for activation refusal.
  • Negative checks (each restored): re-narrowing the storage header guard fails the legacy-decode test; dropping Object.hasOwn from provider recognition fails the inherited-member test.

New tests pin each decision: a retired backend never rebinds even when unlocked with a ready connection available; inherited object members (__proto__, toString) are not providers; an Automation cannot be created on the retired backend while a stored one still decodes; the pending chat view matches no offered model choice.

Two legacy-row tests — the storage decode test and the activation-refusal regression — used to seed 'fake' through the writer, demonstrating the write this PR removes. Both now seed under the writer, which is the only way such a row was ever produced.

Not run locally, left to CI: Playwright E2E and the repository-wide run.

AI use

  • No generative tool made a substantive contribution
  • Generative tooling made a substantive contribution

Tool(s) and scope: Claude Code (Opus 5) wrote the changes and this description. CodeRabbit and Qodo reviewed; findings were verified independently before acting. AI review is not independent human review. Generated-by trailers are on every commit and must survive squash.

Checklist

  • Tests cover the change and fail without it
  • Lint, format, typecheck and the affected suites pass locally

Does this PR entail a change in behavior?

  • Yes — described under Summary above
  • No

@coderabbitai

coderabbitai Bot commented Aug 19, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Problem solved

This PR retires 'fake' from the live backend protocol.

  • BackendKind now contains only 'ai-sdk'.
  • PersistedBackendKind preserves legacy 'fake' values in stored sessions, runs, and Automations.
  • Unknown providers now cause backendKindOf to throw.
  • Legacy 'fake' records remain readable but cannot activate.
  • Fake-backend product branches and obsolete readiness code are removed.
  • Pending chat sessions now use the configured connection and model.

Design and scope

The PR extends the existing backend type and decoding rules. It does not create a parallel live-backend path. PersistedBackendKind provides a narrow compatibility boundary for durable data.

The change is the smallest coherent solution shown by the diff. It separates live protocol values from persisted compatibility values without requiring data migration. The remaining fake-session handling supports reads, search, stale-session processing, and activation refusal.

The deleted chat-readiness.ts module and its 550-line test file remove obsolete code. The remaining diagnostic formatting is inlined. New pending-session and legacy-decoding tests preserve the relevant regression coverage.

Validation

Added or updated coverage verifies:

  • Unknown providers throw from backendKindOf.
  • Unknown providers are not real connections.
  • Legacy 'fake' sessions survive storage reopening unchanged.
  • Pending sessions use configured models and connections.
  • Pending-session fallbacks preserve metadata and connection state.

Required check results are unverified from the supplied evidence.

Complexity delta

The PR removes:

  • 'fake' from the live BackendKind authority.
  • The unknown-provider fallback.
  • isFakeBackend and related product-layer branches.
  • The unused desktop readiness module and its test suite.
  • Hardcoded fake values in pending-session construction.

The PR adds:

  • PersistedBackendKind as an explicit durable-data compatibility type.
  • isPersistedBackendKind validation at decoding boundaries.
  • A pendingSessionView helper and focused tests.
  • A small amount of documented legacy 'fake' handling.

The PR reduces live backend states and public protocol surface. It adds one explicit persisted-data state and validation boundary. The added compatibility complexity is necessary to avoid data migration and preserve existing records. Total maintenance complexity decreases, based on the removal of obsolete code and fake-specific product branches.

Review-relevant risks

  • Persisted backend types and protocol decoding change. Existing legacy records remain readable, but activation behavior depends on the new validation boundaries. Material changes to public contracts require independent human review under repository policy.
  • Unknown-provider handling changes from fallback behavior to an exception. This can affect error paths and user-visible diagnostics. Material changes to user-visible behavior require independent human review under repository policy.
  • Fake-backend session filtering and stale-session handling remain intentionally active. Review must confirm that legacy data remains accessible without exposing a live fake backend.
  • The objective references release artifacts and E2E compilation, but this diff does not show removal of all such material. Release and governance effects require independent human review under repository policy.

The person performing the merge must review the final diff. A maintainer makes the final determination.

Walkthrough

The change retires FakeBackend as a live backend, preserves legacy "fake" values for persisted records, updates provider readiness and session projection logic, adds pending-session view construction, and removes obsolete desktop chat-readiness code and tests.

Changes

Backend compatibility and session readiness

Layer / File(s) Summary
Provider recognition and readiness
packages/core/src/session.ts, packages/core/src/llm-connections.ts, packages/core/src/connection-readiness.ts, packages/core/src/chat-model-choice.ts, apps/desktop/src/renderer/model-catalog-choices.ts
BackendKind now represents only live backends. PersistedBackendKind retains legacy "fake" values. Unknown providers now fail explicitly, and readiness and model selection use recognized provider defaults.
Persisted backend propagation
packages/core/src/{agent-run.ts,backend-types.ts,runtime-inputs.ts,scheduled-task.ts,workspace.ts}, packages/runtime-host/src/protocol/*, packages/runtime/src/{session-manager.ts,test-only/fake-backend.ts}, packages/storage/src/*
Runtime, protocol, registry, and storage contracts use PersistedBackendKind. Decoding continues to accept legacy fake sessions and automations. Storage tests cover round-tripping fake session metadata.
Pending session projection
apps/desktop/src/renderer/pending-session-view.ts, apps/desktop/src/renderer/app-shell.tsx, packages/core/src/session-send-projection.ts, apps/desktop/src/main/__tests__/pending-session-view.test.ts
The renderer uses pendingSessionView to construct transient session summaries. Model and connection fallbacks are covered by tests. Fake connection slugs now undergo normal lookup and readiness handling, while fake backends retain fake_backend handling.
Desktop readiness cleanup
apps/desktop/src/main/chat-readiness.ts, apps/desktop/src/main/__tests__/chat-readiness.test.ts, apps/desktop/src/main/main-window.ts, apps/desktop/src/main/search/thread-search.ts, apps/desktop/src/renderer/app-shell-session-start-actions.ts
The desktop chat-readiness module and its tests are deleted. Renderer diagnostics and comments now reference native error formatting and Runtime Host ownership. Fake-session filtering behavior remains unchanged.

Estimated code review effort: 4 (Complex) | ~45 minutes

Merge Risk: 🟡 Moderate · up to d8506

The PR retires the live 'fake' backend, but the current implementation still permits new sessions and scheduled tasks to be created with the retired value and may incorrectly recognize reserved provider names such as 'proto'. This can produce unusable records or incorrect model/connection behavior, so those bounded correctness issues should be fixed before merge.

Sequence Diagram(s)

sequenceDiagram
  participant AppShell
  participant pendingSessionView
  participant NewChatModel
  participant SessionSummary
  AppShell->>NewChatModel: select model and connection
  AppShell->>pendingSessionView: pass session metadata and fallback connection
  pendingSessionView->>SessionSummary: create transient active session summary
  SessionSummary-->>AppShell: return view data
Loading

Possibly related PRs

Suggested reviewers: jackwener, m4n5ter

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the primary change: removing 'fake' from the live backend surface while retaining persisted compatibility.
Description check ✅ Passed The description includes the required summary, verification, AI-use declaration, checklist, behavior change, and issue reference.
Linked Issues check ✅ Passed The PR addresses the linked issue's backend type, unknown-provider, legacy compatibility, and product-layer requirements; release artifact removal is explicitly attributed to #3226.
Out of Scope Changes check ✅ Passed The changes remain within the linked objective and remove obsolete backend special cases, add regression coverage, and fix the pending-session placeholder.
Ai Use Disclosure ✅ Passed The description selects generative tooling, names Claude Code and its scope, and both PR commits contain standalone Generated-by: Claude Code trailers.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/3211-retire-fake-backend-kind

Comment @coderabbitai help to get the list of available commands.

@Astro-Han
Astro-Han marked this pull request as ready for review August 19, 2026 12:51
@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Retire fake from live backend types while preserving persisted data

✨ Enhancement 🐞 Bug fix 🧪 Tests 🕐 40+ Minutes

Grey Divider

AI Description

• Separates selectable backend types from persisted legacy values while retaining 'fake' decode
 compatibility.
• Rejects unknown providers instead of synthesizing a live fake backend.
• Uses real model defaults for pending sessions and removes obsolete desktop readiness code.
Diagram

graph TD
  A["Live Provider"] --> B["BackendKind"] --> C["New Writes"] --> D[("Durable State")] --> E["PersistedBackendKind"] --> F["Decode Guards"] --> G["Activation Gate"]
  H["Provider Registry"] --> G
Loading
High-Level Assessment

The live-versus-persisted type split is the safest approach. Keeping fake in BackendKind would preserve an invalid selectable state, while migrating legacy rows to ai-sdk would misrepresent unrunnable sessions and automations. Retaining decode compatibility and rejecting legacy values at activation preserves data without falsely making it executable.

Files changed (30) +267 / -110

Bug fix (2) +48 / -16
app-shell.tsxBuild pending session views from current model defaults +9/-16

Build pending session views from current model defaults

• Replaces the hardcoded fake placeholder with 'pendingSessionView', using the selected new-chat model and current default connection while the real summary loads.

apps/desktop/src/renderer/app-shell.tsx

pending-session-view.tsCreate canonical pending session placeholder +39/-0

Create canonical pending session placeholder

• Introduces a helper that builds an 'ai-sdk' session summary from the next new-task model, default connection, and permission mode.

apps/desktop/src/renderer/pending-session-view.ts

Refactor (17) +126 / -82
main-window.tsInline renderer diagnostic error formatting +1/-2

Inline renderer diagnostic error formatting

• Replaces the deleted chat-readiness error helper with equivalent local 'Error' message extraction for smoke diagnostics.

apps/desktop/src/main/main-window.ts

model-catalog-choices.tsSimplify model-consumer provider filtering +2/-5

Simplify model-consumer provider filtering

• Removes the tautological backend-kind check and treats enabled connections as model consumers when their provider is registered.

apps/desktop/src/renderer/model-catalog-choices.ts

agent-run.tsType persisted run backends separately +8/-4

Type persisted run backends separately

• Changes run headers to 'PersistedBackendKind' and explicitly retains 'fake' in durable run decode validation.

packages/core/src/agent-run.ts

backend-types.tsAllow backend interfaces to represent persisted kinds +2/-2

Allow backend interfaces to represent persisted kinds

• Changes 'AgentBackend.kind' to 'PersistedBackendKind', preserving compatibility with historical and test-only fake backends.

packages/core/src/backend-types.ts

chat-model-choice.tsRemove redundant backend-kind model filter +1/-1

Remove redundant backend-kind model filter

• Uses provider registration and connection enablement as the complete eligibility check now that every live provider uses 'ai-sdk'.

packages/core/src/chat-model-choice.ts

connection-readiness.tsBase connection readiness on known providers +19/-20

Base connection readiness on known providers

• Replaces fake-backend detection with provider-registry membership. Unknown providers still fail with the existing 'fake_backend' reason to preserve product taxonomy.

packages/core/src/connection-readiness.ts

llm-connections.tsMake unknown backend lookup explicit +14/-4

Make unknown backend lookup explicit

• Changes 'backendKindOf' to throw for unknown provider types instead of producing the retired 'fake' backend value.

packages/core/src/llm-connections.ts

runtime-inputs.tsAccept persisted backend kinds in session inputs +2/-2

Accept persisted backend kinds in session inputs

• Types session creation inputs with 'PersistedBackendKind' so runtime boundaries can represent durable legacy session data.

packages/core/src/runtime-inputs.ts

scheduled-task.tsPreserve legacy automation backend values +5/-2

Preserve legacy automation backend values

• Types frozen execution templates with 'PersistedBackendKind' and documents why decoders continue accepting 'fake'.

packages/core/src/scheduled-task.ts

session-send-projection.tsRemove dead fake-slug projection branches +18/-21

Remove dead fake-slug projection branches

• Keeps explicit fake-backend sessions blocked while removing redundant special handling for the 'fake' connection slug. Readiness now rejects unusable connections through provider knowledge.

packages/core/src/session-send-projection.ts

session.tsSplit live and persisted backend domains +24/-3

Split live and persisted backend domains

• Narrows 'BackendKind' to 'ai-sdk' and introduces 'PersistedBackendKind' for durable values that may still contain 'fake'. Session headers and summaries adopt the persisted type.

packages/core/src/session.ts

workspace.tsType workspace defaults as persisted backend data +2/-2

Type workspace defaults as persisted backend data

• Changes workspace backend defaults to 'PersistedBackendKind' so historical workspace state remains representable.

packages/core/src/workspace.ts

scheduled-task.tsRetain fake automation protocol decoding +8/-3

Retain fake automation protocol decoding

• Updates scheduled-task protocol decoding to use 'PersistedBackendKind' while continuing to accept historical fake execution templates.

packages/runtime-host/src/protocol/scheduled-task.ts

session-catalog.tsUse persisted backend type in session catalog +5/-1

Use persisted backend type in session catalog

• Replaces the inline backend union with 'PersistedBackendKind' and retains legacy fake values in wire decoding.

packages/runtime-host/src/protocol/session-catalog.ts

session-manager.tsDispatch persisted backend kinds in the registry +6/-6

Dispatch persisted backend kinds in the registry

• Changes backend registry keys and operations to 'PersistedBackendKind', allowing durable legacy values to reach explicit missing-factory or compatibility handling.

packages/runtime/src/session-manager.ts

fake-backend.tsConfine fake backend typing to persisted compatibility +2/-2

Confine fake backend typing to persisted compatibility

• Types the test-only fake backend with 'PersistedBackendKind', keeping it outside the live selectable backend domain.

packages/runtime/src/test-only/fake-backend.ts

session-store.tsPreserve fake session headers during decoding +7/-2

Preserve fake session headers during decoding

• Renames the durable backend guard and documents why session normalization must continue accepting legacy 'fake' values.

packages/storage/src/session-store.ts

Tests (3) +78 / -1
pending-session-view.test.tsCover pending-session model selection and fallbacks +46/-0

Cover pending-session model selection and fallbacks

• Adds tests proving pending chat placeholders use the configured new-task model, then fall back to the default connection or an empty model state.

apps/desktop/src/main/tests/pending-session-view.test.ts

llm-connections.test.tsTest fail-closed unknown-provider behavior +6/-1

Test fail-closed unknown-provider behavior

• Verifies 'backendKindOf' throws for unknown providers while 'isRealConnection' remains the non-throwing usability check.

packages/core/src/tests/llm-connections.test.ts

session-store.test.tsVerify legacy fake sessions survive storage reopen +26/-0

Verify legacy fake sessions survive storage reopen

• Adds coverage proving fake-backend session headers remain readable without migration or rejection after closing and reopening the store.

packages/storage/src/tests/session-store.test.ts

Documentation (8) +15 / -11
thread-search.tsDocument legacy fake-session search exclusion +8/-4

Document legacy fake-session search exclusion

• Clarifies that fake transcripts remain excluded because they contain retired simulator output, including seeded E2E fixtures.

apps/desktop/src/main/search/thread-search.ts

app-shell-session-start-actions.tsUpdate setup-error ownership documentation +1/-1

Update setup-error ownership documentation

• Points setup-required error handling at Runtime Host instead of the removed desktop chat-readiness module.

apps/desktop/src/renderer/app-shell-session-start-actions.ts

provider-connection-detail.tsxAlign unknown-provider documentation with readiness logic +1/-1

Align unknown-provider documentation with readiness logic

• Updates the fallback comment to reference 'isRealConnection' after removal of the fake-backend predicate.

apps/desktop/src/renderer/settings/provider-connection-detail.tsx

model-catalog.tsUpdate model-catalog readiness reference +1/-1

Update model-catalog readiness reference

• Documents unknown-provider filtering in terms of 'isRealConnection' rather than the removed fake-backend predicate.

packages/core/src/model-catalog.ts

provider-auth.tsUpdate provider-auth readiness reference +1/-1

Update provider-auth readiness reference

• Aligns unknown-provider fallback documentation with the new registered-provider readiness model.

packages/core/src/provider-auth.ts

model-fetcher.tsUpdate model-fetcher provider guard reference +1/-1

Update model-fetcher provider guard reference

• Documents unknown-provider discovery failures in terms of 'isRealConnection'.

packages/runtime/src/model-fetcher.ts

model-runtime.tsUpdate runtime adapter provider guard reference +1/-1

Update runtime adapter provider guard reference

• Aligns unknown-provider runtime resolution documentation with the new readiness predicate.

packages/runtime/src/model-runtime.ts

test-connection.tsUpdate connection-test readiness reference +1/-1

Update connection-test readiness reference

• Documents unknown-provider test failures using the replacement 'isRealConnection' terminology.

packages/runtime/src/test-connection.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2


ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 975a4dd6-1095-44b6-b539-813433b64b02

📥 Commits

Reviewing files that changed from the base of the PR and between 7bb776b and d8506e3.

📒 Files selected for processing (32)
  • apps/desktop/src/main/__tests__/chat-readiness.test.ts
  • apps/desktop/src/main/__tests__/pending-session-view.test.ts
  • apps/desktop/src/main/chat-readiness.ts
  • apps/desktop/src/main/main-window.ts
  • apps/desktop/src/main/search/thread-search.ts
  • apps/desktop/src/renderer/app-shell-session-start-actions.ts
  • apps/desktop/src/renderer/app-shell.tsx
  • apps/desktop/src/renderer/model-catalog-choices.ts
  • apps/desktop/src/renderer/pending-session-view.ts
  • apps/desktop/src/renderer/settings/provider-connection-detail.tsx
  • packages/core/src/__tests__/llm-connections.test.ts
  • packages/core/src/agent-run.ts
  • packages/core/src/backend-types.ts
  • packages/core/src/chat-model-choice.ts
  • packages/core/src/connection-readiness.ts
  • packages/core/src/llm-connections.ts
  • packages/core/src/model-catalog.ts
  • packages/core/src/provider-auth.ts
  • packages/core/src/runtime-inputs.ts
  • packages/core/src/scheduled-task.ts
  • packages/core/src/session-send-projection.ts
  • packages/core/src/session.ts
  • packages/core/src/workspace.ts
  • packages/runtime-host/src/protocol/scheduled-task.ts
  • packages/runtime-host/src/protocol/session-catalog.ts
  • packages/runtime/src/model-fetcher.ts
  • packages/runtime/src/model-runtime.ts
  • packages/runtime/src/session-manager.ts
  • packages/runtime/src/test-connection.ts
  • packages/runtime/src/test-only/fake-backend.ts
  • packages/storage/src/__tests__/session-store.test.ts
  • packages/storage/src/session-store.ts
💤 Files with no reviewable changes (2)
  • apps/desktop/src/main/tests/chat-readiness.test.ts
  • apps/desktop/src/main/chat-readiness.ts

Included review availability: Your plan provides up to 3 included reviews per hour; 1 remains after this review.

Comment thread packages/core/src/llm-connections.ts
Comment thread packages/core/src/runtime-inputs.ts Outdated
@qodo-code-review

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (3) 📘 Rule violations (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Automation mutations accept fake 🐞 Bug ≡ Correctness
Description
Fix-now: inbound scheduled-task create/update mutations use decodeExecution, whose renamed
persisted-kind guard still accepts fake, and core mutation normalization also accepts and persists
it. Clients can therefore write a new Automation that is guaranteed to fail only later at
activation, contradicting the claim that fake acceptance exists solely to decode frozen legacy
templates.
Code

packages/runtime-host/src/protocol/scheduled-task.ts[528]

+  if (!isPersistedBackendKind(execution.backend))
Relevance

●●● Strong

Protocol-boundary validation findings are consistently accepted, and this decoder feeds
create/update mutations rather than decode-only legacy reads.

PR-#3103
PR-#3079

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The protocol routes create and update requests through the same execution decoder that accepts the
persisted union; normalization and storage then preserve that effect unchanged, while activation has
only a refusal factory for fake.

packages/runtime-host/src/protocol/scheduled-task.ts[75-114]
packages/runtime-host/src/protocol/scheduled-task.ts[488-529]
packages/core/src/scheduled-task.ts[127-206]
packages/core/src/scheduled-task.ts[468-497]
packages/storage/src/scheduled-task-store.ts[231-281]
packages/runtime-host/src/server/execution-composition.ts[291-303]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The scheduled-task mutation decoder treats live create/update input as durable data and accepts `backend: 'fake'`, allowing newly written unrunnable Automations.

## Issue Context
Legacy stored/output templates must remain readable. Reusing one decoder for both read compatibility and mutation input is insufficient because those directions have different backend invariants; separate or parameterize the closest existing execution decoder rather than adding new runtime behavior, and keep the persisted union only on the durable/read side.

## Fix Focus Areas
- packages/runtime-host/src/protocol/scheduled-task.ts[103-114]
- packages/runtime-host/src/protocol/scheduled-task.ts[513-529]
- packages/core/src/scheduled-task.ts[127-206]
- packages/core/src/scheduled-task.ts[468-497]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. New sessions accept fake backend 🐞 Bug ≡ Correctness
Description
Disposition: fix-now. Widening the live writer contract CreateSessionInput.backend to
PersistedBackendKind allows callers to create and persist new 'fake' sessions, despite the
invariant that this legacy value is read-only for decoded durable records, after which activation
dispatches it through the registry and the Runtime Host can only refuse it as fake_backend rather
than rejecting it at the write boundary.
Code

packages/core/src/runtime-inputs.ts[38]

+  backend: PersistedBackendKind;
Relevance

●●● Strong

The PR explicitly documents writers must use BackendKind; permitting fake at the creation boundary
violates its stated live/durable separation.

PR-#3226

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The PersistedBackendKind documentation explicitly requires writers to use BackendKind, but the
changed creation contract permits 'fake' and forwards it unchanged into durable header
construction without validation. The newly added storage test demonstrates the prohibited write path
by passing backend: 'fake' to the normal store.create API and persisting that value.

packages/core/src/runtime-inputs.ts[28-40]
packages/runtime/src/session-manager.ts[948-953]
packages/storage/src/session-store.ts[928-978]
packages/core/src/session.ts[262-283]
packages/storage/src/tests/session-store.test.ts[849-868]
packages/runtime/src/session-manager.ts[772-781]
packages/storage/src/session-store.ts[389-400]
packages/storage/src/session-store.ts[928-975]
packages/storage/src/tests/session-store.test.ts[849-873]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description

`CreateSessionInput` is a live writer contract, but using `PersistedBackendKind` for its `backend` field allows callers to create and persist new sessions with the legacy-only `'fake'` value. Restrict creation to the existing live backend selection type so invalid sessions are rejected at the write boundary.

## Issue Context

Legacy session headers must remain decodable with `PersistedBackendKind`, but that persisted union is read-only and writers must use the existing `BackendKind` type. No new behavior or type is needed: update the creation contract accordingly, and change the storage test to seed a legacy serialized/header record below the writer boundary instead of calling `store.create` with `backend: 'fake'`.

## Fix Focus Areas

- packages/core/src/runtime-inputs.ts[5-40]
- packages/storage/src/session-store.ts[928-978]
- packages/storage/src/__tests__/session-store.test.ts[849-873]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Loading sessions show wrong model 🐞 Bug ≡ Correctness
Description
Disposition: fix-now. Whenever activeId is set before its existing SessionSummary is present,
the fallback declares that existing session to use the next-new-chat target; the enabled in-session
switcher uses those fabricated fields for current-choice comparison and can therefore submit an
unnecessary model change to the real session.
Code

apps/desktop/src/renderer/app-shell.tsx[R1263-1269]

+      ? pendingSessionView({
+          sessionId: activeId,
          name: shellCopy.newConversation,
-    isFlagged: false,
-    isArchived: false,
-    labels: [],
-    hasUnread: false,
-    status: 'active',
-    backend: 'fake',
-    llmConnectionSlug: 'default',
-    connectionLocked: false,
-    model: 'fake-model',
-    // Transient placeholder while the real SessionSummary loads --
-    // matches the configured default so the composer doesn't flash a
-    // hardcoded value before the real session data settles.
-    permissionMode: defaultPermissionMode,
-        }
+          permissionMode: defaultPermissionMode,
+          newChatModel,
+          defaultConnectionSlug: defaultConnection,
+        })
Relevance

●●● Strong

Recent session-switch race findings are accepted; this placeholder similarly fabricates state for a
different active session.

PR-#2523
PR-#3048

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
activeSessionForView takes this fallback solely from activeId being nonempty and activeSession
being absent. newChatModel is independently derived as the target for a new chat, but the model
switcher derives its current value and no-op decision from the fallback session fields, while the
mutation targets the active ID.

apps/desktop/src/renderer/app-shell.tsx[1258-1270]
apps/desktop/src/renderer/use-shell-chat-model.ts[107-121]
apps/desktop/src/renderer/app-shell.tsx[3141-3151]
packages/ui/src/chat-model-switcher.tsx[187-195]
packages/ui/src/chat-model-switcher.tsx[225-239]
apps/desktop/src/renderer/app-shell-session-settings-actions.ts[140-173]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The fallback is used for every active ID lacking a loaded summary, not only an explicit new-session creation. It assigns the next new chat's connection/model to that session, and the in-session model switcher treats this as the current persisted target.

## Issue Context
A placeholder for an unresolved existing session must not represent new-chat defaults as that session's configuration. Reuse the existing loading/pending seam to disable or withhold in-session model selection until the real summary arrives; only use `newChatModel` for an explicitly identified pending new-task surface.

## Fix Focus Areas
- apps/desktop/src/renderer/app-shell.tsx[1258-1270]
- apps/desktop/src/renderer/pending-session-view.ts[14-38]
- packages/ui/src/chat-model-switcher.tsx[187-195]
- packages/ui/src/chat-model-switcher.tsx[225-239]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context sources
Review mode: 🧠 Deep: This broad, behavior-changing refactor spans 32 files and 63 hunks, including durable decoding, runtime backend dispatch, readiness/rebinding, UI state, and a public API contract change, creating multiple independent opportunities for subtle regressions.

Grey Divider

Tip of the day
💡 Did you know, you can show, collapse, or hide each part of a finding: code, evidence, and all

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread packages/runtime-host/src/protocol/scheduled-task.ts Outdated
Comment thread packages/core/src/runtime-inputs.ts Outdated
Comment thread apps/desktop/src/renderer/app-shell.tsx
@Astro-Han

Copy link
Copy Markdown
Contributor Author

Qodo: all three verified, all three acted on.

1. Automation mutations accept fake — fixed (82a8ed0). Correct, and your reading of the decoder was sharper than the other review's: decodeEffect really does serve both decodeScheduledTask (stored) and create/update input, so parameterizing rather than narrowing was the right call. It now takes the direction — stored accepts the retired value, mutation refuses it. The core-side normalizeExecution turned out not to be a decoder at all (stored Automations are read back with JSON.parse and never reach it), so that one just refuses fake. The comments I had written on both were false and are gone.

2. New sessions accept fake — declined, with evidence. Narrowing CreateSessionInput.backend fails 102 typechecks, 5 of them in production: session-manager.ts (spawn subagent ×2, create revision, branch from turn) and session-revision-coordinator.ts (conversation copy). Each derives a new session from an existing one and inherits header.backend. Closing the write boundary first requires deciding what deriving from a legacy row should do — refusing needs five refusal semantics and copy; rewriting to ai-sdk is the mistake this PR argues against, since llmConnectionSlug still points at nothing. Recorded in the PR body as deferred rather than folded in. Your accompanying point about the test was right and is applied: it now seeds the legacy row below the writer instead of calling store.create({backend: 'fake'}).

3. Loading sessions show wrong model — you were right, and it reversed my previous commit. I had replaced model: 'fake-model' with the next-new-chat target, treating the literal as the bug. The literal was doing real work: it matched no offered choice, which is how the switcher spells "not known yet". Naming a plausible model made the no-op guard at chat-model-switcher.tsx:229 fire against a session that was never on it — a user switching onto that model gets the change silently dropped. The placeholder now carries an empty connection/model pair, keeping that property deliberately instead of by accident, with a test asserting it matches nothing offered. Withholding in-session selection until the summary lands is better still, but it belongs to the switcher's loading contract in @maka/ui, not to this cleanup.

Separately, tracing why the placeholder mattered surfaced something neither review flagged: shouldRebindSessionToDefault listed fake_backend, but nothing performs that rebind — the only executor had no caller and is deleted in this PR, and the two consumers of 'rebind' only use it to suppress a notice. That false promise is why the rail and the composer were reading session.backend directly instead of trusting the projection. Removing it (81a6b45) lets both drop the workaround.

@Astro-Han
Astro-Han force-pushed the refactor/3211-retire-fake-backend-kind branch 2 times, most recently from 0e37426 to 3d5cb0c Compare August 20, 2026 00:35
`BackendKind` was doing two jobs: naming the backend a build may select,
and typing the backend value read back from durable state. The in-process
FakeBackend stopped shipping in #3226, so the first job narrows to
`'ai-sdk'`; the second becomes `PersistedBackendKind`, which still admits
`'fake'` and now types the session header, the session-catalog wire
projection, the run header, the Automation template, the workspace
defaults, and the backend registry that dispatches off them.

No data migration. Sessions, runs and Automations written by builds that
shipped FakeBackend keep `'fake'` on disk: narrowing the decode guards
would make those rows read back as malformed, and rewriting them to
`'ai-sdk'` would make an unrunnable task look runnable, since their
`llmConnectionSlug` still points at nothing. Activation already refuses
them with the product's `fake_backend` reason.

`backendKindOf` no longer answers `'fake'` for an unrecognized
`providerType` — it throws. That fallback was the last live producer of
the value, and there is no honest backend to name for a provider this
build cannot describe; the non-throwing question ("can this connection be
used?") is `isRealConnection` / `isConnectionReady`. With no provider
declaring `'fake'`, the former `isFakeBackend` collapses to "is this
providerType one the build knows", so it is named for what it tests.

The send projection's dead `slug === 'fake'` branches go with it: a
session carrying that slug is refused one line earlier by its backend, and
a connection carrying it is refused by the readiness gate.

Also deletes `apps/desktop/src/main/chat-readiness.ts`. Its send gate —
`requireReadyConnection`, `assertSessionCanSend`,
`ensureSessionCanSendOrRebind`, and a third copy of the Chinese connection
error copy — had no caller left once the gate moved to Runtime Host; the
only live import was the one-line `errorMessage` helper, now inlined at
its single use site.

Part of #3211

Generated-by: Claude Code
The placeholder `SessionSummary` the chat view shows between "a session id
became active" and "its real summary arrived" was hardcoded to
`backend: 'fake'` / `model: 'fake-model'` / `llmConnectionSlug: 'default'`,
directly above a comment claiming it matched the configured default.

It did not. The composer reads those fields straight off this object — the
model switcher's current value and the quote companion's inherited-model
line — so the switcher matched no offered choice and the companion named a
model no session ever had. The `'fake'` there was also the last live write
of the retired backend value in product code, standing in for "not loaded
yet".

The placeholder now carries what the next new task would start on, falling
back to the default connection slug and then to no model, which is what
the comment always claimed. Extracted to `pending-session-view.ts` so the
shape is testable outside the shell component.

Closes #3211

Generated-by: Claude Code
`PROVIDER_DEFAULTS` is an object literal, so `PROVIDER_DEFAULTS[providerType]`
resolves inherited members: `'__proto__'`, `'toString'` and `'constructor'` all
read back truthy and pass as registered providers.

Every recognition site was doing that lookup itself, and two of them had been
holding the leak closed by accident. `backendKindOf` ended `?? 'fake'`, and the
model-choice gates tested `backendKind !== 'ai-sdk'` — both fell through to
"not usable" for an inherited member. Narrowing those in the previous commit
removed the accident without replacing it: `backendKindOf` began returning
`undefined` typed as `BackendKind`, and an inherited member started reading as
a model-consumer connection. Calling that check tautological was wrong; it was
tautological in the type system only.

`providerDefaultsOf` now owns the question and answers it with `Object.hasOwn`.
The connection-catalog codec's `PROVIDER_TYPES` set — until now the only site
that got this right, via `Object.keys` — folds into it, so provider recognition
has one implementation rather than a correct one and several approximations.

Part of #3211

Generated-by: Claude Code
`shouldRebindSessionToDefault` listed `fake_backend`, so an unlocked session on
the retired backend projected as `{kind:'rebind'}`. Nothing performs that
rebind — the only executor was `ensureSessionCanSendOrRebind`, which had no
caller and is now deleted, and neither Runtime nor Runtime Host has an
equivalent. The two consumers of `'rebind'` only use it to suppress a notice.

It could not have worked anyway. Every other reason on that list names a broken
*connection*, which another connection can stand in for. A retired backend is
not: activation dispatches off the session header's own `backend`, so pointing
the session at a healthy connection still leaves `'fake'` in the header and
still gets refused.

The cost was paid elsewhere. Because the projection said "recoverable" for rows
that are not, the surfaces that must answer "is this task usable?" bypassed it
and read `session.backend` themselves — the rail's stale marker and the
composer's connection/model labels. Removing `fake_backend` from the list makes
the projection answer `blocked` for these rows, and both surfaces drop their
workaround and read the reason like any other. The
`'任务已过期 · 请先配置真实模型'` notice, written long ago and suppressed for
unlocked rows ever since, now shows.

Part of #3211

Generated-by: Claude Code
…t know

The previous commit replaced the placeholder's `model: 'fake-model'` with the
connection and model the next new task would start on. That was wrong in a way
the literal was not.

This fallback covers every active id whose summary has not arrived, not only a
freshly created task, so the session behind it is usually an existing one bound
to some other model. Naming the new-chat default made the composer assert that
configuration as the session's own: the model switcher shows it as current, and
its no-op guard compares against it — so a user switching onto that very model
has the change silently dropped against a session that was never on it.

`'fake-model'` avoided this by accident: it matched no offered choice, which is
exactly how the switcher spells "not known yet". The placeholder keeps that
property and states it deliberately, with an empty connection/model pair, and
without borrowing a retired backend name to mean "not loaded".

Withholding in-session model selection entirely until the summary lands is the
better answer, but it belongs to the switcher's own loading contract in
`@maka/ui`, not to this cleanup.

Part of #3211

Generated-by: Claude Code
…ivation

Two validators accepted `'fake'` under a comment claiming they were decoders
keeping frozen Automations readable. Neither is a decoder.

`normalizeExecution` in core is reached only from
`normalizeCreateScheduledTaskInput` / `normalizeUpdateScheduledTaskInput`;
stored Automations are read back with `JSON.parse` in `scheduled-task-store.ts`
and never pass through it. It now refuses `'fake'` outright.

The protocol's `decodeExecution` genuinely serves both directions — reading a
stored task and validating an inbound create/update — but those carry different
backend invariants, so one shared answer had to be wrong for one of them. It
takes the direction as an argument: `'stored'` still accepts the retired value,
`'mutation'` does not. Without this a client could write a brand new Automation
guaranteed to fail later at activation.

The storage regression test seeded its legacy row through `store.create`, which
is the write path this is closing. It now writes the row underneath the store,
which is also the only way such a row was ever produced — and the test fixture
stops defaulting every session in the file to the retired backend, so narrowing
the header guard fails one named test instead of all seventeen.

Part of #3211

Generated-by: Claude Code
@Astro-Han
Astro-Han force-pushed the refactor/3211-retire-fake-backend-kind branch from 3d5cb0c to 0d7048c Compare August 20, 2026 00:53
@Astro-Han

Copy link
Copy Markdown
Contributor Author

@CxHsin @me2seeks — no obligation, but I'd value your eyes on this one if you have time, since it lands right on top of work you each own.

@CxHsin: #3096 made TurnOrigin a single authority in runtime-inputs.ts; this PR applies the same move to the file's backend field, except by deleting it rather than narrowing it — a live build has exactly one backend, so the field carried no choice, only the chance of writing the retired 'fake'. The store stamps every new header instead. If that reads as the wrong shape of "single authority" to you, I'd rather hear it now.

@me2seeks: two things touch your ground. runtimeToolBoundaryProtocol in runtime-kernel.ts withheld the T1 durable tool boundary from non-ai-sdk headers; I removed the gate on the argument that activation refuses those headers before the kernel ever sees them (execution-composition.ts). And in app-shell.tsx, the pending-chat placeholder now carries an empty connection/model pair instead of borrowing a retired backend. Both are load-bearing claims about code you know better than I do.

Context is in the PR body and the per-commit messages. Reviews from committers are already requested, so treat this purely as an invitation, not a queue.

中文版

@CxHsin @me2seeks —— 没有任何义务,但这个 PR 正好压在你们各自的地盘上,有空的话我很希望听听你们的意见。

@CxHsin#3096runtime-inputs.ts 里的 TurnOrigin 收成了单一权威;这个 PR 对同一个文件的 backend 字段做了同样的事,只不过是删掉而不是收窄——一个 live build 只有一个 backend,这个字段本来就不承载任何选择,只承载写入已退役的 'fake' 的机会。改由 store 给每个新 header 盖章。如果你觉得这不是「单一权威」该有的形状,我更希望现在就听到。

@me2seeks:有两处落在你熟悉的地方。runtime-kernel.ts 里的 runtimeToolBoundaryProtocol 会对非 ai-sdk 的 header 扣下 T1 持久化工具边界;我把这个 gate 删了,理由是这类 header 在进 kernel 之前就已经被 activation 拒绝(见 execution-composition.ts)。另外 app-shell.tsx 里待建会话的占位符现在带的是一对空的 connection/model,而不再借用一个已退役的 backend。这两条都是承重的判断,而那些代码你比我熟。

背景在 PR 正文和各个 commit message 里。committer 的 review 已经另外请了,所以这条纯粹是邀请,不是排队。

@me2seeks

Copy link
Copy Markdown
Contributor

Both claims check out, with one correction to the framing.

T1 gate: safe to remove, but not because "activation refuses those headers before the kernel sees them." SessionManager.sendMessagestartTurn has no backend-kind check; the kernel does see a legacy 'fake' header, and AgentRun.begin() durably persists the run record, user message, turn state, and the connectionLocked flip before the refusal fires inside the reserveRun hook (ensureActivebackends.build('fake')). The gate was therefore reachable — what it withheld was the protocol marker on the doomed run's initial event (and the continuation-start event on the continuation path), which post-PR is now persisted with the marker. That's still safe under the stronger invariant: no run on a 'fake' header ever reaches tool dispatch, and every marker consumer I traced — recovery-resolver's per-run scan, the read-model projection skip, continuationStartMatchesClaim, and the continuation-admission V1 check — treats a marked run with zero tool operations identically to an unmarked one. The marker's semantics ("which contracts were live from the run's first event") stay truthful for a run that dies at reserveRun. So: endorse the removal; suggest fixing the "before the kernel sees them" wording, since it papers over the (pre-existing) durable litter a doomed send leaves behind.

Placeholder: endorse. The placeholder's backend/connection/model fields have exactly one consumer — the switcher's current-value and no-op comparison. modelChoiceValue('', '') is ':', which no offered choice can produce and parseModelChoiceValue rejects, so the no-op guard can never swallow a deliberate pick. Health notice, send projection, and the setSessionModel mutation are all keyed on the real session; the placeholder is view-only. The empty chip during the load window beats flashing fake-model.

One housekeeping note: your reply to Qodo's finding #2 says "declined, deferred" — but 0d7048cf0 later deleted CreateSessionInput.backend outright, which is stronger than the narrowing Qodo asked for. Might be worth a follow-up line in that thread so the comment record matches the final state.

中文版

两条判断都成立,但第一条的论证措辞需要修正。

T1 gate: 可以删,但理由不是「activation 在 kernel 见到这些 header 之前就拒绝了它们」。SessionManager.sendMessagestartTurn 之间没有 backend 检查;kernel 确实会看到遗留的 'fake' header,而且 AgentRun.begin() 在拒绝触发(reserveRun 钩 → ensureActivebackends.build('fake'))之前就已经持久化了 run 记录、用户消息、turn state 和 connectionLocked 翻转。所以这个 gate 并非不可达——它扣下的是注定失败的 run 的 initial event(以及 continuation 路径的 continuation-start event)上的协议标记,而这个 PR 之后这些事件会带着标记落盘。删除依然安全,因为更强的不变量成立:'fake' header 的 run 永远到不了工具分发,而我追踪的每个标记消费者——recovery-resolver 的按 run 扫描、read-model 投影跳过、continuationStartMatchesClaim、continuation admission 的 V1 检查——对一个带标记但零工具操作的 run 和不带标记的 run 处理完全一致。标记的语义(「从 run 的第一个事件起哪些契约生效」)对一个死在 reserveRun 的 run 依然为真。所以:认可删除;但建议修正「before the kernel sees them」的措辞,因为它掩盖了注定失败的发送所留下的(既有的)持久化残留。

占位符: 认可。占位符的 backend/connection/model 字段只有一个消费者——切换器的 current-value 和 no-op 比较。modelChoiceValue('', '')':',任何真实选项都产生不了这个值,parseModelChoiceValue 也会拒绝它,所以 no-op 守卫永远不可能吞掉用户的真实切换。健康提示、发送投影和 setSessionModel 变更都 keyed on 真实 session;占位符是纯视图。加载窗口期显示空芯片,比闪一下 fake-model 好。

一件收尾的事:你对 Qodo 第二条 finding 的回复说「declined, deferred」——但 0d7048cf0 后来把 CreateSessionInput.backend 整个删了,这比 Qodo 要求的收窄更强。建议在那个线程里补一句,让评论记录和最终状态一致。

@M4n5ter M4n5ter left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

English

LGTM — I found no blocking correctness or design issues.

Two non-blocking follow-ups may be worth considering separately:

  • SessionHeaderPatch still inherits backend from SessionHeader, so trusted internal code using SessionStore.updateHeader() can technically write the retired fake value. Current product protocols expose closed patch shapes and no existing caller updates this field, so this is not reachable through a supported user flow. A future cleanup could make backend immutable in the patch type and reject it at the storage writer boundary.

  • ScheduledTaskExecutionTemplate.backend remains durable state, but the fire path does not use it when creating the execution session. This behavior predates this PR, and normal product flows did not create fake Automations, so it should not block this change. Longer term, the field should either be enforced at fire time—rejecting retired legacy values—or removed from new canonical writes while remaining tolerated by the legacy decoder.

The PR otherwise achieves its goal: fake is removed from the live backend surface, new retired-backend writes are closed through supported paths, and legacy durable records remain readable and fail activation through the existing product reason.

中文

LGTM——没有发现需要阻塞合并的正确性或设计问题。

有两个非阻塞事项可以后续单独处理:

  • SessionHeaderPatch 仍从 SessionHeader 继承了 backend,因此受信任的内部代码理论上可以通过 SessionStore.updateHeader() 写入已经退役的 fake。目前产品协议使用封闭的 patch 结构,也没有现有调用方更新该字段,因此正常用户路径无法触发。后续可以在 patch 类型中将 backend 设为不可变,并在 storage writer 边界拒绝该字段。

  • ScheduledTaskExecutionTemplate.backend 仍会被持久化,但任务触发路径在创建执行 Session 时并不读取它。该行为早于本 PR,正常产品流程也不会创建 fake Automation,因此不应阻塞本次修改。长期来看,应当选择一种明确语义:在触发时校验该字段并拒绝退役值,或者停止在新记录中写入它,仅由 legacy decoder 容忍历史字段。

除此之外,本 PR 已实现其核心目标:从 live backend surface 移除 fake,关闭受支持路径中的新退役值写入,同时保持历史持久化记录可读取,并通过现有产品原因拒绝激活。

@Astro-Han

Copy link
Copy Markdown
Contributor Author

Thanks — both verified, both left out of this PR.

SessionHeaderPatch.backend — right about updateHeader(), but a second writer sets it deliberately: configuration.backend is spread into a header patch by setExecutionBoundaryKindSync, hardcoded 'ai-sdk' at session-catalog-coordinator.ts:468, and the no-op guard above it requires header.backend === 'ai-sdk' to skip the commit. So reconfiguring a legacy 'fake' row rewrites it to 'ai-sdk' — and since the model switcher is not disabled for those rows, that is today the only way such a session becomes runnable. backend?: never would delete that recovery, not close a dead hole. Worth deciding first whether recovery or "start a new task" (what the copy says) is the product answer, then giving that one writer a named seam.

ScheduledTaskExecutionTemplate.backend — agreed, and stronger than stated: after this PR it cannot be read, since #createSession builds a SessionCreateInput that has no backend field. One write path is still open — executionTemplateFromHeader copies header.backend without passing normalizeExecution — unreachable in practice (the creating tool runs inside a turn; a 'fake' session cannot start one). Dropping it from new canonical writes while the decoder keeps tolerating stored ones is the right shape, but that is a durable-record and wire-contract change and wants its own PR.

中文

两条都查证了,都不进本 PR。

SessionHeaderPatch.backend —— updateHeader() 那半你说得对,但还有第二个写入方是刻意的:configuration.backendsetExecutionBoundaryKindSync 展开进 header patch,session-catalog-coordinator.ts:468 硬编码 'ai-sdk',而上面的 no-op 判断要求 header.backend === 'ai-sdk' 才跳过提交。于是重新配置一个 legacy 'fake' 行就会把它改写成 'ai-sdk'——而模型切换器对这类行并未禁用,这是目前唯一能让这种会话重新可用的路径。backend?: never 删掉的是这条恢复路径,不是一个死洞。应当先决定「恢复」还是文案所说的「新建任务」才是产品答案,再给那个唯一的写入方一个具名入口。

ScheduledTaskExecutionTemplate.backend —— 同意,而且比你说的更强:本 PR 之后它读不到了,因为 #createSession 构造的 SessionCreateInput 已经没有 backend 字段。写入侧还开着一处——executionTemplateFromHeader 直接抄 header.backend,不经过 normalizeExecution——实际不可达(创建工具跑在 turn 内,'fake' 会话开不了 turn)。停止在新记录中写入、由解码器继续容忍历史字段是正确形状,但那是持久记录与 wire 契约变更,应当单独开 PR。

🤖 Drafted with Claude Code

@CxHsin

CxHsin commented Aug 20, 2026

Copy link
Copy Markdown
Contributor
English

I think this direction is sound. In the current live build, backend is no longer a meaningful runtime choice for callers, so removing it from CreateSessionInput and having the store consistently write 'ai-sdk' better reflects the “single authority” principle than retaining a field that can only contain one valid value.

The write model and persisted model should remain distinct, however. Existing data may still contain 'fake', so durable headers must continue to read and recognize it rather than being narrowed to 'ai-sdk' outright. I suggest keeping PersistedBackendKind = BackendKind | 'fake' and rejecting legacy 'fake' at activation with the product-level reason.

In short, I support removing caller-facing backend, but not removing 'fake' from the persisted/read compatibility layer.

中文

我认为这个方向是合理的。当前线上构建中,backend 已经不再是调用方可选择的运行时配置,因此从 CreateSessionInput 中删除它,并由 store 统一为新 session 写入 'ai-sdk',比保留一个实际上只能填写固定值的字段更符合“single authority”原则。

不过,写入模型和持久化模型仍应保持区分。旧数据中可能仍然包含 'fake',因此 durable header 应继续读取并识别它,而不能直接收窄为只接受 'ai-sdk'。建议保留 PersistedBackendKind = BackendKind | 'fake',并在 activation 阶段使用产品级原因拒绝 legacy 'fake'

简而言之,我支持删除面向调用方的 backend,但不支持删除持久化/读取兼容层中的 'fake'

`CreateSessionInput.backend` was the last live path that could write the
retired `'fake'` into a new session. Narrowing it to `BackendKind` would only
move the question: four derivation sites in `session-manager.ts` plus the
conversation copy in `session-revision-coordinator.ts` inherit the source
header's backend, so each would need an answer for what deriving from a legacy
row means.

Deleting the field dissolves that question. A live build has exactly one
backend, so the field carried no choice — the store stamps `'ai-sdk'` on every
new header and no caller names a backend at all. A session derived from a
legacy row is now a real session whose connection slug resolves to nothing,
which is what the readiness projection already says about that row.

Two backend-kind guards go with it, for separate reasons:

- `runtimeToolBoundaryProtocol` withheld the T1 durable tool boundary from
  non-`ai-sdk` headers. The branch was reachable: nothing checks the backend
  kind between `sendMessage` and the kernel, and `AgentRun.begin()` persists the
  run record, user message, turn state and the `connectionLocked` flip before
  the refusal fires inside `reserveRun`. What the gate withheld was only the
  protocol marker on the initial event of a run that dies before tool dispatch,
  and every marker consumer treats a marked run with zero tool operations the
  same as an unmarked one. The marker stays truthful; the gate bought nothing.
- `sessions:create` rejected a non-`ai-sdk` backend arriving over IPC. That
  one really is unrepresentable now: the field is gone from the request type.

Tests that dispatched through a `'fake'` registry key now register the live
one, which is how they would be written today. The activation-refusal
regression test seeded its legacy row through `createSession({backend:
'fake'})` — the very write this removes — and now seeds it under the writer,
which is the only way such a row was ever produced.

Generated-by: Claude Code
@Astro-Han
Astro-Han force-pushed the refactor/3211-retire-fake-backend-kind branch from 0d7048c to 6295359 Compare August 20, 2026 10:09
@Astro-Han

Copy link
Copy Markdown
Contributor Author

@me2seeks — correction accepted, and verified: AgentRun.begin() writes the run record, user message, turn state and the connectionLocked flip before reserveRun ever reaches backends.build('fake'). "Before the kernel sees them" was wrong. Commit message and PR body now state the actual invariant and credit the correction. Thanks for tracing the four marker consumers — that's the part I hadn't done.

On Qodo #2: already reversed. 3815770497 in that thread says the decline was wrong and points at the deletion; the thread is resolved, which collapses it, so the stale "declined" comment is what stays visible.

@CxHsin — that is exactly the split shipped: PersistedBackendKind = BackendKind | 'fake' still types every durable record, legacy rows keep 'fake' on disk unrewritten, and activation refuses them with the fake_backend product reason. No migration.

中文

@me2seeks —— 纠正接受,我也核对过:AgentRun.begin()reserveRun 走到 backends.build('fake') 之前,就已经写下了 run 记录、user message、turn state 和 connectionLocked 翻转。「before the kernel sees them」是错的。commit message 和 PR 正文已改为陈述真正的不变量,并注明了这处更正。谢谢你把四个 marker 消费者都追了一遍——那部分我没做。

关于 Qodo 第二条:其实已经反转过了。那个线程里的 3815770497 说明当初的拒绝是错的,并指向了最终的删除;线程已 resolve 会折叠,所以留在外面可见的是那条过时的「declined」。

@CxHsin —— 这正是本 PR 采取的切分:PersistedBackendKind = BackendKind | 'fake' 仍然为每一条持久记录定型,旧数据的 'fake' 原样留在磁盘上不做改写,activation 用 fake_backend 这个产品级原因拒绝它们。不做迁移。

🤖 Drafted with Claude Code

@Astro-Han
Astro-Han merged commit 8de1f29 into main Aug 20, 2026
2 checks passed
@Astro-Han
Astro-Han deleted the refactor/3211-retire-fake-backend-kind branch August 20, 2026 10:22
Astro-Han added a commit to Astro-Han/maka-agent that referenced this pull request Aug 20, 2026
…tests

apache#3249 removed `backend` from `CreateSessionInput`, so the four Session
fixtures this branch adds no longer compile against `main`. The field carried
no choice for a live build, and the store stamps every new header itself, so
the fixtures need nothing in its place.

Generated-by: Claude Code
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

test(release): FakeBackend is a first-class BackendKind and desktop E2E material ships in release artifacts

4 participants