diff --git a/docs/video-batch-continuation.md b/docs/video-batch-continuation.md
new file mode 100644
index 0000000..78b991c
--- /dev/null
+++ b/docs/video-batch-continuation.md
@@ -0,0 +1,12 @@
+# Video batch continuation and HostBuffer failure
+
+The September 9 investigation found two separate problems:
+
+1. Automatic continuation defaulted off and its checkbox had moved inside Generation Settings. Paused batches hid their “Run remaining” controls. Resume reused the previous one-shot budget, so it paused again after the next shot. Resume also restored a live flag before the shot queue overwrote it with `autoRun=false`.
+2. The local `aigen-headless.log` traceback reports `comfy_aimdo/host_buffer.py` → `comfy/memory_management.py` while the Qwen text encoder is loading weights. This is not FFmpeg frame extraction. Commits `da8d0e4` and `4bf9922` change FFmpeg retries and do not address that model-loader failure.
+
+New video batches now default to automatic continuation. Its checkbox stays outside the settings drawer. Paused batches show “Run remaining”; the bounded action says “Run only shot N”. Resume restores automatic continuation in both the Studio row and shot queue and passes the refreshed run-all policy to the dispatcher. Explicit one-shot / next-three actions remain bounded. Errors still stop the batch instead of silently skipping failed shots.
+
+For the observed Windows HostBuffer failure, the connector now prepares Comfy's supported `--disable-dynamic-vram` launch flag when that installed version supports it. This selects estimate-based loading instead of AIMDO dynamic loading. It changes memory management, not the generation graph, handoff frame, refinement, model precision, or source segments. It may change loading speed and memory use; GPU inference has not been tested. `COMFY_DYNAMIC_VRAM=true` on the connector opts back into upstream defaults.
+
+The memory change takes effect only after the connector is reloaded and Comfy is next started. Do not restart either during an active render. No running job or existing queue was changed during diagnosis. The checkbox default applies to new batches; use “Run remaining” on an existing paused batch after deploying the UI fix.
diff --git a/pages/index.vue b/pages/index.vue
index ed887b1..bec7143 100644
--- a/pages/index.vue
+++ b/pages/index.vue
@@ -1099,16 +1099,6 @@
-
-
-
- Run remaining shots automatically
- Overnight set-and-forget. Leave this off to edit prompts on Queue (under this job) and process the next shot when you are ready.
-
-
+
+
+
+ Run remaining shots automatically
+ Overnight set-and-forget. Leave this off to edit prompts on Queue (under this job) and process the next shot when you are ready.
+
+
+
(null)
const unlockPassword = ref('')
-const queueAutoRun = ref(false)
+const queueAutoRun = ref(true)
const refineExtensionFrame = ref(true)
const saveLosslessAnchor = ref(true)
const refinementDenoise = ref(REFINEMENT_DENOISE_DEFAULT)
diff --git a/pages/queue.vue b/pages/queue.vue
index 91b78f4..a075821 100644
--- a/pages/queue.vue
+++ b/pages/queue.vue
@@ -94,7 +94,7 @@
:disabled="Boolean(busyId)"
@click="cutIn(job)"
>
- {{ job.cutIn ? 'Next after current shot' : (canCutIn ? 'Run next' : 'Start now') }}
+ {{ job.cutIn ? 'Next after current shot' : (canCutIn ? 'Prioritize batch' : 'Start batch') }}
!['--enable-dynamic-vram', '--disable-dynamic-vram', '--fast-disk'].includes(arg)), '--disable-dynamic-vram']
+}
diff --git a/server/api/queues/[id]/pause.post.ts b/server/api/queues/[id]/pause.post.ts
index ffdd803..3e72b51 100644
--- a/server/api/queues/[id]/pause.post.ts
+++ b/server/api/queues/[id]/pause.post.ts
@@ -4,7 +4,7 @@ import { toggleStudioQueuePause } from '~/server/utils/studioQueue'
export default defineEventHandler(async (event) => {
const { owner } = assertLibraryOwner(event)
const id = String(getRouterParam(event, 'id') || '')
- const body = await readBody<{ resume?: boolean }>().catch(() => ({} as { resume?: boolean }))
+ const body = await readBody<{ resume?: boolean }>(event).catch(() => ({} as { resume?: boolean }))
const queue = getShotQueue(owner, id)
if (!queue) throw createError({ statusCode: 404, statusMessage: 'Queue not found' })
const result = await toggleStudioQueuePause(owner, {
diff --git a/server/utils/shotQueue.ts b/server/utils/shotQueue.ts
index 4edc0b6..fef4436 100644
--- a/server/utils/shotQueue.ts
+++ b/server/utils/shotQueue.ts
@@ -445,6 +445,7 @@ export async function setShotQueuePause(owner: string, id: string, pause: boolea
else if (queue.status !== 'complete') queue.status = 'paused'
} else {
queue.stopAfterCurrent = false
+ if (pendingSegmentCount(queue) > 0) queue.autoRun = true
if (live?.library) live.library.queueAutoRun = queue.autoRun === true
if (queue.status === 'paused') queue.status = 'idle'
}
diff --git a/server/utils/studioQueue.ts b/server/utils/studioQueue.ts
index 58f8065..34e6f0c 100644
--- a/server/utils/studioQueue.ts
+++ b/server/utils/studioQueue.ts
@@ -1808,6 +1808,10 @@ export async function toggleStudioQueuePause(owner: string, options: {
if (options.queueId && job.shotQueueId !== options.queueId) continue
job.pauseAfterCurrent = false
job.pausedByUser = false
+ if (job.shotQueueId && job.status !== 'complete' && job.status !== 'cancelled') {
+ job.resumeAutoRun = true
+ job.payload.queueAutoRun = true
+ }
}
syncPausedFlag(store)
return structuredClone(store)
@@ -1831,7 +1835,7 @@ export async function toggleStudioQueuePause(owner: string, options: {
}
const busy = await videoJobsBusy()
if (heldPaused[0] && !busy) {
- await resumeHeldStudioJob(heldPaused[0]).catch(() => null)
+ await resumeHeldStudioJob(rows.find(row => row.id === heldPaused[0].id) || heldPaused[0]).catch(() => null)
} else {
if (heldPaused.length && busy) {
await mutate(owner, (jobs) => {
@@ -1936,6 +1940,10 @@ export async function onStudioQueueBurstStarted(owner: string, queueId: string,
if (queue?.autoRun === true) {
row.resumeAutoRun = true
row.payload.queueAutoRun = true
+ } else if (queue) {
+ // An explicit one-shot / next-three request must replace an earlier run-all policy.
+ row.resumeAutoRun = false
+ row.payload.queueAutoRun = false
}
row.updatedAt = Date.now()
syncPausedFlag(store)
diff --git a/tests/comfy-memory-policy.test.mjs b/tests/comfy-memory-policy.test.mjs
new file mode 100644
index 0000000..b216e2e
--- /dev/null
+++ b/tests/comfy-memory-policy.test.mjs
@@ -0,0 +1,11 @@
+import test from 'node:test'
+import assert from 'node:assert/strict'
+import { stableMemoryArgs } from '../scripts/comfy-memory-policy.mjs'
+
+test('Windows uses the supported non-dynamic loader without changing workflow or precision flags', () => {
+ const args = ['main.py', '--enable-dynamic-vram', '--fast-disk', '--enable-manager', '--fp16-vae']
+ assert.deepEqual(stableMemoryArgs(args, { platform: 'win32', cliSource: '--disable-dynamic-vram', dynamicVram: 'false' }), ['main.py', '--enable-manager', '--fp16-vae', '--disable-dynamic-vram'])
+ assert.deepEqual(stableMemoryArgs(args, { platform: 'win32', cliSource: '' }), args)
+ assert.deepEqual(stableMemoryArgs(args, { platform: 'linux', cliSource: '--disable-dynamic-vram' }), args)
+ assert.deepEqual(stableMemoryArgs(args, { platform: 'win32', cliSource: '--disable-dynamic-vram', dynamicVram: 'true' }), args)
+})
diff --git a/tests/studio-queue.test.mjs b/tests/studio-queue.test.mjs
index e661b6b..667d7f6 100644
--- a/tests/studio-queue.test.mjs
+++ b/tests/studio-queue.test.mjs
@@ -18,7 +18,7 @@ const compiled = ts.transpileModule(source, {
// Execute the real queue module against temporary stores and a fake GPU.
// No production library, network, Comfy process, or render is touched.
-function fixture({ rows = [], lives = [], queue = { running: 0, pending: 0 }, history = {} } = {}) {
+function fixture({ rows = [], lives = [], queue = { running: 0, pending: 0 }, history = {}, shotQueues = [] } = {}) {
const root = mkdtempSync(join(tmpdir(), 'aigen-queue-test-'))
const owner = 'test-owner'
mkdirSync(join(root, 'users', owner), { recursive: true })
@@ -39,7 +39,9 @@ function fixture({ rows = [], lives = [], queue = { running: 0, pending: 0 }, hi
patchPendingJob: () => {}, deletePendingJob: () => {}
},
'~/server/utils/shotQueue': {
- getShotQueue: () => null, pendingSegmentCount: () => 0
+ getShotQueue: (_owner, id) => shotQueues.find(q => q.id === id), pendingSegmentCount: queue => queue?.pendingCount || 0,
+ isShotQueueCancelled: () => false, listShotQueues: () => shotQueues,
+ setShotQueuePause: async (_owner, id, pause) => { const q = shotQueues.find(q => q.id === id); if (q) { q.stopAfterCurrent = pause; q.autoRun = !pause } }, pauseShotQueue: async () => {}
},
'~/server/utils/comfy': { fetchLiveQueue: async () => queue, fetchHistory: async () => {
if (history instanceof Error) throw history
@@ -55,6 +57,7 @@ function fixture({ rows = [], lives = [], queue = { running: 0, pending: 0 }, hi
'~/utils/imageV2': { imageV2StackSpecials: () => ({}) },
'~/utils/globalLocks': { allowIdentityRefs: () => false },
'~/server/utils/generationLog': { updateGenerationLogByStudioJob: async () => {} },
+ '~/server/utils/videoChain': { startQueueBurst: async (_owner, id, count) => { started.push({ id, count }); return { jobId: 'resumed-video' } } },
'~/server/utils/musicChain': {
startMusicJob: async () => {
const live = liveJob(`started-${started.length}`, 'music')
@@ -191,3 +194,20 @@ for (const history of [new Error('history timed out'), null]) {
assert.equal(f.started.length, 0)
})
}
+
+test('Resume a manually held video batch runs all remaining shots instead of one', async () => {
+ const item = { ...row('paused-video', 'held', undefined), kind: 'video', shotQueueId: 'shots', pausedByUser: true, payload: { prompt: 'video', queueAutoRun: false, extensions: [] } }
+ const shots = { id: 'shots', autoRun: false, stopAfterCurrent: true, pendingCount: 9, status: 'paused' }
+ const f = fixture({ rows: [item], shotQueues: [shots] })
+ await f.api.toggleStudioQueuePause('test-owner', { jobId: item.id, resume: true })
+ assert.deepEqual(f.started, [{ id: 'shots', count: 'all' }])
+ assert.equal(f.rows()[0].payload.queueAutoRun, true)
+ assert.equal(shots.autoRun, true)
+})
+test('explicit bounded burst clears an earlier run-all policy', async () => {
+ const item = { ...row('bounded', 'held', undefined), kind: 'video', shotQueueId: 'shots', resumeAutoRun: true, payload: { prompt: 'video', queueAutoRun: true, extensions: [] } }
+ const f = fixture({ rows: [item], shotQueues: [{ id: 'shots', autoRun: false, pendingCount: 9 }] })
+ await f.api.onStudioQueueBurstStarted('test-owner', 'shots', 'one-shot-live')
+ assert.equal(f.rows()[0].resumeAutoRun, false)
+ assert.equal(f.rows()[0].payload.queueAutoRun, false)
+})