enhancement
major
minor
defect
critical
major
minor
major
#30200
Agent Access and Recording: Commands operate on stable object IDs; technical keys are used only for recording and playback
Context
Agent access (tl-layout-view-agent, #30048) and the recording/playback of user interactions in the React UI are based on the same headless projection of the controls (ScriptingTreeProjector, ScriptingSession). Objects (table rows, tree nodes, selection options, layout elements, Kanban cards, dragged objects) were identified within this projection using a domain-specific key: the JSON of a ModelName. Commands such as selectByKey, selectByKeys, activateByKey, expandByKey, selectItemByKey, selectCardByKey, and dropObjects accepted these keys. The browser, on the other hand, used its own session-specific IDs and other commands.
Observed
In one application (E-Home, 8.0.0-alpha12.817, running behind Apache 2.4 as a reverse proxy), a GET request to /agent-api/observe returned 84 MB of JSON for 175 controls in a window containing a table (~20 rows), a tree, and a form with 13 selection fields. Almost all of this was the ` argsSchema ` of the commands with key arguments, embedded again for each control:
| Control / Command | Number | Size per argsSchema |
| dropdownSelect / selectByKey | 13 | 2,922,465 bytes |
| tableView / selectByKey | 2 | 2,922,432 bytes |
| tableView / activateByKey | 2 | 2,922,448 bytes |
| tableView / dropObjects | 2 | 2,924,585 bytes |
| treeView / dropObjects | 1 | 2,924,585 bytes |
The schema was the JSON schema of a ModelName with all subtypes—essentially the entire configuration type system. A second call to /observe in the same window was answered by the proxy after ~3 s with a 502 error ("Error reading from remote server"): The response was fully constructed as a string in memory before being written, and an error (e.g., OutOfMemoryError) caused the connection to be terminated without a status code. (The compact view is denoted by mode=actions; the parameter actions=true was ignored.)
The cause was that the agent adopted the object name from the playback layer: An agent operates within a running session and only needs a short ID, whereas playback in a new session requires the session-independent ModelName. Furthermore, the browser’s IDs were not object identifiers: the table addressed rows by their position (rowIndex), the tree used identityHashCode (which is not unique), and the selection assigned new IDs each time the options were loaded.
Solution
- Stable object ID per control. ObjectIdIndex assigns an ID to each displayed object that remains stable as long as the object is displayed and resolves in both directions. The table, tree, selection, layout, and Kanban use it. The table addresses rows in the client log using the row ID instead of the position; a click after reordering therefore always hits the clicked row.
- Commands operate solely on IDs. The agent, browser, and playback use the same command vocabulary. Arguments that carry an object ID are marked with @DisplayedObjectId (in the case of drag-and-drop, with a reference to the source control). The key commands selectByKey, selectByKeys, activateByKey, expandByKey, collapseByKey, contextMenuByKey, selectItemByKey, selectCardByKey, and dropObjects are omitted; valueChanged for the selection is a typed command (SelectValueArguments).
- Identity service per control (ObjectIdentities): `keyOf(id) ` returns the business key of a displayed object, and `idOfKey(key) ` returns the ID. Resolution occurs only among the objects currently displayed by the control (tree: visible nodes; table: rows of the current view, including those outside the visible area but not in collapsed groups; selection: the options of its model; layout/Kanban: displayed elements, cards, and columns). Nothing is expanded or scrolled; expanding is a separate, recorded step. An object displayed in multiple locations receives a unique ID for each location; its key resolves to the first displayed location.
- Recording and playback are handled generically. During recording, selected IDs are replaced by the key, and a source control is replaced by its address; during playback, they are resolved to the current IDs at the respective control, after which the normal command executes. A non-displayed object causes the step to fail with the message “The object is not displayed.” Multiple selections using Ctrl/Shift are recorded click by click, along with a drop on the row/node and the zone where it was made.
- Two projection modes (ProjectionMode): The agent’s observation contains IDs and labels; confirmations are captured and verified in key mode and are therefore valid across sessions. The Kanban board projects all displayed cards by column.
- Agent API. Each argument schema appears once per observation in a `schemas` table; actions reference it via `$ref`; this also applies to commands with `@ReactParam` parameters. Responses are written directly into the response as compact JSON (with`pretty=true ` applied). The observation is fully constructed before the first byte is written; any error that occurs during this process—including an "Error"—is logged and responded to with a 500 status code and a JSON error message. An unknown value for `mode` is rejected with a 400 status code. Measured in tl-demo-react (object list, ticket in progress, 497 nodes): 311 KB (previously 3.9 MB at the same stage without key commands; the report above: 84 MB for 175 controls).
- Access tokens with an `_` in the ID. The ID of an access token consists of Base64url characters, which also include the separator `_`. A token whose ID contained a `_` (roughly one in six) was rejected with a 401 "No session." error because the ID was read up to the first `_`. The ID is now read based on its fixed length; tokens that have already been issued work with this change.
- Tab key in editable tables. Pressing Tab from the last field of the current row selects the next row and places the focus in its first field; Shift+Tab does the same for the last control of the previous row, even at the edge of the loaded section. Read-only checkboxes (aria-readonly) and invisible input fields do not count as editable.
There are no saved recordings containing the omitted commands (recordings are stored only in memory).
Migration
- Custom React controls (subclasses of ReactControl) that override scriptingScalarState() should instead override scriptingScalarState(ProjectionMode mode); the version without parameters is final. Implementations of ScriptingNode.scriptingState() become scriptingState(ProjectionMode mode). An object is added to the projection using ScriptingModelKey.putIdentity(projection, mode, objectIdentities(), ...) rather than via its key.
- A custom control whose commands address objects stores their IDs in an ` ObjectIdIndex`, returns it via `objectIdentities()`, and marks the ID arguments of its typed argument interfaces with `@DisplayedObjectId` (lists additionally with `@JsonBinding(SimpleJsonListBinding.class)`). A ` recordCommand` override that merely translates IDs into keys is no longer needed.
- Deprecated types: ModelKeyArguments (the constant KEY is now in ScriptingModelKey), SelectByKeysArguments, SelectByNameArguments, DropObjectsArguments, and the *ByKeyArguments from tables and trees.
- Agents and tools on `/agent-api`: The key commands no longer exist. Objects are addressed using the IDs from the observation (key/`nodeId`/`rowId`/`value`; see the respective argument schema); the table uses `rowId` instead of `rowIndex`, and `drop` expects `keys` as a JSON array. Argument schemas are listed under `schemas ` and are referenced via `$ref`; there is no longer a `params` field. The compact view is `mode=actions`, and `pretty=true ` returns indented output.