Conversation
`git_dir_bytes` was declared INTEGER for the PostgreSQL backend (tables.py Integer + Alembic 0019 `INTEGER`), i.e. int4 with a 2 GiB ceiling. The column exists to observe workspace-repo bloat (#1596), so the values it is there to record are exactly the ones that overflowed: any agent whose `.git` passed 2,147,483,647 bytes made every SyncHealthService upsert raise `psycopg2.errors.NumericValueOutOfRange: integer out of range`, and that agent's sync health went dark at the moment it mattered. SQLite never showed it (its INTEGER is 64-bit), which is how it shipped on 2026-07-14. Dual-track (Invariant #9): - schema.py: `git_dir_bytes BIGINT` — single source of truth for both backends (init_schema_postgres translates the same string), so fresh PG builds get int8 via 0001_baseline. - tables.py: `BigInteger`. - Alembic 0062: `ALTER COLUMN git_dir_bytes TYPE BIGINT` (proven on a real postgres:16 upgraded from 0061 with the column forced back to int4: information_schema reports `bigint` afterwards and a 44 GiB insert lands). - SQLite `agent_sync_state_git_dir_bytes_bigint`: a declared-type rebuild via the #1160 rename-swap, NOT a bare no-op. schema-parity compares a fresh init_schema DB against an upgraded one by declared column type, so a no-op would leave upgraded files reading INTEGER against a fresh BIGINT and turn that guard red forever. One row per agent, all columns copied verbatim, the one index re-created, idempotent. CI regression seam: `TestGitDirBytesRoundTrip` in test_1596_git_sync_observability.py is now `requires_postgres`, so the schema-parity PostgreSQL tier (#2434) runs its [postgres] leg — the leg that had been red for two months while the tier selected only marked tests. A new information_schema assertion names the column type rather than a stack; two SQLite tests pin the rebuild (rows preserved, index back, no-op pre-#1596). Audit of sibling byte-count columns: `agent_shared_files.size_bytes` stays Integer — bounded by MAX_FILE_SIZE_BYTES (50 MB) at the only writer, so it cannot reach int4's ceiling by construction. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The rename-swap copies exactly the columns its INSERT...SELECT lists and DROPs the old table. Compare the live agent_sync_state column set against that list first and raise — before touching anything — on an unknown column, so a future/unforeseen column is surfaced as a boot failure (`first_pending`, #1160) rather than silently destroyed. No known path produces one today; this is a belt on a migration whose failure mode is data loss. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…s are BigInteger, PG tests need the marker Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…2791) Log out and log back in on the main app with a Workspace tab open from the previous session, and the NEW session dies within seconds. That tab holds the old JWT, its 20s poll 401s, and the handler calls `authStore.logout()` — which removes `localStorage['token']`, i.e. the token the re-login had just written. The handler never asked whether the credential that failed was still the current one. Underneath it, one browser held the platform JWT in two places that could disagree (the in-memory `axios.defaults` copy vs localStorage re-read per request), with no `storage` listener anywhere under `src/frontend/src`, and three separate 401 implementations that had each drifted. `utils/platformSession.js` makes all three singular. **One source.** `readStoredToken()` is the only reader. The `axios.defaults` copy is no longer written (`setupAxiosAuth` is a documented no-op); `main.js` installs a global axios REQUEST interceptor that rebuilds the header per request, so ~368 bare-`axios` call sites get the current credential without being rewritten and a new one cannot forget to opt in. This is the AC's second half ("or is provably never read in preference to the store") and it is the stronger of the two. An explicit header still wins, and exactly one caller needs that: the logout revoke. #2258 clears local state BEFORE the revoke, so with the defaults copy gone the revoke would have gone out unauthenticated and #187 would have silently stopped revoking anything. The token is captured before the clear and passed after it. **One verdict.** `sessionLostVerdict()` → `ignore | stale | logout`, pure so a node-env spec can reach it. `stale` — the failed token is not the stored one — is the fix for the report: adopt the current session instead of destroying it. The Workspace veto closes AC #5: a client whose browser holds a DEAD operator JWT is no longer thrown onto the operator login by `initializeAuth`'s `fetchUserProfile`. It stays scoped by path as well as by portal token, so an expired operator JWT still bounces off an operator surface. **One handler.** `setPlatformUnauthorizedHandler` / `notifyPlatformUnauthorized`. `main.js` registers the reaction; `api.js`, the global interceptor and `portalHttp` report to it. `api.js` no longer hard-reloads, no longer leaves `auth0_user` behind, and carries no private predicate. **Cross-tab sync.** A `storage` listener adopts a sibling's login and drops the mirror on a sibling's logout — without a second server revoke and without writing to storage, since N background tabs reacting to one event would each clear it again. Neither branch navigates: a background tab pushing /login is the noise this issue reports. `workspaceSession.spec.js`'s predicate block asserted its own hand-copied `shouldBounce` helper — which is why it stayed green while the three real predicates drifted, and would have stayed green through this change too. It now asserts the real function. Verified: 131 files / 2937 tests pass. The three load-bearing guards were mutation-checked — removing the `stale` arm reds 2, letting the interceptor overwrite explicit headers reds 1, restoring `api.js`'s own logout reds 2. Related to #2791 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VpvcfgWkmQPD7DrDLmATTf
…eletes (#2791) Review finding on my own diff. The docblock's "why not `axios.create()`" argued from `stores/auth.js` mutating `axios.defaults.headers.common.Authorization` at login and deleting it at logout — the exact copy #2791 removes. The conclusion survives the mechanism (the global is still the only thing carrying a live credential, now because the request interceptor resolves it per request and `create()` gives an instance its own chain the global never reaches), which is precisely why the comment would have gone on reading as true. A comment that describes a mechanism the code no longer has is the class this repo's learnings ledger already records; the old reason is kept in parentheses because it explains why the answer did not change. Related to #2791 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VpvcfgWkmQPD7DrDLmATTf
…ng budget, not the reader-race 300s ceiling (#2789) A 3600s agent that took a 429 thirty seconds into a Workspace turn was switched to a healthy subscription and re-run with `timeout_seconds=300`: the agent server killed its own process group at exactly 300s with `stop_reason=tool_use` — actively working — and the turn was discarded after being billed. Re-sending the same message succeeded. Both inline retries read `_AUTO_RETRY_MAX_TIMEOUT_S`. That constant was written for #678's reader-race retry — a re-dispatch of a turn that never started, where "reader races fire fast; 5 min is plenty" is right. The SUB-003 retry (#792/#2638) is a full re-run of the user's turn and earns the budget the turn was given. `remaining_s` already bounds first attempt + retry at the operator's `execution_timeout_seconds`, so the second ceiling constrained nothing that was not already constrained and cost every turn honestly longer than five minutes. - SUB-003 retry: `min(remaining_s, 300)` -> `remaining_s`. The #678 path keeps its ceiling; the constant's docstring now names which path owns it and why the portal marker derivation depends on that. - `_AGENT_HTTP_SLACK_S` (10): the first dispatch gives the backend's HTTP read 10s more than the agent's own budget so the agent's structured 504 beats a bare ReadTimeout. The retry collapsed both onto one instant; it now keeps the same slack. - `_AttemptState.applied_timeout_seconds`: `state.start_time` is reset before an inline retry, so `_handle_timeout` measures the retry — it now judges that against the retry's own budget instead of the untouched configured one. A retry that ran its full allowance was being labelled #2106 NETWORK ("aborted after 300s of 3600 seconds allowed"), sending operators to raise a limit that was never reached. None on the first attempt, so #2106's own tests are unaffected. Set inside the #678 CB gate's `else`, not above it: a retry the breaker refuses never runs. - `_warn_if_retry_budget_clamped`: the applied budget appeared in no log line; a clamp is now stated at WARNING where it is decided, silent when nothing was taken. `test_792_subscription_retry.py::test_retry_timeout_bounded` asserted `<= _AUTO_RETRY_MAX_TIMEOUT_S` while running at `timeout_seconds=300`, where the ceiling and the remaining budget are the same number — it passed for every value of this bug. Re-anchored to the bound that is load-bearing. New tests in `test_2789_subswitch_retry_budget.py` are mutation-checked: reverting the budget fails 2, mutating the attribution wiring fails the end-to-end one. Fixes #2789 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VpvcfgWkmQPD7DrDLmATTf
…nd is not a clamp (#2789) `_warn_if_retry_budget_clamped` warned on every seat switch: the re-run is always shorter than the turn by what the first attempt spent, so a 429 at 30s logged "clamped to 3570s of 3600s — a timeout at that point is this ceiling". There is no ceiling on that path any more; the 30s is arithmetic. The helper is now `_log_retry_budget` and takes the elapsed time: the #678 reader-race ceiling stays a WARNING ("clamped"), the SUB-003 re-run logs the breakdown at INFO, and escalates to WARNING only when less than the reader-race ceiling is left — a re-run that short is likely hopeless and is still billed, which is the one an operator wants to see. /review I1 on #2817. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VpvcfgWkmQPD7DrDLmATTf
…-bigint # Conflicts: # docs/memory/learnings.md
… nothing — the rebuild's justification was false Two merge-train findings on #2805: 1. dev's head moved to `0062_execution_fan_out_task_id` (#2532) after this branched, and this revision declared the same parent — a live #2068 fork, two heads, `upgrade head` applying zero revisions. Re-chained as `0063_agent_sync_state_git_dir_bytes_bigint` off `0062_execution_fan_out_task_id`; `check_alembic_heads.py` on the merged tree: 64 revisions, 1 head. 2. The SQLite rename-swap rebuild was justified by "the schema-parity suite would go red forever" — disproved by a one-line negative control (registration removed, suite still green): both parity fixtures build from empty, so `init_schema` creates the table in both snapshots and the guard cannot see this column's declared type. A boot-time DROP TABLE of a live table for a CI benefit that does not exist is the wrong trade. The rebuild, its column list and its three tests are gone; a note beside `_migrate_agent_sync_state_git_dir_bytes` says why the SQLite track deliberately carries nothing, a test pins that it stays that way for a reason, and the learnings entry now states the honest lesson: run the negative control before writing a guard's behaviour into a durable file. The seven sibling columns with the same int4-vs-`< 2**63` mismatch are filed as #2827. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VpvcfgWkmQPD7DrDLmATTf
…ute under test, and the veto reads the per-tab store (#2791) Merge-train review of #2811, all six ❌/W items: C1 App.vue wrote `axios.defaults.headers.common['Authorization']` on every boot with a token. Axios merges that default into the request BEFORE the interceptor chain runs, so it arrived at the per-request rebuild looking explicit and won over storage for the life of the tab — the whole mechanism was inert, and 119 bare-axios sites kept the boot-time token after a sibling re-login. The write is removed (the store seed + WS connect stay); `adoptStoredSession` / `applySessionEndedElsewhere` delete any such copy as a belt, so a tab can never ride a credential storage no longer holds (W2). C2 The "nobody writes the default" guard walked auth.js only. It now walks src/frontend/src/** and asserts zero writers; App.vue would have failed it. C3 architecture/workspace.md + workspace-session-signout.md said the copy is written nowhere while App.vue wrote it. Both now describe what is true and why the guard is tree-wide. C4 handlePlatformUnauthorized ended `router.push('/login')` without `return`, so notifyPlatformUnauthorized's absorber never engaged and a redundant navigation escaped as an unhandled rejection. It returns the navigation. CI The reaction, the storage listener and the request rebuild were inline in main.js and pinned by regex — restoring the reported bug on the `stale` branch and inverting the listener both stayed green. They are now `reactToPlatformUnauthorized`, `reactToStorageEvent` and `applyRequestCredential` in utils/platformSession.js, taking their collaborators as arguments; the spec EXECUTES them with fakes, and a five-mutation battery (stale→logout, inverted listener, header never applied, navigation dropped, App.vue writer back) is red on every one. main.js is wiring only, and the source guards assert exactly that. W1 The Workspace veto read `portalTokenPresent` from shared localStorage while portalHttp gates on the per-tab store; a client signing in in another tab stranded an operator's Workspace tab on an expired JWT with `ignore`. It reads `useClientPortalStore().portalToken` now. W3 The 22 direct `localStorage.getItem('token')` reads outside the reader (WS/EventSource included) go through `readStoredToken()`; a tree-wide guard keeps "one reader" true. W4 The stale `installCrossTabSync()` reference is gone with the rewrite. W6 The storage listener also hears `auth0_user`, and adopting an identical token refreshes the user from storage, so a sibling login's profile landing a tick after its token is not missed. 3066 frontend unit tests green; vite build green. Fixes #2791 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VpvcfgWkmQPD7DrDLmATTf
…ry-budget # Conflicts: # docs/memory/learnings.md
… not the last retry's (#2789) Merge-train review C1 on #2817: `state.start_time` is re-stamped before each inline retry so `_handle_timeout` classifies the attempt it measures — which made it the wrong clock for a BUDGET. On the #678→#792 interplay (502 → reader-race retry → 429 → switch) `elapsed_s` counted only the reader-race retry, and the SUB-003 retry re-granted nearly the whole turn a third time: 6610s of slot time on a 3600s cap, past the slot lease (timeout+300), the watchdog (age > timeout) and the portal marker. - `_AttemptState.turn_started_at`: the turn's clock, never reset; `_turn_elapsed_seconds` reads it. The SUB-003 budget is derived from it, and AFTER the 3s settle delay (W3), so every second the turn has spent counts against it. - `_log_retry_budget` keys "clamped" on a `ceiling=` the caller names, never on `elapsed_s <= 0` (W4). - `client_portal.portal_attempt_ceiling_seconds` imports `_AGENT_HTTP_SLACK_S` instead of a magic 10 (W2). - Tests: the tautological portal test is replaced by two that EXECUTE the interplay on a controlled clock — the SUB-003 grant at t=3005 on a 3600s turn is ≤605s (was 3605s), and the executed worst case (attempt 1 to cap, reader-race to ceiling, SUB-003 the remainder) fits the marker. The literal classifier test is replaced by one that drives `_handle_timeout` with a state carrying an applied budget (W1). Mutations: retry-clock budget → 2 red; classifier ignoring the applied budget → 2 red. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VpvcfgWkmQPD7DrDLmATTf
… not just the first (#2789) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VpvcfgWkmQPD7DrDLmATTf
… a silent fallback (#2789) /review I1 on #2817: `turn_started_at` defaulted to None with an `or state.start_time` fallback in the reader — a future construction that names only `start_time` would have anchored the turn budget on the reset clock without anyone noticing. `__post_init__` derives it from `start_time` (the same instant at construction) and the reader no longer falls back to anything. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VpvcfgWkmQPD7DrDLmATTf
…ue 422 (#2696) `transcribe_portal_audio` mapped every non-200 from ElevenLabs onto `422 "Could not transcribe the audio"`. A missing endpoint permission, a rejected key, exhausted credits, a provider rate limit and a rejected audio container all read identically, while the actionable status word sat in a backend WARNING one line above — a live instance cost an operator with container access a full round-trip to answer a question the system already knew. `stt_capability_service.classify_stt_failure()` (pure) maps a provider answer onto a named category with its own client status and sentence: permission / auth / quota 401·402·403 by status word → 503, operator-actionable rate_limit 429 → 429, the existing retry wording audio 400·413·415·422 → 422, "the recording could not be read" provider 5xx → 502, the existing transport wording unknown anything else → 502, still says who failed No arm returns the old string; a test sweeps every status 300-599 to pin that, so an unrecognised provider answer cannot regress to it. The client sentence never carries the provider body. `record_live_failure` keeps the status word + category for THIS key (`stt:last_failure:<digest>`, 24h; per-process fallback) and still feeds #2695's capability cache on a 401/403, so the mic hides on the next load. `GET /api/settings/elevenlabs` (admin-only) carries it as `stt_last_failure`, and Settings -> Voice renders "Last voice-input failure: <why> (HTTP <status> <word>) — <time>" under the capability badge. Fail-soft is unchanged: every branch raises ClientPortalError, never a 500, and the client can always type instead. Fixes #2696 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015owqMKD5QDjzZrUTF2Joht
…#2696) Review finding, reproduced by driving the classifier: a 401 whose body carries no status token was classified `quota` whenever its SENTENCE happened to contain one of `_QUOTA_WORDS`. 401 {"detail":"Invalid API key for your plan"} -> quota 401 {"detail":{"message":"…not valid for this subscription"}} -> quota Both are ordinary auth failures. The client was told "the account is out of credits or not on a plan that allows it" and the admin panel said the same, so an operator whose key simply needed replacing was sent to a billing page. That is worse than the opaque 422 this issue replaces — it is confidently wrong in a direction someone acts on. The cause is that `provider_status_word` collapsed two different things: `det["status"]`/`det["code"]`, which are machine tokens the provider documents, and `det["message"]`/a bare string detail, which is prose written for a human. The comment above `_QUOTA_WORDS` already claimed matching happened on "the status token"; the code matched whatever came back. `provider_status_parts(body) -> (token, prose)` keeps them apart, and `classify_stt_failure` matches ONLY the token. A 401/403 carrying prose alone is now `auth` — the honest reading of "the key was rejected and the provider did not say why". `provider_status_word` stays as the operator-facing display value (token else prose), so nothing is lost from the panel: the prose still shows, it just no longer votes. Verified in the backend image: 38 passing in this suite (5 new table rows + 3 new tests), 66 with #2695's alongside it, and the portal/settings sweep has the identical non-passing set on this branch and on its base — no new failures. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q19uRCksdn4DiRAJ55rfpZ
… provider-error branch Re-applied after rebasing onto dev: #2699 squash-merged with its own final wording of this bullet, so the branch's copy of the paragraph conflicted and dev's was taken; this is #2696's one addition to it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VpvcfgWkmQPD7DrDLmATTf
… and invalidate forgets it (#2696) The /review on #2702 (I1): `read_last_failure` fell through to this worker's local copy on a Redis MISS — the pre-#2695-fix shape of `read_cached`, which that PR was held for. After the rebase the two readers in one module disagreed about what a miss means; now they share the rule (Redis answers ⇒ authoritative, miss evicts the local copy; local only when Redis cannot be asked). `invalidate` also drops the key's last-failure row, so re-saving a fixed key does not show the operator the pre-fix failure for 24h beside a fresh `capable` verdict. Mutation: restoring the fallthrough turns the new two-worker test red. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VpvcfgWkmQPD7DrDLmATTf
…ct, and says so when it cannot (#2828) On 2026-09-15 the watch fired correctly and answered "conflicts with dev — head check not evaluated" for four of nine open PRs, two of which carried a live two-heads fork. All four conflicts were `learnings.md` append collisions. A watcher that stops on ANY conflict is absent on exactly the busy days a fork is likeliest; both forks reached the train green. - The conflict arm now asks WHERE. `git merge-tree --write-tree` writes the merged tree on exit 1 too, and lists the conflicted paths on its own stdout; only a conflict under `src/backend/(enterprise/backend/)?migrations/ versions/` is a `conflict`. Anything else is evaluated on the real three-way merge of the version directories, with the unrelated paths carried onto the verdict as `conflictsElsewhere`. - `alembic-head-verdict.js`: `clean`/`fork` name the unrelated files on the status description and the sticky (capped at 20, fenced); `conflict` — now only a revision file edited on both sides — publishes a visible `error` status plus a sticky that names the file, instead of a comment alone. #2029's rule is against a false `success`; it never argued for silence, and silence is how two forks rode to the train. Proven against the real case, not the YAML: the evaluate step extracted and run locally against origin/dev + #2805's pre-fix head → `fork` with `learnings.md` named (was `unknown`); the fixed head → `clean`; a synthetic both-sides revision edit → `conflict`. Shape pins and executed verdict tests updated in test_2533; the "a conflict publishes no status" pin is re-anchored with the reason. Not here, by scope: per-PR learnings fragments (the collision's own fix) and a merge-time check (direction 3) — both named on the issue. Fixes #2828 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VpvcfgWkmQPD7DrDLmATTf
…ords its mutation (#2829) Three of five ejections on the 2026-09-15 merge train — the third train running — were tests that prove the code was written rather than that it runs: source-text regexes over the module under test (#2811), a bound check at the one value where both bounds coincide (#2817), a docstring claim about CI never negative-controlled (#2805). All green. - docs/testing/STRATEGY.md: a new "Evidence bar for a test" section beside the harness bar — the three spellings, the two greps (the live-consumer grep is the one that decides), guard-vs-source-only with the train's own pair (#2819 kept, #2811 ejected, same shape), mutation as the fix standard, bound tests away from the coincidence — each with what enforces it. - .github/pull_request_template.md: a Testing checkbox for "every new test executes the changed path" and a `Mutation:` line naming the test(s) that go red with the fix reverted ("n/a — not a fix" otherwise). The trailing space after the colon matches the existing `Journey Impact:` line — a fill-in prompt. - docs/memory/learnings.md: the class, with the prior occurrences. The skill half — /review Step 2.5 and /validate-pr §5.4 answered first and in writing, /implement's two done-criteria — is trinity-dev#29. Fixes #2829 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VpvcfgWkmQPD7DrDLmATTf
…iling (#2827) `_coerce_nonneg_int` admitted anything up to INT8_MAX for all eight int columns, but only `git_dir_bytes` is BIGINT on PostgreSQL (#2800). The other seven are int4, so a value the boundary admitted but the column could not hold made the whole upsert raise NumericValueOutOfRange and the agent's sync health went dark. The four ahead/behind counters were not coerced at all. - default ceiling is now INT4_MAX; `git_dir_bytes` and the lock-report ints opt into INT8_MAX explicitly - `_coerce_counter` bounds ahead_main/behind_main/ahead_working/ behind_working (and the legacy ahead/behind keys) the same way Tests drive INT4_MAX+1 through `_sync_agent` per column (rejected → NULL/0, BIGINT admitted), pin that INT4_MAX itself lands, and round-trip each column's admitted maximum through SyncStateOperations on both backends. Mutation (default ceiling back to INT8, counter left raw) → 3 red. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VpvcfgWkmQPD7DrDLmATTf
…arm guard is pinned (#2832) — mechanical, per the merge-train note on the PR - `git -c core.quotePath=off merge-tree …` plus `^"?` in VERSION_LINES: a revision path git C-quotes (non-ASCII, `"`, `\`, control byte) no longer slips past the anchor into the *elsewhere* class, where the guard would run over a marker-bearing file that check_alembic_heads.py omits as unparseable and read PASS. - The `[ -z "$evaluable_conflict" ]` guard on the unknown arm — the one line the #2828 fix turns on — is now asserted; deleting it fails test_an_unrelated_conflict_no_longer_stops_the_evaluation (mutation-checked). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VpvcfgWkmQPD7DrDLmATTf
…2702) — mechanical, per the merge-train note on the PR `record_live_failure` stores the provider's status word / prose as the operator `detail` for 24h and the Settings panel renders it. The client half is a constant, so this row is the only place a provider body reaches any surface; the panel promises the key is never echoed, so enforce it here with `scrub_secret` instead of assuming it of the provider. One test drives a body carrying the key and asserts neither the returned failure nor the stored row contains it (mutation: scrub removed → red). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VpvcfgWkmQPD7DrDLmATTf
… not a literal 10 (#2817) — mechanical, per the merge-train note on the PR Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VpvcfgWkmQPD7DrDLmATTf
…ter expiry (#2811) — mechanical, per the merge-train note on the PR `handlePlatformUnauthorized` read `portalToken` alone. `endSession({expired})` nulls that token and sets `platformFallbackSuppressed` in the same breath, so for a client tab holding a dead operator JWT the 5 s ticket retry's next 401 fell through to `logout` and threw the client from the OTP form onto the operator login — the #2258/#2261 bounce, reopened for exactly that instant. `portalTokenPresent` now folds in the suppression flag (per-tab sessionStorage, so W1 cannot return). The wiring pin follows, and the tautological "racing away" spec case (identical inputs both sides) is replaced by one that can fail: client tab → ignore, live or expired; operator → logout. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VpvcfgWkmQPD7DrDLmATTf
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Integration surface for #2833, #2832, #2702, #2805, #2817, #2811 — the 2026-09-16 merge train.
Never merged. This PR exists so the six members are tested together on one tree (they are individually green against
dev, never against each other). Once this is green the members merge individually, squash, in the order above; this PR is then closed and the branch deleted.Assembly:
dev@0bddbc959+ each member's head via--no-ff(one merge commit per member, so a red job bisects to a PR). The only member-vs-member collisions weredocs/memory/learnings.mdappend-collisions (#2832, #2805, #2817), resolved keep-both on this branch; the same resolution is re-applied on each member after the previous one lands.Member validation, mechanical fixes pushed to each member branch and the per-PR reviews are on the member PRs.
🤖 Generated with Claude Code
https://claude.ai/code/session_01VpvcfgWkmQPD7DrDLmATTf