Learned preferences
GET / POST /v1/preferences — list, inspect, approve, reject, and deprecate preferences mined from the user's conversation.
The preference-evolution system mines durable user preferences from conversation (e.g. "I never trade meme coins", "reply concisely", "prefer on-chain data over K-line") and stages them through a state machine before they take effect.
M1 shipped read-only endpoints; M2 added the approve / reject
/ deprecate mutations + the total field on the list endpoint; M3
added the undo endpoint and tool-level enforcement of
hard_constraint rows.
M4 adds the bridge: every transition into active now writes the
preference through to the relevant downstream stores — user_tags
for hard constraints that name a valid 11-dimension tag, a memories
mirror for every personal_style row (so the style hint is FTS-
indexed), scenario-classifier boosts for behavioral_preference rows
targeting a scenario, and a methodology priority multiplier for rows
targeting a methodology id. Transitions out of active (deprecate /
reject) reverse each write. Bridge writes are best-effort and never
block a state transition; failures surface through logs with
learning/preference-bridge. See the
Self-improving system feature
page for the full write-path diagram.
Every preference has three classifying axes:
kind— one ofpersonal_style,behavioral_preference,hard_constraint.dimension— one ofrisk,chain,asset_class,timing,methodology,style,constraint.state— one ofproposed,active,deprecated,rejected.
See the project plan at ~/.claude/plans/autoclaw-agent-agent-lazy-lollipop.md for the full lifecycle.
GET /v1/preferences
List preferences with optional filters.
| Method | GET |
| Path | /v1/preferences |
| Auth | Authorization: Bearer <token> required when GATEWAY_AUTH_TOKEN is set |
| Query | state, dimension, kind, limit (default 100, max 500) |
| Category | learning |
Response 200:
{
"ok": true,
"data": {
"items": [
{
"id": "pref_a1b2c3d4e5f6",
"user_id": "default",
"dimension": "asset_class",
"kind": "hard_constraint",
"statement": "Never trade meme coins",
"structured": { "operator": "exclude", "subjects": ["meme_coin"], "scope": ["buy", "swap"] },
"evidence": null,
"source": "manual",
"state": "active",
"confidence": 0.65,
"times_observed": 4,
"times_violated": 0,
"dedup_key": "asset_class:hard_constraint:never trade meme coins",
"bridges_user_tag": null,
"approved_at": "2026-04-15T08:00:00Z",
"last_ask_at": null,
"created_at": "2026-04-13T10:00:00Z",
"updated_at": "2026-04-15T08:00:00Z"
}
],
"count": 1,
"total": 1
}
}count is the number of rows in this page (≤ limit); total is the
number of rows matching the filters across the full table. When
count == limit && total > limit, narrow the filters or page through
with smaller limit values to see the rest.
Response 422: returned when state, dimension, kind, or limit is invalid.
GET /v1/preferences/:id
Fetch a single preference by id.
| Method | GET |
| Path | /v1/preferences/:id |
| Auth | Authorization: Bearer <token> required when GATEWAY_AUTH_TOKEN is set |
| Category | learning |
Response 200: same row shape as the items[] element above.
Response 404: returned when the id does not exist.
POST /v1/preferences/:id/approve
Promote a proposed row to active. Setting approved_at if not
already set. Equivalent to the REPL /preferences approve command
and minara preferences approve <id>.
| Method | POST |
| Path | /v1/preferences/:id/approve |
| Auth | Authorization: Bearer <token> required when GATEWAY_AUTH_TOKEN is set |
| Body | optional { "reason": "..." } (audit trail, ≤ 500 chars) |
| Category | learning |
Response 200: the updated preference row in the standard envelope.
Response 404: id not found.
Response 409: invalid transition (e.g. row is already rejected)
OR another writer beat this caller (CAS lost — re-GET to see the
current state).
POST /v1/preferences/:id/reject
Promote proposed (or active, when retiring an existing
preference) → rejected. Same shape as approve above; rejected
is terminal — the row is preserved as an audit trail but never
re-surfaced.
| Method | POST |
| Path | /v1/preferences/:id/reject |
| Auth | Authorization: Bearer <token> required when GATEWAY_AUTH_TOKEN is set |
| Body | optional { "reason": "..." } |
| Category | learning |
POST /v1/preferences/:id/deprecate
Promote active → deprecated. The preference stops being
injected into the system prompt but the row is preserved (so the
similarity gate can still detect re-proposals as duplicates and
skip re-asking). Use reject if you want the row to never come
back as a similar-statement match.
| Method | POST |
| Path | /v1/preferences/:id/deprecate |
| Auth | Authorization: Bearer <token> required when GATEWAY_AUTH_TOKEN is set |
| Body | optional { "reason": "..." } |
| Category | learning |
POST /v1/preferences/:id/undo
Back out a strong-signal auto-activated hard_constraint within
the configured undo window (default 24h, see
PREFERENCE_HARD_UNDO_WINDOW_HOURS). The row transitions to
deprecated, tool-level enforcement stops immediately, and the
audit trail (auto_activated_at timestamp, original statement)
is preserved. M3-introduced.
Use deprecate instead when either (a) the preference was
approved manually (so auto_activated_at is null) or (b) the
window has expired.
| Method | POST |
| Path | /v1/preferences/:id/undo |
| Auth | Authorization: Bearer <token> required when GATEWAY_AUTH_TOKEN is set |
| Body | optional { "reason": "..." } (≤ 500 chars) |
| Category | learning |
Response 200 — row deprecated.
Response 404 — id not found.
Response 410 Gone — row is auto-activated but the undo window
expired. Use deprecate instead.
Response 413 — body > 4 KB.
Response 422 — row was approved manually (no
auto_activated_at). Use deprecate instead.
Response 409 — CAS lost (another writer changed the row
first) or invalid structural transition.