0064 — A PMS robot owns one session per account, through one core lease and one alert path; a vendor supplies only facts and screens

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

  1. 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.
  2. 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.
  3. 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.
  4. 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_failed outcome and an alert, never a substitute value that answers a different question.
  5. Recognition grows by capture. A driver recognises only screens it has captured; anything else goes to the AI with an ai_fallback alert, 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.
  6. 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.
  7. 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.
  8. 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