Direct answer
Prevent duplicate model transfers by defining a stable work identity from the artifact, not by disabling the button in one screen. Every request should map to a model key plus immutable revision and transfer policy. Enqueue the same logical request idempotently; a genuinely different model revision must have a different identity. Separate the component that owns the transfer from screens that observe progress. A screen leaving composition should stop observing, not silently cancel shared work. An explicit cancel action should address the work identity the user selected and update all observers consistently. The data file also needs a single-writer policy: deduplicating scheduler entries is not sufficient if two direct download flows can write the same temporary path. In this repository, ModelDownloader.download returns a cold Flow. Each collection executes the flow body; therefore collecting the same flow twice is not equivalent to observing one already-running transfer. Both collectors can inspect the same temporary-file length and attempt to write to the same destination. ModelManager.startBackgroundDownload has an in-memory state guard for that method, but this guard is not durable across process death and does not automatically protect every caller of downloadModel or ModelDownloader. The code also exposes downloadModel as a flow path. These observations support a design review, not a claim that users have experienced duplicate downloads or that WorkManager is currently used for model transfer. WorkManager provides unique-work APIs for applications that need persistent scheduling and defines policies for what happens when work with the same name is enqueued again. Unique work can prevent duplicate scheduler records, but it does not replace artifact-key design, file locking, idempotent state transitions, or HTTP range validation. Choose the uniqueness key carefully: one global key would incorrectly serialize unrelated model variants; a key that omits the artifact revision could merge transfers for different bytes. If the application uses an in-process coroutine instead of persistent work, it still needs a shared coordinator or mutex around the file-writing operation and a clear policy for process recreation. Do not imply a particular scheduler is mandatory for every transfer. Make transfer ownership explicit. A repository or service can own a transfer state machine keyed by model identity. Each caller receives a read-only observation of state, while commands such as enqueue, pause, resume, and cancel go through the owner. The owner serializes file operations and records which attempt has the temporary file open. If an observer starts after work has begun, it should receive the current state rather than start a second request. If an observer disappears, the transfer should follow the documented product policy, not the accidental lifecycle of the screen. If a user asks to cancel, all observers should see cancellation, and no stale worker should later promote the partial file. State must be durable enough for the promise being made. An in-memory map protects only the current process. If work is expected to continue after the app process is killed, persist a transfer record or use a persistent scheduler, then reconcile the record with the actual temporary file at execution time. Never trust the database alone: compare artifact revision, file path, byte length, and response metadata before resuming. Conversely, a file on disk does not prove a live worker exists or that the artifact is valid. For multiple app modules sharing a model directory, define whether identity and locking are per app process or cross-process; a Kotlin mutex cannot coordinate independent processes. Test concurrency as a state-machine problem. Issue the same enqueue request from two UI entry points, repeat a tap while state is loading, recreate an activity, attach two observers, cancel from one observer, and enqueue a second artifact. Assert that one transfer owner writes a given partial file, the other observer sees the same state, cancellation has one terminal effect, and a different artifact does not get merged. Add process death between enqueue and worker start if persistent execution is promised. Inject a range mismatch or revision change to ensure deduplication never overrides byte-integrity policy. A test should inspect temporary and final files as well as UI state, because a single progress indicator can hide competing writes. Use work identity and file identity together. Work name might be derived from artifact id and revision; file paths should also be revision-specific or protected by an atomic ownership claim. Persist a unique attempt token so a stale worker cannot write after a newer attempt takes over. Before final promotion, verify that the attempt still owns the candidate and that required content checks pass. If a second request targets the same artifact, attach it to the existing work or return a conflict/active state. If it targets a new revision, create separate candidate state and preserve the currently working artifact until the candidate passes validation. ### Procedure 1. Define an artifact transfer key from model id, immutable revision, destination policy, and any format distinction that changes the bytes. Document whether concurrent different artifacts may proceed together. 2. Route all writes through one owner. Use unique persistent work where durability is required, or a process-level coordinator and mutex when work is intentionally process-scoped. Make observers read-only and commands explicit. 3. Make enqueue idempotent. Repeated requests for the same key attach to or replace work according to a documented policy; a different revision must not accidentally reuse the prior work name or temporary file. 4. Reconcile scheduler state with disk before every attempt. Verify attempt token, file ownership, artifact revision, and partial-response metadata before resume; close and invalidate stale writers before another one starts. 5. Run concurrent and lifecycle tests. Cover multiple entry points, duplicate taps, activity recreation, multiple observers, cancel, second model selection, process recreation, stale worker completion, and final verification. ### Acceptance criteria - Repeating an enqueue for the same artifact identity cannot create two writers for the same temporary file. - A distinct artifact revision is never coalesced into the old revision’s work or partial bytes. - Observers can attach or detach without starting or cancelling the shared transfer; an explicit cancel is routed to the owner. - A stale attempt cannot promote a file after ownership has moved to a newer attempt. ### Comparison | Mechanism | Protects | Does not protect by itself | |---|---|---| | Unique WorkManager work name | Duplicate scheduled work with the same chosen key | Concurrent direct flows or unsafe shared-file writes | | In-process mutex/coordinator | One process’s critical file operation | Process death or another application process | | Revision-specific attempt and file identity | Candidate ownership and artifact separation | HTTP response authenticity or digest verification | ### FAQ **Does unique WorkManager work guarantee one model file writer?** It can deduplicate scheduled work with the same identity, but callers may also use direct flows or another scheduler. Protect the destination at the transfer owner and file-operation layer as well. **Should all model downloads use one unique-work name?** No. A global key can combine or block different artifacts. Include a stable model identity and immutable revision, and decide explicitly whether variants may download concurrently. **Can a screen own and cancel the transfer?** A screen can send an explicit cancel command, but it should not accidentally own transfer lifetime merely because it observes progress. Detaching an observer and cancelling shared work are different operations. **Is an in-memory mutex enough?** It can serialize writers in one process, but it disappears on process death and does not coordinate separate processes. Match the ownership mechanism to the durability and process-boundary claims the app makes.
Two screens request the same model while another model remains selectable
A user taps download twice from onboarding and settings, then rotates the phone. The transfer uses a temporary file, while a second model variant can also be requested. The app needs one writer per artifact without making every model share one global lock.
Decision
Key work by immutable artifact identity, route writes through one owner, and keep observers separate from commands. Deduplicate only equivalent requests; keep different revisions as separate candidates.
Expected outcome
Both screens display one authoritative transfer, an explicit cancel affects that transfer once, and a different model never shares its temporary file or verification state.
Evidence to collect
The claim becomes reviewable only when the following evidence is attached to the same artifact and test run:
- Worker key and artifact schema
- Repeated-tap and process-recreation fixtures
- Single transfer and observer-state assertions
A completed byte stream is not enough. Final placement, integrity, runtime load, and recovery must also pass.
Implementation workflow
- Choose a stable transfer key. Combine model id, immutable revision, and any format or destination option that changes the actual bytes. Document whether separate models may download concurrently; avoid one global key and avoid reusing a key after the artifact changes.
- Give file writes one owner. Route every transfer through a repository, service, or worker owner. Use unique persistent work when process-surviving scheduling is required, or a process-scoped coordinator and mutex when it is not. Direct collectors must not each open the same temporary path.
- Separate observation from control. Expose transfer state as read-only data to screens. Enqueue, pause, resume, and cancel should be owner commands containing the transfer key. A screen leaving composition detaches its observer but does not accidentally terminate shared work.
- Reconcile work with the temporary file. Before resume, confirm the current attempt token, artifact revision, actual file path, byte offset, and previous range response. Invalidate stale writers and do not trust either scheduler metadata or a file on disk alone.
- Test races and stale completion. Start duplicate requests from separate entry points, attach multiple observers, rotate, cancel, enqueue another revision, and simulate a stale worker finishing late. Inspect disk contents and final promotion ownership in addition to screen state.
Keep each transition observable. A failure should identify the stage, artifact, runtime, and recovery action without logging private user content.
Acceptance criteria
- Equivalent requests share one transfer owner and cannot write concurrently to one partial file.
- Different revisions retain separate work keys, temporary files, and verification states.
- Observer attachment or detachment has no hidden enqueue or cancellation side effect.
- A stale worker cannot promote an artifact after a newer attempt acquires ownership.
Decision table
| Dimension | Choice or evidence | How to interpret it |
|---|---|---|
| Unique scheduled work | Scheduler-level deduplication | Useful for the same stable key; direct writers still need coordination. |
| Process mutex | In-process serialization | Protects one process but not process death or other processes. |
| Attempt token and revision path | File ownership and candidate isolation | Prevents stale promotion but does not verify remote response trust. |
Failure patterns to prevent
- Using one global worker for different models
- Showing conflicting progress from duplicate jobs
Also prevent silent fallback, unpinned artifacts, missing cancellation, and conclusions that combine unlike configurations. Store unsuccessful runs alongside successful ones.
Minimum reproducibility record
| Layer | Record |
|---|---|
| Device | Manufacturer, model, chipset, RAM class, operating-system build |
| Software | Application version and git commit |
| Model | Family, variant, revision, format, file length, hash, quantization |
| Runtime | Name, revision, requested backend, observed backend evidence |
| Workload | Fixture revision, input hash, prompt hash, output policy |
| Outcome | Completed, failed, cancelled, fallback, and privacy-safe diagnostics |
Release checklist
- ☐ The primary query is answered without an unsupported number.
- ☐ Every artifact and runtime is pinned.
- ☐ The representative task and failure policy are explicit.
- ☐ Lifecycle, cancellation, cleanup, and fallback are tested.
- ☐ User-content and network boundaries are documented.
- ☐ Result wording applies only to the recorded configuration.
- ☐ The page links to raw method or evidence when results are added.
Sources and related evidence
- Android persistent background work
- Android WorkManager unique work and management
- Android WorkManager persistent work
- Kotlin shared mutable state and concurrency
Chinese deployment and troubleshooting content is organized in the .奇连 AI 端侧专题.
Frequently asked questions
Does unique WorkManager work guarantee one writer to the model file?
Not by itself. Other code can create a direct cold Flow or use another scheduler, so the transfer owner and destination write path also need coordination.
Should every model use one WorkManager key?
No. A global key can combine different artifact requests or block unrelated variants. Derive identity from model and immutable revision, then document concurrency policy.
Should a screen own and cancel a download?
A screen may issue an explicit cancel command, but observing progress should not accidentally make the screen the lifetime owner. Detaching observation and cancelling shared work are separate actions.
Is an in-memory mutex sufficient?
It serializes operations only inside one live process. It is lost after process death and cannot coordinate separate processes, so use it only when that matches the transfer durability promise.