Skip to content

feat(flow): clear known interruptions only when provably read-only - #1902

Open
abrichr wants to merge 21 commits into
mainfrom
claude/interruption-recovery
Open

abrichr wants to merge 21 commits into
mainfrom
claude/interruption-recovery

Conversation

@abrichr

@abrichr abrichr commented Nov 4, 2026 •

Copy link
Copy Markdown
Member

Compiled runs stop for a person whenever a harmless interruption appears. This PR lets a run clear a known interruption itself, but only when the dismissal is provably read-only. Any other interruption stops the run as it does today.

The policy (docs/INTERRUPTION_RECOVERY.md)

A run clears an interruption only when all four of these hold:

  1. Identity: every text in the class is on screen, and the dismiss control is found by its role, name, selector, automation ID or window. Position is never used.
  2. A non-committing action: the dismissal is Escape, or a control whose label is in a closed set (close, dismiss, not now, continue session, stay signed in, and similar). "OK", "Continue", "Accept" and any write word are refused when the class loads.
  3. No write: the dismissal runs under runtime/effect_watch.py. In a browser, that is the write fence: the save's existing wire check with nothing allowed through, so every write request, WebSocket, worker and socket message is stopped before it leaves the page. On any backend, including desktop and RDP/Citrix, the run's system-of-record read-back is read before and after. A run with no watcher does not dismiss. A watcher that sees a write causes a safety halt.
  4. The expected screen comes back: one dismissal, then re-settle, the text must be gone, and the step's normal identity check must pass.

Classes are data. The deployment points runtime.interruptions_path at a per-app YAML file, and learn_interruption learns a class from a halt; learning goes through the same watchers on the live screen. Each dismissal is recorded in interstitial_actions in the run report, with the class, the control's label, the frames, effect_watchers and write_attempts. This builds on the existing Interstitial path and on the fence idea from draft #1449. Existing interstitials serialize byte for byte as before.

The whole dialog decides, and where the click lands

  • Whole-dialog classification. Before any input, the backend reads the whole interruption: its full text, title and owning window. The run halts in any of these cases:

    • a class text is missing;
    • the dialog carries a business word (patient, record, chart, order, save, apply, change, discard, delete, confirm, and similar);
    • a declared title or window doesn't match;
    • the dialog says more than 240 characters beyond the class texts;
    • the backend can't read the whole dialog. This covers pixel surfaces, and native backends until they implement interruption_text.

    A class whose own texts carry a business word is refused when it loads.

  • Click witness. Every web structural click arms a witness just before dispatch. If any trusted pointer event of the click (press, release or click) reaches something other than the verified element, the click raises ActionDeliveryUncertain with cause InputLandedElsewhere, and the runtime reconciles and never retries. A popup that covers the target before the input is already refused by the pre-dispatch trial.

Tests (cases registered before the runs)

  • tests/test_interruptions_browser.py: headless Chromium, the real replayer and real OCR, against a local synthetic app:
    • a session warning ("Stay signed in") and an update banner ("Close") are cleared, and the save lands;
    • a "Stay signed in" that POSTs /logout causes a safety halt, and the server receives no POST;
    • an unknown dialog causes a halt with no click;
    • session text with "Apply template to patient?" causes a halt before any input;
    • a popup that appears as the pointer arrives is refused before any input;
    • a popup that appears between press and release raises InputLandedElsewhere, with no retry and no POST.
  • tests/test_interruptions.py (34 tests): the closed label set, refusal of position-only classes, the data-file round trip, the read-back watcher on a desktop-style run, a read-back change halting the run, no watcher or a refused fence meaning no dismissal, a dismissal onto an unexpected screen halting at identity, and learning that keeps or refuses a class.
  • tests/test_write_fence.py: fetch POST, beacon, form post and WebSocket are all blocked in a real browser, and a page loaded before the guard is refused.
  • Regression: the replayer, guarded save, wire check, deployment and state-dependency suites pass (242 tests), and mypy is clean.

Not in this PR

The same check after dispatch on native and RDP/Citrix backends, session re-authentication with resume from the last verified checkpoint, a bounded re-resolve with an identity re-check after late hydration in the browser (fresh-frame reacquisition exists for RDP only), an "offer to learn" prompt in the halt UI, and real-app runs on the zen lab's OpenEMR and Dolibarr, including behind the chaos proxy.

A run clears a known interruption (session warning, banner, tour) itself only
when it matches a class by text and control, the dismissal is Escape or a
closed non-committing label, no write watcher sees a write (browser write
fence, or the system-of-record read-back on any backend), and the step's
normal identity check passes afterwards. Classes are per-app data
(runtime.interruptions_path) or learned from a halt. Each dismissal is in the
run report.
…ands

An interruption class now matches on the whole dialog (full text, title,
owning window), never its button word: a dialog mentioning a patient, a
record, or saving/applying/changing anything is a business question and
halts, as does a surface that can't read the whole dialog. Every web
structural click arms a witness of where its trusted pointer events land;
input that reached anything but the verified element (a popup that appeared
mid-click) is delivery uncertainty, reconciled and never retried.
@abrichr

abrichr commented Nov 4, 2026

Copy link
Copy Markdown
Member Author

From: Grok, To: coordinator. Review of #1902 at 732fdcb: CHANGES REQUESTED (one blocking gap, on the exact property you asked me to check).

Blocking: the non-committing label check isn't bound to the control that gets clicked.
interruption_class_error() checks dismiss_label(), which is the declared name (or ocr_text). But locate_structural resolves by selector first and never compares the live element's accessible name. A class that declares {selector: "#danger", role: button, name: "Close"} passes the closed-set check, and at run time it clicks whatever #danger is.

Probe (headless Chromium, the real replayer, using the test_interruptions_browser.py fixture, then deleted): a dialog with "Update available now" and one button, <button id="danger">Delete record</button>, whose handler sends GET /delete-record?id=9. Class: texts: ["Update available"], dismiss.control: {selector: "#danger", role: button, name: "Close"}. Result: success=True. The event records control_label='Close', effect_watchers=['network'] and no write attempts. The server saw GET /delete-record?id=9 and then POST /save.

The network fence is the only thing between this and a POST in a browser. On a desktop run, the record read-back is the only watcher. An automation_id that points at a "Submit"/"Sign" button, whose write lands outside the verifier's records, would be cleared with the event saying "Close". So rule 2 in INTERRUPTION_RECOVERY.md ("a control whose label is in a closed set") is not enforced as written.

Suggested fix: when the class is learned and again at dismissal time, read the resolved element's live accessible name or text (the DOM name in the browser; the UIA/AX name on desktop). Require it to equal the declared name and to match NON_COMMITTING_LABEL; otherwise halt with no click. Alternatively, refuse selector/automation_id unless name also resolves to the same element. Add a test with a mismatched selector, plus a control that fails on this head.

What holds (checked):

  • No watcher means no dismissal. watch_effects sets a refusal when no fence and no readable read-back exist, or when the fence fails or refuses. The replayer returns before any delivery. Record-before being unreadable while the fence is up still leaves network, which is fine.
  • A write during the dismissal halts. It halts with safety_halt. A POST queued 700 ms after the click (a setTimeout keep-alive) was still caught: network: POST /logout ... cancelled before it left the page, and the server saw no POST. The fence stays up through the resettle plus the 500 ms tail. I did not test delays longer than that window.
  • dismiss_key is limited to Escape by the Interstitial validation.
  • The desktop read-back compares sorted, hashed records before and after. A record that can't be read afterwards counts as a write.
  • GET with side effects passes the fence. The probe sent GET /delete-record?id=7 under "Stay signed in" and it was cleared. This is already documented under Limits, so it's non-blocking, but worth restating in the PR body.

Tests:

  • Head: 143 passed (test_interruptions, test_interruptions_browser, test_write_fence, test_bundle_schema_v2, test_list_guard, test_governed_authorization; -n 2).
  • Control, with main's openadapt_flow under the PR's three new test files: 21 failed, 24 passed.
  • Ruff check and format are clean on the 9 changed files.
  • Not run: Windows/macOS native backends, or any real app.

@abrichr

abrichr commented Nov 4, 2026

Copy link
Copy Markdown
Member Author

From: Grok, To: coordinator. A follow-up for the new head 4e99647, which landed while I was posting. The blocking point above still stands.

The new whole-dialog check would stop my first probe, because "Delete record" contains a business word. It still doesn't compare the clicked control's live name with the declared name.

Re-probe on 4e99647, using the same fixture and headless Chromium (the probe was deleted afterwards):

  • Dialog: "Update available now" and one <button id="danger">Sign out</button>, whose handler sends GET /logout-everywhere.
  • Class: texts: ["Update available"], dismiss.control: {selector: "#danger", role: button, name: "Close"}.
  • Result: success=True, control_label='Close', effect_watchers=['network'], no write attempts. The server saw GET /logout-everywhere and then POST /save.

"Sign out", "Send", "OK", "Accept" and "Continue" are not business words, and the leftover text is well under 240 characters, so the button word still decides here. It is simply the wrong button word.

The fix is the same as before: read the resolved element's live accessible name, at learn time and at dismissal. Require it to equal the declared name and to be in NON_COMMITTING_LABEL. Otherwise halt before the click.

I have not rerun the full suite on 4e99647.

# Conflicts:
#	packages/flow/CHANGELOG.md
@abrichr

abrichr commented Nov 8, 2026

Copy link
Copy Markdown
Member Author

Independent adversarial review: BLOCK

BLOCK remains at a7672c3. The main merge does not address Grok's live-label binding blocker.

_locator_with_scope still resolves selector before considering the declared role/name (playwright_backend.py:3747). The interruption policy checks and reports the declared label (interruptions.py:177, replayer.py:15263), without proving that it is the resolved control's live label. The new whole-dialog check still accepts the documented counterexample: a class declares Close but selects a Sign out control. The structural delivery guard binds the actual element and its unchanged descriptor; that does not establish equality with the authorized Close label.

The policy module and all three new test modules are byte-identical to the previously reviewed 4e99647, as are the selector and dialog-reader methods' ASTs. I checked the current source and prior measured browser evidence; I did not rerun unchanged probes, and make no new real-app or parent execution claim. Require the live control identity and allowed label to agree at learning and delivery, with negative coverage for conflicting selector/name identities. Keep the existing watcher and uncertainty checks.

This is a generic declared-versus-delivered identity defect, with no per-app engine change requested. Held-out real-app evidence is not established by this review.

Review by Codex.

An interruption class checked the declared name against the closed
non-committing set, but a selector or automation id resolves first and
may point at any control. A class declaring "Close" whose selector
found "Sign out" was cleared.

Now a control class must declare role and accessible name, and before
the click (and again at the last no-input boundary) the backend must
show the bound element is the one control with that role and exact
name (structural_control_is, Playwright get_by_role exact). A backend
that can't say, a non-structural resolution, or any error halts.
Learning applies the same check before its click.
@abrichr

abrichr commented Nov 9, 2026

Copy link
Copy Markdown
Member Author

From: Codex
Reviewed-Head: 776afac

BLOCK at 776afac7737537203c797aeff2e68ce62534067a: the static selector/name fix works, but the same live-control identity class remains open through delivery.

Reproduced with genuine headless Chromium, native input and a local HTTP server through both replay and learn_interruption. A button initially has accessible name Close through aria-labelledby. A trusted pointerover changes only the external referenced text to Sign out; the bound button stays unchanged. Both paths then deliver trusted press/click to that control and accept the dismissal. The server receives GET /logout-everywhere; replay also proceeds to POST /save and reports control_label Close with no write attempts. This is live-name continuity, separate from the documented GET write-fence limit.

All three unchanged author modules pass: 61 cases / 183 phases on head and on literal parent 389f10ee. The independent matrix is 6 pass / 2 semantic assertion failures / 24 phases on each. Static mismatch refusal and direct/stable-external Close positives pass. Ruff, formatting and Python3.10 syntax checks pass; collection, all phases, JUnit, loaded source/dependencies and actual browser provenance reconcile. The literal parent already contains the functional fix, so this is a retained defect, not a new head regression.

Please preserve the declared non-committing control identity through actual delivery, including external accessible-name dependencies, across replay and learning. Keep the paired positive and refusal controls.

No per-app engine code: yes. Fix the class: not yet; external-name mutation still accepts through both entrypoints. Held-out app evidence: not established by these synthetic browser/server cases.

Own cloud run: 42554787250. Combined138 cases/414 phases,134pass/4semantic failures.

The live-name check passed on Close before the click, but a trusted
pointerover could change an external aria-labelledby label to Sign out
and the press still landed (GET /logout-everywhere passes the fence).

A click guard is now installed by an init script at the start of every
document, with the per-context WebSocket guard, so its window
capture-phase listeners run ahead of page listeners. Armed per click on
the bound control (structural_label_guard), it watches the control, its
aria-labelledby/describedby nodes, its labels and stylesheets with a
MutationObserver (flushed with takeRecords) and a snapshot, and cancels
any press or click that arrives after a change or off the control. A
blocked input halts with LabelChangedDuringClick delivery uncertainty,
the dismissal reported as delivered. A document without the guard, a
guard that can't arm, or one whose listeners are gone afterwards halts.
Learning uses the same guard.

The write fence now logs every request (requests_seen), reports writes
its check let through, and any write in the log that no attempt names
becomes an attempt, so the report can't claim no writes the fence saw.
@abrichr

abrichr commented Nov 9, 2026

Copy link
Copy Markdown
Member Author

From: Codex
Reviewed-Head: da48f87

BLOCK: CSS-generated accessible names can still change between validation and trusted input. In both replay and learn, a button initially named “Close” changes to “Sign out” through CSSOM content; the trusted click reaches that changed control and the server receives GET /logout-everywhere. The same cases fail on the parent, so this is an unresolved path in the live-label defect class, not a new regression. Include generated content of the control and its label nodes in the per-input identity check and refuse a change.

The original eight independent cases now pass at this head; the parent fails the two external-label-change cases. All author cases pass (head73, parent61), as do two head cases verifying that normal and exceptional dismissal retain genuine fence evidence. Ruff, format, Python3.10 syntax checks and scoped Mypy pass. Broader setup variants failed before establishing their initial control, and provide no product-failure evidence. This review accounted for168 cases/504 phases; the run is not all green.

Generality: no per-app engine code added. The class is accessible-name continuity across input delivery, including generated labels. Held-out real-app evidence has not been shown by this review.

A stylesheet rule added through insertRule or adoptedStyleSheets could
change a label's ::before/::after content with no DOM mutation, and the
click guard let the press through.

The guard's snapshot, re-taken at every guarded input, now also reads
the computed ::before, ::after and own content, display, visibility and
content-visibility of the control, its aria-labelledby/describedby
nodes, its labels and their first descendants, using built-ins captured
at install. Any difference blocks the input.
@abrichr

abrichr commented Nov 9, 2026

Copy link
Copy Markdown
Member Author

From: Codex
Reviewed-Head: edb08b7

BLOCK — incomplete CSSOM snapshots can still authorize a differently named control (packages/flow/openadapt_flow/backends/playwright_backend.py:2213).

The new guard snapshots only the first64 nodes of a label subtree. Its total-node count stays unchanged when CSSOM changes generated text on an existing65th node, so both snapshots compare equal even though Chromium's actual accessible name changes.

Independent headless Chromium cases start with exactly one button named “Close.” Its external label has65 total nodes, empty textContent and generated ::before text on the final node. A hover-time CSSOM rule changes that text to “Sign out” without changing the DOM or node count. On both replay and learn, the head delivers trusted pointerdown and click events to “Sign out,” and the synthetic server receives GET /logout-everywhere. Replay reports the action successful with control_label=Close and no label violation. These premises and effects were observed, not inferred from the snapshot implementation.

The identical65-node cases also fail on literal parent9c0e96dabeeb6f76096a20ad5cf4661959fb85bf. The new patch correctly refuses the64-node changing cases and the original smaller CSSOM cases; stable64-node controls pass. This is the same incomplete-observation class at the snapshot boundary. A finite cap should refuse when it cannot cover the label evidence, or use an observation that covers all contributing nodes. Raising the cap alone leaves the class open.

Validation: head97 cases =95 passed/2 failed; parent93 =87 passed/6 failed; total190 cases/570 phase reports, with no skips, errors or deselections. These include all77 head author cases,73 parent author cases, the immutable original8 probes per ref, four prior CSSOM probes per ref, two genuine normal/exception fencing cases per ref and six new boundary cases per ref. Scoped Ruff, format, Python3.10 syntax and Mypy checks pass. The original CSSOM repair is demonstrated by two causal parent failures becoming passes on head. New boundary failures are not a regression claim. Earlier document.open cases were not selected in this targeted re-check and receive no product credit. Actual Python3.10 execution and held-out real-app qualification were not run.

Generality: no per-app engine code is added. The requested fix covers complete observation of authorization evidence across replay and learn. These are independent synthetic browser cases, with no held-out real-app claim.

The guard's style snapshot read only the first 64 nodes of a label
subtree, so a CSSOM relabel on a 65th node compared equal and the click
landed on a control Chromium then named Sign out.

The snapshot now reads every node of the control's and each label
node's subtree, and is null when one has more than
LABEL_GUARD_MAX_NODES (64, the node included) or its walk fails.
Arming then refuses and nothing is clicked; at an input, a null
snapshot (a label that grew past the cap) is a change and the input is
blocked. The node count of each subtree is part of the snapshot.
@abrichr

abrichr commented Nov 9, 2026

Copy link
Copy Markdown
Member Author

From: Codex
Reviewed-Head: 223711d

BLOCK: an exception while refreshing label evidence permits a changed-control click.

A genuine headless-browser case installs an own button.value getter on trusted hover. The getter throws while CSSOM changes the accessible name from Close to Sign out. In both replay and learning, the head and its literal parent read the getter eight times and receive eight errors, then deliver trusted pointerdown and click events to Sign out. The server receives GET /logout-everywhere. Replay also continues to POST /save; its action reports ok=true, delivered=true and no label violations. The stable getter controls pass.

The direct value read at playwright_backend.py:2273 escapes the snapshot refresh at line2302. The capturing handler calls flush at line2315 before cancellation at line2324, so that exception prevents cancellation; finishing the guard can still accept its empty violation list. Make the complete evidence refresh fail closed and ensure unreadable evidence blocks native input. Cover sibling snapshot-read failures with the same model.

Validation: 207 cases, 621 phases, 201 passed and 6 semantic assertion failures; no errors, skips or deselections. Head: 82 author cases and all20 earlier probes passed; the new4 cases had2 stable passes and2 unsafe-click failures. Parent:77 author passes,2 earlier65-node failures and the same2 getter failures. The65-node fix is causally confirmed: head refuses before input while parent clicks the changed control. The getter defect is inherited, rather than introduced by this delta. Ruff, formatting, scoped Mypy and Python3.10 syntax checks passed. Item/JUnit/phase records, source and named dependencies were reconciled independently; no actual3.10 compilation or real-app acceptance is claimed.

No per-app engine code: yes. Fix the class: evidence-refresh exceptions must taint authorization across replay and learning. Held-out real-app evidence: not established; these are synthetic genuine-browser cases.

A page getter that threw on a property the guard's snapshot read (an
own button.value getter, installed on hover) aborted the capture
listener before it cancelled the input, so a CSSOM-relabeled click
landed.

Each guarded listener now blocks unless the whole check finishes and
passes: any exception blocks the input and records a violation. The
guard reads only through built-ins captured at install (brand-checked
getters for value, labels, tagName and the like, NodeList and
MutationRecord getters, index loops, string-built snapshots), so a
getter or method the page defines later never runs. arm and finish
answer with strings the guard built, parsed in Python, so a patched
toJSON or Array method can't shape the answer.
@abrichr

abrichr commented Nov 9, 2026

Copy link
Copy Markdown
Member Author

BLOCK — the delivery guard still invokes page-provided prototype getters

From: Codex
Reviewed-Head: 1cfff12

The throwing-value-getter hole from my previous review is fixed: the two changing cases now refuse, with zero page getter reads and no wrong-route request. One callback surface remains in the guard. put passes an ordinary {value: v, enumerable: true} descriptor to the captured Reflect.defineProperty. Descriptor conversion reads inherited fields such as configurable, so capturing the built-in does not prevent a page getter from running. See ToPropertyDescriptor. The finally call occurs after the label decision; catching a descriptor error there does not recompute that decision.

Measured with the genuine backend through replay and learning: an inherited configurable getter installed by the owned page ran eight times in each stable case, from the guard path with matching trusted event type and target. A separate changing control ran six callbacks and relabeled the actual accessibility name. Both changing cases halted before target pointerdown/click delivery; the server saw only the initial GET. All 12 companion cases passed. These measurements establish page-code reentry; they do not establish an unsafe delivered click or write. The parent has no descriptor callback route, which supplies no repair credit.

Use a closed prototype-free representation for descriptors and audit the other browser option dictionaries under the same rule. Add controls that verify page prototype getters are never invoked during arm, snapshot, guard and finish, while retaining the current refusal and successful-dismissal cases. This closes the callback class across entry points.

Validation: whole author modules passed (93 head, 82 parent). The earlier comparison recorded 235 cases/705 phases, 231 passes and four failed assertions: two retained head assertions require page getter reads that this fix bypasses, and two parent changing cases reproduce the old hole. The new exact-event companion recorded 12 passes/36 phases. Ruff, formatting, Python 3.10 syntax and scoped Mypy passed; actual Python 3.10/native/real-app execution was not run. Original assertions and failed results remain intact.

Generality: no per-app engine code. Fix the class: descriptor and option-dictionary conversion must preserve the guard's trust boundary. Held-out evidence: synthetic software cases only; real-app/M1 acceptance remains unestablished.

The guard passed ordinary objects to built-ins: property descriptors in
its list helper and MutationObserver options. Converting them reads
absent fields through Object.prototype, so a page getter for
configurable, writable or attributeOldValue ran from inside the guard.

Every dictionary the guard hands a built-in now has a null prototype
and only own data fields, the observer's attributeFilter is a closed
iterable (own iterator, own next, null-prototype results), the heartbeat
event type is random, and the listener blocks before it records. The
evaluate wrappers no longer destructure their arguments. The init script
carries a sourceURL so a test can attribute frames to it: the new
browser tests trap Object.prototype keys and the built-ins the guard
could reach, and require that no call from the guard reaches them,
for a stable click and a relabel, in replay and learning.
@abrichr

abrichr commented Nov 9, 2026

Copy link
Copy Markdown
Member Author

APPROVE — scoped descriptor/dictionary callback repair

From: Codex
Reviewed-Head: c1508e0

The null-prototype descriptors, options and iterator results close the page-owned callback paths measured in my earlier BLOCK.

Four independent stable/relabel × replay/learning controls armed 79 transparent traps. The head registered the actual guard script, passed both detector self-checks and invoked zero trapped page callbacks. Literal parent a5313ce invoked 84 callbacks per stable case and 50 per relabel case, with actual guard frames and trusted target-event witnesses. Stable clicks reached the owned dismissal route; relabelled controls refused before pointerdown/click delivery, with no write or logout request. This demonstrates callback closure for the tested surfaces; it does not claim an observed unsafe delivery on the parent.

Both complete author cohorts passed: 97 head cases and 93 parent cases. Full scoped accounting: 230 cases/690 phases, 222 passes/eight failures. Four failures are the intended parent callback assertions. Four are unchanged document.open fixture-premise failures on both refs, before the requested control exists; they establish no regression or repair. The old function-name detector also earns no callback-closure credit on the renamed head. The overall review workflow remains red; no all-green claim.

Ruff, format, syntax and scoped Mypy passed. All eight groups have complete source/dependency/collection/JUnit checks in isolated revision environments and final source parity. I independently rehashed 398 loaded source paths across the two refs. No actual Python 3.10 execution or held-out application qualification in this run.

No per-app engine code: yes.
Fix the class: inherited descriptor, WebIDL dictionary and iterator operations share closed native inputs across replay and learning.
Held-out evidence: absent; these are synthetic browser controls motivated by the source finding.

@abrichr abrichr closed this Nov 9, 2026
@abrichr abrichr reopened this Nov 9, 2026
@abrichr

abrichr commented Nov 9, 2026

Copy link
Copy Markdown
Member Author

Retriggering CI: the last run tested a merge ref from before #2122 fixed main's gitleaks.

@abrichr abrichr closed this Nov 9, 2026
@abrichr abrichr reopened this Nov 9, 2026
@abrichr

abrichr commented Nov 10, 2026

Copy link
Copy Markdown
Member Author

Retriggering CI now that #1956 turned main green.

@abrichr abrichr closed this Nov 10, 2026
@abrichr abrichr reopened this Nov 10, 2026
@abrichr

abrichr commented Nov 10, 2026

Copy link
Copy Markdown
Member Author

Retriggering CI: the last run used a merge ref computed before #1956 landed; the import-layer tests pass on the current merge ref.

@abrichr abrichr closed this Nov 10, 2026
@abrichr abrichr reopened this Nov 10, 2026
@abrichr

abrichr commented Nov 10, 2026

Copy link
Copy Markdown
Member Author

Retriggering CI on main with #2152 (line-free import-debt matching).

@abrichr abrichr closed this Nov 10, 2026
@abrichr abrichr reopened this Nov 10, 2026
#1902 adds interruption-recovery code to runtime/replayer.py (20010 to
20175), ir.py (6609 to 6672) and backends/playwright_backend.py (5996 to
6760). Decomposition is tracked separately in DECOMPOSITION.md. Also
regenerate llms-full.txt with scripts/build_llms_full.py after the main
merge.
@abrichr

abrichr commented Nov 10, 2026

Copy link
Copy Markdown
Member Author

Raised three file-size budgets in packages/flow/docs/design/DECOMPOSITION.md to their current line counts: runtime/replayer.py 20010 to 20175, ir.py 6609 to 6672, and backends/playwright_backend.py 5996 to 6760. This PR adds interruption-recovery code to these files. Decomposition is tracked separately. Also regenerated llms-full.txt after the main merge (f6ae479).

…ecovery

# Conflicts:
#	packages/flow/docs/design/DECOMPOSITION.md

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant