Skip to content

refactor(desktop): own Conversation publication and observation - #5869

Merged
Astro-Han merged 2 commits into
apache:mainfrom
chihumyum:refactor/conversation-owner
Sep 30, 2026
Merged

Astro-Han merged 2 commits into
apache:mainfrom
chihumyum:refactor/conversation-owner

Conversation

@chihumyum

@chihumyum chihumyum commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Refs #4582 — R2 M2 / C, based on d6876d708fdb8dc0bf35fd8e194ada4d69c92599 after #5850 and #5851.

AppShell currently owns transcript publication, observation resources and the messages threaded into its children. This moves those responsibilities into a persistent ConversationProvider and ConversationLifecycle. Actual transcript, pending-message, usage and approval readers subscribe below that owner. An ordinary durable-message update with unchanged chrome/pending/usage projections no longer renders Shell or Composer in the production-component harness.

  • Keep Catalog as the requested-selection authority. Publish Session, rows and range together; reject obsolete handoffs, seed completions and errors. Transcript open, seed/retry, publication and disposal share one lifecycle with injected Desktop services.
  • Replace root publication setters, range refs and the full Session UI controller with explicit target/chrome reads and semantic commands. Register both construction/observation owners and seal their private modules with the existing architecture checker.
  • Preserve the persistent Composer independently of conditional transcript mounting, live-to-durable handoff, shared-session identities and all retained live Turns. AppShell's stateful hook inventory decreases from 54 to 44 call sites (32 to 29 hook names).

Catalog readers filter rail-only bookkeeping while still observing status/profile changes. Reading-position failures retain their original localized action-failed copy and desktop diagnostic scope. Transcript read/refresh copy has one production source and direct tests; consumer Session refs are readonly, and unused bootstrap/ShellRun forwarding modules are removed.

No intended product, visual, shortcut, IPC or storage change. The render evidence establishes notification isolation for the tested path, not elapsed-time or full-app performance gains.

Verification

Local checks passed with Node 24.19.0 / npm 11.19.0:

  • Desktop production build and all Desktop typechecks, including stories.
  • Desktop tests: 3,078 passed; UI tests: 694 passed.
  • Architecture/hook-gate tests: 147 passed; strict architecture check against upstream/main and AppShell hook gate passed.
  • Follow-up regression on 7fb2baaa3a236626451fc84772ae82dd36206916, after rebuilding: all 34 Electron tests passed (20 files, 3.7 minutes, one worker, no retries). This includes streaming/remount recovery, local-message persistence across renderer/application restart, unavailable live endpoint with cached history, history paging, references, draft focus, /compact, Workbar/Terminal/Side Chat lifetimes and WorkHub native window reconstruction.
  • Re-ran 49 focused owner/error-copy/bootstrap/ShellRun/reading-position tests against rebuilt output: all passed. Four new assertions first reproduced lifecycle render churn and incorrect restore copy on the pre-fix implementation, then passed after the fix; production read/refresh copy is covered in all three locales.
  • Lint, format, Desktop/UI scoped Knip, locale hygiene, ASF headers, Astryx and Windows inventories, and git diff --check passed.

New tests mount the production owner/readers and exercise notification isolation, persistent Composer and observer lifetimes, A→B→C handoff, stale results, seed-before-transcript ordering, retry and cleanup. The fake transcript rejects snapshots before its first batch, matching the Desktop adapter.

Windows native interaction and real-account acceptance were not run locally.

Integration and review focus

The current assumptions remain Catalog-owned selection, the existing Main/preload Host observer, distinct requested/published/owner identities, and one Conversation workspace per renderer. See the feature README for public capabilities, retained transitional consumers and redesign triggers. Shared observation across surfaces or a relocated Host observer requires revisiting this lifecycle seam.

  • D / refactor(desktop): move Composer staging into a persistent owner #5868: staging can remain under its persistent Composer owner. C wraps the existing Composer and transcript leaves, preserving D's inner readers. The second PR to land must rebase shared AppShell/public-entry/test changes and regenerate architecture/Astryx inventories from merged source. Readiness, revision/submission and delivery recovery are subsequent M3 work.
  • perf(desktop): bound transcript work on session switches #5712: integrate bounded history through the injected transcript controller and private reading lifecycle. Do not restore a Shell-owned range controller. Invocation-time Copy/Save/revision reads still mean the published range; this PR does not implement full-history export or history-window redesign.

Review gates

  • Hosted CI passes on the updated PR head 7fb2baaa3a236626451fc84772ae82dd36206916. The previous implementation run passed on 7524313ab; it does not validate this follow-up commit.
  • Independent human review, including C/D integration and the publication/lifecycle boundary, is complete.

AI use

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

Tool(s) and scope: OpenAI Codex implemented the refactor, tests and documentation and ran the local verification. The commit includes Generated-by: OpenAI Codex; retain it in the final squash commit. Final review and merge remain human decisions.

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

Move transcript publication, observation resources and actual readers below the Conversation owner. Keep AppShell on explicit target, chrome and semantic command capabilities while preserving the persistent Composer.

Generated-by: OpenAI Codex
@github-actions github-actions Bot added the effort/XXL Over 2500 readable lines label Sep 30, 2026
@chihumyum
chihumyum marked this pull request as ready for review September 30, 2026 07:43

@hqhq1025 hqhq1025 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Reviewed exact head 7524313ab3ff7b47844a5a4e4dd1750d7341fd0a. I found no substantiated P0–P3 issue in the publication and observation paths I inspected.

This refactor moves transcript publication and its reader identity into the Conversation workspace (apps/desktop/src/renderer/features/conversation/model/conversation-workspace.ts:44-84,142-149). ConversationLifecycle owns observation, reading position, and live-to-durable reconciliation (apps/desktop/src/renderer/features/conversation/ui/conversation-lifecycle.tsx:108-152); the observation effect rejects late handoffs, reuses the reader across subscription retries, and closes it on retirement (apps/desktop/src/renderer/features/conversation/controller/use-conversation-observation.ts:113-192). Transcript, Composer, and cross-feature message readers subscribe beneath the owner rather than receiving message arrays through AppShell (apps/desktop/src/renderer/features/conversation/ui/conversation-readers.tsx:35-92). No storage schema, Host protocol, or IPC contract changed in this PR.

I checked A→B→C selection/publication, seed and error fencing, observation disposal/retry, transient-message projection, invocation-time Copy/Save reads, and the persistent Composer boundary. Local Node 24 build:test, Desktop typecheck (including stories), 87 focused Desktop tests, 121 architecture tests, and the AppShell hook gate passed. Current-head hosted test/label checks passed; merge-tree against fresh main 5ac266b1 and git diff --check are clean. I did not independently run the packaged Electron lifecycle, real-account acceptance, or the full Desktop/UI suites, and I did not prove the claimed render isolation outside the focused production-component harness. This 62-file refactor still needs independent human integration review; GitHub reports REVIEW_REQUIRED.

Automated review notice: This comment was posted by an automated review agent operated by hqhq1025. It is not an independent human review and does not replace one.

@Astro-Han Astro-Han left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Reviewed at 7524313a, comparing moved logic against main: event subscription/retry/backoff and teardown order on switch/close, shell-run sync, event-stream health polling (incl. visibility gating), pending interaction load/subscribe, live-content seed generations and reveal, the 1000 ms streaming-settle fallback, refresh/retry commands, transcript publication, provider placement, and the always-mounted lifecycle. Behavior is preserved; the new drop of buffered display events on observation end is documented and safe given Host replay on resubscribe. Hook gate, renderer architecture guard (121/121), renderer + stories tsc, and 239 focused desktop tests pass locally.

No P0–P2. P3s inline, plus:

  • publishedSession (model/conversation-workspace.ts:125) is a frozen getter-only object but is passed as activeIdRef (use-conversation-target.ts:56) into factories typed with a writable .current; a future write would compile and throw at runtime. Consider a Readonly ref type.
  • renderer/bootstrap-selection-lease.ts and renderer/shell-run-update-state.ts re-exports are now imported only by their own tests, while the README says no compatibility layer is kept.

Not verified: packaged Electron, Host reconnect and profile handoff; render-count impact of the first inline note.

Automated review notice: This comment was posted by an automated review agent. It is not an independent human review and does not replace one.

const view = useSyncExternalStore(workspace.publication.subscribe, workspace.publication.getSnapshot);
const catalog = useSessionCatalogController();
const requestedId = useExternalStoreSelector(catalog, selectActiveSessionId);
const requested = useExternalStoreSelector(catalog, selectSessionById, requestedId);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P3: These two catalog row reads no longer pass shellSessionRowEqual, so the lifecycle re-renders on every row update (e.g. activityAt/preview text while streaming), rebuilding handlers and a new frame scheduler each time. Subscriptions are id-keyed so it's perf-only, but passing the equality (or selecting just the needed fields) would restore the old behavior.

searchTarget={props.searchTarget} clearSearchTarget={props.clearSearchTarget} sessionUi={ui}
landmarkSessionId={displayed?.shared || displayed?.localState === 'pending' ? null : displayed?.id ?? null}
listTurnLandmarks={props.listTurnLandmarks} setTurnIndex={workspace.setTurnIndex}
onRestoreError={(error, sessionId) => ui.setMessageLoadErrorBySession((current) => ({ ...current, [sessionId]: transcriptErrorMessage(error, uiLocale, 'read') }))}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P3: For an unrecognised error while restoring the reading position, this used to surface the generic operation-failed copy via localizedShellErrorMessage under the desktop scope; it now shows "Task content is temporarily unavailable" under message-read. If intended, worth a note and a test; otherwise keep the previous mapping.


import type { UiCatalog, UiLocale } from '@maka/core/ui-locale';
import { classifiedErrorFallback } from './operation-diagnostics.js';
const COPY = {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P3: This duplicates the read/refresh strings from shell-copy. The old messageReadErrorMessage / messageRefreshErrorMessage are now only called from expected-error-presentation.test.ts, so the test pins the unused helpers while the production path here is untested and the two copies can drift. Suggest removing the old helpers and pointing the test at this module.

Filter Catalog bookkeeping at the Conversation lifecycle, preserve restore-error copy and diagnostic scope, and test production transcript copy directly. Mark Session identity consumers readonly and remove unused legacy forwarding modules.

Generated-by: OpenAI Codex

@hqhq1025 hqhq1025 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Reviewed the current Conversation ownership follow-up. The lifecycle now compares requested and displayed catalog rows by the fields it actually consumes (apps/desktop/src/renderer/features/conversation/ui/conversation-lifecycle.tsx:54-63), avoiding event-rate rail bookkeeping renders while still reacting to status and profile changes. Restore errors use the Desktop action fallback rather than the transcript-read fallback (apps/desktop/src/renderer/application/contracts/transcript-copy.ts:26-31); read and refresh retain their prior locale-specific copy and diagnostic scopes. The removed shell files are re-export shims. I found no substantiated P0–P3 issue in the inspected increment.

Node 24 Desktop main build, renderer typecheck, 19 focused ownership/error tests, 121 architecture checker fixtures, and the architecture guard pass. The current-head hosted test check, fresh-main merge-tree, and diff check are clean. I did not run packaged Electron, real account/session switching, or the full UI suite.

Automated review notice: This comment was posted by an automated review agent operated by hqhq1025. It is not an independent human review and does not replace one.

@Astro-Han

Copy link
Copy Markdown
Contributor

Thanks @chihumyum — moving publication and observation out of AppShell into a sealed Conversation owner is a big clarity win, and the follow-up on the review notes was quick and precise. Merging now.

@Astro-Han Astro-Han left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Approved at @Astro-Han's explicit request: two independent automated reviews of this head found no blocking issues, and CI is green.

@Astro-Han
Astro-Han merged commit 9961cd5 into apache:main Sep 30, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/XXL Over 2500 readable lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants