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."
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.
| Row | Production sent | Today's code (all layers on) | All blind layers off |
|---|---|---|---|
| Over-share (counts, rosters) | 18 | 0 | 0 |
| Question left unanswered | 12 | 4 | 5 |
| Meaning changed from the draft | 10 | 29 | 8 |
| Tour offered | 72 | 60 | 29 |
| Held, nothing sent | 0 | 6 | 35 |
| Layer | Fired (cards) | Cards worse when off | Verdict |
|---|---|---|---|
| Tour-times check | 31 | 9 (7 are clock drift; net it helped on 2) | Keep the job, move it into Clara's turn |
| Name the home | 13 | 3 (all real) | Keep the job, move it into Clara's turn |
| Count-check redraft | 10 | 6 (4 real) | Keep |
| Bare-handoff recovery | 0 differ | 0 | Dead, delete |
| After-trace redraft | 1 differs | 0 | Dead, delete |
| Held redraft | 0 differ | 0 | Dead, delete |
| Greeting restore | 8 | 1 (judge noise) | Dead, delete |
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).
| Row | Today | All four off |
|---|---|---|
| Emails sent | 61 | 60 |
| Held | 6 | 7 |
| Over-share | 0 | 0 |
| Tour offered | 60 | 59 |
| Unanswered | 4 | 5 |
Shipped as PR #21992 (fix, no switch), armed to merge 2026-10-22.
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 | Today |
|---|---|
| System prompt | 264,510 characters, 1,082 lines |
| Tools offered | 23 tools, 47,030 characters |
| Tokens in, first call | 109,658 |
| Time to draft | 75 s, then a timeout |
| What was sent | A blind redraft that never saw the renter's message |
| Section | Chars | Needed on a first reply? |
|---|---|---|
| Guest-card first-email composer rules | 29,435 | Partly (conflicts inside) |
| Tour scheduling | 22,921 | No |
| Presenting units | 18,480 | No |
| On-behalf visitors / companions | 18,217 | No |
| Autonomous property rule | 14,855 | One line |
| Rules for this first email | 12,274 | Yes (conflicts inside) |
| Answer-and-tour rule | 11,853 | One line |
| Always-route rule | 8,591 | One line |
| Hard-fail quick check | 7,235 | No |
| Post-approval rule | 7,000 | No |
| Active capabilities | 6,857 | No |
| Concern-before-call-to-action rule | 5,052 | No |
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.
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 it does | PR | State |
|---|---|---|
| A text thread keeps the number it is already on | #20975 | merged |
| A double text or a redelivery gets one reply, not two | #20974 | merged |
| A held text expires instead of going out stale, and re-checks every gate before it goes | #20985 | merged |
| A text with a photo or file leaves on Clara's line; the renewal letter's intro and PDF come from one number | #20982 | merged |
| A prospect's reply inside AppFolio wakes Clara, when she is the one talking | #20992 | merged |
| A STOP or START texted from a number AppFolio cannot tie to a property is still recorded (found by the live run, check 5) | #21031 | merged |
| A teammate's text in AppFolio puts Clara on hold for that person | #20993 | merged |
| STOP on AppFolio's texting line opts the person out on both numbers | #20995 | merged |
| The thread rule reads past a household member's texts and never counts a text sent to someone else | #21000 | merged |
| Check | Pass means | Result |
|---|---|---|
| Baseline, Oct 16 | Reply into an existing AppFolio thread; zero texts on Clara's line | proven Oct 16 36 s to the phone (before tonight's changes) |
| 1. Full reply loop | Prospect texts AppFolio's number, Clara wakes, replies through AppFolio, filed once, follow-ups stop | pass (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 thread | Thread started from the guest card, reply received from AppFolio's number | not 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 line | Reply comes from Clara's line; nothing through AppFolio | pass (Oct 19, 05:53Z): the reply came from Clara's line in 46 s; no door row, no AppFolio thread started |
| 4. Double text | Exactly one reply | pass (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 line | Both numbers refuse; START restores | pass (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 Clara | Clara stays silent | unit-covered only; needs a second AppFolio login to drive live. Not tested |
| 7. Held text expires | Expired or re-sent under the new rules | not exercised: no held text was eligible in any run tonight (the 5 held rows at the Willows are final; nothing new was held) |
| What goes out | Today | Door on |
|---|---|---|
| Leasing text, brand-new prospect | Clara's line | AppFolio's texting line (thread started from their guest card) |
| Leasing text, conversation already on Clara's line | Clara's line | Clara's line (a thread keeps its door) |
| Leasing text, conversation already in AppFolio | Clara's line | AppFolio thread |
| Maintenance, vendor and staff texts | Clara's line | Clara's line |
| Renewal letter (intro and PDF) | Clara's line | Clara's line, both pieces |
| Voice | Clara's line | unchanged |
| leasing mailbox | unchanged. Guest-card emails keep leaving from the leasing mailbox. The text door touches no email. |
What else changed tonight:
Same 41 people Clara texted since Oct 2. "Right number" means the number the person last wrote to.
| Company setting | Old rule (Oct 16) | Tonight |
|---|---|---|
| Clara's line (today's setting) | 41/41, all on Clara's line | 41/41, no change |
| AppFolio door turned on: right number | 9/41 | 33/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 misses | People | What 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 number | 3 (…6728, …9771, …8330) | Defensible either way: texts came from both numbers, they answered neither. |
| Western Slope | Situs Group | |
|---|---|---|
| Clara's own AppFolio login | yes (user 540) | none recorded |
| Inbox copy (mirror) | on since Oct 16 | off, never run |
| AppFolio database in the mirror | yes | one to add (to confirm it is exactly one) |
| Robot session kept warm | yes | to confirm (could not read tonight) |
| AppFolio requests, Oct 18 | 3,324 of 4,000 (83%) | none yet |
| Text door setting | off (Clara's line) | off |
| Turn-on | one company setting | login, mirror, database, warm session, then the setting |
| When | after the Willows table is green | not this week: Willows, then Western Slope first (Oct 14, D7) |
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, 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
| # | Question | Options (recommendation first) | Touches a prior ruling |
|---|---|---|---|
| D-A | After 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-B | Rollback 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-C | Who 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-D | Evenings 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-E | When 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). |
| # | Finding | Who / how often (Western Slope, real data) | Severity | Closes it |
|---|---|---|---|---|
| 1 | Inbox copy goes blind on budget between 3:30 and 7 PM Denver; replies in AppFolio wait until midnight | 57 of 129 texts to Clara's line arrive after 3:30 PM, about 3 a day | fixed, 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 |
| 2 | A staff text stops Clara on that lead for good, nobody is told | 11 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 refused | blocking, your pick | D-A |
| 3 | Tenants' AppFolio texts wake Clara (leasing voice answers a repair question) | About 10 of 61 recent inbound texts are from tenant numbers | fixed, proven live | #21563 merged, Willows tenant check passed 05:33Z |
| 4 | Leasing-only held only by accident; held maintenance drafts and some tenant replies could leave via AppFolio | Maintenance is in drafts at Western Slope, so live | fixed, 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 |
| 5 | No AppFolio card → no text at all, held for good, never falls back to Clara's line | 4 of 35 recent leads (phone callers); the card-after-call fix is live but no leasing caller has arrived since | worse, unproven fix | Watch the first post-fix caller |
| 6 | Rollback 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 returns | 44 threads today whose newest text came via AppFolio | worse if we roll back | D-B; expire held rows on rollback (small fix) |
| 7 | Door-down alert fires only in Sentry, never in #alerts | Oct 16 Willows episode had no channel post | ops | Route to #alerts (small fix) |
| 8 | Retry 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 rare | worse | #20989 |
| 9 | Some 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 reply | 0 people hit this since Oct 2 | cosmetic | Follow-up issue |
| 10 | Past 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 number | Rare | cosmetic | Follow-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.
| Behavior | Status | Evidence |
|---|---|---|
| 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 door | proven live | Willows runs Oct 19 (05:51–11:11Z) and Oct 20 16:58Z |
| Tenants never wake (#21563); leasing-only send rule (#21569) | proven live | Willows run Oct 21 05:25–05:39Z on build 6e1e4a9b, below |
| 15-minute peeks after the reserve (#21568) | proven live | Western 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 only | Merged 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 text | unit-covered only | Needs a second AppFolio login / the first real send |
| Willows run on the deployed fixes (reply loop, double text, tenant stays silent) | passed 3 of 3 | Oct 21 05:25–05:39Z, build 6e1e4a9b on website and both Lambdas; table below |
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).
| Check | Sent (UTC) | Result | Evidence |
|---|---|---|---|
| 1. Prospect reply (card 197) | 05:25:20 | pass, slow: 5 min 17 s to the phone | Copy 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 text | 05:30:48 and 05:30:52 | pass, 87 s | MSG#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:55 | pass | Tenant 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.
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.
| Who and what | People | Staff text type | Lead wrote back | Staff answered the reply | Clara wrote them since Oct 9 | Clara wrote after staff |
|---|---|---|---|---|---|---|
| Prospects (leasing) | 19 | 7 "found a place in Ridgway?" check-ins, 4 "closing your guest card", 4 "still want a showing?", 2 intros, 1 application link, 1 real conversation | 1 of 19 (a no-thanks) | n/a | 10 | 1 (…6055, the tour) |
| Residents: work orders | 8 | "Was your work order fixed?" | 13 of 14 | first answer median 18 min (n=12); every reply median 5 min | 2 ("rate your repair" survey; one went 10 min before staff asked the same thing) | 0 |
| Residents: renewal | 3 | Renewal offer | 0 | 0 | ||
| Residents: move-in | 2 | Codes, keys, signatures | 1 (email) | 0 | ||
| Residents: other | 1 | "We saw your email" | 0 | 0 |
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.
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.
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.
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 off | Who | How often (measured unless marked) | Severity | What closes it |
|---|---|---|---|---|---|
| 1 | The 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 day | Today 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. |
| 2 | A 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 Clara | 11 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. | Blocking | Hand-back decision (H1) plus a list of muted leads |
| 3 | A 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 card | 4 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 sent | Watch the first post-fix caller. Fallback rule is Fede's call. |
| 4 | A tenant's text to the office number wakes Clara. "Leasing only" is not enforced. | Tenants | 10 of 61 live inbound texts since Oct 16 come from numbers tied to a tenant (4 tenant-only) | Blocking under Fede's leasing-only ruling | Merge and deploy #21563 (still open) |
| 5 | On rollback, every lead in an AppFolio thread switches to Clara's number. Their replies inside AppFolio stop waking her. | Leads whose thread moved into AppFolio | About 4 new leads per door-on day (inference: 72 new inquiries in 19 days), plus 10 who move on day one | Blocking, only if we roll back | Keep the wake on for AppFolio threads after rollback, or decide this before the flip |
| 6 | Slower 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 replies | Texts to Clara from 7 to 9 PM: 9 in 19 days. After 9 PM: 0. | Tolerable | None needed |
| 7 | The number changes mid-conversation. Mass texts, texts the router can't place and the first renewal offer don't name their thread. | Open conversations | 3 of 49 move to AppFolio because Kat's text is newest (by design). The rest are not counted. | Tolerable | Name the thread in those senders |
| 8 | A held text can be re-sent after 9 PM. The retry step doesn't re-check quiet hours (#20989). | Leads | 0 such retries at Western Slope so far. 1 Clara-started text to a lead after 9 PM since Oct 2. | Tolerable, rare | #20989 |
| 9 | Clara'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 email | 3 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 answered | Print the AppFolio number when the door is on |
| 10 | A send marked "transactional" can still reach someone who texted STOP (#21279) | People who opted out | 1 STOP on Clara's line since Oct 2. 0 live STOPs on the office line. | Tolerable | A7 fix |
| 11 | Leasing texts from the office number, maintenance texts from Clara's | Renters in both lanes | 0 people got both since Oct 2 (5 got maintenance only) | Cosmetic | Tell Western Slope |
| 12 | Lockbox 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, leads | 6 texts with codes (2 people), 2 with links | Cosmetic, net better | None |
| 13 | The first text comes from the office number | New leads | Every 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. | Cosmetic | None |
| 14 | AppFolio's own texts land in the same thread: Clara-seat "Rental Application" texts, notices and bulk sends. They don't mute Clara. | Leads | In 5 days: 3 Clara-seat texts, 41 notices, 208 bulk sends | Cosmetic | None |
| 15 | Premise 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 leads | No longer worse | Better | None |
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:
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.
| Text | Number | Note |
|---|---|---|
| First text to a new lead | AppFolio | Opens 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 lead | AppFolio | Only 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 tenant | Clara's | That lead's tour and application texts go through AppFolio, so they see two numbers. |
| Fallback reply after Clara's turn crashes | Clara's | It carries no conversation, so the leasing-only check reads "unknown". |
| Follow-up and re-engagement texts, staff-asked texts | AppFolio | |
| Tour confirmation, reminders, slot lost, outcome, unconfirmed nudge, post-tour chase | AppFolio | |
| Weekend tour check-in and morning-of nudge | Clara's | It names no company or property, so the switch is never asked. One-line fix. |
| Application link, lease approval | AppFolio | |
| Household copies (tour, application, chase) | AppFolio | Also when the co-applicant is a current tenant: no check is made. |
| Self-guided lockbox code, delay notice, leave check | AppFolio | Text only, no picture. Staff can see the code inside AppFolio. |
| Relay of a teammate's note | AppFolio | When 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 reply | Depends | Follows 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 bounces | Clara's | Names 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's | Now 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 tool | Clara's | The renewal letter's words and its PDF both go from Clara's number. |
| Collections notice, mass texts | Clara's | Mass texts are hard-coded as going to tenants. |
| Vendor, technician, staff, emergency, login code, staff notices | Clara's | Sent straight through Twilio. |
| A tenant's reply during tour booking | AppFolio | Filed as leasing whoever is texting. |
| Any text with a photo or file | Clara's |
| Row copied in | Wakes Clara? | Why |
|---|---|---|
| Prospect's text, nobody at the office has answered, or Clara's text is the newest | Wakes | Clara answers on the same thread. |
| Applicant's text (labeled applicant) | Wakes | Treated as a prospect. |
| Prospect's text after a teammate or the shared login answered | No | Reason "teammate after Clara". The hold never lifts (H3). |
| Tenant's text (every person on the thread is labeled tenant or occupant) | No | New reason "tenant thread". Filed on the open renewal conversation if there is one. |
| Household member labeled "occupant" | No | Counts as a tenant. |
| Thread that mixes a tenant with a guest card or applicant | Wakes | Not tenant-only. If the thread is filed on the tenant, the reply goes from Clara's number. |
| Staff text | No | Not incoming. It puts Clara on hold for that person and closes their open handoff. |
| Mass text or automatic AppFolio notice | No | Not incoming. Ignored when deciding who spoke last. |
| AppFolio's own AI, a Clara row from the wrong login | No | "Not a renter" or "Clara not on thread". |
| STOP, START, HELP | No | Reason "keyword". STOP and START are recorded on the number for both of Clara's numbers. |
| Rows from before the switch went on (history) | No | Reason "history". |
| Email copied from AppFolio | No | Reason "not text". |
| Any row while the switch is off | No | Reason "door off". The tenant check runs first, so tenants log "tenant thread". |
| Row that cannot be matched to a person | No | Held, never wakes. |
| Row arriving after the evening budget pause | Late | Not copied until midnight Denver, then answered at midnight. |
| Behavior | What the code does | Evidence |
|---|---|---|
| Rule 1: photo, file, staff, vendor (and now any non-leasing text) go from Clara's number | Checked first | Unit only (row 16 never run; leasing-only fix not run) |
| Rule 2: a conversation keeps its number | Reads up to 100 recent messages. A failed read lets the switch decide. | Live Oct 19 05:53Z |
| Rule 3: the company switch | Read 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 alert | Never falls back to Clara's number | Unit only for the no-card case |
| Reply into an existing thread | Live Oct 19 11:01Z (59 s), Oct 20 L3 (99 s) | |
| First text opens the thread from a guest card | At most 3 cards | Live once Oct 20 16:58Z L3 (about 18 s, about 27 requests) |
| Double text gets one reply | Decided once per batch | Live Oct 19 11:05Z |
| Read-back of AppFolio's message id (#21068) | No id means "unknown", never resent, and a door-down alert | Live L3. The false-alarm path is still open. |
| Held texts: retry, expiry (30 min for replies, 6 h for others), superseded replies | Same 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 episode | Opened by a final failure, the 10th busy hold, or an expiry | Unit only (a false one fired Oct 16, #21068) |
| STOP and START on the AppFolio number | Recorded on the phone number, so Clara's number honors them too | Live Oct 19 and Oct 20 01:31Z |
| Send after STOP is refused | The same check guards both numbers. Hole: "transactional" texts still go out (A7, #21279). | Live (a reply only) |
| Tour confirmation and 1-hour reminder | Live Oct 20 01:25Z and 14:00Z | |
| Follow-up and re-engagement through AppFolio | Not yet (the 0338 touch was still due) | |
| Teammate text puts Clara on hold | Permanent, and staff cannot see it | Unit 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 hours | The drain does not check them (#20989) | Code only |
| Issue | One line | Weight |
|---|---|---|
| #21068 | The read-back gives up after about 4 s, raising a false door-down alert and holding the new thread | worse than today |
| #20636 | Opening a thread tries only 3 of the person's cards, then refuses | worse than today (that lead's text is held, not sent) |
| #20989 | The drain sends held texts during quiet hours | worse than today |
| #21279 | After STOP, Clara tries to confirm by text; the blocked reply shows as sent | worse than today |
| #21069 | START from AppFolio records no person | nice to have |
| #20987 | Outbox rows keep phone numbers forever, and the drain query grows with them | nice to have |
| #20988 | A held text is invisible on the thread | worse than today |
| #20990 | Expired texts alert as "door down" | nice to have |
| #20991 | Clara's own Twilio sender drops attachments | nice to have (not the door) |
| #20999 | A backdated AppFolio STOP overrides later email opt-ins | nice to have |
| #21010 | Wake edge cases: a 100-row batch cut, a failure that drops wakes, the author label | nice to have |
| #21306 | Tour reply test pin | nice to have |
| #21358 | A second STOP does not fan out | nice to have |
| #21453 | A refused first follow-up text is never retried and raises no alert | worse than today (refusals are likelier on the door) |
| #21454 | Releasing a held first email writes a fake inbound message | nice to have |
| #21455 | Parked AppFolio write-backs are never retried | nice to have |
| #21514 | A real send-once marker on the Twilio side of held texts | nice to have |
| #21316 (new) | Senders that name no conversation switch numbers at the flip | blocking (the two-number risk) |
| #21325 (new) | A failed thread lookup still sends a mass text through AppFolio | nice 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 thread | nice to have |
| #21260 (new) | A failed tenant-record read silently falls back to the guest card | nice to have (tenants are now off the door) |
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.
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.
| Turn on | Roll back | |
|---|---|---|
| Preferred | Admin, Customers, Western Slope, Modules card, "Where Clara's texts go out" toggle ON. This records who and when. | Same toggle OFF |
| Fallback | Raw write below with appfolio_inbox | Raw write below with clara_line |
| Takes effect | Next text (a plain read, no cache). Reply wakes start on the next minute tick. | Same |
| Who presses | Fede, in his own words | Owner 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>"}}'
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) | Count | At rollback |
|---|---|---|
| Threads whose newest text came in through AppFolio (the inbox copy) | 44 | Their AppFolio replies stop waking Clara. The reason is recorded as "the team holds this", so nothing pages. |
| Threads Clara has texted through the door | 0 | Their 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 outbox | 0 | Correction 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"}}'
| Surface | Now | Healthy on day one |
|---|---|---|
| Wakes by reason | none (door off) | Mostly woke. Watch for teammate_after_clara and clara_not_on_thread: both are silent. |
| Outbox held texts and alert latches | 0 rows | no_address on callers with no guest card is expected. Any door_down is real unless it matches #21068 or #20990. |
| Inbox-copy holds | 37, all no_property | Flat |
| AppFolio budget | 3,398 requests used of the 4,000 daily cap; the 600-request reserve hit Oct 20 | Reserve hit late or never. Door-on estimate: about 15:30-17:00 Denver. |
| Door-down alert | Sends to Sentry as a fatal error | No #alerts post was found for the Willows episode on Oct 16. Treat it as Sentry only until shown otherwise. |
| Leak check | 20 texts delivered from Clara's line on Oct 20 | Texts 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
| Page | Verdict |
|---|---|
| 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 holds | real A person is waiting. A door-down follows within 30 minutes. |
| "No reply for 15 minutes" on an inbox-copy reply | fixed 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.
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 time | Reads | Lead's reply → Clara's answer, typical / worst | After the reserve is hit |
|---|---|---|---|
| 12–6 AM | every 30 min | ~16 / ~32 min | n/a (budget resets at midnight) |
| 6–8 AM | every 5 min | ~4 / ~7 min | n/a |
| 8 AM–7 PM | every 60 s | ~2 / ~2.5 min | ~9 / ~17 min (from ~6:30 PM) |
| 7–10 PM | every 5 min | ~4 / ~7 min | ~9 / ~17 min |
| 10 PM–12 AM | every 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):
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).
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.
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:
Settings, read now: the door is unset at every company except the Willows test company, so every other company texts from Clara's line.
"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.
| Window | People | Right number | Stay on Clara's line | Move to AppFolio |
|---|---|---|---|---|
| Since Oct 2 (the page's 41 was 33/41 on Oct 18 data) | 49 | 36 (73%) | 39 | 10 |
| Last 14 days | 35 | 25 (71%) | 25 | 10 |
There are 13 misses since Oct 2, in three groups:
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:
| What | Count | Oldest | Why | What the door would do |
|---|---|---|---|---|
| Texts the door itself is holding | 0 | – | The door has never been on here | – |
| Inbox copy, parked as "tenants later" | 707 (180 from renters) | Oct 1 | Parked 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" | 27 | Oct 14 | Ambiguous person | Nothing. They are never offered again. |
| Inbox copy, "no match" | 13 | Oct 2 | Number matches no one | They are offered again, but all 13 are history, so filing them is silent. |
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).
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 | Blocks WS today? | Smallest close |
|---|---|---|---|
| 1 | Fede'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 deployed | Decided 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 |
| 2 | The teammate hold is permanent and invisible to staff. | No it fails toward silence | Hand-back pick H1; a list of muted threads |
| 3 | A caller with no AppFolio card gets no text (4 of 35). | No card-after-call fix merged Oct 20, not yet seen live | Read-only: the first post-call door send is sent, not held |
| 4 | The hold has never run with a real teammate. | No | Read the first held conversation after Kat's first text |
| 5 | The inbox copy pauses at 3,400 in the evening. | No sends keep their reserve | Trim the copy's reads later |
| 6 | The read-back can raise a false "door down" (#21068). | No didn't recur Oct 20 | The code fix |
| 7 | Only 3 cards are tried when starting a thread (#20636). | No 8 of 8 have one card | None needed now |
| 8 | Maintenance and renewal-tool texts stay on Clara's line. | No | Say so to WS |
| 9 | A "transactional" send can still reach a stopped number (#21279). | No | A7 fix |
| 10 | Situs 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.
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.
| Piece | What changes on the card | PR |
|---|---|---|
| Message notes | Every 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, reminders | The first text to a lead, each follow-up when a lead goes quiet, and tour reminders and confirmations get a note. | #21667 |
| Outcome notes | Tour 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 deleted | The Willows-only "write notes from texts" setting is gone; notes write everywhere. | #21669 |
| Ordering fix | Notes 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 deleted | Tours 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) |
| Harness | The 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 |
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.
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.
| Rule | Text | |
|---|---|---|
| We hold a number the person gave us (never one found in a signature; STOP blocks texts, not emails about their own tour) | yes | only if they ever reached us by email |
| No number we may text | no | yes, and the message asks for a number |
| Text-only lead | yes | no, 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.
| PR | Covers | State |
|---|---|---|
| 1. Shared channel rule | One helper (text / email / both) used by the booked-tour confirmation, the prospect-asked tour-moved confirmation, and both follow-up cadences | building, branch fede/tour-confirmation-all-channels |
| 2. Reminders and nudges | 24h 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 email | after PR 1 merges |
| 3. Codes, desk, application | Self-guided code and notes, staff-booked showing notices, application link (adds the text), household copies per person; one consent check everywhere | after 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.
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.
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.
| Part | Kept | Example |
|---|---|---|
| What they said or need | Yes, facts they stated | Wants to tour Fri morning or Sat. Has one dog. |
| What we did or offered | Yes | Emailed open times (Fri 10:30–12, Sat 10–2:30). |
| Outcome | Yes | She picked Fri 10:30. / Not interested: "found a place closer to work." |
| Next step | When there is one. A future date is the one date we write | Booked Fri 10/23 10:30 AM. |
| Channel line, subject, address, time, name, phone, email, greeting, full message | Never | — |
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 note | New 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 replies | Keep 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 line | Same as email: one line per exchange that moved. |
| Text replies, AppFolio texting inbox | Drop. AppFolio shows them. |
| Follow-up nudges | Fold 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 reminders | Fold into the tour-day outcome only when it matters: "No-show. Day-before reminder went out, no reply." |
| Phone call | Keep, 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-off | Fold into the outcome line ("…confirmed by email"). |
| Tour booked / moved / cancelled, toured, no-show, running late | Keep. 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, archived | Keep. |
| Application sent | Drop when sent through AppFolio (it logs it). Keep otherwise. |
| No reply after 2 weeks | Keep, and it carries the nudge rollup. |
| Team's answer to a question Clara passed on | Keep, shortened: "Team: no smoking on the balcony." |
| Part of the line | Made by | Risk |
|---|---|---|
| Outcome, slot, next step, counts ("3 times"), channel word ("by email") | Our own records, fixed sentences | None beyond today's |
| What we offered | The tool calls Clara made (times offered, answer given) | None: read from what was sent |
| What they said or need | The 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 model | A 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.
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.
| Piece | Change |
|---|---|
| Email and text turn notes, the first "Email Sent" comments | Replaced by one exchange line, written at an outcome or after 15 quiet minutes |
| Nudge and reminder notes | Removed; counted into the closing line or the tour-day line |
| New-lead copy | Dropped when AppFolio made the card; one line in her words when Clara did |
| Call note | Same summary, rewritten to the rule and the fields |
| Outcome sentences | Shortened; gain "confirmed by email" and the nudge rollup |
| Contacts before the card exists | Now covered: lines are written from our records once the card is there |
An improvement to live behavior: no switch, live everywhere on merge. Proof:
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.
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:
| Clara action | Western Slope (since Sep 17) actions / landed / share | Camellia (since Apr 1) actions / landed | Situs (since Sep 29) |
|---|---|---|---|
| First contact, email | 83 / 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, text | 4 / 0 / 0% | (in the row above) / 0 | |
| Replies in a conversation | 111 / 3 / 3% | 432 / 0 | |
| Follow-up nudges (lead went quiet) | 98 / 0 / 0% | 219 / 0 | |
| Tour booked | 36 / 31 as AppFolio Showings / 86% | 87 / 0 | |
| Tour moved / cancelled | 5 / 5 and 4 / 2 | 11 / 0 and 16 / 0 | |
| Tour reminders and confirmations to the renter | 38 / 0 | 75 / 0 | |
| Self-guided access code sent | 0 sent (no note step exists) | 0 sent | |
| Application sent | 25 / 24 / 96% | 47 links / 0 | |
| Handed off to the team (text/email) | 11 conversations / 3 / 27% | 18 / 0 | |
| Prospect phone calls | 60 / 26 / 43% | 333 / 0 | |
| Went cold or not interested | 12 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 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.
| Action | Card today | Under (a) | Example note |
|---|---|---|---|
| First email | Email Sent entry | same | (full email, as today) |
| Reply / first text | nothing | note | Clara texted: our standard lease is 12 months. |
| Nudge | nothing | note | Follow-up 2 of 4 (text): still interested in 2841 Morningside? No reply yet. |
| Tour booked / moved | Showing only | Showing + note | Tour booked Wed 11:00 AM, virtual. Moved to Fri 1:30 PM at renter's ask. |
| Code sent | nothing | note | Self-guided code sent for Sat 10:00 AM at 2988 Krista. |
| Handed off | sometimes | always | Asked about a 6-month lease; handed to the leasing team. |
| Went cold | almost never | note + inactive | No reply after 4 follow-ups over 2 weeks; marked inactive. |
| Call | 43% of calls | every call | Call: asked about 2978 Emerald; transferred to leasing. |
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.
| Option | What the card shows | Volume at Western Slope (Sep 17 – Oct 20) |
|---|---|---|
| (a) Consistent, complete recommended | Every contact and every outcome, at all three customers; message notes skipped only where AppFolio already shows the text | about 490 entries over 68 leads (about 7 per lead, about 15 a day) |
| (b) Consistent, terse | One note per lead per day summarising what Clara did, plus outcomes | 244 notes (lead-days with any Clara action) |
| (c) As-is | First email only at Western Slope; nothing at Camellia or Situs | today'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).
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).
| PR | What it does |
|---|---|
| #19167 | The shared shape of a mirrored text, and the off-by-default company setting. |
| #19191 | Turns 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). |
| #19187 | A mirrored text wakes nothing: no Clara reply, no hold, no follow-up touch. |
| #19198 | The readers that judge "who answered last" see staff texts as staff, not as Clara. A fix, live everywhere. |
| #19218 | Places 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. |
| #19222 | Tests for the message handler. |
| #19219 | The raw store, one per AppFolio database, with the wall: rows for a company whose setting is off are dropped. |
| #19220 | Clara's decision engines handle mirrored rows; history rows move nothing. |
| #19232 | Labels each row by who sent it: a named staff member, a bulk blast, an AppFolio notice. |
| #19233 | Files a stored row into its conversation, carrying the no-send import check. |
| #19243 | The 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. |
| #19246 | One writer per door: on a mirrored company the old guest-card importer files only emails and closes its text rows. |
| #19248 | The watch itself: reads the list, remembers how far it got, catches up after a gap and backfills a newly turned-on company. |
| #19204 | What staff and Clara see: "Kat · AppFolio text, Oct 9 4:12 PM", a bulk blast once with a count. Unchanged without mirrored rows. |
| #19263 | Runs one tick per database, with a claim so two runners never read the same database at once. |
| #19264 | merged · not proven live The production lookups placement needs; a cross-kind id match now also needs the name to agree. |
| #19276 | merged · not proven live Runner-B: files new rows into conversations. |
| #19281 | merged · 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. |
| #19284 | merged · not proven live The report command: per-tick and lag numbers. |
| #19282 | merged · not proven live Keeps a bulk blast's recipient count. |
| #19249 | Coworker Line: a staff caller is never handed to the tenant repair-intake agent. |
| #19254 | Coworker Line: finds a building from a street address; counts vacancy by unit status. |
| #19295 | armed Registers a nightly test workflow the drift check was complaining about on every PR |
| Measure | Result |
|---|---|
| Ticks in a 75-minute watch | 4 (3 at the late 15-minute pace, 1 at the overnight 30-minute pace) |
| Requests per tick | 3 |
| Seconds per tick | 2.4 to 2.5 |
| Total requests | 12 |
| Rows stored | 0 (80 rows belonged to companies with the setting off; the wall dropped them) |
| Backfill | Empty: the 7-day window held no texts (newest JP&Co text Oct 6) |
| Logins triggered | 0 |
| 403s | 0 |
| Unread flags | Unchanged |
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.
| Call | Rerun 1 | Rerun 2 | Final, fresh keys |
|---|---|---|---|
| 1 | pass | pass | pass |
| 2 (balance) | pass | Right; style fail | All 5 answers right; judge fails on style (#19274) |
| 3 (hand-off) | fail refused without a street address | pass fixed by #19249 | fail 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 6 | Clara's 7 right; key stale | fail count right (7); Q3 read all 8 vacant units, not three and an offer (#19285) |
| 5 (work order) | pass | Right; style fail | pass |
| 6 | pass | pass | pass |
| Score (before: 1/6) | 4/6 | 3/6 | 3/6 strict; numbers right except call 3's slip |
| Control | Planted 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.
TL;DR: one real ask, the Western Slope turn-on.
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
| Fact | Today | Source |
|---|---|---|
| Where tenants and prospects talk to the office | The 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 it | Nothing 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 number | Answers 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 off | It 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 size | 90 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 live | Western 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 today | Clara 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 |
Three places the core could live. Recommendation first.
| Option | What it is | For | Against |
|---|---|---|---|
| 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" service | Its 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 service | Gera'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.
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.
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.
| When the other system offers | We speak | Examples |
|---|---|---|
| A real API | REST calls, webhooks when they exist | Twilio, Microsoft Graph, the AppFolio Reports API |
| An agent-facing interface | MCP or a CLI | Google Drive, Monday.com, Salesforce (later, if ever) |
| Only a web app (legacy) | The browser agent, exposed as an API: session, list, fetch one, write one | AppFolio'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.
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).
| Option | Browser cost per customer | Freshness | Catch |
|---|---|---|---|
| A. New browser session per poll | About $100 a month at one poll a minute (Browserbase, 6-hour max, idle billed) | 1 minute | Login or context restore on every poll; slow; the robot's write pattern, wrong for reads |
| B. One browser always on, watching the inbox | About $120 a month, plus reconnect logic every 6 hours | Seconds | Fragile; 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 $0 | On 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 |
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.
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).
What the Western Slope read says (read-only, 2026-10-12).
| Fact | Number | So |
|---|---|---|
| Current tenants with a phone who already match a PropFlow person | 337 of 401 (84%) | Most live threads land on day one. |
| Past tenants who match | 35 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 skips | 172 of 517 current tenants, 65 with a phone | Same step; they text the office like anyone else. |
| Leaseholders who never had a Clara conversation | 330 of 354 | Almost every attach creates a thread; the create path is the main path. |
| Bare phone match landing on a housemate | about 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 id | Join on ids, never labels. |
| Prospect inquiries already sitting on the front-door placeholder | 25 | The matcher never adds to them; a build check enforces it. |
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.
| Rule | Statement | Why |
|---|---|---|
| R1 | Inbound through any door lands in the person's one conversation, badged with its door. | Staff and Clara see one history, whoever answered where. |
| R2 | A 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. |
| R3 | From 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. |
| R4 | Clara'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. |
| R5 | Opt-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. |
| R6 | One 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. |
| R7 | A 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. |
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 row | Wanted? | 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 blast | keep; 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's | No | one 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 properties | No | collections, 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 minutes | No | mirror 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 thread | No | store signature-stripped text; bulk and system rows fall outside the commitment predicate; private-info detectors on Clara's replies |
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.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.
| State | Clara on the PMS door | The office | When |
|---|---|---|---|
| Reads | Mirrors 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 rest | Answers 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.
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.
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.
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:
| Outcome | decide.ts | What leaves |
|---|---|---|
| Clara handled it, or a person already has it (8c's team's turn) | 1 handled | nothing; 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 ask | one note to the company inbox; Clara waits for the answer |
| Escalated, with a reason ("unhandled" included) | 3 handoff | one 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.
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.
| Gap | What happens today | Fix before any send |
|---|---|---|
| Double send | A 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-way | A 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 brakes | No 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. |
| Identity | Humans 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 lapse | A 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. |
| Switch | Per property, script-only, no page. | One customer setting on the customer page, default off. |
| Tenants and work orders | No 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.
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 sends | Record | Lands in | What the Willows prototype proves |
|---|---|---|---|
| General tenant message | tenant on a unit | the tenant's thread | tenants can be reached, not only prospects |
| Work-order update | tenant on the work order | the same tenant thread, next to AppFolio's own "System" rows | work-order number in the body; a planted wrong tenant is refused |
| Guest-card reply | prospect | the prospect's thread | the mirror marks the sent row as ours; nothing leaves Clara's number |
| Application message | prospect | AppFolio's own button (exists) plus the link text in the thread | a killed call never yields a second application |
| Renewal offer | tenant on the lease | the tenant's thread | renewals 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.
| Step | Measured |
|---|---|
| Our post to AppFolio's stamp on the row | 4.9 s |
| Robot answers with the row id | 6.7 s |
| Our read-back finds the row | 11.6 s |
| The mirror sees the row | 29 s (one sample, inside the one-minute tick) |
| Unread mark on the thread | unchanged |
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:
| Step | Check | If not |
|---|---|---|
| Right person | the 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 company | refuse |
| Nothing new | the send carries the newest message id Clara read; the thread has not moved (no newer text, no staff reply) | refuse, Clara decides again |
| Once | one fixed key per send; the robot refuses a key it has seen; a retry reads before it sends | refuse |
| Proof | the same cursor read shows one new row from Clara's user with our words; a 200 is never "sent" | red row = failed, final, alert |
| Hygiene | warm session; never log in or mark read; no STOP on the number; 910 characters | refuse, 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 Fede | Options | Recommendation |
|---|---|---|---|
| D1 | Where the door is set | a) per company, per channel · b) one switch for both · c) per property | a: Situs wants one setup; text and email need separate proof |
| D2 | Which lanes it covers | a) every lane · b) per family | a: one mode for every customer |
| D3 | Guest card or thread | a) thread with the guard · b) guest card | a |
| D4 | AppFolio door down | a) hold to the use-by, alert, decide again · b) send from our line · c) park for a person | a: b is the surprise second number; c is the queue you ruled out |
| D5 | A person typing in PropFlow | a) same resolver and outbox · b) only inside AppFolio | a: one writer per door |
| D6 | Send timing | a) right away plus a 15-minute sweep · b) the mirror's bands | a: a reply must not wait 30 minutes at night |
| D7 | Situs text follow-up before the Willows proof | a) wait · b) start on Clara's number, move later | a: 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.
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)
| Outcome | Phones | Share |
|---|---|---|
| Resolves to a thread | 45 | 35% |
| Held: one person on file twice at the same building (applicant + tenant) | 74 | 58% |
| Held: other two-person cases | 7 | 5% |
| Held: no AppFolio thread | 2 | 2% |
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):
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 recipients | Today | After the three fixes |
|---|---|---|
| Sends through AppFolio | 12 (29%) | 35 (85%), plus up to 6 more once their guest cards exist |
| Held: front-door property | 12 | 0 |
| Held: no AppFolio thread | 17 | 0 (thread started from the guest card) |
| Held: same person twice | 1 | 0 |
| 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.
| Check | Result | Evidence |
|---|---|---|
| Robot sent and read back the text | Not reached: refused before the send | 3 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 id | Held, not sent | outbox record harness.start-thread.willows.2026-10-17, state held |
| Mirror files it as Clara | Nothing to file | no text was sent |
| Zero texts on Clara's own line | Pass | Twilio: 0 messages to the test number since Oct 17 |
| No AppFolio login | Pass | robot log for the window: 3 card reads only |
| Request cost about 20, not about 220 | 12 (reads only) | JP&Co day counter 0 → 12 on Oct 17, no mirror tick between |
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.
| # | Scenario | Expected | Observed | Result | Control |
|---|---|---|---|---|---|
| A | Tenant texts the Willows line in AppFolio | Filed as tenant within 60 s; Clara not woken | Filed 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) |
| B | A teammate replies in AppFolio | Filed once as an office reply; no wake | Filed once as an office reply over 3 ticks; 0 Clara replies | Pass | Fired (teammate's id in Clara's set) |
| C | Clara's send through AppFolio, read back by the mirror | Filed once, as Clara | Filed 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 thread | Pass | Fired (old seat set) |
| C2 | A Clara row stored before the seat fix gets relabelled | Reads as Clara once the fix is live | Stays "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: #19404 | Fired |
| D | Duplicates, out-of-order rows, AppFolio ignoring the cursor, overlap page read again | Every text once, in time order | 1/1/1/1 copies, in order. Live: every tick re-reads the overlap page (8 duplicates) and files none twice | Pass | Fired (store without dedupe) |
| E | Robot 500, 502 with stale-cookie wording, 110 s timeout, AppFolio 429, tick past its deadline | Tick records the failure, no refresh or login, next tick recovers | All 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 recovered | Pass | Fired (retry, refresh, no deadline) |
| F | Cold session, twice in a row | Watch pauses, never logs in | Paused both times, 0 login or refresh asks, recovers once warm. The code sends the read with "never refresh" set | Pass | Fired |
| F2 | A database that stays cold alerts someone | An alert after repeated cold ticks | 5 cold ticks: only the tick log records them. No error, no Sentry event, no alert | Fail: #19405 | Fired |
| G | Send with no confirmation | Read-back decides; never a second send | Send 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 post | Pass | Fired (22 of 22) |
| H | Does the background thread read mark texts read? (#19292) | Unread state unchanged | Thread 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) | Pass | Not applicable (live read) |
| I | 20-minute live soak plus the full day | Steady ticks, 0 logins, 0 refusals | Day: 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 logins | Pass | Not applicable |
| J | The page as Fede sees it (light, dark, phone) | Right labels, right order, no other company | Order 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 yet | Pass, one open question | Not applicable |




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.
| Text | AppFolio stamp | Mirror saw it | Filed | Clara silent |
|---|---|---|---|---|
| 1 (held before the fix) | +3 s | 2 h later, after deploy | +1 s | yes |
| 2 (held before the fix) | +3 s | 2 h later, after deploy | +5 s | yes |
| 3 known prospect | +2 s | +52 s | +57 s | yes |
| 4 right after a tick (worst case) | +1 s | +60 s | +63 s | yes |
| 5 burst, first | +1 s | +28 s | +29 s | yes |
| 6 burst, 3 s later | +2 s | +22 s | +26 s | yes |
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.
Proposed — pending Fede Oct 13. Nothing built or sent.
"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 8c | Before | After |
|---|---|---|
| The signal | Is a person already replying in this thread? | The whole guest card: who created it, notes, activity log, every communication |
| How Clara decides | Fixed rules | Hard rules plus a reasoning layer |
| Where she starts | From scratch, following up until a tour | Mid-journey: a showing already happened, or staff text Clara to add the card |
| A person is on it | Quiet in that thread for 4 h | Picks up where the human left off; never interferes |
| Follow-ups | A timer sends the next scripted follow-up | The 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.
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 turn | Stops two voices | Reuses | No answer in 24 h | Slowest 10% wait | Both answered (ceiling) |
|---|---|---|---|---|---|
| Today, office only | — | — | 22% | 41 h | — |
| A. The clock: 4 h after the renter's message | the turn, and the read before a send | escalation gate, 8a's read, delivery status, watch tick | 0% | 4 h | 15% |
| B. A person: an escalation released | the same, no clock | the "human replied" hold, the release | 22% | 40 h | 0% |
| C. Whichever comes first (recommended) | as A | A, plus the release | 0% | 4 h | 15% (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:
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.
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.
| Time | What happened | Who knew |
|---|---|---|
| Tue 1:26 PM | Anthropic 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 PM | First alert in #alerts: sign-in lock tripped. | Engineering |
| 2:11 PM | Our automatic triage names the dead key and raises it for a person. | Engineering |
| 2:36 PM | Second alert: work-order lookups refused. | Engineering |
| 3:09 PM | A change stops one alert per work order. A symptom; the key stays dead. | Engineering |
| Wed 12:59 AM | A 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 AM | Two more alerts name the key. Triage tests it live, rejected, and raises "rotate the key" again. | Engineering |
| about 5:20 AM | The day's sign-in allowance is used up. | Robot log only |
| 9:26 AM | A person with console access swaps the key. | — |
| 9:30 AM | A waiting guest card is refused: no sign-ins left until 10:17. | Robot log only |
| 9:38–9:46 AM | The three no-home emails (a separate gap; the tours were booked after the swap). | Western Slope |
| 10:18–10:20 AM | First try hits a redirect loop; the robot resets its browser by itself and signs in. | — |
| 10:30–10:31 AM | The 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.
| Company | Emails from the outage | "Showing not recorded" | "Guest card not created" | Late in AppFolio |
|---|---|---|---|---|
| Western Slope | 0 | 3 (no-home gap) | 0 | 11 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 Group | 0 | 0 | 0 | Robot also locked out; nothing was waiting |
| JP&Co, ConAm | 0 | 0 | 0 | None; JP&Co's robot stayed signed in |
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 AppFolio | Saved first? | In a 21-hour outage | Who hears | Replay |
|---|---|---|---|---|
| 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 queue | waits, retrying for 72 h, then parks | engineering, only at the park; client staff for a new lead's card at the park, and at once for a showing with no home | an engineer's script, one write at a time |
| Linking a new card (waits for it to appear) | yes | gives up after 10 min | engineering | a replay re-parks at once |
| Work orders: create, notes, edits, vendor, close | yes, on the work order | kept, but nothing re-sends them; a close not sent within 24 h is reverted by our work-order sync | engineering; a dashboard notice | the next message or edit, or an admin flush |
| Handyman set as the vendor; voiding a renewal after the office picks month-to-month | no, a direct call | dropped | nobody (a log line) | none |
| Unpublishing a pre-leased listing | no, rechecked every 15 min | heals on the next run | engineering logs | automatic |
| Staff and admin buttons | no | fails at the click | whoever clicked | click again |
| Dark today (off at customers) | ||||
| Clara's texts into the AppFolio inbox | held when they fail | held for good; only "busy" holds are retried | engineering, once per outage | "busy" holds only |
| Renewal offers | yes, with a retry job | heals about 3.5 h after recovery | property manager email and text at 24 h | automatic |
| Lease draft on approval | no | stuck for good | office email | none |
| Turnover work orders, move-out charges | yes, with a sweep | given up for good after about 18 min | engineering: "file manually" | a person clears a flag |
| Holdover month-to-month | no, a daily run | heals the next day | engineering, #alerts | automatic |
| Term changes, move-out drafts, listing publish, post-call guest cards, messages through the card thread | no | dropped | engineering logs at most | none |
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 works | A failure email to a client | The next new sender | Cost | |
|---|---|---|---|---|
| A. One list, checked at the door | every staff email names its kind; a kind not marked safe for clients is refused | blocked when it runs | only if its author labels it right | small |
| 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 fails | covered | medium: move the 19 senders |
| C. A filter on outgoing email | scan staff email for failure wording | mostly blocked | no: wording drifts | small |
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.
| Pattern | In plain words | Do we have it? | What today showed |
|---|---|---|---|
| Save first, send later (outbox) | the write is saved with the change that caused it; a worker sends it | yes, the guest-card queue | 0 lost |
| Retry with growing gaps, same id | try later, further apart, never a double | yes | kept trying a key we knew was dead |
| Breaker with a test try | stop calling a broken system; one cheap try decides when to resume | a sign-in lock, no test try | doomed tries spent the allowance |
| Hold and replay (dead letter) | work that can't go now waits, then replays | parked writes, an engineer's script | nothing replays by itself |
| Durable workflow (Temporal) | each write is a program that survives restarts and waits as long as needed | for tours and follow-ups, not AppFolio writes | — |
| Reconcile sweep | compare what we meant to write with what AppFolio has | for listings only; turnover's is dark and quits after 18 min; work orders have none | — |
| Key health check | test every key on a clock, not on first use | no | dead 20 hours before the swap |
| A. Harden our queue | B. Hold pile, an agent replays | C. A durable workflow per write | |
|---|---|---|---|
| The idea | every AppFolio write goes through the queue; during an outage writes wait and never expire; recovery wakes the queue at once | parked work collects; after recovery an Opus agent reads each item and replays, skips or escalates | each write is a Temporal workflow that sleeps until a "robot is back" signal |
| Good | small steps on code that held today; predictable | judgment for odd cases (the tour already happened) | durable by design; full history |
| Costs | about a dozen direct-call writes must move in | slower, costs tokens, can be wrong | a 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:
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 lost | All writes in, after the fix | Customer messages | Engineering alerts per outage | Recovery summary | Duplicates | Lines changed | Defects caught | |
|---|---|---|---|---|---|---|---|---|
| Target | 0 | 2 min after sign-in | 0 | 1, plus escalations | 1 | 0 | fewer is better | 5 of 5 |
| Today (the real incident) | 0 | 65 min (21 h from the fault) | 0 from the outage; 3 from the no-home gap | 4 lines and 2 asks, no escalation | 0 | 0 | — | — |
| Prototype A | ||||||||
| Prototype C | ||||||||
| B's agent, dry run |
Prototypes stay local until Fede picks; then each change climbs the testing ladder to Willows.
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.
Code read at origin/main, Oct 14 (read-only). Line numbers drift; search the named function if a line has moved.
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).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).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).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.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.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).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-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).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).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.
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 marked | In | Out | Who sends the outbound |
|---|---|---|---|
| Texts on the AppFolio line, prospects only | 4,015 | 5,808 | named 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 only | 95 | 675 + 131 automatic | named staff 512; the shared login 163 |
| Calls logged on cards | 264 | staff | |
| Shared leasing mailbox, all mail | 13,765 | 201 (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 emails | 290 texts, 204 emails | Clara 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) | 813 | of 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.
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).
| Number | What it says | What 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 steady | a 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 login | most of the inbox is outbound and not a conversation | the 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 days | the 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 3 | the 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 8 | AppFolio's own identity is thin | identity 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 text | residents at Western Slope do not open maintenance by text; the office pushes updates out by text | Fede'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 failure | the 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's | the signature is the property's owner, not the manager | attribute by property, never by signature or by database (trap 65) |
| At least 6 of 100 last messages touch a sensitive personal matter | the mirror holds sensitive text | same 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 Mountain | the inbox is an office-hours channel | the 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.
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.
| When it arrives | Share of inbound texts | Office answers in (median) | No staff text within 24 h | The tick |
|---|---|---|---|---|
| Mon to Fri, 8 am to 5 pm | 84% | 8 min | 19% | every minute |
| Mon to Fri, evenings and overnight | 8.6% | 13.8 h | 31% | 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 Sunday | 7.2% | 70 min | 30% | the weekend's own curve |
| Deepest night, 10 pm to 7 am, any day | 0.9% (37 texts a year) | 10.3 h | 40% | 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 5 | 20 s | 8% | 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.
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.
| # | Trap | Customer sees | Mitigation | Phase |
|---|---|---|---|---|
| 1 | Loading 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 |
| 2 | Per-building polling (today's feed). | Throttling, then every AppFolio job stalls. | One list read per customer per tick; delete the per-property path. | 1 |
| 3 | The 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 |
| 4 | 429 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 |
| 5 | Session 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 |
| 6 | Cursor 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 |
| 7 | Edited or deleted messages in the PMS. | Stale or duplicate bubbles. | Key by id, store a content hash, update in place. | 1 |
| 8 | Time 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 |
| 9 | Email bodies: quoted history, signatures, HTML; MMS images. | Walls of text, missing photos of the leak. | Strip quoted history, keep raw, store attachment references. | 1 |
| 10 | Bulk 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 |
| 11 | Shared logins ("Assistant PM", "Clara PropflowAI") used by humans. | Wrong attribution. | Label by login name; never infer human or robot from it. | 1 |
| 12 | Wrong person: one number, many parties. | A cosigner's text on the tenant's thread. | The PMS party wins; ambiguity recorded; household link. | 1 |
| 13 | Former prospect, now tenant at another building. | Two people, split history. | Identity merge by PMS ids and claims; one person, thread per property. | 1 |
| 14 | Unmapped 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 |
| 15 | Tenant created by the mirror, then created again by the rent-roll sync. | Duplicates everywhere. | Both create paths match by PMS tenant id first. | 1 |
| 16 | Duplicate 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 |
| 17 | Backfill 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 |
| 18 | The 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 |
| 20 | A 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 |
| 21 | Clara'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 |
| 22 | Prompt bloat from two years of history. | Slow, costly, worse replies. | Window plus summary (section 6). | 1 |
| 23 | Touch 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 |
| 24 | The 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 |
| 25 | A 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 |
| 26 | Owner and vendor parties in the inbox. | Dropped silently. | Pending with reason; counted on the customer page. | 1 |
| 27 | Robot 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 |
| 28 | PII 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 |
| 29 | Two doors, two replies. | Clara answers twice. | Rule R6: thread lock, one reply per burst. | 2 |
| 30 | Staff and Clara answer at the same moment. | Two voices. | Just-in-time thread read before every send; "staff engaged" window. | 2 |
| 31 | Opt-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 |
| 32 | Staff promises in context. | Clara repeats or contradicts "I'll send it tomorrow". | Prompt rule plus eval; promises stay untracked. | 2 |
| 33 | No 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 |
| 34 | PMS send fails, Clara falls back to her number. | New-number surprise. | Queue, retry, tell the office; never fall back silently. | 3 |
| 35 | Yardi 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 |
| 36 | Polling 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 |
| 37 | A 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 |
| 38 | STOP 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 |
| 39 | Household 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 |
| 40 | Unit labels differ between systems ("Unit 8" vs "8"). | Wrong unit, or no match. | Join on unit id. | 1 |
| 41 | Ties at a page break skip a row in the list walk. | A thread missing from the backfill. | Overlapping pages, dedupe by id. | 0, 1 |
| 42 | A 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 |
| 43 | The 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 |
| 44 | Two session probes per robot call double the request count. | Budget spent on probes. | One probe, none within a minute of a success. | 1 |
| 45 | The 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 |
| 46 | Willows 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 |
| 47 | Today'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 |
| 48 | The 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 |
| 49 | Role-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 |
| 50 | Stored 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 |
| 51 | Misfiling 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 |
| 52 | A 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 |
| 53 | The 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 |
| 54 | AppFolio 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 |
| 55 | Failed 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 |
| 56 | Photos 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 |
| 57 | Mirror 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 |
| 58 | A 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 |
| 59 | Replay 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 |
| 60 | Onboarding 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 |
| 61 | The 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 |
| 62 | Echo 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 |
| 63 | A 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 to | 26 of the newest 100 Western Slope threads end with a blast and 28 with a notice; only 16 end with a staff-written message | the mirror types every message (person, staff, bulk, system, Clara); only a staff-written message counts as an answer | |
| 64 | The label says "Assistant PM" for a message Kat wrote, because the office shares one login | 34 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)" | |
| 65 | A 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 database | 17 automatic notices in the Western Slope database carry an owner entity's name | attribute by the property on the row, never by a signature or by the database alone | |
| 66 | The mirror shows a photo-less thread for a text that was only a photo, or a staff "sent you a picture" with nothing attached | the AppFolio feed drops media and text-less rows today (parser lines 61-115 and 218-227); the inbox probe cannot see media on the list | phase 1 carries media (keys, types, the AppFolio attachment id) and stops dropping text-less rows; the inbox shows a photo marker | |
| 67 | A 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 |
| 68 | A 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).
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.
| Phase | Minimum slice | Switch | Proof before the next step |
|---|---|---|---|
| 0 · Design, canaries, the read | This 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 PR | Request 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 switch | The 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 email | Capture the email row shape first, then decide; nothing promised (tenants already email the leasing mailbox, which is a door today). | same setting | Same 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 off | Replay 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 PMS | Scope 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 2 | Echo 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 setting | A clean supervised week per lane; the monthly re-measure on this page. |
| 5 · Second PMS | A Yardi (or Entrata) adapter: session, list, thread, ids; capture first. | none new | Built 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).
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.
| Group | Scenarios | What must hold |
|---|---|---|
| Adapter | every 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 captured | nothing dropped; bulk stays bulk; a "System" or no-sender row is marked automatic; media keys carried |
| Watch | a 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 question | one 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 wall | two companies on one database; a row with no property; a re-read of the same page; a row already filed | only 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 placement | the ladder in order; a phone shared by two people; two active tenancies; an unknown number; a past tenant; a conversation that does not exist yet | one 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 |
| Rendering | the 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 |
| Engines | a staff text, a tenant text, a notice and a blast arrive on a mirrored thread; a history row lands | no 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 reached | exactly 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 Willows | the watch on the real line for a day; one real application-link send | request logs match the schedule; unread flags unchanged; the thread view shows the labels |
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).
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:
| Point | This page, v1 | Opus round 1 | Engines map | Verdict in v1.1 |
|---|---|---|---|---|
| Change signal | The customer-level list, keyed on the row id | Same; the brief's Option B (a portfolio report call, then card reads) watches the wrong thing: guest-card reports never see tenants | n/a | Agreed. The inbox list is the only change signal; Option B is dropped. |
| Who talks to AppFolio | "Cookie replay" from the sync runner | All AppFolio traffic inside the robot: one jar, one pacer, one lock | n/a | Opus is right; section 3 and decision 2 rewritten. |
| Is phase 1 inert? | "Clara reads, never replies"; a hidden fork on leasing follow-ups | Not zero change: staff texts stand follow-ups down; keep it, prove it | Nine engines react to stored rows alone, most by role, plus the wake and the hold if left wired | Both. 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 depth | 12 months, inert | 24 months stored, 90 days in the prompt | No-window readers see history unless told | 24 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). |
| Attribution | Assumed from the row | The robot drops the sender on thread reads, keeps the first party only, staff texts render "From mailbox" | Same | Phase 1 work, named in the adapter slice. |
| Read just before each follow-up (brief's Option D) | Part of the watch | Not needed with a cheap list poll; a phase 3 guard right before Clara sends through AppFolio | n/a | Agreed; moved to phase 3. |
| The forever-hold | Replace first, before phase 1 | Phase 1 passes no hold, so delete it in phase 2 | Permanent, too wide, exempts nothing transactional | Phase 1 stamps no hold (rule); the deletion is phase 2's first fix. |
| STOP across doors | Record in phase 1, enforce in phase 2 | Phase 1 rule, no switch; owner call on cross-line revocation | Legal; the keyword check has one caller, the Twilio webhook | Phase 1, no switch; cross-line revocation is decision 9. |
| Gera's "find them where they are and continue there as Clara" | Phase 3 | Phase 3 | No tenant send path exists; shared login | Phase 3, after the robot-only login and the tenant send route. |
| Prompt window | 30 days or 40 messages plus a summary | 90 days plus the last 20 | n/a | 90 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:
| Blocker | What was wrong on the page | Verdict in v1.4 |
|---|---|---|
| 1. No organization wall | One 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 login | The 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 false | Clara 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 renewals | R5 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 text | Section 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 exist | A 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 wrong | Holds 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.
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.
| Decision | Third pass | v1.5 |
|---|---|---|
| 1 Shape | Keep 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 Read | Agree; run the keepalive test before any phase 1 code. | Kept. |
| 3 Identity | Order 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 Backfill | 24 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 Channels | Texts first; capture the email row, then decide; promise nothing. | Taken; 1b is no longer promised. |
| 7 WS read | Agree; its numbers land before phases 2 to 4 are decided. | Taken. |
| 8 Phase 3 | Decide the scope after the year of data; until then a prerequisite list. | Taken. |
| 9, 10, 11 | Agree. | Kept. |
Four things nobody had raised, all taken:
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.