Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
31a87d0
fix(gallery): recall legacy guidance, Krea-2 seed variance, empty ref…
joshistoast Oct 11, 2026
5cabce5
Merge branch 'wt/parity-recall' into feat/webv1-parity-batch
joshistoast Oct 11, 2026
68d5f9c
feat(gallery): warn on in-use deletion, load video workflows, add det…
joshistoast Oct 11, 2026
b7f7175
feat(workflow): honor authored form controls, link form URLs, arrange…
joshistoast Oct 11, 2026
1cdf4f4
Merge branch 'wt/parity-gallery' into feat/webv1-parity-batch
joshistoast Oct 11, 2026
5f414f2
docs(workflows): describe the Arrange nodes toolbar action
joshistoast Oct 11, 2026
91a7f6a
feat(queue): restore front-of-queue invoke, retry, batch cancel, hist…
joshistoast Oct 11, 2026
898ffb5
refactor(gallery): share the embedded-workflow loader between images …
joshistoast Oct 11, 2026
0de7ef7
Merge branch 'wt/parity-workflow' into feat/webv1-parity-batch
joshistoast Oct 11, 2026
d8addf4
Merge branch 'wt/parity-queue' into feat/webv1-parity-batch
joshistoast Oct 11, 2026
4e821af
fix(workflow): arrange unconnected nodes into a grid
joshistoast Oct 11, 2026
4c5a27b
fix(workflow): honor authored form bounds on invoke and keep inverted…
joshistoast Oct 11, 2026
429a6cb
perf(workbench): keep editor boot request count unchanged
joshistoast Oct 11, 2026
73cb580
chore(perf): re-record route byte baselines for the added locale stri…
joshistoast Oct 11, 2026
e57c5f1
Merge branch 'main' into feat/webv1-parity-batch
joshistoast Oct 11, 2026
54902fb
fix(workflow): drop a manual memo the React Compiler rule rejects
joshistoast Oct 11, 2026
f5ba648
fix(workflow): end slider tracks on the step multiples the node accepts
joshistoast Oct 11, 2026
7f4f06c
fix(queue): keep Invoke Front through run recovery
joshistoast Oct 12, 2026
8a5d4fd
fix(queue): explain a cache without capacity and an unreadable histor…
joshistoast Oct 12, 2026
9eafee4
perf(queue): stop rereading finished details and sessions to choose w…
joshistoast Oct 12, 2026
98bd979
fix(queue): report a batch with nothing left to cancel and announce d…
joshistoast Oct 12, 2026
6cf20a3
feat(queue): return retried runs to where they were headed
joshistoast Oct 12, 2026
4ec22ca
fix(workflow): ignore form bounds beyond the node and warn on every c…
joshistoast Oct 12, 2026
907712a
fix(gallery): fall back by name for reference-image models and tidy d…
joshistoast Oct 12, 2026
af78170
fix(queue): add a video rerun its active run cannot adopt to that run…
joshistoast Oct 12, 2026
66f8cd8
fix(workflow): keep the precision of steps written in exponent form
joshistoast Oct 12, 2026
f1c7d03
fix(queue): stage a rerun no run adopted in one tab only
joshistoast Oct 12, 2026
5176424
fix(queue): keep the staging selection when a retry reopens a run
joshistoast Oct 12, 2026
1d7398e
fix(queue): adopt a retry from its own response and ignore other user…
joshistoast Oct 12, 2026
8595984
fix(queue): say where a closed project's Canvas retry is delivered
joshistoast Oct 12, 2026
407cd56
fix(queue): keep focus on retried reads
joshistoast Oct 12, 2026
cb5ce07
fix(workflow): warn when an authored step is ignored
joshistoast Oct 12, 2026
10b1fe7
refactor(gallery): key the video menu query under galleryKeys
joshistoast Oct 12, 2026
3a27938
test: cover Invoke Front dispatch, retry lineage journaling, mock cha…
joshistoast Oct 12, 2026
7853792
fix(queue): stage every sibling rerun a tab finishes while staging th…
joshistoast Oct 13, 2026
ad56c41
fix(queue): claim a rerun's delivery durably so no later tab stages i…
joshistoast Oct 13, 2026
b374b39
fix(queue): stage a rerun only where its project is open and never re…
joshistoast Oct 13, 2026
4320e64
feat(queue): let browsers claim a queue item's delivery
joshistoast Oct 13, 2026
4d6875b
fix(queue): stage a retried Canvas result once across tabs and browsers
joshistoast Oct 13, 2026
3faae9d
Merge branch 'main' into feat/webv1-parity-batch
Pfannkuchensack Oct 13, 2026
be0b41f
Merge branch 'main' into feat/webv1-parity-batch
joshistoast Oct 15, 2026
6ff2fbe
Merge branch 'main' into feat/webv1-parity-batch
joshistoast Oct 17, 2026
f16f23f
Merge branch 'main' into feat/webv1-parity-batch
Pfannkuchensack Oct 17, 2026
5995943
Merge remote-tracking branch 'origin/main' into feat/webv1-parity-batch
joshistoast Oct 17, 2026
0f6d680
test(gallery): wait for the grid before scrolling to a load-more failure
joshistoast Oct 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,9 @@ If a required component is missing, **Invoke** is disabled and its tooltip names

</Steps>

Hover over **Invoke** to see what will be queued, for example *1 prompt × 4 iterations → 4 generations*. When Invoke is disabled, the same tooltip lists what is stopping it.
Hover over **Invoke** to see what will be queued, for example *1 prompt × 4 iterations → 4 generations*. When Invoke is disabled, the same tooltip lists what is stopping it. To run a batch ahead of the work you already have queued, press **Ctrl+Shift+Enter** (**Cmd+Shift+Enter** on macOS) or choose **Invoke (Front of Queue)** from the command palette. When several people share a server, the default round-robin queue still alternates between users, so the batch only moves ahead of your own jobs; a server set to first-in, first-out (`session_queue_mode: FIFO`) runs it ahead of everyone's pending jobs.

The **Queue** panel lists recent jobs. Cancel a pending, waiting or running job from its row, and retry a failed or canceled job from the session the server saved for it, independently of your current settings. A retried job's results go where the original was headed while the tab that sent it is still open: back to its place in the Canvas staging area, or to the Gallery board it was sent to. This also holds when you retry several failed items of one batch one after another. A retried Canvas job is staged in a tab where its project is open; if that project is open nowhere, its result does not appear. Once that tab is gone, for example after a reload, or once its Canvas was replaced with a new document, Canvas results are staged once, at the current bounding box, by a tab with the project open, and Gallery results appear in the Gallery, on **Uncategorized** unless the job itself saved them to a board. Expand a row to cancel its whole batch while other batches keep running, or to open **View JSON**, which shows the graph the server actually ran, its results and the embedded workflow. The panel menu clears failed items, clears every finished item while keeping queued and running work, or clears the whole queue. Administrators also see the **Invocation Cache** beneath the queue, next to the model cache, where cached node outputs can be cleared, disabled or re-enabled.

## Routing images to the Gallery or the Canvas

Expand Down
4 changes: 2 additions & 2 deletions docs/src/content/docs/Users Guide/06.Gallery/introduction.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -148,9 +148,9 @@ Drag items onto a board in the board list; if the list is hidden, it opens while

### Deleting media

Press **Delete** (or **Backspace**), choose **Delete Image**, **Delete Video** or **Delete Selection** from the menu, or use the delete button in the selection bar. Deleting permanently removes the items from every board and from disk, and cannot be undone. A confirmation asks first. Tick **Don't ask me again** and confirm to stop asking (cancelling leaves it on); you can turn it back on, or off, with **Confirm image deletion** in Settings under Gallery.
Press **Delete** (or **Backspace**), choose **Delete Image**, **Delete Video** or **Delete Selection** from the menu, or use the delete button in the selection bar. Deleting permanently removes the items from every board and from disk, and cannot be undone. A confirmation asks first. Tick **Don't ask me again** and confirm to stop asking for ordinary deletions (cancelling leaves it on); you can turn it back on, or off, with **Confirm image deletion** in Settings under Gallery.

Deleting does not check whether an image is still used elsewhere, for example on a canvas or as a reference image, so check before you delete media you are still working with. If deleting is interrupted, InvokeAI cleans up any leftover files the next time it starts.
When the open project still uses something you are deleting, for example as a canvas layer, a reference image, an upscale or video input, or in a workflow, the confirmation always appears and lists where, even if you turned confirmations off. Only the open project's current work is checked: other projects, undo history and canvas snapshots are not, so check before you delete media you still need there. If deleting is interrupted, InvokeAI cleans up any leftover files the next time it starts.

## Related pages

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ Hover over a numbered circle, or tap it on a touchscreen, for a short descriptio
|---|---|---|
| 1 | **Workflow panel** | The workflow's **form**: its most important settings as simple controls, like a Generate panel for this workflow. |
| 2 | **View** / **Edit** | **View** uses the form. **Edit** lets you design the form, edit the workflow's details (name, description, author, tags, notes), and inspect the selected node. |
| 3 | **Editor toolbar** | Tools for panning, box and lasso selection, and erasing nodes or connections; **Zoom in**, **Zoom out** and **Fit view**; **Download workflow as PNG**; and **Node opacity**. |
| 3 | **Editor toolbar** | Tools for panning, box and lasso selection, and erasing nodes or connections; **Zoom in**, **Zoom out** and **Fit view**; **Arrange nodes**, which lays out the whole graph, or just the selected nodes in place, as one undoable step; **Download workflow as PNG**; and **Node opacity**. |
| 4 | **Workflow name** | The active workflow. Click it to open the **Workflows** library. |
| 5 | **Workflow actions** | **Add node** (**+**), **Save to library…**, the save status, settings, and the **⋯** menu with **Workflow details…**, **Rename workflow…**, **Import workflow JSON…**, **Export workflow JSON**, **New workflow** and more. |
| 6 | **The graph** | The workflow's nodes and connections. Drag nodes by their title bar; scroll to zoom; drag empty space to pan. |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ INVOKEAI_SESSION_QUEUE_MODE=FIFO
`session_queue_mode` only applies in multiuser mode. In single-user mode the queue is always FIFO regardless of this setting, since all jobs belong to the same account.
:::

Round-robin fairness is determined from when each user's jobs were last started. Retained terminal queue history (see [`max_queue_history`](#configuration-reference)) does not slow dequeue scheduling — each dequeue's cost scales with the number of users who currently have pending jobs, not with the size of the history.
Round-robin fairness is determined from when each user's jobs were last started. Retained terminal queue history (see [`max_queue_history`](#configuration-reference)) does not slow dequeue scheduling — each dequeue's cost scales with the number of users who currently have pending jobs, not with the size of the history. Administrators can change `max_queue_history` without editing the file: open **Settings → Server → Queue history**, where a blank value keeps every finished item and `0` prunes them all at the next start.

### First Administrator Account

Expand Down
36 changes: 36 additions & 0 deletions invokeai/app/api/routers/session_queue.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@
EnqueueReceiptLimitError,
ItemIdsResult,
PruneResult,
QueueItemDeliveryRequest,
QueueItemDeliveryResult,
RetryItemsResult,
SessionQueueCountsByDestination,
SessionQueueItem,
Expand Down Expand Up @@ -882,6 +884,40 @@ def cancel_queue_item(
raise HTTPException(status_code=500, detail=f"Unexpected error while canceling queue item: {e}")


@session_queue_router.post(
"/{queue_id}/i/{item_id}/delivery",
operation_id="claim_queue_item_delivery",
response_model=QueueItemDeliveryResult,
)
def claim_queue_item_delivery(
current_user: CurrentUserOrDefault,
queue_id: str = Path(description="The queue id to perform this operation on"),
item_id: int = Path(description="The queue item whose results are delivered"),
request: QueueItemDeliveryRequest = Body(description="The delivery change"),
) -> QueueItemDeliveryResult:
"""Claims, confirms or releases staging a queue item's results, so that only one of its owner's browsers stages
them. A claim is held for a while unless confirmed, released or renewed."""
session_queue = ApiDependencies.invoker.services.session_queue
try:
# Waiting browsers ask often: authorize by the item's owner alone, without reading its session.
owner = session_queue.get_queue_item_owner(item_id)
if owner.queue_id != queue_id:
raise HTTPException(status_code=404, detail=f"Queue item with id {item_id} not found in queue {queue_id}")
if owner.user_id != current_user.user_id and not current_user.is_admin:
raise HTTPException(status_code=403, detail=f"You do not have permission to mutate queue item {item_id}")
if request.action == "claim":
return session_queue.claim_queue_item_delivery(item_id, request.claimant)
if request.action == "confirm":
return session_queue.confirm_queue_item_delivery(item_id)
return session_queue.release_queue_item_delivery(item_id, request.claimant)
except SessionQueueItemNotFoundError:
raise HTTPException(status_code=404, detail=f"Queue item with id {item_id} not found in queue {queue_id}")
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=f"Unexpected error while changing queue item delivery: {e}")


@session_queue_router.get(
"/{queue_id}/counts_by_destination",
operation_id="counts_by_destination",
Expand Down
26 changes: 17 additions & 9 deletions invokeai/app/api/sockets.py
Original file line number Diff line number Diff line change
Expand Up @@ -920,16 +920,24 @@ async def _emit_bulk_queue_item_event(
"""
item_ids_by_user: dict[str, list[int]] = getattr(event_data, item_ids_by_user_field)
admin_sids = self._admin_sids()

def scoped(item_ids: list[int], user_ids: list[str]) -> dict[str, Any]:
update: dict[str, Any] = {
item_ids_field: item_ids,
"user_ids": user_ids,
item_ids_by_user_field: dict.fromkeys(user_ids, item_ids),
}
if isinstance(event_data, QueueItemsRetriedEvent):
# A retry's new item ids are as private as the retried ids they are paired with.
owned_item_ids = set(item_ids)
update["retried_items"] = [
item for item in event_data.retried_items if item.source_item_id in owned_item_ids
]
return update

for user_id in event_data.user_ids:
user_room = f"user:{user_id}"
owner_item_ids = item_ids_by_user.get(user_id, [])
owner_event_data = event_data.model_copy(
update={
item_ids_field: owner_item_ids,
"user_ids": [user_id],
item_ids_by_user_field: {user_id: owner_item_ids},
}
)
owner_event_data = event_data.model_copy(update=scoped(item_ids_by_user.get(user_id, []), [user_id]))
await self._sio.emit(
event=event_name,
data=owner_event_data.model_dump(mode="json"),
Expand All @@ -938,7 +946,7 @@ async def _emit_bulk_queue_item_event(
)
await self._sio.emit(event=event_name, data=event_data.model_dump(mode="json"), room="admin")

sanitized = event_data.model_copy(update={item_ids_field: [], "user_ids": [], item_ids_by_user_field: {}})
sanitized = event_data.model_copy(update=scoped([], []))
await self._sio.emit(
event=event_name,
data=sanitized.model_dump(mode="json"),
Expand Down
5 changes: 5 additions & 0 deletions invokeai/app/services/events/events_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
QUEUE_ITEM_STATUS,
BatchStatus,
EnqueueBatchResult,
RetriedQueueItem,
RetryItemsResult,
SessionQueueItem,
SessionQueueStatus,
Expand Down Expand Up @@ -384,6 +385,9 @@ class QueueItemsRetriedEvent(QueueEventBase):
retried_item_ids_by_user: dict[str, list[int]] = Field(
description="The retried root queue item IDs keyed by owner user ID."
)
retried_items: list[RetriedQueueItem] = Field(
default_factory=list, description="Each retried root queue item with the queue item that re-runs it"
)

@classmethod
def build(
Expand All @@ -394,6 +398,7 @@ def build(
retried_item_ids=retry_result.retried_item_ids,
user_ids=user_ids,
retried_item_ids_by_user=retried_item_ids_by_user,
retried_items=retry_result.retried_items,
)


Expand Down
23 changes: 23 additions & 0 deletions invokeai/app/services/session_queue/session_queue_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
ItemIdsResult,
NodeFieldValue,
PruneResult,
QueueItemDeliveryResult,
QueueItemOwner,
RetryItemsResult,
SessionQueueCountsByDestination,
SessionQueueItem,
Expand Down Expand Up @@ -292,6 +294,27 @@ def get_queue_item_for_api(self, item_id: int) -> SessionQueueItem:
"""Gets a queue item in the response projection used by API reads."""
return self.get_queue_item(item_id=item_id)

@abstractmethod
def get_queue_item_owner(self, item_id: int) -> QueueItemOwner:
"""Gets a queue item's queue and owner without reading its session, for authorizing frequent requests."""
pass

@abstractmethod
def claim_queue_item_delivery(self, item_id: int, claimant: str) -> QueueItemDeliveryResult:
"""Claims delivering a queue item's results for a claimant: `claimed` unless they were delivered or another
claimant's unexpired claim holds them."""
pass

@abstractmethod
def confirm_queue_item_delivery(self, item_id: int) -> QueueItemDeliveryResult:
"""Marks a queue item's results delivered, whoever claimed them."""
pass

@abstractmethod
def release_queue_item_delivery(self, item_id: int, claimant: str) -> QueueItemDeliveryResult:
"""Drops a claimant's claim of a queue item's undelivered results, if it holds one."""
pass

@abstractmethod
def get_queue_item_workflow_json(self, item_id: int) -> str | None:
"""Gets only the workflow metadata JSON for a queue item, if present."""
Expand Down
44 changes: 43 additions & 1 deletion invokeai/app/services/session_queue/session_queue_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import json
from collections.abc import Mapping
from itertools import chain, product
from typing import Any, Generator, Literal, Optional, TypeAlias, Union
from typing import Any, Generator, Literal, NamedTuple, Optional, TypeAlias, Union

from pydantic import (
AliasChoices,
Expand Down Expand Up @@ -536,9 +536,51 @@ def validate_result(self) -> "EnqueueBatchReceipt":
return self


class RetriedQueueItem(BaseModel):
source_item_id: int = Field(description="The ID of the retried root queue item")
item_id: int = Field(description="The ID of the queue item that re-runs it")


class RetryItemsResult(BaseModel):
queue_id: str = Field(description="The ID of the queue")
retried_item_ids: list[int] = Field(description="The IDs of the queue items that were retried")
retried_items: list[RetriedQueueItem] = Field(
default_factory=list, description="Each retried root queue item with the queue item that re-runs it"
)


QueueItemDeliveryAction: TypeAlias = Literal["claim", "confirm", "release"]
QueueItemDeliveryStatus: TypeAlias = Literal["claimed", "delivered", "held", "released"]


class QueueItemDeliveryRequest(BaseModel):
"""A change to who stages a queue item's results, so that only one browser does."""

action: QueueItemDeliveryAction = Field(
description="claim: take delivering the results; confirm: mark them delivered; release: give up a claim"
)
claimant: str = Field(
min_length=1, max_length=128, description="An id the client chooses for the browser tab or run claiming"
)


class QueueItemOwner(NamedTuple):
"""The queue a queue item is in and the account that owns it."""

queue_id: str
user_id: str


class QueueItemDeliveryResult(BaseModel):
"""Who stages a queue item's results after a delivery request."""

status: QueueItemDeliveryStatus = Field(
description="claimed: the claimant may deliver; delivered: the results were delivered; "
"held: another claimant is delivering them; released: the claimant holds no claim"
)
retry_after_ms: Optional[int] = Field(
default=None, description="When held, how long until the other claim lapses, in milliseconds"
)


class ClearResult(BaseModel):
Expand Down
Loading
Loading