PMS conversations in Clara: architecture proposal

One conversation per person, whatever number or app they used. A read layer any property system can plug into. Clara reads first, later decides when to chime in, later writes back through the PMS like an employee.

Proposed — pending Fede's review  v1.9, 2026-10-19 (Oct 19 text-door decision section added; v1.8 of Oct 15: Reception named; 7a. Nothing falls through; decision 13, Gera, Oct 15; v1.7 of Oct 12 folded in) · replaces the 10-11 brief at this URL · paired with Opus 5.5 rounds 1 and 2 and a fresh Fable 5.1 third pass, all folded in · Oct 15: AppFolio claims audited against captures; unverified items marked (Fede's prove-it rule) · internal

Oct 22: first-reply prompt, inspected Oct 22: rewrite layers, measured Oct 19: ready to decide (text door at Western Slope) Oct 21: audit and re-inspection · Evening delay Oct 20: door readiness, read-only Oct 20–21: guest-card notes policy (decided) Overnight build Oct 12–13: what shipped, what Fede decides 1. The gap 2. The shape 3. Read: watch any page 4. Mirror: copy and identity 5. One thread, many doors 6. Clara's context 7. When Clara chimes in 7a. Nothing falls through (Reception) 8. Sending through the PMS 8a. Outbound doors (new) 8b. Willows battle test 8c. When Clara replies (proposed) 8d. When AppFolio is down (proposed) 9. Western Slope inbox, two years 10. Traps 11. Build order 12. Decisions 13. Tensions 14. Reference

Every customer's staff and tenants talk inside the PMS inbox, and PropFlow sees none of it. Clara answers her own number blind; at Western Slope that already makes two voices on one lead. Fede's gut: most of that inbox is routine, and the rest is what the office should spend its day on.

The proposal: a PMS communication layer. One generic core (watch a list, fetch what changed, mirror it into the person's single conversation, one front door for every inbound message) and a thin adapter per property system. Clara's number and the PMS's number become two doors into the same room, so nobody is ever told "we have a new number".

Named Reception (Gera, Oct 15); its parts are listed in 7a.

First deliverable, in Fede's words: "Every text in that inbox, sent or received, lands in the matching PropFlow conversation, labeled with who sent it (Kat, Jay, Assistant PM) and when. Clara reads, never replies."

Oct 22: the rewrite layers, measured

Oct 22: the rewrite layers, measured (proposed — pending Fede's go)

Proposed — pending review  2026-10-22. Read-only replay of 78 real first emails (66 Western Slope, 12 Camellia). Same writer draft into every setting; three model judges grade each sent text; the judges passed their planted-defect controls.

Before and after (cards out of 78)

RowProduction sentToday's code (all layers on)All blind layers off
Over-share (counts, rosters)1800
Question left unanswered1245
Meaning changed from the draft10298
Tour offered726029
Held, nothing sent0635

Layer by layer

LayerFired (cards)Cards worse when offVerdict
Tour-times check319 (7 are clock drift; net it helped on 2)Keep the job, move it into Clara's turn
Name the home133 (all real)Keep the job, move it into Clara's turn
Count-check redraft106 (4 real)Keep
Bare-handoff recovery0 differ0Dead, delete
After-trace redraft1 differs0Dead, delete
Held redraft0 differ0Dead, delete
Greeting restore81 (judge noise)Dead, delete

Recommendation

  1. Delete the four dead layers. One at a time each looked free, but with all four off at once on the same 78 cards, 1 real card changes (see below).
  2. Move tour times and name-the-home into a full-context retry inside Clara's own turn, so she fixes it with the whole conversation in view.
  3. Keep the count check.

All four dead layers off at once (same 78 cards)

1 real card changes: Pomona Park (2f770954). The fact cut removed the email's only tour-times line. Today the after-trace redraft puts the times back; with it off alone, the held-email retry did. Each one-at-a-time run showed 0. With both gone, the email is held and the property team is asked to reply. The other 2 "worse" cards were a judge split (0365fde5) and noise from live tour times (d6e14119).

RowTodayAll four off
Emails sent6160
Held67
Over-share00
Tour offered6059
Unanswered45

Shipped as PR #21992 (fix, no switch), armed to merge 2026-10-22.

What could not be measured

  • Home facts and tour times are read as of today, not send day (18 of 31 tour-check fires are drift).
  • 27 cards had no saved draft and were skipped.
  • 11 inquiries were missing.
  • Only layers on the main turn were counted; the fact-trace layer (fired 9) was not isolated.
Oct 22: the first-reply prompt, inspected

Oct 22: the first-reply prompt, inspected (the writer fix — building on the harness)

Proposed — pending review  2026-10-22. Read-only inspection of the Ember Estates first email. Nothing was re-run or sent.

Follow-up: the whole prompt, on every channel, is inspected and re-architected on Clara prompt architecture (proposed).

What was measured

WhatToday
System prompt264,510 characters, 1,082 lines
Tools offered23 tools, 47,030 characters
Tokens in, first call109,658
Time to draft75 s, then a timeout
What was sentA blind redraft that never saw the renter's message

Three findings

  1. The rules contradict each other. 6 length rules, 5 rules on how many homes to name, 3 on tour times. The model's thinking is that argument written out.
  2. The tool gate re-fetches facts already in the prompt. It forces three lookups the home-facts block already answered.
  3. About 85% is irrelevant to a first reply (maintenance, renewals, walk-ins, existing tenants), much of it written for weaker models.

Biggest sections

SectionCharsNeeded on a first reply?
Guest-card first-email composer rules29,435Partly (conflicts inside)
Tour scheduling22,921No
Presenting units18,480No
On-behalf visitors / companions18,217No
Autonomous property rule14,855One line
Rules for this first email12,274Yes (conflicts inside)
Answer-and-tour rule11,853One line
Always-route rule8,591One line
Hard-fail quick check7,235No
Post-approval rule7,000No
Active capabilities6,857No
Concern-before-call-to-action rule5,052No

Tonight's objective and bar

One coherent first-reply prompt. The writer's own draft is what is sent, with no rewrite layers, replayed on the same 78 cards. Bar: 6 or fewer held, 0 over-share, 98% or more with a tour offered.

Oct 19: ready to decide, Clara replying through AppFolio at Western Slope

Oct 19: ready to decide — Clara replying through AppFolio at Western Slope

Proposed — pending review  2026-10-19, morning. Built dark overnight; nothing changed for any real customer.

Built: 9 small changes so a conversation on AppFolio's texting line works both ways: all 11 merged (8 from the code audit, 3 from defects the live run found). Western Slope still texts only from Clara's line. Western Slope still texts only from Clara's line.

Proven: the Willows runs tonight (three rounds, 05:51Z to 11:11Z), one row per check below. Checks 1, 3, 4 and 5 pass; 2, 6 and 7 could not be exercised live (reasons in the rows).

Fede's ruling, Oct 19 morning: on hold. No turn-on, no further building. He wants a design document first on how Clara behaves inside an inbox that staff also use (over-eager on one side, silent on the other), with real threads and what other products do: Clara in the shared inbox. The decisions below stay listed for the record; none is asked today.

What shipped tonight

What it doesPRState
A text thread keeps the number it is already on#20975merged
A double text or a redelivery gets one reply, not two#20974merged
A held text expires instead of going out stale, and re-checks every gate before it goes#20985merged
A text with a photo or file leaves on Clara's line; the renewal letter's intro and PDF come from one number#20982merged
A prospect's reply inside AppFolio wakes Clara, when she is the one talking#20992merged
A STOP or START texted from a number AppFolio cannot tie to a property is still recorded (found by the live run, check 5)#21031merged
A teammate's text in AppFolio puts Clara on hold for that person#20993merged
STOP on AppFolio's texting line opts the person out on both numbers#20995merged
The thread rule reads past a household member's texts and never counts a text sent to someone else#21000merged

Proof at the Willows (production, the door is on there)

CheckPass meansResult
Baseline, Oct 16Reply into an existing AppFolio thread; zero texts on Clara's lineproven Oct 16 36 s to the phone (before tonight's changes)
1. Full reply loopProspect texts AppFolio's number, Clara wakes, replies through AppFolio, filed once, follow-ups stoppass (Oct 19, 11:01Z, after the two fixes): the text was filed and woke Clara once; her reply left through AppFolio and reached the phone from AppFolio's number 59 s after the text; zero texts from Clara's line; filed once; the two stale door alerts closed on that send. Earlier the same morning this check failed (reply left on Clara's line), which is what the two extra fixes repaired
2. New lead, no AppFolio threadThread started from the guest card, reply received from AppFolio's numbernot proven; blocked on one phone number. This is the path Western Slope hits most (a lead arrives, Clara's first text starts the AppFolio thread). Driving it needs a test number with no history at all, and none of our 26 Twilio numbers qualifies (each has a person, a thread or is a live line). Buying one ordinary number is a purchase, so it is a yes/hold for Fede (decision 1 below). The Oct 16 attempt did deliver through AppFolio, but the read-back was given up on and a false "door down" alert stayed open three days (#21068)
3. Thread on Clara's lineReply comes from Clara's line; nothing through AppFoliopass (Oct 19, 05:53Z): the reply came from Clara's line in 46 s; no door row, no AppFolio thread started
4. Double textExactly one replypass (Oct 19, 11:05Z): two texts 5 s apart were filed in one pass, woke Clara once, got one reply through AppFolio answering both. Earlier the same morning: two replies on Clara's line (fixed)
5. STOP, then START, on AppFolio's lineBoth numbers refuse; START restorespass (Oct 19): with a guest card (08:30Z) and without one (11:08Z, after #21031), STOP and START are both recorded on the person's number at the text's own time; Clara took no turn and we sent nothing back. Not exercised: a refused send while opted out (no follow-up was due)
6. Teammate text mutes ClaraClara stays silentunit-covered only; needs a second AppFolio login to drive live. Not tested
7. Held text expiresExpired or re-sent under the new rulesnot exercised: no held text was eligible in any run tonight (the 5 held rows at the Willows are final; nothing new was held)

Before → after, per lane (a person at Western Slope)

What goes outTodayDoor on
Leasing text, brand-new prospectClara's lineAppFolio's texting line (thread started from their guest card)
Leasing text, conversation already on Clara's lineClara's lineClara's line (a thread keeps its door)
Leasing text, conversation already in AppFolioClara's lineAppFolio thread
Maintenance, vendor and staff textsClara's lineClara's line
Renewal letter (intro and PDF)Clara's lineClara's line, both pieces
VoiceClara's lineunchanged
Emailleasing mailboxunchanged. Guest-card emails keep leaving from the leasing mailbox. The text door touches no email.

The resolver: which number a text leaves from

A text needs to go out 1. Voice, staff, vendor, photo or file? checked first, always 2. Conversation already on a number? new tonight: a thread keeps its door 3. Otherwise: the company setting Clara's line, or the AppFolio thread 4. AppFolio unreadable, or no guest card? only reached when rule 3 says AppFolio yesyesyes Clara's line The number it is already on The company's door Hold: send nothing, one alert
Rules are checked top to bottom. Rule 2 is the one new rule tonight. With the setting at Clara's line (Western Slope today), rule 3 always lands on Clara's line.

What else changed tonight:

  • A prospect's reply inside AppFolio now wakes Clara, when her text is the newest one from the office.
  • A teammate's AppFolio text puts her on hold for that person.
  • STOP on the AppFolio line opts the person out on both numbers.
  • Held texts expire instead of going out stale.
  • A double text gets one reply.
  • The renewal letter arrives from one number.

Replay on Western Slope's real people (read-only, nothing sent)

Same 41 people Clara texted since Oct 2. "Right number" means the number the person last wrote to.

Company settingOld rule (Oct 16)Tonight
Clara's line (today's setting)41/41, all on Clara's line41/41, no change
AppFolio door turned on: right number9/4133/41

The Oct 16 "35/41" counted who AppFolio could reach, not the right number: 32 of those 35 would have switched number mid-conversation.

The 8 remaining missesPeopleWhat it means
Last wrote to Western Slope staff inside AppFolio, before the inbox copy started (history)5 (…1449, …5740, …8013, …7495, …8187)Design question: decision 2 below. Counting history makes it 37/41.
Never wrote back on either number3 (…6728, …9771, …8330)Defensible either way: texts came from both numbers, they answered neither.

Side effects and things to watch

  • Number switch mid-conversation: prevented by the thread rule.
  • Two numbers per person across lanes: one person can get leasing texts from AppFolio's number and maintenance texts from Clara's line.
  • AppFolio request budget: see the table below. The inbox copy already uses most of a day's budget; each door send adds about 20 requests.
  • Tenants answering a door text: the wake covers prospects and applicants only, so a tenant's reply in AppFolio does not wake Clara; many tenant rows still wait for tenant support in the inbox copy.
  • No AppFolio record: the text is held, with one alert. Nothing goes out.
  • AppFolio user "Clara Propflow" (540): has sent 8 texts at Western Slope (7 history, 1 live on Oct 18) with the door off. Verified: every one is AppFolio's own "Rental Application for <address>" template, the text copy AppFolio sends beside its rental-application email when Clara presses that button under this login. Not a Clara-written text, and not the door.
  • Prospect texts AppFolio's number first: no Clara text on the thread, so Clara does not wake. At Western Slope the leasing line forwards to Clara's line, so first contact normally arrives there.
  • After a STOP the follow-up sequence is refused at the send gate, not stopped.
  • Tour confirmations still go out during a teammate hold, same as Clara's line today.

Prerequisites per customer

Western SlopeSitus Group
Clara's own AppFolio loginyes (user 540)none recorded
Inbox copy (mirror)on since Oct 16off, never run
AppFolio database in the mirroryesone to add (to confirm it is exactly one)
Robot session kept warmyesto confirm (could not read tonight)
AppFolio requests, Oct 183,324 of 4,000 (83%)none yet
Text door settingoff (Clara's line)off
Turn-onone company settinglogin, mirror, database, warm session, then the setting
Whenafter the Willows table is greennot this week: Willows, then Western Slope first (Oct 14, D7)

Decisions for Fede

1. Turn the text door on at Western Slope for leasing? On hold (Fede, Oct 19): design document first. When it is decided, the remaining Willows proof (check 2, the new-lead thread start) needs one clean test number. Checks 1, 3, 4 and 5 are green. (b) Yes, everything through AppFolio with a one-time "new number" note (not built). (c) Hold.

2. Should a staff text inside AppFolio from before the inbox copy (history) move a person's thread to AppFolio? (a) Recommended: no, as built (33/41): nobody switches number by surprise. (b) Yes (37/41).

3. Situs Group. (a) Recommended: after Western Slope runs a supervised week. (b) Same day as Western Slope.

4. Delete the old September per-property send switch (114 references, replaced by the company setting)? (a) Recommended: yes, one cleanup PR chain. (b) Keep.

Follow-ups filed: #21068 #21069 #20987 #20988 #20989 #20990 #20991 #20999 #21010

Open housekeeping: the shared main code checkout is stuck mid-rebase on another session's local commits; every builder tonight worked around it. Its owner needs to resolve it.

Overnight build, Oct 12 to 13

Oct 21: audit and re-inspection before the text door goes on at Western Slope

Overnight, Oct 20 → 21. Fede: "no turning on today, let's get it ready overnight, go deep and audit everything." Read-only audits on real Western Slope data plus three fix PRs for his rulings from the evening. Nothing is on. Proposed — pending review

Update, Oct 21 13:46:54Z (7:46 AM Denver): the door is ON at Western Slope. Fede: "turn it on and watch it", "revert or turn off if you see customer impact". Session 007 is watching the inbox, every wake, every door send and the budget through the day; rollback triggers: any door text to a tenant, staff or vendor; a lead reply unanswered over 10 min by day; a no-card lead held over 30 min; door-down or send failures. Evening and overnight reply delay (up to 15 min) is under a separate inspection today. Staff step-in (D-A) runs on today's behavior: a staff text means Clara steps back from that lead.

TL;DR for the morning.
  1. The door works. Every leasing path is proven live at the Willows, including the three fixes below: all merged and deployed at 05:20Z, then a Willows run on that build passed all three checks (prospect reply, double text, tenant stays silent). A fourth fix (#21576, a tenancy match keeps the text on Clara's line) merged at 06:1xZ, unit-covered only, deploying.
  2. Three of your evening rulings are now code, merged and live everywhere (dark: the door is still off at Western Slope): tenants never wake Clara (#21563), leasing only on the send side (#21569), the inbox copy keeps peeking after the budget reserve so afternoon replies are seen within 15 minutes (#21568; live proof is this afternoon's Western Slope state row).
  3. Fifteen ways the door could make someone worse off were found; four are real, three are closed by those PRs, one is yours to decide: what Clara does after staff text a lead in AppFolio.
  4. Rollback is one setting but not free: every lead in an AppFolio thread moves to Clara's number on their next text, silently, and held texts freeze with no expiry.
  5. Two decisions below are yours (D-A and D-B). The rest are recommendations I will carry out under the standing grant unless you say otherwise.

Decisions for Fede

#QuestionOptions (recommendation first)Touches a prior ruling
D-AAfter a staff member texts a lead inside AppFolio, what does Clara do?(a) The lead's next message decides. If they answer staff, Clara records it and stays quiet; if they write to Clara, she answers. Her own follow-up nudges pause after a staff text. No clock, no button. · (b) Only a staff reply stops her (she may jump into Kat's thread). · (c) Staff hand it back with a button (your Sep 8 "no buttons" rule says no). · As built today: stops forever, invisibly.Sep 3 "if another human replies we stop"; Oct 16 "no timer". (a) keeps both. A timed comeback is not proposed: 0 of 19 prospects staff texted ever wrote back later.
D-BRollback plan if the door goes on and we turn it off.(a) Accept the number change and have Clara's first text after rollback say "this is Clara's direct number" (small build before the flip). · (b) Accept it with no notice. · (c) No rollback path: fix forward only.None.
D-CWho goes through the door.Decided Oct 20: leasing only. Tenants, maintenance, renewals, staff, vendors always on Clara's line (#21569). Brand-new leads start in AppFolio; leads already on Clara's line stay there; leads Kat already texted in AppFolio answer there.Oct 19 D7 (tenants wake) reversed Oct 20.
D-DEvenings and the request budget.Decided as a fix: keep peeking at the inbox every 15 min from the reserve (#21568). Follow-up #21566 halves the copy's daily cost (emails every 6 h, one robot check per call). The 4,000/day cap is ours, not AppFolio's; raising it is a separate call, not recommended until #21566 lands.Sep 30 Situs 429 storm (why the cap exists).
D-EWhen to turn on.After the three PRs are deployed and the Willows run below is green, on a weekday morning with this session watching. Not before you pick D-A.Oct 19 hold (lifted Oct 20, then withdrawn the same evening).

Findings, ranked

#FindingWho / how often (Western Slope, real data)SeverityCloses it
1Inbox copy goes blind on budget between 3:30 and 7 PM Denver; replies in AppFolio wait until midnight57 of 129 texts to Clara's line arrive after 3:30 PM, about 3 a dayfixed, deployed#21568 merged 04:5xZ, live; proof this afternoon on the Western Slope state row. Remaining edge: two 220-request phone lookups can still exhaust the reserve
2A staff text stops Clara on that lead for good, nobody is told11 of 45 leads already have a staff text as the newest office message, about 2 a day; one real case (a lead moving a tour) would have been refusedblocking, your pickD-A
3Tenants' AppFolio texts wake Clara (leasing voice answers a repair question)About 10 of 61 recent inbound texts are from tenant numbersfixed, proven live#21563 merged, Willows tenant check passed 05:33Z
4Leasing-only held only by accident; held maintenance drafts and some tenant replies could leave via AppFolioMaintenance is in drafts at Western Slope, so livefixed, deployed#21569 merged and live; the three extra tenant paths (team-note relay, tenant on the tour path, tenant co-applicant copies) are #21576, merged
5No AppFolio card → no text at all, held for good, never falls back to Clara's line4 of 35 recent leads (phone callers); the card-after-call fix is live but no leasing caller has arrived sinceworse, unproven fixWatch the first post-fix caller
6Rollback moves every AppFolio lead to Clara's number silently; held texts freeze with no expiry and all fire a false "door down" if the door returns44 threads today whose newest text came via AppFolioworse if we roll backD-B; expire held rows on rollback (small fix)
7Door-down alert fires only in Sentry, never in #alertsOct 16 Willows episode had no channel postopsRoute to #alerts (small fix)
8Retry job ignores quiet hours: a follow-up held at 9 PM can go out at 3 AM from the office number (#20989)Hold windows are 30 min or 6 h, so rareworse#20989
9Some leasing texts stay on Clara's line, so a lead sees two numbers: weekend tour check-in (no company on the send), bounce-fallback text, crash-fallback reply0 people hit this since Oct 2cosmeticFollow-up issue
10Past tenants: inbound treats them as prospects, the send rule treats them as "not leasing"; a returning renter writes in via AppFolio and is answered from Clara's numberRarecosmeticFollow-up issue

Corrected premises: a lead who texts the office number first does wake Clara (fix deployed); every first text already says "it's Clara with Western Slope". What gets better: one voice in the office inbox, staff see Clara's texts, replies to the office number reach Clara.

Proven live vs unit-covered only

BehaviorStatusEvidence
Reply into an existing AppFolio thread; double text → one reply; STOP/START on both numbers; a thread keeps its number; thread started from the guest card; tour confirmation and 1-hour reminder via the doorproven liveWillows runs Oct 19 (05:51–11:11Z) and Oct 20 16:58Z
Tenants never wake (#21563); leasing-only send rule (#21569)proven liveWillows run Oct 21 05:25–05:39Z on build 6e1e4a9b, below
15-minute peeks after the reserve (#21568)proven liveWestern Slope, Oct 20 night: after the reserve stop at 6:56 PM the copy read nothing until the fix deployed, then peeked at 11:03, 11:19, 11:34 and 11:50 PM Denver (sync log)
Tenancy match beats a declared leasing module (#21576: team-note relay, tenant on a tour path, tenant co-applicant copies, approved held leasing reply)merged, unit-covered onlyMerged Oct 21 06:1xZ; dispatcher-level test with planted defect; no live Willows path exercises it without a tenant-on-tour setup
Teammate hold; held-text expiry; door-down alert; texts with media; Clara's AppFolio seat at Western Slope (user 540) sending a real textunit-covered onlyNeeds a second AppFolio login / the first real send
Willows run on the deployed fixes (reply loop, double text, tenant stays silent)passed 3 of 3Oct 21 05:25–05:39Z, build 6e1e4a9b on website and both Lambdas; table below

Willows run, Oct 21 05:25–05:39Z (deployed build 6e1e4a9b)

Four texts from bench numbers to the Willows AppFolio line +15046087154. No settings changed. Zero texts left Clara's own Willows line (+18442853526) during the run. JP&Co request budget went 2,326 → 2,383 (+57).

CheckSent (UTC)ResultEvidence
1. Prospect reply (card 197)05:25:20pass, slow: 5 min 17 s to the phoneCopy MSG#1167059894 → decision=woke 05:29:46Z → REC#reply.d831fa91… sent 05:30:40Z, filed with pmsMessageId 1167060071 → reply from +15046087154 (Twilio SM311e699…). Slow because the inbox copy was in its 15-minute night mode until the first pickup; the 50–100 s figure holds only once the faster mode kicks in.
2. Double text05:30:48 and 05:30:52pass, 87 sMSG#1167060081 decision=coalesced, MSG#1167060087 decision=woke, both filed in one pass; one REC#reply.2de5164d… filed with pmsMessageId 1167060125; one reply (SM4bcb7c7…).
3. Tenant stays silent (#21563)05:32:55passTenant 1851, neutral body. MSG#1167060139 filed, decision=tenant_thread 05:33:45Z, no new outbox row (22 before and after), nothing sent to the tenant from either Willows number through 05:39Z, no runtime log line for the conversation.

Found during the run (not door bugs, filed separately): (1) in the prospect turn, the lead-status step moved card 197 to "not interested" citing an old STOP and ignoring the later START, and marked the guest card inactive in AppFolio 18 s later (card 197 is now Inactive, "No Response"; Clara kept answering the thread). Root cause: the after-turn classifier prompt lists a STOP as "withdrew" and never checks the live opt-out status, so a START that undid the STOP is ignored. Affects every prospect on Clara's line as well, not door-specific; one-line prompt fix plus a test from this thread. Issue #21577. Bench debt: card 197 and inquiry 2398fd62… stay "not interested" until the fix lands and they are reset. (2) The tenant's text filed onto an old May maintenance voice thread rather than the renewal thread; consistent with that tenant having said they are moving out, no action. (3) The brief's log name messaging/appfolio-inbox-text is stale: prospect wakes log as inbound-router reply_on_clara_thread plus a dispatch-log line; the tenant line logs pms=<id> without the sms: prefix.

Audit 1: what staff stepping in should do to Clara (real Western Slope threads)

Short answer. Since Oct 16, staff texted 33 people in AppFolio. 19 were prospects; 18 of those texts were staff starting the conversation (check-ins, templates, closing the guest card), not answering anything. Only 1 prospect wrote back, to say no. So today's rule, "a teammate's text puts Clara on hold for that person forever," mostly fires on leads nobody at the office is working. The one prospect it would have hurt was trying to move a booked tour on Clara's line the next day. Recommended: the lead's next message decides who answers. No clock and no button.

The 33 people, Oct 16 to Oct 20

Who and whatPeopleStaff text typeLead wrote backStaff answered the replyClara wrote them since Oct 9Clara wrote after staff
Prospects (leasing)197 "found a place in Ridgway?" check-ins, 4 "closing your guest card", 4 "still want a showing?", 2 intros, 1 application link, 1 real conversation1 of 19 (a no-thanks)n/a101 (…6055, the tour)
Residents: work orders8"Was your work order fixed?"13 of 14first answer median 18 min (n=12); every reply median 5 min2 ("rate your repair" survey; one went 10 min before staff asked the same thing)0
Residents: renewal3Renewal offer00
Residents: move-in2Codes, keys, signatures1 (email)0
Residents: other1"We saw your email"00

Kat sent 11 of the 19 prospect texts and the shared office login sent 8. The Oct 20 group of 8 who last wrote to staff: none wrote again, and Clara sent nothing. 9.6 for scale: the office answers a weekday text in a median 8 min, and an evening text in 13.8 h. Most residents ended on a thank-you; 2 real asks are still open. Both worked-twice leads (…4860, …4236) are in the 19: staff sent a cold check-in after Clara had already settled the question, and neither answered. That is a visibility problem (staff can't see Clara's thread), not a hand-back problem. Residents are outside the leasing-only door (#21563), so the hold question is really about the 19 prospects.

The one case that matters (…6055)

Oct 18: Clara books a tour. Oct 19: Kat sends an intro in AppFolio ("message me to set up a time"). Our inbox copy filed it into the same conversation. Oct 20: the lead texts Clara's line twice to move the tour, and Clara handles it in a minute. If the text door had been on, the hold would have refused those replies. The lead would still have got a reminder for the time they had asked to change. This is inferred from the hold code; it did not happen, because the door is off. Right now 1 of 287 Western Slope conversations is held, and it is an email thread.

Lead writes Who did theyjust answer? Staff's turn: Clararecords it, stays quiet Clara answersand finishes the ask a staff text Clara, or her line

Decision for Fede

1. The lead's next message decides. Recommended. A staff text never puts Clara on hold. If the lead answers staff in AppFolio, that turn is staff's: Clara records it and stays quiet. If the lead writes on Clara's line, or answers Clara's text, Clara answers. Clara's own follow-up nudges stop after a staff text, so the lead never hears two voices. That pause is the one point where this option touches your note tonight. The reason: 10 of the 19 prospects already had Clara nudging them, and the second voice cost one STOP (…8013, shared-inbox page). Keep the pause or drop it, your call. Keeps Oct 16 (no clock) and Sep 8 (no buttons). It reads Sep 3's "IF another human replies we stop" as stopping for that turn, not for good, which matches your note tonight. On this data: …6055 gets its tour moved, and …9541 stays with staff.

2. Only a staff reply stops her. A staff text that answers something the lead asked stops Clara until the lead writes again. A text staff start on their own never stops her. This follows Sep 3's wording exactly ("replies"). The risk: when a lead answers Kat's check-in (…9541), Clara may jump into Kat's conversation. That is the "is it for me?" rule from the Oct 19 call.

3. Clara waits for staff to hand it back (H1, from the shared-inbox page). Clara stays off until staff hand the thread back; a waiting lead shows on a list. This conflicts with Sep 8: a "hand back to Clara" button asks a person to decide something the system already knows.

Not proposed: Clara coming back after a set time (H2). That would reopen the Oct 16 no-clock ruling. Do you want to reopen it? On this data a clock buys nothing: 0 of 19 prospects wrote back later. Separately, under every option staff need to see when Clara is quiet on a thread. Today they see nothing.

Sources: propflow-prod, read-only, Oct 21 ~03:50Z. Inbox copy PMSMIRROR#appfolio#westernslopepm MSG# (980 rows; staff = senderKind staff, since Oct 16 00:00Z, 33 phones). AppFolio guest-card emails are excluded (Kat emailed about 20 cards). Conversations via conversation-list-index since Oct 2 (287), matched by phone and person. Clara's line = her own sms and email turns, not team notices. Code: origin/main df30ee1a50 wake-on-reply.ts, auto-reply-hold.ts.

Audit 2: every way the door makes someone worse off than today

Audit 4: who is worse off with the door on than today?

Read-only, Oct 21 ~04:00Z Today the door is off: Clara texts from her own line and the inbox copy only reads. The test is Fede's bar, "hard to make it worse than right now". Counts come from Western Slope production since Oct 2, read only (287 conversations, 5,768 messages, 980 inbox-copy rows), plus code at origin/main df30ee1a50. The deployed build includes the call-card fix and the wake-on-first-text fix.

5 items are blocking today. Once the budget fix lands, 4 remain. Two of those need a design call: the teammate hold and the no-card hold. One needs a PR to merge and deploy: tenants waking Clara (#21563). One only bites if we roll back.

#Worse offWhoHow often (measured unless marked)SeverityWhat closes it
1The inbox copy goes blind in the evening. The lead's reply waits until midnight and is answered at midnight. Clara's own evening texts don't show in PropFlow until then.Leads who reply late in the dayToday 57 of 129 texts to Clara's line arrive after 3:30 PM Denver (about 3 a day; 40 after 5 PM)Blocking until the fix lands. After that: tolerable. A reply is seen within 15 min instead of 20 s. One hard edge remains: two phone lookups (440 requests) can use up the 600 reserve. After that, a send that no longer fits is held for good.Budget-fix PR, option (b). Later, cut the copy's daily reads.
2A teammate's text stops Clara on that lead for good. Follow-ups are refused on both numbers, replies don't wake her, and staff are never told she stepped back.Leads Kat, Jay or the shared login texted after Clara11 of the 45 leads Clara texted have a staff text as the newest office message, all since Oct 16 (about 2 a day). The shared-inbox page found 7 of 27 left hanging after staff took over. Today this hold does nothing.BlockingHand-back decision (H1) plus a list of muted leads
3A lead with no AppFolio card gets no text at all. The text is held: retried for up to 6 h in case a card lands, then final. It never goes out on Clara's line. Staff see nothing; only engineering gets an alert.Phone callers and other leads with no card4 of 35 before the call-card fix. Since the fix deployed there have been 0 leasing callers (6 calls, none leasing), so the fix is unproven live.Blocking until a caller's text is seen sentWatch the first post-fix caller. Fallback rule is Fede's call.
4A tenant's text to the office number wakes Clara. "Leasing only" is not enforced.Tenants10 of 61 live inbound texts since Oct 16 come from numbers tied to a tenant (4 tenant-only)Blocking under Fede's leasing-only rulingMerge and deploy #21563 (still open)
5On rollback, every lead in an AppFolio thread switches to Clara's number. Their replies inside AppFolio stop waking her.Leads whose thread moved into AppFolioAbout 4 new leads per door-on day (inference: 72 new inquiries in 19 days), plus 10 who move on day oneBlocking, only if we roll backKeep the wake on for AppFolio threads after rollback, or decide this before the flip
6Slower replies. Today about 20 s; with the door 59 to 99 s by day (Willows), and 5 to 30 min in the evening and overnight as the copy checks less often.Every lead who repliesTexts to Clara from 7 to 9 PM: 9 in 19 days. After 9 PM: 0.TolerableNone needed
7The number changes mid-conversation. Mass texts, texts the router can't place and the first renewal offer don't name their thread.Open conversations3 of 49 move to AppFolio because Kat's text is newest (by design). The rest are not counted.TolerableName the thread in those senders
8A held text can be re-sent after 9 PM. The retry step doesn't re-check quiet hours (#20989).Leads0 such retries at Western Slope so far. 1 Clara-started text to a lead after 9 PM since Oct 2.Tolerable, rare#20989
9Clara's own number is still printed. The tour email's "text us" close uses her line (in code), so a lead ends up with two numbers. Staff don't see the half on Clara's line. Website and listings: not checked.Leads who get a tour email3 tour emails since Oct 2. The saved bodies don't include the footer, so the count of printed numbers isn't measured.Tolerable: texts to her line are still answeredPrint the AppFolio number when the door is on
10A send marked "transactional" can still reach someone who texted STOP (#21279)People who opted out1 STOP on Clara's line since Oct 2. 0 live STOPs on the office line.TolerableA7 fix
11Leasing texts from the office number, maintenance texts from Clara'sRenters in both lanes0 people got both since Oct 2 (5 got maintenance only)CosmeticTell Western Slope
12Lockbox codes and links become visible to staff in AppFolio. Downside: codes are stored there. Upside: staff see what Clara promised, which stops cold restarts (6 of 80).Staff, leads6 texts with codes (2 people), 2 with linksCosmetic, net betterNone
13The first text comes from the office numberNew leadsEvery lead-facing first text opens "it's Clara with <company or property>" (44 of 51 first texts; the 7 that don't are staff and maintenance texts, which stay on Clara's line). 1 says she is an AI, same as today.CosmeticNone
14AppFolio's own texts land in the same thread: Clara-seat "Rental Application" texts, notices and bulk sends. They don't mute Clara.LeadsIn 5 days: 3 Clara-seat texts, 41 notices, 208 bulk sendsCosmeticNone
15Premise wrong: a lead who texts the office first now wakes Clara (#21315, deployed). Today those texts reach staff only. One exception: a lead with an older staff text stays with staff (8 of 45), same as today.New leadsNo longer worseBetterNone

What gets better

  • One voice per lead. Staff see Clara's texts before they type: 6 of 80 leads were restarted cold by staff who couldn't see her thread.
  • Replies to the office number reach Clara: 61 live inbound texts in 5 days. On weekday evenings the office's median answer time is 13.8 h; Clara answers in seconds to minutes.
  • Leads see one number: the office's, the same one shown on AppFolio and the listings.
  • A STOP on either number is honored on both.
  • Clara's AppFolio login (user 540) is already labeled as Clara on 10 copied rows. That is evidence for readiness row 18.

Audit 3: every code path through the door, after the fixes

Read-only. Compared against origin/main on Oct 20 night. Two fixes were read. The tenant fix is PR #21563 (merged Oct 21 04:2xZ). The leasing-only fix is PR #21569 (open, armed); the tenancy-wins rule it needs is on branch fix/text-door-tenancy-wins, PR to follow once #21569 merges. Its tests now include a dispatcher-level case that runs the tenancy lookup through the real send path.

Bottom line. With both fixes and the switch on, almost every leasing text goes through AppFolio, and every maintenance, renewal, collections, vendor, staff and emergency text stays on Clara's number. Three gaps remain:

  • Five leasing paths still use Clara's number, so a lead sees two numbers.
    • The weekend tour check-in. It never tells the switch which company it belongs to.
    • The text Clara sends when an email bounces.
    • The fallback reply sent when Clara's turn crashes.
    • Any reply to a lead who once rented from the company, or whose phone matches a tenant.
    • An approved held reply that was filed under maintenance or renewals.
  • A few tenant texts can still go through AppFolio. These are:
    • the relay of a teammate's note, which skips the "is this a tenant?" check when the thread is not flagged as a known caller;
    • a tenant's reply on the tour-booking path;
    • tour and application copies sent to a co-applicant who is a current tenant;
    • an approved held reply where Leasing is the only module waiting for approval.
  • Texts held before the fix can still go out through AppFolio after it deploys. The retry job (the "drain") never re-checks the leasing-only rule. Held rows expire within 30 minutes (replies) or 6 hours (other texts), so this window is short.

Incoming side. After #21563, a text in a thread where everyone is labeled tenant or occupant is copied and never wakes Clara. Prospects and applicants still wake her unless a teammate answered after her. A thread that mixes a tenant with a guest card still wakes Clara. If that thread is filed on the tenant's record, the leasing-only fix sends her answer from Clara's number. The text came in on AppFolio's number, so that person sees two numbers.

At 2 AM. Confirmed: the drain does not check quiet hours (#20989). Clara's normal replies never check quiet hours, on either number. A lead who texts AppFolio at 2 AM gets Clara's answer through AppFolio within about 1 to 2 minutes, because the inbox copy is running again after midnight. A follow-up held around 9 PM can still go out as late as 3 AM. A lead who texts after about 7 PM, once the inbox copy has paused for the day's budget, gets the answer at about midnight.

Proven live vs unit-only. Proven live at the Willows (our test property): the reply loop, double texts, STOP and START, a conversation staying on Clara's number, the first text that opens an AppFolio thread, tour confirmation and the 1-hour reminder. Unit tests only: neither fix, the tenant no-wake, the leasing-only routing, drain expiry, the teammate hold, the door-down alert, media, quiet hours.

1. Outgoing: which number each text uses (switch on, both fixes)

TextNumberNote
First text to a new leadAppFolioOpens the thread from the guest card (tries at most 3 cards, #20636). No card means the text is held and an alert fires.
Clara's everyday reply to a leadAppFolioOnly when the person has never rented from the company and is not matched to a tenant. Otherwise it goes from Clara's number.
Reply to a lead who once rented, or whose phone matches a tenantClara'sThat lead's tour and application texts go through AppFolio, so they see two numbers.
Fallback reply after Clara's turn crashesClara'sIt carries no conversation, so the leasing-only check reads "unknown".
Follow-up and re-engagement texts, staff-asked textsAppFolio
Tour confirmation, reminders, slot lost, outcome, unconfirmed nudge, post-tour chaseAppFolio
Weekend tour check-in and morning-of nudgeClara'sIt names no company or property, so the switch is never asked. One-line fix.
Application link, lease approvalAppFolio
Household copies (tour, application, chase)AppFolioAlso when the co-applicant is a current tenant: no check is made.
Self-guided lockbox code, delay notice, leave checkAppFolioText only, no picture. Staff can see the code inside AppFolio.
Relay of a teammate's noteAppFolioWhen Clara relays it, the code forces "leasing" unless the thread is flagged as a known caller, so a tenant can get it through AppFolio.
Approved held replyDependsFollows the module the reply was filed under, not the person. A lead's reply filed under maintenance or renewals goes from Clara's number. A tenant's reply filed under Leasing goes through AppFolio.
Text after an email bouncesClara'sNames no company. It is also hard-coded as "prospect", so tenants would follow once that is fixed.
Maintenance to a tenant (work order, rating, vendor updates)Clara'sNow on purpose: the code says not leasing. Before the fix it stayed on Clara's number by accident.
Renewal offer, reminders, change of terms, recap, month-to-month notice, renewal toolClara'sThe renewal letter's words and its PDF both go from Clara's number.
Collections notice, mass textsClara'sMass texts are hard-coded as going to tenants.
Vendor, technician, staff, emergency, login code, staff noticesClara'sSent straight through Twilio.
A tenant's reply during tour bookingAppFolioFiled as leasing whoever is texting.
Any text with a photo or fileClara's

2. Incoming: texts copied from AppFolio, wake or not (after #21563)

Row copied inWakes Clara?Why
Prospect's text, nobody at the office has answered, or Clara's text is the newestWakesClara answers on the same thread.
Applicant's text (labeled applicant)WakesTreated as a prospect.
Prospect's text after a teammate or the shared login answeredNoReason "teammate after Clara". The hold never lifts (H3).
Tenant's text (every person on the thread is labeled tenant or occupant)NoNew reason "tenant thread". Filed on the open renewal conversation if there is one.
Household member labeled "occupant"NoCounts as a tenant.
Thread that mixes a tenant with a guest card or applicantWakesNot tenant-only. If the thread is filed on the tenant, the reply goes from Clara's number.
Staff textNoNot incoming. It puts Clara on hold for that person and closes their open handoff.
Mass text or automatic AppFolio noticeNoNot incoming. Ignored when deciding who spoke last.
AppFolio's own AI, a Clara row from the wrong loginNo"Not a renter" or "Clara not on thread".
STOP, START, HELPNoReason "keyword". STOP and START are recorded on the number for both of Clara's numbers.
Rows from before the switch went on (history)NoReason "history".
Email copied from AppFolioNoReason "not text".
Any row while the switch is offNoReason "door off". The tenant check runs first, so tenants log "tenant thread".
Row that cannot be matched to a personNoHeld, never wakes.
Row arriving after the evening budget pauseLateNot copied until midnight Denver, then answered at midnight.

3. Behaviors: proven live or unit-only

BehaviorWhat the code doesEvidence
Rule 1: photo, file, staff, vendor (and now any non-leasing text) go from Clara's numberChecked firstUnit only (row 16 never run; leasing-only fix not run)
Rule 2: a conversation keeps its numberReads up to 100 recent messages. A failed read lets the switch decide.Live Oct 19 05:53Z
Rule 3: the company switchRead on every send, no cache. A read failure means Clara's number.Live (every Willows run)
Rule 4: no thread and no guest card means held plus one alertNever falls back to Clara's numberUnit only for the no-card case
Reply into an existing threadLive Oct 19 11:01Z (59 s), Oct 20 L3 (99 s)
First text opens the thread from a guest cardAt most 3 cardsLive once Oct 20 16:58Z L3 (about 18 s, about 27 requests)
Double text gets one replyDecided once per batchLive Oct 19 11:05Z
Read-back of AppFolio's message id (#21068)No id means "unknown", never resent, and a door-down alertLive L3. The false-alarm path is still open.
Held texts: retry, expiry (30 min for replies, 6 h for others), superseded repliesSame record id each retry. Gates re-run. Quiet hours and the leasing-only rule are not re-checked.Unit only (row 17; one transport_failed hold sat at the Willows Oct 20)
Door-down alert, one per episodeOpened by a final failure, the 10th busy hold, or an expiryUnit only (a false one fired Oct 16, #21068)
STOP and START on the AppFolio numberRecorded on the phone number, so Clara's number honors them tooLive Oct 19 and Oct 20 01:31Z
Send after STOP is refusedThe same check guards both numbers. Hole: "transactional" texts still go out (A7, #21279).Live (a reply only)
Tour confirmation and 1-hour reminderLive Oct 20 01:25Z and 14:00Z
Follow-up and re-engagement through AppFolioNot yet (the 0338 touch was still due)
Teammate text puts Clara on holdPermanent, and staff cannot see itUnit only
Tenant text never wakes Clara (#21563)Unit only (not deployed)
Leasing only (#21569 open, armed; tenancy-wins branch to follow)Unit only. Dispatcher-level test covers the tenancy lookup; no live Willows run yet (planned after deploy).
Quiet hoursThe drain does not check them (#20989)Code only

4. Open issues touching the door

IssueOne lineWeight
#21068The read-back gives up after about 4 s, raising a false door-down alert and holding the new threadworse than today
#20636Opening a thread tries only 3 of the person's cards, then refusesworse than today (that lead's text is held, not sent)
#20989The drain sends held texts during quiet hoursworse than today
#21279After STOP, Clara tries to confirm by text; the blocked reply shows as sentworse than today
#21069START from AppFolio records no personnice to have
#20987Outbox rows keep phone numbers forever, and the drain query grows with themnice to have
#20988A held text is invisible on the threadworse than today
#20990Expired texts alert as "door down"nice to have
#20991Clara's own Twilio sender drops attachmentsnice to have (not the door)
#20999A backdated AppFolio STOP overrides later email opt-insnice to have
#21010Wake edge cases: a 100-row batch cut, a failure that drops wakes, the author labelnice to have
#21306Tour reply test pinnice to have
#21358A second STOP does not fan outnice to have
#21453A refused first follow-up text is never retried and raises no alertworse than today (refusals are likelier on the door)
#21454Releasing a held first email writes a fake inbound messagenice to have
#21455Parked AppFolio write-backs are never retriednice to have
#21514A real send-once marker on the Twilio side of held textsnice to have
#21316 (new)Senders that name no conversation switch numbers at the flipblocking (the two-number risk)
#21325 (new)A failed thread lookup still sends a mass text through AppFolionice to have (mass texts are now Clara's number)
#21313 (new)The inbox copy uses up the day's budget. The reserve shipped, but the evening blind spot remains.blocking for evening replies
#21300 (new)Separate retryable holds from final ones, and mark expired replies on the threadnice to have
#21260 (new)A failed tenant-record read silently falls back to the guest cardnice to have (tenants are now off the door)

5. Looks wrong

  1. The weekend tour check-in (leasing) uses Clara's number because it names no company. It is a one-line fix and is not on #21316's list.
  2. The relay of a teammate's note forces "leasing" for any thread not flagged as a known caller. This skips the new tenant check, so a tenant can get it through AppFolio. The fix is to declare "none" and let the thread decide.
  3. The two halves disagree about former tenants. Incoming code and the thread starter treat a past tenant as a prospect. The leasing-only check treats any tenancy, including past ones, as "not leasing". So a returning renter's texts come in through AppFolio and are answered from Clara's number.
  4. The leasing-only check sends a conversation with no stamped company (older rows) to Clara's number. A lookup error does the same. Meanwhile the thread rule's comment says failures lean toward the door (D4). The two failure rules point opposite ways.
  5. Approved held replies follow the module they were filed under, not the person's lane. They can go either way.
  6. The drain never re-checks the leasing-only rule or quiet hours.
  7. Stale comments:
    • The adapter's header still says "EVERY outbound text".
    • The setting's comment says "every lane" and "nothing reads this yet".
    • The adapter still opens tenant threads (#21214), which is now unreachable except through the leaks listed in item 2.
Sources (file:line)
  • Routing: dispatcher.ts:518-538 (branch); text-door-recipient.ts:22-44 (branch); thread-party.ts:11-26 (branch, organization check at :17); company-text-door.ts:13-29; thread-door.ts:41,55-97; text-door-setting.ts:1-6.
  • Senders:
    • weekend-tour-checkin.ts:380-388 (no touch or propertyId); ndr-bounce.ts:584-591; reply-release.ts:2780 (the knownCaller module rule); held-text.ts:382.
    • process-envelope.ts:972, :1212, :571; inbound-router.ts:1923, :1961.
    • tour.ts:212, :813, :1187, :1411; post-tour-chase.ts:637, :1058; send-application-link.ts:982, :1095, :1547, :1976; send-tour-outcome-sms.ts:721, :993, :1173, :1573.
    • first-reply-text.ts:312; prospect-outreach.ts:1329; prospect-reengagement.ts:969; tour-confirm-nudge.ts:599; self-guided-lockbox-code-delivery.ts:303; new-lease-package-reviews decision route:588.
    • mass-comms.ts:561; renewal.ts:1039, :1562; mtm-warning-send.ts:59; recap-sms.ts:294; send-renewal-letter-mms.ts:241, :335; handlers.ts:403; approved-dun-send-ops.ts:493; rating.ts:883; notify-tenant-of-pms-creation.ts:306; handle-vendor-completion.ts:1460, :2459, :2671; tech-sms-dispatch.ts:557, :1378.
  • Incoming (tenant-wake branch): wake-on-reply.ts:56-82, 105-116; file-message.ts:196-200, 267, 271, 274, 292-296, 315-321, 337-338, 431-434; place.ts:60-64; pms-mirror-reply-wake.ts:22-58; pms-inbox-watch-job.ts:116-160; handler.ts:1629-1655.
  • Drain and outbox: pms-text-door-drain.ts:24 (quiet hours not re-checked), :41-44, :93, :105-140, :151-161, :184-235; pms-text-outbox-store.ts:23, :149-165; appfolio-inbox-text.ts:59-60, 195-240, 285-362; text-thread-start.ts:28, 80-112; watch.ts:153, 234, 256.
  • Live runs: session 002 door/readiness/runs-oct19-night.md (R2-R4, Oct 20 14:00Z reminder, L3 at 16:58Z); appfolio-door-flip-readiness rows 1-18.

Audit 4: flip, rollback, and what to watch on day one

Read-only audit, Oct 21 04:00Z The switch and its rollback are a single setting with no cache: a change applies on the next text, and the copy job reads it within a minute. Rolling back is safe for sending, but quiet for listening: AppFolio replies stop waking Clara, and no alert fires.

Operating and rolling back the AppFolio text door at Western Slope

Checked against the code on main (website build df30ee1a) and against production data. Today the door is off at Western Slope (no value stored). Leasing runs live, not Supervised. Maintenance runs in drafts. A teammate hold under the door therefore shows in no Slack channel.

1. Flip and rollback

Turn onRoll back
PreferredAdmin, Customers, Western Slope, Modules card, "Where Clara's texts go out" toggle ON. This records who and when.Same toggle OFF
FallbackRaw write below with appfolio_inboxRaw write below with clara_line
Takes effectNext text (a plain read, no cache). Reply wakes start on the next minute tick.Same
Who pressesFede, in his own wordsOwner on incident. It is a rollback, not a turn-on.
aws dynamodb update-item --region us-east-1 --table-name propflow-prod \
 --key '{"PK":{"S":"ORG#org_c42d9150-b9ad-46f8-8f5f-b0fe56bacf38"},"SK":{"S":"PROFILE"}}' \
 --update-expression 'SET pmsTextDoor=:d, pmsTextDoorChangedAt=:n, pmsTextDoorChangedBy=:a' \
 --condition-expression 'attribute_exists(PK)' \
 --expression-attribute-values '{":d":{"S":"clara_line"},":n":{"S":"'$(date -u +%FT%T.000Z)'"},":a":{"S":"<your user id>"}}'

2. Who a rollback touches

Print the list before you flip back. The query applies the sender's own rule: the newest text in a thread decides which number Clara uses. Script: scratchpad/a5/door-threads.py, run with python3 -I. It is read-only.

Today (door off)CountAt rollback
Threads whose newest text came in through AppFolio (the inbox copy)44Their AppFolio replies stop waking Clara. The reason is recorded as "the team holds this", so nothing pages.
Threads Clara has texted through the door0Their next text comes from Clara's number, +19708220641. No intro text goes out automatically (no template exists; see #21316). Sending one manually is outbound to customers, so it is Fede's call.
Held door texts in the outbox0Correction They do not expire in 7 days. Only the alert latch has a 7-day expiry. Held texts freeze with no expiry, because the drain skips any company whose door is off. If the door is flipped on again, they expire at once and fire a false "door down" alert.
# held texts that would trip a false alert on a later re-flip
aws dynamodb query --region us-east-1 --table-name propflow-prod \
 --key-condition-expression 'PK=:p AND begins_with(SK,:r)' --filter-expression '#s=:h' \
 --expression-attribute-names '{"#s":"state"}' \
 --expression-attribute-values '{":p":{"S":"PMSTEXTOUTBOX#org_c42d9150-b9ad-46f8-8f5f-b0fe56bacf38"},":r":{"S":"REC#"},":h":{"S":"held"}}'

3. Day-one watch

SurfaceNowHealthy on day one
Wakes by reasonnone (door off)Mostly woke. Watch for teammate_after_clara and clara_not_on_thread: both are silent.
Outbox held texts and alert latches0 rowsno_address on callers with no guest card is expected. Any door_down is real unless it matches #21068 or #20990.
Inbox-copy holds37, all no_propertyFlat
AppFolio budget3,398 requests used of the 4,000 daily cap; the 600-request reserve hit Oct 20Reserve hit late or never. Door-on estimate: about 15:30-17:00 Denver.
Door-down alertSends to Sentry as a fatal errorNo #alerts post was found for the Willows episode on Oct 16. Treat it as Sentry only until shown otherwise.
Leak check20 texts delivered from Clara's line on Oct 20Texts from Clara's line to staff, vendors and maintenance tenants are still expected after the flip. A leak is a text to a leasing prospect.
aws logs filter-log-events --region us-east-1 --log-group-name /aws/lambda/propflow-appfolio-sync-prod \
 --start-time $(($(date +%s)-86400))000 --filter-pattern '"decision="' --query 'events[].message' --output text \
 | grep -o 'org=org_c42d9150[^ ]* pms=[^ ]* decision=[a-z_]*' | awk '{print $3}' | sort | uniq -c
aws dynamodb query --region us-east-1 --table-name propflow-prod --key-condition-expression 'PK=:p' \
 --expression-attribute-values '{":p":{"S":"PMSTEXTOUTBOX#org_c42d9150-b9ad-46f8-8f5f-b0fe56bacf38"}}' \
 --query 'Items[].[SK.S,state.S,reason.S]' --output text
aws dynamodb query --region us-east-1 --table-name propflow-prod --select COUNT \
 --key-condition-expression 'PK=:p AND begins_with(SK,:h)' \
 --expression-attribute-values '{":p":{"S":"PMSMIRROR#appfolio#westernslopepm"},":h":{"S":"HOLD#"}}'
aws dynamodb get-item --region us-east-1 --table-name propflow-prod \
 --key '{"PK":{"S":"PMSMIRROR#appfolio#westernslopepm"},"SK":{"S":"STATE"}}' \
 --query 'Item.watch.M.[day.S,dayRequests.N,reserveHeldDay.S,lastTickAt.S]'   # plus the peek fields once that branch lands
KEY_SID=$(security find-generic-password -a twilio-api-key-sid -w); KEY_SECRET=$(security find-generic-password -a twilio-api-key-secret -w)
curl -s -u "$KEY_SID:$KEY_SECRET" "https://api.twilio.com/2010-04-01/Accounts/ACff85d85c80b83355a53032a5f31d7bed/Messages.json?From=%2B19708220641&DateSent%3E=<flip date>&PageSize=1000" \
 | jq -r '.messages[] | [.date_sent,.to,.status] | @tsv'   # keep rows after the flip instant; match To against prospects

4. Pages on day one: noise or real

PageVerdict
Door-down after a read-back timeout (#21068)noise The text was delivered. Check the phone or Twilio before acting.
Door-down from an expired held text (#20990)noise-ish The same alert title, but the reason starts expired:
Door-down on a re-flip (held texts frozen, above)noise Predictable
"No reply for 15 minutes" on a reply the door holdsreal A person is waiting. A door-down follows within 30 minutes.
"No reply for 15 minutes" on an inbox-copy replyfixed The last false one was Oct 20, 15:00Z. #21474 is deployed.
"Bench-bounce"Not found in #alerts in the last 48 h. The nearest pages are two Oct 20 email bounces at a real Western Slope home. Those are real and unrelated to the door.

A real incident is a person waiting, which shows as a held reply or a text leaking onto Clara's line. A wrong no-wake decision is silent by design, so read the wake counts every few hours on day one.

Evening and overnight delay (Fede, Oct 21 morning: "too slow for leads")

Read-only inspection Oct 21 ~14:30Z. Numbers from the schedule code, the sync log and the Western Slope state row; "inferred" where marked.

Why it is slow. Two causes. The inbox copy spends its 4,000-request day (our own cap after the Situs 429 storm) by about 6:30 PM and drops to one read every 15 minutes. And after midnight the schedule itself reads only every 30 minutes, worse than the 15 minutes reported earlier. A third cause makes every evening conversation cold: Clara's own door send does not start the fast 60-second mode; only a newly found inbound text does.

Denver timeReadsLead's reply → Clara's answer, typical / worstAfter the reserve is hit
12–6 AMevery 30 min~16 / ~32 minn/a (budget resets at midnight)
6–8 AMevery 5 min~4 / ~7 minn/a
8 AM–7 PMevery 60 s~2 / ~2.5 min~9 / ~17 min (from ~6:30 PM)
7–10 PMevery 5 min~4 / ~7 min~9 / ~17 min
10 PM–12 AMevery 15 min~9 / ~17 min~9 / ~17 min

Where the budget goes (average Oct 16–20, 3,502 a day): inbox reads 65% (762 reads × 3 requests: one for the page, two session checks the robot makes on every call), guest-card email re-reads 31% (every 2 h), thread reads 3%, door sends 0 so far. Who it touches: about 1–2 leads a day write after 6:30 PM (inferred from 129 texts to Clara's line since Oct 2: 40 after 5 PM, 9 between 7 and 9 PM, none after 9 PM); overnight is rare.

Fix, no cap raise, no switch (building now, Oct 21):

  1. Robot: one session check per call, none within a minute of a success. Cuts ~1,300 requests a day. Robot PR appfolio-browser-agent#586 (armed). Our counter charged a fixed 2 per call for those checks; a PropFlow PR makes it charge the real number the robot now reports (building).
  2. Inbox copy: a Clara door send starts fast mode; the reserve peek honors fast mode (60 s while a conversation is live, still never below the floor); email re-reads every 6 h instead of 2 h (saves ~700). PR #21590 (armed; four planted defects red, 531 inbox-copy tests green).
  3. Schedule: read every 60 s from 6 AM to midnight and every 2 min overnight. Lands only after step 1 is live, or the copy would hit the reserve every evening. Expected total ≈2,830 a day, under the 3,397 reserve line (inferred). Result: evening ~2 min, overnight worst ~3.5 min.

Rejected: a cheaper "anything new?" signal (the list read is already the cheapest check; AppFolio's "you received a message" emails reach no mailbox we read); raising the cap (would need ~5,400 a day; AppFolio's real limit is unknown, Fede's call, not needed); longer fast mode after a send (does not help a cold evening reply).

Oct 20: is the text door ready at Western Slope? (read-only, pending review)

Withdrawn Oct 20 evening The "turn on now" recommendation below is withdrawn. Fede, Oct 20 evening: "no turning on today"; leasing only; a tenant's AppFolio text is copied, never a turn (#21563); a staff text should not stop Clara for good (design question); the inbox copy going blind at 7 PM on budget is worse than today. A full overnight audit and the five design decisions are in Oct 21: audit and re-inspection · Evening delay. The numbers and the gap list below still hold.

A. Is the text door ready at Western Slope? (read Oct 20, 21:10Z)

The path Western Slope hits most has now worked end to end once. A new lead arrives, Clara's first text opens the AppFolio thread from the lead's guest card, the lead replies, and Clara answers in the same thread. That ran at the Willows on Oct 20 at 16:58Z on a fresh test number (Fede approved buying it at 02:50Z). It cost about 27 AppFolio requests, the reply round trip took 99 s, and nothing went out on Clara's own line. Since Oct 19, five things have changed:

  • Requests are now set aside for sends.
  • Some senders now name the thread they belong to: teammate answers relayed to the renter, and texts to household members.
  • Every tenant or prospect reply in AppFolio now wakes Clara.
  • A tour reminder has gone out through AppFolio.
  • Callers now get a guest card after the call.

Settings, read now: the door is unset at every company except the Willows test company, so every other company texts from Clara's line.

1. Would people get the right number?

"Right number" means the number the person last wrote to. We replayed the door's rules from the current code over everyone Clara texted, with the door turned on. Nothing was sent.

WindowPeopleRight numberStay on Clara's lineMove to AppFolio
Since Oct 2 (the page's 41 was 33/41 on Oct 18 data)4936 (73%)3910
Last 14 days3525 (71%)2510

There are 13 misses since Oct 2, in three groups:

  • 8 people last wrote to staff in AppFolio, either before the inbox copy started or on another conversation. Clara keeps her own line for them, as decided.
  • 2 tenants whose AppFolio texts are still parked in the inbox copy.
  • 3 prospects (…4860, …4203, …2516) whose newest text is Kat's in AppFolio. They move to AppFolio by design.

Most people already talking to Clara keep her number. The flip mainly changes brand-new leads. If the 35 people from the last 14 days were all brand-new:

  • 31 could be reached in AppFolio: 23 already have a thread there and 8 would get a new thread from their guest card.
  • 4 would get no text at all. All 4 are phone callers with no AppFolio card, and their calls came before the "card after every call" fix.

2. What is held at Western Slope right now

WhatCountOldestWhyWhat the door would do
Texts the door itself is holding0–The door has never been on here–
Inbox copy, parked as "tenants later"707 (180 from renters)Oct 1Parked before tenant texts started being filed (Oct 20)Nothing. They are never offered again and can't wake Clara.
Inbox copy, "same number on two leases"27Oct 14Ambiguous personNothing. They are never offered again.
Inbox copy, "no match"13Oct 2Number matches no oneThey are offered again, but all 13 are history, so filing them is silent.

3. Teammate hold: what happens when Kat texts in AppFolio

  • Follow-ups: they are refused one by one at send time. That covers texts on Clara's own line on that conversation too.
  • How long: forever. Nothing releases it, and a prospect's reply doesn't either.
  • Tours: confirmations and reminders still go out.
  • What staff see: nothing in PropFlow or AppFolio, apart from a Slack line in Supervised mode.
  • Prospect replies to Kat: Clara stays asleep.
  • Today, with the door off: the hold does nothing. Since Oct 16, staff texted 33 people in AppFolio, and 12 of them are people Clara texted in the last 14 days.

4. AppFolio request budget

Today's count is 2,701 of 4,000 at 15:08 Denver. The inbox copy now stops at 3,400 so sends always keep 600. It hit that limit on the night of Oct 19. When that happens, replies in AppFolio wait until after midnight Denver.

A send costs about 7 requests into an existing thread and about 20 to start a new one. Clara sent 134 texts to renters and prospects in the last 14 days: a median of 7 a day, peak 23. Even if all 23 opened new threads, 23 × 20 = 460 requests, under the 600 reserve (arithmetic).

5. Situs and the setting

Situs: no Clara seat on the company connection or on any of 53 property rows, no inbox copy, no budget row. AppFolio there has two Clara users, 1254 and 1259, per session 000. Western Slope's seat is user 540. The inbox copy already labels that user's texts as Clara's: 2 live rows since Oct 16.

Gap list

#GapBlocks WS today?Smallest close
1Fede's Oct 19 hold (lifted Oct 20 evening). The "tenants wake Clara too" spillover (about 9 since Oct 16, estimate) is removed by #21563: a tenant's AppFolio text is copied, never a turn.Until #21563 is merged and deployedDecided Oct 20 evening: hold lifted ("you can turn on anytime you think we are ready"), leasing only, tenants kept out: fix PR #21563 (no switch). Flip after a green Willows dry run on the deployed build; this session owns the flip, the watch and the rollback
2The teammate hold is permanent and invisible to staff.No it fails toward silenceHand-back pick H1; a list of muted threads
3A caller with no AppFolio card gets no text (4 of 35).No card-after-call fix merged Oct 20, not yet seen liveRead-only: the first post-call door send is sent, not held
4The hold has never run with a real teammate.NoRead the first held conversation after Kat's first text
5The inbox copy pauses at 3,400 in the evening.No sends keep their reserveTrim the copy's reads later
6The read-back can raise a false "door down" (#21068).No didn't recur Oct 20The code fix
7Only 3 cards are tried when starting a thread (#20636).No 8 of 8 have one cardNone needed now
8Maintenance and renewal-tool texts stay on Clara's line.NoSay so to WS
9A "transactional" send can still reach a stopped number (#21279).NoA7 fix
10Situs has no Clara seat and no inbox copy.No (Situs only)Record user 1254 or 1259, then turn on the inbox copy

Sources: propflow-prod read-only, Oct 20 ~21:10Z. ORG#<id>/PROFILE (7 orgs via entityType-createdAt-index GSI3PK=Organization); ORG#…/ATTACH#pms_credential; PROP META via proporg-index; PMSMIRROR#appfolio#westernslopepm STATE (dayRequests, reserveHeldDay) and MSG# (976 rows); PMSTEXTOUTBOX#<org>; CONV# via conversation-list-index since Oct 2 plus PERSON#…/INQUIRY#. Replay: session 002's fetch.py and replay.ts, re-pointed at origin/main 46638b5bc7 (resolvePmsTextAddress, pickPmsTextStart, pmsTextStartRecordsOf, threadTextDoorOf), scratchpad q/. Tenant-record starts are not modelled. One cohort entry with an email address in its phone field is excluded from the counts. Willows L3 run: session 002 runs-oct19-night.md and readiness row 6. Code: wake-on-reply.ts:51-77, file-message.ts:430-443, dispatch-envelope.ts:462-469, auto-reply-hold.ts:66-89, pms-mirror-store.ts:213, watch.ts:153-154, appfolio-text-start.ts:68-77.

Oct 20: what lands on the AppFolio guest card, per customer, and one notes policy (decided Oct 21)

Decided Oct 21  Fede chose option (a), consistent and complete, with no setting ("I also don't want a setting"). Build anchored on #21618. Fede also ruled on the Showings setting the same night: "why is there a switch for this? If they have AppFolio it should always be yes." That switch is deleted; tours become AppFolio Showings at every company with AppFolio connected.

Built Oct 21–22  Live everywhere since Oct 22, 01:06Z. Every Clara contact and every outcome now lands on the guest card as a note, the same way at Western Slope, Camellia and Situs. No switch anywhere.

What shipped (all merged)

PieceWhat changes on the cardPR
Message notesEvery reply Clara sends by text or email gets a note. Skipped only when the text went out through AppFolio's own texting line, which AppFolio already shows.#21637
First text, nudges, remindersThe first text to a lead, each follow-up when a lead goes quiet, and tour reminders and confirmations get a note.#21667
Outcome notesTour booked, moved, cancelled; not interested; message taken for the team; lead archived; self-guided code sent; toured on the call. One neutral note writer, no PMS names in core code.#21622, #21677
Texting switch deletedThe Willows-only "write notes from texts" setting is gone; notes write everywhere.#21669
Ordering fixNotes queued before the guest card exists now go out in the same pass the card is created, not 15 minutes later (found on the Willows run below: a booked-tour note landed 7.7 minutes late).#21687
Showings switch deletedTours become AppFolio Showings at every company with AppFolio connected. Turns on at Camellia and Situs on merge; Western Slope already had it on.#21699 (merged Oct 22 05:09Z, live 06:15Z)
HarnessThe touring harness gained a notes dimension: 76 journey cases across both text doors, each listing every expected card entry, all passing, with mutation controls that go red when a note step is removed.#21630, #21656

Proof

  • Willows live runs (Oct 22): three bench renters texted the Willows line. Run 1 (01:07–01:20Z, card 325) booked, moved and cancelled a tour: four outcome notes on the card, all eight queued writes landed. Runs 2 and 3 (06:30–06:48Z, cards 311 and 331): every Clara reply got its note within about 5 seconds, and a brand-new card and its "Tour booked" note arrived 3.5 seconds apart (the ordering fix proven live; before it the note trailed by 7.7 minutes).
  • Read-only replay of real cards: Camellia (50 cards) and Western Slope (34 cards) replayed on the old and the new code: 0 reply/no-reply flips, 0 subject changes, wording drift between new and old equal to the drift between two old runs (the engine calls the live model each run, so byte-identical is not measurable). The replay wrote 0 notes on both, as designed: a replay never sends a first touch.
  • Showings: switch gone, live proof still open. Three booking runs on Oct 22 could not produce a Showing, each for a different reason. Runs 1 and 2: the tour was saved without the unit the renter chose (once with no unit, once with a listing placeholder), fixed in #21727 (merged, live 10:47Z). Run 3 on the fixed code: Clara would not offer unit 102, the only Willows unit that exists in AppFolio, because another test had put an approved application on it at 03:16Z (an approved application holds a home, the Oct 15 rule), and the bench number's earlier tour was moved instead of a new one booked. Open, one issue each: the six seeded Willows units that do not exist in AppFolio (#21725), the failed Showing write retrying without end (#21726), a listing placeholder stored as the card's apartment (#21728), Clara inventing "occupied" for a held unit (#21735), a moved tour dropping the agreed unit (#21736). The Showing path itself worked at 01:07Z the same night (card 325, unit 102 named up front, real Showing on the card). Next proof: once test application #128 clears unit 102, one text booking from a bench number with no open tour. First real booking at Camellia and Situs is the live proof there.

Follow-ups, one issue each: an approved re-send of a lockbox code gets no note of its own (#21688); the rest of the reviewer's suggestions are on #21635.

Which channel a prospect notification uses Decided Oct 22

Fede, Oct 22: "The rule was to go back on the channel the person used. We need to change that. If it was through email and now we have the phone number, we send through both. If it was just through text, we just confirm through text. The addition is always communicating through the phone number." Applies to every notification Clara sends on her own, not only the tour confirmation. Origin: a Zillow email lead at Western Slope gave her number when Clara asked, booked, and got the confirmation by email only.

RuleTextEmail
We hold a number the person gave us (never one found in a signature; STOP blocks texts, not emails about their own tour)yesonly if they ever reached us by email
No number we may textnoyes, and the message asks for a number
Text-only leadyesno, until they write to us by email

Proof of the gap, live (Oct 22, 10:30 AM Denver): Tanya's first text from Clara was the day-before tour reminder, sent through Western Slope's AppFolio texting line (filed, read back as AppFolio message 1235624713). It was also the first Clara text ever to leave through the AppFolio door at a customer. Her confirmation the day before went by email only; #21753 and #21747 have been live since 10:45 AM Denver Oct 22; from now on the confirmation is the first text. Willows live proof the same afternoon: the confirmation text does not go out when the renter picks a time by email (the reply pipeline creates the tour without the text step); fix in progress; Tanya's first text was the reminder, not a confirmation.

What the audit found (Oct 22, read-only, every sender traced): about ten senders text whatever number is on file with no check that the person gave it (both tour reminders, the slot-gone notice, the three self-guided notes, the application link, staff-asked texts, household copies); almost every sender picks one channel, never both; email-only leads are never told their tour slot is gone; a reminder whose text is refused sends nothing even when we hold an email; three slightly different consent checks exist.

PRCoversState
1. Shared channel ruleOne helper (text / email / both) used by the booked-tour confirmation, the prospect-asked tour-moved confirmation, and both follow-up cadencesbuilding, branch fede/tour-confirmation-all-channels
2. Reminders and nudges24h and 1h reminders, slot-gone notice (adds the missing email path), weekend check-in, confirm nudge, after-tour follow-up; a refused text falls back to emailafter PR 1 merges
3. Codes, desk, applicationSelf-guided code and notes, staff-booked showing notices, application link (adds the text), household copies per person; one consent check everywhereafter PR 1 merges

Decided under the standing grant, with the audit's recommendation (say "hold" on any): an email lead who cancels or moves by email also gets the text; a text-only lead gets emails only once they write to us by email; the lockbox code follows the new rule with email as the backup when the text fails (replaces the Oct 20 "every channel we have" line); a lead who texted STOP still gets emails about their own tour; a call lead with an email on file gets text only; when the only number we have came from a signature, Clara asks "best number to reach you?" so the lead still gets texts. Four docs pages state the old same-channel rule (clara-coworker-architecture, settings-cleanup-strategy-2026-09, leasing-engine-designs, self-guided-tour-reach-2026-10) and get a one-line correction when PR 1 merges.

Card notes, written like a leasing agent Proposed — pending Fede's go

Fede, Oct 22: "we're trying to imitate a human… Anytime there's a major note: we followed up, the person said this, this was the outcome, this is what we learned. Not every piece of metadata every turn." It replaces the "one line per message" shape above; the inventory above still holds.

1. The rule

A note is what a leasing agent would jot down after a touch that mattered. It is past tense, one or two short sentences, 5 to 30 words. It says what the renter said or needs, what we did or offered, how it ended, the next step when there is one, and anything we learned. It never carries the channel line, the address, the subject, the time, the renter's name or contact details, greetings, or anything AppFolio already shows on the card. That covers texts and emails sent through AppFolio, Showings and application events. First person is fine: "Emailed her the open times." Western Slope and Situs staff write exactly this: 6 to 7 words, after calls and showings.

PartKeptExample
What they said or needYes, facts they statedWants to tour Fri morning or Sat. Has one dog.
What we did or offeredYesEmailed open times (Fri 10:30–12, Sat 10–2:30).
OutcomeYesShe picked Fri 10:30. / Not interested: "found a place closer to work."
Next stepWhen there is one. A future date is the one date we writeBooked Fri 10/23 10:30 AM.
Channel line, subject, address, time, name, phone, email, greeting, full messageNever—

2. When Clara writes one

Leasing-mailbox emails and texts from Clara's own line don't show in AppFolio, so a touch that moved the conversation gets a line. Texts through AppFolio's inbox already show, so no line. A fast back-and-forth is one line, written at an outcome or after 15 quiet minutes, from our own records. That also fixes Tanya's four contacts that were lost because her card didn't exist yet.

Today's noteNew behavior
New lead email (renter's first message)Drop when AppFolio made the card (its inquiry panel shows the message). Keep as one line, in the renter's words, when Clara made the card.
First email reply, later email repliesKeep as a line only if it moved the conversation (offered times, answered a question, asked for something). Merged with the renter's replies into one line per exchange.
Text replies, Clara's own lineSame as email: one line per exchange that moved.
Text replies, AppFolio texting inboxDrop. AppFolio shows them.
Follow-up nudgesFold into one rollup on the closing line: "Followed up 3 times by email over 2 weeks, no reply." If the renter answers, her reply's line says "Replied after the 2nd follow-up."
Tour remindersFold into the tour-day outcome only when it matters: "No-show. Day-before reminder went out, no reply."
Phone callKeep, rewritten to the rule: no channel line, no number, 30 words at most.
Message sent in the same turn as a booking, move, cancel or hand-offFold into the outcome line ("…confirmed by email").
Tour booked / moved / cancelled, toured, no-show, running lateKeep. Short form: "Booked Fri 10/23 10:30 AM, in person."
Not interested (before or after a tour)Keep, with the reason in the renter's words.
Message taken, handed off, callback asked, lockbox code sent, archivedKeep.
Application sentDrop when sent through AppFolio (it logs it). Keep otherwise.
No reply after 2 weeksKeep, and it carries the nudge rollup.
Team's answer to a question Clara passed onKeep, shortened: "Team: no smoking on the balcony."

3. How the text is made

Part of the lineMade byRisk
Outcome, slot, next step, counts ("3 times"), channel word ("by email")Our own records, fixed sentencesNone beyond today's
What we offeredThe tool calls Clara made (times offered, answer given)None: read from what was sent
What they said or needThe model fills fixed fields (move-in, beds, budget, pets, number of people, question asked, reason for stopping), with a forced tool call. The sentence is then built from those fields, not written by the modelA wrong or invented fact. Guarded by: every value must appear in the renter's own message; empty field = left out

Fair housing: only facts she stated. No judgments, no guesses, nothing on race, religion, origin, language, disability, family makeup or age. "2 people" is fine; who they are is not. No field exists for it.

Versus copy-paste: a copy can't misstate, but it is noisy and copies protected details verbatim. Fields can misstate, so every value is checked against her words. Fixed sentences cover the common cases (booked, moved, cancelled, toured, no-show, no reply, archived, message taken, handed off, code sent), so most lines never touch the model.

4. Three cards, before and after (newest first)

Tanya, Zillow email lead, card 3035. 4 notes today (2 full emails, 4 contacts missing) → 2 notes covering all 10 contacts.

BEFORE
Email · to 1…@convo.zillow.com
Subject: Re: Tanya is requesting information…
Hi Tanya, You're all set — tour confirmed for Friday, October 23 at
10:30 AM at 2841 Morningside Court…

Tour booked for Fri, Oct 23 at 10:30 AM, in person.

Email · to 1…@convo.zillow.com
Subject: Interested in 2841 Morningside Court
Hi Tanya, The home at 2841 Morningside Court is open now — a 4-bedroom.
I've got Friday morning open between 10:30 AM and 12:00 PM, or Saturday
between 10:00 AM and 2:30 PM…

Email
Subject: Tanya is requesting information…
I would like to schedule a tour. Possibly Friday morning or Saturday?

(missing: Clara asking for her number; her "Friday at 10:30 works",
her number, her "see you then")
AFTER
Booked Fri 10/23 10:30 AM, in person. Confirmed by email.

Wants to tour Fri morning or Sat. Emailed open times (Fri 10:30–12,
Sat 10–2:30); she picked Fri 10:30 and sent her number when asked.

Phone lead, made-up example. 5 notes today (a call block, the booking, 2 reminder copies from Clara's line, toured) → 2.

BEFORE
Toured.
Text · to …4421 / Hi Maria, just a reminder your tour is in one hour…
Text · to …4421 / Hi Maria, a reminder about your tour tomorrow at…
Tour booked for Sat, Nov 7 at 11:00 AM, in person.
Call · with …4421 / Caller asked about 2-bedroom availability at…
(a paragraph summary of the call)
AFTER
Toured. Asked to apply; sent the application link.

Called in, wants a 2 bed by 12/1, has one dog. Told her dogs are OK
with a pet deposit; booked Sat 11/7 11 AM.

Renter who went quiet, made-up Zumper lead (AppFolio made the card). 6 notes today (the inbound copy, first reply, 3 nudge copies, no reply) → 2.

BEFORE
No reply after 2 weeks. Archived.
Email · to … / Subject: Re: … / Hi Jordan, just checking in one last time…
Email · to … / Subject: Re: … / Hi Jordan, still interested in…
Email · to … / Subject: Re: … / Hi Jordan, wanted to follow up…
Email · to … / Subject: Re: … / Hi Jordan, Thanks for reaching out…
Email / Subject: New Zumper lead… / Do you allow cats? Moving Dec 1.
AFTER
Followed up 3 times by email over 2 weeks, no reply. Archived.

Asked about cats and a Dec 1 move. Emailed: cats OK, open Dec 1,
offered Thu or Sat tours.

5. What it takes

PieceChange
Email and text turn notes, the first "Email Sent" commentsReplaced by one exchange line, written at an outcome or after 15 quiet minutes
Nudge and reminder notesRemoved; counted into the closing line or the tour-day line
New-lead copyDropped when AppFolio made the card; one line in her words when Clara did
Call noteSame summary, rewritten to the rule and the fields
Outcome sentencesShortened; gain "confirmed by email" and the nudge rollup
Contacts before the card existsNow covered: lines are written from our records once the card is there

An improvement to live behavior: no switch, live everywhere on merge. Proof:

  1. Read-only replay of real Western Slope and Camellia cards: old notes next to new notes for 20 cards, published on this page.
  2. One Willows run end to end: a real email lead, Clara's replies, a booked tour, and the card read back from AppFolio.
  3. On Fede's go: rewrite card 3035's notes by hand to the new shape, as the gut check on a real Western Slope card.

Fede: three yes/no calls

  • One rollup for follow-ups instead of a line each? Recommend yes: "Followed up 3 times, no reply" says it all.
  • When Clara made the card, keep the renter's first message as one line in her words? Recommend yes: AppFolio has nothing else showing what she asked.
  • First person ("Emailed her the open times") rather than third ("Clara emailed the renter")? Recommend yes: it is how Kat writes at Western Slope, and most lines need no subject at all.

Oct 20 inspection below, kept as written. Read-only at the time; nothing was built, sent or changed that day.

Recommendation: option (a). Every Clara contact and every outcome goes on the guest card, the same way at every customer. When a text goes out through AppFolio's own texting line, AppFolio already shows it, so we add only the outcome notes there.

What staff see on the card today

Fede's belief holds, and it is worse than he thought outside Western Slope. At Western Slope, Clara's first email lands on the card as an "Email Sent" entry, and then the card goes quiet. Nudges and replies never show up, so staff reasonably conclude Clara never followed up. At Camellia, nothing Clara has done since April is on any card. At Situs, Clara has not contacted a single lead yet, so there is nothing to note.

Three causes, all in the code:

  • Follow-up nudges have no note step at all.
  • Replies: email replies never get a note. Text replies get one only if the building has a texting switch turned on, and it is off at every customer.
  • Other notes are uneven: some outcomes write a note, some write nothing, and tours become AppFolio Showings only where a company setting is on (on at Western Slope only).
Clara actionWestern Slope (since Sep 17)
actions / landed / share
Camellia (since Apr 1)
actions / landed
Situs (since Sep 29)
First contact, email83 / 78 / 94%128 leads / 0 0%0 actions. 213 leads on file, Clara has not contacted any yet (texts wait for the AppFolio texting line).
First contact, text4 / 0 / 0%(in the row above) / 0
Replies in a conversation111 / 3 / 3%432 / 0
Follow-up nudges (lead went quiet)98 / 0 / 0%219 / 0
Tour booked36 / 31 as AppFolio Showings / 86%87 / 0
Tour moved / cancelled5 / 5 and 4 / 211 / 0 and 16 / 0
Tour reminders and confirmations to the renter38 / 075 / 0
Self-guided access code sent0 sent (no note step exists)0 sent
Application sent25 / 24 / 96%47 links / 0
Handed off to the team (text/email)11 conversations / 3 / 27%18 / 0
Prospect phone calls60 / 26 / 43%333 / 0
Went cold or not interested12 leads / 1 (landed today) / 8%11 / 0

"Landed" means our robot confirmed the write. Check against AppFolio's own Oct 9 export for Western Slope: 14 application notes on both sides, and 47 "Email Sent" entries vs our 48. Camellia: 0 of the 83 Camellia guest cards created since April carry any note, and the card-write path only existed from Sept 18. Since then, Camellia saw 36 leads contacted, 76 nudges, 17 tours and 61 calls, with zero writes until a first card was created today. Counts include any bench tests on customer lines; none were flagged.

AppFolio's texting line vs Clara's line

AppFolio logs texts sent through its own texting line on the card by itself: Western Slope and Situs cards show "Text Sent" activity with no Clara involvement. Texts from Clara's own number are invisible to AppFolio. Today the code has no rule to skip our text note when the text went through AppFolio's line. At the Willows, where that line is on, 39 text notes were written Oct 16–20, so those texts probably appear twice (inference; not checked message by message). The fix: one rule, keyed on the line. On Clara's line we write the message note. On AppFolio's line we don't, because AppFolio already does. Outcome notes are written on both.

Before → after for a typical Western Slope lead

ActionCard todayUnder (a)Example note
First emailEmail Sent entrysame(full email, as today)
Reply / first textnothingnoteClara texted: our standard lease is 12 months.
NudgenothingnoteFollow-up 2 of 4 (text): still interested in 2841 Morningside? No reply yet.
Tour booked / movedShowing onlyShowing + noteTour booked Wed 11:00 AM, virtual. Moved to Fri 1:30 PM at renter's ask.
Code sentnothingnoteSelf-guided code sent for Sat 10:00 AM at 2988 Krista.
Handed offsometimesalwaysAsked about a 6-month lease; handed to the leasing team.
Went coldalmost nevernote + inactiveNo reply after 4 follow-ups over 2 weeks; marked inactive.
Call43% of callsevery callCall: asked about 2978 Emerald; transferred to leasing.

Decision for Fede

On Sept 22 Fede said "we don't need to write every turn, that's too much noise". That ruling is what produced today's one entry and then silence. Picking (a) or (b) reverses it.

OptionWhat the card showsVolume at Western Slope (Sep 17 – Oct 20)
(a) Consistent, complete recommendedEvery contact and every outcome, at all three customers; message notes skipped only where AppFolio already shows the textabout 490 entries over 68 leads (about 7 per lead, about 15 a day)
(b) Consistent, terseOne note per lead per day summarising what Clara did, plus outcomes244 notes (lead-days with any Clara action)
(c) As-isFirst email only at Western Slope; nothing at Camellia or Situstoday's 78 first-contact entries

Sources: production write-intent outbox (all 1,649 rows), conversation message rows, tour rows and prospect inquiries per company; AppFolio guest-card export of Oct 9 (guest-card corpus); code at origin/main 46638b5bc7 (guest-card outbox, note intents, text-note filing, prospect outreach, showing sync).

Overnight build, Oct 12 to 13: what shipped and what Fede decides this morning

TL;DR. 15 PRs merged. The mirror pieces sit dark behind one setting, pms_inbox_mirror, default off, on only at the test company; the reader fixes are live everywhere. Lookups (#19264) and runner-B (#19276) merged. All twenty PRs of slice 1 are merged. The every-minute watch is live at the Willows test company since 6:18 am Denver; Western Slope and Situs stay off. Western Slope is not on.

Nothing was sent to anyone. An import check (#19233) goes red if the filer reaches sending code; the lookups and writer are pinned to exact import lists. The harness now runs on the real lookups and filer: 55/55 pass, 55/55 planted defects red. The no-send scenario landed 42 rows with 0 wakes, holds or sends; its planted texting engine was caught. The real ingest path filed 41 rows, 0 outbound. The earlier run proved nothing: it still fed the old importer, so nothing landed. One bug found: bulk blasts lost "sent to 200 residents" (#19282, armed).

BEFORE: OCT 12, 8 PM None of the chain existed. A text in the AppFolio inbox never reached PropFlow, so Clara answered her own line blind. Plain box: merged. Accent box: open or armed, not merged yet. AFTER: OCT 13, MORNING 1 AppFolio inbox The office texting line, read only. Willows test line watched. 2 Robot read MERGED Robot list route live; adapter #19191. 3 Watch job MERGED Schedule #19243, core #19248, runner #19263. 4 Placement and lookups MERGED: placement #19218, filer #19233, lookups #19264. 5 PropFlow conversation MERGED Labels #19232, rendering #19204. The clock (lambda) OPEN #19281, in review. Until it merges, nothing ticks. Report command OPEN #19284, in review. Lag and request numbers. Runner-B: file new rows MERGED #19276, not proven live.
The mirror chain, before and after the night. Nothing ticks until the clock merges.

Merged and open tonight

PRWhat it does
#19167The shared shape of a mirrored text, and the off-by-default company setting.
#19191Turns the robot's inbox pages into that shape; a bulk blast stays one bulk row, automatic notices arrive as messages from a user named "System" (108 captured rows); only the 5 auto-replies have no sender (ws-shadow/raw).
#19187A mirrored text wakes nothing: no Clara reply, no hold, no follow-up touch.
#19198The readers that judge "who answered last" see staff texts as staff, not as Clara. A fix, live everywhere.
#19218Places a text on the right person: AppFolio ids, then phone when the name agrees, then the unit's leases; a filed row is never overwritten.
#19222Tests for the message handler.
#19219The raw store, one per AppFolio database, with the wall: rows for a company whose setting is off are dropped.
#19220Clara's decision engines handle mirrored rows; history rows move nothing.
#19232Labels each row by who sent it: a named staff member, a bulk blast, an AppFolio notice.
#19233Files a stored row into its conversation, carrying the no-send import check.
#19243The schedule: every minute in office hours, 5 min early and evening, 15 min late, 30 min overnight, every minute for 30 min after any change.
#19246One writer per door: on a mirrored company the old guest-card importer files only emails and closes its text rows.
#19248The watch itself: reads the list, remembers how far it got, catches up after a gap and backfills a newly turned-on company.
#19204What staff and Clara see: "Kat · AppFolio text, Oct 9 4:12 PM", a bulk blast once with a count. Unchanged without mirrored rows.
#19263Runs one tick per database, with a claim so two runners never read the same database at once.
#19264merged · not proven live The production lookups placement needs; a cross-kind id match now also needs the name to agree.
#19276merged · not proven live Runner-B: files new rows into conversations.
#19281merged · not proven live The lambda: the clock that runs the watch. The review closed two blockers before merge: two overlapping runs could have filed the same text twice, and a stale-session marker could have forced a login, which the watch must never do.
#19284merged · not proven live The report command: per-tick and lag numbers.
#19282merged · not proven live Keeps a bulk blast's recipient count.
#19249Coworker Line: a staff caller is never handed to the tenant repair-intake agent.
#19254Coworker Line: finds a building from a street address; counts vacancy by unit status.
#19295armed Registers a nightly test workflow the drift check was complaining about on every PR

Willows performance report (the real AppFolio inbox, 0 new rows tonight)

MeasureResult
Ticks in a 75-minute watch4 (3 at the late 15-minute pace, 1 at the overnight 30-minute pace)
Requests per tick3
Seconds per tick2.4 to 2.5
Total requests12
Rows stored0 (80 rows belonged to companies with the setting off; the wall dropped them)
BackfillEmpty: the 7-day window held no texts (newest JP&Co text Oct 6)
Logins triggered0
403s0
Unread flagsUnchanged

No lag numbers (AppFolio time to PropFlow write): that needs a text into the Willows inbox, shared with JP&Co staff, so we sent none unasked. You text the Willows line, or our Twilio test number does on your yes.

Live in production since 12:18Z Oct 13 (6:18 am Denver). The every-minute watch now runs from our sync job for the Willows test company. First three production ticks: 3 requests each, 2.0 to 2.6 s per tick, 0 new rows, 0 duplicates, 0 held, no errors, no paused database. Ticks came 5 minutes apart because the morning band (6 to 8 am) is 5 minutes by design; they go to 1 minute at 8 am. No text has reached the Willows inbox yet, so there is still no lag number: that needs one text into the Willows line.

Western Slope coworker bank

CallRerun 1Rerun 2Final, fresh keys
1passpasspass
2 (balance)passRight; style failAll 5 answers right; judge fails on style (#19274)
3 (hand-off)fail refused without a street addresspass fixed by #19249fail Q5: a company-wide "opened last week" kept the last building (2, not 22). Rule is in the prompt, did not hold; passed at 08:34Z (#19283)
4 (vacancy)fail 7 vs key 6Clara's 7 right; key stalefail count right (7); Q3 read all 8 vacant units, not three and an offer (#19285)
5 (work order)passRight; style failpass
6passpasspass
Score (before: 1/6)4/63/63/6 strict; numbers right except call 3's slip
ControlPlanted wrong key (13) failed as it should; Clara said 7.

Both fixes you asked about are proven: staff never go to tenant repair intake (call 3, Q1 to Q4); vacancy counts by unit status (call 4, Q1). Stale tenant on a vacant unit: #19273. ElevenLabs was degraded 08:55 to 09:35 UTC, and three calls from 09:47 to 10:13 UTC had no audio. Not ours. Sentry shows no new production errors then, which is not proof calls succeeded.

Decisions I took on your standing rules

  • #19212, option A: an unknown number gets a phone-only person, as on Clara's line today.
  • #19215, option B(i): one writer per door. The old importer stops filing texts on a mirrored company (#19246).

Yes / hold this morning

TL;DR: one real ask, the Western Slope turn-on.

  1. Western Slope turn-on: yes once the clock and report merge and a Willows text gives a lag number, and after we confirm at Willows, with the first real text, that reading a changed thread does not clear its unread mark in AppFolio (unverified today, tracked as #19292). The change: today a prospect's AppFolio text wakes Clara and she can text back; mirrored, she reads and never replies until the engage decision.
  2. Known number, unknown name (#19240): accept for slice 1. Same as Clara's line today.
  3. Situs has no front-door row: slice 1 is Western Slope only. Unmatched Situs texts would sit pending.
  4. 64 Western Slope tenants with no person record land on the front door: accept, issue to have the sync create them.
  5. Lag numbers: you text the Willows line, or yes/hold on our Twilio test number.
  6. JP&Co robot-user 403: grant the robot user read access rather than roll back; the mirror and guest-card reads need it. Your call.
  7. #19256: hold, not slice 1. No move-out walk path on the Coworker Line.
  8. #19260: rename 9 Fairhaven test buildings named like real customers.
  9. #19247: block the old backfill script on mirrored companies before any turn-on.
  10. Schedule per database, not per company: accept as designed.
  11. #19151: another lane, no action here.

Dark: pms_inbox_mirror | feature: copy every AppFolio inbox text into the matching PropFlow conversation, labeled, Clara reads never replies | proof: pending (Willows watch ran, 0 rows) | turn-on: Fede

Still open

  • #19228: a text can be sent twice. Open, not touched tonight.
  • Shared-login texts not labelled with a person (#19234).
  • Bulk sends from Clara's own seat are labeled once with a count; not exercised tonight.
  • Harness gaps #19235 and #19241 are closed; left: identity fallbacks and two rendering hooks.
  • Decision 7, the one-year read, runs attended.
  • #19292: whether the thread read clears AppFolio's unread mark
  • #19294: a send import at the top of the shared sync Lambda handler is not caught by any pin (the job and its branch are pinned)
The full proposal
Phase 3: Clara's own sends through the PMS come back in through the watch, recognized by her seat, so each shows once. Clara's phone line Calls and texts on her number. Live today. The customer's email Leasing and staff mailboxes. Live today. Their PMS inbox AppFolio texts, read (emails not read yet) by the robot; thin adapter. This proposal. Listing sites, and later Zillow and Apartments.com today. Yardi and Entrata adapters next. Meetings last. Clara core, the same for every door 1 Front door: one message-arrived event 2 Identity: who, which property, which conversation 3 One conversation per person, every door in it 4 Situation and the company brain 5 Engage decision, then one gate 6 Reply through the door it came in Staff, in PropFlow Thread view, hand-off, alerts. Phase 1: Clara reads, the office keeps answering in AppFolio. Replies, same door Her number and mailbox today. The PMS door in phase 3. The PMS stays the record Work orders, leases, guest cards. Nothing migrates.
Figure 1. One core, many doors. Each place a conversation already happens is a spoke into the same core; the PMS inbox is the new spoke. Everything inside the core is the same for every door and every PMS; the proof that it is generic is a second adapter built without touching it.

1. The gap, measured

FactTodaySource
Where tenants and prospects talk to the officeThe PMS inbox (texts and emails), company-wide, one texting number per company. Who is in it is not settled: Gera's gut number is about 90% tenants; the newest 100 Western Slope threads (9.5) hold 47 applicants, 6 prospects, 11 tenants and 36 numbers AppFolio could not match. The year read (section 9) settles it.Gera sync 10/12; probe 10/12
What PropFlow sees of itNothing at any customer. A guest-card feed exists and is on at Willows (test property) only.code map, verified 10/12
What Clara does with tenant texts on her own numberAnswers from what she knows, blind to the PMS thread. The forever-hold mutes her on a leasing thread after one staff text.brief 10/11
Why the existing feed stays offIt reads every home every minute: about 130,000 robot calls a day at Western Slope's 90 properties. No request budget, no skip when throttled.handler.ts ~5536-5560
Western Slope size90 properties, 284 guest cards (137 active), about 40 new leads a week, 1,526 work orders. Inbox volume: the newest 100 text threads span 10 days, about 10 a day, 35 on a Monday with blasts; 43 of 100 are one-to-one human conversations (9.5).read-only prod 10/12
Where each customer's tenant texts liveWestern Slope: on the office line (the AppFolio texting number), so the inbox is the record; the after-hours emergency number on about 220 fridge magnets forwards to the call center. Situs: our text-only read returned "no messages found"; email cards were not read; Eileen says about half of tenant texting runs on two staff members' personal cells, which no mirror can see. JP&Co: not read yet. This question opens every customer's plan (third pass): Western Slope first, Situs not second.customer voices Sep 18 and Oct 7; third pass 10/12
Renewals todayClara sends them through her own number. If the company talks through the PMS, renewals, maintenance and leasing all move under the same setting (section 8).Fede 10/12

2. The shape: a generic core, adapters at the edge

Three places the core could live. Recommendation first.

OptionWhat it isForAgainst
A. Core in the sync job runner (recommended)A new job class "watch" in the existing PMS sync runner, with a typed adapter interface. The AppFolio adapter is the first module. The robot service stays the browser arm.Smallest build; reuses schedules, budgets, quiet-pass memo, progress rows; one deploy path.The runner is AppFolio-named and shaped; the interface must be enforced by a build check or it rots into AppFolio code.
B. A separate "PMS comms" serviceIts own runner with the adapter interface; the robot as its browser arm; writes into PropFlow through the existing writers.Clean boundary; Yardi and Entrata never touch the AppFolio runner.New plumbing (deploy, secrets, alarms, budgets) before any customer value.
C. The watcher inside the robot serviceGera's "our own webhook": the browser service watches the inbox and posts changes to PropFlow.Change detection next to the session.Business logic (identity, dedupe) ends up in the browser repo; an always-on browser is the costly shape Fede rejected.

The adapter contract (what each PMS module must provide): how to get a session (login, MFA, cookie jar), the list endpoint and its row shape, the thread endpoint and its row shape, the id spaces (party id, message id, property id), the write endpoint for phase 3, and the traps (which calls mark a thread read, rate limits). Everything else, watching, cursors, budgets, dedupe, identity, writing into conversations, is core and never knows which PMS it serves.

2b. One front door for every message

Fede: "one gate where all messages come in, and it doesn't matter if it's Yardi or AppFolio or email or a Twilio text or a phone call: the same core engine, otherwise it will be really hard to maintain." Today the inbound loop is wired per source: the Twilio webhook, the mailbox ingest, the call transcript and the guest-card feed each do their own identity, write and engine wake-up. pending the communication-layers map, which lists how coupled each path is.

Twilio text Mailbox email Phone call transcript AppFolio watch Yardi watch (later) Other apps (later) One front door: a message-arrived event who (ids and claims), door, channel, body, when, source system, source id, backfill flag A connector only produces this event. It never writes a conversation or wakes an engine. Identity the person and the property Store one row per source id; a retry never duplicates Attach to the person's one conversation Engines and readers engage decision, follow-ups, alerts, holds; the thread view and Clara's context
Figure 2. The front door. A connector only produces events; it never writes a conversation or wakes an engine itself. The watch in section 3 is one connector among six.

Minimum slice: a typed event and one handler, used by the AppFolio watch from day one. The Twilio and email paths move behind the same door one PR at a time, each a fix with no switch, proven byte-identical on replay. That is how the "different implementations" get retired instead of multiplied.

2c. Integrations: one connector shape, three protocols

When the other system offersWe speakExamples
A real APIREST calls, webhooks when they existTwilio, Microsoft Graph, the AppFolio Reports API
An agent-facing interfaceMCP or a CLIGoogle Drive, Monday.com, Salesforce (later, if ever)
Only a web app (legacy)The browser agent, exposed as an API: session, list, fetch one, write oneAppFolio's inbox and guest cards today, Yardi next

The connector shape is the same in all three: get a session, list, fetch one, write one, emit front-door events. The browser agent is "just another API", wrapped. Nothing above the connector knows which protocol it spoke. This is the concept only; no other app is in scope now.

3. Read: watch a list, on the session the robot already keeps warm

Fede's question: new browser every poll, or one always on? Neither. The robot already keeps every customer's AppFolio login warm with one plain web request every two minutes (its heartbeat), and starts a browser only to log in or renew, about once a day. A poll is one more plain request on that warm session. So the cost that matters is requests toward the PMS, not browser minutes. All AppFolio traffic stays inside the robot: one cookie jar, one pacer, one lock per customer. The sync runner asks the robot; it never replays cookies itself (a second client on the same session is how bursts and 429s start).

OptionBrowser cost per customerFreshnessCatch
A. New browser session per pollAbout $100 a month at one poll a minute (Browserbase, 6-hour max, idle billed)1 minuteLogin or context restore on every poll; slow; the robot's write pattern, wrong for reads
B. One browser always on, watching the inboxAbout $120 a month, plus reconnect logic every 6 hoursSecondsFragile; no vendor pushes page-change events, we would still be polling the DOM; Fede 10/12: "I don't think an always-on one makes sense"
C. Plain requests on the robot's warm session (recommended)Minutes of browser a day, for login renewal only; about $0On the door's own clock (Fede 10-12, 8 pm: "we need a distribution, and a dynamic polling mechanism"): one minute in busy hours, five in quiet hours, fifteen while people sleep, and one minute for the 30 minutes after any change; the pacer stretches it further only on a 429 or while a sync runs (decision 12, figure 8)The list endpoint must be parsed fully (the robot's parser drops fields today); a session that lapses is logged back in by the robot's own heartbeat within a tick, behind its breaker, never by the watch; an inbox not read for 30 minutes raises an alarm
yes no One pacer in the robot, a daily ceiling per customer. A 429 stretches the tick; there is never a retry loop. Tick on the door's schedule: 1, 5 or 15 minutes by hour; one minute for 30 minutes after a change List read one request per customer: thread, latest message id, unread, status, party Top row same as the high-water mark? Yes, most ticks do nothing, write nothing Walk the rows until one is older than the mark; overlap one page; dedupe by message id Thread read only where the per-thread fingerprint changed Mirror into PropFlow one row per PMS message id Advance the mark per customer, by time, never by id alone
Figure 3. The watch primitive. On a quiet tick it costs one request and no writes. Stop by time, never by id alone: row ids are not strictly decreasing (ties seen at Willows), so id paging can skip a row. The same primitive, with a different adapter, watches a Yardi list.

Verified at Willows on Oct 12, with no login and no browser. The company-wide inbox list is one JSON request (AppFolio's own conversations endpoint), newest first, 20 rows a page, not limited to the signed-in user (inbox-capture/request-log.md; shape.md). Each row carries the party (AppFolio id, name, a status such as "tenant (past)" or "applicant", the matched phone), the property and unit with their AppFolio ids, the latest message (id, body, direction, delivery status, the staff sender's name when staff sent last), the unread flag, the assigned user, and a candidate texting-line id (unverified: that field has 292 distinct values across 433 rows; the thread id field is empty on all 433). The row's id is the latest message id, so it moves on every send or receive: that is the change signal, and it is also why that id is not a thread key (the thread key is the outside number plus the texting line). Listing marks nothing read (two unread rows stayed unread across three polls) and a quiet poll returns byte-identical pages (inbox-capture/request-log.md; shape.md). The robot already exposes this read through a texting-inbox route: two session probes plus one list request per call, and it refuses to log in when the session is cold.

Still assumed. Our request is text-only by its own parameters. The inbox itself is not text-only (next paragraph). Also open: the page-size ceiling, the unread and read filters, the texting-line id (unverified: the candidate field has 292 distinct values across 433 rows; the thread id field is empty on all 433), and whether the list request can replace the heartbeat's own keepalive (if it can, the heartbeat and the poll are one request, and the robot raises the event: decision 1). One small robot change closes the first three: a query passthrough and a values-only shape mode, about 150 lines. The fourth is a canary.

Email is not read today. AppFolio's inbox shows email cards inline with texts in one timeline (Fede's screenshot, Oct 15, relayed by the renewals session; not reproduced here). Our read asks only for texts, sms[include]=true&chat[include]=false, with no email parameter (inbox-capture/request-log.md). Its 433 captured list rows are all type "sms" (ws-shadow/raw, ws-renewals-2026-10-15/raw). So the mirror sees no email today. Probed Oct 15 on the deployed robot (5 requests, Western Slope): the list endpoint ignores email[include] (texts+email byte-identical to texts-only; email-only returns 0 rows; chat include works). Emails are reachable only through AppFolio's per-person communication history, which the robot reads today for guest cards only (inbox-email-probe-2026-10-15/).

Request math at Western Slope, on the schedule. Page one is enough for a quiet tick, because new activity moves a thread to the top. On the measured curve (figure 8) the schedule is one minute from 8 am to 7 pm, five minutes from 6 to 8 am and 7 to 10 pm, fifteen minutes from 10 pm to midnight and thirty from midnight to 6 am (Fede, 10-12 night: fifteen is too often while people sleep): about 740 ticks a day instead of 1,440. At three requests a tick (the list plus the robot's two session probes) that is about 2,300 requests a day, or about 800 if the list read rides the keepalive (the phase 0 canary), plus one thread read per changed thread (about 10 a day). Against about 130,000 a day for the per-building design. A bulk blast moves many threads to the top in the same minute: that tick walks the list down to the high-water mark, one page per 20 threads, and writes the bulk rows from the list rows alone, one row with a count.

Why the tick slows down at night, with the data. Fede, 10-12 afternoon: "every 15 minutes at night seems a bit slow… what if there's an emergency?"; 8 pm: "I do think it makes sense to be less frequent; this is why we need a distribution, and a dynamic polling mechanism… I need data." The data is in (9.6, figure 8): of 3,983 inbound texts in a year on Western Slope's AppFolio line, 3.0% arrived between 8 pm and 8 am and 0.9% between 10 pm and 7 am, about one every ten nights; there is no after-work bump on this door (5 pm 3.2%, 7 pm 1.3%, 9 pm 0.4%) and only a small pre-work ramp (6 and 7 am, 1.5% together). So the schedule in decision 12 polls every minute while the office line is busy, every five minutes in the evening and early morning, every fifteen while people sleep, and drops back to one minute for 30 minutes after any change, which covers a conversation in progress. The worst case for a 2 am text is a 30-minute wait; the office today answers a weekday-evening text in a median 13.8 hours, and emergencies stay on the phone line and the call center. Limits: the year of texts is prospects only (tenants beyond a 5-household canary are not in it), and 90% of those texts answer something the office texted out the same day, so the evening is quiet partly because nothing goes out in the evening; Clara's own line, which answers at every hour, takes a third of its texts between 5 and 9 pm (3.5 weeks, our own tests not separated). The curve is per door and per customer, recomputed monthly, and it will move once Clara texts people in the evening.

Does reading mark a thread read, for everyone? Fede 10-12. Two different things: the thread page (the screen a person opens) fires AppFolio's mark-as-read (unverified: from a code comment, never captured); the list and thread JSON endpoints the watch uses do not (verified at Willows: unread rows stayed unread across three polls; the robot's code carries the never-load-the-page rule). So the watch never changes what the office sees as unread. Whether that flag is one per company or one per login is not in AppFolio's documentation; the probe hints it is shared (threads nobody on our side ever opened showed as read, while 27 of them still waited for an answer). Phase 0 settles it at Willows in one test: send a text to the Willows line, read it through the watch, and look at the inbox from a second login.

4. Mirror: copy it, land it on the right person

Each PMS message becomes one PropFlow message, keyed by the PMS message id so a re-read never duplicates. It carries: channel (text or email), direction, when it happened, the body, the door it came through (which PMS texting line), and who sent it: a named staff user (Kat, Jay), a shared login shown by its login name ("Assistant PM", "Clara PropflowAI": humans have used both, so never guess person or robot from it), or a bulk send (one renewal reminder to 200 tenants, labeled bulk).

no no no yes yes yes An inbox row party id and status, name, matched phone, property id, unit id, latest message 1 A person already holds this AppFolio party id? 2 A person with this phone at this customer, and the name agrees? 3 The unit's leases, current or former, carry this name? 4 Build the person from AppFolio's own directory record household member, past tenant The conversation for this person at this property create it if missing, through the existing create path, then write the message No property mapped on the row a plain conversation on the number, on the placeholder if it must: no inquiry, no tour, no AppFolio write; retried every sync (Fede 10-08)
Figure 4. Identity, in the order the Western Slope read supports: AppFolio ids, then phone only when the name agrees, then the unit's leases, then build from the directory. The engine decides; nobody clicks.

What the Western Slope read says (read-only, 2026-10-12).

FactNumberSo
Current tenants with a phone who already match a PropFlow person337 of 401 (84%)Most live threads land on day one.
Past tenants who match35 of 539 (6.5%)Backfill needs the "build from the directory" step; at JP&Co 15 of the newest 21 inbox rows were past tenants.
Household members the rent-roll sync skips172 of 517 current tenants, 65 with a phoneSame step; they text the office like anyone else.
Leaseholders who never had a Clara conversation330 of 354Almost every attach creates a thread; the create path is the main path.
Bare phone match landing on a housemateabout 2%Phone only counts when the name agrees.
Unit label text ("Unit 8" vs stored "8")0 of 185 match by text, 185 of 185 by unit idJoin on ids, never labels.
Prospect inquiries already sitting on the front-door placeholder25The matcher never adds to them; a build check enforces it.
  • Owners and vendors. Their messages are counted and kept pending until those modules exist; never dropped.
  • Backfill is onboarding. The same reader walks the list backwards, one to two years deep, paced, resumable, and writes rows with their real timestamps plus a backfill mark. Backfilled rows are context, never events: they do not wake a follow-up, set "engaged", or fire an alert (section 6).
  • Keep current. A cursor per customer per channel (the highest message id seen) plus the list fingerprint. Losing the cursor costs a re-read, not duplicates.

5. One thread, many doors: the split number

A tenant has Clara's number and the company's AppFolio number in their phone. Both must work forever, and nobody is told to switch. The model: the person is the key; a number is just a door. In Gera's portfolio model a line attaches to the customer or to a property; the PMS texting number becomes one more line at the customer level, provider "appfolio", and the PMS mailbox likewise. Every message records its door. pending: whether the line type admits a non-Twilio provider today (identity report); if not, that is the one schema change.

Clara's number (text) Clara's mailbox The company's AppFolio texting number AppFolio email The phone line (voice) Maria R., a tenant (made-up example) one conversation, every door in it Clara answers through the door it came in Her number: today. AppFolio: phase 1 the office answers there; phase 3 Clara does.
Figure 5. Doors into one room. Rule R2 is what makes "no new number" true: a message is answered where it arrived.
RuleStatementWhy
R1Inbound through any door lands in the person's one conversation, badged with its door.Staff and Clara see one history, whoever answered where.
R2A reply goes out through the door the message came in. Until phase 3 Clara does not answer on the PMS door, with one exception that already runs: the rental-application link she sends through AppFolio's own button at Western Slope (3 in the last 10 days). Those sends come back through the watch and are filed as Clara's, by her seat id, from day one.Nobody meets a new number. The office keeps answering in AppFolio exactly as today.
R3From phase 3: Clara-initiated messages (follow-ups, renewals, reminders) use the person's last inbound door; with none, the customer's setting: Clara's number today, "through the PMS" once phase 3 exists. One setting, all lanes.Renewals today go through our number; the move is one setting, never a per-lane switch.
R4Clara's own PMS sends come back through the watch; recognized by her seat (her AppFolio user id) and matched to the sent message, they stay one bubble.No echo, no "teammate answered" false signal.
R5Opt-out is per person, keyed by the number, as the code and the law already have it: a STOP typed on either door stops both, from phase 1. STOP and START replay in time order and the latest wins, live and in the backfill. Until phase 3 Clara's own number keeps carrying renewals and reminders exactly as today (decision 11).Compliance and no surprise numbers. Opus round 2: the earlier "per door" wording would have stopped Western Slope's renewal texts from Clara's number this month.
R6One inbound burst, one reply. If a person texts both doors within minutes, the thread is locked for one reply, sent through the most recent door (the watch lags up to a minute; before phase 3, R2 settles it anyway).Two doors must never mean two answers.
R7A human replying from the PropFlow thread view on a PMS-door conversation goes through the PMS door too (phase 3); before that there is no composer on the thread at all (the messages API is read-only); the paths that can leak Clara's number are operator scripts, hold approvals and recovery runs, which decision 10 covers.The easiest way to leak a new number is a staff member typing in PropFlow.

6. Clara's context: is it automatically in the loop?

Short answer: yes, through two doors that are already live, and that cuts both ways. Clara's reply turn loads the whole conversation, keeps staff rows, wraps each one as a teammate note ("teammate turn, not yours", naming who wrote it in AppFolio) and trims to an 80,000-token window. The follow-up engine reads every conversation the person has and already stands down when a teammate answered and the renter has not replied since. So a mirrored staff text is in her head the moment it lands. That is Fede's double-text fix ("Jay sends an AppFolio message, and then Clara sends out different things"), and it is a behavior change on day one, to be named and proven with a before and after replay.

Phase 1 is not a passive copy. Two things are wired today and both would fire on tenant threads: a live inbound text wakes Clara through the normal inbound path (the wake is gated on the per-property sync switch alone, not on PMS delivery), and a staff text stamps a permanent send block on the person's thread (only where the property's delivery is set to the PMS and a Clara seat exists). At Western Slope today that means: flip the existing switch and a tenant's text to the office gets no hold, a full Clara turn, and a reply from Clara's Twilio number. That is the second-number failure Fede named. And even with the wake and the hold removed, stored rows alone change decisions, because most readers look at the row's role (staff rows carry the assistant role) and never at who wrote it.

Store-only effect of a mirrored rowWanted?What to do before the mirror lands
Follow-up gate stands down when a teammate answered a lead (a second staff row makes the thread "engaged")Yes, for a real answer; no, for a bulk blastkeep; bulk rows carry their own kind and never count as engagement; proven by replay
Prospect summary (feeds the voice agent's "previous interactions" line and the returning-prospect block), the first-email prompt and the returning-prospect flag read staff text as Clara's own words; the promise ledger extracts staff promises as Clara'sNoone provenance predicate in every role-only reader (is this Clara's own send, or a staff send) shipped as a fix, no switch, before or with the mirror
Collections: any tenant row after enrollment stops the rent reminder, blocks re-enrollment and pages the PM; a blast plus N "thanks" is N PM actions. Weekend tour check-in cancels on any tenant row. The 12-hour outreach cooldown counts a staff row as Clara's touch, across propertiesNocollections, the weekend check-in and savings filter by the row's writer and door; the 12-hour outreach cooldown keeps counting a real staff text (that is the double-text fix it was built for) and leaves out only system and bulk rows
Savings, caller log, "handled by", "Clara responded" badge: a staff-only thread shows "Clara → tenant", every tenant "Thanks!" credits ten saved minutesNomirror threads carry an origin stamp; counters exclude it
Staff signatures with cell numbers and emails enter prompts unredacted and as "authoritative ground truth"; a blast like "rent $1,450, late fee after the 5th" is extracted as a PM commitment on every recipient's threadNostore signature-stripped text; bulk and system rows fall outside the commitment predicate; private-info detectors on Clara's replies
  • Rendering. "Jay · AppFolio text, Oct 9 4:12 PM: …" in the thread view and in the prompt (today the staff kind renders "From mailbox"). Bulk sends render once with a count, not 200 times. A photo renders as a photo, or at least as "[photo]" with the file fetched inside the budget (Fede 10-12: a lot of these conversations carry one). The audit landed: photos from Clara's number already render inline on the thread view; the gap is getting them out of AppFolio (traps 56 and 66). The localhost mock exists (local branch proto/pms-mirror-rendering, never a PR; the real conversation page at localhost:3099/conversations/conv_proto_maria_242 on the Mac, screenshots in the scratchpad): every row shows "Jay · AppFolio text, Oct 9 4:12 PM", the shared login shows as "Assistant PM (shared login)", an automatic notice as "AppFolio notice", the bulk send once with "sent to 200 residents", a photo as a thumbnail or "[photo]" when the file is gone; the real prompt builder prints the same lines. Two findings for slice 1: with a header on every row the thread no longer folds a burst of messages into one bubble; and in the prompt the bulk and notice rows sit inside Clara's own turn unframed, so the shipped version must frame them as not hers. Not built in the mock: bulk detection, the photo fetch, the email preview card's header.
  • Window. Clara's prompt gets mirrored messages from the last 90 days plus the last 20 regardless of age, plus a one-paragraph summary of older history; a two-year backfill never enters a prompt whole. Everything stays visible on the page.
  • Wrapper wording. The teammate wrapper is written for leasing (tour rules). A maintenance or renewal text from Kat needs a neutral note: who, when, via AppFolio, what was said.
  • Backfill is inert, by rule and by reader. Rows older than the turn-on cursor carry a history mark: they never move recency, never hold, never wake, never alert, never count as touches. Readers with no time window still see them unless told: the follow-up gate's "engaged", first touch, the prompt readers, promise extraction, the life-safety scan on escalated threads, and savings. Those readers honor the mark in the same prerequisite fix, and so does the work-order creator, which today attaches every tenant photo in the conversation to a new work order: it takes only photos after the turn-on cursor or from the current issue, and never one whose source is AppFolio. A thread minted for a backfilled row takes its start and last-message time from the rows, never from the import moment.
  • Promises staff made ("I'll send the lease tomorrow") are context Clara must not contradict; they are never tracked as Clara's promises (Fede 10-08: promise tracking only through Clara).
  • The company brain stays separate. The mirror feeds the person's conversation, never the brain (which has no reader yet); what the office teaches Clara is a different path (vision page, section 5).

7. When Clara chimes in

Rewritten tonight. Fede, 10-12: "I don't like this, it's too complicated." There is one mode for every customer (Fede 10-08), so no customer picks a level. The PMS door has two states, and the second is the same for everyone once it exists.

StateClara on the PMS doorThe officeWhen
ReadsMirrors everything, answers nothing there. Her own-number behavior gains the history (the double-text fix). The one send that already runs, the application link through AppFolio's button, stays.Keeps answering in AppFolio exactly as today; sees one history per person in PropFlow.Phase 1, the day the mirror setting is on at a customer.
Answers the routine share, hands over the restAnswers routine asks through the PMS door (a work order's status, a balance, a date, hours, a document, a how-to) and sends everything else to the office with a reason. Emergencies are never hers alone: the emergency line and the message stay. The first week at a customer runs through the supervised hold.Escalations with reasons; the routine share leaves their queue.Phase 3, one setting per customer, default off, Fede turns it on.

What "routine" is comes from the data, not from a policy table. Fede: "in the last year of data, both Situs Group and Western Slope, it's like sixty percent was routine, and of that so much was maintenance or payment questions, and then like twenty percent was human escalation; add real-life distributions." Figure 7 (below the engage decision) shows it, measured tonight on 1,083 real messages across six doors with one rubric and a 92% hand-check: on Western Slope's AppFolio text door 90% of what people send is routine, meaning Clara can close it herself: 69% is a question she can answer from facts on file (hours, a work-order date, an application status) and 21% is a thanks or an ok that needs no reply; 10% needs a person in the office; 0.3% is an emergency; on Clara's own line 74% routine, 24% human, 1.6% emergency; Situs's inbox 65 / 33 / 1.3. Fede's gut fits Clara's line and the Situs inbox; the AppFolio text door is more routine than that because most writers are applicants replying to a staff question (78% of its texts). Maintenance and payments are not most of the routine pile at any door (4 to 13% of all messages; 44% of what tenants send on the text door); leasing is. If Clara answered the routine share through the PMS door at Western Slope, she would take about 90% of the text door and the office would keep about 34 messages a month. The engage decision below runs on those labels; its thresholds are set on this mix and re-measured monthly from the mirrored rows.

The bar to move a customer from Reads to Answers: a clean supervised week on the PMS door with every draft held and reviewed, zero contradictions and zero double texts in the replay, and a measured routine share on figure 7 above a floor set once the number exists. The floor is set from figure 7 once Fede picks it.

no no no yes yes yes Inbound through the PMS door 1 A history row, or Clara only reads at this customer? 2 Lane maintenance, renewal, billing, leasing, access, other 3 A staff member replied on this thread in the last N hours? 4 Routine by policy and record data? an open work order's status, a renewal date, office hours Escalate with a reason Clara acknowledges only if the policy says so Record only Record; the office owns it Draft the reply, then the gate supervised: hold for the office live: send through the PMS door
Figure 6. The engage decision. Signals that already exist: the supervised hold, "teammate engaged", touch budgets, quiet hours, opt-outs. New: the lane classifier for tenant traffic and the routine policy, both calibrated on the Western Slope read (section 9).
Share of what people send on each door (random samples, 1,083 real messages read, 92% hand-check agreement) Routine, needs an answer Routine, a thanks or an ok Needs a human Emergency Western Slope · AppFolio texts, prospect cards · a year of data, 394 read (±5 pts) 394 read 69% 21% 9.9% Routine 90% · needs a human 9.9% · emergency 0.3% · leasing 59% of messages, maintenance and payments 10.7% Western Slope · AppFolio texts, tenants · 16 conversations, 90 read (small: ±10 pts) 90 read 62% 22% 16% Routine 84% · needs a human 16% · emergency 0% · leasing 31% of messages, maintenance and payments 22.7% Western Slope · leasing mailbox · 4 months of data, 215 read (±7 pts) 215 read 79% 5.9% 15% Routine 84.9% · needs a human 15% · emergency 0.3% · leasing 64% of messages, maintenance and payments 4.4% Western Slope · Clara's line, calls and texts · 25 days of data, 245 read (±6 pts) 245 read 67% 7.3% 24% Routine 74.3% · needs a human 24% · emergency 1.6% · leasing 51% of messages, maintenance and payments 13.4% Situs Group · AppFolio texts, prospect cards · all 61 that exist (small: ±12 pts) 61 read 54% 31% 13% Routine 85% · needs a human 13% · emergency 1.6% · leasing 61% of messages, maintenance and payments 11.5% Situs Group · leasing mailbox · a year of data, 78 read (small: ±11 pts) 78 read 56% 9% 33% Routine 65% · needs a human 33% · emergency 1.3% · leasing 24% of messages, maintenance and payments 12.8% Routine = Clara could answer from facts on file or it needs no answer. Each row names the time window the sample came from and how many messages were read; ± is the margin at that size, so the three rows under 100 (Western Slope tenants, both Situs rows) are indicative only and the decision rests on the three large rows. Emergencies: 8 confirmed in 1,083 messages; at Western Slope about one a month on the office line and about five a month on Clara's line (25 days). Fede's gut (60% routine, 20% human) fits Clara's line and the Situs inbox; the AppFolio text door is more routine than that because most writers are applicants replying to staff (78% of texts answer a staff question). The edge checks say add 2 to 6 points to "needs a human" on the 85 to 90% doors. Not read: Situs tenant texts (thread pages would mark them read), a full year of Western Slope tenant texts, call logs.
Figure 7. The real-life mix, door by door, measured on random samples of real messages at Western Slope and Situs (scratchpad pms/mix/mix.md: lanes, handling, message type, emergencies per month, and what could not be read).

Figure 9 is the same messages sorted by topic instead of by who should answer. Leasing is the pile everywhere except the Situs mailbox; maintenance only matters where tenants write; "other" is mostly a person asking for a person.

What people write about on each door (same 1,083 messages as figure 7, by topic) Leasing Maintenance Payments Renewals and lease terms Move-in / move-out Notices and documents Access, keys, parking Complaints Other / "get me a person" Western Slope · AppFolio texts, prospect cards (394 read) 59% 7.3% 5.5% 6.3% 12% Top three: Leasing 59% · Other / "get me a person" 12% · Maintenance 7.3% Western Slope · AppFolio texts, tenants (90 read) 31% 16% 6.7% 5.6% 11% 4.4% 7.8% 17% Top three: Leasing 31% · Other / "get me a person" 17% · Maintenance 16% Western Slope · leasing mailbox (215 read) 64% 7.1% 5.7% 13% Top three: Leasing 64% · Other / "get me a person" 13% · Move-in / move-out 7.1% Western Slope · Clara's line, calls and texts (245 read) 51% 6.9% 6.5% 5.3% 21% Top three: Leasing 51% · Other / "get me a person" 21% · Maintenance 6.9% Situs Group · AppFolio texts, prospect cards (61 read, thin) 61% 8.2% 6.6% 4.9% 11% Top three: Leasing 61% · Other / "get me a person" 11% · Maintenance 8.2% Situs Group · leasing mailbox (78 read) 24% 9% 5.1% 7.7% 12% 6.4% 6.4% 26% Top three: Other / "get me a person" 26% · Leasing 24% · Notices and documents 12% Leasing is the pile at every door except the Situs mailbox, where half the outside senders are agencies, lawyers or family. Maintenance is big only where tenants write: 16% of what tenants send on Western Slope's text door, 12% of calls on Clara's line. "Other" is mostly someone asking for a named person or a callback. Ranges per cell are in the mix report; the Situs text door is thin (61 messages, one conversation is a quarter of them).
Figure 9. What people write about, door by door (scratchpad pms/mix/mix.md sections 5 and 6, with ranges).

The supervised hold, as the code behaves (Opus round 2): a held draft expires after 24 hours, never a week; general replies are not held today; approval is per message through an emailed link; there is no daily digest. So the first week on the PMS door needs one change, named in phase 3: every PMS-door draft is held, and a held draft waits 7 days.

Settings: one for the mirror (new capability, default off, Fede turns it on per customer) and one for Clara answering through the PMS (new customer-facing action, default off, all lanes). The applications lane already has a switch of its own (the application-link mode, organization level); the one-setting rule absorbs it in phase 3. Rules, not settings: the N-hour "staff engaged" window, the routine labels, the escalation reasons, quiet hours, the cadence.

7a. Nothing falls through (While you were out)

Decided Oct 15 (Gera)  Written by Trinity, audited against this page, Gera's portfolio model, the constitution and origin/main 2bd0f0c; Smith's four fixes folded in.

The name (Gera, Oct 15): Reception. The whole system, front door, identity, one conversation, the engage decision, the doors and the catch-all, is Reception, what a PM office actually has. Its parts: the front door (one message-arrived event), the desk (identity), the person's file (one conversation), whose turn (the engage decision), the doors (outbound), While you were out (the catch-all, 7a) and the Team notice (what staff see of it, #19603). Anything built outside Reception is a bug, not a feature.

Why. Oct 13, 3:10 and 3:13 PM: a Western Slope commercial tenant called twice about a ceiling leak at a building not synced to PropFlow. Clara could not place the address, took no message, offered a person and closed; nobody at the company was told. Smith, Oct 15: prod reads the team-notify engine on at Western Slope with a team inbox on file (the Oct 13 drop itself is not yet traced), so the failure is a path that never handed off, not a switch. Voice got four per-path "if nothing happened, tell someone" checks that week, each resolving recipients its own way. This rule replaces them.

The rule. Every inbound message ends its turn with exactly one recorded outcome, written by one shared outcome writer, never by a check each path runs for itself. The outcome list is the one the team-notify engine already has (src/lib/team-notify/decide.ts), not a second vocabulary:

Outcomedecide.tsWhat leaves
Clara handled it, or a person already has it (8c's team's turn)1 handlednothing; the thread is marked engaged (successes and thank-yous included)
Clara needs an answer from staff (a renter's question she cannot close, tour notes)2 askone note to the company inbox; Clara waits for the answer
Escalated, with a reason ("unhandled" included)3 handoffone note to the company inbox; human-owned until released (figure 10)
Something broke inside Clara, or the door is down'alert'engineering only, never the customer (8a, Oct 13; 8d)

A turn that ends with no outcome is written as 3 handoff, reason: unhandled; silence is never an outcome. "Record only" is not a fifth value: on the PMS door, until phase 3, decision 4 holds, the outcome is computed and stored (phase 2, section 11) and no note leaves. Reasons are finite and specific: unplaceable address, unsupported building (a commercial or other row the PMS pull marks unsupported, never typed by hand), wants a person and no route, out-of-scope topic, emergency chain exhausted (all to the company inbox); error, door down (engineering, 'alert').

The note. It leaves through our door (8a): one email from the team-notify engine (routeTeamNotice → teamNotifyEngine, its existing company setting, nothing bypasses it and nothing new switches it) to the recipient the engine's one resolver picks from the door the message arrived on; today that is the company's team email, and the portfolio resolver (nearest wins, org as fallback) decides it when it lands. The voice backstop's property-first lookup (#19528) is a second resolver and retires into this one. A sent email is immutable, so one notice per person per day (the person-day key), and a second message from the same person while their hand-off is open is a threaded reply on the engine's Message-ID, never a new note. Notes go at once, day or night (Gera, Oct 15: no morning digest; Fede, Oct 13: "just email their general email"). The caller hears back on the door they came in. Staff see every note as an event line in the caller's own thread and as the Team notices kind on /conversations (#19603).

How it lands. No switch: recording the outcome is a correctness rule, live everywhere on merge. The front door calls the writer for the PMS door; voice, Twilio and the mailbox call the same writer from where they are until they move onto the door (decision 1(a) keeps them off this initiative's front door; the writer is shared code, not a door move). First two cases, in order: a text to an unmapped company number, which today gets an empty ack and no notice (twilio/webhook/route.ts:644); the voice hangup backstop, which today runs only on the bench (hangup-message-backstop.ts:130, a fix behind a gate, a hot-rule-8 miss, its own follow-up). Proof: the Oct 13 calls and the 1,083-message sample of figure 7 replayed into exactly one outcome each, with a planted no-outcome turn as the red control; the Willows rung with a real call to an unplaceable address and the note read in the team inbox.

8. Sending through the PMS, like an employee (phase 3)

The seam exists, and one lane already uses it. The robot can send a text or an email on a guest-card thread, attributed to the shared "Clara PropflowAI" login, and at Western Slope Clara already presses AppFolio's own "send rental application" button from the guest card (3 in the last 10 days; built 2026-09-18 behind the organization's application-link mode, gated so nothing else sends). There is no tenant route and no work-order route, so it covers prospects only. Phase 3 is real work, not just guardrails: the tenant route, a durable send-once claim, a cap and a send window, a Clara seat per customer, the 7-day hold. (Phase-3 report and Opus round 2, 2026-10-12.)

How solid is the existing send? (Fede 10-12: "I built this a while ago… I don't know if that implementation is solid or needs more testing.") Yes, this is the one, and it is solid for the one thing it does: an application link on a prospect's guest card. Tests: 9 on the writer, 27 on the application-link mode, 31 on send-on-ask, 10 on the message store, 12 on the sender, plus the shared outbox drain (224) and a replay case list. A robot failure parks the row with a reason, never silently; a repeat ask lands on the same row; the stored message is keyed by AppFolio's message id. Not confirmed: a numeric retry cap, so a retry after a robot timeout could send twice; that is the one known gap, and the harness's send scenarios (section 11) are built to find it. Nothing exists for tenants or work orders. The other gap is attribution: a text from Clara's login is recognized as hers only by seat id, and the robot does not carry the sender id on thread rows today, so her own send can be filed as a teammate's reply (round 2, 🔴2). Last touched 2026-10-11.

GapWhat happens todayFix before any send
Double sendA retry after a timeout can send twice: AppFolio returns no message id and the duplicate check lives in one runner's memory.A durable claim before every send; the echo match closes it.
STOP is one-wayA STOP typed on the AppFolio thread revokes nothing in PropFlow; only Twilio events do.Mirror STOP as an opt-out event at the front door; rule R5.
No brakesNo send window (a 2 AM reply goes at 2 AM), no per-customer daily cap. The robot does pace requests per host and shares a 429 cool-down; what is missing is a cap on sends.Window and a send cap in the robot.
IdentityHumans have used the shared login; per-customer Clara logins are planned, not built.A Clara seat per customer before turn-on (Gera builds, Fede gates).
Login lapseA full login with an SMS code: unmeasured (90 s is our refresh timeout); the breaker backs off up to 6 hours; about five failed logins lock the account (observed Sept 7 on Western Slope, not AppFolio documentation).During an outage Clara stays silent on that door; never a fallback to her number, which would announce a new number.
SwitchPer property, script-only, no page.One customer setting on the customer page, default off.
Tenants and work ordersNo route.Build the tenant route; it carries maintenance, renewals and tenant comms.

Scope: all lanes under one setting. Leasing replies, maintenance updates, renewal offers and reminders (today through our number), tenant communications. A customer that chooses the PMS door gets it everywhere; a customer that does not keeps Clara's number everywhere. No per-lane switches. The report's order: replies inside existing PMS threads first (smallest blast radius, never splits a thread), then everything through the PMS per customer after proof.

8a. Outbound doors: sending through AppFolio from every module (Decided Oct 13)

8a. Outbound doors: one decision in front of every send

What changed, Oct 13 afternoon: this section is new. Written from two independent drafts (Fable and Opus 5.5, max effort, read-only on the code), this morning's Situs call transcript and Fede's screenshot of their inbox. Status: Decided Oct 13.

The ask (Fede): Clara should not always reply from her own number. At Situs everything goes through AppFolio; at Western Slope either; some customers have a dedicated email address. Keep the core, add a layer that decides the door. Renewals, maintenance updates, guest cards, applications and tenant messages must all get it.

The answer: the general piece is a door decision, not a bigger outbox. Every text already leaves through one dispatcher and every email through one sender. One resolver goes inside those two, driven by one company setting (text door, email door). The outbox we already have becomes the machinery behind the AppFolio door. Clara behaves the same behind every door.

 leasing · maintenance · renewals · tenant messages · a person typing in PropFlow
                                  │  "this needs to go out"
                                  ▼
          dispatch() for texts · sendPropertyEmail() for email   (gates unchanged:
                                  │                                consent, quiet hours, human-replied hold)
                                  ▼
                   ┌──────── DOOR RESOLVER ────────┐   1 voice, staff, vendors → ours, always
                   │ who is it for · which company │   2 a reply → the door it came in
                   │ which channel                 │   3 else the company setting (text, email)
                   └──────┬────────────────┬───────┘   4 unreadable or door down → send nothing, never our line
                          ▼                ▼
                 OUR DOOR                APPFOLIO DOOR
                 text: Clara's line      OUTBOX row: one id per send, one claim
                 email: building,            │
                 client or our mailbox       ▼
                                         ROBOT, one writer per AppFolio database, warm session, never logs in
                                         guard: person id + role + phone + property, thread unchanged since Clara read it
                                             │ send
                                             ▼
                                         AppFolio inbox thread ──► read-back: one new row from Clara's user
                                             │                      red "failed" row = failed, alert, no resend
                                             ▼
                                         the MIRROR reads the same thread, files that row once by id, so one bubble

Today, where each lane is pinned to our number. Leasing texts, tour reminders, the maintenance "we created your work order" text and the Western Slope renewal offer all go through the dispatcher but pass no property, so the one existing PMS switch (a per-property field set on no property) never fires. The renewal letter PDF calls Twilio directly. Collections refuses the AppFolio door. Staff, techs and vendors text from our line by design and stay there. Email already climbs building mailbox, then the client's shared mailbox, then ours; there is no ConAm-style "dedicated address" setting in code, that is the shared-mailbox rung. The application link is the one lane already sending through AppFolio, from the guest card.

Fede's five sendsRecordLands inWhat the Willows prototype proves
General tenant messagetenant on a unitthe tenant's threadtenants can be reached, not only prospects
Work-order updatetenant on the work orderthe same tenant thread, next to AppFolio's own "System" rowswork-order number in the body; a planted wrong tenant is refused
Guest-card replyprospectthe prospect's threadthe mirror marks the sent row as ours; nothing leaves Clara's number
Application messageprospectAppFolio's own button (exists) plus the link text in the threada killed call never yields a second application
Renewal offertenant on the leasethe tenant's threadrenewals code unchanged; only the door differs; over 910 characters refuses

First live result, Oct 13 at 3:45 pm Denver (prototype, local branches, no PR). One text, "Clara test via AppFolio inbox 1 (ignore)", sent into Fede's own Willows thread by the JP&Co Clara AppFolio user through the robot's warm session, no login. AppFolio stamped it delivered on the thread's own line.

StepMeasured
Our post to AppFolio's stamp on the row4.9 s
Robot answers with the row id6.7 s
Our read-back finds the row11.6 s
The mirror sees the row29 s (one sample, inside the one-minute tick)
Unread mark on the threadunchanged

What changed Oct 13, 22:57Z (production): the tester's own text, held 3 h 27 min as "two companies" (his number is also on a Camellia record), filed into his existing Willows conversation on the first watch tick after the property-line fix went live, together with the staff-side row before it and Clara's test reply after it. Placed by the property's own texting line. One defect left: Clara's reply was labelled as staff because it was filed within the 5-minute lookup cache after the seat-id write (#19382); the held rows also keep their hold markers after filing, so the report over-counts holds (#19388). 23:43Z: a second test text from Clara's seat filed into the same conversation 32.5 s after AppFolio's timestamp (stored at 29 s), not held, labelled as Clara. The seat fix is proven live.

What the send is: one form post with the page's security token. AppFolio answers only "true" (pms-send-discovery.md: {"callback_result": true}), no message id and no protection against sending twice, so read-back is the only proof and the key must live on our side. Thread rows do carry the sender's user id. Harness: 22 scenarios pass, every planted defect red, no stray calls. Two defects found: the mirror labelled Clara's row as a staff reply because it recognises Clara by the old shared login's user id, not the per-database Clara user (fix in progress, no switch); and only the guest-card send names the person, the inbox and work-order sends name only the phone, so the guard must come from our side. Open: tenant and applicant sends not yet observed; a person with threads on two lines; a person with no thread yet.

Guest card or inbox thread (Fede: "that's probably more efficient, but extreme caution")? The thread. It is the only place tenants can be reached, it is the thread the mirror already reads, and AppFolio shows its own red "Message failed to send" row there. The guest card keeps card-only actions: the application button, notes, logged calls. The guard, every step refusing with a named reason and sending nothing:

StepCheckIf not
Right personthe thread is opened by person id + role (tenant, applicant, prospect) + phone, the way AppFolio's own page refreshes it, never by phone alone or list position; all three plus the property match the person Clara resolved from our record; everyone on the thread belongs to the sending companyrefuse
Nothing newthe send carries the newest message id Clara read; the thread has not moved (no newer text, no staff reply)refuse, Clara decides again
Onceone fixed key per send; the robot refuses a key it has seen; a retry reads before it sendsrefuse
Proofthe same cursor read shows one new row from Clara's user with our words; a 200 is never "sent"red row = failed, final, alert
Hygienewarm session; never log in or mark read; no STOP on the number; 910 charactersrefuse, never cut the text

Robot routes to build (L4, Smith's repo): read one thread by person from a cursor; send in a thread taking the key, the expected person and property and the expected head, returning the row id, time, sender and status; a note on a tenant or work-order record (Situs wants Clara's call notes there). The guest-card send stays for the application button and drops its phone lookup. Bulk send is never called.

Rules that never bend: one robot writer per AppFolio database; a claim lost mid-send is "unknown" and only a read settles it (today it returns to pending with a zero try count, a real double-send hole in the application send; a fix, ships now with no switch); sends share the mirror's request budget and pause; a reply carries a use-by (15 minutes) and expires into Clara deciding again, never into a list for a person; a door flip withdraws pending rows and re-sends nothing.

Situs, week one: mirror on; text door = AppFolio for the whole company (Hugo: one setup across the portfolio); sender = Situs's own Clara user; voice stays on our line (AppFolio has no voice door; Crescendo forwards to us); call notes as record notes; email holds until their mailbox is connected; door down = Clara silent on text and engineering alerted, staff see the thread as today.

Build: the Willows prototype runs locally first, no PR, no CI, result on this page. After Fede's go on D1 to D4: a chain of small dark PRs, the default-off company setting first, then outbox row shape, resolver, dispatcher wiring, robot routes, drain with guard and read-back, mirror matching by AppFolio id, shared budget. Dark: pms_text_door, pms_email_door | feature: Clara texts and emails through the client's AppFolio | proof: pending (Willows five sends) | turn-on: Fede

Decided Oct 13 (Fede, by voice): text only, one company switch (email stays on the company's own email). When on, it covers every lane. Clara replies inside the AppFolio inbox thread for everything. No fallback: when AppFolio is down the message holds and engineering is alerted, never Clara's own line or a person (a self-draining queue is a later investigation). Nobody types messages in PropFlow; staff text Clara. Send right away. Situs waits for the Willows proof and then uses this. Status: Decided, chain started (PR 1 = the company setting, default off).

#Decision for FedeOptionsRecommendation
D1Where the door is seta) per company, per channel · b) one switch for both · c) per propertya: Situs wants one setup; text and email need separate proof
D2Which lanes it coversa) every lane · b) per familya: one mode for every customer
D3Guest card or threada) thread with the guard · b) guest carda
D4AppFolio door downa) hold to the use-by, alert, decide again · b) send from our line · c) park for a persona: b is the surprise second number; c is the queue you ruled out
D5A person typing in PropFlowa) same resolver and outbox · b) only inside AppFolioa: one writer per door
D6Send timinga) right away plus a 15-minute sweep · b) the mirror's bandsa: a reply must not wait 30 minutes at night
D7Situs text follow-up before the Willows proofa) wait · b) start on Clara's number, move latera: b gives prospects two numbers, the split Situs asked to end; calls and work orders still start

Read back every send, share the mirror's budget, keep work orders in their own pipeline: taken under standing rules, not asked. Out of scope: bulk sends, attachments through AppFolio, voice through AppFolio, Yardi and RealPage. Engineer-level footnotes with file and function names: the two drafts in the session scratchpad, folded into the reference section on the next pass.

Status 2026-10-16 — the send works; Western Slope is not ready

At the Willows, Clara's text reply went out through the AppFolio inbox thread and the test phone received it from AppFolio's texting number in 36 seconds, with AppFolio's message id read back and zero texts on Clara's own line. The door is on at the Willows; the Oct 19 proof runs there, see Oct 19: ready to decide.

What the door would do at Western Slope today (read-only replay, 128 phones with mirrored texts)

OutcomePhonesShare
Resolves to a thread4535%
Held: one person on file twice at the same building (applicant + tenant)7458%
Held: other two-person cases75%
Held: no AppFolio thread22%

Of the 41 people Clara actually texted there in the last 14 days, 12 would resolve today (29%); 12 more are held only because their conversation sits on the company's front-door property, which has no AppFolio account.

Fede picks (recommendation first):

  • D8 Same person on file twice: A reply in the thread they last wrote from (recommended) · B always prefer the tenant record · C keep holding.
  • D9 Conversation on the front-door property: A look the person up by where the mirror places them (recommended) · B keep holding.
  • D10 Staff and vendor texts: always Clara's own line, never the door (fix building; no pick needed).

Recommendation: do not turn the door on at Western Slope until D8 and D9 are built and replayed; the next replay should show most recent recipients resolving.

What changed Oct 16, 09:00Z (Fede: "get this to 100% accuracy"): the gap is three parts, not two, and the biggest is people with no AppFolio text thread yet. Of the 41 people Clara texted in the last 14 days: 12 resolve today; 4 more once the front-door rule (D9) looks them up by mirror placement; 1 once the same-person rule (D8) lands; 24 have no AppFolio thread at all, because Clara texted them first. 18 of those 24 have an AppFolio guest card, so the door will start the thread from the card (the robot's existing guest-card text, which never creates a card). The remaining 6 have no AppFolio record on their inquiry; that residue is measured and reported, not hidden. Building now as three small PRs, dark, no new switch; proof is the replay before and after plus one Willows live send for the start-a-thread path.

Of the 41 recent recipientsTodayAfter the three fixes
Sends through AppFolio12 (29%)35 (85%), plus up to 6 more once their guest cards exist
Held: front-door property120
Held: no AppFolio thread170 (thread started from the guest card)
Held: same person twice10
Held: no AppFolio record at all(counted as no thread)6, re-sent on their own when the card appears

Willows live run, Oct 17 06:19Z (start a thread, all three fixes and both robot fixes live): the door refused to send, correctly, so the send path is still not proven. Before (Oct 16 14:40Z): the text went out but was never read back, the mirror held it, and the start cost about 220 AppFolio requests. Now: our record for the test number points at three guest cards (141, 58, 105); the robot read each one and none carries that number in AppFolio, so the phone guard held the text as "card number does not match" and sent nothing. It cost 12 requests (3 card reads), no login, no Twilio text. The card that does carry the number (160, the one AppFolio texted from on Oct 16) is on none of our lists, so this is most likely a record-link gap at the Willows (inference; the card-160 fact is from the Oct 16 robot answer, not a fresh read). The door also tried only 3 of the roughly 12 card ids on our record; both are checked before the day's second proof text. The refusal opened one engineering alert (06:19:19Z), which closes on the next send. Proof texts used today: 0 sent, 1 attempt.

CheckResultEvidence
Robot sent and read back the textNot reached: refused before the send3 card reads 06:19:11–06:19:18Z, no message post; outcome card_phone_mismatch, 7.5 s
Outbox row is sent with the AppFolio idHeld, not sentoutbox record harness.start-thread.willows.2026-10-17, state held
Mirror files it as ClaraNothing to fileno text was sent
Zero texts on Clara's own linePassTwilio: 0 messages to the test number since Oct 17
No AppFolio loginPassrobot log for the window: 3 card reads only
Request cost about 20, not about 22012 (reads only)JP&Co day counter 0 → 12 on Oct 17, no mirror tick between
Willows battle test (Oct 13–14)

What changed, Oct 14, 01:45Z: this section is new. It tests texts coming in from AppFolio and Clara's texts going out, failure cases included, at The Willows only. No AppFolio login was triggered: Clara's login counter stayed at 0 in 24 h, read before and after. No thread page that marks texts read was opened, and no test text was sent (the last allowed send was kept back, see below).

Result: 12 of 14 pass. 2 real defects filed. Every planted defect was caught (14 of 14 controls fired). The full harness is 76 of 78, with all 78 controls firing. The two failures are the defects below. How it ran: the production code path (the watch tick, the AppFolio source, the robot client with its refresh rules, the company wall, the filer) against a stand-in robot that answers the real route. Each check runs again with a defect planted, and the planted run must fail. Live production evidence sits next to each row.

#ScenarioExpectedObservedResultControl
ATenant texts the Willows line in AppFolioFiled as tenant within 60 s; Clara not wokenFiled as tenant on the first tick; worst case one 60 s busy-hours interval plus the tick (p95 4.1 s). 0 Clara replies, 0 sends. Live, Oct 14 01:51Z: Fede's text from his phone filed as tenant into his existing Willows conversation, Clara not woken, but 9.5 min late: two cold-session ticks (01:49, 01:54) at the 5-min evening cadence, recovered on its own at 02:00. Correct, not fast (#19413: retry at 60 s after a cold tick)Pass (label, placement, no wake); Fail (60 s)Fired (planted wake; quiet-hours clock)
BA teammate replies in AppFolioFiled once as an office reply; no wakeFiled once as an office reply over 3 ticks; 0 Clara repliesPassFired (teammate's id in Clara's set)
CClara's send through AppFolio, read back by the mirrorFiled once, as ClaraFiled once as Clara over 3 ticks. Live: the 23:43 send filed as Clara 32.5 s after AppFolio's time, and each AppFolio id appears once in the threadPassFired (old seat set)
C2A Clara row stored before the seat fix gets relabelledReads as Clara once the fix is liveStays "office reply" for good: the store writes each row once. Live: the 21:45 test send is still filed as a staff reply and counts as "staff engaged"Fail: #19404Fired
DDuplicates, out-of-order rows, AppFolio ignoring the cursor, overlap page read againEvery text once, in time order1/1/1/1 copies, in order. Live: every tick re-reads the overlap page (8 duplicates) and files none twicePassFired (store without dedupe)
ERobot 500, 502 with stale-cookie wording, 110 s timeout, AppFolio 429, tick past its deadlineTick records the failure, no refresh or login, next tick recoversAll five: 1 call (0 past the deadline), no retry inside the tick, paused 60 s (120 s for 429), 0 refresh or login asks, next tick files the text. Live: the 20:57 tick hit a cold session and the 20:58 tick recoveredPassFired (retry, refresh, no deadline)
FCold session, twice in a rowWatch pauses, never logs inPaused both times, 0 login or refresh asks, recovers once warm. The code sends the read with "never refresh" setPassFired
F2A database that stays cold alerts someoneAn alert after repeated cold ticks5 cold ticks: only the tick log records them. No error, no Sentry event, no alertFail: #19405Fired
GSend with no confirmationRead-back decides; never a second sendSend harness 22 of 22: AppFolio says no → refused after 1 post; row never shows → "sent, unconfirmed" after 3 reads, no resend; post dies → "unknown", the retry is refused, 1 postPassFired (22 of 22)
HDoes the background thread read mark texts read? (#19292)Unread state unchangedThread read through the data feed (list and thread only, no read-receipt call). Unread count 1 before, 1 right after, still 1 fourteen minutes and two ticks later (battle-test/h-probe/h-run-2.json; the before reading is not on disk, h-run-1.json is empty)PassNot applicable (live read)
I20-minute live soak plus the full daySteady ticks, 0 logins, 0 refusalsDay: 689 ticks, p50 2.6 s, p95 4.1 s, 3.0 requests a tick, 2,062 requests (limit 4,000), 1 cold tick and no 403s. Soak window: 4 quiet-hours ticks of 3.0–4.7 s, 3 requests each, 0 errors, 0 loginsPassNot applicable
JThe page as Fede sees it (light, dark, phone)Right labels, right order, no other companyOrder right, readable in both themes and on a phone, and no other company's names in the list or the messages. The 21:45 bubble mixes "Clara" on the label with office styling (part of #19404). Open: the thread title reads "Fede (as Kat)", source not found yetPass, one open questionNot applicable
Fede's Willows conversation: the Sept 28 card, then Oct 13 rows labelled Clara, Fede's own text, and the two Clara test sends
Fede's thread, light. The 3:45 PM row says Clara but has the office avatar: #19404.
The same conversation in dark mode
The same thread, dark.
The Willows conversation list with masked phone numbers
The Willows list, light. Phone numbers are masked.
Fede's conversation at phone width
Phone width.

What changed Oct 14, 02:00Z: the real-tenant case is now proven live for correctness (Fede's own text, tenant label, same conversation, Clara silent) and failed for speed: 9.5 min because the robot session was cold for two checks and the evening cadence is 5 min. Fixes in flight: a cold-session warning and 15-min page (#19410), and a 60-s retry after any cold check (#19413).

What changed Oct 14, 04:30Z (outbound door chain, all dark, default off everywhere): merged: the company setting, its admin card, the app-side send call, the robot route that posts into an existing inbox thread and reads it back (never logs in, refuses the wrong thread). Armed: the routing inside Clara's text sending (hold + one engineering alert on door-down, never her own line; the record id is the message's own id, never a content hash). Under review: re-driving texts held while the mirror was mid-read, and linking the read-back row to the sent record. Before the Willows flip: those two plus the vendor/staff question (recommend: vendor and staff texts stay on Clara's line; they have no inbox thread). Western Slope dry run: the read-only shadow harness is built and proven (0 writes, 6 controls blocked) but read 0 rows: the robot's Western Slope login has been dead since ~00:30Z (two refused logins, breaker, no re-login since); handed to the robot lane, no login attempted from here. Fairhaven cannot run the mirror as set up (no property on an AppFolio database).

What changed Oct 14, 06:30Z: the door-routing step is merged (dark; every company still on Clara's own line). The cold-check retry fix is merged: a cold robot session now costs the mirror about a minute per check instead of five. The relabel fix is merged and the one mislabelled afternoon row in Fede's Willows thread was repaired. Still open: the busy-hold drain and the read-back link, both retargeted to main and being rebased.

What changed Oct 14, 08:00Z: the outbound text door chain is fully merged and dark (setting, card, send primitive, robot route, door routing, drain of held texts, read-back link). Every company is still on Clara's own line. Next: Fede's answer on vendor and staff texts, then the Willows flip and one Clara text through the door.

What changed (Oct 14, 10:45 AM Denver; corrected 1:30 PM after an independent check): the Western Slope read-only dry run ran, after the robot's login key was fixed this morning. Two watch passes, no logins, no writes. It read 100 conversations (201 texts, part of the week; it stopped at its own request cap). 53 conversations would land on the right person and property. 47 would be held: 21 because our reader skipped the conversation's main person (a bug, fix under review), 12 unmatched prospects, 10 vendors (out of scope by Fede's ruling), 2 unknown numbers. Line mapping and Clara's seat were not blockers; an earlier version of this note said otherwise. The unread check proved nothing yet (the one unread conversation was never opened), and the no-send proof used stand-in controls rather than the real send code. Next: merge the two reader fixes, read the full week, and run a real unread test there.

Label check on the real inbox (Oct 14, 3:30 PM Denver): we replayed 920 stored texts from the Western Slope and Willows inboxes through the current reader and compared our label on each with AppFolio's. 689 of 920 match (75%, up from 58% on the old reader). The earlier "200 applicants shown as tenants" was a reader artefact, not a disagreement: an independent check of all 9 threads in production found people who applied and then moved in; AppFolio calls them tenants and so do our records, with matching ids and active leases. What is left: 184 rows where AppFolio's automatic texts showed as a teammate named "System" (fix merged today, not in this replay), one future tenant we still call an applicant, and one applicant our records still call a prospect (one issue filed for both). Prospects first: a fix that files only prospect and applicant threads and holds tenant threads as a dry run is in the merge queue.

Willows live battle test, passed (Oct 14, 2:30 PM Denver): six real texts from a test phone to the Willows AppFolio number, read by the mirror on the robot's warm session, no logins. Before the fix: both texts reached AppFolio within 3 seconds but PropFlow held them with nowhere to file (the guest-card link lived on the card, and the mirror only looked on the person). After: every text was filed on the prospect's existing Willows conversation, once, in send order, labelled "AppFolio text", in 26 to 63 seconds (median 42 s). The two held texts were filed on their own once the fix deployed. Clara stayed silent on all six (the mirror never wakes her), nothing was sent to anyone, AppFolio's unread count was untouched, no engineering alerts fired. Test card made inactive. Two defects filed: an inactive card still rewrites the person's name, and hold records linger after filing.

TextAppFolio stampMirror saw itFiledClara silent
1 (held before the fix)+3 s2 h later, after deploy+1 syes
2 (held before the fix)+3 s2 h later, after deploy+5 syes
3 known prospect+2 s+52 s+57 syes
4 right after a tick (worst case)+1 s+60 s+63 syes
5 burst, first+1 s+28 s+29 syes
6 burst, 3 s later+2 s+22 s+26 syes

Kept back: the last allowed live send was not used, because today's deployed send route can still fall back to a login. C was proven by replaying the two real sends from 21:45 and 23:43. Smaller things seen: on the Sept 28 card, two sentences run together with no space ("notes.Done"), filed as #19407. The thread title reads "Fede (as Kat)". It is not in the code, so it is likely a test-persona label stored on the record; this is unconfirmed.

8c. When Clara replies on a mirrored thread (Proposed, pending Fede)

8c. When Clara replies on a mirrored thread

Proposed — pending Fede  Oct 13. Nothing built or sent.

Fede's direction (Oct 14, voice note)

"The signals should come from multiple sources: who created the card, any notes, any activity log, any communication log. Clara should have access to everything. It's fine to have some hard-coded rules, but there could also be a discretionary reasoning layer, maybe longer term. It has to scale, not be custom to these customers. There's always referrals, or 'I found someone manually.' Clara should be aware that a person is already on this, and we shouldn't interfere. Example: the showing already happened; they might be toward the application stage. Kat did a showing outside the system today. How do we put that back in? Her texting Clara 'add this guest card, the showing already happened, send her the application'? How could Clara be a useful co-worker and not continue a machine that is about to get cut off?"

Later the same evening he added: Clara acts like an AppFolio employee who checks the guest card, activity log, notes and what was already sent before acting; one block of context with everything a human can see goes to the model alongside the deterministic rules, for first outreach and especially follow-ups.

What changes in 8cBeforeAfter
The signalIs a person already replying in this thread?The whole guest card: who created it, notes, activity log, every communication
How Clara decidesFixed rulesHard rules plus a reasoning layer
Where she startsFrom scratch, following up until a tourMid-journey: a showing already happened, or staff text Clara to add the card
A person is on itQuiet in that thread for 4 hPicks up where the human left off; never interferes
Follow-upsA timer sends the next scripted follow-upThe timer wakes Clara; she re-reads the whole card and every communication; if nothing happened she proceeds, if something did she adapts or stays quiet

The ask (Fede): "if a tenant replies, then that wakes up Clara… what if a human took over or is already replying, then we don't want to chime in."

The answer. A teammate's message makes Clara aware, never awake. A renter's message is hers unless a teammate wrote in the last 4 hours; then the team has 4 hours to answer before it comes back to her. Escalation still wins. Figure 6: N is 4, and step 3 becomes "wait, then come back". Scope: the AppFolio door, which the team sees; Clara's own line is unchanged. Until phase 3 is on, decision 4 holds: verdicts are recorded, nothing is sent.

WHO WROTE THE ROW Renterinbound; the only rowthat can wake Clara Teammateoffice side, not provenhers: she is aware, quiet Clara's ownmatches a send sherecorded, never a label Contexta blast or an automaticnotice: aware only History rows, and rows the wall could not place: aware only. Unsure who wrote it: teammate, the quiet side. WHOSE TURN Clara's turna renter message gets her answer(recorded only until phase 3) Team's turn: 4 ha renter message waits;a teammate's answer closes it a teammate writes 4 h with no answer: comes back or, under C, a release: at once Read the thread againjust before a send or an approval;anything new but hers: stop Sends through the door it came in (8a) escalated, from either turn Human-owned: escalatedwins over both turns; captured,never answered, no timer ends only when released on purpose
Figure 10. The engage rule, proposed. The read before a send is 8a's guard, reused.

Do we have this today? Partly. Teammate rows never wake Clara, but neither do renter rows on the mirror. For replies, the only live "a person has it" signal is escalation, with no timer; the "human replied" hold binds at no property. Clara's own follow-ups already stop when staff are engaged. There is no window and no read before a send.

What ends the team's turnStops two voicesReusesNo answer in 24 hSlowest 10% waitBoth answered (ceiling)
Today, office only——22%41 h—
A. The clock: 4 h after the renter's messagethe turn, and the read before a sendescalation gate, 8a's read, delivery status, watch tick0%4 h15%
B. A person: an escalation releasedthe same, no clockthe "human replied" hold, the release22%40 h0%
C. Whichever comes first (recommended)as AA, plus the release0%4 h15% (no releases to price)

Recommended, updated Oct 14: decide from all-card evidence, not the thread alone. Hold when a person is on it. The lead-source gate itself is owned by the lead-source gate page, which is primary. Still proposed, pending Fede's pick; no code before that.

Recommended: C, 4 h for both clocks. The only new code is the window and its come-back. Why 4 h: 89% of renter texts after a teammate's message arrive within 4 h of it, and the office answers 74% of office-hours waits within 4 h (81% within a day); longer mostly buys silence. Shorter means more overlap: 1 h gives 21% both answered. That column is a ceiling, measured with no Clara in the thread, and it is repetition, not collision: the read before a send stops collisions (trap 30).

Must never happen:

  • Clara answers a teammate or herself. Only inbound rows wake her; "hers" is a send she recorded (8a's read-back id), never the sender label (wrong both ways at Willows on Oct 13). Until that id exists, a send of hers that the echo match misses reads as a teammate's: quiet, never loud. So the id lands first.
  • Two voices at once (trap 30): the read before a send, and at a Supervised approval.
  • Wrong company, old rows, blasts (traps 10, 17, 53, 54): only rows the wall placed in one company wake her; history and blasts are awareness only.
  • A failed text as an answer (trap 55): it starts the team's turn but answers nobody, so the message still comes back.
  • Silence forever (trap 18): the come-back.

How it fits. Supervised decides whether a draft waits; this rule decides whether there is one. The approval recheck reads the "human replied" flag (bake-gate.ts L434–438), which phase 2 deletes (traps 18, 48), so the recheck moves to the thread read in the same change. The come-back rides the watch tick, no new job: it needs a fresh read anyway (overnight, up to 30 minutes late). AppFolio assigning a card to Clara's seat cannot be a hand-back yet: that stamp also marks Clara's own claim.

Coming back, Clara answers the question and names herself once. She never apologizes, blames, repeats or contradicts a teammate, and she restates a teammate's promise. A thanks or an ok gets nothing. Anything else goes to the office with a reason.

Prior art. Intercom: a teammate's reply stops Fin; reassigning does not (open request). Zendesk: one controller, released explicitly. HubSpot: hand-back after inactivity is an open idea. Search snippets only: ManyChat pauses 30 minutes after a human reply; EliseAI resumes follow-ups after a staff reply. C is a timed pause plus an explicit release.

Harness (local, never CI): 16 scenarios on the four real rows of Fede's Willows test thread and the traps above. Today's labeler fails one (a person's text on the shared login filed as Clara's); 15 await the build, reference passing; 16 of 16 planted defects caught. Data: Western Slope prospect texts, 12 months, 2,957 waits, counts only.

8d. When AppFolio is down: today's incident and fault-first writes (Proposed, pending Fede)

8d. When AppFolio is down: today's incident and fault-first writes

Why nobody was paged for this outage, and the on-call design that follows: On-call and alerting: the state of it (Oct 14).

Proposed — pending Fede  Oct 14. Incident review and design; nothing built. Times are Mountain.

The ask (Fede): "we should obviously never alert the customer. It should be an engineering alert… as soon as we recover… replay the work that was lost and move on… fault first."

Two things happened; only one was the outage.

1. The outage. The robot's AI key died at 1:26 PM Tuesday, and Western Slope's robot could not sign in to AppFolio for 21 hours. Nothing was lost: all 11 waiting writes went in by 10:31 AM, the oldest 20.5 hours late. No message went out because of it.

2. The emails. Three tours booked this morning had no home picked; each emailed Western Slope's leasing team to add the showing in AppFolio. An open fix deletes that email.

Alerts worked; the hand-off didn't. First alert at 37 minutes, cause named at 45, key swapped 19 hours after that: only a person could, and nothing escalated.

Part 1. What happened

TimeWhat happenedWho knew
Tue 1:26 PMAnthropic starts rejecting the robot's AI key. Western Slope's sign-in dies at its first step, an AI read of the login page, before any password.—
2:03 PMFirst alert in #alerts: sign-in lock tripped.Engineering
2:11 PMOur automatic triage names the dead key and raises it for a person.Engineering
2:36 PMSecond alert: work-order lookups refused.Engineering
3:09 PMA change stops one alert per work order. A symptom; the key stays dead.Engineering
Wed 12:59 AMA robot change stops counting AI failures as bad passwords. Each doomed try still spends our robot's sign-in allowance (24 a day).Engineering
2:00–2:07 AMTwo more alerts name the key. Triage tests it live, rejected, and raises "rotate the key" again.Engineering
about 5:20 AMThe day's sign-in allowance is used up.Robot log only
9:26 AMA person with console access swaps the key.—
9:30 AMA waiting guest card is refused: no sign-ins left until 10:17.Robot log only
9:38–9:46 AMThe three no-home emails (a separate gap; the tours were booked after the swap).Western Slope
10:18–10:20 AMFirst try hits a redirect loop; the robot resets its browser by itself and signs in.—
10:30–10:31 AMThe 15-minute sweep sends all 11 waiting writes.—

Detection worked; the loop from alert to action did not, and recovery then waited on an allowance and a timer.

Five whys.

  1. Writes stopped: Western Slope's robot could not sign in.
  2. Sign-in starts with an AI read of the page, and the key was dead: one key, no health check, no spare. Why it died is open; the console's key log will say.
  3. 19 hours to fix: only a person could swap the key, and each ask was one line among about 30 in #alerts, with no page, repeat or clock.
  4. An hour more: overnight retries had spent the sign-in allowance on a key we knew was dead.
  5. Ten more minutes: nothing wakes waiting writes on recovery; they wait for the next sweep.
CompanyEmails from the outage"Showing not recorded""Guest card not created"Late in AppFolio
Western Slope03 (no-home gap)011 guest-card writes: 2 overnight, up to 20.5 h late; 9 this morning, up to 1 h 14 min. No work orders were waiting.
Situs Group000Robot also locked out; nothing was waiting
JP&Co, ConAm000None; JP&Co's robot stayed signed in

Part 2. How AppFolio writes work today

TODAY: ONE APPFOLIO WRITE Clara actsa lead, a reply,a tour Saved to dothe guest-cardqueue, 13 kinds Workeron save, andevery 15 min Robot signs inAI reads the page;24 tries a day AppFoliocard, note,showing what happens next AppFolio unreachablewaits and retries for 72 h;the queue raises no alertuntil it gives up Can't be done as askeda tour with no home: parkedat once, and an email tothe leasing team Robot backnothing wakes the queue;each write waits out its gap(up to 1 h) and the sweep after 72 h Parkedengineering is paged; a new lead's card also emails the leasing team.Replay is an engineer's script, one write at a time. Nothing replays itself. Outside the queuework orders, renewals, listings,texts: table below Red: the customer hears about it. Amber: work waits. Today's outage stayed amber; the three emails came from the middle red box.
Figure 11. The write path as it is. The queue held up today; its weak spots are the red boxes and the slow wake-up.

Only the guest-card queue waits well, and it gives up at 72 hours and replays only by hand. Other writes drop, stick or quit within minutes. Four paths tell client staff about our failure, two of them live.

Write to AppFolioSaved first?In a 21-hour outageWho hearsReplay
Live at customers
Guest cards and what hangs off them: notes, assign, first-reply record, application send, showings (11 of the queue's 13 kinds)yes, the guest-card queuewaits, retrying for 72 h, then parksengineering, only at the park; client staff for a new lead's card at the park, and at once for a showing with no homean engineer's script, one write at a time
Linking a new card (waits for it to appear)yesgives up after 10 minengineeringa replay re-parks at once
Work orders: create, notes, edits, vendor, closeyes, on the work orderkept, but nothing re-sends them; a close not sent within 24 h is reverted by our work-order syncengineering; a dashboard noticethe next message or edit, or an admin flush
Handyman set as the vendor; voiding a renewal after the office picks month-to-monthno, a direct calldroppednobody (a log line)none
Unpublishing a pre-leased listingno, rechecked every 15 minheals on the next runengineering logsautomatic
Staff and admin buttonsnofails at the clickwhoever clickedclick again
Dark today (off at customers)
Clara's texts into the AppFolio inboxheld when they failheld for good; only "busy" holds are retriedengineering, once per outage"busy" holds only
Renewal offersyes, with a retry jobheals about 3.5 h after recoveryproperty manager email and text at 24 hautomatic
Lease draft on approvalnostuck for goodoffice emailnone
Turnover work orders, move-out chargesyes, with a sweepgiven up for good after about 18 minengineering: "file manually"a person clears a flag
Holdover month-to-monthno, a daily runheals the next dayengineering, #alertsautomatic
Term changes, move-out drafts, listing publish, post-call guest cards, messages through the card threadnodroppedengineering logs at mostnone

Part 3. Goal 1: customers never get an engineering message; engineering always does

Why it can happen. 19 places can email client staff any text through one door, and one checks the "never a client" list; the notice engine takes AppFolio failures as client facts; and "parked" means "tell the leasing team".

How it worksA failure email to a clientThe next new senderCost
A. One list, checked at the doorevery staff email names its kind; a kind not marked safe for clients is refusedblocked when it runsonly if its author labels it rightsmall
B. Two doors, by construction (recommended)client notices are built only from a closed list of client events (a tour booked, a lead needs a person). Failure code has no client door to call; it can only reach engineering.cannot be written: the build failscoveredmedium: move the 19 senders
C. A filter on outgoing emailscan staff email for failure wordingmostly blockedno: wording driftssmall

Recommended: B. Proof: a planted "please add it in AppFolio" notice on a failure path fails the build. A's list becomes B's check.

The other half: engineering hears once, with a clock. Today each failure is its own line, nothing escalates or says "recovered", and the queue stays silent for 72 hours.

ONE OUTAGE, ONE STORY (PROPOSED) Opensa key check fails, orthe first write fails:1 alert naming the fix Still open30 min: a needs-a-personflag; 2 h: raised again;at most hourly after Recoversthe check passes: thequeue wakes at once,oldest first, same ids Closes1 summary: back at,writes in, 0 lost,minutes to recover While it is openwrites wait and never expire; client staff hear nothing; a write that can't be done as asked waits for engineering. Every place a third-party write can fail must open an episode; a test lists them and fails if one is silent.
Figure 12. Proposed: one alert per outage with an escalation clock, and a recovery summary. Today's would have read "back at 10:20, 11 writes in, 0 lost".

Goal 2: assume AppFolio, the robot and every key will fail, and lose nothing

PatternIn plain wordsDo we have it?What today showed
Save first, send later (outbox)the write is saved with the change that caused it; a worker sends ityes, the guest-card queue0 lost
Retry with growing gaps, same idtry later, further apart, never a doubleyeskept trying a key we knew was dead
Breaker with a test trystop calling a broken system; one cheap try decides when to resumea sign-in lock, no test trydoomed tries spent the allowance
Hold and replay (dead letter)work that can't go now waits, then replaysparked writes, an engineer's scriptnothing replays by itself
Durable workflow (Temporal)each write is a program that survives restarts and waits as long as neededfor tours and follow-ups, not AppFolio writes—
Reconcile sweepcompare what we meant to write with what AppFolio hasfor listings only; turnover's is dark and quits after 18 min; work orders have none—
Key health checktest every key on a clock, not on first usenodead 20 hours before the swap
A. Harden our queueB. Hold pile, an agent replaysC. A durable workflow per write
The ideaevery AppFolio write goes through the queue; during an outage writes wait and never expire; recovery wakes the queue at onceparked work collects; after recovery an Opus agent reads each item and replays, skips or escalateseach write is a Temporal workflow that sleeps until a "robot is back" signal
Goodsmall steps on code that held today; predictablejudgment for odd cases (the tour already happened)durable by design; full history
Costsabout a dozen direct-call writes must move inslower, costs tokens, can be wronga big move; still needs one shared sign-in gate

Recommended: prototype A and C on one harness; the numbers pick. B's agent runs in both, only on leftovers rules can't settle (the tour already happened): dry first, acting once its calls match ours.

Fix or new? Fixes: no switch, live everywhere on merge, nothing new for customers, only fewer emails. Already moving: the fix for showings with no home; the guest-card email is next.

After the pick, one small change each:

  1. Delete the last "please add it in AppFolio" email.
  2. Key check every 5 minutes, plus Figure 12's alert.
  3. No sign-in tries while the check fails.
  4. Recovery wakes the queue.
  5. No expiry during an open outage.
  6. Direct writes move into the queue, one per change.
  7. Goal 1's two doors and build check.

Part 4. Prove it locally first

Harness (local, never CI): an in-memory queue, a fake robot and a fake clock replay today. The key dies; 11 writes and a no-home tour arrive; the swap lands at hour 20 and sign-in returns at hour 21, after a redirect loop. Variants: a 5-minute blip, a 4-day outage, a crash mid-write, a duplicate event, a write refused for good.

Planted defects, each must turn a score red: no recovery wake-up; a write marked done without AppFolio's confirmation; a parked write sent to the staff door; no alert de-duplication; no recovery summary.

Work lostAll writes in, after the fixCustomer messagesEngineering alerts per outageRecovery summaryDuplicatesLines changedDefects caught
Target02 min after sign-in01, plus escalations10fewer is better5 of 5
Today (the real incident)065 min (21 h from the fault)0 from the outage; 3 from the no-home gap4 lines and 2 asks, no escalation00——
Prototype A
Prototype C
B's agent, dry run

Prototypes stay local until Fede picks; then each change climbs the testing ladder to Willows.

Decisions for Fede

1. How tight for customers. (a) Recommended: two doors by construction, with a build check. (b) One list, checked at the door. (c) A filter on outgoing email.

2. What to prototype. (a) Recommended: A and C head to head, with B's agent in both. (b) A only. (c) All three, separately.

3. The key check. (a) Recommended: build it now as a fix; it needs neither 1 nor 2. (b) Wait for the prototypes.

4. A spare AI key, swapped in when the check fails. (a) Recommended: yes, once the console shows why the first died (your console step). (b) No spare.

For engineers: code pointers and sources

Code read at origin/main, Oct 14 (read-only). Line numbers drift; search the named function if a line has moved.

  • Queue rules (src/lib/domain/pms/guest-card-outbox.ts): two retry clocks, infra 72 h from enqueue with backoff 5 s doubling to 60 min, other errors 1 h with backoff to 5 min (556–591, 622–643); infra classifier, where "login breaker open" lands (678–686); the 12 h self-healing ceiling never applies because the infra branch decides first (drain 3681–3685); the stuck sweep skips rows with a future next attempt, so an outage pages nothing until the 72 h park (1207–1232); redriveFailedIntent (1476–1519); correctFailedShowingTarget (1570).
  • Drain (guest-card-outbox-drain.ts): park on non-retryable (3654–3656); clocks, and nextAttemptAt ignoring retry-after (3679–3702); per-attempt logWarn only (3727–3730); notifyCardNotCreated on any create_card park, including the 72 h infra give-up (3740); notifyShowingNotRecorded on no_home (3767); expected parks silent (3866–3875); no_home to alertEngineering (3879–3887); pms-outage logError (3888–3904); cancel_tour is never claimed and has no producer (3126–3135).
  • Runner (guest-card-outbox-drain-runner.ts): card-not-created email through tellStaff, hello@ copy, bypasses the kill switch, not suppressed in test scope (93–177); silent-skip alert (180–204); showing-not-recorded (211–278); event trigger, not called by the showing Lambda or mark-inactive (818–851).
  • Store (src/lib/data/dynamo/pms-write-intent-outbox.ts 277–333): redrive keeps enqueuedAt, so a row parked at 72 h re-parks on its next infra failure, and a redriven link_card re-parks at once.
  • Cron (src/app/api/cron/guest-card-outbox-drain/route.ts 157–158, 190–203; vercel.json 68–69; with-run.ts 155–170, 250–260): every 15 min; "newly failed" logCritical; page deduped 24 h per job.
  • Robot client (src/lib/integrations/appfolio-browser-agent/l4-core.ts 674–728, 1134–1150; l4-permanence.ts 51, 62–71): budget and breaker refusals map to PMS_LOGIN_UNAVAILABLE, never permanent. Robot side: lib/loginBudget.ts (5 an hour, 24 a day, charged before the drive), lib/appfolioLoginFlow.ts 87–113 (a model failure no longer trips the breaker, still spends budget).
  • Alerting: team-notify/eng-alert.ts 19–29 (default sink is console.warn); team-notify/compose.ts 225 ("please add it in AppFolio"); alerts/silent-skip-alert.ts 157–224 (once per company and cause per hour); pms/pms-inbox-watch-alert.ts 35–45 (cold-session episode alert: inbox read only, and only where the mirror is on); the per-company outage alarm (PMS_OUTAGE_ALARM) is never fed by the queue.
  • Staff email door: staff-notify/tell-staff.ts (pass-through; 19 calling files); notifications/internal-only-notice.ts 26–82 (the audience list; one caller, held-first-email-team-notice.ts 105).
  • Writes outside the queue (Part 2 rows): work-order sync src/lib/temporal/workflows/maintenance-workflow.ts 216–226, 435–455, flush-all skips partly synced and conversation-less work orders (src/app/api/admin/maintenance/work-orders/flush-all/route.ts 98–104), close reverted after 24 h (src/lib/domain/pms/writers/work-order.ts 1301–1340); handyman vendor assign, claim never released (src/lib/domain/maintenance/tech-sms-dispatch.ts 751–791, 1036–1049, 1147–1151); office month-to-month void (src/app/api/leasing/renewals/approve/route.ts 337–372); listing unpublish (src/lib/domain/leasing/listings-sync.ts 160–182); text door holds, no expiry, only busy drains (agents/clara/lib/messaging/adapters/appfolio-inbox-text.ts 175–222, src/lib/pms/pms-text-door-drain.ts 53–78); renewal prepare, PM email and text at 24 h with no infra filter (src/lib/domain/renewals/retry-failed-prepare.ts 1056, 1253, 2082); lease auto-draft, office email via tellStaff, marker never cleared (src/lib/domain/leasing/new-lease/new-lease-draft-notify.ts 107, 151–180; auto-draft-on-approval.ts 141–146, 853); turnover work orders and charges, terminal after 6 sweeps (src/lib/domain/turnover/turnover-wo-reconciler.ts 578–645; src/lib/temporal/activities/turnover.ts 799–885, 1797–1833); holdover (src/lib/domain/leasing/renewal-auto-start.ts 1272, 1776–1785); post-call guest card (src/lib/integrations/voice/postcall-guest-card-write.ts 212). The agent job queue acks a login outage and never redelivers it (lambda/agent-runtime/src/runtime/dispatch.ts 162–175, 218–227). Not audited: src/lib/domain/maintenance/release-work-order.ts (landed during the audit).
  • In flight: #19579 (a showing with no home is held and retried, never told to staff; open), #19576 (delete the card-not-created email; issue), #19574 (tour booked on the wrong home; issue). Merged: #19351 (no per-work-order page while the sign-in lock is open, 3:09 PM Tuesday), #19410 (cold-session episode alert), #19473 (text door busy-hold drain, dark).
  • Sources (read-only): write-queue rows by state, Oct 13–14; SendGrid activity for staff emails; Sentry issues; #alerts history and its triage thread; robot request logs, 9:20–10:40 AM.

9. Western Slope's inbox, one to two years

Fede's question: what share of the inbox could Clara handle, so the office spends its day on the rest? Nobody knows yet; the read gives the number. One read, two uses: the two-year read is the phase 1 backfill (pending rows inside Western Slope's organization, waking nothing) and the analysis runs over that store. It is built once and runs on the subscription, outside CI. Labels and counts land on this page; message text is read by the classifier, the judges and Fede's spot checks, all on the subscription, and never enters the repo, CI, an eval file or a page.

  1. Validated at Willows on Oct 12: the list read, no mark-as-read, stable fingerprints. Still to validate there before touching a customer: the thread read for a tenant party (verified for prospects only) and an overlapping page walk, because ties at a page break can skip a row.
  2. Pass A, list pages only (minutes). One hundred threads tonight (a single probe, aggregates only, no text kept), then the full walk back two years at one request per 20 threads. Gives thread count, the tenant / applicant / prospect mix (tests the "90% tenants" figure, which is a gut number until then), properties, assigned staff, unread, failed sends, and one last message per thread for a first rough taxonomy. Landed: the probe numbers are in 9.5.
  3. Pass B, thread reads (hours). Each thread's messages from the start, after the robot keeps the sender's name on every row. At one to two pages a thread, two to three thousand threads is 3,000 to 6,000 requests: two to five hours at one request every two or three seconds, overnight, resumable, stopping on the first throttle, JSON only. Raw rows stay in the organization's store and on our machines (tenant PII), never in the repo.
  4. Classify offline on the subscription, no metered keys: lane (maintenance request, maintenance update, renewal reminder, renewal reply, rent and billing, lease and move, access and keys, leasing inquiry, tour, application, general question, complaint, emergency, notice or blast, other); sender class (named staff, shared login, bulk, system, tenant, prospect, other); handling (template or bulk; routine and answerable from facts on file; routine but needs an action Clara cannot take yet; needs human judgment; noise such as "ok", "thanks"). Bulk needs no model: the same body to many threads within minutes. Quality: 200 random messages labeled twice by independent judges plus 20 Fede spot-checks; agreement reported.
  5. Numbers for Fede, on this page: messages per month in and out by lane; share of staff texts that are templates or blasts; share of inbound that needs a human, by lane (the "what percent escalates" number); staff texts per person by lane and median staff response time, office hours versus after hours; inbound after hours and how long it waited; tenant versus prospect versus unknown; double texts (a Clara text and a staff AppFolio text to the same person within two hours); placement rate (share of threads that would land in a PropFlow conversation today).

9.4 The 12-month communications census (Western Slope, as of Oct 12)

Read-only, counts only, no AppFolio calls: the AppFolio prospect timeline already synced into PropFlow (1,172 guest cards, Sep 2025 to Sep 2026), the shared leasing mailbox (the September audit for the year, a scrubbed four-month corpus for the detail), the Clara line since Sep 18, and the work orders. Each block covers a different population, so the rows are never added across blocks. The tenant side of the AppFolio inbox is the one channel this census cannot see yet; the two-year read above is what closes it.

Channel, twelve months unless markedInOutWho sends the outbound
Texts on the AppFolio line, prospects only4,0155,808named staff 3,762 (65%); the shared "Assistant PM" login 1,242 (21%, since March); AppFolio automation 804 (14%); Clara 0
Emails on AppFolio cards, prospects only95675 + 131 automaticnamed staff 512; the shared login 163
Calls logged on cards264staff
Shared leasing mailbox, all mail13,765201 (four months)only 2,248 (16%) are prospect inquiries; 2,389 are Zillow digests; the rest is operational mail, voicemail notices (about 1,000 a year), vendors, promos
Clara's line, Sep 18 to Oct 12 (upper bound: our own test calls cannot be separated)193 calls, 138 texts, 99 emails290 texts, 204 emailsClara 278 texts and 202 emails; staff inside PropFlow 10; staff inside AppFolio 0, by construction (message sync off or unset on all 90 properties)
Work orders created (requests, not messages; months before June look partial)813of the 422 with a readable log: resident portal 41%, staff-keyed 34%, AppFolio call center 16%, system 9%

Maintenance by email: almost never, as Fede said. In four months of the leasing mailbox (5,517 emails) four tenants emailed a new maintenance or property-condition request (a swamp cooler, carpet, a stove filter, a full dumpster), four chased a request made another way, two were borderline; 547 work orders were created in the same window, so email is under 1% of new requests. About eleven maintenance voicemails landed in the same inbox, three of them new. We saw no email intake path: an emailed request becomes a staff-keyed work order by hand ("I'll let the team know", "put a new work order in"). The resident portal, the call center and staff typing carry the volume. Caveat: staff mailboxes and the tenant half of the AppFolio inbox were not read; some staff-keyed orders may have started as an email or a text.

Year to date, Jan 1 to Oct 12 (read Oct 12, the mailbox read-only): 11,690 emails reached the leasing inbox; 6 opened a new maintenance request and 5 chased one, against about 1,700 work orders (AppFolio's own list through August, the mirror since), so 0.4% of new requests arrived by email, 0.6% with the follow-ups; 15 repair voicemails landed in the same inbox, 3 of them new. By month in the scratchpad (pms/census-ytd.md); the inbox folder only, staff mailboxes and texts not read.

What else the census says. Email is the biggest pile but mostly machines; texts are the biggest two-way channel we can see, and the office sends more than it receives (5,808 out against 4,015 in). A fifth of staff texts go out under a shared login, so attribution by signature matters (trap 64). Zillow is 79% of inquiry emails but 46% of all inquiries AppFolio recorded; Rent. and Zumper send none. On Clara's line, 161 of 269 conversations match a real AppFolio prospect or tenant and 148 sit on the company front-door row with no building known. Pending the PMS pull, labeled as estimates: about 20 tenant, vendor and owner text conversations active per day in the AppFolio inbox (the Oct 8 canary), about 35 emails and 57 texts per household over a tenancy, and the call-center log beyond the 68 calls that became work orders.

Method and the exact read commands: scratchpad pms/census.md and census-work/ (scripts and aggregates, no names, no text). The by-hand sorts of emails and voicemails are good to about plus or minus two; the guest-card, work-order and PropFlow counts recount exactly.

9.5 What the newest 100 threads look like (read Oct 12, list only)

One read-only call to the robot's inbox-list route at Western Slope on Oct 12, 18:28 Mountain: five list pages, newest 100 text conversations, no thread opened, no login, seven GETs in all. Aggregates only; the raw rows were deleted. The numbers below are floors (a thread counts on the day of its last message).

NumberWhat it saysWhat it changes on this page
100 threads in 10.2 days, about 10 a day; 35 on Monday Oct 12 alone (19 of them bulk blasts)the inbox is lumpy, not steadya one-minute tick at 20 rows a page still sees a 35-thread Monday in two pages; the request math in section 3 holds
43 of 100 end with a one-to-one human message (27 from the person, 16 from staff); 26 end with a bulk blast; 28 with an AppFolio automatic notice; 3 with a send from Clara's loginmost of the inbox is outbound and not a conversationthe mirror marks each message as person, staff, bulk, system or Clara; the follow-up gate stands down only on a staff-written message, never on a blast or a notice (trap 63)
0 of 100 flagged unread, while 27 end on the person's message; 15 look like they need an answer; median wait 78 hours, 11 waiting more than 3 days, the longest 10 daysthe unread dot is no signal at all"who is waiting" is computed from direction and time, never from unread (decision 2); it is also the first number leadership never sees today
Last sender: a shared "Assistant PM" login 34 (25 of them blasts), AppFolio's "System" 28, Kat 4, Jay 4, Clara's login 3the login is not the person; staff sign with a first name ("-Kat")attribution takes the signature's first name over the login name when both exist, and shows the login otherwise (phase 1 parser, trap 64)
47 applicants, 6 prospects, 11 tenants (4 current, 4 past, 3 hidden), 36 numbers AppFolio could not match to anyone; 42 threads with no property; unit set on 8AppFolio's own identity is thinidentity runs against PropFlow's people first (decision 3); an unmatched number lands as a plain conversation on the company's front-door row (the placeholder), never on a placeholder building and never as a leasing record (decision 3)
Topics of the last message: 50 maintenance updates (27 automatic, 20 blasts, 3 staff), 17 thanks and acks, 7 application, 5 renewal, 3 complaints, 0 maintenance requests raised by textresidents at Western Slope do not open maintenance by text; the office pushes updates out by textFede's question about inbound maintenance by text or email: rare by text here, under 1% by email over four months (9.4)
4 threads end in an undelivered staff message ("number may not exist", "does not support text messages")staff do not see the failurethe mirror carries delivery status; a failed staff send is something Clara can surface (phase 2)
17 automatic notices are signed with an owner entity's name, not Western Slope'sthe signature is the property's owner, not the managerattribute by property, never by signature or by database (trap 65)
At least 6 of 100 last messages touch a sensitive personal matterthe mirror holds sensitive textsame care as the AppFolio inbox: raw rows never in the repo, classification on the subscription, no PII in evals or pages
92 of 100 last activities fall Mon to Fri 8 to 18 Mountainthe inbox is an office-hours channelthe tick follows the door's clock (decision 12); the year read in 9.6 puts 3% of inbound texts between 8 pm and 8 am

Not proven by this read: whether listing leaves threads unread (every row was already read; the Oct 12 JP&Co capture showed unread rows staying unread across three list reads). SMS only; email and chat conversations in the inbox are not in this count.

9.6 When texts arrive, and how fast the office answers (read Oct 12)

Read-only, counts only, Denver time: a year of Western Slope's AppFolio texts from the prospect timeline already synced into PropFlow (3,983 inbound, Sep 2025 to Sep 2026), the 5-household tenant canary, and the Clara line since Sep 18 (production, Query reads only). Fede: "I need data… a distribution of after work to when people go to bed, then almost zero when people are sleeping, maybe a window before work, then core work hours." The answer: core hours carry almost everything, there is no after-work bump on this door, the pre-work window is small, and the night is near zero.

Texts in, by hour, on Western Slope's AppFolio line (a year, prospects: 3,983) 15 / 30 MIN · 0.3% 5 MIN EVERY MINUTE · 95.8% 5 MIN 15 / 30 MIN 12a 3a 6a 211 480 9a 560 516 421 12p 333 339 406 3p 329 128 6p 9p How fast a text is answered, by when it arrives Denver time, Sep 2025 to Sep 2026. Tenants beyond a 5-household canary are not in the year of texts, and 90% of these texts answer something the office texted out the same day, so the evening is quiet partly because nothing goes out in the evening. The schedule: an hour under a tenth of the busiest hour's traffic polls every 5 minutes, under a fiftieth every 15 until midnight and every 30 from midnight to 6 am (Fede, 10-12: 15 is too often while people sleep); any change pulls the next 30 minutes back to one minute. Office hours, Mon to Fri 8 to 5 84% of texts office answers in a median 8 min 19% get no staff text in 24 h Weekday evenings and overnight 9% of texts median 13.8 h 31% get no staff text in 24 h Saturday and Sunday 7% of texts median 70 min 30% get no staff text in 24 h Clara's line, any hour 3.5 weeks, 200 texts and emails median 20 s 8% get no reply in 24 h
Figure 8. Texts in by hour on the office line, the three bands of the tick schedule, and how fast a text is answered by when it arrives. Tenants beyond the canary are not in the year of texts.
When it arrivesShare of inbound textsOffice answers in (median)No staff text within 24 hThe tick
Mon to Fri, 8 am to 5 pm84%8 min19%every minute
Mon to Fri, evenings and overnight8.6%13.8 h31%every minute to 7 pm; five minutes 7 to 10 pm and 6 to 8 am; fifteen 10 pm to midnight, thirty to 6 am
Saturday and Sunday7.2%70 min30%the weekend's own curve
Deepest night, 10 pm to 7 am, any day0.9% (37 texts a year)10.3 h40%fifteen minutes to midnight, thirty after
Clara's line, any hour (3.5 weeks, 200 texts and emails)57% outside Mon to Fri 8 to 520 s8%n/a

What it changes on this page. Decision 12: the tick follows the door's clock instead of one minute always (Fede 10-12, 8 pm). The office's evening wait (median 13.8 hours, 31% with no staff text in 24 hours) is the "who is waiting" number the third pass asked for: the "waiting on the office line" view in slice 2 shows it for the first time, and the evening and weekend share (16% of texts) is phase 3's case. Limits: prospects only, and 90% of inbound texts answer something the office texted out the same day, so the curve follows the office's own sends; on Clara's line, which answers at every hour, a third of texts arrive between 5 and 9 pm (our own tests not separated). The curve is per door and per customer, recomputed monthly from the mirrored rows, and it will move once Clara texts people in the evening. Method and the full tables: scratchpad pms/hours/hours.md.

Risks: rate limits (one 429 stops the run), account lockout (the read never logs in; it pauses on a cold session), marking threads read (JSON only), PII (raw data stays in the store and on our machines; evals use synthetic names), attribution (shared logins reported as their own sender group). Willows shares its database with a live customer, so Willows polls fingerprint rows and never store previews or numbers. The 12-month cross-channel census is 9.4; the September leasing-inbox audit is its email half.

10. Traps, side effects and likely bugs

What a customer would see if we got it wrong, and the mitigation. Phase = where it first bites. Sources: code map, 10/11 and 10/12 reads, Opus round 1 (43 traps, merged), the engines map (20 engines). Rows 47 to 62 were added in v1.1.

#TrapCustomer seesMitigationPhase
1Loading a thread page marks it read in AppFolio (unverified: from a code comment, never captured).Tenant texts look handled; nobody answers.JSON reads only; a test that fails if any read path requests the page.1
2Per-building polling (today's feed).Throttling, then every AppFolio job stalls.One list read per customer per tick; delete the per-property path.1
3The list endpoint is unknown; rows may lack an unread flag or a last-message id.Either missed changes or full re-reads.Capture first; fingerprint on whatever the row carries; fall back to a slower full walk.1
4429 storms (Situs at connect; incident of 2026-09-30, AppFolio sync arms; see the feature-flag sprawl audit).All sync for that customer stops.Customer-level budget shared with the report jobs; stop on the first 429; quiet nights.1
5Session expiry triggers repeated logins.AppFolio locks the account; about five failures lock it, observed Sept 7 on Western Slope, not AppFolio documentation (HANDOFF-westernslope-appfolio-access-2026-09-07.md).One session per customer; one re-login per expiry; bounded retries; alert.1
6Cursor lost or reset.Old messages re-appear as new; engines re-fire.Idempotent by PMS message id; "already have" check before any engine signal.1
7Edited or deleted messages in the PMS.Stale or duplicate bubbles.Key by id, store a content hash, update in place.1
8Time zones in PMS timestamps.Messages out of order, wrong day in "last week".Store UTC, display in the property's zone, sort by time not id.1
9Email bodies: quoted history, signatures, HTML; MMS images.Walls of text, missing photos of the leak.Strip quoted history, keep raw, store attachment references.1
10Bulk sends (one reminder to 200 tenants).200 "Jay wrote" rows; engines think staff engaged everyone.Detect bulk (same body, same minute, many parties); label bulk; not an engagement signal.1
11Shared logins ("Assistant PM", "Clara PropflowAI") used by humans.Wrong attribution.Label by login name; never infer human or robot from it.1
12Wrong person: one number, many parties.A cosigner's text on the tenant's thread.The PMS party wins; ambiguity recorded; household link.1
13Former prospect, now tenant at another building.Two people, split history.Identity merge by PMS ids and claims; one person, thread per property.1
14Unmapped property or unit on a row.Message lands on the company placeholder.A plain conversation on the company's front-door row (the placeholder), never an inquiry, a tour or an AppFolio write (Fede 10-08: a conversation may sit on the placeholder, a leasing record never); retried every sync (decision 3).1
15Tenant created by the mirror, then created again by the rent-roll sync.Duplicates everywhere.Both create paths match by PMS tenant id first.1
16Duplicate guest cards in the PMS (card 2993 at Western Slope).Two parties, one person: messages split.Many-to-one party map; the merge engine handles it.1
17Backfill wakes the follow-up engine.Clara nudges a lead from last year, or stops a live cadence.Backfill mark; engines ignore rows older than the turn-on cursor.1
18The forever-hold.One staff text and Clara never speaks on that lead again.Replace with the follow-up decision; first fix after the design.1
19"Lead with no reply for 15 minutes" alerts fire on mirrored tenant texts.Alert spam while the office is already answering in AppFolio.Alerts only for Clara's doors in phase 1; the engage decision owns them later.1
20A staff member replies from the PropFlow thread view.The tenant gets a text from a number they have never seen.Rule R7: reply box routes through the PMS door or refuses.1
21Clara's seat not configured for the customer.Her own PMS sends look like a teammate; "teammate answered" stops her.Turn-on check: seat present, echo test passes.1, 3
22Prompt bloat from two years of history.Slow, costly, worse replies.Window plus summary (section 6).1
23Touch budgets and the team-inbox quiet rule count mirrored rows.Clara goes quiet for the wrong reason.Only Clara's sends count as touches; mirrored rows are context.1
24The old Willows feed and the new watch write the same rows.Double bubbles at the test property.Turn the old job off when the new one lands; same id key.1
25A customer that never uses PMS texting (Situs: "no messages found").Polling for nothing.The schedule already idles at fifteen minutes in empty hours (decision 12); show "no inbox traffic for N days" on the customer page, which is itself a finding about that customer (section 1: Situs texts tenants from personal cells).1
26Owner and vendor parties in the inbox.Dropped silently.Pending with reason; counted on the customer page.1
27Robot session shared with the write jobs (work orders at Western Slope).The watch evicts the session mid-write.One session per customer, a lock, the watch yields to writes.1
28PII and retention.Tenant texts in a replay fixture or a PR.Raw stays in prod and our machines; synthetic names in evals; a build check for real numbers.0
29Two doors, two replies.Clara answers twice.Rule R6: thread lock, one reply per burst.2
30Staff and Clara answer at the same moment.Two voices.Just-in-time thread read before every send; "staff engaged" window.2
31Opt-out on one door, text on the other.A complaint, possibly a TCPA claim.Rule R5; consent basis written before any cross-door send.2, 3
32Staff promises in context.Clara repeats or contradicts "I'll send it tomorrow".Prompt rule plus eval; promises stay untracked.2
33No replay corpus for tenant conversations.We ship on leasing-shaped tests.Build the tenant replay set from the Western Slope read, synthetic names.1, 2
34PMS send fails, Clara falls back to her number.New-number surprise.Queue, retry, tell the office; never fall back silently.3
35Yardi assumes AppFolio's session scheme.The "generic" core is AppFolio in disguise.The adapter contract owns sessions; a build check that core never imports an adapter.4
36Polling at 3 AM at full cadence.Noise in AppFolio's logs, cost, and Gera's "why every two seconds".The schedule polls every fifteen minutes while people sleep (decision 12, figure 8); an empty tick is one request and no writes.1
37A retry after a timeout sends twice through the PMS (no message id back, per-runner memory).The tenant gets the same text twice.Durable claim before send; echo match.3
38STOP typed in the AppFolio thread revokes nothing in PropFlow.Clara texts someone who opted out.STOP mirrored as an opt-out event at the front door; rule R5.1 record, 2 enforce
39Household members and past tenants are unknown to PropFlow (172 of 517 current, 573 of 598 past at Western Slope).Most backfilled threads have nobody to land on.Build the person from AppFolio's directory record.1
40Unit labels differ between systems ("Unit 8" vs "8").Wrong unit, or no match.Join on unit id.1
41Ties at a page break skip a row in the list walk.A thread missing from the backfill.Overlapping pages, dedupe by id.0, 1
42A company with several texting lines; the parser drops the line id.Two threads collapse into one.Keep the line id in the row; thread key = number plus line.1
43The watch triggers a login (90 seconds, SMS code; about five failures lock the account, observed Sept 7 on Western Slope, not AppFolio documentation (HANDOFF-westernslope-appfolio-access-2026-09-07.md)).The customer's AppFolio account locks.The watch never logs in; only the robot's own refresh does, once per lapse.1
44Two session probes per robot call double the request count.Budget spent on probes.One probe, none within a minute of a success.1
45The Twilio and email paths keep their own identity code beside the front door.Three answers to "who is this" that drift apart.A build check: every inbound path emits the front-door event.1, 2
46Willows shares a database with a live customer (JP&Co).Camellia tenant texts in a test fixture.Willows polls fingerprint only; no previews or numbers stored.0
47Today's mirror wakes Clara on the per-property sync switch alone (not on PMS delivery). A full Clara turn runs before any hold check."Thanks!" forwarded to the technician; "5 thanks" read as a rating; a duplicate work order; a renewal "yes" meant for the PM reaches the commit tools; the reply leaves from Clara's Twilio number.No wake for mirrored rows in phase 1 (a rule, no switch); the engage decision owns waking from phase 2.1
48The forever-hold is also too wide: it silences technician relays, rating asks, "work order created" notices and PM-approved rent reminders; a blocked relay raises a false "resident never reached" alert; a blast stamps N holds.Maintenance updates stop reaching the tenant and the PM is told the tenant was unreachable.Phase 1 passes no hold; phase 2 deletes it; transactional kinds exempt in the meantime.1, 2
49Role-only readers take staff text for Clara's: the prospect summary behind the voice agent's "previous interactions" and the returning-prospect block, the first-email prompt, the promise ledger, savings, the caller log, the "Clara responded" badge.Clara repeats Jay's screening talk as her own; a staff promise becomes Clara's promise; reports say Clara handled threads she never touched.One provenance predicate in every reader, shipped first as a fix with no switch (section 6).1
50Stored rows alone move other engines: collections stops the dun, blocks re-enrollment and pages the PM on any tenant row; the weekend tour check-in cancels; the 12-hour cooldown counts a staff row as Clara's touch across properties.A rent reminder silently skipped; a tour nudge cancelled by an unrelated "ok"; N PM actions after one blast.Those readers count only Clara's-line traffic or filter by writer and door; bulk rows carry their own kind.1
51Misfiling rules: a closed inquiry beats an active tenancy when placing a row; a contact match beats the PMS party id; one claim winner per phone.A maintenance text lands in last year's leasing thread and Clara answers as leasing.Active occupancy outranks a closed inquiry for tenant rows; party id beats contact match; a number that maps to several people stays pending.1
52A staff answer in AppFolio never closes an escalation matter or unmutes a thread.Reminders keep nagging about a question Kat already answered; the next forward re-quotes it.A staff outbound after an open matter counts as "staff answered" (rule).2
53The life-safety scan on escalated threads reads every row with no time bound.A backfilled "smell of gas" from 2024 forces a full hold today.History rows are excluded from the scan.1
54AppFolio automatic notices are decided "not ours" and count as staff replies. Automatic notices arrive as messages from a user named "System" (108 captured rows); only the 5 auto-replies have no sender (ws-shadow/raw).A system notice stops follow-ups or stamps holds on every recipient.A "System" sender or no sender means automatic; context only.0, 1
55Failed staff sends (5 of 21 rows at Willows on Oct 12) counted as answered.Clara thinks a tenant was answered when they never got the text.Keep the delivery status; failed never counts as answered.1
56Photos dropped: rows carry media, the parsers keep text only (parser lines 61-115) and drop a row with no text (218-227), so a photo-only text vanishes and a captioned photo keeps the caption.A photo of the leak shows as an empty text or not at all; the office opens AppFolio to see it.Media on the message types; keep text-less rows; download into the same upload step Twilio photos use (S3 key, file id, attachment record), so the thread, Clara's prompt and the work order handle it with no further change; a photo marker in the inbox list. Audit Oct 12: Twilio photos work end to end; email photos are saved but never linked; outbound texts carry no photo (a technician's dispatch text is text-only; the renewal letter is the only MMS).1
57Mirror threads look like Clara threads (source Clara, active, the person's phone), so the router's active-conversation lookup and the work-order thread picker can choose a thread the mirror just bumped.Clara's technician relay goes to the wrong thread.Origin stamp on mirror threads; the matter's own thread wins in the picker.1
58A thread minted for a backfilled row starts at the import moment.It becomes the person's "newest" thread, hides Clara's recent touch elsewhere, and counts as since-launch in savings.Start and last-message times come from the rows.1
59Replay with no frozen set: live reads keep changing, so "byte-identical" cannot be judged.False confidence in the proof.A frozen snapshot of the Western Slope raw store is the replay set, with a planted wrong-placement control.1
60Onboarding backfill in the same hour as a new customer's first sync (Situs had a 429 storm within minutes of connecting: incident of 2026-09-30, AppFolio sync arms; see the feature-flag sprawl audit).Lockout or a dead sync on day one.Backfill starts after the first sync settles, at night, paced.1
61The customer forwards their main line to Clara and AppFolio forwards calls to its texting number onto that main line (not verified).A call to the texting number may land on Clara and loop.One real call per customer after forwarding is on (the Sep 22 rule).1
62Echo of Clara's own PMS sends: AppFolio returns no message id and appends a signature plus "Reply STOP to unsubscribe" (unverified: from a code comment).Clara's text shown twice, or as staff, so she silences herself.Match by Clara's login first, the receipt key second, stripped body within five minutes last.3
63A bulk blast or an AppFolio automatic notice is read as "staff answered" and the follow-up gate stands down, or the forever-hold is stamped, for a person nobody actually spoke to26 of the newest 100 Western Slope threads end with a blast and 28 with a notice; only 16 end with a staff-written messagethe mirror types every message (person, staff, bulk, system, Clara); only a staff-written message counts as an answer
64The label says "Assistant PM" for a message Kat wrote, because the office shares one login34 of 100 last messages come from the shared login; staff sign with "-Kat" or "-Jay"attribution reads the signature when present and keeps the login as the fallback; the label is "Kat (shared login)"
65A thread is attributed to the wrong company because the automatic notice is signed with the property owner's entity, or because two brands share one database17 automatic notices in the Western Slope database carry an owner entity's nameattribute by the property on the row, never by a signature or by the database alone
66The mirror shows a photo-less thread for a text that was only a photo, or a staff "sent you a picture" with nothing attachedthe AppFolio feed drops media and text-less rows today (parser lines 61-115 and 218-227); the inbox probe cannot see media on the listphase 1 carries media (keys, types, the AppFolio attachment id) and stops dropping text-less rows; the inbox shows a photo marker
67A third voice: AppFolio's own resident agent (Resident Messenger, generally available since June 2026 (unverified; the captures carry empty is_agent and current_agent_run_id fields), in the resident app; its reach into the texting inbox is not documented) answers before Clara.Two assistants in one thread; Clara argues with or duplicates its answer.AppFolio automation is its own sender bucket (the probe counts 28 "System" senders already); its answer counts as the office's for the follow-up gate; Clara never replies to it.1
68A bulk or notice row stamped with a non-human author kind.The "is this Clara's own send" test is "author is not human" (message-provenance.ts), so the row counts as Clara's: the runaway ceiling, agent metrics and the outreach budgets all move.Bulk and notice rows carry their own kinds and never read as Clara's or as "staff answered"; the harness's engines scenarios pin it.1
69"A new message role nobody reads" as the way to make mirrored rows inert.The role is a closed four-value type (tenant, assistant, tool_use, tool_result); a fifth value is a model change every reader must learn.Mirrored rows keep the existing roles; the door, the kind and the history mark carry the difference.1

The ten most likely to bite first (Opus round 1, agreed): today's mirror answers office texts from Clara's Twilio number (47, 2, 20); staff names missing, "Office" instead of "Kat" (11); tenants never match and stay pending, Clara still blind (39); a "rent is due" blast counts as a staff answer and stops follow-ups everywhere (10); the backfill moves recency and wakes old threads (17, 58); inbox paging silently skips a thread (41); anything that opens a thread page clears the office's unread badges (1); the per-property job causes a 429 storm or a lockout at connect (2, 4, 43, 60); a former prospect, now a tenant, lands in a leasing thread (13, 51); a STOP sent to the office line never reaches Clara (38).

11. Build order, minimum slice, proof

Started 10-12, 8 pm, on Fede's go. His words: "we have enough to start building… I do want to start validating our assumptions on the architecture… a local harness with real data, or the Willows… reading the messages and ingesting them, putting them in the proper conversation, we're going to have to build it no matter what, as well as the ability to send messages through the PMS, which we already built before; we need to battle test it… starting with an extremely comprehensive test harness that can cover end to end the new architecture is probably the first thing"; "the message comes in, how do we map the identity, put it in the right thread, if there's no thread we create a new conversation in PropFlow: all those things are the core engine"; "maybe there's one feature flag for all this"; "the first deliverable is get the information into PropFlow and see it on the UI, and a backfill of that, safely, without sending anything out"; STOP and START "for later"; photos "not core here, later, the same upload step as today, just a different source". What starts tonight, each dark behind one company setting ("mirror the PMS inbox", default off) and each a chain of small PRs: (1) the harness, end to end, local, on real-shaped rows (Willows captures with made-up names), with a fake robot, the scenarios in the table below and the send-path battle test; (2) the core engine it exercises: adapter contract, watch, store, identity and placement, the conversation created when none exists, labels in the thread view and the prompt, the history mark; (3) the existing send path under the harness and live at Willows. Later, by his ruling: STOP and START, photos, the engage decision, Clara answering through the PMS. Open decisions 1, 4 and 8 block none of it: the harness tests both tick owners and both history placements.

PhaseMinimum sliceSwitchProof before the next step
0 · Design, canaries, the readThis page; the Willows capture (done); robot-only canaries: a test text to Willows' line then the list and thread JSON with unread unchanged, the same thread looked at from a second login (is "unread" one flag per company or per login), one tenant thread saved raw (sender id, direction code, media, line id, every party), one inbound email row, whether the list request keeps the session alive, the count of texting lines on the JP&Co database, and first of all the keepalive test, which decides whether the robot or the sync runner owns the tick (decision 1); the Western Slope probe and two-year read with its numbers.none, no PRRequest logs and headless screenshots; numbers on this page; Fede's decisions below.
1 · Mirror; Clara reads (Fede's first deliverable: "get the information into PropFlow and see it on the UI, and a backfill of that, safely, without sending anything out")Slice 1, started 10-12: the core engine: the watch on the door's schedule, the store, identity and placement by the ladder (figure 4), the conversation created when none exists, labels by sender and door in the thread view and the prompt ("Kat · AppFolio text, Oct 9 4:12 PM"), for people already in PropFlow (84% of current tenants with a phone match a PropFlow person) and for unknown numbers as a plain conversation on the company's front-door row; no wake and no hold from a mirrored row (Fede: Clara is not woken "just to start on messages that come from outside"); the organization wall first (one watch per AppFolio database, one setting per company; a row is placed only through a property or person of a company whose setting is on; a row for a company whose setting is off is dropped); one pacer and a daily ceiling in the robot; store first, then place; the store never overwrites a row already filed (the Willows 09-13 incident: 51 rows re-filed, 17 stale wakes, one tour booked off a ten-day-old text). The safe backfill: 24 months read into the organization's raw store; history rows carry a mark and are inert: never a wake, a hold, an alert, a recency bump or a touch (without the mark, the day the backfill lands Clara would read a year of old texts as live traffic, go quiet on some people and restart follow-ups on others); where they land first is decision 4. Fixes that ship with it, no switch: Clara's own AppFolio sends recognized by seat id (her application links already flow at Western Slope); the six role-only readers that would read a staff text as Clara's (section 13: the outreach cooldown, the prior-contact flag, the first-email prior-thread check, the prospect summary, the promise transcript, the intake classifier) fixed; bulk and notice rows excluded from "staff answered" and never counted as Clara's; the work-order photo sweep never takes a photo whose source is AppFolio. Adapter: the robot keeps the sender's user id and name, the raw direction code (bulk stays bulk), a no-sender marker for AppFolio's notices, the texting line id, every party and the media on every row; list and thread routes with a values-only mode and a start cursor (the robot repo; handed to Smith's side on the bridge). Later, by Fede's ruling: STOP and START on mirrored rows; photos (the same upload step as a Twilio photo, a different source); person creation from the directory; the "waiting on the office line" view and the Coworker Line reading the mirrored thread (both store-only reads, slice 2).one company setting "mirror the PMS inbox" (Fede: "maybe one feature flag for all this"), default off, visible on the company page, Fede turns it on per customer; the fixes carry no switchThe harness end to end (table below), each scenario with a planted-defect control; byte-identical replay of every customer's book for the fixes; the exit proof for the new reader: 300 real Western Slope conversations where staff texted on the AppFolio line, replayed before and after from the raw store, zero contradictions, zero duplicate tour offers, the follow-up stood down on every staff-answered thread, judged on the subscription; Willows live on the real line; dark at Western Slope; the supervised week.
1b · AppFolio emailCapture the email row shape first, then decide; nothing promised (tenants already email the leasing mailbox, which is a door today).same settingSame ladder.
2 · Decide, recorded (no send)Delete the forever-hold (a fix; it binds nowhere today once phase 1 passes no hold); lane classifier for tenant traffic; compute "would Clara reply, and why" for every mirrored inbound and store it, send nothing; escalation with reason; the team-inbox quiet rule extended to AppFolio-line texts.one company setting "Clara answers through the PMS", default offReplay over the backfill; a judged sample against what staff actually did; double replies, contradictions, wrong holds and leaks counted.
3 · Clara answers the routine share through the PMSScope decided after the year-of-data numbers (decision 8); until then this row is the prerequisite list. A robot-only Clara login per customer; a durable send-once claim; a per-organization daily cap and a send window; never bulk; a tenant send route (none exists; the work-order send throws "not implemented"); echo by login id; a fresh thread read right before each send; failure rule (never fall back to Twilio); every PMS-door draft held, 7-day hold life, for the first week; renewals and maintenance move under the setting; the application-link switch folded into it.same setting as phase 2Echo test at Willows; one renewal and one maintenance update end to end at Fairhaven; a supervised week at the first customer with every message held and reviewed.
4 · Widen "routine"Lanes move from "hands over" to "answers" as the measured mix and the supervised weeks justify it; the same rules at every customer, never a per-customer level.same settingA clean supervised week per lane; the monthly re-measure on this page.
5 · Second PMSA Yardi (or Entrata) adapter: session, list, thread, ids; capture first.none newBuilt without a change to the core; the adapter build check (the core never imports a vendor) holds.

Rules that apply: Fede's go of 10-12, 8 pm covers the harness, the core engine and the send-path battle test; everything else waits for its decision on this page; each chain opens as one builder, each PR one concern under 300 lines, dark and mergeable alone; the fixes above ship as the only code path, live everywhere; prototypes (the read, the classifier) run locally on the subscription and never enter CI. Ladder: unit tests with a planted defect, read-only replay byte-identical, Willows live, Fairhaven sandbox in the customer's shape, supervised week (testing playbook).

The harness, end to end (Fede 10-12: "an extremely comprehensive test harness that can cover end to end the new architecture is probably the first thing")

Local, on the subscription, never in CI until the core lands as code: real-shaped rows (the Willows list and thread captures with made-up names; never a customer's rows), a fake robot that serves recorded pages and records every request, the real store and readers underneath, and a planted-defect control per scenario (a check with no control does not count). The live rung is Willows: the watch on the real line and one real application-link send.

GroupScenariosWhat must hold
Adapterevery field on a list row and a thread row; a row from the "System" sender (an AppFolio notice; 108 captured rows) or with no sender (5 auto-replies); a bulk row; a photo-only row; a text-less row; an email row once capturednothing dropped; bulk stays bulk; a "System" or no-sender row is marked automatic; media keys carried
Watcha quiet tick; a new message; a bulk blast of 200; a tie at a page break; a 429; a cold session; the schedule bands and the 30-minute burst; the keepalive questionone request on a quiet tick; every row seen once; the mark advances by time; a 429 stretches and never loops; a cold session pauses and never logs in
Store and walltwo companies on one database; a row with no property; a re-read of the same page; a row already filedonly the company whose setting is on keeps its rows; the holding area counts; nothing is written twice; a filed row is never overwritten
Identity and placementthe ladder in order; a phone shared by two people; two active tenancies; an unknown number; a past tenant; a conversation that does not exist yetone conversation per person at the property; the new conversation created with the row's time, never the import time; the unknown number on the front-door row, never a leasing record
Renderingthe thread view and the prompt on a mirrored thread; a bulk send; a photo; a shared-login send signed "-Kat""Kat · AppFolio text, Oct 9 4:12 PM" in both; bulk once with a count; a photo or "[photo]"; the signature's name over the login
Enginesa staff text, a tenant text, a notice and a blast arrive on a mirrored thread; a history row landsno wake, no hold, no alert; the follow-up gate stands down on the staff text only; the cooldown ignores bulk and notices; the six fixed readers see staff, not Clara; a history row moves nothing
Send (the existing path)an application link end to end against the fake robot; a robot timeout then a retry; two asks in a minute; a cold session; a cap reachedexactly one send per ask; a parked row with a reason, never silence; the message keyed by AppFolio's id; the known double-send gap found or disproved
Live at Willowsthe watch on the real line for a day; one real application-link sendrequest logs match the schedule; unread flags unchanged; the thread view shows the labels

12. Decisions for Fede

TL;DR: Fede's 8 pm go starts the harness and the core engine tonight under one setting. Decided tonight: decision 12 (the tick schedule, bands from figure 8, thirty minutes after midnight), decision 7 (one year, Western Slope and Situs). Decision 1 runs on the recommendation (our sync job owns the clock). Decision 4 (Fede, 10 pm): backfill is a per-company number, default 7 days to prove the mechanism (Fede: 'it could even be a week') (a year is too much; one to three months decided later from mirrored data); old rows land in conversations on day one; the history mark stays internal, never shown; the hard rule for the whole exercise is that a mirrored row can never cause a reply or any outbound, proven in the harness. Fede wants it on at a customer by Tuesday once battle-tested. Decision 8 (phase 3 scope) waits for the year of data. Everything else is recommended as written and can change later. Recommendation first on each. The third pass (a fresh Fable 5.1, unsteered) changed decisions 1, 3, 4, 5 and 8; section 13 shows where it disagreed and what was kept.

1. The shape. (a) Recommended: generic core plus adapters; one shared "a message arrived" event that the watch raises and that identity, placement and the engines consume; the adapter build check stays (the core never imports a vendor). Not in this initiative: moving Twilio, the mailbox and voice onto that front door behind a call-site build check (v1.4 recommended it; the third pass is right that it came in as a round-2 suggestion, that the voice route has no seam, and that it is platform work with no customer effect; it stays on the vision page as its own initiative). Where the tick lives is decided by the phase 0 keepalive canary, not by this page: if a list read resets AppFolio's idle timer, the tick rides the robot's keepalive and the robot raises the event (v1.4's option c, rejected for a reason that does not hold: change detection is not business logic); if not, the sync runner hosts the watch and asks the robot. (b) A separate PMS-comms service now. (c) Move every live path onto the front door in this initiative (v1.4's recommendation).

2. How we read. (a) Recommended: plain requests on the session the robot already keeps warm; one list read per customer per tick, threads on change; the sync runner asks the robot and never replays cookies itself; a browser only for login renewal. (b) A new browser per poll. (c) One browser always on.

3. Identity, and one phone shared by several people. (a) Recommended: AppFolio ids first, then phone only when the name agrees, then the unit's leases, then create the person from AppFolio's directory only when they text after turn-on (never hundreds of past tenants and household members from history; third pass, and Fede: person creation is later); a message lands once, ranked active tenancy, then applicant, then prospect, then past tenant; two active tenancies at different properties stay pending and are counted; nothing found means a plain conversation on the number, on the placeholder when the row carries no property (Fede 10-08: a conversation may sit on the placeholder, a leasing record never), retried every sync, never an inquiry. Before any of it, the organization wall: JP&Co's database also holds the Willows test building, so one database gets one watch and one setting per company; a row is placed only through a property or person of a company whose setting is on, a row that belongs to a company whose setting is off is dropped, and a row with no property or person lands on the company's front-door row and is counted, never guessed. (b) Copy a shared-phone text into every matching conversation. (c) Phone and email only; no creation; unmatched rows wait for a tenant module.

4. Where history lands first. 24 months are read either way (Fede: "at least a year", and tonight "a backfill, safely, without sending anything out") and stay in the organization's raw store. (a) Recommended: into conversations on day one as history rows, inert by rule (the history mark honored by every no-window reader and the work-order photo sweep, section 6), so the office sees the whole history in PropFlow from the first day; Clara's prompt gets the last 90 days plus the last 20 messages. (b) The third pass's shape: live texts forward only in slice 1, history moved into conversations in slice 2 once the readers honor the mark; inert by construction, a smaller first slice, the history visible a week or two later. Fede's trade: day-one history against a smaller first slice; the harness tests both. (c) 12 months. (d) 90 days.

5. Phase 1 and the engines. (a) Recommended: the follow-up gate sees staff answers from day one (the double-text fix), proven by the before-and-after replay. v1.4's "the provenance fix in every role-only reader" was over-scoped, as the third pass said, but not as far as it said: checked against the code tonight (section 13), the two readers that gate the double text already tell staff from Clara, four more are right or harmlessly blind, and six read a mirrored staff text as Clara's and are fixed in slice 1 (the outreach cooldown, the prior-contact flag, the first-email prior-thread check, the prospect summary, the promise transcript, the intake classifier). Direction-only readers keep counting mirrored texts as real traffic. (b) Mirror only; every engine blind to mirrored rows until phase 2.

6. Phase 1 channels. (a) Recommended: texts first; capture the AppFolio email row shape, then decide, nothing promised (tenants already email the leasing mailbox, a door today). (b) Wait and ship both together, per "all channels always".

7. The Western Slope read. (a) Recommended: one read, two uses: it is the backfill and the analysis; tonight's 100-thread probe sizes it; the full read runs as soon as the tenant-thread read and the overlapping walk are validated at Willows, paced, resumable, overnight, numbers on this page, and those numbers land before phases 2 to 4 are decided. (b) A separate throwaway analysis read first. (c) Wait for the design review.

8. Phase 3 scope. (a) Recommended: decide it after the year-of-data numbers (figure 7, section 9); until then phase 3 is a prerequisite list, not a scope (a robot-only Clara login per customer, a tenant send route, send-once on retry, a cap, a send window, the 7-day hold), and the direction stays: replies inside existing PMS threads first, per customer, silence during a login outage. (b) Everything through the PMS from day one. (c) First touches stay on Clara's number for good; replies go through the PMS.

9. A STOP typed to the office's AppFolio line. (a) Recommended: it also silences Clara's own line for that person (the law ties revocation to the person's number; honoring it is the safe choice); written as an opt-out with its own source, live and in the backfill, STOP and START replayed in time order with the latest winning, an unplaced STOP applied to every organization on that database, enforced from phase 1. (b) Per line: a STOP on the office line stops only PMS-carried sends.

10. AppFolio-line texts before Clara can send through AppFolio. (a) Recommended: never answered from Clara's Twilio number, even a prospect at 11 PM (the wrong-number trap), and the same rule for operator scripts, hold approvals and recovery runs, which today send from her number. (b) Prospects answered from Twilio after hours, tenants never.

11. Renewals this month. (a) Recommended: renewals and reminders keep going from Clara's number until phase 3 exists, as today (supervised by 10/14, live around 10/21); the mirror only adds the office's AppFolio texts to her context, so she stops contradicting them. (b) Pause renewals at Western Slope until Clara can send through AppFolio.

12. The tick schedule. (a) Recommended, and Fede's words at 8 pm ("a dynamic polling mechanism… I need data"): the tick follows the door's own clock. Each customer's door gets an hour-of-day curve for weekdays and one for weekends from its last 90 days of mirrored inbound, bootstrapped from the year read: an hour at a tenth or more of the busiest hour's traffic polls every minute, under a tenth every five minutes, under a fiftieth every fifteen; any change seen on a tick, and any text Clara sends through that door, pull the next 30 minutes back to one minute; floor one minute and ceiling fifteen, both fixed; recomputed monthly, shown on the customer page. At Western Slope today: one minute 8 am to 7 pm, five minutes 6 to 8 am and 7 to 10 pm, fifteen 10 pm to midnight, thirty midnight to 6 am (figure 8; Fede 10-12 night), about 740 ticks a day. (b) One minute always (v1.4). (c) A fixed office-hours split with no measurement.

13. Nothing falls through. (a) Decided Oct 15 (Gera): the rule in 7a, written by one shared outcome writer on the engine's existing outcome list; notes at once, day or night; unsupported buildings come from the PMS pull, never typed by hand; the system is named Reception. (b) Keep per-path backstops. (c) A morning digest for non-emergency hand-offs. Nothing on Fede's plate: the engine is already on at Western Slope with a team inbox on file; the Team notices kind shipped (#19603).

13. Tensions and disagreements

Opus 5.5 round 1 (an independent proposal and trap list, 7,100 words, read-only against origin/main and the Willows capture) and the engines map (20 engines read against a mirrored row) landed tonight. Where they disagree with this page or with each other, and the verdict:

PointThis page, v1Opus round 1Engines mapVerdict in v1.1
Change signalThe customer-level list, keyed on the row idSame; the brief's Option B (a portfolio report call, then card reads) watches the wrong thing: guest-card reports never see tenantsn/aAgreed. The inbox list is the only change signal; Option B is dropped.
Who talks to AppFolio"Cookie replay" from the sync runnerAll AppFolio traffic inside the robot: one jar, one pacer, one lockn/aOpus is right; section 3 and decision 2 rewritten.
Is phase 1 inert?"Clara reads, never replies"; a hidden fork on leasing follow-upsNot zero change: staff texts stand follow-ups down; keep it, prove itNine engines react to stored rows alone, most by role, plus the wake and the hold if left wiredBoth. Keep the one wanted effect; the provenance fix in every role-only reader and the no-wake, no-hold rule ship first, as fixes (section 6, phase 1 row).
Backfill depth12 months, inert24 months stored, 90 days in the promptNo-window readers see history unless told24 months stored (Fede: at least a year), 90 days plus the last 20 in the prompt, history mark honored by every no-window reader (decision 4).
AttributionAssumed from the rowThe robot drops the sender on thread reads, keeps the first party only, staff texts render "From mailbox"SamePhase 1 work, named in the adapter slice.
Read just before each follow-up (brief's Option D)Part of the watchNot needed with a cheap list poll; a phase 3 guard right before Clara sends through AppFolion/aAgreed; moved to phase 3.
The forever-holdReplace first, before phase 1Phase 1 passes no hold, so delete it in phase 2Permanent, too wide, exempts nothing transactionalPhase 1 stamps no hold (rule); the deletion is phase 2's first fix.
STOP across doorsRecord in phase 1, enforce in phase 2Phase 1 rule, no switch; owner call on cross-line revocationLegal; the keyword check has one caller, the Twilio webhookPhase 1, no switch; cross-line revocation is decision 9.
Gera's "find them where they are and continue there as Clara"Phase 3Phase 3No tenant send path exists; shared loginPhase 3, after the robot-only login and the tenant send route.
Prompt window30 days or 40 messages plus a summary90 days plus the last 20n/a90 days plus the last 20 plus the summary; a default, not a decision.

Round 2 (Opus 5.5 reads v1.1 of this page adversarially against main; 7 blocking, 15 important, 17 contradictions; scratchpad pms/opus-round2.md). The seven blockers and what changed in v1.4:

BlockerWhat was wrong on the pageVerdict in v1.4
1. No organization wallOne AppFolio database can feed two of our companies (JP&Co's holds the Willows test building); the store is filed by building and 42 of 100 Western Slope rows have none, so the watch had nowhere to put them without guessing.Accepted. Organization wall and a database-level holding area are the first phase 1 item; decision 3 rewritten; the Willows canary counts texting lines first.
2. Sender labels break on Clara's own loginThe adapter carried names; the parser needs the sender's user id for texts, so Clara's own AppFolio texts would file as a teammate reply.Accepted. The adapter carries the user id and direction code; seat-id recognition is phase 1.
3. "Dark until phase 3" is falseClara already sends application links through AppFolio at Western Slope (3 in 10 days).Accepted. Section 8, R2 and the phase 1 row say so.
4. Opt-out per door would stop renewalsR5 said per door; the code and the law key opt-out by number; applied as written it would stop this month's Western Slope renewals from Clara's number.Accepted. R5 rewritten (per number, STOP and START in order), R3 marked phase 3, decision 11 added.
5. The cooldown remedy re-opened the double textSection 6 said the 12-hour cooldown should count only Clara's-line traffic; a mirrored staff text standing Clara down is exactly the fix.Accepted. The cooldown keeps counting real staff texts; only system and bulk rows are left out.
6. Backfill is not inert once photos existA new work order uploads every old tenant photo in the conversation to AppFolio.Accepted. The photo sweep honors the history mark and never uploads a photo whose source is AppFolio.
7. The supervised hold was described wrongHolds expire after 24 hours, general replies are not held, there is no digest.Accepted. Section 7 states the code's behavior; the 7-day PMS-door hold is named as phase 3 work.

Also taken from round 2: decision 1 (the front door as a shared module with a build check), decision 10 widened to operator paths, the Fede 10-08 placeholder ruling quoted as given, the robot already paces, the heartbeat relogs a lapsed session, the newest 100 threads are half applicants and a third unknown numbers rather than "90% tenants", the Coworker Line labels every assistant row "Clara" and renewal threads are excluded from person matching (two more readers for the provenance fix), the backfill's real request count (3,500 to 7,000 plus photos, with session checks), and the switchover order for the per-property loop. Not taken: nothing in the blockers; the 15 important gaps are tracked in the file. Contradictions 1 to 17 are resolved in the sections above or listed in NOT VERIFIED.

Third pass: a fresh Fable 5.1 at max effort, unsteered (Fede 10-12)

It read v1.4 cold, then checked it against both Opus rounds, the research files and the code, read-only (scratchpad pms/third-pass.md, 1,400 words). Its opinion, in its words: "Build the mirror. The read is nearly free and 'Clara sees what the office said' ends the two-voices problem on leasing threads today. The real risk is scope, not technology: the page has grown from 'mirror the inbox' into 'rebuild every door into PropFlow', and phase 1 carries about ten prerequisites before the first visible result. What Fede asked for is a one-to-two-week slice." It agreed with six decisions, changed five, and found four things nobody had raised.

DecisionThird passv1.5
1 ShapeKeep core plus adapters and one event; drop the build check and the migration of Twilio, mailbox and voice (a round-2 suggestion that became a commitment; the voice route has no seam). The keepalive test decides where the tick lives.Taken. The migration leaves this initiative (it stays on the vision page as its own); the adapter build check stays; the keepalive canary is phase 0's first test.
2 ReadAgree; run the keepalive test before any phase 1 code.Kept.
3 IdentityOrder and wall right. Create a person when they text again, never hundreds from history; a row for a company whose setting is off is dropped; one wording for the placeholder rule.Taken. Trap 14, decision 3 and 9.5 now say the same thing.
4 Backfill24 months into the raw store first, not into conversations at turn-on; history lands later once the readers honor the mark.Presented as option (b). It changes the first deliverable Fede asked for (history in PropFlow from day one), so it is his trade, not the page's; the harness tests both.
5 Engines"Provenance fix in every role-only reader" is over-scoped: the two readers that matter already tell staff from Clara; direction-only readers are right to count real traffic. Two code traps: a non-human author kind reads as Clara's send; the message role is a closed four-value type.Taken in part, after the code check below: six readers, not every reader and not none. Traps 68 and 69 added.
6 ChannelsTexts first; capture the email row, then decide; promise nothing.Taken; 1b is no longer promised.
7 WS readAgree; its numbers land before phases 2 to 4 are decided.Taken.
8 Phase 3Decide the scope after the year of data; until then a prerequisite list.Taken.
9, 10, 11Agree.Kept.

Four things nobody had raised, all taken:

  • Where a customer's tenant texts live decides what the mirror is worth there. Western Slope's office line is where tenants are; Situs runs about half of its tenant texting on two staff members' personal cells and our text-only read returned "no messages found"; email cards were not read. Western Slope first; Situs is not the second customer. Now a row in section 1.
  • A third voice. AppFolio's own resident agent (Resident Messenger, generally available since June 2026 (unverified), in the resident app; its reach into the texting inbox is not documented) could answer before Clara. AppFolio automation is its own sender bucket and its answer counts as the office's: trap 67.
  • Phase 1 had no exit proof. Now named: 300 real Western Slope conversations where staff texted, replayed before and after, zero contradictions, zero duplicate tour offers, the follow-up stood down.
  • Phase 1's value for the office is "who is waiting", not "one history in PropFlow". The office lives in AppFolio. The "waiting on the office line" view and the Coworker Line reading the mirrored thread are store-only reads: slice 2, and the evening wait in 9.6 is the number they show.

Code check on decision 5 (tonight, origin/main, read-only). The two readers that gate the double text already tell staff from Clara: the prompt frames a human-authored row with an external name as a teammate's turn, and the follow-up gate counts any human-authored outbound as "staff answered". Four more are right or harmlessly blind (the hold writer, the runaway ceiling, the commitment extractor, the agent metrics; the collections chase and the tenant count look only at tenant rows). Six readers key on the message role alone and would read a mirrored staff text as Clara's: the 12-hour outreach cooldown, the prior-contact flag, the first-email prior-thread check, the prospect summary, the promise transcript and the intake classifier; one more ("handled by") is unclear. So the third pass is right that "every reader" was over-scoped, and the engines map is right that the fix is real: six named readers, pinned by the harness's engines scenarios. One more trap from the check: the "is this Clara's own send" test is "author is not human", so a bulk or notice row must never carry a non-human author kind unless it is meant to count as hers (trap 68).

Where this page still differs from the third pass. (1) Decision 4: the page keeps day-one history as the recommendation because it is what Fede asked for; the third pass's smaller slice is option (b) with its reason. (2) "Decide phases 0 and 1 tonight, 2 to 5 later": taken, with one nuance: phase 2 (compute "would Clara reply", send nothing) needs no decision beyond the mirror and can follow slice 1 without waiting on the numbers; only its thresholds wait. (3) Section 7: the third pass wanted one paragraph until the data lands; the page keeps the two-state table because Fede ruled the levels out and the table is the rule, and figure 7 replaces the theory the moment it lands. Fact corrections it made to itself and to this page: the 220 fridge magnets carry the after-hours emergency number, not the texting line; "about 90% tenants" was a gut number; the cadence was stated three ways; "text never leaves the store" was false.

7a vs decision 1(a): 1(a) keeps voice, Twilio and the mailbox off this initiative's front door, while 2b and 7a move paths "behind the door one PR at a time". Resolved Oct 15: the outcome writer is shared code every path calls from where it stands; it is not a door move, so 1(a) stands and 7a lands everywhere.

14. Reference (for agents)

Sources, reports, verified versus assumed
  • Fede, 2026-10-12 (dictated): the first deliverable line; "it doesn't matter what number they use"; "analyze this inbox for Western Slope, at least a year or two"; "a generic core platform layer, plug and play for different PMSs"; "one gate where all messages come in"; "the browser agent is just another API"; "pair with an Opus 5.5 in max thinking mode"; renewals and maintenance move under the same PMS-send setting.
  • Fede, 2026-10-12, 8 pm (dictated): the go on the harness and the core engine; one setting for all of it; the first deliverable restated (into PropFlow, on the UI, a safe backfill, nothing sent); STOP and START and photos later; the dynamic tick from a measured distribution.
  • Third pass (2026-10-12, a fresh Fable 5.1 at max effort, unsteered, read-only): scratchpad pms/third-pass.md. Its sources for the three numbers it added: the identity table in section 4 (337 of 401 current tenants with a phone match a PropFlow person, 84%); scratchpad inbox-identity-check.md (24 of 354 people on an active lease have an existing Clara conversation, all voice plus one email, zero texts); competitors-pms-native.md lines 22 and 126 (AppFolio's Resident Messenger agent generally available 2026-06-16, in the resident app).
  • Code check on decision 5 (2026-10-12, origin/main, read-only): 14 readers from the engines map, 6 correct, 3 harmlessly blind, 6 role-only and wrong (autonomous-outreach-cooldown.ts:39, prospect-prior-contact.ts:35, first-email-prior-thread.ts:184 and 201, prospect-summary.ts:594, daily-review/transcript.ts:109, intake-classify.ts:180), handled-by.ts unclear; isClaraAuthoredOutbound is "author kind is not human" (message-provenance.ts:126-134); Message.role is tenant | assistant | tool_use | tool_result (types.ts:9261).
  • Hour-of-day read (2026-10-12, read-only: the local guest-card dump for the AppFolio year, production Query and GetItem only for the Clara line; Denver time; counts only): scratchpad pms/hours/hours.md, with the per-hour table, the reply-time tables by period and by reply rule, and the one-row-per-wait cross-check.
  • Localhost rendering mock (2026-10-12, worktree propflowai-proto-pms-mirror-rendering, branch proto/pms-mirror-rendering, no commit, no PR): scratchpad mocks/README.md, thread.png, thread-before.png, thread-photo.png, prompt.txt; 7 files, +348/-23; typecheck and eslint clean; 11 of 11 label checks with a time-zone control; the dev server runs on Turbopack (the repo's dev script since 2026-04-19; Webpack cannot build this page).
  • Year-to-date email census (2026-10-12, the leasing mailbox read-only through the app's Microsoft login, Jan 1 to Oct 12): scratchpad pms/census-ytd.md and census-ytd-counts.json.
  • Gera sync 2026-10-12, raw transcript: staff-sent comms are Fede's top priority; "for now we just keep polling"; always-on browser rejected; about 90% of inbox messages are tenants.
  • Code map (read-only, origin/main f9a83c70dc): thread read l4-core.ts ~3843 and the never-load-the-page rule at ~3779; parser guest-card-message-parser.ts; writer guest-card-message.ts (store ~154, attach ~403, relationship rule ~470-585); container-resolver.ts (find ~292, build ~582); conversation-home-index.ts (umbrella refusal ~155); handler.ts guest_card_messages ~5551-5826, quiet pass ~5221-5520; schedules.json ~278; request-budget.ts; pms-job-quiet-pass.ts.
  • Inbox list capture at Willows (2026-10-12, through the deployed robot's texting-inbox read route, no login): endpoint, parameters, raw and parsed row shapes, safety evidence (7 GETs, zero PATCH, unread rows unchanged), fingerprint design, four open items; scratchpad inbox-capture/shape.md and request-log.md.
  • Tenant identity at Western Slope (read-only prod, 2026-10-12): the 84% / 6.5% / 172 / 330 / 2% / 185 numbers above; the rule order; scratchpad inbox-identity-check.md.
  • Phase-3 sending report (2026-10-12): one guest-card route, no tenant route, STOP one-way, double-send on retry, no window or cap, shared login, 90-second login and lockout at about five failures, per-property script-only switch; scratchpad phase3-send-via-appfolio.md.
  • Browser infrastructure research 2026-10-12: Browserbase Hobby $20 for 100 hours then $0.12 an hour, Startup $99 for 500 hours then $0.10, 6-hour session cap, idle billed, contexts persist cookies, no page-change events; Steel (Apache-2.0); Browserless self-host enterprise only; EC2 t4g.medium about $24.50 a month.
  • Opus 5.5 round 1 (2026-10-12, max effort, read-only): six-layer target, the watch-a-list primitive, one job per database, store-first placement, the shared-phone rule, the split-number doors, 43 traps, levels 0 to 3, the two-pass Western Slope read, six decisions, seven disagreements; scratchpad pms/opus-round1.md.
  • Decision engines map (2026-10-12, origin/main f9a83c70dc): 20 engines read against a mirrored staff row and a mirrored tenant row, store-only effects, backfill table, phase 2 and 3 notes, a NOT VERIFIED list (bulk and system payload shape first); scratchpad decision-engines-map.md.
  • Western Slope inbox probe (2026-10-12 18:28 MT, read-only, list-only: 7 GETs, zero thread pages, zero logins; account echoed westernslopepm): the numbers in 9.5; raw rows deleted after the run; scratchpad pms/ws-inbox-probe.md.
  • Photo and MMS audit (2026-10-12, checkout 76ff997899): Twilio inbound photos end to end (webhook route.ts 109, 465-484; conversation-manager.ts 8265-8348; ChatSurface.tsx 545-570; work order attach handle-create-work-order.ts 1210-1252; AppFolio portal upload work-order-writer.ts 167-194); AppFolio parser has no media field and drops text-less rows; email photos saved unlinked (process-inbound-email.ts 735-777), Outlook and Gmail parts dropped; sendSms takes no media (twilio/client.ts 149-154), sendMms only for renewal letters; no inbox photo marker; unused sync-photos-to-appfolio.ts. The suspicion that PR #1100 dropped the technician photo was checked and does not hold: #1100 touched dispatch-envelope, dispatcher, envelope and telegram only; the dispatch text to a technician has never carried a photo.
  • Company brain and coworker history (2026-10-12, code at 76ff997899): the brain is write-only (notes store, email intake, transcript note, invite-to-join, all merged dark); no reader; a staff teaching has no path to the brain; quotes tagged by provenance; scratchpad pms/company-brain-history.md. Folded into the vision v2, section 4.3.
  • 12-month communications census (2026-10-12, read-only, counts only, no AppFolio calls; prod DynamoDB Query/GetItem, the scrubbed four-month mailbox corpus, the September audit page): the tables in 9.4; scratchpad pms/census.md with the exact read commands in its section 7.
  • Opus 5.5 round 2 (2026-10-12, adversarial read of v1.1 against main fe6471c and the robot at a0c7c61): 7 blocking, 15 important, 17 contradictions, verdicts on decisions 1 to 10, a NOT VERIFIED list of twelve items; scratchpad pms/opus-round2.md. Folded into sections 3, 5, 6, 7, 8, 11, 12 and 13 in v1.4.
  • Guest-card send path check (2026-10-12): files, test counts, gate, dedupe, failure handling, git dates; in section 8.
  • Real-data mix (2026-10-12, random samples of real messages: 1,083 people-messages across six doors, classified on the subscription with Haiku, 92% hand-check agreement; production read with Query and GetItem only; Gmail and the saved mailbox copy read-only; no AppFolio request): scratchpad pms/mix/mix.md and mix.json. Not read: Situs tenant texts, a full year of Western Slope tenant texts, call logs.
  • Pending as of v1.6: nothing; the repo-shape investigation and the PMS-native competitor half are on the vision page.
  • Verified: the per-thread endpoint and cursor; the list endpoint, its rows and that it marks nothing read; the per-property loop; no customer-level budget on the robot reads; Willows-only turn-on of the existing feed; the identity numbers. Unverified: mark-as-read fires only on the page load (code comment, never captured). Assumed: email threads in the list; synced tenants carrying PMS ids on every row; the line type's provider field; tenant-party thread reads.
PropFlow Docs