Skip to main content

Lua v4 GUI Evidence and Actions

Lua v4 exposes GUI state as immutable evidence and permits one guarded, irreversible operation at a time. Lua decides which observed slot is meaningful. Hexis revalidates the screen and selected slot evidence on the client thread before sending anything.

The source foundation implements hexis.gui.snapshot, hexis.gui.transact, and filtered gui.changed subscriptions. They remain non-executable in these docs until a named distributed build passes controlled live acceptance.

Observe one supported screen

hexis.gui.snapshot({}) requires gui.read. Its request must be empty. It returns one of:

{status = "closed", observed_tick = 1042}
{status = "unsupported", observed_tick = 1042}
{
status = "open",
observed_tick = 1042,
screen = screen_snapshot,
}

closed means there is no open container screen. unsupported means a container screen is open but its top container is not a rectangular 1-through-6 row inventory. An open result contains only the top container's 9 through 54 slots, never the player's 36 inventory slots:

screen_snapshot = {
generation = 7,
revision = 18,
title = "Chronomatron (3)",
rows = 6,
slot_count = 54,
slots = {
{
index = 0,
row = 0,
column = 0,
ref = {
screen_generation = 7,
observed_revision = 18,
index = 0,
fingerprint = "<64 lowercase hexadecimal characters>",
},
item = {
empty = false,
id = "minecraft:red_wool",
name = "Red",
count = 1,
foil = true,
lore = {"Remember this tile"},
lore_truncated = false,
},
},
},
}

Indexes, rows, and columns are zero-based. The title and item name are at most 256 characters; item IDs are at most 128 characters. A slot exposes at most 16 lore lines of at most 256 characters each. lore_truncated reports omitted lines. An empty slot has ID minecraft:air, an empty name, and count zero.

The slot reference is opaque compare-and-submit evidence, not permission to click that index forever. A screen generation identifies one exact container menu instance. Its revision starts at 1 and advances for every observed title or slot fingerprint change. Opening a replacement menu creates a new generation. Screen generation is separate from the event envelope's world generation.

Submit one guarded operation

hexis.gui.transact requires gui.interact. An activate request is:

local result = hexis.await(hexis.gui.transact({
screen = {
generation = screen.generation,
revision = screen.revision,
},
operation = {
kind = "activate",
target = screen.slots[13].ref,
guards = {screen.slots[50].ref},
},
}), {timeout = 2})

screen.generation and screen.revision are positive integers. target has exactly the four fields returned in a slot reference. guards is optional and contains at most eight slot references. Target and guard indexes must be unique, and every index is from 0 through 53.

On one client-thread turn, Hexis requires the same screen generation, the target and every guard to still exist, and each fingerprint to still match. The current screen revision may be newer than the request: unrelated slot changes do not invalidate an activate when all named evidence is unchanged. Immediately before the irreversible Minecraft call, the action atomically claims ownership of its terminal result. Hexis then sends exactly one vanilla left-button PICKUP click for the target. There is no public button, click type, shift-click, drag, arbitrary packet, repeat count, or batch operation. The source foundation spaces accepted click submissions by at least 50 milliseconds.

Closing is the only other operation:

hexis.gui.transact({
screen = {generation = screen.generation, revision = screen.revision},
operation = {kind = "close"},
})

Unlike activate, close requires both the same generation and the exact current revision. It accepts no target or guards and makes the same terminal ownership claim immediately before closing the container.

This creates one deterministic boundary for cancellation and an irreversible side effect. Cancellation or lifecycle invalidation wins if it claims terminal ownership first, so the click or close is not submitted. If the GUI action claims first, cancellation cannot replace its outcome: the submitted result wins and is published after cleanup. There is no state where Lua receives cancelled while the action knowingly proceeds with a later click.

The action atomically owns INVENTORY_SCREEN, MOVEMENT, CAMERA, ATTACK_USE, and HOTBAR. Its action-specific data is:

{
operation = "activate", -- or "close"
applied = true,
submitted = true,
screen_generation = 7,
basis_revision = 18,
validated_revision = 19, -- present after validating an open screen
slot = 12, -- present for activate outcomes after screen validation
}

applied and submitted are both true only after the one operation was sent. If the mechanism throws after claiming the irreversible operation but before it can report whether application completed, Hexis returns non-retryable internal_error with this narrower data instead:

{
operation = "activate", -- or "close"
screen_generation = 7,
basis_revision = 18,
application_uncertain = true,
}

Hexis quarantines the shared GUI resources after that indeterminate commit. Lua must not infer that the operation either did or did not apply, and a later GUI action fails closed until the quarantine is cleared by runtime recovery.

The action can return:

CodeRetryableMeaning
okNoOne requested operation was submitted
screen_closedNoNo supported container remained open
screen_changedNoA different screen generation replaced the target
gui_precondition_failedYesThe exact close revision, target, or a guard no longer matched
busyYesOne of the five GUI control resources was unavailable
cancelledNoCancellation won before the irreversible commit claim, so no operation was submitted
internal_errorNoThe transaction or safe cleanup failed; a post-claim exception reports application_uncertain = true and quarantines GUI resources

Shared lifecycle invalidation can instead return world_changed or authority_lost. Malformed tables, unknown fields, or a missing declared capability are catchable Lua errors before dispatch. Lua must observe again before considering any retry.

Subscribe from an exact revision

gui.changed shares the opaque subscription, events.next, hexis.select, unsubscribe, timeout, and run-cleanup model with filtered chat. It requires the events.gui.filtered capability:

local changes = hexis.events.subscribe({
type = "gui.changed",
filter = {
screen = {generation = screen.generation},
after_revision = screen.revision,
slots = {12, 49}, -- optional
},
})

The request has exactly type and filter. The filter requires one positive screen generation and positive after_revision. Optional slots contains 1 through 54 unique indexes from 0 through 53. Omitting it admits changes from every top-container slot. Lifecycle and title events have no slot and are delivered even when slots is present.

The returned subscription is opaque, reusable, and owned by the current run. A run may hold at most 16 active subscriptions total across GUI and chat. A subscription created for a screen generation that is no longer current receives a synthetic closed GUI record instead of attaching to another screen.

Subscription uses the tracker's synchronized boundary for atomic cursor replay and listener admission. That synchronized call may run on the Lua worker: retained changes newer than after_revision are queued before the listener is admitted for future delivery. Later screen mutations originate on the client thread and enter only the subscription's thread-safe queue. Lua still consumes them on its single owner lane; the client thread never invokes Lua. The tracker retains at most 256 ordered mutations for the current screen. A stale cursor produces resync_required rather than pretending the missed delta history is complete.

Each envelope has the shared event fields and GUI-specific data:

{
type = "gui.changed",
sequence = 42,
observed_tick = 1043,
observed_at = {monotonic_seconds = 92.125},
world_generation = 3,
source = "minecraft_client",
dropped_before = 0,
data = {
kind = "slot_changed",
screen_generation = 7,
previous_revision = 18,
revision = 19,
slot = 12,
before = old_slot_snapshot,
after = new_slot_snapshot,
},
}

kind is exactly slot_changed, title_changed, closed, replaced, or resync_required. A title change adds title. A slot change adds slot, before, and after; their references use the previous and new revisions. The three terminal or resync records contain only the common data fields.

Each GUI subscription queues at most 64 records and shares the 128-record run limit with chat. Producers never block the Minecraft client. Overflow drops old evidence, sets dropped_before, and turns the next deliverable GUI record into resync_required. A script must not continue from partial deltas after that result. It must take a fresh snapshot and subscribe from that snapshot's generation and revision, or stop. The two reduced acceptance bundles stop.

Reduced official workloads

The embedded catalog contains two reduced acceptance bundles. Experiment Table has strict config schema version 2; Melody keeps an empty schema:

BundleSource evidenceExplicit exclusions
hexis:experiment-table-reduced-acceptanceReacts to an enabled, already-open Chronomatron, Ultrasequencer, or Superpairs screen using revisioned snapshots, mutation events, target fingerprints, and a control-slot guard where timing state mattersOpening the table, selecting experiments, claiming rewards, auto_renew, currency or resource spending, and full relaunch parity
hexis:melody-reduced-acceptanceReacts to quartz-note changes on an already-open 54-slot Harp, derives repeated presses from the wool stack above a note, and resnapshots after every separately guarded clickOpening the Harp, song selection, navigation, recovery, latency compensation, and any perfect-score claim

Experiment Table stops on close, replacement, stale-journal resync, event closure, or timeout. Its current policy records Chronomatron foil transitions, captures Ultrasequencer order from item counts, and remembers Superpairs cards from item name and lore. When a revealed card matches remembered evidence, it now selects the earlier mate slot instead of re-clicking the currently revealed slot.

Its config schema version 2 contains exactly three required booleans: solve_chrono, solve_ultra, and solve_superpairs. All three default to true. At run start the script takes one immutable config snapshot; if the already-open screen's corresponding solver is false, it exits before creating the GUI subscription or submitting a transaction. There is no auto_renew field. The bundle does not invoke any purchase or renewal flow, and all currency spending remains outside its contract.

Melody races screen mutations against a narrow score-chat subscription and stops when either the screen becomes unusable or a matching % message arrives. A stack can lead to multiple clicks, but each is a separate gui.transact action followed by a fresh snapshot. There is no unsafe or batched click escape hatch. Correct perfect-score behavior remains unproven.

Both workload packs are strict schema 1 data, not general GUI automation configuration. Source tests prove parsing, ordered event composition, and one submitted target in a headless harness. They do not establish real server timing or menu correctness.

Live acceptance gates

Before these calls or bundles become public and AI-runnable, a named distributed build must verify supported and unsupported containers, slot and title mutations, replacement and closure, cursor replay, journal and queue resync, stale target and guard rejection, unrelated-revision tolerance, contention, cancellation on both sides of the irreversible commit boundary, post-commit uncertainty and quarantine, and cleanup on a real client.

Experiment Table additionally needs controlled live runs of all three games with each solver enabled and disabled, across rounds, and without accidental reward, renewal, or currency clicks. Melody needs measured note-to-click latency, stacked-note behavior, score termination, lag and tick-rate variation, and song completion. Until that evidence exists, the bundle is a reduced reaction test, not a perfect-score product.