The model sometimes stops after receiving 'Fase approvata' without calling
`tht session finalize`, leaving the session open. Now the gate itself calls
finalize after advancing the max phase (F8), making session closure
deterministic regardless of model behavior.
- reviewer_confirm kind:phase: after phase advance at max_phase, gate calls
`tht session finalize <session>` (best-effort with recovery message)
- SKILL.md updated: model no longer needs to call finalize itself
- Tests: 2 new JS tests (auto-finalize at max phase; no-finalize at non-max)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Lever 1: Join-graph via FK logics in annotations + suggest-fks command
- TableAnnotation.foreign_keys field stores curated logical FKs (DWH has no FK constraints)
- tht schema suggest-fks: mine from approved SQL, heuristics (time_key → dim_time),
same-name discovery + explicit --assume flag for multi-owner PKs
- mschema renders 【Foreign keys】 section populated; validation in merge.py
- SKILL.md F4 now reads FKs from mschema-text, no custom data_time_key logic
Lever 2: Context-pack consolidation at kickoff (tht search pack)
- Single embedding of question, reused for schema + evidence + solved searches
- One command: tht search pack <question> --session <id> → retrieval_pack.md
- Graceful degradation when Ollama/vector store unreachable (exit 0, empty sections)
- SKILL.md F1 prescribes as first call; reduces model thinking turns via pre-retrieval
Lever 3: Phase-summary recap v2 auto-construction from session ledger
- tht session show --json includes full decisions ledger
- tht phase meta --json exports 'emits' (substantive decision types per phase)
- Gate appends deterministic 【Decisioni registrate in questa fase】 section (appendLedgerSection)
- Model authors only summary + checks; recap table comes from persisted state (exact by construction)
- SKILL.md Disciplina 6: brief model output, gate fills the rest
Tests: 358 Python (including 10 FK + 3 pack + 1 session-ledger tests) + 111 JS gate tests, all pass.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Transcript analysis (session 2026-07-06-175012, GLM 5.2) showed F1 at 567s:
182s wasted on a useless `tht schema introspect` (re-introspecting the remote
DWH although physical.yaml was already materialized) plus ~220s of model
thinking inflated by ~7 exploratory turns (--help/find/cat). The actual
searches cost ~15s; reviewer gates (~145s, untouched) are the quality contract.
- schema_cmd.py: introspect now exits 0 with "OK (cache)" in ~1s when
physical.yaml exists; --refresh forces the real re-introspection.
Deterministic cross-model guarantee, verified live on psd (163 tables, 1.2s).
- SKILL.md: F1 toolbox (only `tht search find` + `tht schema render`; no
introspect/--help/filesystem browsing; batch all searches in one turn);
F4 step 1 is render-only with a one-shot introspect fallback.
- tht-gate.js: `tht schema introspect ... --refresh` added to FORBIDDEN
(maintenance stays shell-only, never in-session).
- tests: 4 new pytest cases (cache hit placement proven with fake credentials,
refresh bypass, corrupt-catalog fall-through, render fallback message) and
2 gate anti-bypass JS cases.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Extract shared statusBadgeClass() helper (src/viewers/statusBadge.ts) so
SqlViewer, CteResultViewer and PhaseSummaryViewer can't drift: ok/success/
passed/promoted render green (--success), warn renders amber (--warning),
error/failed stay destructive red, everything else stays neutral outline.
Previously CteResultViewer/PhaseSummaryViewer mapped "ok"/"promoted" to the
default badge variant, which is bg-primary (GSD red) — success states
rendered red.
- enrich.js: buildCteResultV2 now falls back preview.rows to [] instead of
null when last_test.preview_rows is missing (pre-upgrade ok records), and
CteResultViewer reads result.preview?.rows?.length with a null-safe
fallback so it degrades to the "No preview rows" empty state instead of
crashing.
- PhaseSummaryViewer: section.items is optional (model-authored sections can
be prose-only); render (section.items ?? []) instead of crashing.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The tht-gate.js reviewer_confirm now builds structured v2 artifacts before the
widget so the reviewer approves gate-derived data, not raw model text:
- new pure modules gate/artifact-contracts.js (soft validators, {ok,errors},
legacy-passthrough) and gate/enrich.js (index/description enrichment,
buildCteResultV2 fusing thin model data with `tht cte info`, phase enrichment)
- cte_plan v2: validate + enrich + persist via `tht cte plan --name … --doc -`
(names derived from data.ctes[]); legacy `names` param kept as fallback
- cte_result v2: rebuild from `tht cte next`/`tht cte info` (sql + preview from
the persisted test record); null/error last_test -> actionable textResult
- phase v2: soft-validate + fill phase from meta + catalog descriptions
- prepareReviewerArguments coerces artifact.data too (GLM double-stringify);
legacy markdown strings pass through unchanged
- SKILL.md: Phase 6 cte_plan payload A + thin cte_result guidance; Discipline 6
payload C example; Discipline 7 reworded for gate-rebuilt cte_result
Legacy (non-v2) paths unchanged. TypeBox stays Type.Any() for artifact.data;
validation is soft (textResult) so models self-correct instead of looping.
All 102 gate JS tests green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The "respond with gate widgets; use '!' for free text" notice is terminal-
flavored. Like the reLoop Esc guard, it used ctx.hasUI, which on pi >=0.80 is
true in RPC too, so it leaked to the browser. Now guarded on ctx.mode === "tui".
The plain text is still swallowed by the `handled` return in all modes; only the
warning is dropped in RPC. No functional ctx.hasUI guard remains in the gate.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fix six GUI defects, verified live against the real stack:
- NavSessions: mark the in-progress (active) session in yellow (warning
token) — both its status dot and its row background.
- SteerInput: the composer is now an auto-growing textarea that wraps and
grows vertically (caps at 160px, then scrolls); Enter sends, Shift+Enter
inserts a newline.
- WorkflowBar: show a synthetic title of the current phase under the 8 dots
from static EN/IT strings (no LLM); English is displayed to match the chrome.
- CentralStatus: reformat the 5-line system-message tail as a structured,
monospace list with per-line markers and an emphasized last line.
- AppShell + CentralStatus: move the left Model-activity panel toggle off the
working spinner (now a pure status indicator) onto a dedicated arrow button
(→ opens, ← closes).
- gate: rename the phase-confirm button "Conferma e prosegui" → "Salva e
procedi" (builders.js + its L1 test).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
reLoop's "Esc non chiude il gate..." warning is terminal-specific. On pi 0.73
ctx.hasUI was false in RPC, so `if (ctx.hasUI)` effectively meant "TUI only". On
pi >=0.80 hasUI is true in RPC too (dialog-capable UI via the bridge), so the
notice leaked to the browser on any invalid gate response. Guard on
ctx.mode === "tui" to restore the original intent. The fake pi runtime gains
mode:"tui" so the roundtrip test still exercises the notice.
Audit of the other 5 ctx.ui.notify: left as-is. They are valid in both live
modes (TUI and RPC, both hasUI=true) and the gate cannot run headless
(emitAndWait needs a UI), so a guard would be dead code. Their dual-mode
rationalization belongs with the future present() work (tracked in the eval doc).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- aritmolab-provider.js: import pi-ai from @earendil-works (was @mariozechner).
Verified live that the aritmolab/qwen provider still registers under pi 0.80.3
(get_available_models -> aritmolab, deepseek, zai). Removes the dual-package
reliance on the frozen @mariozechner install still on disk for rollback.
- docs/general/pi-configuration.md: built-in models package is @earendil-works/pi-ai
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Some models (GLM 5.2) send object params (reviewer_confirm's artifact,
write_schema_linking's schema_linking) as JSON-encoded strings. These failed
TypeBox validation before execute(), making the model retry in an unbounded loop
(observed ~2100s hang). jsonObjectOrSelf() coerces them back to objects in
prepareReviewerArguments and write_schema_linking.
Also close an anti-bypass gap: pi's write/edit tools were unrestricted on the gate
code, so a looping model actually patched tht-gate.js. GATE_CODE_FILES now blocks
any write under .pi/extensions. Requires a pi restart to take effect.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
buildSelectRequest injected an `altroOption` ({id:"other", opens:freetext}) into the
select descriptor, but SelectWidget/MultiselectWidget never route option.opens — so it
rendered as an inert "Altro — specifica…" button next to the working reserved
"Other — specify" control (which now owns free-text since the reviewer-gate-ux fix).
Remove the injection, the now-unused altroOption()/ALTRO_LABEL, and the unused
allow_other flag from both builders (no frontend consumer). Free-text stays offered on
every gate via the reserved "Other" control. The ArtifactGateWidget opens/LinkageHost
linkage (reject-with-reason capability) is intentionally kept — it is a separate,
tested feature, not the injected duplicate.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- WorkflowBar: center the phase pills (add justify-center).
- tht-gate.js: the gate was sending the descriptive phase name (e.g.
"chiarimento") instead of the workflow.yaml id ("F1") in the widget's
`phase` field, so the frontend's F1..F8 match never hit and pills never
lit up. Rename phaseName -> phaseId, return p.id.
- CentralStatus: expand the live "working" tail from a single truncated
line to up to 5 lines (line-clamp-5 safety net for unbroken paragraphs).
- ModelActivityPanel: render the streamed tail through react-markdown +
remark-gfm instead of raw per-line <p> tags, so emphasis/headings render
and blank-line paragraph breaks (previously stripped) are preserved;
tail-cut now operates on paragraphs instead of physical lines.
Updated tests accordingly (WorkflowBar/CentralStatus/ModelActivityPanel/
AppShell.session-mgmt); full frontend suite (98/98) + harness gate node
tests (34/34) + tsc -b pass.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Move the AritmoLab provider (Qwen) into harness/.pi/extensions/ so it is
auto-discovered only when Pi runs with cwd=harness, keeping it project-local.
GLM (~/.pi/agent/models.json) and DeepSeek (built-in) stay user-wide. File is
.js (not .mjs) because Pi extension auto-discovery matches only /\.(ts|js)$/.
Removes the gemma4-26b-a4b model (404 at the endpoint) from both the provider
and the model-matrix default list.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The cheat-sheet, Discipline 2, and Phase 7 said F7 closes with
reviewer_confirm kind:"sql" — but the gate's kind:"sql" only records
sql_approved:phase:7; F7 is not in _AUTO_ADVANCE_PHASES, so advancing to
F8 still needs reviewer_confirm kind:"phase" (mirrors F6). Same
record-vs-advance trap this branch removes elsewhere.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
F3/F4 close with reviewer_confirm kind:'phase' (they do NOT auto-advance);
advance:true only auto-advances F2-empty/F6-skip; rewrite_question belongs
to F3 not F1; F4 uses the new write_schema_linking tool with the documented
SchemaLinking shape. Adds a per-phase artifact/close cheat-sheet.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
tht() gains an optional stdin arg; the tool pipes the schema-linking
object to 'tht session set-schema-linking --file -', which validates
against SchemaLinking and returns the exact error on failure — so the
model stops hand-writing the artifact and validating with ad-hoc python.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
reviewer_confirm kind:'cte_result' registered cte_approved --subject
phase:6, which decision_cmd rejects (exit 5) and next_cte never
recognizes — dead-ending F6. Derive the CTE name from 'tht cte next'
(plan-order single source of truth) and approve by name. sql path
(sql_approved:phase:N) unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A1 — SessionMenu gains a Resume item, gated to status!=="finalized" && !archived
(matching the backend's 409 read-only guard), wired in AppShell to doResume ->
POST /sessions/:id/resume. SessionMenu.test.tsx (3 tests); frontend 96/96, tsc clean.
A2 — diagnosis-first clean-room repro driving `pi --mode rpc` with the backend's
exact resume handshake shows the cold-start stall NO LONGER reproduces on pi
0.79.4 (8/8 chained into `tht session show` + `read SKILL.md` in-turn, fresh and
partway sessions). The earlier narrate-and-stop predates the pi upgrade.
Defense-in-depth anyway: RIPRENDI_KICKOFF hardened to force the in-turn tool call
(gate_resume_kickoff.test.js + live regression 2/2). Gate JS 34/34.
PROJECT_STATE open-item #1 (resume stall) flipped to RESOLVED; cross-model resume
robustness folded into workstream G.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
F — reviewer_select options may now carry a `decision` payload {type, subject,
detail?, rationale?} plus an optional `advance`. Picking such an option IS the
confirmation: the gate persists it directly (tht decision add) and optionally
advances, with no redundant reviewer_decide/reviewer_confirm follow-up gate.
Options without a payload stay ask-only; back/exit/Other never persist.
Pure logic extracted + exported for unit tests: resolveSelectOutcome (classifies
the response) and decisionAddArgs (shared with reviewer_decide, DRY). Gate JS
suite 33/33 (gate_select_decision.test.js, +5); harness pytest 269 unchanged.
Contract docs updated together: reviewer_select tool description, SKILL.md
(widget summary, disciplines 2-3, Phase-1 single-pick), and the CLAUDE.md gate
note. Live verification (model truly emits reviewer_select+decision, decision in
review_decisions.jsonl, no follow-up gate) deferred to workstream G — it is
model-behavior-dependent.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The frontend's reviewer widgets uniformly send the picked option in a `choices`
array (SelectWidget/ArtifactGateWidget: `choices: [optionId]`), but the gate's
reviewer_select and reviewer_confirm(reject) handlers read `resp.choice`
(singular). Result: every single-select gate saw an undefined choice, answered
"Nessuna scelta ricevuta", and re-presented forever — the workflow could never
pass F1. (reviewer_decide/multiselect already read `resp.choices`, so it worked.)
Add a shared selectedChoice(resp) helper reading choices[0] (falling back to the
legacy singular choice); both handlers use it.
TDD: gate/__tests__/gate_choice.test.js RED->GREEN; full gate suite 28/28.
Verified LIVE (Playwright -> real Pi -> GLM 5.2): a single-select F1 answer is
now accepted and the workflow advances (clarification 2/4 -> 3/4). The same run
also live-verified the F1 hang fix (418187a).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ctx.ui.input in `pi --mode rpc` correlates extension_ui_response on its own
top-level RPC id (crypto.randomUUID), not the descriptor id the gate carries
in `title`. SessionBridge replied with the descriptor id, so Pi silently
dropped the response and the model never resumed — every reviewer widget hung
after the human answered.
SessionBridge now stores Pi's top-level m.id (pendingPiId) and replies
extension_ui_response{ id: pendingPiId, value: <uiResponse> }; value still
carries the descriptor id so the gate's internal resp.id === descriptor.id
check still holds.
The fake-pi double had masked the bug by forcing m.id == descriptor.id; it now
mirrors real Pi (distinct randomUUID, correlate on it, drop unknown ids), with
a negative regression test. SKILL.md Phase 1 also now steers multi-answer
disambiguation to reviewer_decide (multiselect).
Tests: backend 67/67, tsc clean, fake-pi contract 2/2.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- loadEnvFromDotenv: read ctx.cwd/.env into the process environment
- prepareReviewerArguments: normalize/parse reviewer tool inputs
- tidy reserved-option filtering and tht command argument assembly
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Set quietStartup:true in .pi/settings.json to suppress Pi banner on RPC stdout.
Add test pinning both quietStartup and theme values. Document project-local trust
requirement (--approve) so no trust prompt blocks RPC loop iteration.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
When THT_SESSION env var is set, /nuova-domanda injects a kickoff that
tells the model to use the pre-created session id instead of running
`tht session new`. /riprendi-sessione and no-env-var paths unchanged.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Removed event.source === "interactive" guard from entry detection so
/nuova-domanda and /riprendi-sessione activate lockActive+pendingKickoff
regardless of source (TUI or RPC prompt).
- Removed event.source !== "interactive" from free-input filter; lock now
blocks/steers all user input when active, not only interactive keystrokes.
- Added typebox@1.1.38 devDep + fake_pi_runtime.registerCommand (gap from Task 2).
- New test: gate_entry.test.js (2 tests: lock activates on RPC; !-steer passes).
- Full suite: 19/19 pass, zero regressions.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Implementazione del piano di remediation progressiva sui difetti emersi
dall'analisi dell'harness. Tutto verificato: 214 test Python (incl. L0 su
Postgres reale), 14 test JS del gate, ruff pulito.
Blocco 1 (CRITICA, integrazione gate↔CLI):
- phase advance: gate usa --auto + exit 6; reviewer_confirm kind:phase fa
advance esplicito che applica i prerequisiti (prima non avanzava per le
fasi a conferma umana).
- cte plan riceve i --name dal gate (param names); set-question con id
posizionale; skill `tht search find`; nuovo comando `tht memory save-one`
con dedup hash client-side in save_one_memory.
Blocco 2 (D15, stato post-rollback):
- campo `phase` su DecisionRecord + effective_decisions phase-aware per i
subject "a nome" (cte_approved ecc.); _compute_promotions e finalize sulla
vista effective; finalize confronta col piano CTE effettivo, non glob;
`decision add --retracts` + comando `decision retract`.
Blocco 3 (D7 read-only + D6 manifest):
- assert_read_only su tutti e quattro i codepath (direct + REST);
- manifest author/summary/updated_at/updated_by/schema_version popolati +
helper touch_manifest sulle mutazioni.
Blocco 4-5 (D14a/D14b):
- decision_min_phase data-driven via `emits:` in workflow.yaml;
- formula evidence: status auto, search_formulas, gruppo CLI `tht formula`,
`search find --kind formula`, load_evidence_dir salta i .sql.md.
Blocco 6 (robustezza):
- taskdoc slice promoted_tables + bound enforced; report escaping/bound +
rsplit note; filtro kind reader REST/direct; conteggio upserted robusto;
guard REST run_query non-list; LSH disallineato -> LshIndexError.
Blocco 7 (pulizia):
- dead code gate e KIND_TO_TABLE morto rimossi; doc Postgres-only
(README + connection.py).
Blocco 0 (parziale): test di compatibilità firma gate↔CLI
(tests/integration). Rinviati: fake-Pi runtime completo, artifact-gate da
disco (#23), parità eligibility REST/direct (#28), unificazione
reserved-labels (#30), memory_rejected da deselezione (#33).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Due cambiamenti interconnessi da user review:
1. language come parametro workspace (spec decisione 9):
- Config.language (default 'en') + workspaces PSD con 'language: it'
- Generalizza Thoth oltre l'italiano: descrizioni tabelle/colonne ed evidence
sono nel workspace language; le istruzioni della skill restano in inglese
(piu' affidabili per modelli piccoli, meno ambigue)
2. Skill riscritta in INGLESE preservando la semantica COMPLETA dell'originale
(autocritica: la mia riscrittura precedente aveva perso ~10 vincoli precisi):
- 'promuovere' ambiguo (3 accezioni: phase advance / recommend / memory promote)
-> 'never advance a phase or record a decision without confirmation'
- recuperati vincoli persi: choice-is-confirmation (no reviewer_confirm dopo
reviewer_decide), reviewer_select SOLO per iterazione no-decision, messaggi
auto-contenuti obbligatori, artefatto = superficie di decisione (gate rilegge
da disco per CTE/SQL), candidati con provenienza+score non verita', opzione
'leave ambiguity open', F1 passa lista completa non solo ultima
- language contract esplicito (istruzioni EN, output nel workspace language)
Sottomoduli cte/memoria/rewriting/sql-generation in inglese, semantica tecnica
intatta (regole AV-SQL, dim_time trick, max 5 memorie solo 3 tipi riusabili).
Verifica: 0 residui nsp/chirone, tutti i tht <cmd> citati registrati, 165 passed.
Skill ex-novo che riflette Thoth (non copia di ChironeWp3):
- vocabolario widget-descriptor (reviewer_select/decide/confirm) invece di 'dialog native'
- D11 save-one in F2 (upsert mirato vs full resync)
- D14a value_grounded (LSH multi-colonna non collassa) + D14b concept_formula in F4
- D13 free-text e D15 rollback nelle discipline trasversali
- memory vive SOLO nel vectordb (drop registry, spec 5): save-one/promote senza registry
- F8 datamart onesto (stub NotImplementedError)
Sottomoduli cte/memoria/rewriting/sql-generation portati adattando nsp->tht, con
i vincoli precisi trasferiti fedelmente (max 5 memorie, solo 3 tipi riusabili,
CTE solo WITH senza SELECT, dim_time join non aritmetica, sql_final pulito).
Verifica: zero residui nsp/chirone/psd nella skill; ogni 'tht <cmd>' citato e'
registrato (correzione: 'tht formula retrieve' era inesistente -> riformulato in
'ricerca nelle evidence'). Suite: 165 passed.
settings.json (theme: thothii-mono), the two slash-command prompts
(/nuova-domanda, /riprendi-sessione), and the theme JSON. Renamed PsdWp3 -> ThothII
in prompt prose and the theme name. No tests (config files). pi --mode rpc launched
with cwd=harness/ finds the .pi/ directory + the extensions (nsp-gate.js, gate/).
Rewrite of ChironeWp3's gate extension. The pure widget-descriptor CONSTRUCTION
is in ./gate/builders.js (L1-tested, C1); this file is the GLUE -- it depends on
the Pi runtime (pi.on, pi.registerTool, ctx.sendRaw) and is verified end-to-end at
L2 (Task D4), NOT unit-tested here. A fake-Pi runtime mock (cross-cutting
follow-up) would let it run in CI.
PRESERVED VERBATIM (load-bearing runtime glue, spec D4):
- anti-bypass tool_call hook: FORBIDDEN (nsp phase advance|reopen, decision add,
cte plan) + PROTECTED_FILES (review_decisions.jsonl, session_manifest.yaml,
cte_plan.json)
- input lock + the input hook: /nuova-domanda|/riprendi-sessione entry detection,
free-input block, the `!`-prefixed steer channel
- before_agent_start kickoff injection + the two kickoff payloads (model prose)
- agent_end prose safety net (nudges the model back to reviewer_* tools)
- session_start state reset
- exit-code contracts with the CLI (5 = gate refusal, 6 = needs human,
7 = not-ready silent no-op)
- textResult / nsp() / relayIfNspFails / advanceIfReady helpers
TWO CORRECTIVE CHANGES vs source:
1. F2 single source: workflow facts (max_phase, phase names, schema-linking phase)
come from `nsp phase meta --json`, NOT from JS-mirrored constants. The source's
PHASE_NAMES array (truncated to 7) is gone; F8/datamart can no longer drift.
2. D2/D4 widget-descriptor: reviewer interaction is emitted as a widget-descriptor
(built by ./gate/builders.js) and awaited by id via emitAndWait + the
extension_ui_response dispatcher. This replaces the source's blocking native TUI
primitives (ctx.ui.select/custom) and introduces the correlation-by-id layer
ChironeWp3 never had.
Four tools wired: reviewer_select, reviewer_decide (persists via nsp decision add),
reviewer_confirm (gate; privileged action on approve), rewrite_question. Plus the
/torna slash command for rollback. No-limbo invariant preserved: cancel/undefined
re-presents the widget; real escapes are always in the descriptor's reserved field.