ADR-022: Open points of guided processes
Status: Proposed
Date: 2026-10-09
Context
A guided process proposes one change at a time on an approval card with three answers: Übernehmen, Andere Variante and Überspringen (ADR-018). A skipped proposal is a finding the editor did not want now, not one that is wrong. nr-llm ADR-214 (item 9, "Open points persist in nr_mcp_agent") has the chat keep such findings beyond the conversation and offer them again the next time the process runs on the record, and it fixes the rules:
- Recorded server-side in exactly one case: the editor answers a card with Überspringen. No tool records one, because a model call cannot guarantee that the record follows the card decision.
- Keyed by the process skill, the subject record and the target record and
fields of the pending write, as nr-llm's
PendingCallView::$pendingTargetnames them. The preview's prose is never parsed. - Closed only by an applied write to the same record and field: a card the
chat approved whose call came back with its write step stating
COMPLETEand no hook failure after the write.
The parts of the key come from three changes: the card's answers and the denial reason (ADR-018), the conversation's skill and page (ADR-019), and the chat's tools (ADR-020).
Decision
Recording. The worker reads the card before it hands the decision to
nr-llm, from the view whose turn digest is the decision's, so the key is
what the card showed. Once approve() has accepted a denial with the
reason skip, OpenPointTracker stores one row per field of the target
in tx_nrmcpagent_open_point: process skill uid, subject record, target
table, uid and field. A write that names a record but no field (a move, a
delete) is one row with an empty field. Nothing is recorded when:
- the reason is
variant(a new proposal follows), or the denial has none; - the pending call names no target — it creates its record, or its tool does not say — or the card holds more than one call;
- the conversation has no known skill uid or page;
- nr-llm refuses the decision and hands the card back, or the continuation fails;
- a proposal is withdrawn by a new message, or the run is cancelled.
A skip that reaches the worker always comes from a card that offered it: the request turns a reason sent for any other card into a plain denial (ADR-018).
The subject is the conversation's page. nr-llm ADR-214 names the invocation's subject record; nr-llm has no invocation API yet, so the chat uses the page the conversation is about (ADR-019).
Closing. After an approved decision, AppliedWrite reads the result
nr-llm returned for it: the first tool step is the approved call, and the
write step that follows it must state WriteCompleteness::COMPLETE with
hookFailedAfterWrite not set. Then every open point on the card's target
record and fields is closed, whichever process recorded it, by deleting the
row: the table holds what is open. A partial write, a hook failure after
the write, a failed call, a call without a write step and a write that
states no completeness close nothing.
Offered again. A conversation with a skill and a page gets the open
points of that skill on that page in its system prompt, as record
identities and field names (at most 20), and the read tool
chat_list_open_points lists a page's open points for a user who may show
the page, leaving out points on tables the user may not read. Neither holds
a value or the proposal's text.
Site-wide, for the dashboard. OpenPointVisibility::visibleForCurrentUser()
lists the open points the current backend user may see across the site,
newest first, the limit counted after the visibility rule. Each
VisibleOpenPoint carries the page, the target record's language, the
skill's uid and nr-llm's stored identifier, the target and the date, and a
sentence in the user's backend language, built from the TCA labels and the
page title ("<Field> is missing on “<Page>”" for an empty field, "<Field>
open on “<Page>”" otherwise; the templates are the labels
openPoint.missing and openPoint.open), never model text. The visibility rule also leaves out a point on a field the
user's groups may not edit (exclude without non_exclude_fields),
for every reader.
Process runs. SkillProcessRunDetector implements
ProcessRunDetectorInterface (ADR-018): a conversation with a skill runs
a guided process unless nr-llm marks the skill as plain
(Skill::isProcess()). With it, a write in a skill conversation gets the
three answers, and a new message withdraws a waiting run of that
conversation (ADR-018).
Consequences
- New table
tx_nrmcpagent_open_point: run the database analyzer after upgrading. - Needs nr-llm 0.41. The card's target (
pendingTarget, nr-llm PR 1024) and the write's completeness (writeCompleteness,hookFailedAfterWrite, nr-llm PR 1023) come from nr-llm 0.41. - A finding lost to a withdrawn proposal, a refused decision or a failed continuation is found again the next time the process runs on the record (nr-llm ADR-214).
- An open point stays when its target record is deleted outside the chat; it is closed only by an applied write.
- The subject becomes the invocation's subject record once nr-llm can start a run with an invocation; the key does not change.
Amendment of 2026-10-23: Behalten closes
Context. Überspringen means "not now" and records an open point. When the current content is right, the editor had no answer that settled the point: Überspringen kept offering it, and Übernehmen would have changed it.
Decision. A third denial, Behalten (DenyReason::Keep), is the
decision that the current content stays. Once approve() has accepted
the denial, OpenPointTracker::settle() closes every open point on the
card's target record and fields (OpenPointRepository::close(), whoever
recorded them) and records none. Without a target nothing is closed, as for
every other answer. To nr-llm the denial is ApprovalDenialReason::KEEP
(SKIP before 2026-10-24, see ADR-023), so the model moves on to the next point.
Consequences. Behalten is the second way a point is closed, beside an
applied write. A proposal answered with it is a ProposalOutcome::Kept
and is listed under Behalten in the tour summary.