Skip to content

Decisions app: no Other or free-text escape on select decisions - #2412

Merged
jaylfc merged 3 commits into
devfrom
exec/tsk-4vutsd
Aug 16, 2026
Merged

Decisions app: no Other or free-text escape on select decisions#2412
jaylfc merged 3 commits into
devfrom
exec/tsk-4vutsd

Conversation

@jaylfc

@jaylfc jaylfc commented Aug 14, 2026

Copy link
Copy Markdown
Owner

CARD TITLE (intent, not commit subject): Decisions app: no Other or free-text escape on select decisions

Autonomous build of board card tsk-4vutsd.

single_select and multi_select decisions now always include an Other option
with a free-text field. The backend accepts free-text answers flagged via
other_value while keeping strict option validation for declared choices.
Answers are stored with other_value and note so the agent receives free-text
responses in the same shape as option answers, with notes appended to the
routed message.

Files:
desktop/src/apps/DecisionsApp.test.tsx | 82 ++++++++++++++++
desktop/src/apps/DecisionsApp.tsx | 98 ++++++++++++++++--
tests/test_routes_decisions.py | 169 ++++++++++++++++++++++++++++++++
tests/test_routes_decisions_agent.py | 58 +++++++++++
tinyagentos/decisions/decision_store.py | 4 +-
tinyagentos/routes/decisions.py | 163 +++++++++++++++++++++---------
6 files changed, 516 insertions(+), 58 deletions(-)

… select decisions

single_select and multi_select decisions now always include an Other option
with a free-text field. The backend accepts free-text answers flagged via
other_value while keeping strict option validation for declared choices.
Answers are stored with other_value and note so the agent receives free-text
responses in the same shape as option answers, with notes appended to the
routed message.
@qodo-code-review

Copy link
Copy Markdown

ⓘ Qodo reviews are paused because your trial has ended. Ask your workspace admin to add credits to resume reviews. Manage billing

@coderabbitai

coderabbitai Bot commented Aug 14, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@jaylfc, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 17 minutes

Limit details: You’ve used all 2 included reviews currently available under your plan.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: b65e7612-f98e-41b6-ba97-0b9808b7c8b2

📥 Commits

Reviewing files that changed from the base of the PR and between abc01b3 and e57d243.

📒 Files selected for processing (8)
  • changelog.d/2412-decisions-other-free-text.md
  • desktop/src/apps/DecisionsApp.test.tsx
  • desktop/src/apps/DecisionsApp.tsx
  • docs/agent-coordination.md
  • tests/test_routes_decisions.py
  • tests/test_routes_decisions_agent.py
  • tinyagentos/decisions/decision_store.py
  • tinyagentos/routes/decisions.py

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@gitar-bot

gitar-bot Bot commented Aug 14, 2026

Copy link
Copy Markdown

Gitar is working

Gitar

for o in (existing.get("options") or [])
if o.get("value") is not None
}
if valid and any(v not in valid for v in vals):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CRITICAL: Missing TypeError guard in multi_select Other-path validation

When body.other_value is set for a multi_select decision, any(v not in valid for v in vals) raises TypeError if body.value contains an unhashable type (e.g. a dict or nested list), returning 500 instead of 400. The option-only path already wraps this in try/except TypeError, but the new Other-path branch does not.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

for o in (existing.get("options") or [])
if o.get("value") is not None
}
if valid and any(v not in valid for v in vals):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CRITICAL: Missing TypeError guard in multi_select Other-path validation (agent route)

Same issue as the human path at line 436: any(v not in valid for v in vals) raises TypeError on unhashable body.value items when other_value is provided, returning 500 instead of 400. The option-only path has the guard; the new Other-path branch does not.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

<Button
type="button"
disabled={submitting || multi.length === 0}
disabled={submitting || (multi.length === 0 && !otherSelected)}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: multi_select submit button enabled when Other selected but no text entered

The disabled condition multi.length === 0 && !otherSelected allows the button to be enabled when otherSelected is true but otherText is empty and no regular options are chosen, permitting an empty answer submission. The single_select path already disables submit when Other is selected without text.

Suggested change
disabled={submitting || (multi.length === 0 && !otherSelected)}
disabled={submitting || (multi.length === 0 && (!otherSelected || !otherText.trim()))}

Reply with @kilocode-bot fix it to have Kilo Code address this issue.

selected={singleSelected === opt.value}
disabled={submitting}
onClick={() => submit(opt.value)}
onClick={() => setSingleSelected(opt.value)}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: otherText not cleared when switching from Other to a regular option

When the user types in the Other field then selects a regular option, the stale otherText is still sent to onAnswer. The backend rejects the value + other_value combination for single_select with a 400. Clear otherText when switching away from Other so the submission reflects the current selection.

Suggested change
onClick={() => setSingleSelected(opt.value)}
onClick={() => { setSingleSelected(opt.value); setOtherText(""); }}

Reply with @kilocode-bot fix it to have Kilo Code address this issue.

@kilo-code-bot

kilo-code-bot Bot commented Aug 14, 2026

Copy link
Copy Markdown

Code Review Summary

Status: No Issues Found | Recommendation: Merge

Files Reviewed (9 files)
  • docs/agent-coordination.md
  • changelog.d/2412-decisions-other-free-text.md
  • AGENTS.md
  • CLAUDE.md
  • desktop/package.json
  • pyproject.toml
  • tinyagentos/__init__.py
  • CHANGELOG.md
  • changelog.d/* (14 deleted fragments)
Previous Review Summary (commit a8a3785)

Current summary above is authoritative. Previous snapshots are kept for context only.

Previous review (commit a8a3785)

Status: 4 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 2
WARNING 2
SUGGESTION 0
Issue Details (click to expand)

CRITICAL

File Line Issue
tinyagentos/routes/decisions.py 436 Missing TypeError guard in multi_select Other-path validation - unhashable values in body.value cause 500 instead of 400
tinyagentos/routes/decisions.py 754 Missing TypeError guard in multi_select Other-path validation (agent route) - unhashable values in body.value cause 500 instead of 400

WARNING

File Line Issue
desktop/src/apps/DecisionsApp.tsx 328 multi_select submit button enabled when Other selected but no text entered - allows submitting empty answer
desktop/src/apps/DecisionsApp.tsx 244 otherText not cleared when switching from Other to a regular option in single_select mode - stale otherText causes backend 400
Files Reviewed (6 files)
  • desktop/src/apps/DecisionsApp.test.tsx - 0 issues
  • desktop/src/apps/DecisionsApp.tsx - 2 issues
  • tests/test_routes_decisions.py - 0 issues
  • tests/test_routes_decisions_agent.py - 0 issues
  • tinyagentos/decisions/decision_store.py - 0 issues
  • tinyagentos/routes/decisions.py - 2 issues

Fix these issues in Kilo Cloud


Reviewed by step-3.7-flash · Input: 179.6K · Output: 50.7K · Cached: 5M

@jaylfc

jaylfc commented Aug 15, 2026

Copy link
Copy Markdown
Owner Author

nemotron-super review

VERDICT: No blocking issues found

  • No blocking issues found.

Automated first-pass review by the nemotron-super lane. The lead still reviews before merge.

jaylfc added 2 commits August 16, 2026 08:58
Both doc-gate layers fired legitimately: tinyagentos/routes/decisions.py
changed, and answering behaviour is user-visible.

The doc entry records the per-type rules (single_select rejects value plus
other_value; multi_select still validates every declared element and appends
the free-text one), the note field, and that the strict path is unchanged when
other_value is absent.

It also records two things the diff changes without stating: there is no
allow_other flag, so no decision author can enforce a closed option set, and
the agent mirror path gained the same escape, so an agent holding
decisions_write can now record a value outside the declared options. Both
verified against the source, and raised on the PR for a product call rather
than treated as blockers.
jaylfc added a commit that referenced this pull request Aug 16, 2026
…nteraction

Both doc-gate layers fired legitimately: routes/decisions.py changed, and what
an agent can read back is user-visible.

Recorded deliberately as a table, because the route now behaves three different
ways depending on grant shape, and the global case is the surprising one: a
global (null-project) grant returns null-project decisions ONLY, not every
project's. Anything built against the older wider behaviour will see fewer rows.

Also recorded that the filter-before-limit fix covers the global and
single-project paths but NOT the two-or-more-project path, which still fetches
500 rows and filters in Python afterwards. Stated as a narrower blast radius
rather than a closed issue, and raised on the PR.

Section inserted before 'Config save and restore' rather than at the end of the
file, so it cannot conflict with the section PR #2412 adds to the same doc.
@jaylfc

jaylfc commented Aug 16, 2026

Copy link
Copy Markdown
Owner Author

doc-gate cleared (both layers). Two things the diff changes without saying so.

First, to be clear I read the card the right way round: the title "no Other or free-text escape on select decisions" is the problem statement, so adding the escape is the fix. The diff matches the intent.

Validation on the new path is sound, and I checked it rather than assuming: single_select rejects value + other_value together with a 400, multi_select still validates every declared element against the option list before appending the free-text one, a non-list value is a 400, and the option-only path keeps the original strict check including the TypeError fail-closed. No arbitrary-value hole in the option handling.

1. There is no per-decision opt-out

grep -n "allow_other\|allowOther" tinyagentos/routes/decisions.py tinyagentos/*.py  ->  no matches

So the free-text path is available on every select decision. A decision author cannot declare a closed option set and have it enforced. The comment this PR replaces asserted the opposite invariant:

For select types, the answer must reference the declared options so a stale or malformed client cannot record an arbitrary value.

That invariant now holds only for callers who do not send other_value. Probably intended given the Decisions app is a human question channel, but it is a real narrowing of a stated guarantee and someone should decide it rather than inherit it.

2. The agent mirror path gained the same escape

other_value is handled at both call sites: line 418 in the human POST /api/decisions/{id}/answer, and line 738, which sits inside answer_decision_as_agent (POST /api/decisions/{id}/answer/agent, declared at 669).

So an agent holding decisions_write can now record arbitrary free text where it was previously constrained to the declared options. source is still derived server-side and cannot be spoofed, so in_app vs mirrored_from_chat stays trustworthy in the audit trail, but the value is no longer bounded by the option list.

Neither is a blocker and I have not treated them as one. Both are documented as they behave in docs/agent-coordination.md, so the doc is accurate either way. If you want a closed option set to be enforceable, that is an allow_other flag and a separate card, and I would rather ask than add it here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant