Skip to content

[WIP] Live Collab M3 - Proofreading (without segments list) #8723

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 48 commits into
base: master
Choose a base branch
from

Conversation

philippotto
Copy link
Member

@philippotto philippotto commented Jun 26, 2025

This PR enables experimental (opt-in) collaborative proofreading (ignoring segment lists and meshes). Multiple users can split and merge agglomerates "in parallel". Synchronization happens as follows:

  • in contrast to the master, a mutex is not eagerly acquired anymore as soon as an annotation is opened (in case of the opt-in collab proofreading)
  • [identical to the current master] after each split or merge the annotation is saved to the server (the UI is blocked until saving succeeded)
  • in order to save, a mutex is acquired from the server. afterwards, it is ensured that the client knows about the most recent version that exists on the server. potential updates are polled from the server and incorporated¹ into the client.
  • after saving succeeded, the mutex is released and the UI is unblocked.

¹ the incorporation of the update actions is not trivial, because the frontend maintains a partial mapping from segment ids to agglomerate ids. the update actions only encode which segment ids were split or merged. simply mapping the affected segment ids to their (old or new?) agglomerate ids and refreshing all segment ids that map to these agglomerate ids might not be enough, because the mapping is partial and might have changed in between.
therefore, a first iteration simply reloads all segment ids. when the rest of the pr works, we can re-think this.

URL of deployed dev instance (used for testing):

  • https://___.webknossos.xyz

Steps to test:

  • test old annotation editing
    • single-user (others may edit == false)
    • multi-user (others may edit == true)
  • test new collaborative proofreading mode

TODOs:

  • prototype with mutex acquisition within proofread saga (we don't want this in the long run for various reasons, though)
  • implement ad-hoc mutex acuisition in save process
  • implement ensureNewestVersion mechanism to avoid conflicts during saving
    • avoid theoretical infinite waiting (look for todo comment around buffers.sliding<EnsureHasNewestVersionAction>(1))
  • the active segment in proofreading is marked with a cross. this is simply the anchor position of the active segment. however, that position can be changed by other users which would disturb the current user. the proofreading "anchor" should be stored seperately (doesn't necessarily need to be persisted to the server, I think)
  • Fix agglomerate mapping activation
  • Toast "This annotation is not being edited anymore and available for editing. Reload the page to see its newest version and to edit it." appears, although it shouldn't be necessary.
  • ensure eager mutex acquisition still works for non-proofreading annotations (e.g. state.save.needsMutex boolean which the sagas act on)
    • ⚠️ currently, WK switches automatically to read-only if "others may edit" is false (I always enable this option during testing for now)
  • write tests
    • mock versioned agglomerate handling as backend substitute
    • write tests for "conflicts" (while saving, the backend reports a newer version created by another user)
      • merge with "foreign" merge
      • merge with "foreign" split
      • split with "foreign" merge
      • split with "foreign" split
      • ...?
    • also test the case where not all segment ids might be loaded in frontend
    • fix old tests
      • old proofreading tests
      • mutex tests (should be converted from the old manual-generator-execution to a more integration style (where all sagas run)
      • volume tracing integration tests
      • Fix annotation_saga.spec.ts. Tests seem to not have enough
      • others?
  • fix
    • inconsistent mappings on different clients after concurrent operations
    • [backend] avoid no such vertex in graph toast if a user tries to split an agglomerate that was already split. see [slack]
  • Implement rebase mechanism
  • forbid that update actions are emitted (i.e., users should not be allowed to do such actions) that can NOT be incorporated by other clients. otherwise, ensureNewestVersion would never terminate and other users will wait forever when trying to save. if this code version is never used in prod, it might not matter, though.
    • alternatively, support all update actions (in the long run, the better option anyway)
  • avoid "read-only" flickering in navbar
    • refactor mayEdit state in store?
  • ensure that update action reconciliation is only done for proofreading actions (because other's are not supported yet)
    https://scm.slack.com/archives/C5AKLAV0B/p1752142208929089).
  • [perf] avoid complete refresh of mapping and only refresh affected ids (with proper rebase mechanism)
  • make the new late-mutex approach opt-in (beta flag in sharing modal?)
    • disable all other tools if activated
    • disable segment list editing?
  • how to deal with outdated meshes?
    • auto-reload them? could be slow
    • mark them as stale?
  • clean up
    • refactor mappings.ts code so that it reacts to all setMappingActions? see comment in updateMappingWithMerge
  • ensure backend does accept split update actions when supervoxels are already split and merge update actions when supervoxels are already merged
    • fix duplicate edge problem

Issues:


(Please delete unneeded items, merge only when none are left open)

  • Added changelog entry (create a $PR_NUMBER.md file in unreleased_changes or use ./tools/create-changelog-entry.py)
  • Added migration guide entry if applicable (edit the same file as for the changelog)
  • Updated documentation if applicable
  • Adapted wk-libs python client if relevant API parts change
  • Removed dev-only changes like prints and application.conf edits
  • Considered common edge cases
  • Needs datastore update after deployment

Copy link
Contributor

coderabbitai bot commented Jun 26, 2025

Note

Reviews paused

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.
📝 Walkthrough

Walkthrough

This change introduces a mutex mechanism for annotation editing, enabling exclusive editing rights per user. It adds backend APIs for acquiring and releasing mutexes, updates the frontend to manage and display mutex state, and integrates Redux-Saga logic for continuous mutex acquisition and release. Related types and reducers are updated accordingly.

Changes

Files/Paths Change Summary
app/controllers/AnnotationController.scala, app/models/annotation/AnnotationMutexService.scala, app/models/annotation/AnnotationMutexDAO.scala Added backend API and service/DAO logic for releasing annotation mutexes.
conf/webknossos.latest.routes Added DELETE route for releasing annotation mutex.
frontend/javascripts/admin/rest_api.ts Added function to call backend mutex release API.
frontend/javascripts/viewer/model/sagas/annotation_saga.tsx, frontend/javascripts/viewer/model/sagas/saving/save_mutex_saga.tsx Moved and refactored mutex acquisition saga logic into a new module; integrated mutex state management.
frontend/javascripts/viewer/model/actions/annotation_actions.ts, frontend/javascripts/viewer/model/reducers/annotation_reducer.ts Added action and reducer logic for mutex acquisition state in Redux.
frontend/javascripts/viewer/model/actions/save_actions.ts, frontend/javascripts/viewer/model/sagas/saving/save_queue_draining.ts Added/used new save-related actions to coordinate mutex and save flow.
frontend/javascripts/viewer/default_state.ts, frontend/javascripts/viewer/store.ts, frontend/javascripts/viewer/model/reducers/reducer_helpers.ts Added isMutexAcquired property to annotation state and type definitions.
frontend/javascripts/viewer/api/wk_dev.ts Changed default debugging flags.
frontend/javascripts/test/sagas/annotation_saga.spec.ts Updated import path for saga under test.
frontend/javascripts/viewer/model/sagas/volume/proofread_saga.ts Simplified proofreading saga calls by removing unused parameters.
webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/editablemapping/*.scala Made mag and agglomerate IDs optional in update actions; updated method signatures for optional mag.
frontend/javascripts/viewer/model/sagas/volume/mapping_saga.ts Added debugging saga to reload HDF5 mapping on demand.
frontend/javascripts/viewer/model/sagas/saving/save_saga.ts Adjusted polling interval and deferred local HDF5 mapping update logic.
frontend/javascripts/viewer/model/sagas/saving/save_queue_draining.ts Enhanced save queue draining with mutex acquisition and version checks; simplified conflict error handling.

Assessment against linked issues

Objective Addressed Explanation
Implement mutex-based exclusive annotation editing (Milestone 3, #8724)
Integrate mutex acquisition/release in frontend and backend (Milestone 3)
Update Redux state/types for mutex status (Milestone 3)
Prepare proofreading sagas for mutex integration (Milestone 3)

Assessment against linked issues: Out-of-scope changes

Code Change (file_path) Explanation
Changed default values of debugging flags (frontend/javascripts/viewer/api/wk_dev.ts) Debugging flag changes are unrelated to mutex/proofreading objectives in #8724.
Made mag and agglomerate ID fields optional in update actions (webknossos-tracingstore/...UpdateActions.scala) These changes are for backward compatibility and not directly related to mutex or proofreading.
Updated method signatures for optional mag (webknossos-tracingstore/...EditableMappingService.scala) Not directly related to mutex or proofreading; appears to be a separate API compatibility update.

Suggested labels

backend, new feature

Suggested reviewers

  • MichaelBuessemeyer

Poem

A mutex for my annotation,
Keeps my edits in one location.
With Redux and Sagas, I lock and I free,
So only one bunny can edit with glee.
Now proofreading’s safe,
No more race—
Just happy hopping in my editing space! 🐇✨

✨ Finishing Touches
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch live-m3

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai generate unit tests to generate unit tests for this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@philippotto philippotto changed the title [WIP] Live collaboration for proofreading [WIP] Live Collab M3 - Proofreading (without segments) Jun 27, 2025
@fm3 fm3 changed the title [WIP] Live Collab M3 - Proofreading (without segments) [WIP] Live Collab M3 - Proofreading (without segments list) Jun 30, 2025
case _ =>
Fox.successful(())
}
} yield ()
Copy link
Member

Choose a reason for hiding this comment

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

since we’re not using any info from the findOne other than whether to delete, we could merge the two into one query and just DELETE WHERE annotationId = a AND userId = u

Copy link
Member

Choose a reason for hiding this comment

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

I changed that now in 3f66d4e – note that I could not test end-to-end since the frontend isn’t ready. Let me know if this doesn’t work as expected :)

Base automatically changed from refactor-saga-folders to live-m2 July 1, 2025 13:40
Base automatically changed from live-m2 to master July 7, 2025 09:37
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

♻️ Duplicate comments (1)
app/models/annotation/AnnotationMutexService.scala (1)

71-80: Consider optimizing the mutex release operation.

The current approach uses separate findOne and deleteOne operations. As noted in a previous review, this could be optimized into a single query that deletes only if the user owns the mutex.

Consider replacing the current implementation with a single query:

-def release(annotationId: ObjectId, userId: ObjectId): Fox[Unit] =
-  for {
-    mutex <- annotationMutexDAO.findOne(annotationId).shiftBox
-    _ <- mutex match {
-      case Full(mutex) if mutex.userId == userId =>
-        annotationMutexDAO.deleteOne(annotationId).map(_ => ())
-      case _ =>
-        Fox.successful(())
-    }
-  } yield ()
+def release(annotationId: ObjectId, userId: ObjectId): Fox[Unit] =
+  annotationMutexDAO.deleteOneByUserAndAnnotation(annotationId, userId).map(_ => ())

And add to the DAO:

def deleteOneByUserAndAnnotation(annotationId: ObjectId, userId: ObjectId): Fox[Int] =
  run(q"DELETE FROM webknossos.annotation_mutexes WHERE _annotation = $annotationId AND _user = $userId".asUpdate)

This would be more efficient and eliminate the race condition between the find and delete operations.

🧹 Nitpick comments (3)
frontend/javascripts/viewer/model/sagas/volume/proofread_saga.ts (1)

361-361: Address unused variable warnings for preparatory mutex integration.

The annotationId variables are retrieved from state but not used because the mutex calls are commented out as part of the WIP development. To address the static analysis warnings while maintaining the preparatory code, consider prefixing the variables with underscore:

-const annotationId = yield* select((state) => state.annotation.annotationId);
+const _annotationId = yield* select((state) => state.annotation.annotationId);

This clearly indicates the variables are intentionally unused in the current state while being prepared for future integration.

Also applies to: 445-446, 710-710, 796-797, 902-902, 957-958, 1142-1149

frontend/javascripts/viewer/model/sagas/saving/save_mutex_saga.tsx (2)

29-29: Use import type for type-only imports.

The static analysis correctly identifies that EnsureMaySaveNowAction is only used as a type.

-import { EnsureMaySaveNowAction } from "viewer/model/actions/save_actions";
+import type { EnsureMaySaveNowAction } from "viewer/model/actions/save_actions";

93-106: Clarify the mutex acquisition flow for late-mutex approach.

The conditional logic based on DISABLE_EAGER_MUTEX_ACQUISITION implements different mutex acquisition strategies, but the flow could be clearer.

Consider adding comments to explain:

  1. When DISABLE_EAGER_MUTEX_ACQUISITION is true, mutex is acquired only when saving (late acquisition)
  2. When false, mutex is acquired eagerly when the annotation is opened
  3. The race condition between tryAcquireMutexContinuously and DONE_SAVING ensures mutex is released after save completes

This aligns with the PR objective of implementing a "late-mutex approach" as an opt-in feature.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 80e2ed2 and 9aa961b.

📒 Files selected for processing (19)
  • app/controllers/AnnotationController.scala (1 hunks)
  • app/models/annotation/AnnotationMutexService.scala (2 hunks)
  • conf/webknossos.latest.routes (1 hunks)
  • frontend/javascripts/admin/rest_api.ts (1 hunks)
  • frontend/javascripts/test/sagas/annotation_saga.spec.ts (1 hunks)
  • frontend/javascripts/viewer/api/wk_dev.ts (2 hunks)
  • frontend/javascripts/viewer/default_state.ts (1 hunks)
  • frontend/javascripts/viewer/model/actions/annotation_actions.ts (3 hunks)
  • frontend/javascripts/viewer/model/actions/save_actions.ts (3 hunks)
  • frontend/javascripts/viewer/model/reducers/annotation_reducer.ts (1 hunks)
  • frontend/javascripts/viewer/model/reducers/reducer_helpers.ts (1 hunks)
  • frontend/javascripts/viewer/model/sagas/annotation_saga.tsx (2 hunks)
  • frontend/javascripts/viewer/model/sagas/saving/save_mutex_saga.tsx (1 hunks)
  • frontend/javascripts/viewer/model/sagas/saving/save_queue_draining.ts (5 hunks)
  • frontend/javascripts/viewer/model/sagas/saving/save_saga.ts (1 hunks)
  • frontend/javascripts/viewer/model/sagas/volume/proofread_saga.ts (7 hunks)
  • frontend/javascripts/viewer/store.ts (1 hunks)
  • webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/editablemapping/EditableMappingService.scala (1 hunks)
  • webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/editablemapping/EditableMappingUpdateActions.scala (2 hunks)
🧰 Additional context used
🧠 Learnings (11)
📓 Common learnings
Learnt from: MichaelBuessemeyer
PR: scalableminds/webknossos#8352
File: conf/evolutions/126-credit-transactions.sql:89-130
Timestamp: 2025-01-27T15:01:17.868Z
Learning: Team prefers to discuss concurrency handling approaches (like transaction isolation) during the review phase rather than as individual suggestions.
Learnt from: philippotto
PR: scalableminds/webknossos#8542
File: frontend/javascripts/viewer/model/sagas/volumetracing_saga.tsx:0-0
Timestamp: 2025-05-30T12:36:09.930Z
Learning: The WebKnossos frontend follows Redux immutability principles where reducers act immutably and create new state objects when changes occur. This makes reference equality checks (like `prevVolumeTracing.segments !== volumeTracing.segments`) reliable and efficient for detecting actual state changes, allowing memoization to work correctly.
frontend/javascripts/test/sagas/annotation_saga.spec.ts (1)
Learnt from: dieknolle3333
PR: scalableminds/webknossos#8168
File: frontend/javascripts/oxalis/model/sagas/proofread_saga.ts:1039-1039
Timestamp: 2024-11-22T17:18:04.217Z
Learning: In `frontend/javascripts/oxalis/model/sagas/proofread_saga.ts`, when calling `getMagInfo`, the use of `volumeTracingLayer.resolutions` is intentional and should not be changed to `volumeTracingLayer.mags`.
frontend/javascripts/viewer/model/reducers/annotation_reducer.ts (1)
Learnt from: knollengewaechs
PR: scalableminds/webknossos#8492
File: frontend/javascripts/viewer/model/sagas/skeletontracing_saga.ts:671-674
Timestamp: 2025-05-15T19:44:16.110Z
Learning: Visibility updates for user bounding boxes are intentionally handled separately from other property updates, with dedicated update actions for visibility changes.
frontend/javascripts/viewer/model/sagas/saving/save_saga.ts (1)
Learnt from: dieknolle3333
PR: scalableminds/webknossos#8168
File: frontend/javascripts/oxalis/model/sagas/proofread_saga.ts:1039-1039
Timestamp: 2024-11-22T17:18:04.217Z
Learning: In `frontend/javascripts/oxalis/model/sagas/proofread_saga.ts`, when calling `getMagInfo`, the use of `volumeTracingLayer.resolutions` is intentional and should not be changed to `volumeTracingLayer.mags`.
app/models/annotation/AnnotationMutexService.scala (3)
Learnt from: frcroth
PR: scalableminds/webknossos#8236
File: webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/mesh/MeshFileService.scala:170-173
Timestamp: 2025-04-23T08:51:57.756Z
Learning: In the webknossos codebase, classes extending `FoxImplicits` have access to an implicit conversion from `Option[A]` to `Fox[A]`, where `None` is converted to an empty Fox that fails gracefully in for-comprehensions.
Learnt from: MichaelBuessemeyer
PR: scalableminds/webknossos#8352
File: app/models/organization/CreditTransactionService.scala:0-0
Timestamp: 2025-01-27T12:06:42.865Z
Learning: In Scala's for-comprehension with Fox (Future-like type), the `<-` operator ensures sequential execution. If any step fails, the entire chain short-circuits and returns early, preventing subsequent operations from executing. This makes it safe to perform validation checks before database operations.
Learnt from: frcroth
PR: scalableminds/webknossos#8236
File: webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/mesh/NeuroglancerPrecomputedMeshFileService.scala:161-166
Timestamp: 2025-04-28T14:18:04.368Z
Learning: In Scala for-comprehensions with the Fox error handling monad, `Fox.fromBool()` expressions should use the `<-` binding operator instead of the `=` assignment operator to properly propagate error conditions. Using `=` will cause validation failures to be silently ignored.
frontend/javascripts/viewer/model/sagas/saving/save_mutex_saga.tsx (1)
Learnt from: dieknolle3333
PR: scalableminds/webknossos#8168
File: frontend/javascripts/oxalis/model/sagas/proofread_saga.ts:1039-1039
Timestamp: 2024-11-22T17:18:04.217Z
Learning: In `frontend/javascripts/oxalis/model/sagas/proofread_saga.ts`, when calling `getMagInfo`, the use of `volumeTracingLayer.resolutions` is intentional and should not be changed to `volumeTracingLayer.mags`.
frontend/javascripts/viewer/model/sagas/volume/proofread_saga.ts (1)
Learnt from: dieknolle3333
PR: scalableminds/webknossos#8168
File: frontend/javascripts/oxalis/model/sagas/proofread_saga.ts:1039-1039
Timestamp: 2024-11-22T17:18:04.217Z
Learning: In `frontend/javascripts/oxalis/model/sagas/proofread_saga.ts`, when calling `getMagInfo`, the use of `volumeTracingLayer.resolutions` is intentional and should not be changed to `volumeTracingLayer.mags`.
webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/editablemapping/EditableMappingService.scala (6)
Learnt from: dieknolle3333
PR: scalableminds/webknossos#8168
File: frontend/javascripts/oxalis/model/sagas/proofread_saga.ts:1039-1039
Timestamp: 2024-11-22T17:18:04.217Z
Learning: In `frontend/javascripts/oxalis/model/sagas/proofread_saga.ts`, when calling `getMagInfo`, the use of `volumeTracingLayer.resolutions` is intentional and should not be changed to `volumeTracingLayer.mags`.
Learnt from: dieknolle3333
PR: scalableminds/webknossos#8168
File: frontend/javascripts/oxalis/model/sagas/volumetracing_saga.tsx:433-434
Timestamp: 2024-11-22T17:19:07.947Z
Learning: In the codebase, certain usages of `segmentationLayer.resolutions` are intentionally retained and should not be changed to `segmentationLayer.mags` during refactoring.
Learnt from: frcroth
PR: scalableminds/webknossos#8609
File: app/models/dataset/Dataset.scala:753-775
Timestamp: 2025-05-12T13:07:29.637Z
Learning: In the `updateMags` method of DatasetMagsDAO (Scala), the code handles different dataset types distinctly:
1. Non-WKW datasets have `magsOpt` populated and use the first branch which includes axisOrder, channelIndex, and credentialId.
2. WKW datasets will have `wkwResolutionsOpt` populated and use the second branch which includes cubeLength.
3. The final branch is a fallback for legacy data.
This ensures appropriate fields are populated for each dataset type.
Learnt from: philippotto
PR: scalableminds/webknossos#8602
File: frontend/javascripts/oxalis/model/volumetracing/volume_annotation_sampling.ts:365-366
Timestamp: 2025-05-07T06:17:32.810Z
Learning: The parameter in applyVoxelMap was renamed from `sliceCount` to `sliceOffset` to better reflect its purpose, but this doesn't affect existing call sites since JavaScript/TypeScript function calls are position-based.
Learnt from: dieknolle3333
PR: scalableminds/webknossos#8168
File: frontend/javascripts/oxalis/model/reducers/skeletontracing_reducer_helpers.ts:125-125
Timestamp: 2024-11-22T17:17:39.914Z
Learning: In the `createNode` function within `skeletontracing_reducer_helpers.ts`, the property `resolution` is intentionally kept even when the parameter is renamed to `mag`.
Learnt from: frcroth
PR: scalableminds/webknossos#8236
File: webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/mesh/MeshFileService.scala:170-173
Timestamp: 2025-04-23T08:51:57.756Z
Learning: In the webknossos codebase, classes extending `FoxImplicits` have access to an implicit conversion from `Option[A]` to `Fox[A]`, where `None` is converted to an empty Fox that fails gracefully in for-comprehensions.
frontend/javascripts/viewer/model/sagas/saving/save_queue_draining.ts (1)
Learnt from: dieknolle3333
PR: scalableminds/webknossos#8168
File: frontend/javascripts/oxalis/model/sagas/proofread_saga.ts:1039-1039
Timestamp: 2024-11-22T17:18:04.217Z
Learning: In `frontend/javascripts/oxalis/model/sagas/proofread_saga.ts`, when calling `getMagInfo`, the use of `volumeTracingLayer.resolutions` is intentional and should not be changed to `volumeTracingLayer.mags`.
frontend/javascripts/viewer/model/sagas/annotation_saga.tsx (1)
Learnt from: dieknolle3333
PR: scalableminds/webknossos#8168
File: frontend/javascripts/oxalis/model/sagas/proofread_saga.ts:1039-1039
Timestamp: 2024-11-22T17:18:04.217Z
Learning: In `frontend/javascripts/oxalis/model/sagas/proofread_saga.ts`, when calling `getMagInfo`, the use of `volumeTracingLayer.resolutions` is intentional and should not be changed to `volumeTracingLayer.mags`.
webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/editablemapping/EditableMappingUpdateActions.scala (1)
Learnt from: frcroth
PR: scalableminds/webknossos#8609
File: app/models/dataset/Dataset.scala:753-775
Timestamp: 2025-05-12T13:07:29.637Z
Learning: In the `updateMags` method of DatasetMagsDAO (Scala), the code handles different dataset types distinctly:
1. Non-WKW datasets have `magsOpt` populated and use the first branch which includes axisOrder, channelIndex, and credentialId.
2. WKW datasets will have `wkwResolutionsOpt` populated and use the second branch which includes cubeLength.
3. The final branch is a fallback for legacy data.
This ensures appropriate fields are populated for each dataset type.
🧬 Code Graph Analysis (2)
app/models/annotation/AnnotationMutexService.scala (2)
util/src/main/scala/com/scalableminds/util/tools/Fox.scala (2)
  • shiftBox (296-296)
  • successful (53-56)
app/utils/sql/SqlInterpolation.scala (2)
  • q (19-38)
  • asUpdate (73-73)
frontend/javascripts/viewer/model/sagas/saving/save_queue_draining.ts (2)
frontend/javascripts/viewer/model/actions/save_actions.ts (2)
  • dispatchEnsureMaySaveNowAsync (146-151)
  • doneSavingAction (153-156)
frontend/javascripts/libs/window.ts (2)
  • alert (18-18)
  • location (60-60)
🪛 Biome (1.9.4)
frontend/javascripts/viewer/model/sagas/saving/save_saga.ts

[error] 55-55: Unexpected constant condition.

(lint/correctness/noConstantCondition)

frontend/javascripts/viewer/model/sagas/saving/save_mutex_saga.tsx

[error] 29-29: All these imports are only used as types.

Importing the types with import type ensures that they are removed by the compilers and avoids loading unnecessary modules.
Safe fix: Use import type.

(lint/style/useImportType)

frontend/javascripts/viewer/model/sagas/volume/proofread_saga.ts

[error] 361-361: This variable is unused.

Unused variables usually are result of incomplete refactoring, typos and other source of bugs.
Unsafe fix: If this is intentional, prepend annotationId with an underscore.

(lint/correctness/noUnusedVariables)


[error] 710-710: This variable is unused.

Unused variables usually are result of incomplete refactoring, typos and other source of bugs.
Unsafe fix: If this is intentional, prepend annotationId with an underscore.

(lint/correctness/noUnusedVariables)


[error] 902-902: This variable is unused.

Unused variables usually are result of incomplete refactoring, typos and other source of bugs.
Unsafe fix: If this is intentional, prepend annotationId with an underscore.

(lint/correctness/noUnusedVariables)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: build-smoketest-push
  • GitHub Check: backend-tests
🔇 Additional comments (19)
webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/editablemapping/EditableMappingUpdateActions.scala (2)

14-14: Well-designed backward compatibility approach.

The conversion of agglomerateId and mag fields to Option types is a clean approach for supporting legacy update actions while maintaining type safety. The comment provides clear context for why these fields are now optional.

Also applies to: 19-19


39-40: Consistent optional field implementation.

The same backward compatibility pattern is correctly applied to MergeAgglomerateUpdateAction with both agglomerateId1 and agglomerateId2 fields becoming optional, maintaining consistency with the SplitAgglomerateUpdateAction changes.

Also applies to: 45-45

webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/editablemapping/EditableMappingService.scala (2)

188-188: Proper method signature alignment with update actions.

The parameter change from mag: Vec3Int to magOpt: Option[Vec3Int] correctly aligns with the optional fields in the update actions, maintaining consistency across the codebase.

Also applies to: 191-191


196-196: Effective validation for optional parameters.

The validation logic properly handles the optional magOpt parameter by converting it to a Fox with a descriptive error message when required. This ensures that the method fails gracefully when mandatory data is missing while supporting the backward compatibility requirements.

Also applies to: 199-199

frontend/javascripts/test/sagas/annotation_saga.spec.ts (1)

15-15: LGTM: Import path correctly updated for refactored saga.

The import path update aligns with the refactoring where acquireAnnotationMutexMaybe was moved to a dedicated mutex saga module, improving code organization.

frontend/javascripts/viewer/store.ts (1)

149-149: LGTM: Mutex state flag properly added to Annotation type.

The isMutexAcquired boolean property is correctly defined as readonly, maintaining Redux immutability principles and providing clear state tracking for mutex management.

frontend/javascripts/viewer/default_state.ts (1)

181-181: LGTM: Appropriate default value for mutex state.

Initializing isMutexAcquired to false is the correct default since annotations start without mutex acquisition, maintaining consistency with the expected initial state.

conf/webknossos.latest.routes (1)

158-158: LGTM: Well-structured REST endpoint for mutex release.

The DELETE route for /annotations/:id/mutex correctly complements the existing POST route for mutex acquisition, following REST conventions and maintaining consistent route structure.

frontend/javascripts/viewer/model/reducers/reducer_helpers.ts (1)

159-159: LGTM: Consistent mutex flag initialization in conversion function.

Adding isMutexAcquired: false ensures that server-to-frontend annotation conversions maintain consistent object structure with the default state initialization.

frontend/javascripts/viewer/model/reducers/annotation_reducer.ts (1)

140-145: LGTM! Clean implementation of the mutex state reducer.

The new action case follows the established patterns in the file and correctly uses the updateKey helper function to maintain immutability. The implementation is consistent with other reducer cases in the file.

frontend/javascripts/admin/rest_api.ts (1)

732-736: LGTM! Clean implementation of the mutex release API.

The function follows the established patterns in the file and correctly uses the HTTP DELETE method for releasing the mutex. The implementation is consistent with the existing acquireAnnotationMutex function and integrates well with the broader mutex management feature.

frontend/javascripts/viewer/api/wk_dev.ts (1)

14-14: Good debugging setup for development.

Enabling these debug flags will help with development and troubleshooting of the new mutex functionality. The enhanced logging and version display should provide valuable insights during the WIP development phase.

Also applies to: 31-31

app/controllers/AnnotationController.scala (1)

457-463: Clean implementation of the mutex release endpoint.

The controller method follows established patterns in the codebase, uses appropriate error handling with Fox, and includes performance monitoring via logTime. The implementation is straightforward and correctly delegates to the service layer.

frontend/javascripts/viewer/model/actions/annotation_actions.ts (1)

55-55: Well-implemented action creator for mutex state management.

The new setIsMutexAcquiredAction follows established patterns in the codebase, is properly typed, and correctly integrated into the action type union. The implementation is clean and straightforward.

Also applies to: 91-91, 181-185

frontend/javascripts/viewer/model/sagas/annotation_saga.tsx (1)

2-2: Clean refactoring of mutex logic!

The relocation of mutex acquisition logic to a dedicated module improves code organization and separation of concerns.

Also applies to: 9-9, 14-17, 34-34

frontend/javascripts/viewer/model/actions/save_actions.ts (1)

29-30: Well-structured save flow control actions!

The new actions and async dispatch helper follow the established patterns in the codebase. The Deferred pattern for callback synchronization is consistent with existing implementations.

Also applies to: 43-45, 140-156

frontend/javascripts/viewer/model/sagas/saving/save_queue_draining.ts (2)

68-70: Good integration of save flow control!

The addition of dispatchEnsureMaySaveNowAsync and doneSavingAction properly coordinates save operations with mutex state.

Also applies to: 102-103


206-210: Verify the simplified 409 conflict handling behavior.

The removal of the flag and indefinite sleep simplifies the code, but may change behavior:

  • Previously, only one alert was shown due to the flag
  • The indefinite sleep prevented further save attempts
  • Now the error is thrown immediately after reload

Since multiple retries could trigger multiple alerts before the page reloads, consider if this behavior change is intentional. The immediate throw after reload seems correct as the page will be refreshed anyway.

frontend/javascripts/viewer/model/sagas/saving/save_mutex_saga.tsx (1)

104-104: Add missing yield operator.*

The call effect requires yield* to properly execute the saga.

-      if (doneSaving) {
-        yield call(releaseMutex);
-      }
+      if (doneSaving) {
+        yield* call(releaseMutex);
+      }

Likely an incorrect or invalid review comment.

Comment on lines +54 to +55
// todop
if (false && allowSave) {
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix the disabled early return condition and unclear comment.

The condition if (false && allowSave) effectively disables the early return logic that optimizes version checking when the user has exclusive edit access. This will cause unnecessary server requests even when the mutex system should prevent conflicts.

Issues:

  1. The false && makes this a constant condition that never executes
  2. The comment "todop" appears to be a typo or placeholder
  3. This disables an important performance optimization

Apply this fix to restore the intended behavior:

-    // todop
-    if (false && allowSave) {
+    if (allowSave) {

If this change was intentional for testing/debugging purposes, please add a clear comment explaining why and consider using a feature flag instead of hardcoding false.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// todop
if (false && allowSave) {
if (allowSave) {
🧰 Tools
🪛 Biome (1.9.4)

[error] 55-55: Unexpected constant condition.

(lint/correctness/noConstantCondition)

🤖 Prompt for AI Agents
In frontend/javascripts/viewer/model/sagas/saving/save_saga.ts around lines 54
to 55, the condition `if (false && allowSave)` disables the early return logic
by always evaluating to false, and the comment "todop" is unclear. Remove the
`false &&` to restore the original condition `if (allowSave)` so the early
return works as intended, and replace the "todop" comment with a clear
explanation of the logic. If the disabling was intentional for testing, replace
the hardcoded false with a feature flag and add a comment explaining its
purpose.

Comment on lines 217 to 227
function* releaseMutex() {
const annotationId = yield* select((storeState) => storeState.annotation.annotationId);
yield* retry(
RETRY_COUNT,
ACQUIRE_MUTEX_INTERVAL / RETRY_COUNT,
acquireAnnotationMutex,
annotationId,
);
yield* put(setAnnotationAllowUpdateAction(true));
yield* put(setBlockedByUserAction(null));
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix the releaseMutex implementation to actually release the mutex.

The function is calling acquireAnnotationMutex instead of the expected releaseAnnotationMutex API. Based on the AI summary, there should be a releaseAnnotationMutex function available.

+import { acquireAnnotationMutex, releaseAnnotationMutex } from "admin/rest_api";

 function* releaseMutex() {
   const annotationId = yield* select((storeState) => storeState.annotation.annotationId);
   yield* retry(
     RETRY_COUNT,
     ACQUIRE_MUTEX_INTERVAL / RETRY_COUNT,
-    acquireAnnotationMutex,
+    releaseAnnotationMutex,
     annotationId,
   );
-  yield* put(setAnnotationAllowUpdateAction(true));
-  yield* put(setBlockedByUserAction(null));
+  yield* put(setIsMutexAcquiredAction(false));
 }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In frontend/javascripts/viewer/model/sagas/saving/save_mutex_saga.tsx around
lines 217 to 227, the releaseMutex function incorrectly calls
acquireAnnotationMutex instead of releaseAnnotationMutex. To fix this, replace
the call to acquireAnnotationMutex with releaseAnnotationMutex in the retry
effect to properly release the mutex as intended.

Comment on lines 39 to 40
// todop
const DISABLE_EAGER_MUTEX_ACQUISITION = true;
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Fix TODO typo and make the flag configurable.

The comment has a typo ("todop") and the hardcoded flag should be configurable, possibly through a beta flag as mentioned in the PR objectives.

-// todop
-const DISABLE_EAGER_MUTEX_ACQUISITION = true;
+// TODO: Make this configurable via beta flag in sharing modal
+const DISABLE_EAGER_MUTEX_ACQUISITION = true;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// todop
const DISABLE_EAGER_MUTEX_ACQUISITION = true;
// TODO: Make this configurable via beta flag in sharing modal
const DISABLE_EAGER_MUTEX_ACQUISITION = true;
🤖 Prompt for AI Agents
In frontend/javascripts/viewer/model/sagas/saving/save_mutex_saga.tsx around
lines 39 to 40, fix the typo in the comment by changing "todop" to "todo" and
replace the hardcoded constant DISABLE_EAGER_MUTEX_ACQUISITION with a
configurable flag, such as reading its value from a beta feature flag or
configuration setting, to allow toggling this behavior without code changes.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

♻️ Duplicate comments (4)
frontend/javascripts/viewer/model/sagas/saving/save_mutex_saga.tsx (3)

39-40: Fix TODO typo and make the flag configurable.


177-188: Consider using a finally block as mentioned in the comment.


225-235: Fix the releaseMutex implementation to actually release the mutex.

frontend/javascripts/viewer/model/sagas/saving/save_saga.ts (1)

59-60: Fix the disabled early return condition and unclear comment.

🧹 Nitpick comments (1)
frontend/javascripts/viewer/model/sagas/volume/mapping_saga.ts (1)

158-205: Well-structured debugging feature with clear documentation.

The addition of DEBUG__RELOAD_HDF5_MAPPING action handler and supporting functions provides a useful debugging capability. The code is well-documented with clear comments explaining the debugging purpose.

One minor issue: The error message on line 200 mentions "splitAgglomerate" but should be more generic since this is a general HDF5 reloading function:

-    throw new Error("Could not apply splitAgglomerate because no active mapping was found.");
+    throw new Error("Could not reload HDF5 mapping because no active mapping was found.");
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9aa961b and 29a1608.

📒 Files selected for processing (8)
  • frontend/javascripts/viewer/api/wk_dev.ts (1 hunks)
  • frontend/javascripts/viewer/model/actions/save_actions.ts (3 hunks)
  • frontend/javascripts/viewer/model/sagas/saving/save_mutex_saga.tsx (1 hunks)
  • frontend/javascripts/viewer/model/sagas/saving/save_queue_draining.ts (5 hunks)
  • frontend/javascripts/viewer/model/sagas/saving/save_saga.ts (7 hunks)
  • frontend/javascripts/viewer/model/sagas/volume/mapping_saga.ts (2 hunks)
  • frontend/javascripts/viewer/model/sagas/volume/proofread_saga.ts (6 hunks)
  • frontend/javascripts/viewer/model/sagas/volume/update_actions.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (4)
  • frontend/javascripts/viewer/api/wk_dev.ts
  • frontend/javascripts/viewer/model/sagas/volume/proofread_saga.ts
  • frontend/javascripts/viewer/model/sagas/saving/save_queue_draining.ts
  • frontend/javascripts/viewer/model/actions/save_actions.ts
🧰 Additional context used
🧠 Learnings (5)
📓 Common learnings
Learnt from: MichaelBuessemeyer
PR: scalableminds/webknossos#8352
File: conf/evolutions/126-credit-transactions.sql:89-130
Timestamp: 2025-01-27T15:01:17.868Z
Learning: Team prefers to discuss concurrency handling approaches (like transaction isolation) during the review phase rather than as individual suggestions.
Learnt from: philippotto
PR: scalableminds/webknossos#8542
File: frontend/javascripts/viewer/model/sagas/volumetracing_saga.tsx:0-0
Timestamp: 2025-05-30T12:36:09.930Z
Learning: The WebKnossos frontend follows Redux immutability principles where reducers act immutably and create new state objects when changes occur. This makes reference equality checks (like `prevVolumeTracing.segments !== volumeTracing.segments`) reliable and efficient for detecting actual state changes, allowing memoization to work correctly.
frontend/javascripts/viewer/model/sagas/saving/save_mutex_saga.tsx (2)
Learnt from: dieknolle3333
PR: scalableminds/webknossos#8168
File: frontend/javascripts/oxalis/model/sagas/proofread_saga.ts:1039-1039
Timestamp: 2024-11-22T17:18:04.217Z
Learning: In `frontend/javascripts/oxalis/model/sagas/proofread_saga.ts`, when calling `getMagInfo`, the use of `volumeTracingLayer.resolutions` is intentional and should not be changed to `volumeTracingLayer.mags`.
Learnt from: robert-oleynik
PR: scalableminds/webknossos#8393
File: frontend/javascripts/admin/auth/passkeys_view.tsx:29-35
Timestamp: 2025-07-09T07:30:57.149Z
Learning: In the webknossos project, error handling and user notifications are handled at the API function level (e.g., `removeWebAuthnKey`, `fetchPasskeys`) rather than in the UI components, so additional try-catch blocks in the UI layer are unnecessary.
frontend/javascripts/viewer/model/sagas/volume/update_actions.ts (4)
Learnt from: dieknolle3333
PR: scalableminds/webknossos#8168
File: frontend/javascripts/oxalis/model/sagas/proofread_saga.ts:1039-1039
Timestamp: 2024-11-22T17:18:04.217Z
Learning: In `frontend/javascripts/oxalis/model/sagas/proofread_saga.ts`, when calling `getMagInfo`, the use of `volumeTracingLayer.resolutions` is intentional and should not be changed to `volumeTracingLayer.mags`.
Learnt from: philippotto
PR: scalableminds/webknossos#8602
File: frontend/javascripts/oxalis/model/volumetracing/volume_annotation_sampling.ts:365-366
Timestamp: 2025-05-07T06:17:32.810Z
Learning: The parameter in applyVoxelMap was renamed from `sliceCount` to `sliceOffset` to better reflect its purpose, but this doesn't affect existing call sites since JavaScript/TypeScript function calls are position-based.
Learnt from: dieknolle3333
PR: scalableminds/webknossos#8168
File: frontend/javascripts/oxalis/model/reducers/skeletontracing_reducer_helpers.ts:125-125
Timestamp: 2024-11-22T17:17:39.914Z
Learning: In the `createNode` function within `skeletontracing_reducer_helpers.ts`, the property `resolution` is intentionally kept even when the parameter is renamed to `mag`.
Learnt from: dieknolle3333
PR: scalableminds/webknossos#8168
File: frontend/javascripts/oxalis/geometries/materials/plane_material_factory.ts:568-585
Timestamp: 2024-11-22T17:18:43.411Z
Learning: In the file `frontend/javascripts/oxalis/geometries/materials/plane_material_factory.ts`, the uniform name `allResolutions` should remain unchanged to maintain consistency with the shader code.
frontend/javascripts/viewer/model/sagas/volume/mapping_saga.ts (2)
Learnt from: dieknolle3333
PR: scalableminds/webknossos#8168
File: frontend/javascripts/oxalis/model/sagas/proofread_saga.ts:1039-1039
Timestamp: 2024-11-22T17:18:04.217Z
Learning: In `frontend/javascripts/oxalis/model/sagas/proofread_saga.ts`, when calling `getMagInfo`, the use of `volumeTracingLayer.resolutions` is intentional and should not be changed to `volumeTracingLayer.mags`.
Learnt from: philippotto
PR: scalableminds/webknossos#8542
File: frontend/javascripts/viewer/model/sagas/volumetracing_saga.tsx:0-0
Timestamp: 2025-05-30T12:36:09.930Z
Learning: The WebKnossos frontend follows Redux immutability principles where reducers act immutably and create new state objects when changes occur. This makes reference equality checks (like `prevVolumeTracing.segments !== volumeTracing.segments`) reliable and efficient for detecting actual state changes, allowing memoization to work correctly.
frontend/javascripts/viewer/model/sagas/saving/save_saga.ts (2)
Learnt from: dieknolle3333
PR: scalableminds/webknossos#8168
File: frontend/javascripts/oxalis/model/sagas/proofread_saga.ts:1039-1039
Timestamp: 2024-11-22T17:18:04.217Z
Learning: In `frontend/javascripts/oxalis/model/sagas/proofread_saga.ts`, when calling `getMagInfo`, the use of `volumeTracingLayer.resolutions` is intentional and should not be changed to `volumeTracingLayer.mags`.
Learnt from: philippotto
PR: scalableminds/webknossos#8542
File: frontend/javascripts/viewer/model/sagas/volumetracing_saga.tsx:0-0
Timestamp: 2025-05-30T12:36:09.930Z
Learning: The WebKnossos frontend follows Redux immutability principles where reducers act immutably and create new state objects when changes occur. This makes reference equality checks (like `prevVolumeTracing.segments !== volumeTracing.segments`) reliable and efficient for detecting actual state changes, allowing memoization to work correctly.
🧬 Code Graph Analysis (3)
frontend/javascripts/viewer/model/sagas/saving/save_mutex_saga.tsx (1)
frontend/javascripts/libs/window.ts (1)
  • location (60-60)
frontend/javascripts/viewer/model/sagas/volume/update_actions.ts (2)
frontend/javascripts/viewer/constants.ts (1)
  • Vector3 (13-13)
frontend/javascripts/viewer/store.ts (1)
  • NumberLike (362-362)
frontend/javascripts/viewer/model/sagas/volume/mapping_saga.ts (3)
frontend/javascripts/viewer/model.ts (2)
  • getSegmentationLayers (93-98)
  • getLayerByName (270-276)
frontend/javascripts/viewer/store.ts (1)
  • ActiveMappingInfo (366-374)
frontend/javascripts/viewer/model/actions/settings_actions.ts (1)
  • setMappingAction (205-227)
🪛 Biome (1.9.4)
frontend/javascripts/viewer/model/sagas/saving/save_saga.ts

[error] 60-60: Unexpected constant condition.

(lint/correctness/noConstantCondition)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: build-smoketest-push
  • GitHub Check: backend-tests
🔇 Additional comments (2)
frontend/javascripts/viewer/model/sagas/volume/update_actions.ts (1)

897-955: Good approach to API evolution with backward compatibility!

The refactoring to make agglomerateId, mag, agglomerateId1, and agglomerateId2 optional fields clearly maintains backward compatibility while simplifying the current API. The comments explaining that these fields are "unused in back-end but may exist in older update actions" provide excellent documentation for future maintainers.

frontend/javascripts/viewer/model/sagas/volume/mapping_saga.ts (1)

166-174: Clean implementation of clearActiveMapping.

The function properly creates a new empty Map and dispatches the appropriate action to clear the mapping state. This will be useful for the deferred mapping updates in save_saga.ts.

Comment on lines +40 to +41
// todop: restore to 10
const VERSION_POLL_INTERVAL_COLLAB = 20 * 1000;
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Complete the TODO: Restore poll interval to 10 seconds.

The poll interval was temporarily increased to 20 seconds but should be restored to 10 seconds as indicated by the TODO comment for proper collaborative editing responsiveness.

-// todop: restore to 10
-const VERSION_POLL_INTERVAL_COLLAB = 20 * 1000;
+const VERSION_POLL_INTERVAL_COLLAB = 10 * 1000;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// todop: restore to 10
const VERSION_POLL_INTERVAL_COLLAB = 20 * 1000;
const VERSION_POLL_INTERVAL_COLLAB = 10 * 1000;
🤖 Prompt for AI Agents
In frontend/javascripts/viewer/model/sagas/saving/save_saga.ts at lines 40 to
41, the poll interval constant VERSION_POLL_INTERVAL_COLLAB is currently set to
20 seconds but should be restored to 10 seconds as per the TODO comment. Change
the value from 20 * 1000 to 10 * 1000 to set the poll interval back to 10
seconds for better collaborative editing responsiveness.

Comment on lines 327 to 382
// yield* call(
// updateMappingWithMerge,
// action.value.actionTracingId,
// activeMapping,
// action.value.agglomerateId1,
// action.value.agglomerateId2,
// );
const layerName = action.value.actionTracingId;
const mappingInfo = yield* select((state) =>
getMappingInfo(state.temporaryConfiguration.activeMappingByLayer, layerName),
);
const dataset = yield* select((state) => state.dataset);
const layerInfo = getLayerByName(dataset, layerName);
const { mappingName } = mappingInfo;

if (mappingName == null) {
throw new Error(
"Could not apply splitAgglomerate because no active mapping was found.",
);
}
updateLocalHdf5FunctionByTracing[layerName] = function* () {
console.log("clearing and refreshing mapping because of split/merge action");
yield* call(clearActiveMapping, action.value.actionTracingId, activeMapping);
yield* call(updateLocalHdf5Mapping, layerName, layerInfo, mappingName);
};
break;
}
case "splitAgglomerate": {
const activeMapping = yield* select(
(store) =>
store.temporaryConfiguration.activeMappingByLayer[action.value.actionTracingId],
);
yield* call(
removeAgglomerateFromActiveMapping,
action.value.actionTracingId,
activeMapping,
action.value.agglomerateId,
);
// yield* call(
// removeAgglomerateFromActiveMapping,
// action.value.actionTracingId,
// activeMapping,
// /* todop:
// * segmentId1 was split from segmentId2. This means both used to map to the same agglomerateId.
// * This UA should "invalidate" the current equivalence class to which segmentId1 and segmentId2
// * belong. All members of that equivalence class should be removed from the active mapping
// * and then re-fetched.
// * If our current mapping contains one (or both) of the segmentIds, we can look up the mapped value
// * and then clear the mapping accordingly (this is important because the local mapping might have diverged
// * from the mapping stored on the server; therefore, even if the UA would also encode the mapped value,
// * that wouldn't be sufficient).
// * However, it might be that none of the segmentIds were fetched yet. In that case, there are two options:
// * 1) the entire equivalence class is unknown to the frontend. Nothing needs to be done now.
// * 2) the equivalence class is known, but the segmentIds from the UA are not known. In that case
// * we still need to find out to which id the segmentIds used to map.
// * This can be done by asking the server. For the request we should pass the version that existed
// * right before the update action (because that is the version where the two segments were merged.)
// */

// action.value.agglomerateId,
// );

Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Verify the deferred mapping update logic is safe.

The code now defers clearing and updating mappings until after all actions are processed, rather than updating them immediately. While this optimization makes sense to avoid redundant operations, it could potentially cause issues if subsequent actions depend on the mapping state being updated.

The extensive TODO comment (lines 363-378) raises valid concerns about the complexity of handling split agglomerate actions, especially when the segmentIds aren't in the local mapping.

The TODO comment outlines a complex scenario for handling unmapped segmentIds. Would you like help implementing the logic to fetch the equivalence class from the server when needed?


🏁 Script executed:

#!/bin/bash
# Check for other places where splitAgglomerate/mergeAgglomerate actions are handled
ast-grep --pattern 'case "splitAgglomerate"'
ast-grep --pattern 'case "mergeAgglomerate"'

Length of output: 93


🏁 Script executed:

#!/bin/bash
# Search for splitAgglomerate usage across the repo
rg 'splitAgglomerate' -n .

# Search for mergeAgglomerate usage across the repo
rg 'mergeAgglomerate' -n .

# Verify where deferred mapping updates are collected
rg 'updateLocalHdf5FunctionByTracing' -n .

Length of output: 5475


🏁 Script executed:

#!/bin/bash
# Display context around deferred mapping setup
sed -n '200,260p' frontend/javascripts/viewer/model/sagas/saving/save_saga.ts
sed -n '300,380p' frontend/javascripts/viewer/model/sagas/saving/save_saga.ts

Length of output: 6409


Implement deferred mapping update for splitAgglomerate

The splitAgglomerate case currently never registers a deferred mapping update—even though the merge case does—so after processing all actions the local mapping may remain stale or inconsistent. You should:

  • In frontend/javascripts/viewer/model/sagas/saving/save_saga.ts, around lines 347–380, add to the splitAgglomerate branch an assignment to updateLocalHdf5FunctionByTracing[layerName], mirroring the merge logic.
  • Inside that generator, call clearActiveMapping and updateLocalHdf5Mapping with the correct layerName, layerInfo, and fetched mappingName.
  • Extend the TODO to fetch the full equivalence class from the server when neither segment ID is known locally, before clearing and reloading.

Suggested diff snippet:

 case "splitAgglomerate": {
   const layerName = action.value.actionTracingId;
   const activeMapping = yield* select(
     (store) => store.temporaryConfiguration.activeMappingByLayer[layerName],
   );
+  const mappingInfo = yield* select((state) =>
+    getMappingInfo(state.temporaryConfiguration.activeMappingByLayer, layerName),
+  );
+  const { mappingName } = mappingInfo;
+  if (mappingName == null) {
+    throw new Error(
+      "Could not apply splitAgglomerate because no active mapping was found.",
+    );
+  }
+  const dataset = yield* select((state) => state.dataset);
+  const layerInfo = getLayerByName(dataset, layerName);
+
+  updateLocalHdf5FunctionByTracing[layerName] = function* () {
+    console.log("clearing and refreshing mapping because of split action");
+    yield* call(clearActiveMapping, layerName, activeMapping);
+    // TODO: if segment IDs aren’t in the local mapping, fetch the old equivalence class
+    //       from the server using the previous version before reloading.
+    yield* call(updateLocalHdf5Mapping, layerName, layerInfo, mappingName);
+  };
   break;
 }

This ensures that split actions are consolidated in the same finalization step and that any missing segments trigger a server lookup before reloading.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// yield* call(
// updateMappingWithMerge,
// action.value.actionTracingId,
// activeMapping,
// action.value.agglomerateId1,
// action.value.agglomerateId2,
// );
const layerName = action.value.actionTracingId;
const mappingInfo = yield* select((state) =>
getMappingInfo(state.temporaryConfiguration.activeMappingByLayer, layerName),
);
const dataset = yield* select((state) => state.dataset);
const layerInfo = getLayerByName(dataset, layerName);
const { mappingName } = mappingInfo;
if (mappingName == null) {
throw new Error(
"Could not apply splitAgglomerate because no active mapping was found.",
);
}
updateLocalHdf5FunctionByTracing[layerName] = function* () {
console.log("clearing and refreshing mapping because of split/merge action");
yield* call(clearActiveMapping, action.value.actionTracingId, activeMapping);
yield* call(updateLocalHdf5Mapping, layerName, layerInfo, mappingName);
};
break;
}
case "splitAgglomerate": {
const activeMapping = yield* select(
(store) =>
store.temporaryConfiguration.activeMappingByLayer[action.value.actionTracingId],
);
yield* call(
removeAgglomerateFromActiveMapping,
action.value.actionTracingId,
activeMapping,
action.value.agglomerateId,
);
// yield* call(
// removeAgglomerateFromActiveMapping,
// action.value.actionTracingId,
// activeMapping,
// /* todop:
// * segmentId1 was split from segmentId2. This means both used to map to the same agglomerateId.
// * This UA should "invalidate" the current equivalence class to which segmentId1 and segmentId2
// * belong. All members of that equivalence class should be removed from the active mapping
// * and then re-fetched.
// * If our current mapping contains one (or both) of the segmentIds, we can look up the mapped value
// * and then clear the mapping accordingly (this is important because the local mapping might have diverged
// * from the mapping stored on the server; therefore, even if the UA would also encode the mapped value,
// * that wouldn't be sufficient).
// * However, it might be that none of the segmentIds were fetched yet. In that case, there are two options:
// * 1) the entire equivalence class is unknown to the frontend. Nothing needs to be done now.
// * 2) the equivalence class is known, but the segmentIds from the UA are not known. In that case
// * we still need to find out to which id the segmentIds used to map.
// * This can be done by asking the server. For the request we should pass the version that existed
// * right before the update action (because that is the version where the two segments were merged.)
// */
// action.value.agglomerateId,
// );
case "splitAgglomerate": {
const layerName = action.value.actionTracingId;
const activeMapping = yield* select(
(store) =>
store.temporaryConfiguration.activeMappingByLayer[layerName],
);
const mappingInfo = yield* select((state) =>
getMappingInfo(
state.temporaryConfiguration.activeMappingByLayer,
layerName,
),
);
const { mappingName } = mappingInfo;
if (mappingName == null) {
throw new Error(
"Could not apply splitAgglomerate because no active mapping was found.",
);
}
const dataset = yield* select((state) => state.dataset);
const layerInfo = getLayerByName(dataset, layerName);
updateLocalHdf5FunctionByTracing[layerName] = function* () {
console.log("clearing and refreshing mapping because of split action");
yield* call(clearActiveMapping, layerName, activeMapping);
// TODO: if segment IDs aren’t in the local mapping, fetch the old equivalence class
// from the server using the previous version before reloading.
yield* call(updateLocalHdf5Mapping, layerName, layerInfo, mappingName);
};
break;
}
🤖 Prompt for AI Agents
In frontend/javascripts/viewer/model/sagas/saving/save_saga.ts around lines 347
to 380, the splitAgglomerate case lacks the deferred mapping update assignment
to updateLocalHdf5FunctionByTracing[layerName] that the merge case has. Fix this
by adding a generator function to updateLocalHdf5FunctionByTracing[layerName]
that calls clearActiveMapping and updateLocalHdf5Mapping with the correct
layerName, layerInfo, and mappingName, similar to the merge case. Also, extend
the existing TODO comment to include logic for fetching the full equivalence
class from the server when neither segment ID is known locally before clearing
and reloading the mapping.

@philippotto
Copy link
Member Author

@coderabbitai pause

Copy link
Contributor

coderabbitai bot commented Jul 10, 2025

✅ Actions performed

Reviews paused.

@@ -132,3 +138,36 @@ export const ensureTracingsWereDiffedToSaveQueueAction = (callback: (tracingId:
type: "ENSURE_TRACINGS_WERE_DIFFED_TO_SAVE_QUEUE",
callback,
}) as const;

export const ensureMaySaveNowAction = (callback: () => void) =>
Copy link
Member Author

Choose a reason for hiding this comment

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

maybe ensureHasMutexAction?

(store) => store.temporaryConfiguration.activeMappingByLayer[volumeTracing.tracingId],
);

if (action.type === "PROOFREAD_MERGE") {
Copy link
Member Author

Choose a reason for hiding this comment

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

only necessary because no real rebasing is implemented yet (instead mapping was potentially cleared by polling saga).

if (agglomerateGraph1 == agglomerateGraph2) {
// Agglomerate is merged with itself. Insert new edge anyway, if it does not exist yet
if (agglomerateGraph1.edges.contains(newEdge)) {
if (agglomerateGraph1.edges.contains(newEdge) || agglomerateGraph1.edges.contains(newEdgeInverted)) {
Copy link
Member Author

Choose a reason for hiding this comment

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

doublecheck whether this is the right way to fix this. I could reproduce this by simulating two users which add the edges A->B and B<-A. afterwards, splitting failed because zipping edges (?) with affinities failed due to length mismatch.

Copy link
Contributor

coderabbitai bot commented Jul 29, 2025

✅ Actions performed

Reviews paused.

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.

Milestone 3: Proofreading (without segment list)
3 participants