mirror of
https://github.com/storytold/photocraft.git
synced 2026-11-07 12:13:10 +00:00
* MCP headless: waited commands run as jobs, with progress and cancel (#8129) Headless command_run and command_batch steps that wait (the default) ran job-capable commands inline while holding the session lock. Cancelling a long Gaussian Blur did not stop it: it still added its history step, and doc_inspect and jobs_list queued behind it. No progress was reported. A waited job-capable command now starts as a background job (Headless::command_begin) and the server polls it with the lock released (Headless::job_poll), so other calls are served meanwhile. With a progress token it sends notifications/progress (strictly increasing, at most ten a second; for a batch, finished steps plus the current step's fraction). notifications/cancelled cancels the job and leaves the document unchanged; a drop guard does the same when rmcp drops the request future. Headless keeps the outcome of each waited job as sync_jobs sees it end, since jobs.list keeps only the last few. Both backends share one batch loop, which runs no further step once the request is cancelled. Tool hints: jobs_list is read-only, doc_select is non-destructive and idempotent, and the scoped exports (which refuse to overwrite) are non-destructive. doc_save's description lost a run of spaces. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * MCP headless: cancel a waited job whose request was dropped before it started The Abandon guard was created only after command_begin returned. A request dropped while the start still waited for the session lock left the job it then started running, and its `waited` entry behind. The guard is now armed inside the locked closure that starts the job, so dropping the unread output cancels it. Review feedback on #8132. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * MCP headless: job_poll applies finished jobs before checking the job A caller polling Headless::job_poll directly, without sync_jobs in between, kept seeing Running after the worker finished, because the session only drops a finished job from its running list when its jobs are polled. job_poll now syncs first. The MCP path already synced in headless_op. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * MCP headless: a cancel that loses the race returns the job's result job_abandon ignored whether Session::cancel_job cancelled anything and always dropped the stored outcome, so headless_wait answered `Cancelled` for a job another request had already applied to the document. It now returns how the job really ended: Cancelled only when it cancelled the job, the job's result when it had already ended, and an error when the job cannot be cancelled (printing) and is still running. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * MCP headless: waited commands run as jobs, with progress and cancel (#8129) Headless command_run and command_batch steps that wait (the default) ran job-capable commands inline while holding the session lock. Cancelling a long Gaussian Blur did not stop it: it still added its history step, and doc_inspect and jobs_list queued behind it. No progress was reported. A waited job-capable command now starts as a background job (Headless::command_begin) and the server polls it with the lock released (Headless::job_poll), so other calls are served meanwhile. With a progress token it sends notifications/progress (strictly increasing, at most ten a second; for a batch, finished steps plus the current step's fraction). notifications/cancelled cancels the job and leaves the document unchanged; a drop guard does the same when rmcp drops the request future. Headless keeps the outcome of each waited job as sync_jobs sees it end, since jobs.list keeps only the last few. Both backends share one batch loop, which runs no further step once the request is cancelled. Tool hints: jobs_list is read-only, doc_select is non-destructive and idempotent, and the scoped exports (which refuse to overwrite) are non-destructive. doc_save's description lost a run of spaces. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * MCP headless: cancel a waited job whose request was dropped before it started The Abandon guard was created only after command_begin returned. A request dropped while the start still waited for the session lock left the job it then started running, and its `waited` entry behind. The guard is now armed inside the locked closure that starts the job, so dropping the unread output cancels it. Review feedback on #8132. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * MCP bridge: waited commands poll the app's job, with progress and cancel (#8130) A waited command_run or command_batch step of an engine command now starts it with engine.execute {wait: false} (sent once) and, when the app runs it as a background job, polls jobs.list until it ends. A job can run past the bridge's 55s reply timeout, its progress is forwarded, and cancelling or dropping the request sends jobs.cancel. UI command ids keep wait: true. A failed poll or a job no longer listed is reported with its id rather than guessed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * MCP bridge: keep the cancel guard armed until jobs.cancel returns; clarify the docs The cancel branch disarmed the Abandon guard before awaiting jobs.cancel, so a request future dropped during that await sent no cancel. The control-protocol table now says polling applies only to a waited command_run. Review feedback on #8134. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * MCP headless: job_poll applies finished jobs before checking the job A caller polling Headless::job_poll directly, without sync_jobs in between, kept seeing Running after the worker finished, because the session only drops a finished job from its running list when its jobs are polled. job_poll now syncs first. The MCP path already synced in headless_op. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * MCP headless: a cancel that loses the race returns the job's result job_abandon ignored whether Session::cancel_job cancelled anything and always dropped the stored outcome, so headless_wait answered `Cancelled` for a job another request had already applied to the document. It now returns how the job really ended: Cancelled only when it cancelled the job, the job's result when it had already ended, and an error when the job cannot be cancelled (printing) and is still running. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * MCP bridge: a refused jobs.cancel is not reported as a cancellation bridge_wait discarded the app's reply to jobs.cancel and always answered `Cancelled`. The app refuses to cancel a job that is no longer running, so a job that finished between the jobs.list poll and the cancel was reported as cancelled although its edit may be applied. A refused cancel now answers that the job was not cancelled and points to jobs_list. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * MCP bridge: a cancel during a failed poll still cancels the job When notifications/cancelled arrived while a jobs.list poll was in flight, and the poll and its retry then failed, bridge_wait disarmed the Abandon guard and returned the poll error without checking the waiter, so no jobs.cancel reached the app and the edit ran on after the caller cancelled it. That error path now sends jobs.cancel first when the request was cancelled. A failed poll without a cancel still leaves the job running and names it. Reported in review of #8134. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * MCP bridge: a cancel doesn't wait for a slow jobs.list poll bridge_wait checked for a cancel only between polls, so a cancel that arrived while the app was slow to answer jobs.list waited for the poll, up to the 55s call timeout, before jobs.cancel was sent. The poll now races the cancel. That needs BridgeClient to drop the connection of a call abandoned before its reply: it kept the one cached connection, so jobs.cancel queued behind the unanswered poll on the same socket (the app serves a connection's requests in order), and a half-sent request could share it. A call now takes the connection out of the cache and returns it only once answered, as a timeout or lost reply already dropped it. The poll-error check from the previous commit stays for a cancel that races a failing poll; its test is replaced by one for the slow poll, which covers the reviewed case. A BridgeClient test checks that a dropped call's connection is not reused. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(automation): retain cleanup after failed bridge cancellation * chore: restart CI for PR #8134 * test(automation): classify the smart object replacement MCP tool * test(automation): synchronize dropped bridge waits and accept reconnections * Fix CI after merging upstream * Fix CI after merging upstream * Deduplicate Arabic closure messages after upstream merge * fix(engine): bound active and cancelling background workers * Fix cancellation and draining in bridge merge * Test release-aware draining cleanup * Fix draining regression warning --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Khawar Zamman Wani <wanikhawar@Khawars-MacBook-Air.local> Co-authored-by: FredApps <fredapps@users.noreply.github.com>