Open order matching

How a tenant repair text should find the right open work order when the reply writer runs on Opus 5.5, and what to do about the filing thresholds. Design only: no code has changed.

Nov 1, 2026 · row open-order-matching · sources: tenant comms hand-off, the four cases. Ids and counts only.

The answer in five lines

  1. Fix the list first. Production and the bench do not agree on which orders are open, and that disagreement causes both reported symptoms. One shared "open for intake" rule, used by the live dispatcher and by the bench door.
  2. Then check the match in code. Before a report is placed on an order, run the subject check that already guards the note tool. A mismatch files a new order. It never asks the tenant.
  3. Tighten the subject check for Case 4. "Water" alone must not tie an indoor AC leak to an outdoor spigot order.
  4. Keep the classifier on Sonnet and leave its thresholds alone until steps 1 to 3 are proven. The Opus filing gap is mostly asks, and asks are noise.
  5. Prove it on the bench. Re-run the G3 filing row through the door with production as arm 1 in the same hour, plus Cases 2 to 4 as fixtures, plus a 30-day read-only replay of every placement the new check would flip.
Full design: counts, causes, options, proof, slices

The problem, in counts

FactNumberSource
Work orders with no status2,276 of 4,114hand-off, Data gaps row
Stale open orders per tenant at the larger customer8 to 43hand-off, Filing not at parity row
G3 filing, 20 full-mode texts: add to open / new / ask first / reply onlydecomposed on Opus 5 / 3 / 4 / 8 · production 8 / 5 / 0 / 7hand-off, G3 filing row
Filing after prompt rules 3c to 3e and code check 3ffell from 25 to between 19 and 22 of 42hand-off, Match before filing row
Duplicates on Opus vs today, G3 261 episodes11% vs 5%hand-off, G3 row
Bench work orders shown with today's status, not the status that day64 of 170hand-off, slice H caveats

The four cases (detail and Fede's questions: tracker #repair-review-cases)

CaseOrder Clara usedOrder the office usedWhat went wrong
1 · stove after roachesnew order, plus a note on LAMP-18152new order LAMP-18184Nothing. The labeler counted it as a duplicate; the office made the same call Clara did.
2 · washer and dryerLOWR-17614 (air conditioner)none filedWrong attach. An older dryer order, LOWR-17113, existed. The tracker lists its status as unknown, which fits the no-status rows, and it was not in the list the bench showed Clara.
3 · kitchen extractor plus pestspests on WOOD-17721 (right); extractor on WOOD-17489 (bathroom faucet, wrong)pests scheduled; no hood orderWrong attach. An older hood order, WOOD-15393, existed. The tracker lists its status as unknown, and it was not in the bench's list.
4 · indoor AC leakLAMP-17337 (outdoor spigot)new order LAMP-17787Wrong attach. The only open order was a water leak of a different kind.

Three causes

(a) Two definitions of "open"

Production builds the list in the dispatcher, in both the text lane and the email lane, from getWorkOrdersByPersonId filtered by !WO_CLOSED_STATUSES.includes(status). A row with no status is not closed, so it counts as open. That is where 8 to 43 stale orders per tenant come from: each one is offered as a place to attach.

The bench door (src/lib/domain/clara-bench/door/load.ts, openAt) counts an order as open only if its status is active, or if it was closed after the text. A row with no status is dropped unless something touched it after the text. That is why the dryer order in Case 2 and the hood order in Case 3 were missing from "the list Clara saw".

So the two symptoms come from one set of rows. On the bench those rows are invisible, which produced the wrong attaches in Cases 2 and 3. In production they are always visible, which produces the stale list. Neither behaviour is what we want, and each makes the other's measurements unreliable: a bench filing number is a number about a list that production never shows.

(b) No subject check where the placement is decided

resolveIntakeDecision (in intake-classify.ts) accepts any existing pick whose id is in the list. With one open order it places the report on that order at any confidence, and with two or more it does so from 0.6 up (and below 0.6 too, under the under-split bias). Nothing checks that the report and the order are about the same thing. The subject check from #22383, reportMatchesWorkOrder, guards only the note tool. It runs after the placement is decided and reads the note text.

Even where it runs, it passes Case 4. "Water" and "leak" count as a shared plumbing subject, so an indoor AC leak matches an outdoor spigot order. The spigot is not in its vocabulary at all.

(c) Thresholds set for Sonnet

The ask and new-when-open thresholds (0.6 and 0.8) and the bias toward fewer orders were locked by the owner on 2026-06-10 for a Sonnet classifier. The classifier still runs on AGENT_MODEL: the code default is claude-sonnet-4-6, and the live value is set by environment. On the 20 texts, the decomposed version on Opus asked 4 times where production asked 0. Fede counts noise as the main failure, so an extra ask is a loss even when the order it leads to is right.

Options

A · One "open for intake" rule, shared by production and the bench recommended, part 1

B · Check the subject before placing recommended, part 2

C · Show history orders to the classifier, marked as history only if Fede says so on Case 2 or 3

D · Filing thresholds on Opus

OptionWhat it meansVerdict
D1 · classifier stays on Sonnet, thresholds unchangedOnly the writer moves to Opus. The classifier and its 0.6 / 0.8 thresholds stay where they were tuned.recommended now This matches the hand-off's G3 conclusion. It needs no new owner ruling.
D2 · re-tune the thresholds for an Opus classifierSweep 0.6 / 0.8 on the 20 full-mode texts and the 261 G3 episodes, after A and B. Pick the setting that makes fewest asks without adding duplicates.later, once A and B are proven It changes an owner-locked value, so it is Fede's call, presented with the sweep's numbers.
D3 · lower the thresholds now so Opus files moreShrinks the gap with more placements on a list that is still stale.no It would add wrong attaches (cause a) to close a gap that is mostly asks.

How it is proven

PartProofPass bar
AA pure test of the predicate over every status case, with a test that fails on the old filter (a no-status order from long ago is offered). A read-only census: per tenant, open counts before and after, as a distribution (not a mean), over every company with mirrored orders. The same census counts how many no-status rows have a later tenant message tied to them. That number shows which way the window errs: an order that is really open in the PMS but outside the window would drop out, and the tenant's next "still broken" would file a duplicate. Re-run the G3 filing row through the door, production as arm 1 in the same hour.No active order drops out of the list. Stale counts fall. Few no-status rows outside the window have a recent tied message, and every one that does is listed. A filing row exists for both arms. The day0b compare shows 0 differing cells, or a ledger entry for each one.
BCases 2, 3 and 4 as synthetic fixtures (fixture ids only, same structure, no real text), each failing on main. A 30-day read-only replay of every production existing placement through the new check, with every flip listed and labelled: right split, or a false split we then fix. Maintenance evals, plus a multi-turn case that replays one case's full conversation. A bench row with production in the same hour and the Spanish slice.Cases 2 to 4 file a new order. Case 1 is unchanged. No real flip is left unexplained. Asks do not rise.
LiveOne Willows run (org_sandbox, appfolio-45, existing test tenant, robot number, no new tenants): a different-problem text while an unrelated order is open.A new order is created in the PMS, the old order gets no note, and the record ids are cited in the PR.

The day0b replay grids hold decisions, so a clean compare there does not prove a filing change. The filing row and the placement replay above are the proof for this work.

Slices (each one branched off main, each one working on its own)

  1. Part 1: the shared open-for-intake predicate, the two dispatcher filters and the door's openAt, plus the census. Raise it on the bridge first if the door counts as 001's.
  2. Part 2a: the vocabulary fix for Case 4 in report-matches-work-order.ts. Small; can ship at once.
  3. Part 2b: the resolver calls the matcher, and the classifier writes a subject on every issue. After #22381 lands.
  4. Separate lane: the PMS sync status gap (the 2,276 rows).
  5. Later: D2 threshold sweep, and C only if Fede's Case 2 or 3 answer asks for it.

For Fede

Four questions, one per case, each with options and a recommendation. The driver raises them. None of the slices above waits on the answers, except option C.

PropFlow Docs