Skip to content

fix(operator-queue): an empty options list or a numeric title no longer reads as a rewrite (#3314) - #3414

Merged
vybe merged 3 commits into
devfrom
feature/3314-queue-fingerprint-falsy
Oct 9, 2026
Merged

vybe merged 3 commits into
devfrom
feature/3314-queue-fingerprint-falsy

Conversation

@vybe

@vybe vybe commented Oct 8, 2026

Copy link
Copy Markdown
Contributor

Fixes #3314 — independent fix, one of ten small bug fixes in the trinity-pm chain-easy-1008 run. Base is dev. Draft until the operator merges. The fix for #3385 (bounding operator_queue.type) is stacked on this branch.

What

  • src/backend/services/operator_queue_service.py
    • _canonical_options: any falsy options ([], "", {}, 0, False, None) canonicalises to empty, the way db/operator_queue.py stores it (NULL). An entry written with options: [] no longer reads as changed:options, so answers are no longer refused with 409 item_diverged.
    • New _scalar_text: a numeric or boolean title / question is turned into text in _clamp_ingested_item and in _entry_content, so what is stored and what is fingerprinted agree on SQLite and PostgreSQL.
  • db/operator_queue.py is untouched.
  • Tests: the strict-xfail markers on r88–r91 are removed, a boolean-title case is added, and the real-SQLite round-trip property test now covers falsy options and non-string titles and questions.

Rulings carried (orchestrator, on the operator's behalf — plan file)

Review + security

/review reading pass (claude-fable-5-1, report-only): MERGEABLE, no critical findings. For every value the ingest path accepts, the stored row and the fingerprint agree on both databases; real rewrites of options, title and question are still detected; the 409 item_diverged path and the #3243 title hold are not in the diff. _scalar_text stringifies only bool, int and float, and its output is still clamped. /cso --diff: clean.

Evidence sweep (claude-opus-5-5): GREEN. 56 test files reference the operator queue: 156 passed on the two changed files (shuffled, seed 12345) and 1,641 passed on the other 54, run one file at a time because several take 2–4 minutes each. With the fix reverted, six regression tests go red, including both tests the issue names. git merge-tree against dev f6bedcd is clean.

Tests

Targeted counts above. Not run here: the full unit island (CI).

Before merge

  • Rows stored before this fix: an ask whose title or question was a boolean, or a float that SQLite renders differently from Python, stays flagged as rewritten on both SQLite and PostgreSQL. Integer titles recover. No migration. (The commit message says this for SQLite booleans only; this paragraph is the complete statement — review I1.)
  • Open PR feat(operator-queue): an agent sees its pending asks and can replace one (#3247) #3256 (replace a pending ask) touches the operator queue; a git merge-tree of the two heads is clean.
  • Deliberately not detected as rewrites, because they store and display identically: [] ↔ "" ↔ None, and 123 ↔ "123".

Handoffs

🤖 Generated with Claude Code

Trinity Agent (trinity) and others added 2 commits October 8, 2026 18:54
…d as a rewrite (#3314)

The #2915 sync fingerprint must reproduce what ingest stored. Two seams
diverged, so an untouched agent entry read SYNC_CHANGED and every operator
answer was refused 409 item_diverged:

- options: the store keeps a falsy value ([], "", {}, 0, False) as NULL,
  but `_canonical_options` serialised it ("[]"). It now canonicalises any
  falsy value like None, mirroring the store's truthiness.
- title / question: a JSON number or bool passed the clamp untouched and
  was stored verbatim (read back backend-dependently: SQLite True -> "1",
  PG "true"), while `_entry_content` fingerprinted it as the default. A new
  `_scalar_text` stores it as its text ("123", "True") in the clamp and
  mirrors that in `_entry_content`, so both sides match on every backend.

Not covered: no number conversion before the #3243 title cap check (a
numeric title is still never cap-held; scope kept narrow). `type`
handling untouched (#3385).

Stale rows: asks stored before this fix with a boolean title hold "1" on
SQLite and stay flagged as diverged; integer-titled rows recover on the
next sync. No migration.

Tests: the strict xfails r88-r91 in test_ec_operator_queue_edges.py are
removed and r90b (boolean title) added. With the fix absent all five go
red: r88/r89 `['options'] == []`, r90/r90b `['title'] == []`,
r91 `['question'] == []`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…sy options and non-string titles (#3314)

The Tier-2 real-SQLite property (entry -> clamp -> store -> sync read ->
changed_fields == []) excluded the #3314 inputs while they were strict
xfails. It now generates falsy options ([], "", {}, 0, False) and
int/bool/float(nan, inf)/[]/{} titles and questions, with explicit
examples for `options: []` and `title: 123`. This is the parity guard for
the fingerprint mirroring the store's options truthiness. Only the null
`type` exclusion (#3315/#3385) remains.

With the #3314 service fix reverted the property goes red
(`['title', 'question'] == []` on the explicit examples).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…3314)

merge-train: mechanical, per the merge-train note on the PR. `_scalar_text(0)`
returned "0", but `create_item` stores the `or`-default ("Agent request") for a
falsy title/question, and every row ingested before this fix carries that
default — so the entry fingerprint "0" diverged from the row and flipped such
rows to 409 `item_diverged` on the next sync. Only truthy scalars are
stringified now; a 0/False title keeps displaying the default as before.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@vybe

vybe commented Oct 9, 2026

Copy link
Copy Markdown
Contributor Author

merge-train: pushed bffddc0 to this branch — mechanical fix from validation. _scalar_text stringified falsy scalars (0, 0.0, False) to "0", but create_item stores the or-default ("Agent request" / "(no details provided)") for them, and every pre-fix row carries that default too — so after deploy the entry fingerprint read "0" against a row saying "Agent request" and flipped it to 409 item_diverged on the next sync (reproduced by the validator: title=0/False/0.0 → dev=[] pr=['title','question']). Guarded with and value: integer titles still heal, options-only rows are unaffected, a 0 title keeps displaying the default as it does today. Correction to the body's "Before merge" paragraph: with this guard no pre-deploy row changes verdict. Local: test_ec_operator_queue_{edges,properties} + test_2915_operator_queue_sync_honesty → 241 passed, 7 xfailed.

@trinity-ability trinity-ability left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

merge-train: batch validated on train/20261009-0735 (#3424)

@vybe
vybe merged commit a461789 into dev Oct 9, 2026
23 checks passed
vybe added a commit that referenced this pull request Oct 9, 2026
…t the database (#3385) (#3421)

Fixes #3385 — one of ten small bug fixes in the trinity-pm `chain-easy-1008` run. **Stacked on #3414** (the fix for #3314): base is `feature/3314-queue-fingerprint-falsy`, because both issues edit `_clamp_ingested_item` and the fingerprint helpers. Merge #3414 first, then retarget this to `dev`. **Draft until the operator merges.**

## What
- `src/backend/services/operator_queue_service.py`: new `OPERATOR_QUEUE_TYPE_MAX = 64` and `_bounded_type`. An agent-written `type` longer than 64 characters is shortened with the existing `…[truncated]` marker at ingest (`_clamp_ingested_item`); the ask is never refused or lost. `_comparable_type` applies the same bound, so a shortened row does not read as a rewrite (#2915) and a row stored before this fix at full length still compares equal.
- `src/backend/db/operator_queue.py::_insert_values`: `_DB_BELT_TYPE_MAX_BYTES = 1024`; above it the insert raises `ValueError`, like the id / title / question / context belts. All three create paths go through it.
- Docs: five lines in `architecture/api-endpoints.md`, `feature-flows/operating-room.md` and `requirements/security.md` that list the clamp and belt fields now name `type`.
- Tests in `tests/unit/test_1632_operator_queue_caps.py`: an oversized type is shortened with the marker; every platform-emitted type and every type `ask_operator` accepts is unchanged; the belt rejects an oversized type and passes 64 four-byte characters; `test_3385_clamped_type_is_not_a_rewrite` (clamped row vs raw entry, legacy full-length row, a real rewrite inside the first 52 characters).

## Rulings carried (orchestrator, on the operator's behalf — plan file)
- T1: a fixed constant, no environment variable — as recommended.
- T2: 64 characters — as recommended. The longest type the platform emits is `workspace_problem_report`, 24.
- T3: the DB belt raises, like its siblings — as recommended.
- Orchestrator: stacked on #3314 instead of branching from `dev`, so the two fixes do not hand the operator a conflict in the same two functions.

## Review + security
`/review` reading pass (claude-fable-5-1, report-only, against the stacked base): **MERGEABLE**, zero critical. Census of every literal `type` the platform and the enterprise submodule pass to the insert paths: longest is 24 characters, none is altered. Every truncated value ends in the marker, which no set member can match, so truncation cannot move a value into or out of a set that decides control flow (`_BUDGETED_ALERT_TYPES` and neighbours). The new `ValueError` is reachable only from the file sync loop, which quarantines it under its existing handler. `/cso --diff`: no findings; the error text carries the constant, never the value.

Fixed after review: `eab91c10` — I1, the five doc lines. Noted, not changed: I2, a non-string `type` over 1 KiB now raises the named `ValueError` where the base failed at the bind — same quarantine outcome.

Evidence sweep after the fix (claude-opus-5-5): **GREEN**. 13 operator-queue files ran shuffled (seed 12345), one file per process: 836 passed, 7 xfailed. With both production files taken from the #3314 branch, the two `TestClampType` cases, the belt test and the clamped-row fingerprint case go red. `git merge-tree` against `dev` f6bedcd is clean; the base branch has not moved (`923b1edc`).

## Tests
Counts above. Not run here: `test_ent815_queue_walk`, `test_ent815_broad_list_agent_scope`, `test_ent751_gate_entries` (2–4 minutes each; they ran green on the #3314 base in that PR's sweep) and the full unit island.

## Before merge
- **This PR's checks are weaker than a dev-based PR's:** a PR whose base is a feature branch runs a reduced CI set and `backend-unit-test` does not run. The first full run happens when it retargets to `dev` after #3414 merges — read that run before merging.
- Accepted trade-off: an agent that rewrites an oversized type only after character 52 is not detected as a change. Titles and questions already behave this way.
- Thirteen open PRs share a file with this branch (mostly `tests/registry.json` and `docs/memory/architecture/api-endpoints.md`; #3256 also `db/operator_queue.py` and the three docs). A test-merge of this head against each of #3420, #3417, #3412, #3411, #3407, #3304, #3271, #3256, #2984, #2956, #2713 and #2709 adds no conflict beyond what that PR already has with `dev`.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
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.

2 participants