Zum Inhalt

Failures and hard texts

← Architecture

When a call to GitHub fails

Every call goes through gh api, in GitHub._gh. When it fails, the GitHubError carries what GitHub said: the HTTP status from the message of gh, gh: Not Found (HTTP 404), and the types of GraphQL's errors from GitHub's JSON, such as NOT_FOUND. Its methods decide what happens next:

The error What happens
missing(): status 404 or 410, or the type NOT_FOUND the thing doesn't exist: no other issue, discussion or release, a commit that no release contains, an issue that was deleted or moved
rate_limited(): a secondary rate limit GitHub refused the call before doing anything, so it runs again after a minute, a write too, up to 3 times; gh never shows the header Retry-After, and a minute is what GitHub asks for without it
transient(): a status of 500 or more, or a broken connection, in the words of gh such as connection reset, connection refused, EOF, no such host or a timeout (NETWORK_FAILURE lists them all) a read runs again after 2, 4 and 8 seconds; a write doesn't, since GitHub may have done it anyway
any other, such as the rate limit of the hour the error goes up to the command

Only the places that look a thing up catch an error, and only when it is missing(). Every other error stops the command: main prints it as one escaped ::error:: and the summary lists the writes of the run before it, and the write that failed. A run again repeats nothing, because every step checks first what is done:

How a run of apply after a failed one avoids a second comment: Apply looks for the comment of an earlier run of the same event. Without one, it adds the labels and possible-duplicate, posts the comment, and adds needs-info last, so that an issue never waits for an answer to a question that GitHub didn't post. With one, it posts nothing: it adds the labels of the answer and the lifecycle labels that the comment named, except the labels that anyone changed since the comment, and needs-info once the reporter answered.How a run of apply after a failed one avoids a second comment: Apply looks for the comment of an earlier run of the same event. Without one, it adds the labels and possible-duplicate, posts the comment, and adds needs-info last, so that an issue never waits for an answer to a question that GitHub didn't post. With one, it posts nothing: it adds the labels of the answer and the lifecycle labels that the comment named, except the labels that anyone changed since the comment, and needs-info once the reporter answered.

The sweep runs each step and each issue through isolate: a GitHubError becomes a ::warning:: and a line of SweepIncomplete, which main reports with what was done before it fails the run. The steps of the sweep check what an earlier one did: a fixed issue loses needs-info and stale and gets fixed-in-next-release before its notice, so a failed notice never leaves it waiting for an answer, a comment of the release, of the closing of an unanswered issue or of a duplicate isn't posted twice, and an issue closes before its labels are removed, so the next sweep still finds one that GitHub didn't close.

Which release closes a fixed issue. mark_fixed reads the pull requests merged in the last 14 days, MERGE_LOOKBACK, a hundred at a time: GitHub lists them by their last update, which a merge is, so the pages end with the first one that nobody updated within the lookback. A busy repository merges far more than a page in that time. The query gives the state of each issue that a pull request fixes, so a closed one, as most are by then, costs no request of the token's 1,000 an hour; the read of an open issue decides. After its labels and before its notice, mark_issue_fixed asks whether a final release published after the merge already contains the merge commit, as when the run of the merge failed, so that a read of the releases that GitHub fails loses only the notice; then close_issue_released closes the issue at once, unless another fix still waits, and a later sweep never announces a fix that a released comment came after. close_issue_released takes the fixes of an issue from closedByPullRequestsReferences, only those of this repository, merged at most MERGE_LOOKBACK and LABEL_SLACK, an hour, before its last labeled event of fixed-in-next-release (a sweep labels the issue later in its run than the start that its lookback counts from), and not before a released comment that came before its last reopened event: the release closed the issue with that fix, and the reporter reopened it. first_release skips the releases published before the last of them was merged, or before the label without a fix or when someone other than github-actions[bot] set it, and compare proves that a release contains each merge commit. Both mark_fixed and close_issue_released drop a pull request whose description denies the keyword that names the issue: denies_closing looks for a negation, or partly or partially, that governs the keyword: the last word of the clause within 80 characters before it, which a punctuation mark or a dash ends, once the fillers of DENIAL_FILLERS, such as yet, fully or on its own, are taken away, and an infinitive of DENIAL_INFINITIVES with its to, such as meant to or be able to; any other word in between, such as and, which or the only of an emphasis, belongs to another thought. mark_fixed names such an issue in the summary of the sweep and logs a notice for it. A link that a maintainer made by hand, with no keyword in the description, still counts. releases reads the newest hundred, as a beta channel publishes a pre-release for each fix.

Reading the text of issues

Anyone can write an issue, and every analysis reads the text of every open issue and of the closed ones that changed last, for the excerpts of the other issues. One text that kept a pattern busy for minutes would stall every analysis until its job timed out. So the time to read a text grows only with its length:

How the assistant reads the text of an issue: The text of an issue or a comment, which anyone can write, is read up to GitHub's 65,536 characters. The answers of a form are cut out between their headings, which are found once; from them come the labels of the form and the reporter's language. For the AI, the HTML goes first, comments and tags in one pass, then the link reference definitions and the footnotes without a reference, which GitHub shows as nothing as well, each with a note in its place, for issue.md and for an excerpt of each other issue, whose form answers then become lines. Then the characters that GitHub shows as nothing go, in one pass, after the HTML, so that none can end an HTML comment that GitHub never ends. The excerpts go into issues.jsonl, and issue.md holds up to 20,000 characters of each text in a code fence that the text can't close.How the assistant reads the text of an issue: The text of an issue or a comment, which anyone can write, is read up to GitHub's 65,536 characters. The answers of a form are cut out between their headings, which are found once; from them come the labels of the form and the reporter's language. For the AI, the HTML goes first, comments and tags in one pass, then the link reference definitions and the footnotes without a reference, which GitHub shows as nothing as well, each with a note in its place, for issue.md and for an excerpt of each other issue, whose form answers then become lines. Then the characters that GitHub shows as nothing go, in one pass, after the HTML, so that none can end an HTML comment that GitHub never ends. The excerpts go into issues.jsonl, and issue.md holds up to 20,000 characters of each text in a code fence that the text can't close.

  • No pattern backtracks. Each one looks at a character a bounded number of times: a form heading is one line, a tag ends on its line, and when no > closes a tag on its line, the rest of the line is passed over in one step.
  • HTML comments go as GitHub hides them. A line that starts with a <!-- that never ends hides the rest of the text, as on GitHub, so instructions hidden that way don't reach the AI either. A <!-- without its end within a line stays text, as GitHub shows it.
  • Definitions go as GitHub hides them. strip_definitions reads the text row by row, as GitHub does: a link reference definition starts a block and can run over the rows of its paragraph, a footnote without a reference takes its paragraph and the indented rows after it, and fenced code stays. A reference counts only outside code, indented rows, link targets, definitions and footnotes, and not after a backslash; since taking out a footnote can let a definition after it start a block, the references are counted again until the rows found stay the same. Each row is read a bounded number of times, also when titles or labels never end: the rows that one read in vain hold nothing that could end another of the same kind.
  • Each text keeps to its place. render_issue puts each text in a code fence one backtick longer than its longest run of backticks, under a heading that only the script writes, so no text can pass for another comment, the maintainer or the assistant; Visible takes out the characters that GitHub shows as nothing before the fence is chosen, so that none can split a run of backticks, and after the HTML, so that none can end an HTML comment that GitHub never ends and show the AI what it hides. The security design shows why.
  • Tests and fuzzing time it. The tests read texts of 64 KiB that took the earlier patterns up to hours, and compare the results with theirs on ordinary texts; fuzz/fuzz_issue_text.py fails when reading a crafted text takes longer than half a second.