0064 — A PMS robot owns one session per account, through one core lease and one alert path; a vendor supplies only facts and screens
- Status: Proposed 2026-10-17. Rulings from a
fable-decide --concurround (receipts below). Build plan: appfolio-browser-agentdocs/planning/session-layer.md, slices S1–S7; none merged when this was written (ADR-0004: recorded at decision time). - Date: 2026-10-17
- Deciders: Gera asked for it (2026-10-15, after the jpco redirect-loop lockout): the fix must be expandable to other PMSs, a durable vendor-agnostic session-health layer, so a future PMS driver gets loop detection, fresh-context recovery, credential-only lockout accounting and #alerts reporting for free. Restated 2026-10-17 as a deliberate design pass "instead of accumulating patches", with Fable deciding the open questions. Fede owns the AppFolio robot's login flow and can object.
- Systems: the PMS robot (
appfolio-browser-agent:lib/core/*,lib/vendors/<vendor>/*, the heartbeat and refresh crons, every write route), PropFlow's Sentry project and its customer-impact rule to Slack #alerts, Smith's alert decider (ADR-0056 script-fix-pr), and any future PMS robot driver.
The problem
In one week the robot's login layer took nine distinct failures (catalog in the design doc): a dead model key broke every login; a self-redirecting login page locked jpco for ~12 h; the breaker charged failures nothing had proven; stale cookies on a host nobody cleared drove the loop; lockouts and re-mint failures paged Sentry only, or nothing (one path called an SDK that is not installed); a compiled-away in-page URL read sent every redirected login to the AI; two triggers logged the same account in at once and the loser reported a dead session; and the KV jar, the browser profile and an env var each claimed to be "the session". Each was patched where it surfaced. Every patch added its own exemption, lock or alert call, so the next failure found a new seam.
The common causes: nobody owns the session; four partial locks, none covering probe → login → verify → harvest → persist; instruments trusted without proof; screens matched by id on one host; alerts bolted on per path; recovery patched per incident.
The decision
- One session record per account, owned by the core. It holds state, jar, browser-context id, minted/verified times and the login epoch. Only the lease holder writes it; the browser profile is a cache of it. The context id comes from account config only.
- One lease covers the whole critical section (probe, login, verify, harvest, persist) and every login path goes through it. It fails closed. It returns a jar the winner proved warm to the winner and to every waiter, and each injects it into its own browser; the winner closes its browser before releasing. A waiter whose jar is still dead is "contended", never a failed login. The MFA lock stays as an inner fail-closed guard.
- Typed outcomes decide cost (appfolio-browser-agent #537): only a rejection of something we submitted charges the credential. Loops and instrument failures cost nothing but a short backoff.
- Instruments prove themselves. The robot reads page URL and HTML through CDP in one core reader, not through compiled page functions. An empty or unavailable read is an
instrument_failedoutcome and an alert, never a substitute value that answers a different question. - Recognition grows by capture. A driver recognises only screens it has captured; anything else goes to the AI with an
ai_fallbackalert, and ADR-0056 script-fix-pr turns it into a fixture PR. A core role-based tier is deferred until a second PMS driver is written or a capture shows a form the exact screens miss. - One recovery ladder in core: clear every vendor cookie but the keep-list, then a fresh context carrying only the keep-list, then stop and alert.
- One alert path. Session code reports only through one core emitter, which goes through Sentry with the customer-impact tag and a cause fingerprint to #alerts. Smith's existing decider (ADR-0056 script-fix-pr) is the consumer. No session alert may use another path.
- The driver contract. A PMS driver supplies only facts and scripts: its hosts, session-probe doors, cookie filter and keep-list, exact screen recognisers and signed-in marker, scripted steps per screen role, extra loop shapes, failure classification, and MFA delivery. It does not supply locks, accounting, recovery or alerting, and it may not implement its own.
Rulings (Fable, --concur)
Asked on one evidence snapshot (the design doc's sections 1-3); receipts in ~/.claude/jobs/fable/. "Agree" = Fable and Astra chose the same option independently and it held under option reversal (ADR-0019).
| Decision | Ruling | Receipt |
|---|---|---|
| 2 (serialization) | one core lease over the whole section, jar to winner and waiters; agree, high | fc44c9910 |
| 1 (source of truth) | KV record is truth, browser profile a cache; agree, high | fe6dd0c16 |
| 6 (recovery) | keep-list clear, then fresh context, then stop; agree, high | fb06c2c38 |
| 7 (alerts) | one core emitter through Sentry to #alerts to Smith; agree, high | fe0cb3928 |
| 4 (instruments) | first round flipped under reversal (needs-human, f02460e4b); re-asked with the evidence: CDP reads + no-substitute rule + lint on page-function guards; agree, high |
f304afb6c |
| 5 (recognition) | first round flipped under reversal (needs-human, f638a69bc); re-asked with the evidence: no role tier now; agree, high |
f30cb7b9f |
Both re-asks were new questions carrying the first round's disagreement, as ADR-0019 rule 5 allows; neither first-round question was re-asked as-is.
Consequences
- A second PMS robot is a driver file plus captured screens. It inherits the lease, the session record, the CDP page reader, loop recovery, credential-only lockout and the #alerts → Smith path on day one, and the design doc's conformance test (S7) proves it without vendor code. Its first uncaptured screens go through the AI with alerts until Smith's fixture PRs land, and writing it is the trigger to revisit a role-based tier.
- New alert kinds reach Smith through the path ADR-0056 script-fix-pr already proved end to end on 2026-10-17 (Sentry
JAVASCRIPT-NEXTJS-1JC→ #alerts → decider → PR). That same run opened a PR on the wrong diagnosis, which is the case for decision 4: the decider can only be as right as the instrument that raised the alert. - Risk: slices S1, S2, S4, S5 and S6 change live login behaviour for customers and each needs Gera's OK and live proof before merge. S1 turns script-first login on for every redirected login, a shape that has not run live.
- Revisit if a vendor's session cannot be represented as a cookie jar (token-only API, hardware key): decision 1's record would need a second shape.