xmemcli 1.4.0__tar.gz → 1.7.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: xmemcli
3
- Version: 1.4.0
3
+ Version: 1.7.0
4
4
  Summary: Command-line client for the xmemory HTTP API: PKCE auth, instances, read/write, schema and XMD tools.
5
5
  Keywords: api,cli,llm,memory,xmemory
6
6
  Requires-Python: <4,>=3.12
@@ -20,6 +20,8 @@ xmemcli auth logout
20
20
  xmemcli auth status
21
21
  xmemcli read <query>
22
22
  xmemcli write <text>
23
+ xmemcli write (--mutations-file PATH | --mutations-stdin) [--delete TYPE:ID]…
24
+ xmemcli write --delete TYPE:ID [--delete TYPE:ID]…
23
25
  xmemcli write-status <write_id> [<id> …]
24
26
  xmemcli quota
25
27
  xmemcli instance templates
@@ -29,8 +31,10 @@ xmemcli binding add <instance_id> [--tier autoload|available|off] [--scope proje
29
31
  xmemcli binding remove <instance_id> [--scope project|user]
30
32
  xmemcli context [--text] [--max-tokens N] [--no-live-state]
31
33
  xmemcli trace ls [--instance ID]… [--trace-type T]… [--hours-back N] [--user-id U] [--api-key-id K] [--limit N] [--cursor TS_MS:TRACE_ID] [--counts-only]
32
- xmemcli trace ls --search [--text …] [--request …] [--response …] [--object NAME]… [--field NAME]… [--status S]… [--trace-id ID]… [--session-id S] [--instance ID] [--trace-type T|delete]… [--hours-back N] [--limit N] [--cursor TS_MS:TRACE_ID:INSTANCE_ID]
34
+ xmemcli trace ls --search [--text …] [--request …] [--response …] [--object NAME]… [--field NAME]… [--status S]… [--trace-id ID]… [--session-id S] [--instance ID] [--trace-type T|delete]… [--hours-back N] [--include-llm] [--limit N] [--cursor TS_MS:TRACE_ID:INSTANCE_ID]
33
35
  xmemcli trace get <trace_id> [--trace-type write|read|create|update|evolution]
36
+ xmemcli trace calls --session-id S [--stage S] [--model M] [--outcome succeeded|failed|refused] [--hours-back N] [--limit N] [--cursor TS_MS:TRACE_ID:INSTANCE_ID]
37
+ xmemcli trace call <llm_call_id>
34
38
  ```
35
39
 
36
40
  Global flags / env: `--api-key` / `$XMEM_API_KEY`, `--instance-id` / `$XMEM_INSTANCE_ID`, `--api-url` / `$XMEM_API_URL`, `--rc-dir` / `$XMEM_RC_DIR`, `--binding-dir` / `$XMEM_BINDING_DIR`, `--json`, `--verbose`. See `xmemcli help options`. `trace …` additionally take `--console-url` / `$XMEM_CONSOLE_URL` (the Console origin that serves them; inferred for the public and local pairs).
@@ -48,6 +52,9 @@ Two local files, same discovery rule, different jobs: `.xmemrc.json` holds the c
48
52
  | `xmemcli write --extraction fast` (default) | `extraction_logic: "fast"` |
49
53
  | `xmemcli write --extraction deep` | `extraction_logic: "deep"` |
50
54
  | `xmemcli write --no-wait` | async write; poll with `write-status` |
55
+ | `xmemcli write --mutations-file m.json` / `--mutations-stdin` | `structured_mutations` — the JSON array as given; no `text`, no `extraction_logic` |
56
+ | `xmemcli write --scope Pet:name=Luna --scope-mode drop` | `scope: {objects: [...], mode: "drop"}` — out-of-scope changes are skipped and reported; `reject` is not sent |
57
+ | `xmemcli write --delete Person:name=Ada` | appends `{"object_mutation": {"object_type": "Person", "delete": {"key": {"name": "Ada"}}}}` (`Person:<xuid>` sends `{"xuid": …}` as the key) |
51
58
  | `xmemcli read --read-mode single` (default) | `mode: "single-answer"` |
52
59
  | `xmemcli read --read-mode raw` | `mode: "raw-tables"` |
53
60
  | `xmemcli read --read-mode xresponse` | `mode: "xresponse"` |
@@ -56,9 +63,13 @@ Two local files, same discovery rule, different jobs: `.xmemrc.json` holds the c
56
63
 
57
64
  `--read-mode` selects the **server-side answer shape**; `--json` and the TTY rule govern stdout format.
58
65
 
66
+ ## Terminal colors
67
+
68
+ Interactive help and trace JSON use a dark-terminal palette by default. When `COLORFGBG` reports a white background (`7` or `15`), xmemcli uses darker colors for light terminals. Set `XMEMCLI_THEME=light` or `XMEMCLI_THEME=dark` to choose explicitly when the terminal does not report its background. `NO_COLOR` still disables colors.
69
+
59
70
  ## Curated output
60
71
 
61
- The CLI does not echo raw API payloads. Server internals (`sql`, `diff_plan`, extraction trees) are never emitted. Read responses keep `pending_suggestions` (schema-evolution hint). `--verbose` adds `trace_id` and `console_url` when present.
72
+ The CLI does not echo raw API payloads. Server internals (`sql`, `diff_plan`, extraction trees) are never emitted. Read responses keep `pending_suggestions` (schema-evolution hint). A text write keeps a compact `extracted` summary (`Type(identifier)` tags); a structured write runs no extraction and has none. `write` and `write-status` keep the server's `changes` block (created / updated with old → new / deleted, bulk summaries, advisory records) when the write changed something. `--verbose` adds `trace_id` and `console_url` when present.
62
73
 
63
74
  **`trace …` is the one deliberate exception.** Those commands return the Console's own operation documents — the paged operations list, the Inspect search, and the per-operation detail — **verbatim**, because the Console already curates them per caller (a super admin sees LLM calls and raw token counts; everyone else does not). They are JSON on every stream and carry every internal id the Console does: `trace_id`, `instance_id`, `session_id`, `user_id`, `api_key_id`, and on detail documents `source_id`, `migration_id`, `llm_call_id`, `gateway_request_id`, …
64
75
 
@@ -72,14 +83,18 @@ xmemcli trace ls --instance "$ID" --trace-type write --trace-type read --limit 5
72
83
  xmemcli trace ls --counts-only --hours-back 168 # counts, per-instance counts and series, no rows
73
84
  xmemcli trace ls --search --text "Ada" --instance "$ID" # Inspect search — opt-in, more expensive
74
85
  xmemcli trace get 0001_abc --trace-type write # one operation, every internal id
86
+ xmemcli trace calls --session-id "$SESSION" --stage 'benchmark.*' # a session's LLM calls (super admins)
87
+ xmemcli trace call "$LLM_CALL_ID" # one LLM call whole: prompt, reply, stats
75
88
  ```
76
89
 
77
90
  Paging is a cursor the previous page returns:
78
91
 
79
92
  ```bash
80
93
  PAGE=$(xmemcli trace ls --instance "$ID" --limit 100)
81
- NEXT=$(echo "$PAGE" | jq -r '"\(.next_before_ts_ms):\(.next_before_trace_id)"')
82
- xmemcli trace ls --instance "$ID" --limit 100 --cursor "$NEXT"
94
+ # `select(.has_more_operations)` so the last page yields an empty string rather than the literal `null:null`,
95
+ # which `--cursor` refuses.
96
+ NEXT=$(echo "$PAGE" | jq -r 'select(.has_more_operations) | "\(.next_before_ts_ms):\(.next_before_trace_id)"')
97
+ [ -n "$NEXT" ] && xmemcli trace ls --instance "$ID" --limit 100 --cursor "$NEXT"
83
98
  ```
84
99
 
85
100
  `--search` pages too, with the Inspect endpoint's three cursor fields — follow `has_more`, since a page can come back short:
@@ -92,6 +107,16 @@ NEXT=$(echo "$PAGE" | jq -r 'select(.has_more) | "\(.next_before_sort_at_ms):\(.
92
107
  [ -n "$NEXT" ] && xmemcli trace ls --search --text "Ada" --cursor "$NEXT"
93
108
  ```
94
109
 
110
+ `trace calls` pages the same way, with the same three cursor fields:
111
+
112
+ ```bash
113
+ PAGE=$(xmemcli trace calls --session-id "$SESSION")
114
+ NEXT=$(echo "$PAGE" | jq -r 'select(.has_more) | "\(.next_before_sort_at_ms):\(.next_before_trace_id):\(.next_before_instance_id)"')
115
+ [ -n "$NEXT" ] && xmemcli trace calls --session-id "$SESSION" --cursor "$NEXT"
116
+ ```
117
+
118
+ `--include-llm` adds model-call hits to a free-text search (`--text`, optionally with `--session-id` or `--hours-back`), for a super admin's key only; any other key gets the operations it gets without the flag and no calls. The CLI refuses the flag with a clause flag or an instance, user or key scope, where the Console's search finds no model call. When a page ends on one, its `llm_call_id` is the cursor's third field, so repeat the flag on every page of that walk.
119
+
95
120
  A search with no filter shaping it is the Console's recent-activity view, which reaches back 180 days; when older operations exist, its last page says `truncated` and the CLI warns. A key sees the operations of the instances it can list — never more than its owner sees in the Console; a super admin's key sees what a super admin sees. Full reference: [](#trace--the-consoles-operation-traces).
96
121
 
97
122
  ## Output personality — JSON vs pretty
@@ -138,7 +163,7 @@ xmemcli version
138
163
  xmemcli auth login
139
164
  ```
140
165
 
141
- Version: **1.4.0**.
166
+ Version: **1.7.0**.
142
167
 
143
168
 
144
169
  ---
@@ -238,7 +263,7 @@ Each login binds its own port and names it in the login URL, so concurrent login
238
263
 
239
264
  # `xmemcli` — capabilities reference
240
265
 
241
- **Version:** `1.4.0` (see `xmemcli version`)
266
+ **Version:** `1.7.0` (see `xmemcli version`)
242
267
 
243
268
  `xmemcli` is the command-line client for the **xmemory HTTP API**. It is **human-approved for key acquisition**: an AI-assisted IDE may run reads, writes, and inventory, and may initiate `auth login`, but a key is only issued after one explicit human approval — a click in the Console UI (PKCE handoff) or an approval of a sign-in email (`--email`). Quota limits change only in the **Console UI**. The full acquisition contract is in the auth-flow reference bundled with this package.
244
269
 
@@ -256,12 +281,23 @@ Every command below has dedicated help — see [Help](#help).
256
281
 
257
282
  | Command | Description |
258
283
  |---------|-------------|
259
- | **`write`** `<text>` | Sync write (`POST …/write`). `--extraction fast\|deep` (default `fast`). `--no-wait` queues async write, returns `write_id`. `--scope TYPE:ID` (repeatable) anchors the write to existing objects: only they may be modified/deleted, new objects and links to them are allowed, anything else fails; works with either extraction mode. |
260
- | **`write-status`** `<write_id>` … | Poll async writes until terminal or `--timeout` (default 60s). Accepts multiple ids. |
284
+ | **`write`** `[text]` | Sync write (`POST …/write`). `--extraction fast\|deep` (default `fast`). `--no-wait` queues async write, returns `write_id`. `--scope TYPE:ID` (repeatable) anchors the write to existing objects: only they may be modified/deleted; by default new objects and links to them are allowed and anything else fails, and under `--scope-mode drop` only the objects a `--scope` names may be created and everything else is skipped; works with either extraction mode. `--scope-mode drop` (needs `--scope`) skips out-of-scope changes instead of failing and lists them under `changes.skipped_out_of_scope` (a server change, not a CLI one: past 100 entries the server folds the rest into lines shown with `folded=true` and no key or fields); it creates only records a `--scope` names by primary key, and such a record need not be stored yet — create-or-update exactly the named record. `reject` is the server default and is not sent. Instead of text, a **structured write** sends `structured_mutations` — deterministic creates, updates and deletes with no extraction: `--mutations-file PATH` or `--mutations-stdin` (one JSON array, same shape as the API's `structured_mutations`; see [Structured writes](#structured-writes)) and/or `--delete TYPE:ID` (repeatable, `--scope` syntax: `Person:<xuid>` or `Person:name=Ada`; appended after the file's entries). Text and mutations are exclusive, as are the file and stdin forms; mutations refuse `--scope` and `--extraction deep`; empty or whitespace-only text is refused (exit **2**, before anything is read or sent). A text write's output lists what it extracted as `Type(identifier)` tags (`extracted`); a structured write's does not. Both carry the server's `changes` block (`created` / `updated` with per-field `old_value` → `new_value` / `deleted`, and any other bucket such as `created_keyless_objects`) whenever the write changed something, and on a drop-mode write that skipped everything, where the block carries `skipped_out_of_scope` alone. A `--no-wait` answer carries only the `write_id`; `write-status` reports the `changes` once the write completes. |
285
+ | **`write-status`** `<write_id>` … | Poll async writes until terminal or `--timeout` (default 60s). Accepts multiple ids. A completed write's non-empty `changes` block is kept, as on `write`. |
261
286
  | **`read`** `<query>` | Read from active instance. `--read-mode single\|raw\|xresponse`, `--scope TYPE:ID` (repeatable), `--scope-relations`, `--related-types` (also return `related_types`: per object type the read touched, the fields it did not return and the object types a declared relation links it to; needs `instance.get_own` on the key), `--related-types-depth N` (1, 2 or 3, refused as a usage error otherwise; follow the relations N levels out, each catalog entry saying its distance; implies `--related-types`). |
262
287
 
263
288
  Requires **`--instance-id`** or **`$XMEM_INSTANCE_ID`** for read/write (or auto-pick when exactly one instance is visible — see [Global flags](#global-flags--environment)).
264
289
 
290
+ #### Structured writes
291
+
292
+ `--mutations-file PATH` / `--mutations-stdin` send one JSON array as the API's `structured_mutations`, as given. The server takes at most **100** entries per write, `--delete` entries included; the CLI refuses more with exit **2** before sending anything. `--mutations-stdin` needs a pipe or a file on stdin and refuses a terminal. Each entry is one of:
293
+
294
+ - **`object_mutation`** — `{"object_mutation": {"object_type": "Person", "create": {"key": {"name": "Ada"}, "values": {"age": 36}}}}`. `update` takes the same `key` + `values` (the key may be `{"xuid": "…"}`; a null value clears a field); `delete` takes `{"key": {…}}` alone.
295
+ - **`relation_mutation`** — `{"relation_mutation": {"relation_type": "works_at", "create": {"endpoints": [{"object_name": "person", "key": {"name": "Ada"}}, {"object_name": "company", "key": {"name": "ACME"}}]}}}`. `create` lists every participant role, each keyed by primary key or `{"xuid": "…"}`, with no `key` and no `values`. `delete` takes either `endpoints` (a subset may be given) or a top-level `"key": {"xuid": "…"}` — never both — and `"allow_bulk_delete": true` to remove every matched link (without it, a delete matching more than one link is refused). Relation `update` is always refused: re-link by deleting and creating.
296
+
297
+ `--delete TYPE:ID` appends the object `delete` above; each key field may appear once and none may be empty. A delete that matches no record is a no-op, not an error: exit **0** and no `deleted` line — in a terminal the write reads `write <id> ok · nothing matched or changed`.
298
+
299
+ Every current server accepts structured writes; an older one that had them switched off (`execution.structured_writes_enabled`, since removed) answers **400** `Structured writes are not enabled on this deployment.`; a server that predates structured writes answers **422** with two errors, both reported: `Field required (field: text); Extra inputs are not permitted (field: structured_mutations)`. Both exit **3** with `stage: "request"`.
300
+
265
301
  ---
266
302
 
267
303
  ### Org inventory
@@ -461,8 +497,8 @@ so a typo fails loudly rather than being ignored.
461
497
  ### Trace — the Console's operation traces
462
498
 
463
499
  What the Console shows on an instance's **Operations** tab (a paged list), on **Inspect** (a
464
- search across operations), and in each operation's detail pane — fetched from the same Console
465
- endpoints, with this CLI's API key, and emitted **as the Console serves them**. Nothing is curated
500
+ search across operations), and in each operation's detail pane, plus (for super admins) the LLM calls of a
501
+ session, which no Console view lists — fetched from the same Console endpoints, with this CLI's API key, and emitted **as the Console serves them**. Nothing is curated
466
502
  or reshaped: the Console already curates per caller, and a field the Console adds shows up here
467
503
  the same day. These documents are for scripts and agents; on a terminal they are only indented
468
504
  and coloured (`NO_COLOR` / `FORCE_COLOR` honoured). Flags only — there is no query language to
@@ -471,30 +507,53 @@ learn; the CLI composes the Console's search clauses itself.
471
507
  | Command | Console view | Description |
472
508
  |---------|--------------|-------------|
473
509
  | **`trace ls`** | Operations tab / Home | Operations, newest first, across every instance this credential can see (nothing given — the Home view), or the `--instance ID` given (repeatable, or a comma list; default `--instance-id` / `$XMEM_INSTANCE_ID`). Returns `operations[]`, `counts`, `instance_counts`, `series`, and the paging cursor `next_before_ts_ms` + `next_before_trace_id` (with `has_more_operations`). Flags: `--trace-type T` (repeatable / comma; `write`, `read`, `create`, `update`, `evolution` — `delete` is `--search` only), `--hours-back N` (server default 24, `0` = unbounded) or `--start-ts` / `--end-ts` (epoch seconds), `--user-id`, `--api-key-id`, `--include-console-reads`, `--bucket-ms`, `--limit N` (default 100, Console cap 500), `--cursor TS_MS:TRACE_ID` (the previous page's two halves, joined with a colon), `--counts-only` (`ops_limit=0`: what Home fetches — counters and series, no rows; refused together with `--limit` / `--cursor`, which would have nothing to page). |
474
- | **`trace ls --search`** | Inspect | The Inspect search, for the filters only it can answer: `--text` (free text), `--request` / `--response` (text on either side of the operation), `--object NAME` / `--field NAME` (repeatable; all must match), `--status S` (repeatable / comma; any may match: `ok`, `error`, `stale`, `timed_out`, `internal_error`, `quota_exceeded`, `in_progress`, `schema-gap`), `--trace-id ID` (repeatable / comma), `--session-id`; plus `--instance` (at most one — the Inspect scope), `--trace-type` (also `delete`), `--hours-back` (a floor applied to the hits *after* the search — Inspect's own recency windows decide the candidate pool, so this narrows a result, unlike the query window it is on `trace ls`), `--user-id`, `--api-key-id`, `--limit N` (default 100, Console cap 500), `--cursor TS_MS:TRACE_ID:INSTANCE_ID` (the previous page's three cursor fields, joined with colons). Returns the Inspect document: `merged[]` hits with `kind`, `href` (`/write/{trace}`, `/read/{trace}`, `/create/…`, `/update/…`, `/evolution/…`, or `#` for an instance deletion, which has no detail page), `trace_id`, `instance_id`, `session_id`, `user_id`, `api_key_id` / `api_key_name` / `api_key_prefix`, `status`, `time_taken_ms`, `match_items`; and the paging fields `has_more`, `next_before_sort_at_ms`, `next_before_trace_id`, `next_before_instance_id`, `truncated`. |
510
+ | **`trace ls --search`** | Inspect | The Inspect search, for the filters only it can answer: `--text` (free text), `--request` / `--response` (text on either side of the operation), `--object NAME` / `--field NAME` (repeatable; all must match), `--status S` (repeatable / comma; any may match: `ok`, `error`, `stale`, `timed_out`, `internal_error`, `quota_exceeded`, `in_progress`, `schema-gap`), `--trace-id ID` (repeatable / comma), `--session-id`; plus `--instance` (at most one — the Inspect scope), `--trace-type` (also `delete`), `--hours-back` (a floor applied to the hits *after* the search — Inspect's own recency windows decide the candidate pool, so this narrows a result, unlike the query window it is on `trace ls`), `--user-id`, `--api-key-id`, `--include-llm` (1.7.0: also model-call hits, in a free-text search — see below), `--limit N` (default 100, Console cap 500), `--cursor TS_MS:TRACE_ID:INSTANCE_ID` (the previous page's three cursor fields, joined with colons). Returns the Inspect document: `merged[]` hits with `kind`, `href` (`/write/{trace}`, `/read/{trace}`, `/create/…`, `/update/…`, `/evolution/…`, or `#` for an instance deletion, which has no detail page), `trace_id`, `instance_id`, `session_id`, `user_id`, `api_key_id` / `api_key_name` / `api_key_prefix`, `status`, `time_taken_ms`, `match_items`; with `--include-llm`, for a super admin, model-call hits too: `kind: llm_call`, `trace_id` (the operation that made the call), `llm_call_id`, `llm_operation_type`, `href` (that operation's pane, or `#`), and `llm_sub_stage`, `llm_provider` and `session_id` when the call recorded them (the first two from a Console of 2026-10-08 or later); no `instance_id` and no `status`. Open one with `trace call <llm_call_id>`. Then the paging fields `has_more`, `next_before_sort_at_ms`, `next_before_trace_id`, `next_before_instance_id`, `truncated`. |
511
+ | **`trace calls`** | — (no Console view) | The LLM calls of one session, newest first, a page at a time. **Super admins only**: any other credential gets 403 → exit **3**, stage `permission`. `--session-id` (required), `--stage S` (exact, or a prefix with a trailing `*`, e.g. `benchmark.*`), `--model M` (the model slot, or the model that served the call), `--outcome` (`succeeded`, `failed` — an error that is not a refusal — or `refused`), `--hours-back N` (server default 720, i.e. 30 days; `0` = everything kept), `--limit N` (default 100, Console cap 500), `--cursor TS_MS:TRACE_ID:INSTANCE_ID` (the previous page's three cursor fields: the call time, its operation and its call id). Returns `llm_calls[]` — per call `llm_call_id`, `operation_id`, `operation_type`, `instance_id`, `session_id`, `sub_stage`, `model_type`, `model_id`, `provider`, `provider_type`, `called_at` / `called_at_ms`, `time_taken_ms`, token counts, `is_success`, `is_refusal`, `is_cached`, `error_message`, `finish_reason`, `gateway_request_id`, `client_attempts`, `gateway_repairs`, `reissue_of_llm_call_id`, `reissue_reason`, the requested settings, the applied ones (`applied_temperature`, `applied_thinking`, `applied_effort`) and the prompt-cache counts (`cache_read_tokens`, `cache_write_tokens`); never the prompt or the reply (an `error_message` may quote the gateway's detail about a reply) — and the paging fields `has_more`, `next_before_sort_at_ms`, `next_before_trace_id`, `next_before_instance_id`, `truncated`. |
512
+ | **`trace call`** `<llm_call_id>` | — (no Console view) | One LLM call whole, as `{"found": true, "llm_call": {…}}` (so `jq .llm_call.prompt`): the same fields as a `trace calls` row plus `prompt`, `json_schema`, `result`, `raw_result` (the reply as the model wrote it, when the gateway repaired it) and `rejected_attempts`. It does not list the calls that re-issued this one (`trace get` on the operation shows those as `reissued_as_llm_call_ids`). If the call's stats never landed, `llm_call` carries only `llm_call_id`, `operation_id` and those logged fields. **Super admins only**, as above. An unknown id — the Console's `{"found": false}` — is `stage: "not_found"`, exit **3**; so is a listed call whose logged half (prompt and reply) never landed. |
475
513
  | **`trace get`** `<trace_id>` | Detail pane | One operation's full document. `--trace-type` (`operation_type` on a `trace ls` row; the first path segment of a `--search` hit's `href`) makes it one request; without it the five detail endpoints are probed in order `write, read, create, update, evolution` and the first that knows the trace answers. A deletion's `DL0000_aaa` id is answered without a request: it has no detail page. A miss — the Console's `{"found": false}`, or its 404 for a trace outside your scope — is `stage: "not_found"`, exit **3**. |
476
514
 
477
515
  **`--search` is opt-in, on purpose.** The Inspect search runs text predicates across the trace store with a candidate pool of thousands and a merge — markedly more expensive than the indexed, cursor-paged `trace ls`. Passing a search-only flag (`--text`, `--request`, `--response`, `--object`, `--field`, `--status`, `--trace-id`, `--session-id`) without `--search` is a usage error (exit **2**) whose message says exactly that, so an agent reasons about whether it needs the search or whether `trace ls` narrowed by `--instance` / `--trace-type` / `--hours-back` plus `trace get` will do. Conversely `--counts-only`, `--start-ts` / `--end-ts`, `--bucket-ms` are refused with `--search` because the Inspect endpoint cannot honour them, and `--include-console-reads` because Inspect's only spelling of that toggle is a bare phrase inside `q` itself — which is also why a free-text value containing the words "include console reads" is refused rather than sent (the Console would flip the toggle and strip the phrase from the query).
478
516
 
479
517
  **Breaking change in 1.4.0:** `trace ls --search` now asks the Console for one *page* (100 rows by default) instead of the whole capped result, under the Console's paging version 2 (`paging=2`), in which every search mode pages. A single call is no longer the complete answer: loop on `has_more`, and do not read a row count, or an absent `warnings` array, as completeness. Against a Console that predates paging the CLI says so in a warning, because that Console still answers whole and capped. What it tests is the absence of `has_more`, not the rows: a Console that pages sends the paging fields even when it found nothing, and an empty answer from one that does not is exactly where a row count misleads.
480
518
 
481
- **Paging a search — read this before scripting `--search`.** `trace ls --search` pages like `trace ls`, with the Inspect endpoint's own keyset cursor: pass the previous page's `next_before_sort_at_ms`, `next_before_trace_id` and `next_before_instance_id` back as `--cursor TS_MS:TRACE_ID:INSTANCE_ID` until `has_more` is `false`. Follow `has_more`, never the row count: the Console narrows each page after its row limits (type and status resolution, console-read exclusion), so a page can hold fewer rows than `--limit` — even none — and still have more. Paging reaches every match, with one exception the Console reports itself: a search with no filter that shapes the query (only scope such as `--instance`, `--session-id`, `--user-id`, `--api-key-id` or `--hours-back`) is the Console's recent-activity view, which reaches back 180 days. When this credential has older operations than that, the last page carries `truncated: true` and the CLI emits a **warning** (stderr, and folded into the JSON document's `warnings` array) naming the way past it — a filter such as `--trace-type` or `--text`. That flag comes from the endpoint, which knows what it could not reach; the CLI no longer guesses at a cut from how many rows came back. Asking for a page changes two things worth knowing before you script it: a free-text search answers from one candidate window per page rather than the exhaustive second pass the whole-result path ran, so a rare needle can give a short — even empty — first page with `has_more: true` where one call used to find the match; and a paged request is all-or-nothing, so a Console-side source failure is a `502` rather than a partial answer at exit 0. While a Console deploy is rolling out, a `--cursor` request can reach a node that does not page yet; it ignores the cursor, and the CLI exits **3** saying so rather than hand back its newest rows as the next page — retry the same `--cursor`.
519
+ **Paging a search — read this before scripting `--search`.** `trace ls --search` pages like `trace ls`, with the Inspect endpoint's own keyset cursor: pass the previous page's `next_before_sort_at_ms`, `next_before_trace_id` and `next_before_instance_id` back as `--cursor TS_MS:TRACE_ID:INSTANCE_ID` until `has_more` is `false`. A model-call hit has no instance, so when a page ends on one, `next_before_instance_id` is its `llm_call_id`: pass it back as it came, and give `--include-llm` on every page of that walk, since the cursor is a place in the result that flag shapes. Follow `has_more`, never the row count: the Console narrows each page after its row limits (type and status resolution, console-read exclusion), so a page can hold fewer rows than `--limit` — even none — and still have more. Paging reaches every match, with one exception the Console reports itself: a search with no filter that shapes the query (only scope such as `--instance`, `--session-id`, `--user-id`, `--api-key-id` or `--hours-back`) is the Console's recent-activity view, which reaches back 180 days. When this credential has older operations than that, the last page carries `truncated: true` and the CLI emits a **warning** (stderr, and folded into the JSON document's `warnings` array) naming the way past it — a filter such as `--trace-type` or `--text`. That flag comes from the endpoint, which knows what it could not reach; the CLI no longer guesses at a cut from how many rows came back. Asking for a page changes two things worth knowing before you script it: a free-text search answers from one candidate window per page rather than the exhaustive second pass the whole-result path ran, so a rare needle can give a short — even empty — first page with `has_more: true` where one call used to find the match; and a paged request is all-or-nothing, so a Console-side source failure is a `502` rather than a partial answer at exit 0. While a Console deploy is rolling out, a `--cursor` request can reach a node that does not page yet; it ignores the cursor, and the CLI exits **3** saying so rather than hand back its newest rows as the next page — retry the same `--cursor`. The CLI does not ask for the Console's whole-result `totals` (`include_totals`, which the counts and charts on the Console's instance tab and session page draw); to count a search's matches, walk its pages.
520
+
521
+ **Model-call hits (`--include-llm`, 1.7.0).** Opt-in: without the flag a search asks for none, and its request and answer are what they were before 1.7.0. With it, a free-text search (`--text`) also looks through model calls — text in their prompt or error message, or the whole of their call id, operation id, session id or gateway request id — and returns the ones it matches beside the operations, newest first with them, so calls take places on each page. The Console looks through model calls only in a free-text search, and a model call has no instance, user or key to scope it by, so the CLI refuses `--include-llm` without `--text`, with a clause flag (`--trace-type`, `--status`, `--object`, `--field`, `--trace-id`, `--request`, `--response`), and with an instance, user or key scope (`--instance` or a bound `--instance-id` / `$XMEM_INSTANCE_ID`, `--user-id`, `--api-key-id`): each of those would answer with no model call whatever matched. `--session-id` and `--hours-back` work with it. A model call carries its prompt and error, so the Console serves these hits to a caller with no instance restriction only — a super admin's key — and leaves them out for every other credential rather than refuse the search: a member's key gets the same operations it gets without the flag, and no model-call hits. `auth status` says whether this credential is a super admin's. Each hit names its call: `trace call <llm_call_id>` opens it whole, and `trace get <trace_id>` the operation it belongs to.
522
+
523
+ **LLM calls of a session (1.6.0).** The list holds the calls recorded under that session; a call whose session was not recorded with it is not listed, so treat the list as the session's recorded calls, not a guaranteed complete count. The API records each call's session with the call (a session over 256 characters only best-effort, through the Console's lookup by operation); a per-step benchmark call is listed only when its runner sent a session with it, and a schema generation or instance creation call only when its operation was recorded with a session. `trace calls` pages exactly like `trace ls --search`: follow `has_more`, pass the three `next_before_*` fields back as `--cursor`. On the last page the Console sets `truncated: true` when calls matching the filters exist before the `--hours-back` window, and the CLI warns (stderr, and the JSON document's `warnings` array) — widen the window, or pass `0` for everything kept. The older text fields (`sub_stage`, `model_type`, `provider`) read `""` when nothing reported them, as in `trace get`; every newer one (`instance_id`, `model_id`, `provider_type`, `finish_reason`, `reissue_of_llm_call_id`, `reissue_reason`, `schema_property_order`, `applied_temperature`, `applied_thinking`, `applied_effort`) is `null` then, so `select(.reissue_of_llm_call_id != null)` keeps only re-issues, as it does on `trace get`. The counts, times and outcome flags every call has (`time_taken_ms`, the token counts, `is_success`, `is_refusal`, `is_cached`) are always numbers and booleans; a setting or count only some calls report (`max_tokens`, `temperature`, `no_cache`, `client_attempts`, `cache_read_tokens`, `cache_write_tokens`), `error_message` and `gateway_request_id` are `null` when nothing reported them, and so, in `trace call`, are `session_id`, `json_schema`, `result`, `raw_result` (null for every reply the gateway served as written) and `rejected_attempts`. These commands need a Console that serves them: an older one does not accept a key on these paths and answers **401**, which the CLI reports as exit **4** `auth`.
482
524
 
483
525
  **Two document shapes.** `trace ls` returns the Operations-tab document; `trace ls --search` returns the Inspect document (above). They differ because the Console's two backends differ and this CLI does not reshape what the Console serves. Both carry the same identifiers per row (`trace_id`, `instance_id`, `session_id`, `user_id`, `api_key_id`, `status`), which is what `trace get` needs next.
484
526
 
485
- **Every operation row carries its internal ids.** `operations[]` rows have `operation_type`, `trace_id`, `instance_id`, `timestamp`, `status`, `is_success`, `error_code`, `session_id`, `user_id`, `user_email`, `api_key_id`, `api_key_name`, `api_key_prefix`, `time_taken_ms`, `text_snippet`, `object_names`, `field_names`.
527
+ **Every operation row carries its internal ids.** `operations[]` rows have `operation_type`, `trace_id`, `instance_id`, `timestamp`, `status`, `is_success`, `error_code` (super admins only: it is the server's own cause code, finer than the API's error codes), `session_id`, `user_id`, `user_email`, `api_key_id`, `api_key_name`, `api_key_prefix`, `time_taken_ms`, `text_snippet`, `object_names`, `field_names`.
528
+
529
+ > **Three of those never resolve for an API key**, super admin or not, and this is by design rather than a gap. `user_email` is always absent: the accounts `/users` lookup is closed to keys (an agent may learn who *it* is, never enumerate the organisation's users), so rows keep `user_id` and you resolve names yourself if you need them — and a `User:<email>` search clause matches nothing; filter by `user_id`. For that reason `--user-id` refuses a value containing `@` as a usage error (exit **2**) on both `trace ls` and `trace ls --search`, rather than sending a filter that can only ever return an empty result at exit 0. `api_key_name` / `api_key_prefix` resolve only for the caller's **own** keys, because `GET /api-keys` scopes a key caller to its owner's keys. So a super admin's key does *not* see literally everything its owner's browser sees: it sees the same rows and the same fields, with those three enrichments empty. Everything scope-bearing — which instances, which detail fields, `llm_calls`, xuid substitution — is identical. Detail documents add what the Console's pane shows: `source_id`, `write_mode` / `read_mode`, `scope`, `extracted_objects` (an array holding one `{objects, relations}` document, failed writes included; empty when no extracted objects reached the journal: a write that failed before extraction finished or extracted nothing, or one that timed out), `diff_plan` (sanitised unless super admin), `written_objects`, `migration_id`, `proposal_version`, `schema_version`, `stage`, `triggered_evolution {migration_id, evolution_trace_id, update_trace_id}`, `sql_queries` (the rows owned by the read's instance, for every caller who may open the read), and two tiers of gated fields. **Super admin only** — the accounts API decides, for a key as for a browser session (a key counts only when its owner and its creator are both active super admins): `llm_calls[]` (each model call the operation made; fields below), `step_decisions[]` (what each step of the operation decided, per item; below), `full_error_message`, the write's and read's cause code `error_code`, raw token counts, and unsanitised xuids. **Callers with no instance restriction at all** — a super admin, or a browser session on a `KINGDOM_LOCAL` Console; never a scoped key: `ddl_queries`, `schema_generation`, and the ownerless rows of `sql_queries`.
530
+
531
+ **Each `llm_calls[]` entry**, oldest first, is a call made under the operation's id that names the document's instance, or names none (calls recorded before the API sent one, and work that runs for no instance, such as a schema generation): trace ids are not unique, so another instance's operation can share the id. Its fields: `llm_call_id`; `sub_stage` (the pipeline stage that made the call); `model_type` (the model slot asked for) and `provider` (the one that served it); `called_at`; `time_taken_ms`; `prompt_tokens`, `completion_tokens`, `reasoning_tokens` (part of the completion, not added to it), `total_tokens`; `is_cached`, `is_success`, `is_refusal`, `error_message`, `gateway_request_id`; `prompt`, `json_schema`, `result`. Then what explains a call's outcome:
532
+
533
+ - `reissue_of_llm_call_id` and `reissue_reason`: this call re-issues the earlier call with that id, which failed with that error code. A call sent to another model after it was refused, blocked for its content, or answered with replies that kept failing its schema; a reply cut at its output ceiling sent again with a higher one; and on writes the same overflow sent to another model at the same ceiling: all are re-issues. `reissued_as_llm_call_ids` is the other direction: the calls in the same list that re-issue this one. So either call leads to the other, with no second request.
534
+ - `max_tokens`, `temperature`, `schema_property_order`, `no_cache`: the settings the call requested, not necessarily what the model ran with: depending on the model and its settings, no temperature is sent at all. `schema_property_order` is also null for a call sent without a schema.
535
+ - `finish_reason`: why the model stopped, in the provider's own words, not normalised. `end_turn`, `stop`, `STOP` and `tool_use` are normal stops; `max_tokens`, `length` or `MAX_TOKENS` means the reply hit its output ceiling, and `model_context_window_exceeded` means the prompt and reply outgrew the context window. On a failure it is set where the model side reports it: always for those two, and for a refusal, a content filter or safety stop, or a schema miss after the model side's own retry where it reports those too; null otherwise. Read a refusal from `is_refusal`. One exception to "the provider's words": a cached Gemini reply (`is_cached: true`) first stored before 2026-05-06 reads `stop` whatever Gemini reported.
536
+ - `rejected_attempts`: the replies rejected before this call's answer or error (one that missed the schema, say), as an array; a recorded value that is not valid JSON is served as its text.
537
+ - `model_id` and `provider_type`: the model id the slot pointed at for this call and its provider type (`zai-org/GLM-5` on `nebius`), named for this very call, whether it was answered fresh or from cache. A call refused before any model ran (a rate limit, say) still names the model its slot pointed at. A failed call names the model from the last answer that named one, and none when no attempt got such an answer (every attempt timed out or could not connect) or the call was cancelled. It is the model of that answer only: an earlier attempt's rejected replies may come from the model the slot pointed at before. `model_type` names only the slot, which can be pointed at another model; `model_id` is the model id the slot pointed at when it answered, which for some models is a vendor alias rather than a dated snapshot. Null when the call did not record it, never guessed from the slot.
538
+ - `client_attempts`: how many attempts were made for this call, the one that answered or failed included; an attempt that could not connect counts too. It is the calling service's own count, not something the model side reports. Null for a call that was cancelled or stopped before its first attempt, and for calls made without the usual model client (description generation, for one).
539
+ - `gateway_repairs`: what was done to the reply before it was served, in a fixed order (not the order it happened in): `repaired` (fields written as JSON text were decoded), `json_recovered` (the first of several JSON values was kept), `omitted_scaffolding:<field>` (a reasoning field the reply left out), `schema_not_enforced` (no grammar held the reply to its schema), `strict_fallback` (a strict request was refused and a second, non-strict one answered) and `answered_in_text` (the reply came as text, not as the tool call it was asked for); on a call the caller asked the gateway to hedge, also `hedged` (a second copy of the call was sent) and `hedge_served` (that copy's answer was served). The last three of the first six say how the schema reached the model rather than that the reply was changed, and the hedge flags say how the answer was fetched, so a non-empty list does not by itself mean a repair. A fresh reply reports them, so on a fresh successful reply that names its `model_id`, `[]` means none of these flags applied; on a cache hit or a call that names no model, `[]` means not reported. A failed call reports them where the model side reports what it did before the failure, so there `[]` means none applied or not reported. Re-encoding a reply (key order, whitespace, very large integers rounded) is never reported, and `schema_not_enforced` records the model side's own choice, not a verified grammar.
540
+ - `applied_temperature`, `applied_thinking`, `applied_effort`: what the model side actually sent the provider for the attempt that answered, or on a failure its last attempt: the temperature as sent, the thinking mode (`adaptive`, `enabled`, `disabled`, `level:<x>`, `budget:<n>`) and the effort, or `none` when it sent nothing for one; `other` stands for a value outside the model side's fixed lists. They can differ from the requested settings: a slot that drops temperature, thinking turned off on the retry after a reply missed its schema. Null when not reported (a cache hit, a call that failed before reaching a model, or calls recorded before these fields existed).
541
+ - `cache_read_tokens`, `cache_write_tokens`: how much of `prompt_tokens` the provider read from and wrote to its prompt cache; parts of `prompt_tokens`, never added to it. Null when none or not reported.
542
+ - `raw_result`: the reply as the model wrote it, where a repair or recovery (`repaired`, `json_recovered`) changed its value; `result` holds the changed one. Null otherwise, and not reported on a cache hit or a failed call (a cache hit replays the repaired reply only).
486
543
 
487
- > **Three of those never resolve for an API key**, super admin or not, and this is by design rather than a gap. `user_email` is always absent: the accounts `/users` lookup is closed to keys (an agent may learn who *it* is, never enumerate the organisation's users), so rows keep `user_id` and you resolve names yourself if you need them — and a `User:<email>` search clause matches nothing; filter by `user_id`. For that reason `--user-id` refuses a value containing `@` as a usage error (exit **2**) on both `trace ls` and `trace ls --search`, rather than sending a filter that can only ever return an empty result at exit 0. `api_key_name` / `api_key_prefix` resolve only for the caller's **own** keys, because `GET /api-keys` scopes a key caller to its owner's keys. So a super admin's key does *not* see literally everything its owner's browser sees: it sees the same rows and the same fields, with those three enrichments empty. Everything scope-bearing — which instances, which detail fields, `llm_calls`, xuid substitution — is identical. Detail documents add what the Console's pane shows: `source_id`, `write_mode` / `read_mode`, `scope`, `extracted_objects`, `diff_plan` (sanitised unless super admin), `written_objects`, `migration_id`, `proposal_version`, `schema_version`, `stage`, `triggered_evolution {migration_id, evolution_trace_id, update_trace_id}`, `sql_queries` (the rows owned by the read's instance, for every caller who may open the read), and two tiers of gated fields. **Super admin only** — the accounts `is_super_admin` flag decides, for a key exactly as for its owner's browser session: `llm_calls[]` (`llm_call_id`, `gateway_request_id`, `model_type`, `provider`, …), `full_error_message`, raw token counts, and unsanitised xuids. **Callers with no instance restriction at all** — a super admin, or a browser session on a `KINGDOM_LOCAL` Console; never a scoped key: `ddl_queries`, `schema_generation`, and the ownerless rows of `sql_queries`.
544
+ A value the call did not record is `null`, and `reissued_as_llm_call_ids` and `gateway_repairs` are `[]`; calls recorded before these fields existed read that way throughout.
488
545
 
489
- **A scoped key gets `[]` for `ddl_queries` / `schema_generation`, not the rows.** Those two come from `instance_create_ddl_queries` and `schema_generate_*`, which carry no instance ownership column — `schema_generate_*`'s trace id is client-supplied — so they cannot be scoped in SQL and are served only to callers with no instance restriction at all. This is a deliberate fail-closed choice, not a bug to work around: an empty array means "not available to this credential", never "this operation ran none". `llm_calls[]` behaves the same way (gated on super admin) and always has. `sql_queries` is split: statements recorded with their owning instance reach every caller who may open the read, even when another instance reused the trace id, while statements recorded before ownership was tracked carry no owner and reach unrestricted callers only. A scoped key therefore sees `[]` for reads that old. To let a script tell the two apart, the Console sets a sibling boolean next to each — `sql_queries_withheld`, `ddl_queries_withheld`, `schema_generation_withheld` — that is `true` exactly when a row that exists was withheld from this caller: for `ddl_queries` / `schema_generation` that is every scoped caller, for `sql_queries` a scoped caller on a read that has ownerless rows. So `[]` with `…_withheld: false` is "none recorded", `[]` with `…_withheld: true` is "not yours to see". **Xuids are hidden for every non-super-admin**, including on writes that carry no identity map — the substitution is done by the Console, server-side, so a caller with no browser sees `#hidden` rather than a raw id.
546
+ **Each `step_decisions[]` entry** is one record of what one step of the operation decided, oldest first (by `recorded_at`, then `sub_stage`). A step records each time it runs and decides something (a step that decided nothing records nothing), so one operation can hold several records of one step, and every record is served; none is merged into another. A write's deletion check records in its own process and again where the diff engine applies its gate, and a write retried whole under the same trace id (a stale-schema retry, a stall requeue, a stuck write claimed again) records every step again. Writes, reads and schema evolution record today. A write's extraction records too, under `extraction.` steps named like its model calls (`extraction.structured_output`, `extraction.description_merger`, `extraction.batch_field_extractor`, `extraction.field_verifier`, `extraction.relations_extractor`, `extraction.relations_verifier`, `extraction.deduplication`): what code decided about a reply (candidates folded into one, a reply set aside so the extracted values stand, connections dropped, a candidate left out of the write, a group of types split), and the verifiers' own rejections. Field verification records once per object type, when that type's field processing ends; a write that fails before then records none for that type. Extraction's items have the diff engine's shape: `kind` is `merge`, `object`, `relation`, `relation_type` or `type_group`; `decision` is `folded`, `skipped`, `kept_unverified`, `key_restored`, `rejected`, `uncastable`, `dropped`, `split` or `escalation_disabled`; `detail` holds field names, candidate numbers and counts (`candidate_count`, `candidate_numbers`, `kept_number`, `ignored`, `basis`, `filled_fields`, `conflicting_fields`, `fields`, `nulled`, `omitted`, `connection`, `field`, `candidates_per_link`, `relations`, `types`, `halves`, `refused_max_tokens`, `error_code`), and no field's value except an object's key: in `key`, in a dropped relation's `links` (`Type(field='value')` per link), and a `description` for an object that has no key. `/extract` records its extraction the same way; it has no detail document of its own, so its records show only on a write's document when that write, on the same instance, reuses its trace id. Schema evolution records under `operation_kind` `evolution`: review, decide and apply each journal their steps' records once, when the stage ends, under their own trace, so an apply's show on its update document (its trace is the update id), and the detached pass that resolves an apply's gaps afterwards journals its own under the same trace. Its steps are the two model calls' names, `gap analyzer` (gaps never analysed or stored with no concepts, rows a reply left pending, concepts the parser dropped) and `semantic gate` (what the gate may do with each suggestion, candidates kept without a verdict, verdicts downgraded or not acted on), and the rule steps `evolution.backlog`, `evolution.resolution_filter`, `evolution.clustering`, `evolution.consolidation`, `evolution.prior_decisions`, `evolution.prerequisites`, `evolution.evidence_retirement` and `evolution.apply`; `kind` is `gap` (named by its feedback id in `key` and its concept's object and field), `suggestion` (its fingerprint in `key`, its change's names, and the gaps it was built from in `detail.gaps`, the first 20 with `gaps_total` when there are more), `cluster`, `decision`, `backlog`, `reply_entry`, `build` or `gate`, and no item carries what a user wrote. What a transaction did (a gap resolved, stored with no concepts, deleted or retired) is recorded only once it commits, and a row only when that step's own write or delete reached it. A build retried on a stale schema records both attempts: once the build runs again, every item the stage notes until it ends, the build's and the stage's own after it, carries `detail.attempt` (`2`), so an item without `attempt` was noted before the build ran again, and a `retried` item under `evolution.consolidation` records that it did; an `omitted` item is a marker, not a decision, and carries no `attempt`. A read records what the reader's own rules decided, under the name of the model call whose reply a decision is about (`read.question_decomposer`, `read.sql_syntax_fixer`, `read.empty_result_reviewer`, `read.overfetch_reviewer`, `read.answer_formatter`) or under a rule step (`read.scope`, `read.vector_grounding`, `read.sql_rewrites`, `read.sql_guard`, `read.sql_retries`, `read.answer`, and `read.other` for a family no step claims yet); a decision about a SQL statement a model call wrote goes under `read.sql_rewrites`, with the calling step in `detail.phase` where the rule names it. A read step that decided more than 101 times keeps its first and last 50 decisions, with one item (`decision` `omitted`, `detail.count`) where the rest were left out, cut further from the middle when they do not fit one record. A read journals its records once, when it ends, so a read still running has none yet, and a read retried on a stale schema runs again under the same trace id and records every step again; its items have `kind` (the decision family, such as `sql_guard_refused` or `distinct_order_by_case_dropped`), `decision` (`applied` or `not_applied`), `reason` where the rule says why, and `detail.part`, the part of a decomposed question the decision was made for, counted from 1, where it was made for one. `sub_stage` names the step in the `llm_calls[].sub_stage` vocabulary, so a step's decisions and its model calls join on it (a re-issued call journals under its step's name with a suffix such as `.refusal_fallback`, so it joins on the name as a prefix; the reader's rule steps and `extraction.deduplication` have no model calls of their own) (`diff_engine.deletion_check`, `diff_engine.intent_rules`, `diff_engine.value_span_containment`, `diff_engine.intent_verification_deep`, `diff_engine.alias_confirmation`, `diff_engine.object_resolution`, `diff_engine.plan_construction`, and the `read.` steps above). A step whose decisions act on another step's calls names itself the same way: `diff_engine.intent_rules` acts on the replies under `diff_engine.object_and_relations_deletion_detection` and `diff_engine.batch_field_erasure_detection`. The records hold what the engine decided that the operation's `llm_calls[]` (their prompts and replies) and its `diff_plan` do not already show, so the model's own answers are read in `llm_calls[]`, not here. `decision_count` is the step's true total and `decisions_truncated` marks a record cut to fit (for a read step, also one cut to its first and last 50 decisions), so `decisions` can hold fewer items than the count. `decisions` is the items as the step recorded them, a JSON array, or `null` when the stored text is not one (the count still shows). A pane serves at most 500 records and 4 MiB of `decisions` text, the newest; when it cut older ones it adds `step_decisions_truncated: true` beside the array, and otherwise the key is absent. A trace id a client reuses across writes serves every write's records together, ordered by time with nothing between the writes. The diff engine's items share one shape: `kind` (what the item is: `intent_gate`, `object`, `identity_change`, `merge`, `filtered_deletion`, `filtered_update`, `relation`, `deletion`, `alias`, `scope`, `detection`) and `decision` (what was decided: `deletion_dropped`, `converted`, `rejected`, `kept`, `bound`, `matched`, `left_out`, …), then, where set, `object_type` and `key` (the primary-key values as `field='value'`), `index` (the object's position in the diff engine's sorted object list, not in `extracted_objects[]`), `xuid` (the stored row), `reason`, and `detail` (anything else the step decided). `diff_engine.intent_rules` logs what the engine's rules did to the detection model's answer, in the order they acted, so an item can be dropped and later restored; `diff_engine.plan_construction` says what building the plan decided after every earlier step, which the plan shows only by leaving it out (a merge the relation-budget re-check refused or re-admitted after alias binding, a deletion a rename overrides, an unlink dropped for a row a confirmed delete does not run on or a lookalike row the write does not bind, a move whose old link stays with the replacement guard's cause, a dropped merge or one already a single row, what a scoped write left out). A deletion that verification rejected reads `kind: "deletion"`, `decision: "rejected"` under `diff_engine.intent_verification_deep`, with what the object became and why; the proposal itself is the detection model's reply in `llm_calls[]`. Another part of the system may record items of its own shape here; they are served as recorded. Every detail pane serves its operation's records the same way; the write, read and evolution panes serve them for an operation still running or stale too (create and update have no such pane). A step that refuses the write still records what it decided first: the detector its rules, resolution what it found, plan construction the merges, unlinks and scope it changed. A caller who is not a super admin gets neither `step_decisions` nor `step_decisions_truncated`, exactly as for `llm_calls[]`.
547
+
548
+ **A scoped key gets `[]` for `ddl_queries` / `schema_generation`, not the rows.** Those two come from records that carry no instance ownership — the schema generation's trace id is even client-supplied — so they cannot be scoped to an instance and are served only to callers with no instance restriction at all. This is a deliberate fail-closed choice, not a bug to work around: an empty array means "not available to this credential", never "this operation ran none". `llm_calls[]` behaves the same way (gated on super admin) and always has. `sql_queries` is split: statements recorded with their owning instance reach every caller who may open the read, even when another instance reused the trace id, while statements recorded before ownership was tracked carry no owner and reach unrestricted callers only. A scoped key therefore sees `[]` for reads that old. To let a script tell the two apart, the Console sets a sibling boolean next to each — `sql_queries_withheld`, `ddl_queries_withheld`, `schema_generation_withheld` — that is `true` exactly when a row that exists was withheld from this caller: for `ddl_queries` / `schema_generation` that is every scoped caller, for `sql_queries` a scoped caller on a read that has ownerless rows. So `[]` with `…_withheld: false` is "none recorded", `[]` with `…_withheld: true` is "not yours to see". **Xuids are hidden for every non-super-admin**, including on writes that carry no identity map — the substitution is done by the Console, server-side, so a caller with no browser sees `#hidden` rather than a raw id.
490
549
 
491
550
  **Who sees what — the Console decides; `auth status` tells you which view to expect.**
492
551
 
493
552
  - A regular key sees the operations of the instances the accounts API lists for it — the same set `org list instances` prints. That is at most what its owner sees in the Console, never more.
494
553
  - Access requires the key's role to carry **`data.read`** — the same permission the data-read endpoint requires. A role deliberately scoped away from data (`auth status` shows the codes under `permissions`) is refused with **403 → exit 3, stage `permission`**, whoever owns the key — a permission denial, not a bad credential: rotating or re-issuing the key cannot fix a role that was scoped this way on purpose. An unknown or missing key is the separate 401 → exit 4 (`auth`).
495
- - A key whose owner is a **super admin** sees the super-admin Console: every organisation's instances, no instance filter, and the super-admin-only detail fields above. The accounts `is_super_admin` flag decides — the key sees the same rows and the same scope-bearing fields its owner's browser session sees, minus the three enrichments no key resolves (`user_email`, and `api_key_name` / `api_key_prefix` for other people's keys; see the note above). `auth status` reports it as `super_admin`.
554
+ - A key owned by an active **super admin** and created by one sees the super-admin Console: every organisation's instances, no instance filter, and the super-admin-only detail fields above. A key an organisation's Owner created for a super admin (an Owner may create keys for its members) is scoped like any other key. The accounts `/whoami` `is_super_admin` decides — the key sees the same rows and the same scope-bearing fields its owner's browser session sees, minus the three enrichments no key resolves (`user_email`, and `api_key_name` / `api_key_prefix` for other people's keys; see the note above). `auth status` reports it as `super_admin`.
496
555
  - When the Console **cannot** work out that scope — its accounts API is unreachable, answers non-2xx, is undecodable, or is not configured — it answers **503** with `{"error": "instance_scope_unresolved", "reason": "accounts_…"}` (or `accounts_unavailable` when the key's own lookup failed the same way) instead of an empty document. The CLI reports that as **exit 3, stage `unavailable`**, with a message that says the scope could not be resolved and that this is *not* "no operations": a zero-row answer from a Console that could not decide would have read as a quiet instance.
497
- - No new permission is involved: the Console answers a key exactly as it would answer its owner's browser session, narrowed to the key's instances. It never mints a session for a key. Filters that need to look other users up (`User:`-style email filters) are not offered: an API key can identify only itself, never list or look up users — filter by `--user-id`.
556
+ - No new permission is involved: the Console answers a key as it would answer its owner's browser session, narrowed to the key's instances, and treats it as a super admin's only when its owner and its creator are both active super admins. It never mints a session for a key. Filters that need to look other users up (`User:`-style email filters) are not offered: an API key can identify only itself, never list or look up users — filter by `--user-id`.
498
557
 
499
558
  **The Console origin.** `trace …` talk to the Console, a second origin. It is `--console-url` → `$XMEM_CONSOLE_URL` → inferred: `https://console.xmemory.ai` for the default API, `:8002` for a local `:8000`. A key bound to any other API origin gets a usage error (exit **2**) rather than a guess — the credential-binding rule holds across origins.
500
559
 
@@ -508,7 +567,7 @@ learn; the CLI composes the Console's search clauses itself.
508
567
  |---------|-------------|
509
568
  | **`auth login`** | PKCE via Console `/xmemcli/<challenge>` → `.xmemrc.json`. `--no-browser`, `--timeout`, `--console-url`, `--callback-port` (pins the loopback callback port for `ssh -L`; `0` or omitted means any free port, otherwise 1024–65535; also `$XMEM_CALLBACK_PORT`, which is read globally). An explicitly named Console (`--console-url`, `$XMEM_CONSOLE_URL`, or the one pinned by a single-origin URL) is cross-checked against the API origin before the browser opens or a pasted key is sent anywhere — a provably mismatched pair (mixed loopback-ness, or a production half paired with a non-production one) is a usage error (exit **2**), on the interactive paste path as much as on the browser path. With `--email ADDR` (+ optional `--org-name`, used only when a new account is created; requires `--email`): headless email sign-in — one emailed approval, then the CLI mints its own org-wide key named `CLI email login (<hostname>, <timestamp>)`; details in the auth-flow reference. New-organisation creation additionally requires the deployment to allow public registration. Payload shapes: a fresh email mint carries `method: email`, `key_name`, `api_role`, and `org_created`; the browser path carries `method: pkce` (no role/org fields); a matching `--api-key` or rc credential short-circuits to `status: already_authorized` with `source`: flag / rc (`persisted: false` on the flag arm, which saves nothing); a matching `$XMEM_API_KEY` is copied into the rc and reports `status: authorized`, `source: environment`. Exit codes follow the standard HTTP contract (401 → 4 with stage `auth`, 403 → 3 with stage `permission`, other HTTP/network/server-state failures → 3). |
510
569
  | **`auth logout`** | Remove active `.xmemrc.json` |
511
- | **`auth status`** | Call `/whoami`. No key → exit 0; bad key → auth error; good key → email + prefix + `super_admin` (whether the key's owner is a super admin — what decides which Console view `trace …` return) + `permissions` (the key's own capability codes; `trace …` requires `data.read` among them). |
570
+ | **`auth status`** | Call `/whoami`. No key → exit 0; bad key → auth error; good key → email + prefix + `super_admin` (whether the key is a super admin's, meaning its owner and its creator are both active super admins — what decides which Console view `trace …` return) + `permissions` (the key's own capability codes; `trace …` requires `data.read` among them). |
512
571
  | **`mcp <instance-id>`** | Serve one instance to an MCP client over stdio, using this CLI's credential. Not for humans — see below. |
513
572
  | **`version`** | CLI release version (no network) |
514
573
  | **`status`** | Local health: version + whether `.xmemrc.json` exists, and the `mcp_url` it declares when it declares one (no network) |
@@ -640,7 +699,7 @@ condition, and the hook author is the one reading stderr.
640
699
  2. **Exit 3 / `stage: "permission"` / HTTP 403** → the key works, but the operation needs a broader API role (for example `data.write`, `instance.generate_schema`, `cluster.get_own`). Do **not** treat this as a wrong password.
641
700
  3. **Exit 3 / `stage: "request"`** → other HTTP failure (instance not found, validation error, server error). Read the error message and HTTP status in the JSON body.
642
701
 
643
- Some commands attach a more specific `stage` (`validate`, `usage`, `limit`, `not_found` for `trace get`, …) while still using exit **3** unless the row above applies. `write` adds `processing`, which is in the table above and exits **8** rather than **3**. The `schema suggestions` commands add their own stages: `stale_proposal` (re-run review for a fresh `proposal_version`), `invalid_decision`, `dependency` (accept the dangling dependencies first), `not_initialised` (submit a write first), and `apply`; `review` reports an `evolution_in_progress` condition as a successful status with exit 0.
702
+ Some commands attach a more specific `stage` (`validate`, `usage`, `limit`, `not_found` for `trace get` and `trace call`, …) while still using exit **3** unless the row above applies. `write` adds `processing`, which is in the table above and exits **8** rather than **3**. The `schema suggestions` commands add their own stages: `stale_proposal` (re-run review for a fresh `proposal_version`), `invalid_decision`, `dependency` (accept the dangling dependencies first), `not_initialised` (submit a write first), and `apply`; `review` reports an `evolution_in_progress` condition as a successful status with exit 0.
644
703
 
645
704
  **Envelope errors** (HTTP **200** with non-empty `errors[]`) are not HTTP 401/403; they typically exit **3** with `stage: "request"`. Two exceptions. `QUOTA_EXCEEDED` exits **6** with `stage: "quota"`, the same as it does on a real HTTP 402 — the keepalive flattening does not change the exit code — and this applies to every command that classifies the error, not just `write` and `read`. `WRITE_STILL_PROCESSING` exits **8** with `stage: "processing"` and is not a failure at all; unlike the quota rung this one is **`write`-only**, because its guidance is to poll `write-status`, which is meaningless for any other verb — elsewhere it falls through to **3**/`request`.
646
705
 
@@ -648,6 +707,16 @@ Some commands attach a more specific `stage` (`validate`, `usage`, `limit`, `not
648
707
 
649
708
  **Breaking change in 0.0.5:** HTTP **403** previously exited **4** (`auth`); it now exits **3** (`permission`). Scripts that treated exit **4** as “any auth-layer failure” must check `stage` or handle exit **3** for permission denials.
650
709
 
710
+ **New in 1.6.1:** a removal that names a record nobody stores is reported under `changes.unmatched_deletes`; nothing was deleted for it. In a terminal each one prints as a `note:` line, next to the other advisory records — `note: unmatched_deletes Task task_id='T-999'` — where 1.6.0 printed it among the change lines. `--json` output is unchanged: the block was already kept verbatim. A server that predates the bucket never sends it, so this works against any server.
711
+
712
+ **New in 1.6.0:** `trace calls` lists the LLM calls of one session, a page at a time, and `trace call` opens one call whole — for super admins, as the Console decides. Both need a Console that serves them; an older one answers **401**, exit **4** (`auth`), for a key that is fine.
713
+
714
+ **New in 1.5.2:** a DEEP write can leave a record out when the model's reply for it did not match the schema or ran past the output limit; the server reports each such record under `changes.skipped_candidates`, and it was not stored. In a terminal each one now prints as a `note:` line, next to `overwritten_field_values` and `merged_field_conflicts` — `note: skipped_candidates Dog nickname='Bolt' error_code='model_output_unusable' reason='…'` — where 1.5.1 printed it among the change lines. `--json` output is unchanged: the block was already kept verbatim. A server that skips nothing sends no such bucket, so this works against any server.
715
+
716
+ **New in 1.5.1:** `write --scope-mode drop` asks for a scoped write that skips and reports changes outside its scope instead of failing, and that creates only the records its `--scope` names — which may not be stored yet. The report is `changes.skipped_out_of_scope`: one terminal line per operation, type and set of fields, carrying `count=N`, the number of records it stands for, and naming a record's key only when the `--scope` named that record. A drop-mode write that skipped everything still returns a `changes` block, holding that report alone. A server that predates drop mode refuses `mode` in the scope (exit **3**), which is a clean failure; a deployment part-way through rolling it out is not, so publish 1.5.1 only after the server side is fully deployed — see [Releasing `xmemcli`](../docs/cli/XMEMCLI-RELEASE.md). `--scope-mode reject` is never sent, so plain scoped writes work against any server.
717
+
718
+ **New in 1.5.1:** `write` and `write-status` keep the server's `changes` block — what the write created, updated (field by field, old → new) and deleted, plus bulk summaries and advisory records — verbatim in `--json` output. In a terminal each change is one indented line: a bulk delete or update reads `deleted 3 × Ticket where status='open'`, one the server refused reads `not deleted Ticket: refused — <reason>`, the advisory `overwritten_field_values` / `merged_field_conflicts` records follow as `note:` lines, and past 50 lines the rest is counted in one closing line (`--json` is never truncated); `write-status` with several ids prints a status line per write, each followed by its changes. `changes` is omitted when the write changed nothing, so that output keeps its earlier shape; a drop-mode write that only skipped is the one write that changed nothing and still carries the block. `write` also takes structured mutations (`--mutations-file`, `--mutations-stdin`, `--delete TYPE:ID`) in place of text; the text positional is now optional, and a write with neither text nor mutations, or whose text is empty or only whitespace, is a usage error (exit **2**, `stage: "usage"`). Before 1.5.1 `write ""` reached the server, whose 400 exited **3** with `stage: "request"`, and whitespace-only text was sent and accepted. A structured write's output carries no `extracted`, since it runs no extraction; on a text write the tags now read `Type(identifier)` (`Person(name='Ada')`), as documented, where they read `?(Type)`. A malformed `--scope` (on `write` and `read`) or `--delete` — now including a key field given twice or an empty value — reports `stage: "usage"`, still exit **2**; `--scope` reported `"request"`. `--mutations-stdin` and `--schema-stdin` refuse a terminal instead of waiting on it, and every input file and piped input is read by one reader and decoded as strict UTF-8, so an input that is not UTF-8 (`--mutations-file`, `--schema-file`, `--migration-plan`, `schema suggestions decide --file`, the `xmd validate` / `convert` / `enhance` schema file, or the same bytes piped to `--mutations-stdin` / `--schema-stdin`) is a usage error at exit **2** instead of a traceback at exit **1** — or, piped, a garbled write at exit **0** — and a leading UTF-8 byte-order mark is accepted. Wherever an HTTP error from the API is reported as `HTTP <status>: …`, a body in the API's error envelope now gives the server's own message (`HTTP 400: <message>`, plus the request `field` when the server names one) instead of the first 200 characters of the raw body.
719
+
651
720
  **New in 1.3.0:** exit **8** with `stage: "processing"` for a `write` whose server-side wait window expired while the write kept running. In `--json` mode that document also carries a `write_id` field (omitted when the server sent no handle), so a script polls with it directly instead of parsing the message. Additive — no previously documented mapping changed — but a script that reads “non-zero, and not a code I recognise” as failed-and-retryable will now retry a write that has **not** failed, starting a second one the account pays for. Branch on **8** (or on `stage`) and poll `xmemcli write-status <write_id>` until it settles.
652
721
 
653
722
  **New in 1.2.0:** `instance templates` lists the ready-made instance shapes the service ships, with the prose to choose between them, and `instance create --template <id>` builds one from a pinned schema the server already holds. No schema is generated on that path: no description to compose, no `xmd generate` hop, and the instance comes out identical to the one the console would have made from the same template. (Creating an instance still generates its agent brief afterwards, on this path as on any other.)