This file records user-visible compatibility changes. Detailed development
notes are recorded in docs/updates/ (index: docs/updates/README.md).
The format follows Keep a Changelog. Until 1.0, breaking changes are permitted only when documented here with a migration path.
New entries go under ## Unreleased. The version bump on main is automated
and does not touch this file, so after a release tag appears, move the entries
it shipped into a ## [x.y.z] - date section of their own; the tag's
CHANGELOG.md shows which ones those are (git show vX.Y.Z:CHANGELOG.md).
-
The MCP server speaks the stateless protocol revision 2026-07-28 over stdio and HTTP, per request, beside the
initialize-based ones:server/discover,resultTypeand caching hints on results, the-32020–-32022error codes,subscriptions/listenfor tool-list changes and resource updates, and destructive-tool confirmation as a multi round-trip (input_requiredwith a signedrequestState). Over HTTP a stateless request needs theMcp-Method/Mcp-Nameheaders and is served without a session. Existing clients are served as before. -
The MCP server negotiates protocol version 2025-11-25 and sends its
descriptioninserverInfoto clients of that revision. -
Computer use with
computer_toolset_20260801answerszoomwith a full-resolution crop of the region. -
WorkQueueErrorandCheckpointStoreError(bothAutoControlException) for a database that cannot be opened or used. -
stop_event=onAgentLoop,run_computer_useandrun_dag; the Computer Use and DAG Runner tabs have a Stop action, and closing the window asks a running job to stop. -
parse_multipartfiles carrycontent_base64(the exact bytes). -
Computer use speaks the GA
computer_toolset_20260801, used automatically forclaude-opus-5-5(which rejects the beta tool); passtool_type="computer_toolset_20260801"to use it with other models. -
REST
POST /executeaccepts"raise_on_error": true: the run stops at the first failing action and answers{"ok": false, "error": ...}.AdminConsoleClient.broadcast_execute(raise_on_error=True)reports such a host asok: false. -
HistoryStore.list_runs(script_path=...), andenviron=onvalidate_config/ConfigSchema.validate. -
AC_idempotency_release/ MCPac_idempotency_release/ Script Builder Idempotency: Release: free an in-progress idempotency key whose work failed so a retry runs it. -
cua_action.resolve_key_name/split_key_combo, andcompile_postcondition(before=...). -
pii_text.luhn_validandnormalize_text(strip_format=...). -
http_requestresponses carryset_cookie, the list of everySet-Cookievalue. -
Failure-bundle manifests record
error_typebeside the redacted message, which is empty for exceptions such asTimeoutError(). -
Config-sync routes on the signaling server.
GET/PUT /config/{user_id}serve the bucketsConfigSyncClientpushes and pulls (in memory, secret-checked, 1 MiB per bucket); the client had no server to talk to before. -
approval_gate(db=None). The approval gate theAC_approval_*commands use: file-backed withdb, otherwise one per process. -
adaptive_mean/adaptive_gaussianpreprocessing steps. They takeblock_size/c, which no step used before. -
SoftAssertionsFailed. The exceptionSoftAssertions.assert_allraises; exported from the package. -
State-machine
if_image_foundguard. Fires once the template is on screen ("welcome.png"or{"image": ..., "detect_threshold": ...}). Otherif_*keys, which used to fire unconditionally, now raiseStateMachineError. -
WorkQueue.get_next(stale_after_s=...)(and the same optional argument onAC_queue_next/ac_queue_next) reclaims an item a crashed performer left in progress for that many seconds. -
ac_rrule_next,ac_rrule_occurrencesandac_format_datenow declare the string format they parse. Theirdtstart/now/valueproperties carry"format": "date-time"(or"date") in the tool's input schema, which the descriptions already said in prose and the schema did not. A client generating values from the schema alone used to produce a plain string and get aValueErrorout ofdatetime.fromisoformat.
-
An MCP HTTP request whose
MCP-Protocol-Versionheader names an unsupported version is still a 400, now with a JSON-RPCUnsupportedProtocolVersion(-32022) body listing the supported versions instead of{"error": ...}. -
Quick Connect verifies the host certificate for
wss://targets (use the Advanced viewer with Skip cert verification for self-signed hosts). -
The signaling client and the USB browser's device fetch go through
http_client: http(s) only, the egress policy applies, and credentials do not follow a redirect to another host. -
match_theme,propose_elementsandread_barcodesreturn screen coordinates for a screen grab (they were relative toregion). -
plan_open/open_pathrefuse opaque URL schemes off the allow list; webhook transports are case-insensitive and unknown ones are refused. -
handle_file_dialogwaits for a window titled exactly like the dialog and types only after bringing it to the front; watchdog key rules press their key only in the popup. -
Empty process names and window-title patterns are refused by the waits and process assertions.
-
assert_httpand config sync send their requests throughhttp_client: the egress policy and the body cap apply, and config sync no longer follows redirects. -
S3 store failures raise
ArtifactStoreError; unreadable Office files raiseAutoControlActionException. -
VLM locate / click raise
VLMRequestErrorwhen the request fails, instead of reporting the element as not found. -
Agent tool schemas carry resolved parameter types and omit private and callback parameters; the OpenAI agent backend refuses more than 128 tools.
-
Generated tests and scripts replay actions with
ac.executor.execute_action(..., raise_on_error=True), so they fail at the first failed action; actions holding${...}go through the executor. -
Several
je_auto_control_mcp --list-*flags print one JSON object. -
Accessibility matches with an on-screen rectangle come first, and
click_accessibility_elementreturnsFalsefor a match without one. -
Anchor locate raises when its OCR, accessibility or VLM backend is not set up, instead of reporting the anchor as not found.
-
AC_shell_commandfails when its program cannot start (it reported success) and logs only the program, not its arguments. -
AC_read_file_to_varreadsutf-8-sigby default. -
The Live HUD samples only while it is on screen; its log tail keeps collecting while it is hidden.
-
MCP
tools/callarguments that fail the tool's input schema are answered as a tool execution error (isError: true), not a-32602JSON-RPC error, so the model can correct them. -
The MCP HTTP transport answers a wrong bearer token 401 (was 403), as the MCP authorization spec requires; every 401 from it and the REST API carries a
WWW-Authenticate: Bearerchallenge. -
The REST API answers a known path asked with the other method 405 with
Allow(was 404). -
AdminConsoleClienttreatslabels=[]as no host (was every host). -
Config sync breaks timestamp ties the same way on every client and reports them as conflicts.
-
AC_call_macrorestores the caller's variables of the parameters' names after the call. -
A plugin command named like a block command is refused.
-
The LLM cost table carries current Claude list prices (Opus 4.7 is $5/$25, not $15/$75) and resolves dated or provider-prefixed ids.
-
vex_statementtakesaction_statement=and requires it foraffected(OpenVEX). -
The SBOM prefers PEP 639
License-Expression; licence evaluation reads everylicensesentry. -
perceptual_diffdiscounts anti-aliasing with pixelmatch's test instead of a morphological open, so thin real changes (small text, 1 px rules) now count. -
image_histogramchannels are L1-normalised; histogram intersection divides by the larger mass. -
ssim_comparescales its constants to the images' dynamic range and reports -1..1. -
confusable_skeletonfollows UTS #39 (NFKD, map, NFD);×and÷count as Common script. -
Without rapidfuzz, fuzzy scores are the symmetric Indel ratio (the same as the rapidfuzz backend).
-
Toolset screenshots are fitted into the high-resolution tier (2576 px long edge, 4784 visual tokens) instead of 1568 px / 1.15 MP.
-
parse_dotenvdecodes\'and\\inside single-quoted values, as python-dotenv does. -
format_messagekeeps an apostrophe before#outside a plural and before|(ICU); French has the CLDRmanycategory. -
parse_rruleraisesAutoControlExceptionfor RRULE parts it does not support (BYHOUR,BYWEEKNO,BYYEARDAY…) and forCOUNTwithUNTIL, instead of silently ignoring them. -
CookieJar.updatekeeps a cookie with an empty value (onlyMax-Age<=0or a pastExpiresdeletes one), andparse_traceparentaccepts a newer version (read as00); onlyffis rejected. -
parse_problemignorestitle/status/detail/instanceof the wrong JSON type instead of keeping or coercing them. -
format_annotation/emit_annotations/AC_ci_annotationsraiseValueErrorfor an unknown level instead of emittingerror.generate_sopraisesValueErrorfor a step that is neither a list nor a command string. -
compare_field_value/verify_field_value/fill_and_verifyraiseValueErrorfor an unknownmodeand accept any case. Profiles of numeric columns carry anon_finitecount. -
Webhook methods:
PATCHwebhooks are served; verbs the server cannot answer (e.g.HEAD) are refused when the webhook is added instead of returning 501 on every request. -
Golden-image capture (
take_golden/compare_to_golden) reads its region in mouse coordinates, like every other capture; region goldens taken on a scaled display need re-taking. -
ShellManager.exec_shellacceptscommand=as well asshell_command=, matching the Script Builder and the docs; the newcommand_args()is how every shell entry point turns a command into whatsubprocessreceives. -
replay_timeline/run_sequencerefuse unknown ops and bad speeds. An unknown op raises instead of being skipped, andspeedmust be a positive number. -
WorkQueue.complete/failrequire anin_progressitem. An unknown id or an item in another state raises, so finished work is never requeued and a stale performer cannot overwrite a newer outcome. -
AgentTrace.to_otelreturns OTLP/JSON spans (trace and span ids, nanosecond times, integer enums, typed attributes) instead of flat dicts withduration_s. Cost records refuse negative token counts. -
AssetStore.setvalidates the type and value it is given, and a stringtagsargument is one tag, not a list of letters. -
Plugins cannot replace built-in commands by default.
register_plugin_commandsandload_pluginsskip (and log) a name that already belongs to a built-in or user command; passallow_override=Trueto replace one deliberately. -
Impossible rate-limit requests raise.
TokenBucketandSlidingWindowLimiterrefusen <= 0or above capacity / limit, andCredentialBroker.leaserefuses a TTL that is not a finite positive number. -
Pseudo-localization padding counts visible text only, and
check_catalogcompares printf conversions and argument names rather than whole ICU blocks. -
Colour-match scores are RMS colour distance.
match_colorscores are1 -the root-mean-square HSV distance, hue compared round the colour wheel;min_scorethresholds tuned on the old metric may need adjusting.AC_image_hashrefuses an unknownalgo,hamming_distancerefuses hashes of different sizes, andupscalerefuses a non-positive scale. -
A failed soft-assert batch is an assertion failure. It raises
SoftAssertionsFailed(anAutoControlAssertionExceptionand still anAutoControlActionException), so suites score it failed and lenient runs no longer swallow it. -
raise_on_error=Truereaches into nested bodies. Loops, branches and macros run by a strict list are strict too, so a failure inside them raises instead of being recorded; lenient runs are unchanged. -
JSONPath refuses what it cannot read.
json_queryraisesValueErrorfor an unsupported filter, an unterminated[or a stray character (an unsupported filter used to match every element). Filters take nested fields (@.a.b) and existence tests ([?(@.k)]),trueno longer equals1, and a bare key may contain-. -
More MCP tools are destructive. Tools that run action lists or code, send input, delete data, send data off the machine or loosen a security control carry
destructiveHint: true, soJE_AUTOCONTROL_MCP_CONFIRM_DESTRUCTIVE=1asks before them.ac_export_sarif,ac_compliance_reportandac_assert_visualare no longer read-only. Atools/callargument the schema does not declare is refused with-32602. -
Secret-key matching is by word.
scan_secretsandredact_configno longer treat keys that merely containpassortoken(bypass_proxy,tokenizer) as secrets, and now do treatapiKey,cookie,sessionIdandAuthorizationas secrets. A redaction bounding box with no known coordinate keys raisesValueError. -
An empty window title is an error.
find_window,focus_window,close_window_by_titleand the other title lookups raiseAutoControlActionExceptionfor a blank or non-string title;""used to match every window. -
je_auto_control runexits 1 when any action failed. It used to exit 0 whenever the file loaded. Migration: a pipeline that relied on the old status can ignore it (|| true). -
Chat-ops
/screenshottakes a file name, not a path. The PNG is written into the router context'sscreenshot_dir(default: aje_auto_control_chatopsfolder in the temp directory); anyone in the channel could previously choose any path. Migration: setscreenshot_dir. -
AC_shell_to_varon Windows. A string command is passed toCreateProcessas written; quoted arguments used to arrive with their quotes still on. Output is decoded with the newencodingargument, defaulting to the locale's encoding rather than always UTF-8. Migration: pass"encoding": "utf-8"for a program that writes UTF-8. -
Passphrase-encrypted action files are salted.
encrypt_action_filewith a passphrase now derives the key with scrypt and a random per-file salt, and writesACENC1:+ salt + token; it used one unsalted SHA-256. Older files still decrypt. Migration: none, but a file written by this version cannot be decrypted by an older one. -
max_runsmust be at least 1.Scheduler.add_jobandadd_cron_jobraiseValueErrorformax_runs=0or below, which used to run the job once. Migration: passmax_runs=1. -
Importing the package no longer sets the root logger to DEBUG. A library must not reconfigure its host's logging, and that one line sent every third-party logger's DEBUG records to whatever handlers the host application had configured.
autocontrol_loggernow carries the DEBUG level itself, so this package's own records are unchanged. Migration: a program that relied on the side effect should calllogging.getLogger().setLevel(logging.DEBUG)itself. -
Per-user state paths are resolved when used, not at import. Nine defaults under
~/.je_auto_control/(the action signing and encryption keys, the host fingerprint and known hosts, the host service config, the WebRTC inbox, and the A/B locator, cost and self-healing logs) were fixed when their module was imported, so settingHOME/USERPROFILEafterwards had no effect on them. The class attributesABStore.DEFAULT_PATH,CostStore.DEFAULT_PATHandHealEventLog.DEFAULT_PATHare replaced by the functionsdefault_stats_path(),default_cost_log_path()anddefault_heal_log_path()in the same modules. Migration: call the function where the attribute was read. -
The log file moved out of the current directory. Importing the package opened
AutoControlGUI.logrelative to the cwd, so every process that imported it — including every pytest run on a machine where it is installed, through thepytest11plugin — left a log wherever it started. The file is now~/.je_auto_control/logs/AutoControlGUI.log, or whateverJE_AUTOCONTROL_LOG_FILEnames when the file is first opened, so aconftest.pycan still redirect it after the plugin imported the package (os.devnullturns it off). Because every process shares it, lines carry the process id (time | pid | logger | level | message), and instead of growing without limit it is moved to.1once past 10 MB, at the moment a process opens it. The file is opened on the first record rather than at import, and theLoad Windows Setting/Load Linux x11 Setting/Load Linux Wayland Setting/Load MacOS Settinglines every import used to log are gone, so importing the package writes no file. A file that cannot be opened (read-only home or cwd) is replaced byos.devnullwith oneRuntimeWarning; it used to makeimport je_auto_controlfail. Migration: to keep the old location, setJE_AUTOCONTROL_LOG_FILE=AutoControlGUI.log; changing the working directory before the import no longer redirects the file.
-
The egress policy matches hosts by their IDNA encoding, so soft hyphens, fullwidth characters and ideographic full stops no longer slip past a deny list.
-
GPLv3+,GPL-3.0 LicenseandLGPL-2.1-or-laterare caught by the copyleft deny list. -
The secrets scan skips only values that are a single placeholder.
-
Secret redaction no longer stops at an escaped quote.
-
HTTP cassettes no longer record credential headers, and a
match_onfield they cannot compare raises instead of matching every request. -
JWT decoding rejects characters outside base64url (which made tokens malleable) and a
NaNexpiry that never expired, andencode_jwtno longer lets extra headers overridealg. -
The admin console no longer sends its bearer token to a redirect target, bounds each host's response by
timeout_sas a whole and by size, and reports broadcast labels that name no host. -
The USB passthrough ACL fails closed when its signature is deleted once a signing key exists, keeps a damaged file aside instead of overwriting it, and refuses malformed vendor/product ids that never matched.
-
AC_list_plugins/AC_load_pluginsand the matching MCP tools load only theje_auto_control.commandsentry-point group; another group such asconsole_scriptsran every installed tool'smain(). -
Remote desktop: failed logins no longer use up the host's client slots, a view-only viewer can no longer set the clipboard or write files, and an IP allowlist whose every entry is invalid admits nobody instead of everyone.
-
Anonymous
initializerequests can no longer evict an MCP HTTP session in use, andDELETEoff the/mcppath no longer ends a session. -
The REST API checks the token before reading a POST body, and a valid token is never locked out by other clients' failed attempts from the same IP.
-
The RBAC user store refuses a token another user already has, and no longer replaces every user when its file is damaged.
-
The audit log's hash chain catches a forged row whose hash was cleared and the deletion of its oldest rows, and
clear()leaves anaudit_log_clearedevent instead of an empty, clean-looking log. -
JE_AUTOCONTROL_REQUIRE_SIGNED_ACTIONScovers DAG nodes that run on a remote host; an unsigned action file was dispatched to it. -
Keys, passwords and tokens given to actions are masked in the executor log and result record (signing, encryption and JWT keys; any
password,passphrase,token...), and the MCP audit file masks them at any depth, including insideactionslists. -
The
discoveryextra requires zeroconf 0.149.16, which fixes the 2026 mDNS memory and CPU exhaustion advisories reachable from the local network. -
The
signalingextra requires Starlette 1.0.1 (CVE-2026-48710, "BadHost": a craftedHostheader made middleware see another path than the router served). The signaling server's pre-body guard also decides by the routed path, so it holds on older Starlette too. -
MCP over HTTP: a web page could drive the machine, and one session could confirm another's destructive call. With no token configured (the default) the server accepted cross-site browser requests — a
text/plainPOST needs no CORS preflight — so any page the user opened could run tools; it now refuses a non-loopbackOrigin, and a non-loopbackHostwhen bound to loopback (DNS rebinding).JE_AUTOCONTROL_MCP_ALLOWED_ORIGINSadmits specific browser origins. Replies to server-sent prompts were matched by a sequential id alone, so any client could accept a confirmation shown to another; they are now bound to the session the prompt went to and the ids are random. WithJE_AUTOCONTROL_MCP_CONFIRM_DESTRUCTIVE=1, a destructive call that had no stream to ask on ran unconfirmed; it is refused. A non-ASCII bearer token crashed the request thread in both the MCP and REST servers (never counted toward lockout); it is refused. -
USB passthrough: three ways a viewer got past the ACL. Vendor and product ids were compared as strings but parsed by the backend with
int(x, 16), so0x1050,01050or10_50missed a1050deny rule and opened the device anyway; ids are now normalised to four lowercase hex digits (an optional0xprefix is accepted, anything else is refused). A viewer that omitted the serial skipped rules written for one serial and got whichever matching device the backend found first; that is refused when the ACL has a serial rule for the device. The WinUSB backend logged a requested serial and ignored it; it now refuses to open by serial. Transferlengthandtimeout_msfrom the wire are bounded (control 0–65535, bulk/interrupt up to 1 MiB, timeout up to 60 s), where a single request could make the host allocate a gigabyte. -
A WebRTC viewer could be approved without ever sending the token. The public
approve_pending_viewerguard readnot pending and authenticated, so for a session whose viewer had sent nothing both were false and the approval went through, cancelling the auth deadline and accepting input. Only a viewer that presented the token and is waiting on the user can be approved now; the token is compared withhmac.compare_digestinstead of!=, which leaked how many leading characters matched. -
The
pdfextra now requirespypdf>=6.16.1(was>=4.0).extract_pdf_text,pdf_metadataandassert_pdf_textopen whatever PDF they are given, and every pypdf before 6.16.1 can be driven into an infinite loop or unbounded memory by a malformed file (unterminated inline images, repeated bad cross-reference entries, large/ToUnicodestreams or CID width ranges,TreeObject.insert_child, outlines, XForm objects). Migration:pip install -U "je_auto_control[pdf]". -
requirements.txtstatespillow>=12.3.0directly. It already resolved 12.3.0 through theje_auto_control>=0.0.216floor; the direct line is for the dependency graph, which still reported Pillow 12.2.0 for this file from before that floor existed. -
uv.lockmoves anyio 4.13.0 → 4.14.2, cryptography 49.0.0 → 50.0.1 and pypdf 6.13.3 → 6.19.0. anyio (throughstarlette,[signaling]extra) fixes a TLS host-name encoding flaw that allowed certificate spoofing. cryptography 50 fixes a PKCS#7 EnvelopedData decryption oracle; this package does no PKCS#7 decryption, so the>=48.0.1floor is unchanged.
-
The Flow Editor opens action files saved with a BOM, keeps a wrapped file's other keys on save, and writes atomically.
-
The region selector (template cropping, OCR / screenshot / WebRTC regions) covers every screen and returns native pixels: it was offset by the virtual desktop's origin and did not cover screens with display scaling.
-
Numeric fields in the Image Detect, Auto Click and Screenshot tabs accept a decimal point and refuse grouped digits under any locale.
-
Script Builder: shown defaults equal the executor's (an edit no longer writes
detect_threshold: 0.8into an exact image match, nor a file path for agent-card / SBOM / MCP manifest steps); grid and annotation fields are required; positional arguments and the other keys of a wrapped file survive load and save; decimals can be typed under comma locales. -
WebRTC host annotations from a viewer are validated and bounded.
-
The tray icon keeps the application running only while a host runs.
-
Stopping a host while its approval dialog is open no longer raises.
-
Trust-list and address-book imports report files of the wrong shape.
-
Stop cancels a pending WebRTC auto-reconnect; a new remote window keeps the pen mode; an empty recording is no longer reported as saved.
-
Quick Connect handles ws:// sessions on close and in its status badge, ends the session on an error, and closes a timed-out approval box.
-
The Quick Connect popup forwards mouse and keyboard input.
-
Closing a remote screen window no longer aborts the process when its panel is deleted first.
-
Remote desktop connect errors (such as an invalid host name) are reported.
-
Audio follows its checkbox while the Advanced section is collapsed.
-
The host share text quotes the settings the host started with.
-
Address-book, known-hosts and remote-inbox data of the wrong shape no longer raise in the GUI.
-
The voice router is safe under concurrent use;
match_ensemblevotes on one frame. -
Failure signatures group failures that differ only in timings.
-
AC_delta_observationcounts match its summary, and removed elements carry no stale index. -
The package imports without a home directory; the path guard reports a missing home as
PathNotAllowedError. -
Adaptive timeouts, checkbox reads and contrast checks handle infinite samples, clipped boxes and anti-aliased edges.
-
MCP
ac_kill_processandwait_until_window_titleno longer let psutil or regex errors escape. -
File associations report
Nonefor an unregistered type; file drops and clipboard file lists carry absolute paths. -
The Window Manager tab focuses and closes the selected window, not the first title match.
-
Secrets nested inside action arguments, and webhook URLs, are masked in logs and run records.
-
X-Api-KeyandX-Auth-Tokenare dropped on redirects to another origin. -
A truncated HTTP error body or a deeply nested reply no longer aborts a script.
-
The IMAP trigger fires once per message, honours UIDVALIDITY and accepts non-ASCII mailbox names.
-
HTTP cassettes mask credentials in query strings and bodies.
-
SQLite data sources that cannot be opened raise an action error.
-
Agent turns cut short by
max_tokensor a refusal no longer run their tool calls; OpenAI refusals and filtered replies are not final answers. -
Computer-use scrolls keep their direction, the cursor position is in screenshot pixels, and
ctrl++keeps its plus key. -
The Anthropic VLM backend reads replies in the pixels of the image the model saw, and replies such as
x=512, y=300or decimals are read. -
A reused agent backend starts each run afresh; the agent loop records more command errors as step errors instead of ending the run.
-
Bedrock
global.ids and dated OpenAI ids are priced. -
je_auto_control_mcp --read-onlyrestricts the server's tools, not only the listings. -
The MCP stdio server and the CLIs write UTF-8 whatever the console code page.
-
--portoutside 0-65535 is a usage error instead of a traceback. -
Codegen refuses action shapes the executor refuses, keeps dict key order, and takes lone surrogates and sigil runs in Robot output.
-
SARIF and SOP writers take lone surrogates; SOP creates its folder and writes atomically; Allure results carry start and stop times.
-
A closed window's
COMErrorno longer escapes UIA state reads;find_textno longer finds absent text. -
macOS accessibility elements report their real bounds.
-
Accessibility audits and
describe_screenwork on Windows. -
The accessibility recorder keeps running after a backend error; the Linux control search is bounded.
-
Mixed text formatting reads as unknown; Tesseract errors other than a missing binary say what failed; integer roles match.
-
AC_shell_to_varrefuses cmd metacharacters in a batch file's arguments, and its timeout, like MCPshell_command's, ends everything the command started. -
MCP
shell_commandkeeps output the code page cannot decode strictly. -
An empty Linux clipboard reads as empty; clipboard tool failures are
ClipboardError. -
adb errors no longer carry the command's arguments;
no permissionsdevices get that state. -
USB/IP: device lists on Windows and with alternate settings, speed codes, idle attached devices, and isochronous URBs.
-
import je_auto_controlno longer emits a DeprecationWarning from defusedxml, so it works in test suites that turn warnings into errors. -
SARIF export no longer calls
PurePath.as_uri(), deprecated in Python 3.14. -
USB sharing, its hotplug watcher and the passthrough flag it turned on are released when the USB Sharing panel is destroyed.
-
GUI slots show missing or malformed files, images not on screen, bad regions, unknown commands and closed windows instead of raising.
-
Hidden tabs, the Live HUD's log tail, the main window's language listener and USB prompt dialogs no longer outlive their window.
-
A GUI worker's unexpected exception reaches its failure callback.
-
The Script Builder keeps a choice value it does not list.
-
REST and MCP replies holding a lone surrogate, REST replies that cannot be serialised, huge
/historylimits and JSON nested too deeply no longer drop the connection without a response. -
Requests with conflicting
Content-Lengthheaders are refused. -
Access-log lines of the REST, MCP HTTP and webhook servers escape control characters.
-
A histogram given a
+Infbucket renders it once. -
ResourceProfiler.is_runningis right without psutil. -
Scheduled, triggered, hotkey, webhook and e-mail runs in which an action failed are recorded as errors, with an error snapshot.
-
*/15-style cron jobs keep their pace through the repeated DST hour. -
Re-enabled scheduler jobs wait for their next slot; interval jobs no longer drift.
-
A trigger replaced under the same id is not charged for the old run.
-
One popup-watchdog rule's error no longer stops the others.
-
Concurrent hotkey daemon start/stop no longer leaves a loop running.
-
AC_retrybackoff is capped at 300 s. -
A remote-desktop upload aborted while it was starting no longer leaves its
.partfile and open handle behind. -
The action JSON Schema lists every command, block commands included, and types parameters from their annotations instead of "string".
-
The linter reports a block command's missing required arguments (e.g.
AC_sleepwithoutseconds). -
${webhook.body},${email.subject}and other dotted trigger variables resolve in scripts. -
Step repair repeats a no-op action whatever form the verdict takes.
-
The self-healing log survives a torn multi-byte line.
-
A/B locator reports see other stores' records; a strategy that never succeeded is not recommended.
-
Time-travel replay shows actions logged before the first frame.
-
Agent memory recalls CJK keywords.
-
Variable files with a UTF-8 BOM load.
-
AC_trace_resetstarts a new trace id. -
Exiting while a WebRTC signaling poll is running, or closing the remote-desktop viewer during a file transfer, no longer aborts the process.
-
merge_resultsno longer doubles errors and cases when merging merged reports. -
Time-series buckets place edge points correctly at Unix-time magnitudes.
-
assert_text(regex=True)honoursignore_case. -
wait_until_screen_stablemeasures the quiet time from the first matching frame. -
summarise_llm_costs()works without arguments. -
validate_rowsreports huge integers instead of raising. -
Welch confidence intervals at tiny alpha and df near 1.
-
MCP
initializeanswers with a protocol version the server supports (not whatever the client sent) and declares only server capabilities; an unsupportedMCP-Protocol-Versionheader gets 400;request_samplingneeds the client's sampling capability. -
AC_run_agentwith the Anthropic backend fits screenshots into the model's image tier and maps tool-callx/yback to the screen. -
Screenshot fitting follows the documented resize rule exactly.
-
SLSA provenance omits empty metadata timestamps; verification reports a subject without a name instead of raising.
-
PEP 440 ordering of
.postN.devMand of omitted numbers. -
Redaction boxes merge until none overlap.
-
OSV ranges with several introduced/fixed pairs match every pair.
-
In-memory approval gates and the credential broker are thread-safe.
-
Print-format IBANs are detected (mod-97 checked).
-
SARIF findings without a severity are warnings; the name "Dan" is not a jailbreak marker.
-
Computer use on the beta tool fits screenshots into the model's image tier, declares that size and maps coordinates back, so clicks land correctly on screens above the model's image limits (4K, or 1080p on standard-tier models).
-
ssim_compareaccepts single-channel HxWx1 arrays. -
Heading detection uses the true median line height.
-
Element matching never pairs boxes that do not overlap.
-
average_hash/dhashaccept NumPy arrays. -
apply_unified,unified_diffandthree_way_mergesplit lines at line feeds only, so form feeds and U+2028 inside a line survive and CRLF text keeps its endings. -
Readability counts only sentences that hold a word.
-
normalize_text(casefold=True)stays in the requested form. -
is_balancedchecks bidi controls per paragraph. -
Closing the window while a GUI job is inside a long step (an LLM request) no longer aborts the process.
-
format_messageacceptsoffset: 1and reports an infinite count as not a number. -
read_modecodes a catalogue in the charset its header declares. -
JSON Schema
$reftokens follow RFC 6901 (ASCII indexes, percent-decoded fragment). -
A
;inside an SQL string literal no longer counts as a second statement. -
parse_number("Infinity")raisesValueError. -
The secret vault no longer loses a secret when two managers write it at once.
-
JSON stores read a file with a UTF-8 BOM, and a write waits for a Windows reader instead of failing.
-
HTTP cassettes redact
set_cookie. -
A truncated deflate body raises instead of returning a prefix;
x-gzipis accepted. -
JSONPath
!=keeps nodes that lack the member; a filter string may contain)]. -
parse_multipartkeeps a backslash in a filename. -
LatencyDigestpercentiles over negative values. -
decode_jwtraisesJwtErrorfor a non-stringalg(wasTypeError) and rejects anycritheader. -
The SSE parser no longer drops an event when a CRLF is split so the
\narrives alone. -
An escaped quote inside a quoted Link or Cache-Control parameter no longer ends the string (a Link header's
relcould be lost). -
urls_equal/ URL normalisation decode escaped unreserved characters (%7Eis~). -
Closing a GUI tab or the main window while its background job (Admin Console poll, USB browser, computer use, DAG, LLM planner) is running no longer aborts the process.
-
Closing the LAN browse dialog with Use, Cancel or Esc stops its mDNS browser, and a closed presence tab no longer stays registered with the presence registry.
-
Computer use:
- drags end at the model's
coordinate; keyhonoursrepeat;- modifier keys on clicks, drags and scrolls are held.
- drags end at the model's
-
A DAG remote node whose actions failed on the host no longer counts as succeeded, and the Admin Console broadcast shows a remote action failure as a failed host.
-
GUI threads:
- Admin Console refresh and thumbnails, and the USB Browser and passthrough actions, now run; their workers were collected before starting.
- Worker results are applied on the GUI thread.
- The Quick Connect viewer no longer repaints or opens dialogs from its network thread.
- Stopping or restarting a WebRTC signaling session no longer aborts the application.
-
Traces and reports:
- Replay traces with Unicode line separators read back.
match_persistencerequires every frame to agree.hold_modifiers("shift")presses Shift, not five letters.- Change boxes off the frame score nothing.
- OTLP output is strict JSON with typed array and map values.
generate_sopreads the wrapped action-file form.
-
Utilities:
- Dropping files onto a window no longer leaks memory on failure and no longer reports success for window 0.
- A zero-weight grounding candidate no longer divides by zero.
- Table cells and borderless rows read in reading order.
- A data profile survives
inf/nan. - An unknown verify mode is refused.
- Collation distinguishes accent position and handles decomposed text.
parse_cf_htmlapplies header offsets tostrinput.- A superscript digit no longer crashes role parsing.
- Out-of-range HSV bounds are clamped or wrapped.
-
macOS media keys: pressing a media key no longer raises
AttributeError; the PyObjC selector name was missing its trailing_. -
Error family: twenty-five errors that derived only from a builtin exception (the Android and iOS clients, the remote-desktop wire errors, ACME, the circuit breaker, egress, the Interception loader and others) now also derive from
AutoControlException, so family-only boundaries contain them;except RuntimeError/except ValueErrorstill catch them. -
X11, uinput and macOS input: the uinput backend types the intended keys and scrolls with the same sign rules as XTest; sending keys and clicks to an X window releases what it pressed; an unbound X key raises instead of pretending; recorded wheel events no longer break replay; macOS media keys press and release; a timed-out macOS recording tap is re-enabled.
-
Wayland: libei input keeps working after a screen lock or VT switch and releases each device reference once; screen size follows rotated and scaled outputs; a malformed capture-command override is a screen error.
-
WebRTC media: screen frames are stamped at the rate they are sent and frame rates above 30 fps take effect; host voice plays at the right speed on a mono output; an audio device that fails no longer aborts the connection or wedges later starts; a viewer reused for a second session shows video; mDNS and the asyncio bridge release their resources on failure and stop.
-
USB passthrough, agent loop and locators: the second of two identical USB devices opens by serial, a transfer whose direction contradicts the endpoint is refused and closing gives the device back to the kernel; a malformed agent decision no longer ends the run; a screenshot failure in self-heal is a miss; USB/IP, self-heal and anchor errors are
AutoControlExceptions; the a11y audit no longer flags table cells. -
Layout and data checks: flow selection and sharding see a flow's own history however many other runs followed it; column reading order survives long runs of paragraphs;
ConfigField.envis honoured and lossy int coercion refused; single-column uniqueness ignores nulls; unit lists follow CLDR outside English; A2A card modes are MIME types;withinexcludes the pixel past its region; Windows accessibility reads edit and slider values. -
MCP, USB passthrough and device helpers: an overflowing or short argument no longer leaves an MCP request unanswered; a failed drag releases the button; sampling works over HTTP; waits look once at
timeout=0and survive an infinite poll; USB credits are never missed and transfers on one claim no longer swap data; assertion failures propagate through callbacks; gamepad, clipboard and volume errors stay in theAutoControlExceptionfamily. -
Remote desktop: WebSocket frames follow RFC 6455 masking and control-frame limits and a malformed handshake key no longer kills the handshake thread; a transfer to a path naming no file fails cleanly; the encrypted recorder's frame count matches its entries and a tampered manifest verifies as
False; a mic whose device failed to start can start again; turning off viewer audio keeps the host's voice. -
Triggers and scheduler: concurrent trigger-engine start / stop no longer doubles the polling thread or raises; one malformed email no longer stops a mailbox's polling; mailbox names with spaces or brackets work; a string
max_runsstops the job; a corrupt .xlsx data source is an ordinary action error. -
JSONPath, OCR structure and H.264: quoted unions and mismatched quotes raise, quoted names decode escapes, parenless filters no longer run into the next filter, and ordering follows RFC 9535; OCR field values come from the cell below the label; only hardware encoders that really open are listed and each gets options it accepts (NVENC works); odd-sized frames encode and
close()always closes the container. -
Text, config and registries:
-or-laterlicences are no longer split (a denylist naming one now holds); REST USB booleans must be JSON booleans;.poentries need no blank line between them and CRLF files parse; negative numbers take CLDR's plural category and large counts keep their digits;file://URIs naming another host are refused; coturn fields and XML names cannot inject directives or markup; presence ids are matched as registered and its errors areAutoControlExceptions. -
Data utilities: JWTs are rejected at their expiry second and only in canonical base64url; malformed tokens raise
JwtError; n-gram similarity rejectsn < 1;DagDefinitionErroris anAutoControlException; time-series samples sharing a timestamp keep their order and unknown fills raise; NaN fails range rules; feature flags accept{"variant": ...}and fall back on malformed serves; schema compatibility sees required-only fields and rejects unknown modes; CSV test data takes rows with different keys. -
Small utilities:
use_sslmail defaults to port 465; the resource profiler's report stops atstop()and its speedscope export loads;Content-Lengthmust be ASCII digits andchunkedthe final coding;web_screenshotworks against WebRunner; failed notifications reportshown=Falseand Windows toasts appear; a dead key no longer reads as its US character; inverted and off-image regions are handled in annotate and colour stats. -
Emergency stop on Linux / macOS: the stop key wakes a sleeping or waiting main thread there too (SIGINT is sent to the main thread).
-
Image analysis and packaging: colour-vision simulation uses Machado (2009) in linear RGB; squares are checkboxes and hollow boxes text fields, not radios / toggles; step repair stops after escalating and does not repeat an action that already changed the screen; the SBOM lists each package once and skips extras; the MCP registry manifest fits the schema; generated stubs keep
*//markers; provenance verification reports unchecked subjects. -
Observation and state: the emergency-stop key interrupts a sleeping or waiting script at once, ignores a press made before it started and works more than once; mark labels never overlap or leave the screen; nested placeholders raise; lock waits end at their timeout; observer transitions fire once; CloudEvents enforce source/type/time and carry bytes as
data_base64; coordinate spaces reject zero sizes. -
Key names in file dialogs and popup rules: the default "enter" confirm key and "esc" / "enter" dismiss rules resolve to the Windows names.
-
Input and form helpers: computer-use and CUA key names resolve to the running platform's spelling (Enter, Esc, paging, Backspace, Alt and Super failed on Windows); CUA double clicks click twice and scrolls carry their direction; the action linter knows flow-control commands and lints their bodies; gamepad clicks and the dpad work; clipboard history survives xclip errors; RTF text honours
\ansicpgand text symbols. -
Media and analysis: float bucket edges land in their own bucket; KS p-values for near-identical samples are 1.0; histogram intersection is normalised to 0..1; unreadable or too-short videos are errors, not "no motion"; negative SSIM ignore boxes and masked visual-diff pixels are handled; grayscale arrays, PIL frames and non-ASCII paths load correctly; pypdf's DependencyError is contained.
-
Image matching: rotated, trust, feature, edge, shape, line and saliency finders report screen coordinates (region and multi-monitor origins included); rotated templates are matched with a mask and
sqdiff_normedpicks the best spot; flat templates are refused on every matcher; auto-threshold reports each blob's own peak; ORB with too few matches returnsNone; palette / LA / 16-bit PIL images load as true luminance. -
System and device helpers: keep-awake ends with the process and no longer depends on the calling thread on Windows; file associations report the real content type; checksums reject input with no digits and mod-97 check digits follow ISO 7064; compliance reports reject unknown frameworks and treat "false" as a gap; D-Bus closes its socket on a failed handshake; window capture uses the visible frame and skips minimized windows; newer pycaw versions work.
-
Agent and action helpers: self-healing replay no longer heals a failed assertion and moves a release with its healed press; list-form entries pass relocation; zero-size boxes are not actionable and an unknown enabled state is not "disabled";
act_in_viewgates on the live target; grounding snaps to the innermost element; non-string memory tags, the key-file creation race and pruning of in-progress runs are handled. -
Config and plumbing: failure-hook tickets are redacted before filing and one failing backend no longer stops the rest; symlinked plugins outside the plugin directory are skipped; the search index keeps non-ASCII terms and folds stop words; agent-trace fields may repeat or extend the span arguments; chat commands are case-insensitive and the Slack poller reads every page; NaN counter increments are refused; the diagnostics CLI fails on errors only.
-
Store files are set aside only for damaged content: a transient read error no longer renames a healthy known_hosts, trust list, address book or admin host list and starts it empty; the error propagates instead.
-
Workflow and test tooling: merged shard reports keep errored cases; smart waits never sleep past their timeout; unknown decision-table hit policies and non-callable state-machine predicates are refused (
ANYis supported);assert_pollraises an assertion (PollAssertionError); soft assertion failures survive an exception; seeded test dates no longer move daily; repeated debugger steps and overlapping repeats are counted right. -
Locators and geometry: sideways scroll-to-find no longer scrolls vertically (it needs a horizontal wheel axis or
scroller=); stable element ids are unique and non-overlapping boxes never match; remapped points and grid cells contain their points; window grid cells meet exactly; A/B locator stats survive concurrent writers and damaged files. -
Text processing: long phone numbers and Amex card numbers are masked whole (cards are Luhn-checked), in text and in screenshots; the email and URL-credential scans are linear on crafted input; invisible characters no longer defeat confusable matching or
normalize_text; Japanese and Korean text is not flagged as mixed script; fuzzy.poentries are not served; ICU case bodies are not reported as placeholder mismatches. -
Data formats:
write_workbookstores formula-looking strings as text and writes every key's column; malformed JSON Patch operations and pointer escapes raisePatchError; unsupported JSONPath selectors and filter values raise, and object filters select member values; ICU plural offsets, quoted apostrophes, empty branches and stray braces are handled; XML empty elements round-trip;integer->numberis a widening;ignorecovers subtrees; short values are fully masked; locale parsing is strict. -
Time and statistics: sequence tracking stores only outstanding gaps and refuses jumps beyond
max_gap; the SLO window ends atnow; a UTCUNTILis converted to local time and hugeINTERVALs are rule errors; the latency digest refuses NaN/inf and stays within the recorded range; a lone outlier among identical values is detected; NaN percentiles are refused; frame snapshots no longer share a boundary action. -
Governance and supply chain: license expressions are parsed (AND/OR precedence, parentheses, WITH, case-insensitive ids,
+); VEX statements match the product's version and PEP 503 name; approvers are compared case-folded;traceparentids with a trailing newline are refused and an invalid header extracts asNone; PEP 440 epochs and dev pre-releases sort correctly and findings name their own package's fix;semver_*flag operators follow SemVer; JSON logs keep tracebacks; baggage stays under 8192 bytes. -
HTTP family: the egress policy matches the host urllib connects to (percent-decoded, trailing dot, IP literals in any spelling); response bodies and gzip/deflate decoding are capped at 64 MiB; repeated headers are kept (
set_cookielists everySet-Cookie); credentials are dropped on an https-to-http redirect; multipart names cannot inject parts and parsing keeps line breaks; Link and URL normalisation follow RFC 8288 / RFC 3986. -
Process, secret and file boundaries: recycling a symlink recycles the link, not its target; relative
file://secret refs resolve underbase_dir; cmd metacharacters in a.bat/.cmdargument list are refused on Windows;start_exelaunches the file it checked; log redaction masks quoted values,pwd=and--password <value>; non-ASCII TOTP codes, negative drop points, dotenv tabs and reused ACME authorizations are handled. -
Work queue and durable state: claims are numbered so a stale performer cannot settle a reclaimed item, and repeatedly abandoned items fail instead of looping; a failed
run_resumablestep is retried rather than skipped; idempotency keys can be released; the dedup window and outbox are thread-safe; S3list()stays in its prefix and reads every page. -
Agent requests are bounded: each model call times out after 120 s and only the three newest screenshots are resent, so long runs stay under the request size limit;
export_*_tools(only=[])offers no tools instead of all of them; an empty LLM plan raisesLLMPlanError; VLM coordinates are no longer read out of longer numbers or accepted off the screenshot. -
Computer use reaches the API: the
computertool is sent under its beta (it was rejected on every request), nowcomputer_20251124onclaude-opus-5by default; model-chosen scrolls and waits are bounded and a scroll goes to the model's coordinate. -
python -m je_auto_control -druns a directory's files in sorted order and no longer follows links out of it, and the legacy entry point prints its error to stderr;je_auto_control runexits 1 for a failedAC_run_suiteand accepts--dry-run --varwith loop variables;validateagrees withrunon wrapped and empty files;start-server --port 0reports the real port; the pytest plugin no longer ends the session when a failure screenshot cannot be taken, nor screenshots skipped tests. -
Screen recordings play back at their real length (frames are paced to the declared fps), stop adding an
AC_screenshotrecord per frame, and an unusable frame rate, codec or path raises instead of leaving a recorder that writes nothing; iOSfind_elementhonourstimeout_s. -
AC_run_dagnode actions are placeholder-expanded once, a nestedAC_execute_actionfailure reaches the enclosingAC_try, and a top-levelAC_breakor a dry run no longer overwrites or collapses the record of a repeated identical action. -
AC_parallelreports a failed branch underraise_on_error(soAC_tryandAC_retrysee it), counts branch failures for the caller, keeps the macro recursion limit across branches, and validates a JSON-stringbranchesbefore running any of it. -
The versioned store (
AC_cas_*) keeps versions monotonic across a reload and serialises concurrent writers, and a config-bundle import whose write fails leaves the live file in place. -
Every malformed JWT raises
JwtError, soAC_jwt_decodeanswers{"ok": false}instead of failing, and it acceptsalgorithmsgiven as one name. -
The admin console keeps a damaged or partly unreadable host file instead of dropping entries on the next save, and the USB passthrough viewer caps a message that never sends EOF.
-
USB/IP answers a libusb error without an errno with
-EIOinstead of dropping the client connection. -
match_masked,match_masked_all,match_subpixel,match_autoanddetect_scalereturn screen coordinates when given a region or when the virtual desktop starts at a negative x, instead of frame-local ones. -
Remote desktop: a damaged trust list, known_hosts or address book is moved aside instead of being overwritten, an upload interrupted by a disconnect or a host stop leaves no
.partfile, and stopping the relay ends its paired sessions. -
The socket server reads indented (multi-line) JSON commands whole, and the socket driver docs' client example sends the newline terminator it needs.
-
The MCP HTTP transport releases the state of a session dropped while a request for it was running.
-
A corrupt audit database no longer stops the REST server from starting; it runs without the audit hook, as intended.
-
Two
SecretManagers on one vault keep each other's changes, and a malformed vault raisesSecretStoreErrorinstead ofKeyErrororValueError. -
UserAuthErrorandCredentialBrokerErrorderive fromAutoControlException, so containment boundaries catch them. -
Replayed wheel-up scrolls up on X11 and Wayland, the cleanup release after a failed replay step stays at the cursor instead of
(0, 0), gesture waypoints round to the nearest pixel, and a NaN key-hold duration is refused before the key goes down. -
macOS and Linux hotkeys no longer retry a combo that failed on every tick, logging an error ten times a second;
bind()rejects a key macOS cannot take up front, as it already did on Windows. -
The webhook server reads chunked request bodies instead of running the script on an empty body, accepts a lower-case
bearerscheme, and answers 500 when the run history is unavailable instead of dropping the connection. -
A trigger removed by an earlier trigger's script in the same pass no longer runs; an infinite poll interval no longer kills the trigger or observer thread; one failing observer rule no longer stops the others; and
callback_functionreturnsNonefor any trigger error, as documented. -
Email triggers fire a failing script once per message, not on every poll, and record it as an error; IMAP connections time out after 30 s instead of hanging the watcher; a body in an unknown charset is kept.
-
write_step_videorenders a generator of steps instead of an empty video, and a trajectory rubric given one action name as a string checks that name rather than its letters. -
Codegen output always compiles and runs as the JSON would: keyword or non-identifier flow names, NaN / Infinity values,
AC_execute_actionarguments,{"auto_control": [...]}files and Robot test names that read as headers or comments are all handled. -
Rate limiters refuse a NaN or infinite rate, capacity or window, which let every request through or spun the waiter;
RetryBudgetrefuses a NaN deadline; the sharedLoopGuardis thread-safe;plan_repairreturns no tactics for a negativemax_attempts. -
Cron jobs fire once per slot through the hour repeated when clocks fall back, instead of on every scheduler tick; and a run of a job removed while in flight no longer counts against a new job registered under the same id.
-
AC_validate_jsonreports an invalid regular expression instead of aborting the script, detects a$refcycle through a sub-schema, applies the keywords beside a$ref, keepstrueand1apart inside containers, checksmultipleOfexactly for integers and treats1and1.0as duplicates foruniqueItems. -
Action files saved with a UTF-8 BOM run.
validateaccepted them but every runner and the action linter refused them; a.envfile with a BOM no longer loses its first key. -
A NaN timeout raises
ValueErrorinstead of polling forever inAC_wait_image,AC_wait_pixel,expect_poll, the app-idle, IME, lock and window waits and OCRwait_for_text; the MCP image and pixel waits no longer report an immediate timeout for one. -
Quoted command lines work on Windows.
AC_shell_command,AC_exec_shell_to_varandac_shellpass the string toCreateProcessas written instead of re-quotingshlextokens that still held their quotes. -
Keep-awake requests nest. Releasing one restores the state it replaced, so a second
keep_awake_on()or a nestedkeep_awake()no longer lets the machine sleep. -
The Win32 clipboard setters free their memory when setting fails.
-
FilePathTriggerfires when the file is created and when it is replaced by one with an older timestamp. -
wait_until_filedoes not accept a directory, and every smart wait refuses a NaN timeout or poll interval. -
A missing psutil is an error, not a failed assertion, and
ac_kill_processreports a process that exits mid-kill instead of raising. -
.envvalues containing U+2028, U+0085,\vor\fsurvive a round trip. -
The signaling server refuses unauthenticated requests before reading them. A wrong
X-Signaling-Secretgets 401 and an oversized or length-less POST 413 / 411 before the body is buffered; the MCP HTTP transport accepts a lower-casebearerscheme. -
Replays happen where they were recorded and never leave keys held. Recorded presses, releases and scrolls move to their recorded position; recording gaps keep their total length; a failing step in
run_sequence,replay_timeline,tween_dragordrag_pathreleases held keys and buttons. Windows acceptsctrland every platform acceptsleft/right/middleas button names. -
Sagas roll back.
run_saga/AC_run_sagaran steps leniently, so a failing step was never noticed and nothing was compensated; a compensation that raises is reported incompensation_errors. One failing device no longer aborts the device matrix, a DAG node raising any exception fails that node, and an observer rule removed mid-poll no longer fires. -
Metrics, SARIF, SBOM and version checks follow their specs. Labelled Prometheus metrics no longer render a bogus unlabelled series, partial label sets and non-ASCII names are refused and HELP text is escaped. SARIF levels are normalised, lint issues point at the right 1-based line and file locations are URIs. SBOM purls are normalised and percent-encoded. The vulnerability scan orders PEP 440 and SemVer pre-releases before their release. W3C multi-tenant
tracestatekeys are kept. Step videos resize mismatched frames, fail on an unwritable path and leave the caller's frame untouched. -
Stores keep what they are given. Two skill libraries or element repositories on one file no longer lose each other's saves (a corrupt file still raises rather than being erased); asset commands without
dbshare one store; a config-bundle entry without content no longer empties its file, and imported files are written 0600; agent memory recalls non-ASCII words; action files with a UTF-8 BOM load. -
Plugins load reliably. One plugin file that fails to import no longer stops the rest of its directory,
@dataclassplugins load, a non-function entry-point value is skipped instead of aborting discovery half-way, and the MCP plugin watcher keeps a tool another file still defines. -
Approvals and leases enforce what they promise. Approval commands without
dbshare one gate (a token was forgotten between commands), an anonymous approver is refused, and a lease TTL of NaN or infinity is rejected instead of never expiring. -
Retry, rate-limit and breaker edge cases. Backoff caps huge attempt numbers instead of overflowing,
RetryPolicycaps its first sleep, the sliding-window wait is long enough, asctimeRetry-Afterdates are GMT, the loop guard reports the longest stuck pattern, re-created CAS keys never reuse a version, an interrupted half-open trial no longer jams the breaker, and idempotency claims are atomic with the TTL starting at completion. -
Text and clipboard helpers handle real-world input. Long near-identical strings fuzzy-match again; RTF round-trips characters beyond the BMP, lone CRs and Word's fallback escapes; a file-drop list with an empty path is refused and parsing stops at its terminator; pseudo-localization keeps printf, HTML and ICU placeholders;
slugifyinserts the separator literally; a null CSV cell is empty. -
Image utilities see the colours and files they are given. Grayscale no longer swaps red and blue for PIL images and screen grabs, palette images are read by colour, deskew works on dark themes, and image paths may contain non-ASCII characters. A red glyph can be colour-matched, colour regions accept any PIL mode, a golden of another size is a mismatch rather than an error, and zero-area elements get no mark.
-
An approval
extensioncannot leaveapprovals_dir. -
Test reports and suites count what happened. A setup failure is counted in the JUnit totals and reported to Allure;
assert_httpscores a read timeout or dropped connection as a failed assertion instead of crashing;assert_eventuallyrefuses a NaN timeout (an endless loop); one malformed case or quarantine entry no longer aborts the suite; string tags are one tag; runs with equal timestamps list newest first;critical_steps(top=0)is empty. -
Failures inside nested blocks reach
AC_try,AC_retryand strict callers. A failure inside anAC_loop,AC_if_*branch or macro was swallowed at the block boundary.AC_try/AC_retrynow also catch arithmetic and lookup errors and let the macro depth limit reach the top-level record; a plannedAC_breakis recorded instead of raised. -
A variable's value is never expanded as a placeholder. Commands that run a nested action list (
AC_execute_action,AC_circuit_call,AC_run_saga, ...) expanded it twice, so a value containing${secrets.NAME}was resolved from the vault. -
VEX no longer suppresses findings it does not cover.
apply_vexmatches products by package name instead of substring, honours the statement's aliases, and lets a later statement supersede an earlier one. -
Parsing fixes for
.env, data sources and HTTP headers. Quoted.envvalues drop a trailing comment and may span lines;dump_dotenvoutput parses back unchanged. CSV/JSON data sources skip a UTF-8 BOM. A past cookieExpiresdeletes the cookie and a nameless cookie is ignored; quotedCache-ControlandLinkparameters stay whole;relis case-insensitive; SSE handles a\r\nsplit across chunks and a BOM. -
JSON Patch, JSONPath and unified diffs follow their specs. JSON Patch
addaccepts an index equal to the array length and no longer shares its value with the patch;movechecks its source.apply_unifiedplaces-N,0insertion hunks correctly, keeps body lines that start with---/+++and skips\ No newlinemarkers.three_way_mergereports two insertions at one point as a conflict and applies an identical change once. -
MCP read-only mode and the confirmation gate hold.
ac_bulkhead_runandac_run_chaosno longer run action lists in read-only mode; file writers andac_assert_httpwith a mutating method are out of it; read-only tools no longer create a database at a missingdbpath;resources/readserves only the*.jsonfilesresources/listshows. -
Secrets stay out of reports, bundles and logs. Secret keys are matched by word (
apiKey,db_password,sessionIdcount;tokenizerdoes not); list and tuple items and numbers under a secret key are checked; JWTs and credentials URLs are found; free-text redaction masks prefixed keys, Basic/Token/DigestAuthorizationand URL passwords. Failure bundles maskAC_secret_*arguments, re-raise the block's own error when the bundle cannot be written, and drop a truncated log's partial first line. Screenshot redaction readsx/y/width/heightboxes and handles palette, grayscale and bilevel images. -
Accessibility, OCR and window lookups match what was asked. A blank
containsname no longer matches every element; a blank window title is refused instead of matching (and closing) the first window; Linux single-control lookups honourwindow_title; saving and restoring a window layout no longer reads or moves the wrong window when titles overlap or repeat;show_windowno longer foregrounds a window it was told to minimise or show without activating;wait_for_window/wait_for_textwithtimeout=0look once; OCR text matching normalises Unicode. -
Android input cannot run shell commands on the device. Text typed with
AC_android_textis shell-quoted, andAC_android_keyaccepts only key names and codes;$(...), quotes or;in either used to reach the device shell. -
Android and iOS device errors are contained. uiautomator2, adbutils and facebook-wda errors (no device, several devices, an invalid session) are raised as
UIAutomatorUnavailableError/IOSUnavailableErrorinstead of aborting the rest of a script. -
Image location is accurate.
locate_image_center/locate_and_clickreturn the best-scoring match instead of the first position over the threshold (which was a few pixels off), an identical template now matches at the default threshold of 1.0, andlocate_all_image(draw_image=True)works. A missing template file or a threshold outside 0..1 raisesImageNotFoundExceptionnaming the problem. -
Mouse and keyboard input. Scrolling at a point on a secondary monitor reaches it instead of the primary monitor's edge; a coordinate that is not a number or is out of range raises
AutoControlMouseExceptioninstead of moving somewhere else; on Windows a keycode past 16 bits is refused instead of pressing a different key. -
Computer-use agent actions work. Clicks, double and triple clicks, drags, waits and held keys were translated into calls the executor could not make, so each failed as a step error; they now map to real commands.
-
Agent backends run only the tools they offered. A model reply naming any other
AC_*command (a shell command, for instance) raisesAgentBackendErrorinstead of executing; unparsable or non-object OpenAI arguments raise instead of running the tool with{}; computer-use coordinates are clamped to the display. -
VLM location. Provider errors (rate limit, timeout) are handled like other request failures instead of escaping, and a point outside the requested region is "not found" instead of clicked.
-
Circuit breaker, config sync and ACME. A half-open circuit breaker admits one trial call at a time instead of every concurrent caller, and is safe to share between threads. Config sync rejects a malformed server reply and reports a dropped connection as
ConfigSyncError; the ACME client reports an unreachable CA asAcmeErrorand a bad CSR asJwsError; andrenewal_dueaccepts a naivenow. -
DAG, state machine, recurrence rules and suites. A DAG node whose actions fail is failed (its dependants are skipped), and a slow node no longer holds back unrelated ready nodes. State-machine
afterguards wait for their timer, counted from state entry, and reaching the final state on the last allowed step succeeds. YEARLY RRULEs without BYMONTH cover the whole year as RFC 5545 specifies, a rule that can never match ends instead of overflowing, and invalid INTERVAL / COUNT / BYMONTH / BYMONTHDAY values are rejected. A data-driven suite no longer leaves its row variable set, and anOverflowErrororZeroDivisionErrorfrom an action is recorded instead of aborting the script. -
Remote desktop keeps serving after bad input. A malformed INPUT message or WebSocket frame no longer kills the host's receive thread while its viewer keeps a client slot, nor the viewer's thread without an error callback; a slow or silent peer no longer blocks other viewers from connecting (each connection is handshaken on its own thread); and a signaling timeout or hang-up is reported as
SignalingError. -
Generated code cannot run what an action file smuggles in. Codegen emits a parameter as a keyword argument only when its name is a plain identifier, and the Robot target carries the actions base64-encoded. Pytest code generated with
failure_bundle=Truenow runs (it imported the wrong module asac). -
USB/IP, TLS keys, signaling and relay. A USB/IP client can only send URBs to the device it imported; TLS private keys are written atomically and 0600 from creation; the signaling server compares its secret in constant time and caps live sessions (503 when full); and the relay frees the slot of a parked peer that disconnected.
-
Approval gate, asset store and locator-repair store across processes. Each change re-reads the file under a lock file, so processes sharing it no longer overwrite each other; an approval request can be decided only once.
-
Recording and hotkeys. A recording that cannot start no longer replaces the output file with
[]; starting a second recording stops the first input hook instead of leaking it. Hotkeys on punctuation keys (ctrl+.,ctrl+[) register those keys rather than Delete or the Windows key; a combo Windows cannot register is refused bybindand no longer retried 20 times a second; and an error in the run history or an injected executor no longer ends the hotkey listener. -
Legacy
python -m je_auto_control. A missing or invalid action file is reported as a log line instead of a traceback (the exit status was already 1), and a-dpath that is not a directory is an error. -
JSON-file stores. The flaky-test quarantine, the remote-desktop trust list and known hosts, and the RBAC user store are replaced atomically (readers never see a partial file) and load empty instead of raising when the file is not UTF-8 or has the wrong shape. A failed first read no longer makes the A/B locator store overwrite its counts, and a torn last line in the cost or self-healing log no longer swallows the next record.
-
SQLite-backed stores. Two dispatchers can no longer enqueue the same work-item reference;
WorkQueue.failon an unknown id raises instead of reporting a requeue; the work queue, checkpoint store and agent memory close their connections; a run-history or audit-log database error is anAutoControlException(HistoryStoreError,AuditLogError) instead of asqlite3.Errorthat ended the hotkey listener thread; and two processes writing one audit log keep its hash chain valid. -
Credentials no longer follow a redirect to another host. The HTTP client drops
Authorizationand cookies when a redirect changes host. The Jira, Linear and GitHub failure-hook backends and the Slack bot now obey the egress policy and refuse redirects, and a non-object JSON reply is a failed call instead of an exception that stopped the Slack poll loop. -
Remote-desktop file transfers cannot leave partial files or destroy the original. Both receivers write to a
.partfile and rename it into place only when exactly the announced number of bytes arrived; excess or missing data fails the transfer. The WebRTC inbox refuses Windows device names (nul,COM1.txt) and names ending in a dot or space, and a malformed envelope or destination fails the transfer instead of killing the connection's receive thread. -
One misbehaving admin host no longer fails every host's poll, and an address book whose
hostsis not a list loads empty instead of raising. -
A failing data step no longer aborts the script. SQLite, CSV, regex, PDF and malformed-HTTP errors from
AC_sql_to_var,AC_assert_db,AC_for_each_row,AC_transform_var,AC_pdf_to_varand the HTTP commands are recorded like any other failed action;AC_otp_to_varrefusesstep <= 0instead of dividing by zero. -
HTTP redirects obey the egress policy. Only the first URL was checked, so an allowed host could redirect a request anywhere, including
ftp://. -
Reports. A recorded control character (an ANSI colour code, a stray
\x01) no longer breaks the XML report or makes a JUnit file unreadable; such characters appear as U+FFFD. A report that cannot be written raisesAutoControlHTMLException,AutoControlGenerateJsonReportExceptionorXMLExceptioninstead of being logged and skipped, and reports are written atomically. Exception text is no longer wrapped in an extra pair of quotes. -
Vault passphrases and secret values no longer reach logs or results. The arguments of
AC_secret_*commands are shown as***in the executor's log lines and in the keys of the record it returns (a secret command's key changes accordingly), and the socket server no longer logs the command text. -
Changing the vault passphrase cannot lose secrets. The vault is rewritten once, atomically, instead of being deleted and refilled.
SecretStoreErroris now also anAutoControlException. -
Config-bundle imports keep every backup. Two imports in the same second no longer overwrite the first
.bakfile. -
JE_AUTOCONTROL_REQUIRE_SIGNED_ACTIONScovers every way a file runs. Onlyexecute_fileschecked it: the CLI'srun, the scheduler, triggers, webhooks, hotkeys, the MCPexecute_action_filetool and the GUI ran unsigned files with enforcement on, andexecute_filesitself verified one read of the file and parsed another. All of them now load through the newread_executable_action_json, which verifies and parses the same bytes. -
Signing and encryption keys. Key files are created 0600 in one step and never overwritten by a concurrent process; a key file shorter than 32 bytes, which an interrupted first run could leave empty, is refused instead of being used as an empty HMAC key. An empty explicit key or passphrase is refused.
-
Cron expressions follow standard cron, and a job with no next run stops. With both day fields restricted a day now matches if either does (
0 0 1 * 1is the 1st and every Monday, as in Vixie cron and croniter; it was the Mondays that fall on the 1st),7is accepted as Sunday, and5/15is5,20,35,50rather than5.0 0 29 2 *failed to find its next run whenever the next leap day was over a year away, and the scheduler then fired that job on every tick; the search covers eight years, and a job whose next run cannot be computed is removed and logged. Migration: an expression with both day fields restricted fires on more days than before — write one of them as*to keep the old intersection. -
AllOftriggers no longer lose a cron minute, file change or sequence step to a false sibling. The edge child was checked first and spent its event before a later condition failed; edges are now checked last. -
E-mail triggers leave mail unread with
mark_seen=False. The fetch itself set\Seen; it usesBODY.PEEK[]. -
A webhook whose script fails with any exception answers 500. Types outside four caught ones were recorded as a success and dropped the connection without a reply.
-
USB passthrough claims stalled, leaked and outlived their viewer. After the first 16 transfers every request failed with "credit exhausted" — the host granted the viewer more credit with each reply but never counted it itself — and each failure counted as abuse. Claims were never released when the viewer's channel closed or the host stopped, leaving the device open and its kernel drivers detached (a claimed keyboard or mouse stayed gone). After 65,534 claims the id counter wrapped onto a live claim and orphaned its handle.
-
Multi-viewer WebRTC host: sessions that outlived their viewer. A viewer waiting on the Accept/Reject dialog was torn down by the 5-second auth deadline, so the user approved a dead session. A session whose peer connection failed or closed stayed registered —
session_countonly grew and screen capture never stopped once the last viewer left. An offer that failed (consent refused, timeout) left a session the caller never learned the id of, and the host-service daemon left one behind on every answer that did not arrive within 300 s. All four now end the session. -
Flow control: seven ways a script did something other than it said. A failed assertion inside an
AC_parallelbranch, or in anAC_retrythat ran out of attempts, was wrapped into an ordinary error and swallowed underraise_on_error=False; it now propagates like any other assertion.AC_break/AC_continuewith no enclosing loop escapedexecute_actionentirely and skipped the rest of the script; they are now a recorded failure ("outside a loop"). A macro that calls itself recursed until Python's limit (or, with two self-calls, ran exponentially long); calls now fail pastMAX_MACRO_DEPTH(50) and the failure is recorded at the top level. An empty macro,AC_assert_durationbody orAC_parallelbranch is a no-op, as empty bodies are everywhere else, instead of an error.AC_wait_image/AC_wait_pixelwithtimeout0 now look once instead of never. -
A USB watcher stopped while it was still taking its first inventory forgot that inventory. The poller discarded its priming enumeration whenever
stop()had been called, not only when a newerstart()had replaced it, so a followingpoll_once()reported every connected device as newly added — or not, depending on which side of the enumeration the stop landed. It now discards it only when superseded. -
On Windows the key name
downpressed F17 instead of the Down arrow. The Windowskeyboard_keys_tablewas built from every constant inwin32_vk.py, including theMOUSEEVENTF_*andKEYEVENTF_*flags, anddownwasMOUSEEVENTF_XDOWN(0x80), which isVK_F17. It is nowVK_DOWN, as on Linux and macOS. The 18 other names that were flags rather than keys are gone from the table, so a script using one fails with "unknown key" instead of pressing whatever key shares its value (middledownpressed space,moveandxbutton1sent the left-button code):absolute,eventf_extendedkey,eventf_keyup,eventf_scancode,eventf_unicode,hwheel,leftdown,leftup,middledown,middleup,move,rightdown,rightup,xbutton1,xbutton2,vktovsc,wheel,xup. Migration: click mouse buttons through the mouse API (mouse_x1,mouse_x2,mouse_left…);vk_xbutton1/vk_xbutton2remain for the side-button virtual keys. -
Recording on Windows lost touchpad scrolling up and multiplied it down. A precision touchpad reports the wheel in fractions of a notch (typically ±30 of 120), and the recorder floored each event separately:
30 // 120is 0 and-30 // 120is -1. Scrolling up vanished from the recording and scrolling down replayed about four times too far. The hook now carries the remainder until it makes a whole notch, and drops it when the direction reverses. -
The scheduler could start a job again while it was still running. A job is rescheduled only after it finishes, so until then it still looks due. A single loop cannot overlap itself, but after a
stop()whose join timed out inside a long job, the new run's loop saw the job as due and started a second copy.Schedulernow tracks the jobs it is executing and skips them. -
A remote desktop viewer from a stopped host could attach to the restarted one.
RemoteDesktopHost,RemoteDesktopRelayandRemoteDesktopViewerhad the same shared-event restart as the services below. On the host it mattered most: the accept loop performs the auth handshake (up to 60 s) before re-checking the stop flag, so a handshake begun beforestop()and finished after the nextstart()passed that check and attached a viewer authenticated against the old run to the new one. On the viewer, a receiver outlivingdisconnect()marked the next connection as disconnected and reported its own closing socket through that connection'son_error. Each run now has its own event, and a receiver only touches its own connection. -
Restarting a background service could leave the old loop running beside the new one. Sixteen services — the scheduler, trigger engine, e-mail triggers, hotkey daemon, screen observer, popup watchdog, clipboard history, resource profiler, accessibility recorder, MCP plugin watcher, folder sync, ACME renewal, USB loopback, USB/IP server, the macOS recorder tap and the Slack bot — stopped by setting an event and joining with a timeout, and started by calling
clear()on that same event. When the join timed out because the loop was inside a long iteration (a scheduled job, a hotkey action), the nextstart()cleared the event the old loop was waiting for and it resumed, untracked: every scheduled job ran twice, every hotkey fired twice. Each run now gets its own event, passed to its loop, so a stopped run stays stopped. -
Restarting the USB hotplug watcher could leave a second poller running.
UsbHotplugWatcher.stop()waited only 2 s for the poller, but one enumeration (PowerShellGet-PnpDevice,lsusb,system_profiler) may take up to its 10 s subprocess timeout, sostop()could return — andAC_usb_watch_stopreportrunning: false— while the thread was still running. A followingstart()then calledclear()on the same stop event, and the old poller carried on beside the new one, untracked, for the life of the process. Each run now gets its own event, a poller stopped mid-enumeration no longer overwrites a newer run's snapshot, andstop()waits for up to the subprocess timeout plus one second, logging a warning if the poller is still running after that.usb_devices.SUBPROCESS_TIMEOUT_Sis now public (it was_SUBPROCESS_TIMEOUT_S). -
A failing
hotkey()ortype_keyboard()left keys held down. Both press and then release with nothing protecting the gap, and theirexcept (OSError, RuntimeError, AttributeError, TypeError, ValueError)does not coverAutoControlKeyboardException— the errorpress_keyboard_key/release_keyboard_keyactually raise for an unknown key name, an unsupported platform or a backend failure. Sohotkey(["ctrl", "shift", "esc"])failing onescleftCtrlandShiftdown on the real keyboard, changing the meaning of every later click and keystroke. The release now runs fromfinally: only keys that were actually pressed and not yet released are released, in reverse order, and a release that fails during that cleanup is logged rather than raised, so the caller still sees the original error. -
The Windows recorder dropped mouse side buttons. Playback has always accepted
mouse_x1/mouse_x2, but the low-level hook keyed its button table by message id, andWM_XBUTTONDOWN/WM_XBUTTONUPare one id for both buttons (which one is in the high word ofmouseData). Side clicks were therefore never recorded, and a macro replayed without them with no warning.stop_record_timeline()now yieldsmouse_down/mouse_upevents withbutton"x1"/"x2", andreplay_timeline()maps them tomouse_x1/mouse_x2. An unrecognised side-button value is dropped rather than guessed, because the replay side falls back to the left button for a name it does not know. The legacy down-events-only queue (stop_record()) still omits them: there is noAC_mouse_x1command to put in it. -
Changing a hotkey's combo on X11 left the old key grabbed for the life of the daemon.
LinuxHotkeyBackend._sync_onedropped the previous registration from its own table without callingungrab_key, so the old combo stayed grabbed on the X server: it was swallowed from every application, fired nothing, and_ungrab_allcould not release it at shutdown because it no longer knew about it. Rebindingctrl+alt+kto something else madectrl+alt+kdead system-wide until the process exited. The Windows backend has always unregistered at the same point; the X11 one now does too. Unaffected on Windows and macOS. -
A window closing mid-call let a COM error escape every Windows accessibility read.
comtypesreports a provider failure asCOMError, which derives straight fromException— the reasonwindows_query._uia_errors()exists — but only the two tree-walking guards inbackends/windows_backend.pyused that tuple. The other 37, covering every control pattern (get_value,invoke,toggle,read_table, the text and grid reads, …), named(OSError, AttributeError, …)and therefore contained none of them. An application that stopped responding, or a window that closed between the search that found an element and the call that read it, raisedCOMErrorout of theac_*tool orAC_*command instead of answeringNone/False/[], and past the executor'sAutoControlExceptionboundary. All 37 now use the same tuple. This only widens what is caught: no call that used to succeed behaves differently. -
The WebRTC viewer ended every clean disconnect with an unhandled task exception.
WebRTCDesktopViewer._consume_videocaught(OSError, RuntimeError), but aiortc signals the end of a track by raisingMediaStreamError, which derives straight fromExceptionand so matched neither. Nothing awaits that task, so the normal end of a session — the host stopping its screen share, or the connection closing — reached the console as asyncio's "Task exception was never retrieved" traceback instead of the "video stream ended" line the host's own drain loop already logged. The stream is unaffected either way; only the logging changes. -
A
nullin a remote-desktop entry'stagsbecame a tag named"None".AddressBook.set_tags()cleaned its input withstr(t).strip(), andstr(None)is the non-empty string"None", so a JSONnullin the array — what a client sends for an omitted tag — was stored as a tag and then listed byall_tags()alongside the real ones. Nulls are now dropped. Tags that were already stored this way stay until the entry's tags are set again.
-
mouse_scroll()rejects a scroll direction the platform has no axis for. A name outsidespecial_mouse_keys_tableused to be passed down to the backend unchanged, which meantint('scroll_upp')on Wayland and uinput and an Xlib failure on X11 — deep in the backend, with the offending name nowhere in the message. It now raisesAutoControlCantFindKeyExceptionnaming the direction, the same answer the button table has always given for an unknown button name. Windows and macOS are unaffected: they have a single wheel axis and never read the direction. -
je_auto_control.stop_record()returns an empty list where it used to returnNone. It has always been annotated-> list, but the failure path fell off the end of the function, so a caller that did not writestop_record() or []iterated overNoneand raised in its own code instead.stop_record_timeline()already returned[]on the same failure; the two now agree. -
je_auto_control.mouse_scroll()reports its return type asTuple[int, Union[int, str]]. The value has not changed — X11 and Wayland still hand back the backend axis code the direction name resolved to, and every other platform the name itself — the signature just no longer claims it is always astr. -
The Windows screen backend's
size()returns atuple, not alist. The macOS, X11 and Wayland backends all returned tuples already, and the publicscreen_size()has always been annotatedTuple[int, int]; every caller unpacks the two values, so nothing that used it needs changing.
-
Typing text through the key-event route raised
AttributeErroron the three platforms that cannot do it.type_unicode_keys()(andAC_type_unicode_keys/ac_type_unicode_keys) called the backend'stype_unicode_unitoutright, and only Windows has one, so macOS, X11 and Wayland raised an exception from outside theAutoControlExceptionfamily that the executor, the background poll loops and the request handlers each catch in oneexcept— it escaped every containment boundary in the project. It now raisesAutoControlKeyboardExceptionpointing attype_unicode_text(), which picks a route that works on any platform. -
A backend that could not report the cursor aborted the script instead of raising what the API promises.
press_mouse/release_mouse/click_mousewith an omittedxoryunpackedget_mouse_position()without checking it forNone, so a backend that answers "I don't know" raisedTypeErrorfrom the unpacking — outside theAutoControlMouseExceptionfamily every containment boundary catches. It now raisesAutoControlMouseException.mouse_scrollreached the same unpacking through_scroll_toand now skips the pre-move instead, which is the graceful degradation its own comment already documented for backends that cannot report the cursor. -
je_auto_control.windows.message.window_messagecould not be imported at all. It didfrom ...windows_window_manage import FindWindowW, and that module has no such name —FindWindowWis a method on its privateuser32handle — so importingwindow_messageraisedImportErroron every Windows machine. It now calls the module's publicget_one_window_hwnd, which is also the one that declares HWND-width argtypes rather than letting ctypes truncate a 64-bit handle toc_int. -
Importing the Win32 input backend no longer writes into
ctypes.wintypes.win32_ctype_inputsetwintypes.ULONG_PTR = wintypes.WPARAMon the standard library's own module. Nothing in this package ever read it back, so the only effect the assignment could have was on some other library in the same process askingctypes.wintypeswhether it hasULONG_PTR. -
Stopping an X11 recording that was never started raised instead of returning nothing. The X11 listener's
stop_record()handed back theNoneits queue attribute was constructed with, and the recorder one frame up reads.queueoff that result, sostop_record()without a precedingrecord()produced anAttributeErrorthat the wrapper caught and logged as a failure. It now returns an empty queue, so the publicstop_record()returns the empty list it documents. -
check_key_is_press()passedNoneto the backend for an unknown key name. A name the virtual-key table has no entry for becameNoneand was handed to the platform backend anyway: aTypeErroron Windows and a silentFalseon X11 — that is, "no, it is not pressed" for a key that does not exist. It now logs the lookup failure and returnsNone, which is the documented "could not answer" value.
- Windows on arm64 installs.
opencv-python,cryptographyandje_open_cvnow carry the environment markersys_platform != 'win32' or platform_machine != 'ARM64', because none of the three publishes awin_arm64wheel andpip install je_auto_controltherefore failed on that platform before any of this code ran. Every other platform resolves exactly the same dependency set as before. On Windows arm64, the features that need those wheels —find_image*, the OpenCVscreenshot(), the secret vault, action-file encryption, ACME/TLS and encrypted recording — raise aRuntimeErrororImportErrornaming the missing wheel rather than a bareModuleNotFoundError. Python 3.11 is the floor there, since CPython publishes no official Windows arm64 build for 3.10.
-
The macOS recorder works.
record(),stop_record(),stop_record_timeline(), theAC_record*commands, theac_record_*MCP tools andje_auto_control recordall run on macOS now; they used to refuse outright with "Cannot use recorder on macOS". Capture goes through a listen-only QuartzCGEventTapon its own thread (je_auto_control.osx.listener.osx_listener.OSXInputTap), so it records presses, releases, the wheel and per-event timing, exactly as the Windows hook does. Requires Accessibility permission; without it the tap raisesAutoControlRecordExceptionnaming the permission — where the facade'srecord()logs it, as it does every other backend's start failure — rather than starting a session that silently records nothing. -
je_auto_control.utils.input_macro.recorder_base— the platform-neutral half of recording:timeline(),legacy_action_queue()and theInputRecorderbase the Windows and macOS recorders now share.timelinekeeps working when imported fromje_auto_control.windows.record.win32_input_hook, where it used to live. -
Cross-platform window management. The 23
AC_*window commands and their MCP tools now work on macOS and Linux/X11 as well as Windows, through a backend seam (je_auto_control.wrapper.window_backends). Wayland remains unsupported: the protocol does not let a client enumerate or move another application's windows. -
Linux accessibility backend over AT-SPI2 (
je_auto_control.utils.accessibility.backends.linux_backend), with no new dependency. Serves both X11 and Wayland sessions. -
je_auto_control.utils.platform_id— one place that classifies the operating system family, and the BSDs are now one of them. FreeBSD, OpenBSD, NetBSD and DragonFly route to the X11 backend instead of raising "unknown operating system". -
AutoControlUnsupportedOperationException, raised when a platform backend cannot perform an operation. It subclasses bothAutoControlExceptionandNotImplementedError, so existingexcept NotImplementedErrorhandlers are unaffected while the executor's containment boundaries now catch it. -
je_auto_control.utils.dbus_client— the D-Bus client, moved out oflinux_wayland/soutils/can use it. The old path re-exports it. -
MCP sessions over HTTP.
initializenow mints anMcp-Session-Idand returns it as a response header. A client that echoes it keeps one dispatcher scope — the capabilities it advertised, and the slots its in-flight calls occupy — across every connection it opens, instead of one scope per TCP connection.GET /mcpwithAccept: text/event-streamand a valid session id opens the standing server-to-client SSE stream (one per session; a second gets 409),DELETE /mcpwith the id terminates the session, and a server request is answered byPOSTing an ordinary JSON-RPC response on any connection. Sessions are swept after ten minutes untouched and capped at 128.je_auto_control.utils.mcp_server.http_sessionsholds the registry. -
JE_AUTOCONTROL_MCP_CONFIRM_DESTRUCTIVE=1now works over HTTP — for a client that echoesMcp-Session-Idand holds theGETstream open. It previously fired only on stdio: the prompt needs a server-to-client channel bound to the scope that receivedinitialize, and a connection-keyed scope never survived to thetools/call. A client that does neither still cannot be prompted and its destructive calls still proceed, exactly as for a stdio client that never advertisedelicitation; that fallback is documented and is not a substitute for the bearer token, the127.0.0.1bind orJE_AUTOCONTROL_MCP_READONLY.
-
The MCP HTTP transport answers
GET /mcpdifferently. It used to return405with{"error": "GET stream not supported"}for every request; it now serves the session's SSE stream when the request carriesAccept: text/event-streamand a validMcp-Session-Id, and still returns405when theAcceptheader does not ask for a stream. A request — of any method — carrying anMcp-Session-Idthe server does not know is refused with404rather than served under a fresh scope, which is the signal to re-runinitialize.DELETE /mcpwithout a session header is still accepted as a no-op, so clients that never adopt sessions are unaffected. -
The default run-history database is created when it is first written to, not while
je_auto_controlis being imported.HistoryStoreopens its connection (and makes its parent directory) on first use, so merely importing the package no longer creates~/.je_auto_control/run_history.sqlite. Every method behaves as before; a store that was never used and then closed simply never touched the disk. -
The sign of
scroll_valuepicks the scroll direction on every platform. Windows and macOS have always read it that way; X11 and Wayland took the direction fromscroll_directionalone and usedabs(scroll_value), somouse_scroll(-3)— code written and tested against the Windows convention — scrolled down three notches on Linux instead of up, with no exception and no warning.scroll_directionnow names the direction a positive count takes, and a negative count reverses it, on all four backends.Migration. Code that passed a negative
scroll_valueto Linux or Wayland and relied on the magnitude alone now scrolls the opposite way. Takeabs()at the call site to keep the old behaviour:mouse_scroll(abs(value), scroll_direction="scroll_down"). Code that passed a positive count is unaffected, as is every Windows and macOS caller. -
import je_auto_controlno longer imports OpenCV, NumPy, Pillow,je_open_cvorcryptography. They are imported by the functions that use them. The facade pulled all five in at module scope, so a platform without wheels for them — a FreeBSD desktop, for one — could not use the input automation half of the package at all, though it needs none of them. Nothing moves in the public API and the packages remain hard dependencies; what changes is when a missing one is reported, which is now at the first image or encryption call rather than at import.test_facade_import_is_light.pykeeps it that way. -
macos_record_error_messagenow names a permission, not a platform. It read "Cannot use recorder on macOS", which described a limitation that no longer exists; it now names the Accessibility grant that recording actually needs, and is raised from the event tap rather than from the wrapper.
-
The MCP HTTP transport no longer tries to drain a request body it has already read. Any
4xxdecided after the body was parsed — the new unknown-session404and duplicate-stream409, and the pre-existing "body must be UTF-8"400— called_drain_body(), which then blocked reading bytes that were gone until the 30-second socket timeout, pinning that worker and logging aConnectionAbortedErrortraceback when the peer closed first. The drain is now skipped once the body is consumed, and a peer that has already vanished ends it quietly instead of raising. -
A rejected config bundle aborted the rest of the script. Five framework errors still inherited
Exceptiondirectly —ConfigBundleError, the USB passthroughProtocolError,SessionErrorandUsbClientError, and the work queue'sBusinessError— and the containment boundaries all catch theAutoControlExceptionfamily, so none of them caught these. A malformed bundle passed toAC_config_importtherefore raised straight past the executor's per-action boundary and killed every remaining action, even underraise_on_error=False;AC_usb_remote_devicesandAC_usb_remote_openhad the same path throughUsbClientError. All five derive fromAutoControlExceptionnow, so they are recorded as a failed action like every other framework error.LoopBreak,LoopContinueand the MCP dispatcher's private error carrier stay outside the family deliberately — they are control flow, not failure. -
import je_auto_controlneeded a Python built withsqlite3, and FreeBSD's is not.sqlite3is in the standard library but not in every build of it: CPython links it against a system library, and FreeBSD ships the result as the separatedatabases/py-sqlite3package. Ten subsystems imported it at module scope — run history, checkpoints, the work queue, agent memory, the remote-desktop audit log, SQL data sources, and the error tuples in the REST, chat-ops and MCP containment boundaries — and all ten are reachable from the facade, so the whole package failed to import on a stock FreeBSD, mouse and keyboard included. They go throughje_auto_control.utils.sqlite_supportnow, which fails at the first call that opens a database rather than at import, and raisesAutoControlUnsupportedOperationException— the type the GUI tabs, the REST handler and the executor already report as "not available here" — instead of anImportErrornone of them catch.run_diagnostics()listssqlite3among the optional dependencies, so the gap is visible without reading a traceback. -
mouse_scrolldid nothing at all on the BSDs. It matched Windows, then macOS, then a literal["linux", "linux2"], so a FreeBSD, OpenBSD, NetBSD or DragonFly caller fell off the end of the chain: no backend call, no exception, no log line. It asksplatform_id.is_x11_unix()now, and an unrecognised platform raisesAutoControlMouseExceptioninstead of returning as though it had scrolled. -
A recorded timeline replayed nothing.
replay_timeline's dispatch table held therun_sequenceDSL's vocabulary (press/click/key) and the recorders emit their own (key_down/mouse_up/scroll), and the two were disjoint — sostop_record_timeline()fed toreplay_timeline(), the pipeline both the docstrings and theac_record_stop_timelinetool prescribe, matched no handler, replayed an empty session, and still returned every event as played. The recorder ops dispatch now, and the wheel readsdeltaas well asvalue(reading onlyvaluefell back to the default of one notch, so a three-notch scroll down replayed as one notch the other way). Affects every platform, not only macOS. -
macOS: recorded mouse coordinates were mirrored vertically. The listener read
NSEvent.mouseLocation(), whose origin is the bottom-left of the display, while every replay posts into the top-left spaceosx_mouseuses — so a click recorded near the top of the screen replayed near the bottom. It now readsCGEventGetLocation, which is already in the space the replay posts into. -
macOS: modifier keys were not recorded at all. macOS sends no key-down for Shift, Control, Option or Command, only a
flagsChangedevent carrying the new flag set, so a recording could not say a modifier was held across the actions that followed. They are reconstructed from the flags now. -
macOS:
write()typed a space instead of a backspace, because"\b"had no route in the macOS key table and fell through to the space fallback. -
macOS: USB enumeration returned
apple_vendor_idinvendor_id, a field documented as a four-hex-digit string. A value that is not a hex id is nowNone; the device is still listed andmanufacturerstill names the vendor. -
Linux/X11:
window_rectreturned the client area rather than the frame, disagreeing with Win32'sGetWindowRectby the window decorations. -
Linux/X11:
move_window_by_titleconfigured the client window directly, which under a reparenting window manager positions it in the wrong coordinate space. It now goes through_NET_MOVERESIZE_WINDOW. -
The D-Bus client could not marshal or demarshal signed integers, so any protocol using them (AT-SPI extents among them) failed to decode.
-
Window ownership:
foreground_window_process_idandwindow_process_id(AC_foreground_window_pid,AC_window_pid;ac_foreground_window_pid,ac_window_pid; two Script Builder specs), on the Windows backendget_window_process_id. A title is whatever the application decides to display, so it cannot answer "which program is the user actually in front of" — the process id can. Unavailable reads asNone({"pid": 0}on the JSON surfaces) rather than a bare0, which a caller could otherwise match against a process list and hit the System Idle Process. -
Windows by owning process:
windows_for_process_idandminimize_windows_for_process(AC_windows_for_pid,AC_minimize_windows_for_pid;ac_windows_for_pid,ac_minimize_windows_for_pid; two Script Builder specs). A multi-process application cannot be addressed by title — its windows are named after whatever they display and several of its processes have no window at all — so ownership is the stable key. -
Input posted to a window without focusing it:
post_key_to_windowandpost_click_to_window(AC_post_key_to_window,AC_post_click_to_window;ac_post_key_to_window,ac_post_click_to_window; two Script Builder specs), on the Windows backendget_focused_control,deepest_child_at,post_keyandpost_click. They resolve the window by title substring like every other function here, and post to the control that actually has keyboard focus — or, for a click, to the deepest child under the point, in that child's client coordinates. Posting to the top-level frame (what the oldersend_key_event_to_windowdoes) types nothing in any application with child controls: measured on Character Map, the frame swallowed the key while the focused edit accepted it. Both return whether the messages were queued, and posting remains best effort — applications reading raw input or checking the foreground ignore posted messages. -
JE_AUTOCONTROL_WAYLAND_POINTER_ACCEL— how an operator declares what the library cannot read back.flatsays pointer acceleration is off for the ydotoold device, so an absolute move through the ydotool fallback is exact and needs no warning;strictrefuses that move instead of letting a click land somewhere else; unset (or any unrecognised value, which says so and falls back) keeps the existing warn-once-and-move behaviour. The libei path is absolute at the protocol level and is not affected either way.
-
The
xdg-desktop-portalcapture tier no longer needsgdbusinstalled. It speaks D-Bus directly, solinux_wayland.portal.is_available()now reports whether a session bus address is set rather than whether thegdbusbinary is onPATH. This widens where the last-resort tier runs; the install hint in the "no capture tool found" error and thescreen_capturediagnostics check were reworded to match. -
LibeiBackend.scroll()sends whole wheel clicks, not raw detent counts. libei measures discrete scroll in 120ths of a click, so the previous call asked for 1/120th of the scroll requested and libei logged it as a client bug. Measured against a real EIS server (docker/eis_verify.py). -
Wayland
mouse.scroll()goes through libei where libei is up, instead of always shelling out to ydotool. Motion, buttons and keys already did; scroll was held back because its sign was a guess. The two paths count wheel detents in opposite directions — this repository'swayland_scroll_direction_*constants are in the kernel'sREL_WHEELframe, which is what ydotool writes (positive is up), and libei is in thewl_pointerframe (positive is down) — so the vertical axis is negated on the way to libei and the horizontal one is not. No API change: scrolling on a libei host no longer needs ydotool or a uinput daemon at all. -
A libei emission that a live backend refuses now falls back to the CLI, as
libei's own docstring already claimed it did. Only the connection degraded; a compositor that paused a device, or a session that ended between two calls, raised out ofset_position/press_key/hotkeyinstead of reaching ydotool. A chord refused part-way releases the keys it already pressed before handing over, so no modifier is left held. -
LibeiUnavailablederives fromAutoControlException(as well asRuntimeError, which existing probes catch). It was a bareRuntimeError, so it escaped everyexcept AutoControlExceptioncontainment boundary — the executor, the poll loops, the request handlers and the GUI slots. -
A libei session that completed its handshake is released instead of abandoned.
ei_unrefsegfaults on libei 1.3.901 only for a context whose backend opened and whose handshake never progressed; with an EIS peer to test against, the live case is measurably safe. Teardown no longer leaks a context and a file descriptor per process. -
POST /executeandPOST /execute_fileanswer400, not500, for a command name the executor does not know. Both used to funnel every executor failure into500 {"error": "execute_action failed"}, so a client could not tell a typo in its own request from a broken server. Every name in the list — nested flow-control bodies included — is now checked before anything runs; an unrecognised one comes back as400 {"error": ..., "unknown_commands": [...]}naming all of them, and nothing was executed./execute_fileanswers the same way for a path that is unreadable or holds something that is not an action list. A client that keyed off500to detect a bad request must key off400instead. -
Windows clipboard calls wait out a clipboard another process is holding open instead of failing immediately. Only one process may have it open at a time, so
RuntimeError: OpenClipboard failedused to escape whenever anything else was mid-copy — roughly one call in a thousand on a live desktop.win32_clipboard_api.open_clipboard()is the one place that opens it now, retrying for about 200 ms; the failure is still raised after that. Callers that relied on an immediate failure will see a call take up to 200 ms longer in the contended case. -
send_key_event_to_window/send_mouse_event_to_windownow actually reach the target. They posted to the top-level frame, but keyboard messages go to the control that has focus and a click belongs to the child under the point in that child's client coordinates — so for any window with child controls they did nothing at all while still reporting success. They delegate topost_key_to_window/post_click_to_window. Two visible consequences: the key sender now matches the window title as a substring (it required an exact title before, viaFindWindowW), and the mouse sender accepts a title string as well as the hwnd it always took. -
save_window_layoutnow snapshots only titled windows (its documented behaviour). Untitled entries could never be restored —restore_window_layoutaddresses a window by title and skips blank ones — so they only inflated the saved count, by roughly half on a real desktop.
send_key_event_to_windowandsend_mouse_event_to_window— usepost_key_to_window/post_click_to_window. Both now emit aDeprecationWarningand delegate to the working implementation; see Changed for the behaviour that changes.
je_auto_control.linux_wayland._detect.WAYLAND_GDBUSis gone, along with thegdbusprobe it named: the desktop-portal capture tier no longer shells out to any binary._detectis a private module and nothing else referenced the constant.
-
Wayland: an absolute mouse move through the ydotool fallback counted from the wrong origin.
ydotool mousemove --absoluteemits no absolute event — it drives the cursor into the corner the compositor clamps to and then moves relative to it, and that corner is the top-left of the output layout rather than layout(0, 0). On a layout with a monitor left of the primary one the two differ by the layout origin, soset_position(x, y)landed a monitor's width away from the coordinate the capture path had located. It now subtractslayout_origin(), the same correctiongrab_imageapplies. Measured against a real wlroots session consuming the real ydotool device (docker/Dockerfile.seat, the newseat-verificationjob). Layouts whose outputs all sit at non-negative positions are unaffected. -
Wayland: the same call is only pixel-accurate where pointer acceleration is off. The displacement ydotool sends is relative motion, so the compositor accelerates it — libinput's default adaptive profile moves the cursor exactly twice as far as asked. This cannot be corrected from inside the library, because the factor is the compositor's setting; the backend now logs the caveat once per process rather than mispositioning in silence. Disable acceleration for the ydotoold device (sway:
input type:pointer accel_profile flatandpointer_accel 0), or installliboeffisso the libei path — absolute at the protocol level — is used instead. Once it is off,JE_AUTOCONTROL_WAYLAND_POINTER_ACCEL=flatsilences the warning, and=strictrefuses the move rather than warn about it. -
The Wayland
xdg-desktop-portalscreen-capture tier could never have succeeded.org.freedesktop.portal.Screenshotreturns a request handle and delivers the image later as aResponsesignal directed at the connection that made the call; the bus routes a directed message to its destination and nowhere else. The implementation listened on agdbus monitorsubprocess and called from a separategdbusinvocation — two connections, so the listener was never the addressee. Against a realdbus-daemonthe capture ran out its full 30-second timeout every time. The tier now speaks D-Bus itself on a single connection, subscribing to the request path it predicts before it calls. No API changed; a path that always failed now works. -
On Wayland, a monitor placed left of or above the primary one made every capture path read the wrong pixels. The compositor lays its outputs out on one plane, and that plane starts at a negative coordinate as soon as an output sits left of (or above) the origin — a
-1280,0+0,0pair is one 2560x720 layout whose top-left pixel is at x=-1280. Three places assumed the layout began at(0, 0):screen.size()returnedmax(x + width), the layout's right edge (1280) rather than its width (2560), so everything that composes size with a capture — the mss-shaped shim's monitor list,enumerate_monitors, the recorder, the WebRTC host, the MCP monitor grab — asked for half the desktop and called it the whole screen; the region crop taken when the capture tier cannot apply one itself (gnome-screenshot, spectacle, the portal, an operator's own command) cropped in layout coordinates on a layout-origin image, which returns black padding instead of the left-hand monitor; andgrab_logicalreported an origin of(0, 0), so a template or OCR match found on that monitor was reported 1280 px to the right of where it was seen and the click landed on the wrong screen. The Wayland backend now publisheslayout_origin(),size()returns the bounding box's size, the crop subtracts the origin, and the generic capture layer exposesscreen_grabber.backend_layout_origin()for the paths that map a pixel back to a screen coordinate. Verified against a real headless sway session laid out that way — thewayland-verificationjob now runs its 27 checks over both layouts. -
On Wayland,
set_positioncould move nothing at all and report success. libei accepts absolute motion only inside the regions the compositor advertises for the pointer, and it discards a point outside every one of them without a return code, an event or an error — so the move was lost in silence and never reached the ydotool fallback that could have made it. Region offsets areuint32, so no compositor can advertise a region left of or above the origin, while the layout space this project addresses starts atlayout_origin()and goes negative on the same "monitor left of the primary" desktop fixed above: on such a layout the input and capture halves named different pixels, and the pointer went nowhere rather than to the wrong screen. The libei sender now reads the device's regions, sends a covered coordinate unchanged, retries an uncovered one normalised by the layout origin, and refuses what neither covers so_select_inputhands the move to ydotool. A device that advertises no region is unaffected. Verified against a real EIS peer — theeis-verificationjob now runs 20 checks, five of them on this coordinate space. The ydotool path's own origin remains unverified and unchanged; seeProgress.md. -
The Wayland ydotool fallback reported success while sending nothing on Debian and Ubuntu. ydotool 1.0 replaced its entire command line, and every argument this backend builds arrived in that release (
mousemove --absolute,mousemove --wheel, hexclickbitmasks,key CODE:STATE). Debian bookworm, Ubuntu 22.04 and Ubuntu 24.04 all ship 0.1.8 under the nameydotool, and 0.1.8 exits 0 for those arguments while emitting no events at all — including for the ones it rejects withunrecognised option. Since the backend runs ydotool withcheck=True, nothing raised: clicks, keystrokes and cursor moves silently did nothing and every call reported success. AutoControl now classifies the installed ydotool once per process and raisesAutoControlExceptionnaming the fix instead of emitting. Migration: install ydotool 1.0+ (Arch, Fedora and Debian unstable package it; Debian trixie packages none at all), or setJE_AUTOCONTROL_LINUX_DISPLAY_SERVER=x11to drive XWayland. A version the probe does not recognise is allowed through, so this cannot block a future release. The twoydotoolinstall hints no longer suggestapt install ydotool, which is what produced the broken version. -
Four clipboard writers never worked on 64-bit Windows:
set_clipboard_files,set_clipboard_html,set_clipboard_rtfandset_clipboard_csvall raisedOverflowError: int too long to converton every call, and the matching readers failed whenever that format was actually present. Each module declaredrestypebut notargtypes, so ctypes passed the pointer-width memory handle asc_int. The prototypes and the open/alloc/lock dance now live once inje_auto_control/utils/clipboard/win32_clipboard_api.py, on privateWinDLLhandles so the declarations cannot leak into other user32 callers, andrich_clipboard,clipboard_rich_formats,clipboard_filesandclipboard_formatsall go through it.
-
Unicode text entry by key injection:
type_unicode_keys,type_unicode_text,plan_unicode_keys,unicode_keys_supported(commandsAC_type_unicode_keys/AC_type_unicode_text, MCP toolsac_type_unicode_keys/ac_type_unicode_text), on Windows backend primitivespress_unicode/release_unicode/type_unicode_unit. -
Cross-word OCR matching helpers
find_spans/group_lines. -
monitor_layout.grab_logical/logical_virtual_rect/logical_scale/needs_rescale— screen capture in the coordinate space the mouse uses. -
find_image/find_image_multiacceptall_screensandscreen_region. -
AutoControlFlatTemplateException(a subclass ofAutoControlScreenException) for a template with too little variation to locate. -
Accessibility search scoping and matching:
window_titleonlist_accessibility_elements/find_accessibility_element/click_accessibility_element/control_get_state, acontainssubstring mode with exact-name ranking,find_accessibility_elements,accessibility_status,control_get_state, andrank_by_name(commandsAC_a11y_find_all/AC_control_get_state, MCPac_a11y_find_all/ac_control_get_state). The accessibility GUI tab gains a window filter. -
AccessibilityElement.enabled. -
stop_record_timeline(AC_stop_record_timeline,ac_record_stop_timeline): the recording as press and release, wheel movement anddelta_ms, ready forreplay_timeline. -
utils/input_reach:input_desktop_available,input_reaches_system(AC_input_reachable,ac_input_reachable) — whether input this process sends can actually arrive. The second probe presses F13 to find out. -
utils/keyboard_layout:char_table,layout_char_table,vk_to_char,foreground_keyboard_layout— which character each key produces on the active layout, with a US fallback. -
Window management gains the primitives it was missing:
minimize_window_by_title,foreground_window,window_rectandmove_window_by_title(AC_minimize_window,AC_foreground_window,AC_window_rect,AC_move_window;ac_minimize_window,ac_foreground_window,ac_window_rect).list_windowstakestitled_only, andmove_window_by_titlekeeps the window's current size when width/height are omitted. -
utils/url_canonreaches its delivery surfaces:canonicalize_url,normalize_url,urls_equal,build_queryandparse_queryare exported from the facade, withAC_canonicalize_url/AC_normalize_url/AC_urls_equal, the matchingac_*MCP tools, and three Script Builder specs. The module and its tests already existed; only the wiring is new.
-
set_clipboard_imageaccepts PNG bytes or a path to any Pillow-readable image, andget_clipboard_image/set_clipboard_imageare now exported fromje_auto_control.utils.clipboardand the top-level facade, withAC_clipboard_get_image/AC_clipboard_set_imagecommands. They were previously reachable only through MCP and the GUI, notexecute_action. -
Breaking —
close_window_by_title/AC_close_window/ac_close_windownow actually close the window (they postWM_CLOSE). They previously minimised it: the Win32 call underneath is namedCloseWindowbut minimises, and the wrapper inherited both the call and the wrong promise, so every caller asking to close a window silently got a minimise instead. The old behaviour is available unchanged asminimize_window_by_title/AC_minimize_window/ac_minimize_window. -
focus_windowrestores a window that is minimised before bringing it to the front — focusing a minimised window used to do nothing visible. A maximised window is left maximised (SW_RESTOREwould have un-maximised it). -
show_window_by_titleno longer callsSetForegroundWindowafterSW_HIDE; hiding a window and then pulling it forward are contradictory. -
writeno longer raises on a character missing from the virtual-key table where the backend can inject Unicode; it types that character instead. -
find_text_matchesreturns runs of consecutive word boxes, so a target split across boxes now matches. Results are merged boxes covering the whole run (union rectangle, minimum confidence) rather than one box per word. -
find_image/find_image_multisearch every monitor by default and return virtual-desktop coordinates, which are negative when a monitor sits left of or above the primary. Passall_screens=Falsefor the previous primary-only behaviour. -
match_template/match_template_allcapture every monitor and return screen coordinates. A hit found inside aregionpreviously came back in region-local coordinates; it is now offset by the region's origin. Matches against a caller-suppliedhaystackare unchanged (image-local). -
match_template/match_template_allrefuse an almost-single-colour template instead of returning an arbitrary position. -
element_matchesaccepts a friendly role name ("button") as well as the raw"ControlType_50000"the Windows backend reports. -
AccessibilityBackend.list_elementstakeswindow_title; in-tree backends accept it, and the facade only forwards it when set, so an out-of-tree backend keeps working until someone asks for scoping. -
AccessibilityElement.to_dict()gains anenabledkey. -
The Windows recorder captures through one low-level hook (
Win32InputHook) instead of the two listeners.record/stop_recordkeep their behaviour and return shape. -
An unscoped
list_accessibility_elementswalks one top-level window at a time in z-order, node by node, and stops atmax_results, instead of one uninterruptibleFindAllover the whole desktop. Results are therefore ordered front-most window first, and a smallmax_resultsno longer reaches windows further back. -
The UIAutomation object is created from
CUIAutomation8asIUIAutomation2with a boundedConnectionTimeoutwhere available, so an application that never answers UIA can no longer stall a search for a minute. Falls back toCUIAutomation/IUIAutomationotherwise. -
find_accessibility_elements/AC_a11y_find_all/ac_a11y_find_all:max_resultsnow caps the matches returned (default 50) and the newscan_limitcaps how many elements are examined (default 1500). Callers that passedmax_resultsexpecting a scan bound should passscan_limit.
-
Breaking —
je_auto_control.windows.listeneris gone, with itsWin32KeyboardListenerandWin32MouseListenerclasses. Recording moved towindows/record/win32_input_hook.py, after which nothing in the package or the test suite referenced them. -
Breaking —
je_auto_control.utils.clipboard.clipboard_imageis gone. Its two functions were duplicates of the ones inje_auto_control.utils.clipboard.clipboard, under identical names but with a differentset_clipboard_imagesignature, so importing the wrong module failed at runtime and only for one of the two argument types. Import fromje_auto_control.utils.clipboard(or the top-level facade) instead; the surviving function accepts both PNG bytes and a file path.
-
writefailing a whole string on the first character outside the 192-entry virtual-key table — on a US layout that includes, . / : ? ! _ + @ %and every CJK character, so URLs and non-English text could not be typed at all. -
OCR locating text that the engine split across word boxes (
Save As,另存新檔), which previously reported "not found" for text plainly on screen. -
Template matching never finding a target on a second monitor, and returning coordinates offset by the physical-vs-logical pixel difference on a mixed-DPI desktop (measured ~116 px) and by the virtual-desktop origin.
-
Template images failing to load from a path containing non-ASCII characters (
cv2.imreadreturnsNonethere, which surfaced as "could not read image"). -
list_windowshanding backLP_c_longpointer objects instead of integer hwnds, soint(hwnd)raisedValueErrorand a listed window could not be used in any follow-up Win32 call. TheEnumWindowscallback declared its hwnd asPOINTER(c_int); it is nowHWND, and every Win32 prototype inwindows_window_managedeclaresargtypes/restypeso a 64-bit handle is not truncated to 32 bits. This also un-breaks theac_list_windowsMCP tool, whose handler calledint(hwnd). -
Accessibility listing truncating to
max_resultsbefore filtering, so an element past the cap could never be found however specific the filter. -
control_get_valuereturning a password field's value when a custom-drawn control puts plaintext in ValuePattern instead of masking it. -
The recorder leaking one thread per session: its listener pumped
GetMessageonce andstop_recordnever woke it, so the thread stayed blocked forever.
No compatibility changes.
-
Stable, headless
je_auto_control.apifaçade. -
Portable
autocontrol.failure-bundle/v1diagnostic archives and CLI command. -
Public API lifecycle, capability matrix, security policy, coverage and type checking configuration.
-
Releases are prepared from version tags and use PyPI Trusted Publishing.
-
The USB/IP server binds
127.0.0.1by default (least-privilege). Exporting the attached device to the LAN now requires an explicithost="0.0.0.0".
- New integrations should avoid the eager, historical top-level import surface
and import stable entry points from
je_auto_control.api.
-
macOS cursor position and omitted-coordinate clicks on Retina / HiDPI displays (pixel-vs-point display-height mismatch).
-
Remote-desktop relay hang on Linux + CPython 3.14 when one paired peer disconnected (a cross-thread
shutdown()no longer wakes a blockedrecv()). -
AC_expect_pollcrashing on a not-ready value instead of continuing to poll;AC_parallelbranch variable-scope isolation; malformedrun_suitespecs now report a clean error instead of aborting. -
Windows Interception backend send-to-window click silently no-opping.
-
Wayland partial-coordinate
mouse_scrollraising instead of degrading. -
Action-file save now raises
AutoControlJsonActionException(not a rawUnicodeEncodeError) on non-encodable text; non-ASCII USB/IP busid no longer kills the client thread; SQLite connections are closed; USB ACL removal is case-insensitive.