Where Western Slope (and other new clients) still run a second implementation of something Camellia's running paths already do, and the order to merge each one back so there is exactly one path, everywhere.
2026-09-21 · Proposed — pending review · Fede's ruling, 2026-09-21: one code base for every client; Camellia's running paths are the reference; a second implementation of a capability (send, receive, connect, renew, book, first contact, write-back) is a defect to merge back — no old path running beside a new one.
On this page
Read against origin/main on 2026-09-21. "Corrected" rows were flagged uncertain in the first pass and verified against the live code afterward.
| Capability (plain words) | One path or two | What's different for Western Slope | What could go wrong for a customer | Merge-back size | Status |
|---|---|---|---|---|---|
| Connecting a client's mailbox and keeping the login fresh | Two | Western Slope's company-wide mailbox has its own connect & refresh code, separate from the original one-mailbox-per-building code | A one-time cleanup for an old, abandoned mail subscription only runs on the per-building path — a company mailbox could carry a stale subscription with nobody sweeping it | Medium | Queued |
| Watching for new mail (instant push vs. checking periodically) | One, extended | A mailbox that Microsoft won't let us subscribe to (a common shape for a company-wide inbox) now gets its own periodic checker, feeding the same inbound pipeline every other mailbox uses | Before this shipped (09-15/16) that kind of mailbox had no fallback and no "it stopped working" alert; needs a check that the alert exists for it too | Small | Shipped; alert parity unverified |
| Figuring out which building an email belongs to | Two | A company-wide mailbox has to match mail to the right building by address or name; a single-building mailbox never has to guess | A centralized client with more than one building that isn't in AppFolio has no real matching rule yet — mail could land nowhere | Small | Queued (both already feed the same processing step) |
| Clara's very first email to a brand-new lead | Two | The guest-card "first touch" email still uses its own separate sending code, not the shared sender every follow-up reply uses | A fix made to the shared sender (a hold rule, a template fix) silently never reaches a prospect's first email | Small | In flight — biggest near-term risk |
| Deciding a brand-new lead should get a reply at all | Converging | A new single "a lead arrived" signal now starts the guest-card first touch | Whether every other way a lead can arrive (a listing-site email relay, a phone call) is wired onto that same signal is unverified | Medium | In flight |
| Stopping the automatic follow-up texts/emails once a prospect replies | One, shared — corrected | None found — the function that stops the cadence is called from the one shared inbound-email decision code, and both the per-building mailbox and the company-wide mailbox already run through it | The earlier read of this ("unverified whether it works for a company mailbox") does not hold up against the code — it already works today | n/a | Verified working, not a gap |
| Calendar connect and tour booking | One, layered | A company-wide fallback calendar sits on top of the same per-agent calendar code; not a separate implementation | None found | Small | Shipped |
| Which phone/text line owns a conversation | One | Same resolver for every client | None found | Small | Shipped |
| Writing the outcome back to AppFolio (assign the card, add a note) | Two live — corrected | A newer, safer writer exists and is already wired in for real, but every company still ships in "watch only" mode by default; the original writer is what actually writes for every real customer today | The two writers share the same duplicate-prevention id, so turning the new one on for a company can't double-write — but a fix made only to the new writer doesn't reach a real customer until its company setting is flipped, and the two pull requests that retire the old writer aren't merged yet | Large | In flight — second-biggest risk |
| Company-wide vs. building-specific knowledge | One schema | — | Unverified whether a company-wide fact overrides a specific building's fact when both exist | Unverified | Unverified |
| Company-wide settings (which team books tours, who owns a lead, etc.) | One | Already consolidated 2026-09-19/20 | None found | Small | Shipped |
agents/clara/lib/email/inbox-client.ts (getValidEmailToken) vs. src/lib/data/dynamo/channel-attachment.ts + src/lib/integrations/microsoft/fresh-channel-token.ts.lambda/outlook-subscription-renewer/handler.ts (processProperty/processOrgMailbox) plus the new lambda/company-mailbox-poller/handler.ts and src/lib/domain/email/company-mailbox-poller.ts — both hand off to the same deliverInboundEmail seam.src/lib/integrations/email/org-mailbox-inbound.ts (resolveMailboxScope) — both this and the per-property webhook funnel into processInboundEmailMessage in agents/clara/lib/email/process-inbound-message.ts.src/lib/domain/leasing/clara-guest-card-first-email-send.ts still does not import the shared src/lib/domain/leasing/send-clara-prospect-email.ts as of 2026-09-21 — confirmed by reading its imports directly.maybeSignalProspectReplyByEmail (src/lib/temporal/leasing-reply-bridge.ts:204) is called from decideInboundEmailMessage (agents/clara/lib/email/process-inbound-message.ts:~1954), which processInboundEmailMessage calls for every inbound message regardless of which mailbox delivered it.src/lib/domain/leasing/clara-guest-card-first-email-send.ts:560 only after Clara's own guest-card first email is delivered. Consumers: propflow-record-consumer.ts (calls onProspectEngaged, which starts the follow-up cadence, not stops it) and pms/side-effects/register-dispatcher.ts (the AppFolio write-back dispatcher). It is not the signal that stops cadence on a prospect's reply — that is the separate maybeSignalProspectReplyByEmail path above.src/lib/domain/pms/side-effects/dispatcher.ts calls resolvePmsSideEffectMode (mode.ts:76) and is registered on the cron drain (src/app/api/cron/lead-received-outbox-drain/route.ts) — it is live, not dark; the stale "NOTHING READS THIS YET" text lives only in the admin route's own comment (src/app/api/admin/companies/[id]/pms-side-effect-mode/route.ts:13). Both the dispatcher's adapter (appfolio-adapter.ts) and the original inline writer (src/lib/domain/leasing/guest-card-write-back.ts / guest-card-write-back-outbox.ts) key off the same door-derived ids (gc:assign:<inquiryId>:<target>, gc:append_note:...), so a redelivery or an early "apply" flip cannot double-write. Absent an explicit company setting, every company still defaults to shadow (compute + log, write nothing) — the inline writer is what writes for real today.Curated from a pass over pull requests merged into main in that window that touch mailbox connect/receive, guest-card intake, first email, follow-up cadence, calendar/tours, phone/SMS, or AppFolio write-back — not every merged pull request in the window, only the ones that changed which of these paths exist. Verdict is based on the diff, not the title.
| Date | What happened | Verdict | Still alive on main today |
|---|---|---|---|
| 2026-09-15 | A guest-card mailbox-choice setting (model + writers/backfill) shipped, then both halves were reverted the same day | Added, then removed | No |
| 2026-09-15–16 | A periodic mail-checker for company-wide mailboxes that can't get a push subscription (the poller Lambda), landing beside the existing subscription-based checker | Added a second path, by design — both feed the same intake code | Yes |
| 2026-09-18 | Clara can now write from a company's connected Gmail inbox, alongside the existing Outlook path | Added a second provider, not a duplicate of the same provider's logic | Yes |
| 2026-09-18 | A guest-card-specific email adapter for syndicated guest cards, separate from the shared prospect-email sender | Added a second path beside send-clara-prospect-email.ts | Yes |
| 2026-09-18–19 | Guest-card write-back outbox/seam work (note+assign doors, outbox writer) built toward a shared writer, ahead of the AppFolio side-effect adapter program | Extended toward one path (not yet finished) | Yes (dispatcher side dark by default) |
| 2026-09-20 | The AppFolio side-effect adapter, its dispatcher, and the company-level shadow/apply setting (agent-loop decoupling program, issue #10344) | Added a second AppFolio writer beside the original inline one, dark by default (shadow) | Yes, both writers exist |
| 2026-09-20 | The sync Lambda stopped calling Clara's reply code directly and now drops a queued event instead | Merged two call shapes into one (a producer/consumer split, removing the direct call) | Yes |
| 2026-09-21 | A fix so new-lead events carry the company's own building id instead of one derived from the AppFolio number | Extended existing path (bug fix, not a new one) | Yes |
agent-loop-decoupling-2026-09.html.This table is a curated subset, not every pull request touched in the window — the search window returned several hundred merged pull requests across this repository's fleet of sessions; only the ones that changed which implementation of a capability exists are listed.
Smallest and riskiest first. Each item below is meant to be one pull request, roughly 300 lines, dark (no customer-visible change), safe to ship on its own.
send-clara-prospect-email.ts function instead of its own send block.Re-architecture items noticed during the week that don't yet have a home above. Empty for now.
Decided — option C (Fede, 2026-09-21 ~1:50 PM MT): AppFolio owns the stages it records; PropFlow fills inquiry and tour when the card only says Active. Building. · Origin: a Western Slope prospect with a tour Clara booked showed as "Inquiry" all day (Slack #tickets-bugs, 2026-09-21).
| Company type | Stage source | Tour visibility |
|---|---|---|
| AppFolio-owned (e.g. Western Slope) | Guest card status only, an activity feed with no tour value (status.ts:18-24,48-58; map in prospects-from-mirror.ts:124-133) | None on the list row; tour is stamped on PropFlow's own inquiry record, never joined in (apply-tour-intent.ts:1043-1047) |
| AppFolio-owned, legacy fold | Applied/approved/lease-signed from AppFolio's applications and lease reports get discarded when the row folds onto a card (prospects-merge-native.ts:262-296) | Survives only if the row stays standalone (prospects-merge-native.ts:343-357) |
| PropFlow-owned | Own stage field, monotonic — never regresses once past Inquiry (statuses/prospects.ts:78-97; sticky writer pms/writers/guest-card.ts:825-830) | Tour is part of the same stage progression |
| Option | What changes | What breaks | Effort | Verdict |
|---|---|---|---|---|
| A | Leave stage as AppFolio's; add tour as its own fact on the row (next tour date/state), joined in from the tour record the same way other joins already work | Nothing; the funnel's "upcoming tours" count still won't count Western Slope tours | ~250 lines, one PR | Do this first |
| B | Merge stage by "furthest step reached" everywhere | Reverses a decision made 2026-09-20; an AppFolio "Inactive" could no longer close a lead PropFlow thinks is further along | Small diff, large risk | Not recommended |
| C | One stage model: AppFolio's card can still set applied/approved/rejected/lease-signed/not-interested; when it just says plain Inquiry, PropFlow's own tour record fills in the tour step (reusing a rule already in place elsewhere) | Nothing new; fixes the tour count and the funnel math for AppFolio-owned companies | Two PRs after A, ~200 lines each, dark by default | Recommended, after A |
Separate initiative: parse the tour events already sitting in AppFolio's card activity log so tours a staff member books by hand show up too, not just ones Clara books.
Proposed — pending review (2026-09-21) · At Western Slope, a staff member added a showing on a guest card by hand and AppFolio moved that lead into the touring stage on its own. Clara books tours today but never creates the matching AppFolio showing, so the client's own calendar in AppFolio misses every tour Clara books. Fede asked for Clara's tours to show up there and stay current through reschedules and cancellations.
Tested headlessly against the Willows test property only, on a fictional test card, 2026-09-21. Nothing at Western Slope or Situs was touched.
| Action | How AppFolio does it |
|---|---|
| Create | The guest card's "Schedule a showing" form; posts unit, start/end time, assigned staff, guest card id, and notify flags; returns a showing id |
| Reschedule | Same fields, sent as an update to that showing id |
| Cancel / no-show | A status change on that showing id (Scheduled → Completed once the time passes, or No Show / Canceled — Canceled is final and shows a "SHOWING – CANCELED" badge on the card) |
Three things shape the design. AppFolio texts or emails the prospect itself when a showing is created unless the notify flags are off — ours must always turn those off, since Clara already confirms and PropFlow is the communication layer. AppFolio does not stop a duplicate; a second identical showing was created with no warning, so we save the returned showing id on our tour and never create a second one. A unit is required, and every time is the property's local time.
Today's gap: our existing AppFolio guest-card write queue already has a "cancel tour" kind, but it only adds a note — nothing creates, moves, or cancels a real showing. We also don't read AppFolio's own Showings report, so we can't see a showing staff add by hand, which is why a tour Clara confirmed at one time and a showing staff added at another time conflicted on 2026-09-21.
| Step | What it adds |
|---|---|
| 1 | Showing actions for the AppFolio robot: create, reschedule, cancel — notifications always forced off |
| 2 | Three new kinds in the existing write queue: schedule showing, reschedule showing, cancel showing; the showing id gets saved on the tour |
| 3 | Tour booked/rescheduled/cancelled events feed the queue, in a separate process from Clara's reply loop |
| 4 | Read AppFolio's Showings report to verify our own writes and pick up staff-added showings, so Clara never double-books |
Each step extends the one existing guest-card write queue — no second path. Ships off by default as a company setting on the customer page; Fede turns it on per customer.
When staff add a showing by hand for a prospect Clara already booked: (a, recommended) treat the staff showing as the truth — update Clara's tour to match and tell the prospect; (b) keep Clara's time and alert the team to the mismatch; (c) do nothing until step 4 ships.
Update 5:30 AM MT, 9/22: the trigger is now registered (#10716) and the live Willows run (harness PR #10738, --live --showings) executed the real path. It stopped at the robot: the app sent the wrong field names (fixed in #10741), and creating a showing needs the AppFolio unit token we do not capture yet (#10742). Reschedule and cancel correctly parked with no showing id. No email or text went to anyone; outbound was suppressed. Status: blocked on #10742, not proven.
Infra confirmed deployed and live · live write proof still pending Fede's go · Everything up to the automatic hand-off is now built, merged, and running in production for Willows: the company setting is on, the write queue and AppFolio writer exist, and the trigger that watches Clara's tours and would fire those writes is registered and enabled. The one thing that has NOT happened is an actual book→reschedule→cancel against the real AppFolio account — that needs Fede's explicit go, already asked, still waiting as of this update.
The setting, in plain English: it's called "AppFolio showing sync" (the code name is Organization.pmsShowingSyncEnabled). On the company's admin page (/admin/companies/<company id>) it's a plain on/off switch for that one company. Read directly from the live database just now: it is ON for the real Willows company only — internal org id org_sandbox (AppFolio property appfolio-45, "The Willows"). Off everywhere else.
| Claim | Evidence |
|---|---|
The real Willows is org_sandbox / AppFolio property appfolio-45, not a decoy fixture | Read live: the Property row for appfolio-45 ("The Willows") carries organizationId: org_sandbox. A migration script's own comments independently confirm the same fact — appfolio-45 is Willows and its home org is the sandbox org, not the JP&Co/Camellia customer org (org_jpco), which a naive property-name match could have confused it with. org_jpco was checked too and does not have the setting on. |
| The setting is on for exactly one org | Queried every Organization row live via DynamoDB (7 total): only org_sandbox has pmsShowingSyncEnabled: true. All others (JPCO, Western Slope, Western Slope Mirror, two test companies, PropFlow Staff) read false/unset. |
| The write queue, AppFolio writer, and browser-robot routes are merged and deployed | All MERGED: propflowai #10665 (company setting), #10673 (schedule/reschedule/cancel_showing outbox kinds), #10675 (showing-sync producer), #10678 (setting + outbox + producer, combined), #10716 (registers the stream trigger); browser-agent repo #411 (the three AppFolio robot routes: create/reschedule/cancel showing, notify flags forced off, readback-verified). |
| The automatic hand-off (tour booked/rescheduled/cancelled → write queued) is now really wired up, not just code that exists | Checked live against AWS, not just the code: the DynamoDB Streams trigger is registered and Enabled — event-source-mapping id 08a805d3-7609-4de9-ad12-e5f63a3d4d39, function propflow-showing-sync-stream-consumer-prod:live, filtered to dynamodb.NewImage.SK prefix TOUR#, on the production table's change stream. This was the missing piece in the previous version of this proof (PR #10716 closed it). |
| Live write proof (book → reschedule → cancel against real AppFolio) is PENDING Fede's explicit go | Asked in #agent-trinity 2026-09-22 ~04:20 Mountain (message: "Need your go: for #10661 ... To prove it actually works I need to BOOK, then RESCHEDULE, then CANCEL a real Showing on the live Willows AppFolio account"). Not yet answered as of this update. No real Showing has been created, rescheduled, or cancelled as of this writing. Nothing in this row was run. |
| No ZZ Harness prospect exists yet at Willows to run that proof against | Checked live: no prospect named "ZZ Harness" (or containing "harness"/"zz") exists among Willows's 113 prospects. None created today either — the newest Willows prospect rows predate September. A live proof would need a fresh card, not a reused one. |
| Scope stayed inside Willows only, read-only | Every read above touched only org_sandbox / appfolio-45 and the deployed infra's own state. Western Slope and Situs Group were never queried. No write of any kind was made to AppFolio, DynamoDB, or the outbox. |
The other builder's test fixtures for this feature (branch fede/guest-card-write-harness, read-only, not touched, informational context only — not something this proof used or will use): three fixtures already exist and are marked not-runnable — gc-harness-showing-schedule, gc-harness-showing-reschedule, gc-harness-showing-cancel in scripts/appfolio-guest-card-harness/fixtures.ts, written to use the exact same outbox kind names this feature shipped with (schedule_showing / reschedule_showing / cancel_showing) precisely so they can flip on later "with a rename/no-op, not a rewrite." Each carries liveDrivable: false and a note that a separate builder session owns this surface — flipping a fixture to real is a matter of that session setting its liveDrivable field to true once it decides to drive a real write; deliberately left to that session, not this proof.
What's left: nothing more to build for the happy path — infra is fully deployed. The only remaining step is Fede's go on the live write test itself (book → reschedule → cancel one real Showing at Willows), after which the same already-on setting means the very next tour booked at Willows shows up as a real Showing with no further switch to flip.
Proposed — pending review · Western Slope's first real leasing leads went live 2026-09-21. See the checklist this fed on the onboarding playbook: docs.propflowai.co/a/clara-onboarding-playbook?s=next-client-checklist.
One prospect's three days on that first live day, traced end to end with every miss and its root cause: docs.propflowai.co/a/rca-western-slope-ethan-2026-09-21.
| Assumption | What actually happened |
|---|---|
| A proof on Willows or Camellia proves Western Slope. | Western Slope is a different shape: single-family homes, one company mailbox, one company phone line. The copies of the path that only a new client this shape exercises were never run before go-live. |
| A conversation belongs to a home. | Fede's ruling 2026-09-21: a conversation belongs to the person. At a scattered-site company it is held at the portfolio level — every inquiry and every home that person asks about flows into that one conversation, no duplicates. |
| A setting on the property is enough. | Clara's AppFolio user, her email signature details, and the home's time zone are all company facts. Typed per property instead, they were simply missing on 69 homes. |
| The follow-up writer does not need to know about the home. | It invited a prospect to tour a home Clara had already told him was rented, because it has no availability data and only reads the conversation in one stage. |
| An unknown inbound email can be skipped quietly. | A skipped reply from a real prospect raised no alert. Nobody knew it happened until it was found by hand. |
| A failed write to AppFolio can just park. | A parked write has no door back. It needed a hand fix to recover. |
| If Clara gave a full reply, the team note can be dropped. | Clara told a prospect the property team had all his questions, and the system dropped the team note because her reply looked complete. Nobody on the team was told the prospect still needed a person. |
| The agent's copy of the property reader is the same as the dashboard's. | It is a fork with no company filter. An exact address, "566 Placer Street", matched two rows across companies (the real home and a sandbox mirror copy), three tools failed as "ambiguous", and Clara told the prospect she could not find the home. |
| AppFolio's card status can drive every stage on the Prospects page. | AppFolio has no tour status (Pre-qualified, Active, Waitlisted, Application completed, Approved, Denied, Converted). A prospect with a booked tour showed as "Inquiry" for the whole day. |
| Change | Why | State |
|---|---|---|
| One conversation per person, per company. | So a person asking about several homes at a scattered-site company is one thread, not one per home. | Decision made Inventory and diagrams published: docs.propflowai.co/a/agent-loop-decoupling-2026-09?s=conversation-belongs-to-person. Design not started. |
| Company mailbox replies route to the person's thread. | So a reply to Clara's email is never filed under a catch-all property and skipped. | In review |
| Company-level connector settings for Clara's AppFolio user and her email signature. | So both are set once for the whole company, not missing on every property that isn't Willows. | Merging |
| Follow-up cadence checks the home's status and Clara's last message before inviting a tour. | So a follow-up never invites a tour of a home Clara already said was rented. | Merging #10558 |
| The mirror carries the whole AppFolio card; our page renders from it. | Today the mirror copies a slice (name, contact, status, inquiry list). Max rent is copied and dropped before the page; bedrooms / unit type are fetched and never saved; notes and the Actions Log (with every showing) are downloaded and discarded. A prospect's page showed dashes for facts AppFolio had. | Open Budget + unit type PR in progress 2026-09-21; the full-card rule is the refactor. |
| One home resolver, shared by every channel and tool. | Listing id first, then exact address, then community name, tolerant of typos and partial addresses, always fenced to the caller's company. Today the agent, the email matcher and the dashboard each have their own lookup; the agent's ignores the company. | Open Company fence on the agent's reader: PR in progress 2026-09-21. The shared resolver is the refactor. |
| Tour stage is PropFlow's on a PMS-owned prospect row. | The PMS owns every stage it tracks; the tour stage is ours because the PMS has none. | Open PR in progress 2026-09-21. |
| Inbound text retry. | An aborted first attempt waited six minutes for the queue timer before retrying. | Merged #10546 |
| Alert when a real system-created card or a real prospect reply is skipped. | So a skipped reply is never silent. | Open |
| Parked AppFolio writes get a retry door. | So a parked write doesn't need a hand fix to recover. | Open |
| A team note is never dropped when Clara's reply says the team has it. | So a complete-sounding reply from Clara never causes a real handoff to silently disappear. | Open |
| Assign a guest card to a named staff member, and one-step pause of Clara for one person. | So handing a person off to staff is one action, not a manual, multi-step fix. | Mechanism being built, dark |
| Time zone inferred from address at import. | So a home never launches with no time zone and wrong quiet hours. | Handed to onboarding lane |
| Tour type setting (self-guided vs. staffed), per home or per company. | So Clara and the team never disagree with a prospect about how a tour works. | Needs Fede's go |
Ruling (Fede): "Think about the guest card arriving as a message. It's just very rich and has more information, but in the core system it is an input message that we're going to reply to. It doesn't matter what the transport mechanism is. We should know what it is because there's more structured information, but it should act the same as other channels." And: "If there's a book tour in the first turn, second turn, doesn't matter. It should be all consistent."
Today: since 2026-09-18 the guest-card first email has run through a separate composer with no tools (about 20 files). Engine behaviors — booking, home anchoring, escalation — were re-created there as hand rules, and some got missed: a tour got said but never booked, and the roster got listed next to the correctly named home. The card was originally routed through the engine (issue #9761); a later change pulled it back out.
After: a guest card becomes an inbound message on the person's thread, channel email, carrying the card's structured facts (property/unit id, listing site, move-in date, requested tour time, inquiry text). The engine takes its normal turn with its normal tools. Kept: the review door (hold and approve) and the AppFolio write-back note. Retired: the composer and its rails. Org derives from the property, never the card — the org stays the wall.
| # | Concern | Proof |
|---|---|---|
| 1 | Adapter | Card facts land as a normal inbound message on the person's thread |
| 2 | First-touch prompt overlay | Engine turn reads the same structured facts as a first-touch context |
| 3 | Route the consumer through the engine turn | Same review door as today, no bypass |
| 4 | Booking and home from engine tools; delete hand rails | Tour-said implies tour-booked; correct home shown, not the roster |
| 5 | Retire the composer | No-tools composer path removed |
| 6 | Extend existing harnesses with a four-dimension judge (right home / policies / calendar / escalation) and controls | Harness run with planted-violation controls, green |
| 7 | Sandbox redrive of every guest-card dataset, end to end | Full redrive in the sandbox org, published as the turn-on proof |
PR chain tracked on issue #11694, integration branch fede/guest-card-engine.
Acceptance (Fede, same day): "I want a sandbox environment where we redrive all the data sets we have for guest cards and make sure we are just not doing anything dumb and everything works end to end. After that's done, I want to turn it on." Deadline is this afternoon. Turning Western Slope on is Fede's press.