Skip to content

ai(project): bind asynchronous image and interview side effects to originating project #708

Description

@qnbs

Context

Follow-up from #704 Wave 0B / PR #707.

PR #707 correctly closed the known cross-project mutation races in character/world profile generation, field regeneration, template application, outline generation/application, and snapshot restore. Review convergence also added same-ID project-incarnation tracking, fail-closed identity comparison, stale-outline-preview rejection, and regression coverage.

A final post-merge completeness sweep shows that #707 did not modify the remaining asynchronous AI paths below. They therefore still require an explicit mutation-authority audit rather than being implicitly considered closed by Wave 0B.

Residual surfaces to audit

1. Character portrait generation / refinement

generateCharacterPortraitThunk performs asynchronous AI image generation and then calls:

await storageService.saveImage(characterId, base64)

The thunk later fulfills with { characterId }, and projectSlice applies hasAvatar: true to the then-current project state.

Audit both portrait entry points in useCharacterView:

  • initial portrait generation;
  • portrait refinement.

2. World image generation / refinement

generateWorldImageThunk follows the analogous pattern:

await storageService.saveImage(worldId, base64)

The fulfilled reducer updates hasAmbianceImage in the then-current project state.

Audit both world-image entry points in useWorldView:

  • initial image generation;
  • image refinement.

3. Asset-storage ownership

storageService.saveImage(id, base64Data) is keyed by entity id and does not carry an explicit project id at this boundary.

Determine, for both IndexedDB and filesystem backends, whether a late image result originating under project A can overwrite, alias, or otherwise affect an asset later observed under project B when entity ids collide or project state changes while generation is in flight.

Do not assume Redux protection alone proves asset safety; prove the storage ownership semantics separately.

4. Character interview streaming

Audit the project/streamInterviewChunk path and its streaming producer(s). The reducer resolves characterId / interviewId against the currently active state.data.characterInterviews.

Determine whether a stream originating in project A can continue dispatching chunks after project B becomes active and mutate project-B-owned interview state.

Required invariant

For every asynchronous AI operation or streamed chunk that can mutate project-owned state or project-owned assets:

origin = project A
        ↓
async work / streaming
        ↓
active project becomes B
        ↓
late result/chunk MUST NOT mutate B
late asset write MUST NOT become owned/visible as B's asset

Use the shared project-identity authority introduced by #707 where appropriate. A request/generation token may supplement identity checks but must not replace originating-project identity.

Acceptance criteria

  • Character portrait generation cannot apply state or asset side effects to a different project after a project switch/reset/import/restore.
  • Character portrait refinement has the same guarantee.
  • World image generation cannot apply state or asset side effects to a different project after a project switch/reset/import/restore.
  • World image refinement has the same guarantee.
  • Asset-storage ownership is proven safe for both IndexedDB and filesystem backends, including entity-id collision scenarios; if not, the smallest project-scoped fix is implemented.
  • Character-interview streaming cannot mutate a project different from the one that originated the stream.
  • Focused regression tests cover every confirmed gap, including same-id/new-incarnation cases where applicable.
  • Existing fix(project): guard AI-generation results against a stale/switched active project #707 identity semantics remain fail-closed and are not weakened.
  • pnpm run ci:prepush, exact-head CI/CD and CodeQL are green before merge.

Scope boundary

This issue is about cross-project mutation / asset authority only.

Do not broaden it into the general Wave 0C cancellation/AbortController redesign unless cancellation is strictly required to enforce mutation authority. General cancellation lifecycle work remains owned by #704 Wave 0C.

Do not perform provider/model-catalog, capability-matrix, connection-test, or broader AI architecture work here.

Severity

Treat as a data-integrity follow-up to Wave 0B. P1 until the audit proves or disproves a direct reproducible cross-project overwrite; elevate if a deterministic corruption path is confirmed.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions