Files
ratatoskr/persistent-memory.d/2026-07-19-worldtree-sdk-cutover-slice-5-complete.md
T
vh 5bc39a092e memory: /snapshot — worldtree-sdk cutover slice-5 complete (deab762→4e20030)
Slice-5 (characters + me/capabilities/models) done through the full House Code
Discipline, tags v0.21.16–.18, suite 488 green, live-smoke-proven on :8081/b128,
both heid gates cleared. Current state / in-flight advanced to slice-6 (admin) next;
Recent-decisions index entry + detail file added; substrate at v0.21.18.

persistent-memory.md stays ~333 lines (over the ~300 soft cap): the length is
dominated by the non-archivable Current state / in-flight block plus <30-day July
entries (guarded), so archival can't reach the 250 target — left as-is per the
stop-where-the-guards-stop rule.
2026-07-19 11:34:10 -07:00

7.0 KiB
Raw Blame History

worldtree-sdk cutover — SLICE-5 COMPLETE (characters + me/capabilities/models)

[2026-07-19] Slice 5 of 7 of the worldtree-sdk consumer cutover (issue #20; contract docs/contracts/worldtree_sdk_cutover.contract.md). Full House Code Discipline end-to-end: contract slice-notes → TDD → LIVE smoke → heid-code-review → fixup → heid-bug-hunt → fixup. Both heid panels cleared. Suite 488 green.

Commits (tags v0.21.16.18, on main, not-yet-pushed)

  • deab762 feat — the six routes onto ratatoskr.wt, hand-rolled deleted.
  • d86d6df fix — heid-code-review fixups (CLI presenter degrade-not-crash).
  • 4e20030 fix — heid-bug-hunt fixups (CLI open-world container-type hardening).

What migrated

get_me / get_capabilities / list_character_models / create_character / get_character_state / delete_character moved off the hand-rolled httpx wrappers onto client.me.get() / client.capabilities.get() / client.models.available_for_characters() / client.characters.create|state|delete. All six are open-world reads/acks returned verbatim; none carries a discriminated SDK error, so each maps any ApiError → the SessionApiFailed default — NO new § Error map rows (exact parity with the retired path, which never discriminated a status/code on these routes).

CLI-only rewire--whoami (me + capabilities) and --characters (models → create → state → delete) build a wt.build_client over the injected _probe_client transport and catch wt.SessionApiFailed + ConnectFailed. No web-server caller for any of these six routes.

Deleted the six hand-rolled sessions.py wrappers (net 5 mypy no-any-return errors); endpoint_for_plane + get_session_bifrost (slice-6) + the exception classes stay. Retired the matching test_sessions.py classes (TestGetMe, TestGetCapabilities, TestTransientCharacters); kept TestEndpointForPlane + TestGetSessionBifrost.

Decisions made at TDD (contract § slice-5 notes)

  • create_character omits state when None — SDK-idiomatic inline literal (per branch, to type-check against the SDK's CreateCharacterInput TypedDict without importing its private _types); server-equivalent to the retired explicit state: null (Worldtree's field defaults None either way). The only wire-shape change; the sole call-site never sets state.
  • delete_character returns the SDK's open ACK verbatim (-> Mapping | None, not normalized to the hand-rolled None; 204 → None). The CLI ignores it.

LIVE SMOKE (:8081/b128, WORLDTREE_API_KEY, INV-CUT-5 / DEC-4 cleared)

Drove both probes end-to-end through the CLI (python -c "from ratatoskr.cli import main; main([...])" — the ratatoskr console script isn't on PATH here; python -m ratatoskr.cli imports without calling main, no __main__ guard). --whoami rendered real identity (user_id ratatoskr, tier user, scopes incl. character.*, key_id c990f0be) + ephemeral_template echo. --characters drove the full lifecycle: char-rp catalog → created char_…state pad=[0.234,0.136,0.065] read-back → deleted. Observed real success, not merely non-crash.

heid-code-review (thread 01KXXRN50K…) — 2 fixups

Panel: Gróa + Regin zero (adapter/route-map/error-map faithful); Hulda flagged 2 source-confirmed CLI open-world-presenter crash holes + a live-smoke test-gap. Both holes fixed (the null/element layer):

  • _format_whoami scopes: ', '.join(me.get('scopes', [])) crashes on a present-null scopes (.get(k, []) returns None, not the default) or a non-string element. The contract names _format_whoami the degrade-not-crash exemplar — the cited exemplar had an un-hardened line (allowed_roles was hardened in slice-4, scopes was not).
  • _characters_probe model items: the slice-5 or [] guarded the list-level null but not each entry ([None]/["x"]/[{"name":123}]). Test-gap (live-smoke not in the file set) → accept (it WAS run + recorded).

heid-bug-hunt (thread 01KXXS9S45…) — 3 fixups + 1 accept + 1 dismiss

Cold spec-free diff-scoped panel over the post-code-review-fixup diff. Adapter + route-map + ConnectFailed-at-call-sites sound against the declared invariants (all arms agree). 4 real findings, all CLI open-world paths — the container-type layer BELOW the null/element holes the code-review had just fixed (the code comments cite the CR; the two consults were firewalled from each other and converged independently):

  • [bug, fixed] non-iterable scopes/allowed_roles{"scopes": 123}123 or [] == 123for s in 123 TypeError. New _display_seq(value) helper degrades any non-list (scalar / bare string / null / absent) to empty; applied to both.
  • [bug, fixed] non-iterable items{"items": 123}, same class. Guard models is a Mapping AND items is a list before iterating.
  • [robustness, fixed] non-mapping top-level created/state — a non-mapping SDK passthrough (created=[...]) → .get AttributeError. isinstance(_, Mapping) guard → clean exit-20 abort / pad=None.
  • [robustness, ACCEPTED] the probe leaks its transient character on a mid-lifecycle failure — create → state → delete linear, no finally. PRE-EXISTING (retired probe had the identical structure — "cutover did not worsen it"), TTL-bounded, one-shot diagnostic; a try/finally would swallow a happy-path delete-failure (delete is both teardown and a tested step). Gróa + Heid concur accept is defensible. Documented in contract § slice-5 notes.
  • [DISMISSED] sessions.py dropped get_me/etc. (Hulda, caller-contract) — the intended DEC-3 no-backwards-compat migration (all in-repo callers rewired same-diff); Heid labels it intended-surface-change, not a defect.

Regin BH calibration note: Regin (glm-5.2 non-reasoning) found only the leak, missed the 3 crash paths, and self-graded "0 confirmed" on a control-flow-guaranteed finding — consistent with the crystallized Regin-unreliable-on-bug-hunts pattern (its code-review work this session was reliable). Gróa was the BH standout.

The cumulative lesson (foot-gun for slices 6-7)

Open-world SDK reads need degrade-not-crash guarding at THREE levels, and the two heid lenses caught different ones: the CODE-REVIEW (conformance) caught the null/element layer; the cold BUG-HUNT (robustness) caught the container-type layer below it. Run BOTH — they are complementary, not redundant. The three levels:

  1. container-type — the field value may be a truthy non-iterable scalar (123) or a bare string; or [] only catches null/absent. Guard isinstance(_, (list, tuple)).
  2. element-type — each entry may be a non-mapping; guard isinstance(m, dict).
  3. top-level-mapping — the whole read may be a non-mapping passthrough; guard isinstance(_, Mapping) before any .get.

See also 2026-07-19-worldtree-sdk-cutover-slice-4-complete (the slice-4 arc + the -m double-module class-identity foot-gun).