From 6a2d5e03fa7108d6371ff52753762784644c1299 Mon Sep 17 00:00:00 2001 From: Michael Denyer <97485362+michael-denyer@users.noreply.github.com> Date: Thu, 8 Oct 2026 09:54:42 +0100 Subject: [PATCH 01/11] fix(reflect): keep the parent's verdict out of reviewer prompts (#232) * fix(reflect): keep the parent's verdict out of reviewer prompts A focus note beside the transcript path stated the parent's diagnosis as fact, and two of three reviewers built on it (#231). reflect now adds nothing beside a transcript path, and the fallback digest records events in turn order without a diagnosis, verdict, or cause. Release 0.9.76. * Keep reflect's fallback digest to one line per event --- .claude-plugin/marketplace.json | 2 +- CHANGES.md | 4 ++++ VERSION | 2 +- package.json | 2 +- plugins/pstack/.claude-plugin/plugin.json | 2 +- plugins/pstack/.codex-plugin/plugin.json | 2 +- plugins/pstack/.github/plugin/plugin.json | 2 +- plugins/pstack/skills/reflect/SKILL.md | 4 ++-- tests/skill-rules.test.mjs | 10 ++++++++++ tools/forks.json | 2 +- 10 files changed, 23 insertions(+), 9 deletions(-) diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 25d153ea4..23e897733 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -10,7 +10,7 @@ "name": "pstack", "source": "./plugins/pstack", "description": "If you want to go fast, go deep first. pstack helps you write less, but higher quality code. rigorous agent workflows you can parallelize with confidence. Original pstack by Lauren Tan (poteto).", - "version": "0.9.75" + "version": "0.9.76" } ] } diff --git a/CHANGES.md b/CHANGES.md index 39dbf3d14..6431a9a0f 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,6 +2,10 @@ This file is the release changelog, with one `## - ` entry per release, newest first. The Cursor-to-Claude rewrite rules live in [`tools/substitutions.json`](tools/substitutions.json), and the [sync boundary](CONTRIBUTING.md#the-sync-boundary) in `CONTRIBUTING.md` defines how a change to upstream's skill content is declared. +## 0.9.76 - keep the parent's verdict out of reflect's reviewer prompts + +reflect passes the transcript path to its reviewers with nothing beside it. When the finder cannot locate the transcript, the fallback digest records the session's prompts, corrections, tool calls, results, and files in turn order, and states no diagnosis, verdict, or cause. A reviewer that reads the parent's conclusion first tends to audit that conclusion instead of the session ([#231](https://github.com/michael-denyer/pstack-claude/issues/231)). + ## 0.9.75 - base delegate worktrees on the branch and separate parallel todolists The Feature playbook notes that Claude Code's `isolation: "worktree"` branches from the remote default branch unless `worktree.baseRef` is `"head"`. A delegate that builds on commits the default branch lacks now gets a worktree the parent creates from `HEAD`, and its brief names that base commit ([#228](https://github.com/michael-denyer/pstack-claude/issues/228)). diff --git a/VERSION b/VERSION index dda193330..8667f8a0c 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.9.75 +0.9.76 diff --git a/package.json b/package.json index ac9c89eea..d9498bc07 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "pstack", "private": true, - "version": "0.9.75", + "version": "0.9.76", "description": "If you want to go fast, go deep first. pstack helps you write less, but higher quality code. rigorous agent workflows you can parallelize with confidence. Pi package of the Claude Code plugin; skills are shared, tool names resolve via skills/poteto-mode/references/pi-tools.md. Original pstack by Lauren Tan (poteto).", "author": { "name": "Michael Denyer", diff --git a/plugins/pstack/.claude-plugin/plugin.json b/plugins/pstack/.claude-plugin/plugin.json index d89856796..387fa4a1a 100644 --- a/plugins/pstack/.claude-plugin/plugin.json +++ b/plugins/pstack/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "pstack", "displayName": "pstack (Claude Code port)", - "version": "0.9.75", + "version": "0.9.76", "description": "If you want to go fast, go deep first. pstack helps you write less, but higher quality code. rigorous agent workflows you can parallelize with confidence. Ported from cursor/plugins/pstack to Claude Code. Original pstack by Lauren Tan (poteto).", "author": { "name": "Michael Denyer", diff --git a/plugins/pstack/.codex-plugin/plugin.json b/plugins/pstack/.codex-plugin/plugin.json index fb4a1b0e2..972d3dea3 100644 --- a/plugins/pstack/.codex-plugin/plugin.json +++ b/plugins/pstack/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "pstack", - "version": "0.9.75", + "version": "0.9.76", "description": "If you want to go fast, go deep first. pstack helps you write less, but higher quality code. rigorous agent workflows you can parallelize with confidence. Codex port of the Claude Code plugin; skills are shared, tool names resolve via skills/poteto-mode/references/codex-tools.md. Original pstack by Lauren Tan (poteto).", "author": { "name": "Michael Denyer", diff --git a/plugins/pstack/.github/plugin/plugin.json b/plugins/pstack/.github/plugin/plugin.json index d7468de62..d100dc3f2 100644 --- a/plugins/pstack/.github/plugin/plugin.json +++ b/plugins/pstack/.github/plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "pstack", "displayName": "pstack (GitHub Copilot port)", - "version": "0.9.75", + "version": "0.9.76", "description": "If you want to go fast, go deep first. pstack helps you write less, but higher quality code. rigorous agent workflows you can parallelize with confidence. Ported from cursor/plugins/pstack to GitHub Copilot CLI and app. Original pstack by Lauren Tan (poteto).", "author": { "name": "Michael Denyer", diff --git a/plugins/pstack/skills/reflect/SKILL.md b/plugins/pstack/skills/reflect/SKILL.md index 9a69ac699..505a9d3c8 100644 --- a/plugins/pstack/skills/reflect/SKILL.md +++ b/plugins/pstack/skills/reflect/SKILL.md @@ -27,7 +27,7 @@ Run the finder at `skills/reflect/scripts/find-transcript.mjs` under the install node <plugin>/skills/reflect/scripts/find-transcript.mjs ~/.claude/projects/<encoded-cwd> "<opening prompt fragment>" ``` -It covers the three layouts (flat `<id>.jsonl`, nested `<id>/<id>.jsonl`, subagent `<parent>/subagents/<child>.jsonl`), newest first, and prints the first path whose opening typed prompt carries the fragment. Do not reimplement the scan by hand: the first line of a transcript is session metadata, not a message, a session that starts with `/clear` or a `!` shell command records that command's wrapper and output as `user` records before the prompt, and files run to several megabytes, so the finder streams each candidate and stops at its first typed `user` record. If it exits 1, write a tight digest of the session and pass that instead. +It covers the three layouts (flat `<id>.jsonl`, nested `<id>/<id>.jsonl`, subagent `<parent>/subagents/<child>.jsonl`), newest first, and prints the first path whose opening typed prompt carries the fragment. Do not reimplement the scan by hand: the first line of a transcript is session metadata, not a message, a session that starts with `/clear` or a `!` shell command records that command's wrapper and output as `user` records before the prompt, and files run to several megabytes, so the finder streams each candidate and stops at its first typed `user` record. If it exits 1, write a tight digest of the session and pass that instead. It lists, one line each in turn order, the user's prompts and corrections, the tool calls and their results, and the files touched. It states no diagnosis, verdict, or cause. The reviewers judge the session, not your conclusions about it. ### 2. Spawn three reviewers in parallel @@ -41,7 +41,7 @@ Each reviewer and the synthesizer name a role line in `pstack-models.md` and a d | Tooling | `reflect tooling` | `references/tooling-reviewer.md` | | Divergent | `reflect judgment, divergent, synthesizer` | `references/divergent-reviewer.md` | -Pass each template verbatim, substituting the transcript path or digest where marked. Reviewers return findings in the `Agent` response body. +Pass each template verbatim, substituting the transcript path or digest where marked. Add nothing beside a transcript path, not even a focus note for a long transcript. Reviewers return findings in the `Agent` response body. ### 3. Synthesize diff --git a/tests/skill-rules.test.mjs b/tests/skill-rules.test.mjs index f426b8d2c..d779da098 100644 --- a/tests/skill-rules.test.mjs +++ b/tests/skill-rules.test.mjs @@ -131,6 +131,16 @@ const rules = [ file: "poteto-mode/SKILL.md", phrase: "Record `BLOCKED: independent review` in the todolist", }, + { + source: "#231 reflect digest carries no verdict", + file: "reflect/SKILL.md", + phrase: "It states no diagnosis, verdict, or cause.", + }, + { + source: "#231 reflect adds nothing beside the transcript path", + file: "reflect/SKILL.md", + phrase: "Add nothing beside a transcript path", + }, ]; describe("port-local skill rules", () => { diff --git a/tools/forks.json b/tools/forks.json index 396a42629..90075cf76 100644 --- a/tools/forks.json +++ b/tools/forks.json @@ -230,7 +230,7 @@ }, "plugins/pstack/skills/reflect/SKILL.md": { "kind": "port-feature", - "why": "Finds the session transcript with the port's find-transcript.mjs under Claude Code's projects directory and drops Cursor's readonly agent mode and inline model column.", + "why": "Finds the session transcript with the port's find-transcript.mjs under Claude Code's projects directory, keeps the parent's conclusions out of the reviewer prompts (a fallback digest records events without a verdict, and nothing goes beside a transcript path), and drops Cursor's readonly agent mode and inline model column.", "since": "0.1.0", "upstream": "not-proposed" }, From a2f7d917021ec6b9fb105a25859044839f9c0e02 Mon Sep 17 00:00:00 2001 From: Michael Denyer <97485362+michael-denyer@users.noreply.github.com> Date: Thu, 8 Oct 2026 12:19:54 +0100 Subject: [PATCH 02/11] feat(pstack): add haiku to the default panel (#233) The panel tier in models.json gains haiku, so arena, architect, and interrogate run four models when no override sheet names the role. Release 0.9.77. --- .claude-plugin/marketplace.json | 2 +- CHANGES.md | 4 ++++ VERSION | 2 +- package.json | 2 +- plugins/pstack/.claude-plugin/plugin.json | 2 +- plugins/pstack/.codex-plugin/plugin.json | 2 +- plugins/pstack/.github/plugin/plugin.json | 2 +- plugins/pstack/models.json | 2 +- plugins/pstack/skills/architect/SKILL.md | 2 +- plugins/pstack/skills/arena/SKILL.md | 4 ++-- plugins/pstack/skills/interrogate/SKILL.md | 1 + plugins/pstack/skills/setup-pstack/SKILL.md | 10 +++++----- 12 files changed, 20 insertions(+), 15 deletions(-) diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 23e897733..116143d73 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -10,7 +10,7 @@ "name": "pstack", "source": "./plugins/pstack", "description": "If you want to go fast, go deep first. pstack helps you write less, but higher quality code. rigorous agent workflows you can parallelize with confidence. Original pstack by Lauren Tan (poteto).", - "version": "0.9.76" + "version": "0.9.77" } ] } diff --git a/CHANGES.md b/CHANGES.md index 6431a9a0f..9196b1677 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,6 +2,10 @@ This file is the release changelog, with one `## <version> - <title>` entry per release, newest first. The Cursor-to-Claude rewrite rules live in [`tools/substitutions.json`](tools/substitutions.json), and the [sync boundary](CONTRIBUTING.md#the-sync-boundary) in `CONTRIBUTING.md` defines how a change to upstream's skill content is declared. +## 0.9.77 - add haiku to the default panel + +The default panel is `opus`, `fable`, `sonnet`, `haiku`. With no override sheet, `arena` and `architect` run four runners, `interrogate` runs four reviewers, with `haiku` as Reviewer D, and the `arena` cross-judge pool includes `haiku`. A `pstack-models.md` sheet that names these roles keeps its own lists. The Codex panel is unchanged. On Pi, `haiku` resolves through the `pi.models` table in `models.json`. + ## 0.9.76 - keep the parent's verdict out of reflect's reviewer prompts reflect passes the transcript path to its reviewers with nothing beside it. When the finder cannot locate the transcript, the fallback digest records the session's prompts, corrections, tool calls, results, and files in turn order, and states no diagnosis, verdict, or cause. A reviewer that reads the parent's conclusion first tends to audit that conclusion instead of the session ([#231](https://github.com/michael-denyer/pstack-claude/issues/231)). diff --git a/VERSION b/VERSION index 8667f8a0c..1b525aba6 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.9.76 +0.9.77 diff --git a/package.json b/package.json index d9498bc07..d5d589e15 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "pstack", "private": true, - "version": "0.9.76", + "version": "0.9.77", "description": "If you want to go fast, go deep first. pstack helps you write less, but higher quality code. rigorous agent workflows you can parallelize with confidence. Pi package of the Claude Code plugin; skills are shared, tool names resolve via skills/poteto-mode/references/pi-tools.md. Original pstack by Lauren Tan (poteto).", "author": { "name": "Michael Denyer", diff --git a/plugins/pstack/.claude-plugin/plugin.json b/plugins/pstack/.claude-plugin/plugin.json index 387fa4a1a..04641e31d 100644 --- a/plugins/pstack/.claude-plugin/plugin.json +++ b/plugins/pstack/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "pstack", "displayName": "pstack (Claude Code port)", - "version": "0.9.76", + "version": "0.9.77", "description": "If you want to go fast, go deep first. pstack helps you write less, but higher quality code. rigorous agent workflows you can parallelize with confidence. Ported from cursor/plugins/pstack to Claude Code. Original pstack by Lauren Tan (poteto).", "author": { "name": "Michael Denyer", diff --git a/plugins/pstack/.codex-plugin/plugin.json b/plugins/pstack/.codex-plugin/plugin.json index 972d3dea3..b1b9b07bf 100644 --- a/plugins/pstack/.codex-plugin/plugin.json +++ b/plugins/pstack/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "pstack", - "version": "0.9.76", + "version": "0.9.77", "description": "If you want to go fast, go deep first. pstack helps you write less, but higher quality code. rigorous agent workflows you can parallelize with confidence. Codex port of the Claude Code plugin; skills are shared, tool names resolve via skills/poteto-mode/references/codex-tools.md. Original pstack by Lauren Tan (poteto).", "author": { "name": "Michael Denyer", diff --git a/plugins/pstack/.github/plugin/plugin.json b/plugins/pstack/.github/plugin/plugin.json index d100dc3f2..7485f3580 100644 --- a/plugins/pstack/.github/plugin/plugin.json +++ b/plugins/pstack/.github/plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "pstack", "displayName": "pstack (GitHub Copilot port)", - "version": "0.9.76", + "version": "0.9.77", "description": "If you want to go fast, go deep first. pstack helps you write less, but higher quality code. rigorous agent workflows you can parallelize with confidence. Ported from cursor/plugins/pstack to GitHub Copilot CLI and app. Original pstack by Lauren Tan (poteto).", "author": { "name": "Michael Denyer", diff --git a/plugins/pstack/models.json b/plugins/pstack/models.json index c0b08bed3..579027205 100644 --- a/plugins/pstack/models.json +++ b/plugins/pstack/models.json @@ -2,7 +2,7 @@ "tiers": { "default": "opus", "strongest": "fable", - "panel": ["opus", "fable", "sonnet"] + "panel": ["opus", "fable", "sonnet", "haiku"] }, "efforts": ["low", "medium", "high", "xhigh", "max"], "defaultEffort": "session", diff --git a/plugins/pstack/skills/architect/SKILL.md b/plugins/pstack/skills/architect/SKILL.md index 5a88db228..fa05f77dc 100644 --- a/plugins/pstack/skills/architect/SKILL.md +++ b/plugins/pstack/skills/architect/SKILL.md @@ -93,7 +93,7 @@ The caller's usage is written first and the type sketch derived from it. One fil Role defaults, stamped from `plugins/pstack/models.json` (edit there, rerun `tools/generate.mjs`). A matching role line in the `pstack-models.md` override sheet overrides each at runtime; `/setup-pstack` writes it and lists its path per runtime. -- architect runners: `opus`, `fable`, `sonnet` +- architect runners: `opus`, `fable`, `sonnet`, `haiku` ## Reasoning effort diff --git a/plugins/pstack/skills/arena/SKILL.md b/plugins/pstack/skills/arena/SKILL.md index 7941564cf..085bc9713 100644 --- a/plugins/pstack/skills/arena/SKILL.md +++ b/plugins/pstack/skills/arena/SKILL.md @@ -77,8 +77,8 @@ One synthesized artifact. One short synthesis note alongside, naming the base, t Role defaults, stamped from `plugins/pstack/models.json` (edit there, rerun `tools/generate.mjs`). A matching role line in the `pstack-models.md` override sheet overrides each at runtime; `/setup-pstack` writes it and lists its path per runtime. -- arena runners: `opus`, `fable`, `sonnet` -- arena cross-judge pool: `opus`, `fable`, `sonnet` +- arena runners: `opus`, `fable`, `sonnet`, `haiku` +- arena cross-judge pool: `opus`, `fable`, `sonnet`, `haiku` ## Reasoning effort diff --git a/plugins/pstack/skills/interrogate/SKILL.md b/plugins/pstack/skills/interrogate/SKILL.md index 704e3ab23..0cd9116a4 100644 --- a/plugins/pstack/skills/interrogate/SKILL.md +++ b/plugins/pstack/skills/interrogate/SKILL.md @@ -43,6 +43,7 @@ Launch all reviewers in a single message using the `Agent` tool. Use the `interr | Reviewer A | `opus` | | Reviewer B | `fable` | | Reviewer C | `sonnet` | +| Reviewer D | `haiku` | For each reviewer: - `subagent_type`: `general-purpose` diff --git a/plugins/pstack/skills/setup-pstack/SKILL.md b/plugins/pstack/skills/setup-pstack/SKILL.md index c731f3a72..6dd8a25ac 100644 --- a/plugins/pstack/skills/setup-pstack/SKILL.md +++ b/plugins/pstack/skills/setup-pstack/SKILL.md @@ -70,11 +70,11 @@ why investigators: opus why synthesizer: opus reflect tooling: opus reflect judgment, divergent, synthesizer: opus -arena runners: opus, fable, sonnet -arena cross-judge pool: opus, fable, sonnet +arena runners: opus, fable, sonnet, haiku +arena cross-judge pool: opus, fable, sonnet, haiku swarm workers: opus -architect runners: opus, fable, sonnet -interrogate reviewers: opus, fable, sonnet +architect runners: opus, fable, sonnet, haiku +interrogate reviewers: opus, fable, sonnet, haiku default effort: session session hook: on @@ -113,7 +113,7 @@ The role lines are the same everywhere. What differs is the sheet path, how the Stamped from `plugins/pstack/models.json` (edit there, rerun `tools/generate.mjs`). - Available Claude models: `opus`, `fable`, `sonnet`, `haiku` -- Default panel: `opus`, `fable`, `sonnet` +- Default panel: `opus`, `fable`, `sonnet`, `haiku` - Reasoning effort levels: `low`, `medium`, `high`, `xhigh`, `max` - Default reasoning effort: `session` - Single-role default: `opus` From 5062f28d9d0cedaed2622a9be82961a7d1df051a Mon Sep 17 00:00:00 2001 From: Michael Denyer <97485362+michael-denyer@users.noreply.github.com> Date: Thu, 8 Oct 2026 12:35:37 +0100 Subject: [PATCH 03/11] feat(pstack): sync to upstream df58112 (v0.15.15) (#234) * feat(pstack): sync to upstream 1e56b29 poteto-help offers /setup-pstack when the model sheet is missing and the answer depends on it. * feat(pstack): sync to upstream df58112 (v0.15.15) Upstream dropped one panel model and lowered its Opus effort. The port keeps its own panel and effort from models.json, and new substitution rules stop an upstream model change from conflicting with forked lines. Release 0.9.78. --- .claude-plugin/marketplace.json | 2 +- CHANGES.md | 8 ++++ VERSION | 2 +- docs/reference.md | 2 +- package.json | 2 +- plugins/pstack/.claude-plugin/plugin.json | 2 +- plugins/pstack/.codex-plugin/plugin.json | 2 +- plugins/pstack/.github/plugin/plugin.json | 2 +- .../architect/references/runner-prompt.md | 2 +- plugins/pstack/skills/blast-radius/SKILL.md | 2 +- plugins/pstack/skills/interrogate/SKILL.md | 4 +- plugins/pstack/skills/poteto-help/SKILL.md | 7 ++- .../poteto-mode/scripts/orch/orch.test.ts | 2 +- tools/substitutions.json | 43 +++++++++++++++++-- tools/upstream.json | 2 +- 15 files changed, 66 insertions(+), 18 deletions(-) diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 116143d73..50bfd4b17 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -10,7 +10,7 @@ "name": "pstack", "source": "./plugins/pstack", "description": "If you want to go fast, go deep first. pstack helps you write less, but higher quality code. rigorous agent workflows you can parallelize with confidence. Original pstack by Lauren Tan (poteto).", - "version": "0.9.77" + "version": "0.9.78" } ] } diff --git a/CHANGES.md b/CHANGES.md index 9196b1677..ce8e741aa 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,6 +2,14 @@ This file is the release changelog, with one `## <version> - <title>` entry per release, newest first. The Cursor-to-Claude rewrite rules live in [`tools/substitutions.json`](tools/substitutions.json), and the [sync boundary](CONTRIBUTING.md#the-sync-boundary) in `CONTRIBUTING.md` defines how a change to upstream's skill content is declared. +## 0.9.78 - sync to upstream df58112 (v0.15.15) + +The upstream pin moves from `2cbf585` to `df58112`, upstream v0.15.15, two commits. `/poteto-help` now offers `/setup-pstack` when the model sheet is missing and the answer depends on it. It asks at most once per chat, and a user who declines is told that every role keeps its default model. Upstream also dropped one model from its default panel and lowered its Opus effort from `max` to `xhigh`. The port takes neither change, because `models.json` sets the port's panel and effort. `setup-pstack` keeps the port's default-effort step in place of upstream's reworded budget step. + +The substitution rules that point a default model list at the Models section now match a list of any length, as does the rule for the model family sentence. New rules cover the delegate defaults in `poteto-mode`, the default-model column of the `reflect` reviewer table, and every slug in `setup-pstack`, so an upstream model change on those lines no longer conflicts with the port. `interrogate` says "the Reviewer labels below" and names no label run, since the generator stamps the table from the panel. + +Measured with `bun tools/sync.mjs pstack 1e56b29`, then `df58112`: 3 files merged, 2 updated, 80 unchanged, 36 excluded, and 2 conflicts, both resolved by hand. `poteto-help` keeps the port's `/swarm` row and takes upstream's `/interrogate` row. No file became port-only. + ## 0.9.77 - add haiku to the default panel The default panel is `opus`, `fable`, `sonnet`, `haiku`. With no override sheet, `arena` and `architect` run four runners, `interrogate` runs four reviewers, with `haiku` as Reviewer D, and the `arena` cross-judge pool includes `haiku`. A `pstack-models.md` sheet that names these roles keeps its own lists. The Codex panel is unchanged. On Pi, `haiku` resolves through the `pi.models` table in `models.json`. diff --git a/VERSION b/VERSION index 1b525aba6..5c648ab70 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.9.77 +0.9.78 diff --git a/docs/reference.md b/docs/reference.md index 159aacab2..cbc2a9b77 100644 --- a/docs/reference.md +++ b/docs/reference.md @@ -231,7 +231,7 @@ CI also checks shell scripts, workflows, Markdown, relative links, and the bundl ### Port scope and attribution -The skill tree is synced against upstream `2cbf585` (v0.15.13). +The skill tree is synced against upstream `df58112` (v0.15.15). This repository ports Lauren Tan's pstack from Cursor to Claude Code and shares the skills with other runtimes. It includes seven cursor-team-kit skills and an independently authored `babysit` skill. The port supplies Claude Code plugin registration and routing, Codex manifests and shortcuts, the Codex tool mapping, the Pi package, extension, and tool mapping, and the GitHub Copilot hooks and tool mapping. diff --git a/package.json b/package.json index d5d589e15..fe8e5d117 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "pstack", "private": true, - "version": "0.9.77", + "version": "0.9.78", "description": "If you want to go fast, go deep first. pstack helps you write less, but higher quality code. rigorous agent workflows you can parallelize with confidence. Pi package of the Claude Code plugin; skills are shared, tool names resolve via skills/poteto-mode/references/pi-tools.md. Original pstack by Lauren Tan (poteto).", "author": { "name": "Michael Denyer", diff --git a/plugins/pstack/.claude-plugin/plugin.json b/plugins/pstack/.claude-plugin/plugin.json index 04641e31d..adc4aafed 100644 --- a/plugins/pstack/.claude-plugin/plugin.json +++ b/plugins/pstack/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "pstack", "displayName": "pstack (Claude Code port)", - "version": "0.9.77", + "version": "0.9.78", "description": "If you want to go fast, go deep first. pstack helps you write less, but higher quality code. rigorous agent workflows you can parallelize with confidence. Ported from cursor/plugins/pstack to Claude Code. Original pstack by Lauren Tan (poteto).", "author": { "name": "Michael Denyer", diff --git a/plugins/pstack/.codex-plugin/plugin.json b/plugins/pstack/.codex-plugin/plugin.json index b1b9b07bf..26fa5bdfc 100644 --- a/plugins/pstack/.codex-plugin/plugin.json +++ b/plugins/pstack/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "pstack", - "version": "0.9.77", + "version": "0.9.78", "description": "If you want to go fast, go deep first. pstack helps you write less, but higher quality code. rigorous agent workflows you can parallelize with confidence. Codex port of the Claude Code plugin; skills are shared, tool names resolve via skills/poteto-mode/references/codex-tools.md. Original pstack by Lauren Tan (poteto).", "author": { "name": "Michael Denyer", diff --git a/plugins/pstack/.github/plugin/plugin.json b/plugins/pstack/.github/plugin/plugin.json index 7485f3580..b5a2e1699 100644 --- a/plugins/pstack/.github/plugin/plugin.json +++ b/plugins/pstack/.github/plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "pstack", "displayName": "pstack (GitHub Copilot port)", - "version": "0.9.77", + "version": "0.9.78", "description": "If you want to go fast, go deep first. pstack helps you write less, but higher quality code. rigorous agent workflows you can parallelize with confidence. Ported from cursor/plugins/pstack to GitHub Copilot CLI and app. Original pstack by Lauren Tan (poteto).", "author": { "name": "Michael Denyer", diff --git a/plugins/pstack/skills/architect/references/runner-prompt.md b/plugins/pstack/skills/architect/references/runner-prompt.md index 580ec47a5..b1ad21aef 100644 --- a/plugins/pstack/skills/architect/references/runner-prompt.md +++ b/plugins/pstack/skills/architect/references/runner-prompt.md @@ -17,4 +17,4 @@ Apply the following discipline. The orchestrator compares candidates on these ax - Idempotent state transitions where applicable, per the **make-operations-idempotent** principle skill. Ask what happens if the operation runs twice or crashes halfway. - Short call chains. If tracing the flow needs more than three files, flatten the hierarchy, per the **laziness-protocol** and **minimize-reader-load** principle skills. -You are one of several runners, each on a different model. Produce the best design your model can make. Don't hedge against the others. Differences between candidates are the signal used to pick a base and graft. Converging on a safe-looking middle defeats the exploration. +You are one of the parallel runners, each on a different model. Produce the best design your model can make. Don't hedge against the others. Differences between candidates are the signal used to pick a base and graft. Converging on a safe-looking middle defeats the exploration. diff --git a/plugins/pstack/skills/blast-radius/SKILL.md b/plugins/pstack/skills/blast-radius/SKILL.md index b44b5e813..8ee5a95c4 100644 --- a/plugins/pstack/skills/blast-radius/SKILL.md +++ b/plugins/pstack/skills/blast-radius/SKILL.md @@ -34,7 +34,7 @@ Step 4 is usually one small script that imports the same library the app ships a 3. Look where grep stops. Read the source of the library you call, and check its pinned version and any local patch. Work out when things run: microtasks, unmount and teardown, Solid versus React. Follow what a symbol search misses: the JSON an API returns, a DB column, a wire format, another language reading the same bytes, a feature flag, code three hops downstream. 4. Be honest about each risk. Give it a real chance of happening and a real cost if it does. Keep the risks you confirmed. List the ones you checked and cleared separately. Same rules as `why`. Cite a real `file:line`, a search that finds nothing is still an answer, and never make up a caller or an API. 5. Prove the one fact. Write a script or test that runs the real code, run it, and paste what happened. -6. For a big or wide change, run it as an `arena`. Ask several models the same question and merge the answers. Different models catch different real bugs. +6. For a big or wide change, run it as an `arena`. Ask more than one model the same question and merge the answers. Different models catch different real bugs. ## What to hand back diff --git a/plugins/pstack/skills/interrogate/SKILL.md b/plugins/pstack/skills/interrogate/SKILL.md index 0cd9116a4..e258bdb29 100644 --- a/plugins/pstack/skills/interrogate/SKILL.md +++ b/plugins/pstack/skills/interrogate/SKILL.md @@ -36,7 +36,7 @@ Write one clear paragraph. If you're unsure about the intent, ask the user befor ## Step 3, Spawn Reviewers -Launch all reviewers in a single message using the `Agent` tool. Use the `interrogate reviewers` line in the `pstack-models.md` override sheet (`/setup-pstack` lists its path per runtime), one reviewer per entry, extending or shrinking the Reviewer A/B/C labels below to the configured entry count. If the sheet or that line is missing, use the table defaults. +Launch all reviewers in a single message using the `Agent` tool. Use the `interrogate reviewers` line in the `pstack-models.md` override sheet (`/setup-pstack` lists its path per runtime), one reviewer per entry, extending or shrinking the Reviewer labels below to the configured entry count. If the sheet or that line is missing, use the table defaults. | Subagent | Default model | |----------|---------------| @@ -50,7 +50,7 @@ For each reviewer: - `model`: the configured `interrogate reviewers` entry, or the table default with no configured line. For an `auto` or `inherit-parent` entry, omit `model` so that reviewer runs on the parent model. - `readonly`: `true` -If the `Agent` tool rejects a configured entry, run that reviewer on the table default of its family and say so. Families go by model name, such as Opus, Fable, or Sonnet. With no family match, use Reviewer A's default. If it rejects a table default, check the valid slugs in the `Agent` tool's error message, pick the closest equivalent (prefer the highest-reasoning tier of the same family), spawn with it, and open a separate PR to update the default table. Do not block the review on the slug issue. Never treat an alias entry as a rejected slug or apply either fallback to it. +If the `Agent` tool rejects a configured entry, run that reviewer on the table default of its family and say so. Families go by model name, such as Opus, Fable, or Sonnet. With no family match, use Reviewer A's default. If it rejects a table default, check the valid slugs in the `Agent` tool's error message, pick the closest equivalent (prefer the same family and reasoning tier), spawn with it, and open a separate PR to update the default table. Do not block the review on the slug issue. Never treat an alias entry as a rejected slug or apply either fallback to it. Read `references/reviewer-prompt.md` and fill in the template with: 1. The stated intent diff --git a/plugins/pstack/skills/poteto-help/SKILL.md b/plugins/pstack/skills/poteto-help/SKILL.md index 48fd02a3f..b0c0befae 100644 --- a/plugins/pstack/skills/poteto-help/SKILL.md +++ b/plugins/pstack/skills/poteto-help/SKILL.md @@ -26,6 +26,11 @@ Check the state that changes the answer, and mention it only when it does: - For model or routing questions, identify the runtime and installation type, then read [`setup-pstack`](../setup-pstack/SKILL.md#other-runtimes) for the sheet's location and how that runtime loads it. Check that configuration before saying whether defaults or overrides apply. - No project `verify` or `verify-*` skill or other app harness means agents have no scripted way to drive the app. Mention `/create-verification-skill` when the question is about proving a change works. +When the model sheet is missing and it matters, ask whether the user wants to pick a model for each role and a reasoning budget now. It matters when the user is new, the question is about setup or cost, or the answer depends on which models run. Ask at most once per chat. If the need is also unclear, ask both questions together. Offer two choices: + +- Now: give them `/setup-pstack` to type, and answer their question too. +- Later: answer their question, and add one line saying every role keeps its default model until they run `/setup-pstack`. + ## Get set up 1. Identify the user's runtime and whether they want a native plugin/package or skills-only installation. Read its section in the [README](https://github.com/michael-denyer/pstack-claude/blob/main/README.md#install) or [shared installation reference](https://github.com/michael-denyer/pstack-claude/blob/main/docs/reference.md#shared-skills-installation), then give the matching install command. @@ -57,7 +62,7 @@ The default answer is `/poteto-mode`, which runs most of the others when its ste | Settle types and module shape before code that crosses a function boundary | [`/architect`](../architect/SKILL.md) | | Get several attempts at one brief, merged into the best one | [`/arena`](../arena/SKILL.md) | | Run parallel checks over slices, or race workers | [`/swarm`](../swarm/SKILL.md) | -| Have several models review a diff and try to break it | [`/interrogate`](../interrogate/SKILL.md) | +| Have different models review a diff and try to break it | [`/interrogate`](../interrogate/SKILL.md) | | Fix a bug test-first when a cheap local test exists | [`/tdd`](../tdd/SKILL.md) | | Apply TypeScript rules to `.ts` or `.tsx` work | [`/typescript-best-practices`](../typescript-best-practices/SKILL.md) | | Strip comments before review, using a reviewer that didn't write them | [`/no-comments`](../no-comments/SKILL.md) | diff --git a/plugins/pstack/skills/poteto-mode/scripts/orch/orch.test.ts b/plugins/pstack/skills/poteto-mode/scripts/orch/orch.test.ts index a91257e4d..8faa38159 100644 --- a/plugins/pstack/skills/poteto-mode/scripts/orch/orch.test.ts +++ b/plugins/pstack/skills/poteto-mode/scripts/orch/orch.test.ts @@ -371,7 +371,7 @@ describe("Store", () => { sha: "abc123", verdict: "unit-test-verified", evidence: "reports/verify.md", - verifier: "sol", + verifier: "opus", }); expect(await store.ledger.check({ pr: 184530, sha: "abc123" })).toEqual( recorded diff --git a/tools/substitutions.json b/tools/substitutions.json index 334527247..a420305cf 100644 --- a/tools/substitutions.json +++ b/tools/substitutions.json @@ -48,13 +48,13 @@ "rationale": "where Claude Code exposes its MCP servers" }, { - "regex": "default to one each on `(?:claude|gpt|grok|gemini|composer)-[^`]+`, `(?:claude|gpt|grok|gemini|composer)-[^`]+`, `(?:claude|gpt|grok|gemini|composer)-[^`]+`\\.", + "regex": "default to one each on `(?:claude|gpt|grok|gemini|composer)-[^`]+`(?:(?:,| and|, and) `(?:claude|gpt|grok|gemini|composer)-[^`]+`)*\\.", "files": "^skills/arena/SKILL\\.md$", "replacement": "run one each on the defaults in [Models](#models).", "rationale": "the runner defaults are stamped into the Models section" }, { - "regex": "choose from `(?:claude|gpt|grok|gemini|composer)-[^`]+`, `(?:claude|gpt|grok|gemini|composer)-[^`]+`, `(?:claude|gpt|grok|gemini|composer)-[^`]+`\\.", + "regex": "choose from `(?:claude|gpt|grok|gemini|composer)-[^`]+`(?:(?:,| and|, and) `(?:claude|gpt|grok|gemini|composer)-[^`]+`)*\\.", "files": "^skills/arena/SKILL\\.md$", "replacement": "choose from the runner defaults in [Models](#models).", "rationale": "the cross-judge pool defaults to the stamped runners" @@ -66,7 +66,7 @@ "rationale": "the fallback model is the stamped single-role default" }, { - "regex": "use `(?:claude|gpt|grok|gemini|composer)-[^`]+`, `(?:claude|gpt|grok|gemini|composer)-[^`]+`, `(?:claude|gpt|grok|gemini|composer)-[^`]+`\\.", + "regex": "use `(?:claude|gpt|grok|gemini|composer)-[^`]+`(?:(?:,| and|, and) `(?:claude|gpt|grok|gemini|composer)-[^`]+`)*\\.", "files": "^skills/architect/SKILL\\.md$", "replacement": "use the defaults in [Models](#models).", "rationale": "the runner defaults are stamped into the Models section" @@ -235,6 +235,35 @@ "replacement": ", default in [Models](#models)", "rationale": "a role bullet's default model is stamped into the skill's Models section, never hard-coded in prose" }, + { + "regex": "Defaults `(?:claude|gpt|grok|gemini|composer)-[^`]+` for code, `(?:claude|gpt|grok|gemini|composer)-[^`]+` for prose and judgment", + "files": "^skills/poteto-mode/SKILL\\.md$", + "replacement": "Role defaults in [Models](#models), with \"judgment and prose\" covering prose and judgment", + "rationale": "the delegate defaults are stamped into poteto-mode's Models section, so an upstream model bump on this line is not a change to the port" + }, + { + "regex": "strongest judgment model \\(`(?:claude|gpt|grok|gemini|composer)-[^`]+`\\)", + "files": "^skills/poteto-mode/SKILL\\.md$", + "replacement": "strongest-judgment model (default in [Models](#models))", + "rationale": "same line, the strongest-judgment default" + }, + { + "pattern": "omit Task `model`", + "replacement": "omit `model` on the `Agent` call", + "rationale": "Claude Code's subagent tool is `Agent`, and the bare tool name is on the denylist" + }, + { + "regex": " \\| `(?:claude|gpt|grok|gemini|composer)-[^`]+` \\| `references/", + "files": "^skills/reflect/SKILL\\.md$", + "replacement": " | default in [Models](#models) | `references/", + "rationale": "a reviewer row's default model is stamped into reflect's Models section, so an upstream model bump in the table is not a change to the port" + }, + { + "regex": "(?:claude|gpt|grok|gemini|composer)-[0-9a-z.]+(?:-[0-9a-z.]+)+", + "files": "^skills/setup-pstack/SKILL\\.md$", + "replacement": "<slug>", + "rationale": "the port writes its own sheet from models.json and replaces Cursor's budget step, so every upstream slug in setup-pstack is scrubbed to the placeholder the port already uses; a conflict there can then be written and resolved, and a model bump alone is no conflict. Must follow the default-phrase rules above" + }, { "pattern": "the `pstack-models.mdc` rule and a default.", "replacement": "`pstack-models.md` and a default in [Models](#models).", @@ -256,10 +285,16 @@ "rationale": "same sentence, the other wording upstream uses" }, { - "pattern": "Families go by prefix: `claude-*`, `gpt-*`, and `grok-*`.", + "regex": "Families go by prefix: `[a-z]+-\\*`(?:(?:,| and|, and) `[a-z]+-\\*`)*\\.", "replacement": "Families go by model name, such as Opus, Fable, or Sonnet.", "rationale": "Claude Code names models by family, not by slug prefix" }, + { + "regex": "the Reviewer A(?:/[B-Z])+ labels below", + "files": "^skills/interrogate/SKILL\\.md$", + "replacement": "the Reviewer labels below", + "rationale": "the reviewer table is stamped from the panel in models.json, so the prose names no label run that a panel change would make wrong" + }, { "pattern": ".cursor/skills/", "replacement": ".claude/skills/", diff --git a/tools/upstream.json b/tools/upstream.json index 6bc4d71c6..9758b1f36 100644 --- a/tools/upstream.json +++ b/tools/upstream.json @@ -4,7 +4,7 @@ "pstack": { "upstreamPath": "pstack", "localPath": "plugins/pstack", - "sha": "2cbf585", + "sha": "df58112", "exclude": [ ".cursor-plugin/", ".gitignore", From 29eb31cb8e198c62dd9cff4027e6c4f0f2f63d0b Mon Sep 17 00:00:00 2001 From: Michael Denyer <97485362+michael-denyer@users.noreply.github.com> Date: Thu, 8 Oct 2026 12:56:30 +0100 Subject: [PATCH 04/11] fix(sync): keep the period after a setup-pstack slug (#235) * test(sync): pin the period after a setup-pstack slug * fix(sync): keep the period after a setup-pstack slug The slug rule's last segment admits dots, so a bare slug that ended a sentence took the period into <slug>. A lookbehind stops the match before a trailing dot. * docs(pstack): name the default effort in poteto-help and correct the 0.9.78 sync counts poteto-help offered a reasoning budget, a step the port's setup-pstack does not have. The 0.9.78 entry listed counts that summed to 87 over 82 synced files, so it now states what the two sync commits show. --- CHANGES.md | 2 +- plugins/pstack/skills/poteto-help/SKILL.md | 2 +- tests/sync.test.mjs | 5 +++++ tools/substitutions.json | 2 +- 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index ce8e741aa..bd9c36e2c 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -8,7 +8,7 @@ The upstream pin moves from `2cbf585` to `df58112`, upstream v0.15.15, two commi The substitution rules that point a default model list at the Models section now match a list of any length, as does the rule for the model family sentence. New rules cover the delegate defaults in `poteto-mode`, the default-model column of the `reflect` reviewer table, and every slug in `setup-pstack`, so an upstream model change on those lines no longer conflicts with the port. `interrogate` says "the Reviewer labels below" and names no label run, since the generator stamps the table from the panel. -Measured with `bun tools/sync.mjs pstack 1e56b29`, then `df58112`: 3 files merged, 2 updated, 80 unchanged, 36 excluded, and 2 conflicts, both resolved by hand. `poteto-help` keeps the port's `/swarm` row and takes upstream's `/interrogate` row. No file became port-only. +Measured with `bun tools/sync.mjs pstack 1e56b29`, then `df58112`, over 82 synced files with 36 excluded: the first run changed `poteto-help`, and the second changed five files, `poteto-help` among them. Two conflicts were resolved by hand. `poteto-help` keeps the port's `/swarm` row and takes upstream's `/interrogate` row. No file became port-only. ## 0.9.77 - add haiku to the default panel diff --git a/plugins/pstack/skills/poteto-help/SKILL.md b/plugins/pstack/skills/poteto-help/SKILL.md index b0c0befae..c1ef78558 100644 --- a/plugins/pstack/skills/poteto-help/SKILL.md +++ b/plugins/pstack/skills/poteto-help/SKILL.md @@ -26,7 +26,7 @@ Check the state that changes the answer, and mention it only when it does: - For model or routing questions, identify the runtime and installation type, then read [`setup-pstack`](../setup-pstack/SKILL.md#other-runtimes) for the sheet's location and how that runtime loads it. Check that configuration before saying whether defaults or overrides apply. - No project `verify` or `verify-*` skill or other app harness means agents have no scripted way to drive the app. Mention `/create-verification-skill` when the question is about proving a change works. -When the model sheet is missing and it matters, ask whether the user wants to pick a model for each role and a reasoning budget now. It matters when the user is new, the question is about setup or cost, or the answer depends on which models run. Ask at most once per chat. If the need is also unclear, ask both questions together. Offer two choices: +When the model sheet is missing and it matters, ask whether the user wants to pick a model for each role and a default effort now. It matters when the user is new, the question is about setup or cost, or the answer depends on which models run. Ask at most once per chat. If the need is also unclear, ask both questions together. Offer two choices: - Now: give them `/setup-pstack` to type, and answer their question too. - Later: answer their question, and add one line saying every role keeps its default model until they run `/setup-pstack`. diff --git a/tests/sync.test.mjs b/tests/sync.test.mjs index f9fab44ff..e184a256d 100644 --- a/tests/sync.test.mjs +++ b/tests/sync.test.mjs @@ -188,6 +188,11 @@ describe("applySubstitutions", () => { "Families go by prefix: `claude-*`, `gpt-*`, and `grok-*`.", "Families go by model name, such as Opus, Fable, or Sonnet.", ], + [ + "skills/setup-pstack/SKILL.md", + "One runner is claude-opus-5-5-xhigh and one is gpt-5.5-high-fast. The last is grok-4.7-medium-fast.", + "One runner is <slug> and one is <slug>. The last is <slug>.", + ], [ "skills/reflect/SKILL.md", "One message, three `Task` calls, `subagent_type: generalPurpose`, with `model` set as below.", diff --git a/tools/substitutions.json b/tools/substitutions.json index a420305cf..65c0885e4 100644 --- a/tools/substitutions.json +++ b/tools/substitutions.json @@ -259,7 +259,7 @@ "rationale": "a reviewer row's default model is stamped into reflect's Models section, so an upstream model bump in the table is not a change to the port" }, { - "regex": "(?:claude|gpt|grok|gemini|composer)-[0-9a-z.]+(?:-[0-9a-z.]+)+", + "regex": "(?:claude|gpt|grok|gemini|composer)-[0-9a-z.]+(?:-[0-9a-z.]+)+(?<!\\.)", "files": "^skills/setup-pstack/SKILL\\.md$", "replacement": "<slug>", "rationale": "the port writes its own sheet from models.json and replaces Cursor's budget step, so every upstream slug in setup-pstack is scrubbed to the placeholder the port already uses; a conflict there can then be written and resolved, and a model bump alone is no conflict. Must follow the default-phrase rules above" From 2eeecfcf494428082dad281104634f2644400c82 Mon Sep 17 00:00:00 2001 From: Michael Denyer <97485362+michael-denyer@users.noreply.github.com> Date: Thu, 8 Oct 2026 22:04:51 +0100 Subject: [PATCH 05/11] fix(watch-pr): reject an --interval longer than one timer can sleep (#236) A timer delay above 2^31 - 1 ms is replaced with 1 ms, so --interval above 2147483.647 seconds made every poll sleep return at once. The parser now refuses it. The Lean model of the polling arithmetic comes with the change; its sleep bound assumes a sleep lasts as long as asked, which this keeps true for the CLI. --- .gitignore | 3 + lean/Model/WatchPoll.lean | 1082 +++++++ lean/Model/WatchPoll.mutations | 103 + lean/Model/WatchPoll.statements | 2758 +++++++++++++++++ lean/lake-manifest.json | 6 + lean/lakefile.toml | 6 + lean/lean-toolchain | 1 + .../poteto-mode/scripts/watch-pr/cli.test.ts | 12 + .../poteto-mode/scripts/watch-pr/cli.ts | 11 +- tools/forks.json | 4 +- 10 files changed, 3983 insertions(+), 3 deletions(-) create mode 100644 lean/Model/WatchPoll.lean create mode 100644 lean/Model/WatchPoll.mutations create mode 100644 lean/Model/WatchPoll.statements create mode 100644 lean/lake-manifest.json create mode 100644 lean/lakefile.toml create mode 100644 lean/lean-toolchain diff --git a/.gitignore b/.gitignore index bd1e9cf8c..720d7372a 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,6 @@ plugins/pstack/skills/poteto-mode/scripts/node_modules/ # bun installs the typebox peer for the Pi extension tests /node_modules/ + +# Lake build output for the Lean models +/lean/.lake/ diff --git a/lean/Model/WatchPoll.lean b/lean/Model/WatchPoll.lean new file mode 100644 index 000000000..0bab8ad10 --- /dev/null +++ b/lean/Model/WatchPoll.lean @@ -0,0 +1,1082 @@ +/-! +This model checks the sequential arithmetic of the watch-pr polling loop. + +Sources, in `plugins/pstack/skills/poteto-mode/scripts/watch-pr/`: + +* `policy.ts:399-402` `queryBackoffSeconds` +* `policy.ts:509-562` `pollUntilTerminal` +* `policy.ts:63-85` `NO_CHECKS_CONFIRM_SECONDS`, `noChecksConfirmer` +* `deadline.ts:9-20` `WatchDeadline`, `WatchDeadline.remaining` +* `cli.ts:37-63,122-145,181-192` the option ranges and the real clock + +Assumptions the model makes. The code does not guarantee the ones marked (!). + +1. (!) Every JavaScript number is a whole, non-negative count of seconds, so it + is a `Nat`. The CLI accepts `0 < --interval <= 2147483.647` and any finite + `--timeout >= 0`, fractions included (`cli.ts:37-57`), and the real clock is + `performance.now() / 1_000` (`cli.ts:182,187`), which is fractional. +2. `2 ** (failures - 1)` is an exact natural power here. JavaScript overflows it + to `Infinity` near `failures = 1025`. `Math.max(interval, 60)` is at least 60, + so the product is `Infinity`, never `0 * Infinity = NaN`, and + `Math.min(Infinity, 300)` is 300. The model returns 300 there too. +3. `failures - 1` is truncated in `Nat`, so `queryBackoffSeconds` is a faithful + transcription only for `failures >= 1`. JavaScript computes `2 ** -1 = 0.5` at + `failures = 0`; `queryBackoffHalfSeconds` carries that case in half-seconds. +4. `Infinity` (the deadline of `--timeout 0`, `deadline.ts:15`) is + `Seconds.infinity`. +5. (!) One clock that never runs backwards serves `WatchDeadline.remaining` and + `clock.sleep`. `RunDependencies` takes them as two separate objects. +6. (!) `deadline.remaining()` and the `clock.sleep` call that uses it happen at + one instant. `policy.ts:541-549` emits a verdict between them. +7. The step result is the environment's choice: any outcome, taking any time, + zero included. `StepOutcome` lists the six ways `await args.step()` can end. +8. A clock that advances by at least the slept amount is the theorem hypothesis + `HonestClock`/`HonestRun`; the model itself lets the clock stand still. The + real `clock.sleep` (`cli.ts:189-191`) meets it only for a delay a timer can + hold, at most 2^31 - 1 ms. Every sleep is at most `--interval` or the 300 + second backoff cap, and `sleepSeconds` (`cli.ts:43-51`) rejects a longer + `--interval`. +9. A head SHA is a `Nat`; the code only compares SHAs with `===`. The `Map` + keyed by PR number is a total function to `Option`. +10. `State.retries` and `State.slept` are ghost counters. The code has neither. + +`WatchPoll.mutations` lists the bugs this model must detect. +-/ + +namespace WatchPoll + +/-! ## 1. `queryBackoffSeconds`, policy.ts:399-402 -/ + +/-- policy.ts:402 `Math.min(Math.max(interval, 60) * 2 ** (failures - 1), 300)`. +Faithful for `failures ≥ 1` (assumption 3). -/ +def queryBackoffSeconds (interval failures : Nat) : Nat := + min (max interval 60 * 2 ^ (failures - 1)) 300 + +/-- Twice the JavaScript result of policy.ts:402, for every `failures ≥ 0`: +`2 * min (m * 2 ^ (f - 1)) 300 = min (m * 2 ^ f) 600` over the reals. Half-seconds +keep `failures = 0`, where JavaScript multiplies by `0.5`, inside `Nat`. -/ +def queryBackoffHalfSeconds (interval failures : Nat) : Nat := + min (max interval 60 * 2 ^ failures) 600 + +/-- What `queryBackoffSeconds` guarantees at one input with `failures ≥ 1`: a +floor of 60, a cap of 300, no decrease at the next failure, the cap from the +fourth failure on, the first wait, doubling, and agreement with the half-second +form. -/ +def BackoffSpec (interval failures : Nat) : Prop := + 60 ≤ queryBackoffSeconds interval failures ∧ + queryBackoffSeconds interval failures ≤ 300 ∧ + queryBackoffSeconds interval failures ≤ queryBackoffSeconds interval (failures + 1) ∧ + (4 ≤ failures → queryBackoffSeconds interval failures = 300) ∧ + (failures = 1 → queryBackoffSeconds interval failures = min (max interval 60) 300) ∧ + queryBackoffSeconds interval (failures + 1) = min (2 * queryBackoffSeconds interval failures) 300 ∧ + queryBackoffHalfSeconds interval failures = 2 * queryBackoffSeconds interval failures + +instance (interval failures : Nat) : Decidable (BackoffSpec interval failures) := by + unfold BackoffSpec; infer_instance + +/-- Intervals around every threshold of the function: the floor 60, the values +75, 150 and 300 that reach the cap one failure sooner, 120 and 600 for the +`failures = 0` case, and the largest whole number a double holds exactly. -/ +def backoffIntervals : List Nat := + [0, 1, 30, 59, 60, 61, 74, 75, 76, 119, 120, 121, 149, 150, 151, 299, 300, 301, 599, 600, 601, + 3600, 9007199254740991] + +/-- Failure counts from 1 past the cap, and around JavaScript's overflow of +`2 ** (failures - 1)` at 1024. -/ +def backoffFailures : List Nat := [1, 2, 3, 4, 5, 6, 7, 8, 16, 53, 64, 1023, 1024, 1025, 1100] + +/-- Inputs in the bound where `BackoffSpec` fails. -/ +def badBackoff : List (Nat × Nat) := + (backoffIntervals.flatMap fun interval => backoffFailures.map fun failures => (interval, failures)).filter + fun (interval, failures) => ¬ BackoffSpec interval failures + +#eval badBackoff -- [] +#guard badBackoff.isEmpty + +-- Reach: the bound holds values under the cap and at it, and 4 is the first +-- failure count that gives 300 for every interval. +#guard queryBackoffSeconds 60 1 == 60 && queryBackoffSeconds 60 3 == 240 && queryBackoffSeconds 60 4 == 300 +#guard (backoffIntervals.filter fun interval => queryBackoffSeconds interval 3 < 300) == [0, 1, 30, 59, 60, 61, 74] + +-- `failures = 0`: (interval, twice the JavaScript result). The floor of 60 seconds +-- (120 half-seconds) fails for every interval under 120. +#eval backoffIntervals.map fun interval => (interval, queryBackoffHalfSeconds interval 0) +#guard (backoffIntervals.filter fun interval => queryBackoffHalfSeconds interval 0 < 2 * 60) + == [0, 1, 30, 59, 60, 61, 74, 75, 76, 119] + +/-! ## 2. `WatchDeadline`, deadline.ts:9-20, and `pollUntilTerminal`, policy.ts:509-562 -/ + +/-- A JavaScript number of seconds that may be `Infinity`. -/ +inductive Seconds + | finite (seconds : Nat) + | infinity +deriving Repr, DecidableEq + +/-- deadline.ts:9-16. -/ +structure WatchDeadline where + expiresAt : Seconds +deriving Repr, DecidableEq + +/-- deadline.ts:15 `timeout > 0 ? now() + timeout : Infinity`. -/ +def WatchDeadline.create (timeout now : Nat) : WatchDeadline := + ⟨if timeout > 0 then .finite (now + timeout) else .infinity⟩ + +/-- deadline.ts:17-19 `Math.max(0, this.expiresAt - this.now())`. Truncated `Nat` +subtraction is the `Math.max(0, ·)`. -/ +def WatchDeadline.remaining (d : WatchDeadline) (now : Nat) : Seconds := + match d.expiresAt with + | .finite expiresAt => .finite (expiresAt - now) + | .infinity => .infinity + +/-- policy.ts:519 `deadline.remaining() > 0`. -/ +def Seconds.positive : Seconds → Bool + | .finite remaining => decide (0 < remaining) + | .infinity => true + +/-- policy.ts:537-540 and 557 `Math.min(seconds, deadline.remaining())`. -/ +def Seconds.cap (remaining : Seconds) (seconds : Nat) : Nat := + match remaining with + | .finite r => min seconds r + | .infinity => seconds + +/-- `deadline.remaining()` after the clock advances by `seconds`. -/ +def Seconds.elapse (remaining : Seconds) (seconds : Nat) : Seconds := + match remaining with + | .finite r => .finite (r - seconds) + | .infinity => .infinity + +-- deadline.ts at its boundaries: a timeout of 0 never expires; a positive one +-- counts down to 0 and stays there. +#guard (WatchDeadline.create 0 7).remaining 1000 == .infinity +#guard (WatchDeadline.create 30 7).remaining 7 == .finite 30 && (WatchDeadline.create 30 7).remaining 17 == .finite 20 +#guard (WatchDeadline.create 30 7).remaining 37 == .finite 0 && (WatchDeadline.create 30 7).remaining 100 == .finite 0 +#guard (Seconds.finite 20).elapse 5 == .finite 15 && (Seconds.finite 20).elapse 25 == .finite 0 + && Seconds.infinity.elapse 5 == .infinity + +/-- `seconds ≤ remaining`, with `Infinity` above every number. -/ +def Seconds.covers (remaining : Seconds) (seconds : Nat) : Prop := + match remaining with + | .finite r => seconds ≤ r + | .infinity => True + +instance : (remaining : Seconds) → (seconds : Nat) → Decidable (remaining.covers seconds) + | .finite r, seconds => inferInstanceAs (Decidable (seconds ≤ r)) + | .infinity, _ => inferInstanceAs (Decidable True) + +/-- The fields of `T.PollingOptions` that `pollUntilTerminal` reads. -/ +structure PollingOptions where + interval : Nat + maxQueryErrors : Nat +deriving Repr, DecidableEq + +/-- How one `await args.step()` at policy.ts:522 ends. -/ +inductive StepOutcome + | terminal -- returned `{ kind: "terminal" }` + | sleep (seconds : Nat) -- returned `{ kind: "sleep", seconds }` + | continue_ -- returned `{ kind: "continue" }` + | queryError (retryable : Bool) -- threw `WatcherQueryError` + | deadlineExceeded -- threw `DeadlineExceeded` + | otherError -- threw anything else +deriving Repr, DecidableEq + +/-- The step returned, so policy.ts:523 `failures = 0` ran. -/ +def StepOutcome.succeeded : StepOutcome → Bool + | .terminal | .sleep _ | .continue_ => true + | _ => false + +/-- What the last pass of the loop did. -/ +inductive Last + | start -- no pass yet + | sleptPoll (seconds : Nat) -- policy.ts:556 `clock.sleep(seconds)` + | sleptRetry (seconds : Nat) -- policy.ts:550 `clock.sleep(seconds)` + | continued -- `continue` result: no sleep + | returnedVerdict -- policy.ts:553 + | returnedStatusQuery (failures : Nat) -- policy.ts:536 + | returnedOnDeadline -- policy.ts:561, from 519 or the `break` at 525 + | threw -- policy.ts:526 +deriving Repr, DecidableEq + +/-- The loop is back at policy.ts:519. -/ +def Last.running : Last → Bool + | .start | .sleptPoll _ | .sleptRetry _ | .continued => true + | _ => false + +/-- The argument of the `clock.sleep` call the last pass made. -/ +def Last.sleepSeconds : Last → Option Nat + | .sleptPoll seconds | .sleptRetry seconds => some seconds + | _ => none + +structure State where + failures : Nat -- policy.ts:515 + remaining : Seconds -- `deadline.remaining()` now + last : Last + retries : Nat -- ghost: `clock.sleep` calls at policy.ts:550 since the last successful step + slept : Nat -- ghost: sum of every `clock.sleep` argument +deriving Repr, DecidableEq + +/-- The environment's part of one pass: how the step ended, how far the clock +moved while it ran, and how far it moved during the sleep the pass makes. -/ +structure Event where + outcome : StepOutcome + stepElapsed : Nat + sleepElapsed : Nat +deriving Repr, DecidableEq + +/-- policy.ts:515-518. -/ +def init (remaining : Seconds) : State := ⟨0, remaining, .start, 0, 0⟩ + +/-- One pass from the loop head, policy.ts:519-561. `none` once the function has +returned or thrown. -/ +def pollStep (o : PollingOptions) (s : State) (e : Event) : Option State := + if s.last.running = false then none + else if s.remaining.positive = false then + some { s with last := .returnedOnDeadline } -- 519, 561 + else + let remaining := s.remaining.elapse e.stepElapsed -- 522 + match e.outcome with + | .terminal => -- 523, 553 + some { s with failures := 0, retries := 0, remaining := remaining, last := .returnedVerdict } + | .sleep seconds => -- 523, 554-558 + let requested := remaining.cap seconds + some { failures := 0, retries := 0, remaining := remaining.elapse e.sleepElapsed, + last := .sleptPoll requested, slept := s.slept + requested } + | .continue_ => -- 523, then 519 + some { s with failures := 0, retries := 0, remaining := remaining, last := .continued } + | .deadlineExceeded => some { s with remaining := remaining, last := .returnedOnDeadline } -- 525 + | .otherError => some { s with remaining := remaining, last := .threw } -- 526 + | .queryError retryable => + let failures := s.failures + 1 -- 534 + if retryable = false ∨ failures ≥ o.maxQueryErrors then -- 535-536 + some { s with failures := failures, remaining := remaining, last := .returnedStatusQuery failures } + else + let retryInSeconds := remaining.cap (queryBackoffSeconds o.interval failures) -- 537-540 + some { failures := failures, retries := s.retries + 1, remaining := remaining.elapse e.sleepElapsed, + last := .sleptRetry retryInSeconds, slept := s.slept + retryInSeconds } -- 550-551 + +/-- The state after a sequence of passes. -/ +def run (o : PollingOptions) (s : State) : List Event → State + | [] => s + | e :: es => + match pollStep o s e with + | some t => run o t es + | none => s + +/-- Every sleep the pass requests is at most the deadline budget at the moment of +the request, which is after the step ran. -/ +def SleepWithinBudget (s : State) (e : Event) (t : State) : Prop := + match t.last.sleepSeconds with + | some seconds => (s.remaining.elapse e.stepElapsed).covers seconds + | none => True + +instance (s : State) (e : Event) (t : State) : Decidable (SleepWithinBudget s e t) := by + unfold SleepWithinBudget; split <;> infer_instance + +/-- A retry sleep is the backoff for the failure count just reached, which is at +least 1, capped by the budget. So `queryBackoffSeconds` is never called at 0. -/ +def RetryUsesBackoff (o : PollingOptions) (s : State) (e : Event) (t : State) : Prop := + match t.last with + | .sleptRetry seconds => + 1 ≤ t.failures ∧ + seconds = (s.remaining.elapse e.stepElapsed).cap (queryBackoffSeconds o.interval t.failures) + | _ => True + +instance (o : PollingOptions) (s : State) (e : Event) (t : State) : Decidable (RetryUsesBackoff o s e t) := by + unfold RetryUsesBackoff; split <;> infer_instance + +/-- A pass whose step returned leaves `failures` at 0. -/ +def ResetsFailures (s : State) (e : Event) (t : State) : Prop := + s.remaining.positive = true → e.outcome.succeeded = true → t.failures = 0 ∧ t.retries = 0 + +instance (s : State) (e : Event) (t : State) : Decidable (ResetsFailures s e t) := by + unfold ResetsFailures; infer_instance + +/-- While the loop runs the ghost count of retry sleeps equals `failures`, and it +is 0 or under the error budget. -/ +def Invariant (o : PollingOptions) (s : State) : Prop := + (s.last.running = true → s.retries = s.failures) ∧ (s.retries = 0 ∨ s.retries < o.maxQueryErrors) + +instance (o : PollingOptions) (s : State) : Decidable (Invariant o s) := by + unfold Invariant; infer_instance + +/-- Consecutive retry sleeps are strictly fewer than `maxQueryErrors`. -/ +def RetriesBelowMax (o : PollingOptions) (s : State) : Prop := s.retries < o.maxQueryErrors + +instance (o : PollingOptions) (s : State) : Decidable (RetriesBelowMax o s) := by + unfold RetriesBelowMax; infer_instance + +/-- The deadline is finite, and what was slept plus what remains fits in `timeout`. -/ +def WithinTimeout (timeout : Nat) (s : State) : Prop := + match s.remaining with + | .finite remaining => s.slept + remaining ≤ timeout + | .infinity => False + +instance (timeout : Nat) (s : State) : Decidable (WithinTimeout timeout s) := by + unfold WithinTimeout; split <;> infer_instance + +/-- The clock advanced by at least the slept amount (assumption 8). -/ +def HonestClock (e : Event) (t : State) : Prop := + match t.last.sleepSeconds with + | some seconds => seconds ≤ e.sleepElapsed + | none => True + +instance (e : Event) (t : State) : Decidable (HonestClock e t) := by + unfold HonestClock; split <;> infer_instance + +/-- `HonestClock` on every pass of a run. -/ +def HonestRun (o : PollingOptions) (s : State) : List Event → Prop + | [] => True + | e :: es => + match pollStep o s e with + | some t => HonestClock e t ∧ HonestRun o t es + | none => True + +/-- The pass went back to the loop head and the clock did not move during it. -/ +def Stalls (e : Event) (t : State) : Prop := + t.last.running = true ∧ e.stepElapsed = 0 ∧ (t.last.sleepSeconds = none ∨ e.sleepElapsed = 0) + +instance (e : Event) (t : State) : Decidable (Stalls e t) := by + unfold Stalls; infer_instance + +/-- The CLI default (`cli.ts:126,144`), a budget of three, the smallest budget +with an interval over the cap, and the smallest interval. -/ +def boundedOptions : List PollingOptions := [⟨60, 5⟩, ⟨60, 3⟩, ⟨400, 1⟩, ⟨1, 2⟩] + +/-- No deadline, a deadline already spent, and budgets under and over the +constants 60 and 300. -/ +def boundedBudgets : List Seconds := [.infinity, .finite 0, .finite 1, .finite 100, .finite 1000] + +/-- Every outcome, with a step that takes no time or 20 seconds. The outcomes +that can lead to a sleep also vary the clock during it: it stands still, moves +60 seconds, or moves 400. -/ +def boundedEvents (o : PollingOptions) : List Event := + [0, 20].flatMap fun stepElapsed => + ([StepOutcome.terminal, .continue_, .queryError false, .deadlineExceeded, .otherError].map fun outcome => + ⟨outcome, stepElapsed, 0⟩) + ++ [StepOutcome.sleep 0, .sleep o.interval, .queryError true].flatMap fun outcome => + [0, 60, 400].map fun sleepElapsed => ⟨outcome, stepElapsed, sleepElapsed⟩ + +/-- The states exactly `depth` passes from the start. -/ +def frontier (o : PollingOptions) (start : Seconds) : Nat → List State + | 0 => [init start] + | depth + 1 => + ((frontier o start depth).flatMap fun s => (boundedEvents o).filterMap (pollStep o s)).eraseDups + +def reachable (o : PollingOptions) (start : Seconds) (depth : Nat) : List State := + (List.range (depth + 1)).flatMap (frontier o start) + +structure Transition where + options : PollingOptions + start : Seconds + before : State + event : Event + after : State +deriving Repr + +/-- The timeout that gave the run its starting budget. -/ +def Transition.timeout (x : Transition) : Nat := + match x.start with + | .finite remaining => remaining + | .infinity => 0 + +/-- Every pass from a state at most three passes from the start, so runs of up +to four passes. Four reaches the fourth consecutive failure, where the backoff +is at its cap, and the exit that spends an error budget of three. -/ +def transitions : List Transition := + boundedOptions.flatMap fun o => boundedBudgets.flatMap fun start => + (reachable o start 3).flatMap fun s => (boundedEvents o).filterMap fun e => + (pollStep o s e).map fun t => ⟨o, start, s, e, t⟩ + +/-- The properties of one pass. Each theorem below proves one for every size. -/ +def stepProperties : List (String × (Transition → Bool)) := + [("SleepWithinBudget", fun x => SleepWithinBudget x.before x.event x.after), + ("RetryUsesBackoff", fun x => RetryUsesBackoff x.options x.before x.event x.after), + ("ResetsFailures", fun x => ResetsFailures x.before x.event x.after), + ("Invariant", fun x => Invariant x.options x.before ∧ Invariant x.options x.after), + ("RetriesBelowMax", fun x => RetriesBelowMax x.options x.after), + ("WithinTimeout", fun x => + WithinTimeout x.timeout x.before → HonestClock x.event x.after → WithinTimeout x.timeout x.after)] + +/-- Find counter-examples: each property with a transition in the bound that +breaks it. -/ +def badSteps : List (String × Transition) := + transitions.flatMap fun x => (stepProperties.filter fun (_, p) => !p x).map fun (name, _) => (name, x) + +#eval badSteps -- [] +#guard badSteps.isEmpty + +-- `maxQueryErrors = 0` is outside the CLI range (`cli.ts:58-63`). There the loop +-- makes zero retry sleeps, and 0 is not strictly less than 0. +#eval decide (RetriesBelowMax ⟨60, 0⟩ (init .infinity)) -- false +#guard ¬ RetriesBelowMax ⟨60, 0⟩ (init .infinity) + +/-- The states the properties depend on. -/ +def reachTargets : List (String × (Transition → Bool)) := + [("a retry sleep at the largest count the error budget allows", fun x => + x.options.maxQueryErrors ≥ 2 && x.after.retries + 1 == x.options.maxQueryErrors + && x.event.outcome == .queryError true && x.after.last.sleepSeconds.isSome), + ("the exit that spends the error budget on retryable failures", fun x => + x.event.outcome == .queryError true && x.after.last == .returnedStatusQuery x.options.maxQueryErrors), + ("a retry sleep at the backoff cap", fun x => x.after.last == .sleptRetry 300), + ("a retry sleep cut short by the deadline", fun x => + match x.after.last with + | .sleptRetry seconds => 0 < seconds && seconds < 60 + | _ => false), + ("a poll sleep cut short by the deadline", fun x => + match x.event.outcome, x.after.last with + | .sleep asked, .sleptPoll seconds => 0 < seconds && seconds < asked + | _, _ => false), + ("a successful step after failures", fun x => + x.before.failures > 0 && x.event.outcome.succeeded && x.after.failures == 0), + ("the loop-head exit with the budget spent", fun x => + x.before.last.running && x.before.remaining == .finite 0 && x.after.last == .returnedOnDeadline), + ("an honest run that sleeps the whole timeout", fun x => + x.timeout > 0 && WithinTimeout x.timeout x.after && x.after.slept == x.timeout), + ("a still clock that lets the sleeps pass the timeout, so the clock hypothesis is needed", fun x => + x.start != .infinity && x.after.slept > x.timeout)] + +/-- The reach targets the bounded search misses. -/ +def unreachedSteps : List String := + let all := transitions + (reachTargets.filter fun (_, p) => !all.any p).map fun (name, _) => name + +#eval (transitions.length, unreachedSteps) +#guard unreachedSteps.isEmpty + +/-- The step outcomes of the passes in the bound that stall under an honest clock. -/ +def stallOutcomes : List StepOutcome := + ((transitions.filter fun x => HonestClock x.event x.after ∧ Stalls x.event x.after).map + fun x => x.event.outcome).eraseDups + +-- The loop can go round without the clock advancing, on exactly these two results. +#eval stallOutcomes +#guard stallOutcomes.length == 2 && stallOutcomes.all fun outcome => outcome == .continue_ || outcome == .sleep 0 + +/-! ## 3. `noChecksConfirmer`, policy.ts:63-85 -/ + +/-- policy.ts:66. -/ +def NO_CHECKS_CONFIRM_SECONDS : Nat := 60 + +/-- A value of the `firstSeen` map, policy.ts:70-73. `seenAt` is the field `at`. -/ +structure Sighting where + headRefOid : Nat + seenAt : Nat +deriving Repr, DecidableEq + +/-- The `firstSeen` map of policy.ts:70, keyed by PR number. -/ +abbrev FirstSeen := Nat → Option Sighting + +/-- policy.ts:79-82 `firstSeen.set(pr, sighting)`. -/ +def firstSeenSet (firstSeen : FirstSeen) (pr : Nat) (sighting : Sighting) : FirstSeen := + fun n => if n = pr then some sighting else firstSeen n + +/-- One call of the closure, policy.ts:74-84: the map after the call and the +result. Truncated `now - prior.seenAt` agrees with JavaScript, where a negative +difference is also below 60. -/ +def confirm (firstSeen : FirstSeen) (now pr headRefOid : Nat) : FirstSeen × Bool := + match firstSeen pr with -- 76 + | some prior => + if prior.headRefOid = headRefOid then -- 77 + (firstSeen, decide (now - prior.seenAt ≥ NO_CHECKS_CONFIRM_SECONDS)) -- 78 + else (firstSeenSet firstSeen pr ⟨headRefOid, now⟩, false) -- 79-83, head changed + | none => (firstSeenSet firstSeen pr ⟨headRefOid, now⟩, false) -- 79-83, first sighting + +/-- One call of the closure: the clock reading and the head it was given. -/ +structure Call where + now : Nat + pr : Nat + headRefOid : Nat +deriving Repr, DecidableEq + +/-- The map after the calls in `past`, newest first, from the empty map of +policy.ts:70. -/ +def firstSeenAfter : List Call → FirstSeen + | [] => fun _ => none + | c :: older => (confirm (firstSeenAfter older) c.now c.pr c.headRefOid).1 + +/-- The result of call `c` made after the calls in `past`, newest first. -/ +def confirmAfter (past : List Call) (c : Call) : Bool := + (confirm (firstSeenAfter past) c.now c.pr c.headRefOid).2 + +/-- The results of a sequence of calls, oldest first. -/ +def confirmAll (calls : List Call) : List Bool := + (List.range calls.length).filterMap fun i => calls[i]?.map (confirmAfter (calls.take i).reverse) + +/-- The specification, read backwards from the newest call with no map: the +time `headRefOid` was first seen in the unbroken run of sightings of it that ends +at the latest sighting of `pr`. `none` when `pr` was never seen or its latest +sighting had another head. -/ +def runStart (pr headRefOid : Nat) : List Call → Option Nat + | [] => none + | c :: older => + if c.pr = pr then + if c.headRefOid = headRefOid then + match runStart pr headRefOid older with + | some earlier => some earlier + | none => some c.now + else none + else runStart pr headRefOid older + +/-- The confirmer returns true exactly when the head's current run on that PR +started at least 60 seconds before the call. -/ +def ConfirmSpec (past : List Call) (c : Call) : Prop := + confirmAfter past c = + match runStart c.pr c.headRefOid past with + | some firstSeenAt => decide (firstSeenAt + 60 ≤ c.now) + | none => false + +instance (past : List Call) (c : Call) : Decidable (ConfirmSpec past c) := by + unfold ConfirmSpec; infer_instance + +/-- True only when an earlier call saw the same head on the same PR at least 60 +seconds before. -/ +def ConfirmSound (past : List Call) (c : Call) : Prop := + confirmAfter past c = true → + ∃ p ∈ past, p.pr = c.pr ∧ p.headRefOid = c.headRefOid ∧ p.now + 60 ≤ c.now + +instance (past : List Call) (c : Call) : Decidable (ConfirmSound past c) := by + unfold ConfirmSound; infer_instance + +/-- The first sighting of a PR returns false. -/ +def FirstSightingFalse (past : List Call) (c : Call) : Prop := + (∀ p ∈ past, p.pr ≠ c.pr) → confirmAfter past c = false + +instance (past : List Call) (c : Call) : Decidable (FirstSightingFalse past c) := by + unfold FirstSightingFalse; infer_instance + +/-- A head that differs from the PR's latest sighting returns false. -/ +def HeadChangeFalse (past : List Call) (c : Call) : Prop := + match past.find? fun p => p.pr = c.pr with + | some latest => latest.headRefOid ≠ c.headRefOid → confirmAfter past c = false + | none => True + +instance (past : List Call) (c : Call) : Decidable (HeadChangeFalse past c) := by + unfold HeadChangeFalse; split <;> infer_instance + +/-- No confirmation while every earlier call is under 60 seconds old, which +covers a clock that stands still or runs backwards. -/ +def ClockMustAdvance (past : List Call) (c : Call) : Prop := + (∀ p ∈ past, c.now < p.now + 60) → confirmAfter past c = false + +instance (past : List Call) (c : Call) : Decidable (ClockMustAdvance past c) := by + unfold ClockMustAdvance; infer_instance + +/-- Clock readings on both sides of the 60-second window, in every order, on two +PRs with two heads. -/ +def boundedCalls : List Call := + [0, 59, 60, 61, 120].flatMap fun now => [1, 2].flatMap fun pr => [0, 1].map fun headRefOid => + ⟨now, pr, headRefOid⟩ + +/-- Every history of exactly `length` calls, newest first. -/ +def histories : Nat → List (List Call) + | 0 => [[]] + | length + 1 => (histories length).flatMap fun past => boundedCalls.map fun c => c :: past + +/-- A history of up to three calls and the call made after it. Three earlier +calls hold head A, then B, then A again before the call under test. -/ +def confirmCases : List (List Call × Call) := + ((List.range 4).flatMap histories).flatMap fun past => boundedCalls.map fun c => (past, c) + +/-- The properties of one call. Each theorem below proves one for every history. -/ +def confirmProperties : List (String × (List Call → Call → Bool)) := + [("ConfirmSpec", fun past c => ConfirmSpec past c), + ("ConfirmSound", fun past c => ConfirmSound past c), + ("FirstSightingFalse", fun past c => FirstSightingFalse past c), + ("HeadChangeFalse", fun past c => HeadChangeFalse past c), + ("ClockMustAdvance", fun past c => ClockMustAdvance past c)] + +/-- Find counter-examples: each property with a case in the bound that breaks it. -/ +def badConfirms : List (String × List Call × Call) := + confirmCases.flatMap fun (past, c) => + (confirmProperties.filter fun (_, p) => !p past c).map fun (name, _) => (name, past, c) + +#eval badConfirms -- [] +#guard badConfirms.isEmpty + +/-- The cases the properties depend on. -/ +def confirmTargets : List (String × (List Call → Call → Bool)) := + [("a confirmation", fun past c => confirmAfter past c), + ("a refusal 59 seconds after the first sighting", fun past c => + past == [⟨0, 1, 0⟩] && c == ⟨59, 1, 0⟩ && !confirmAfter past c), + ("a confirmation at exactly 60 seconds", fun past c => + past == [⟨0, 1, 0⟩] && c == ⟨60, 1, 0⟩ && confirmAfter past c), + ("head A again after B, refused although A was first seen 120 seconds earlier", fun past c => + past == [⟨61, 1, 0⟩, ⟨59, 1, 1⟩, ⟨0, 1, 0⟩] && c == ⟨120, 1, 0⟩ && !confirmAfter past c), + ("a clock that stands still", fun past c => + past == [⟨60, 1, 0⟩, ⟨60, 1, 0⟩, ⟨60, 1, 0⟩] && c == ⟨60, 1, 0⟩ && !confirmAfter past c), + ("a clock that runs backwards", fun past c => + past == [⟨120, 1, 0⟩] && c == ⟨0, 1, 0⟩ && !confirmAfter past c)] + +/-- The reach targets the bounded search misses. -/ +def unreachedConfirms : List String := + let all := confirmCases + (confirmTargets.filter fun (_, p) => !all.any fun (past, c) => p past c).map fun (name, _) => name + +#eval (confirmCases.length, unreachedConfirms) +#guard unreachedConfirms.isEmpty + +-- Head A, B, then A: the second run of A confirms 60 seconds after its own start. +#guard confirmAll [⟨0, 1, 0⟩, ⟨10, 1, 1⟩, ⟨100, 1, 0⟩, ⟨159, 1, 0⟩, ⟨160, 1, 0⟩] + == [false, false, false, false, true] + +/-! ## Theorems for every size -/ + +/-! ### `queryBackoffSeconds` -/ + +theorem queryBackoff_ge_60 (interval failures : Nat) (h : 1 ≤ failures) : + 60 ≤ queryBackoffSeconds interval failures := by + -- The truncated model does not need `h`; it limits the claim to where the model is faithful. + have _ := h + have hpow : 2 ^ 0 ≤ 2 ^ (failures - 1) := Nat.pow_le_pow_right (by decide) (by omega) + have hmul := Nat.mul_le_mul_left (max interval 60) hpow + simp only [Nat.pow_zero, Nat.mul_one] at hmul + unfold queryBackoffSeconds + omega + +theorem queryBackoff_le_300 (interval failures : Nat) : queryBackoffSeconds interval failures ≤ 300 := by + unfold queryBackoffSeconds + omega + +theorem queryBackoff_mono (interval f g : Nat) (hf : 1 ≤ f) (hfg : f ≤ g) : + queryBackoffSeconds interval f ≤ queryBackoffSeconds interval g := by + have hpow : 2 ^ (f - 1) ≤ 2 ^ (g - 1) := Nat.pow_le_pow_right (by decide) (by omega) + have hmul := Nat.mul_le_mul_left (max interval 60) hpow + unfold queryBackoffSeconds + omega + +theorem queryBackoff_eq_300 (interval failures : Nat) (h : 4 ≤ failures) : + queryBackoffSeconds interval failures = 300 := by + have hpow : 2 ^ 3 ≤ 2 ^ (failures - 1) := Nat.pow_le_pow_right (by decide) (by omega) + have hmul := Nat.mul_le_mul_left (max interval 60) hpow + have height : (2 : Nat) ^ 3 = 8 := by decide + rw [height] at hmul + unfold queryBackoffSeconds + omega + +theorem queryBackoff_first (interval : Nat) : queryBackoffSeconds interval 1 = min (max interval 60) 300 := by + simp [queryBackoffSeconds] + +theorem queryBackoff_doubles (interval failures : Nat) (h : 1 ≤ failures) : + queryBackoffSeconds interval (failures + 1) = min (2 * queryBackoffSeconds interval failures) 300 := by + obtain ⟨k, rfl⟩ : ∃ k, failures = k + 1 := ⟨failures - 1, by omega⟩ + simp only [queryBackoffSeconds, Nat.add_sub_cancel] + have hdouble : max interval 60 * 2 ^ (k + 1) = 2 * (max interval 60 * 2 ^ k) := by + rw [Nat.pow_succ, ← Nat.mul_assoc, Nat.mul_comm] + rw [hdouble] + omega + +theorem queryBackoffHalfSeconds_eq (interval failures : Nat) (h : 1 ≤ failures) : + queryBackoffHalfSeconds interval failures = 2 * queryBackoffSeconds interval failures := by + obtain ⟨k, rfl⟩ : ∃ k, failures = k + 1 := ⟨failures - 1, by omega⟩ + simp only [queryBackoffHalfSeconds, queryBackoffSeconds, Nat.add_sub_cancel] + have hdouble : max interval 60 * 2 ^ (k + 1) = 2 * (max interval 60 * 2 ^ k) := by + rw [Nat.pow_succ, ← Nat.mul_assoc, Nat.mul_comm] + rw [hdouble] + omega + +theorem backoffSpec (interval failures : Nat) (h : 1 ≤ failures) : BackoffSpec interval failures := by + unfold BackoffSpec + refine ⟨queryBackoff_ge_60 interval failures h, queryBackoff_le_300 interval failures, + queryBackoff_mono interval failures (failures + 1) h (Nat.le_succ failures), + queryBackoff_eq_300 interval failures, ?_, queryBackoff_doubles interval failures h, + queryBackoffHalfSeconds_eq interval failures h⟩ + intro hone + subst hone + exact queryBackoff_first interval + +/-- At `failures = 0` JavaScript returns half of `min (max interval 60) 600`. -/ +theorem queryBackoffHalfSeconds_zero (interval : Nat) : + queryBackoffHalfSeconds interval 0 = min (max interval 60) 600 := by + simp [queryBackoffHalfSeconds] + +/-- At `failures = 0` the result is under the 60-second floor exactly when +`interval < 120`. -/ +theorem queryBackoff_zero_below_floor (interval : Nat) : + queryBackoffHalfSeconds interval 0 < 2 * 60 ↔ interval < 120 := by + rw [queryBackoffHalfSeconds_zero] + omega + +/-! ### `WatchDeadline.remaining` -/ + +theorem remaining_create_finite (timeout t0 elapsed : Nat) (h : 0 < timeout) : + (WatchDeadline.create timeout t0).remaining (t0 + elapsed) = .finite (timeout - elapsed) := by + have hsub : t0 + timeout - (t0 + elapsed) = timeout - elapsed := by omega + simp [WatchDeadline.create, WatchDeadline.remaining, h, hsub] + +theorem remaining_create_infinity (t0 now : Nat) : (WatchDeadline.create 0 t0).remaining now = .infinity := by + simp [WatchDeadline.create, WatchDeadline.remaining] + +/-- `Seconds.elapse` is what a clock advance does to `WatchDeadline.remaining`. -/ +theorem remaining_elapse (d : WatchDeadline) (now seconds : Nat) : + d.remaining (now + seconds) = (d.remaining now).elapse seconds := by + obtain ⟨expiresAt⟩ := d + cases expiresAt <;> simp [WatchDeadline.remaining, Seconds.elapse, Nat.sub_add_eq] + +/-! ### `pollUntilTerminal` -/ + +/-- Split `h : pollStep o s e = some t` into the paths through the loop body and +replace `t` by the state each path builds. -/ +local macro "poll_paths " h:ident : tactic => + `(tactic| ( + simp only [pollStep] at $h:ident + repeat' split at $h:ident + all_goals first | cases $h:ident | skip)) + +theorem covers_cap (remaining : Seconds) (seconds : Nat) : remaining.covers (remaining.cap seconds) := by + cases remaining <;> simp [Seconds.covers, Seconds.cap] + omega + +theorem pollStep_sleep_within_budget (o : PollingOptions) (s t : State) (e : Event) + (h : pollStep o s e = some t) : SleepWithinBudget s e t := by + poll_paths h <;> simp [SleepWithinBudget, Last.sleepSeconds, covers_cap] + +theorem pollStep_retry_uses_backoff (o : PollingOptions) (s t : State) (e : Event) + (h : pollStep o s e = some t) : RetryUsesBackoff o s e t := by + poll_paths h <;> simp [RetryUsesBackoff] + +theorem pollStep_resets_failures (o : PollingOptions) (s t : State) (e : Event) + (h : pollStep o s e = some t) : ResetsFailures s e t := by + poll_paths h <;> simp_all [ResetsFailures, StepOutcome.succeeded] + +theorem invariant_init (o : PollingOptions) (remaining : Seconds) : Invariant o (init remaining) := by + simp [Invariant, init] + +theorem pollStep_keeps_invariant (o : PollingOptions) (s t : State) (e : Event) + (hs : Invariant o s) (h : pollStep o s e = some t) : Invariant o t := by + unfold Invariant at hs ⊢ + poll_paths h <;> simp_all [Last.running] <;> omega + +theorem run_keeps_invariant (o : PollingOptions) (s : State) (es : List Event) + (hs : Invariant o s) : Invariant o (run o s es) := by + induction es generalizing s with + | nil => exact hs + | cons e es ih => + cases hstep : pollStep o s e with + | none => simpa [run, hstep] using hs + | some t => + simp only [run, hstep] + exact ih t (pollStep_keeps_invariant o s t e hs hstep) + +/-- `hmax` is the CLI's range for `--max-query-errors`, `cli.ts:58-63,140-145`. -/ +theorem run_retries_below_max (o : PollingOptions) (remaining : Seconds) (es : List Event) + (hmax : 0 < o.maxQueryErrors) : RetriesBelowMax o (run o (init remaining) es) := by + have hinv := run_keeps_invariant o (init remaining) es (invariant_init o remaining) + unfold Invariant at hinv + unfold RetriesBelowMax + omega + +/-- `hclock` is assumption 8: the clock advanced by at least the slept amount. -/ +theorem pollStep_keeps_timeout (timeout : Nat) (o : PollingOptions) (s t : State) (e : Event) + (hs : WithinTimeout timeout s) (h : pollStep o s e = some t) (hclock : HonestClock e t) : + WithinTimeout timeout t := by + obtain ⟨failures, remaining, last, retries, slept⟩ := s + cases remaining with + | infinity => simp [WithinTimeout] at hs + | finite remaining => + poll_paths h <;> + simp_all [WithinTimeout, HonestClock, Last.sleepSeconds, Seconds.elapse, Seconds.cap] <;> omega + +theorem run_keeps_timeout (timeout : Nat) (o : PollingOptions) (s : State) (es : List Event) + (hs : WithinTimeout timeout s) (hclock : HonestRun o s es) : WithinTimeout timeout (run o s es) := by + induction es generalizing s with + | nil => exact hs + | cons e es ih => + cases hstep : pollStep o s e with + | none => simpa [run, hstep] using hs + | some t => + simp only [HonestRun, hstep] at hclock + simp only [run, hstep] + exact ih t (pollStep_keeps_timeout timeout o s t e hs hstep hclock.1) hclock.2 + +theorem slept_le_of_withinTimeout (timeout : Nat) (s : State) (hs : WithinTimeout timeout s) : + s.slept ≤ timeout := by + unfold WithinTimeout at hs + split at hs + · omega + · exact absurd hs id + +/-- With a finite positive timeout and a clock that advances by at least the +slept amount, the loop's sleeps total at most the timeout. `elapsed` is the time +between creating the deadline and entering the loop. -/ +theorem pollUntilTerminal_slept_le_timeout (o : PollingOptions) (timeout t0 elapsed : Nat) (es : List Event) + (htimeout : 0 < timeout) + (hclock : HonestRun o (init ((WatchDeadline.create timeout t0).remaining (t0 + elapsed))) es) : + (run o (init ((WatchDeadline.create timeout t0).remaining (t0 + elapsed))) es).slept ≤ timeout := by + rw [remaining_create_finite timeout t0 elapsed htimeout] at hclock ⊢ + have hinit : WithinTimeout timeout (init (.finite (timeout - elapsed))) := by + simp [WithinTimeout, init] + exact slept_le_of_withinTimeout timeout _ (run_keeps_timeout timeout o _ es hinit hclock) + +/-- Under an honest clock, only a `continue` result or a `sleep` result with +`seconds = 0`, from a step that took no time, sends the loop round without the +clock advancing. -/ +theorem stall_needs_continue_or_zero_sleep (o : PollingOptions) (s t : State) (e : Event) + (h : pollStep o s e = some t) (hclock : HonestClock e t) (hstall : Stalls e t) : + e.outcome = .continue_ ∨ e.outcome = .sleep 0 := by + obtain ⟨failures, remaining, last, retries, slept⟩ := s + have hfloor := queryBackoff_ge_60 o.interval (failures + 1) (by omega) + cases remaining <;> poll_paths h <;> + simp_all [Stalls, HonestClock, Last.running, Last.sleepSeconds, Seconds.positive, Seconds.elapse, + Seconds.cap] <;> omega + +/-- A `continue` result from a step that takes no time returns the loop to the +same state, so nothing in `pollUntilTerminal` bounds how often it repeats. -/ +theorem continue_without_clock_repeats (o : PollingOptions) (s : State) (sleepElapsed : Nat) + (hlast : s.last = .continued) (hbudget : s.remaining.positive = true) + (hfailures : s.failures = 0) (hretries : s.retries = 0) : + pollStep o s ⟨.continue_, 0, sleepElapsed⟩ = some s := by + obtain ⟨failures, remaining, last, retries, slept⟩ := s + cases remaining <;> simp_all [pollStep, Last.running, Seconds.elapse] + +/-- The same for a `sleep` result with `seconds = 0`. -/ +theorem zero_sleep_without_clock_repeats (o : PollingOptions) (s : State) + (hlast : s.last = .sleptPoll 0) (hbudget : s.remaining.positive = true) + (hfailures : s.failures = 0) (hretries : s.retries = 0) : + pollStep o s ⟨.sleep 0, 0, 0⟩ = some s := by + obtain ⟨failures, remaining, last, retries, slept⟩ := s + cases remaining <;> simp_all [pollStep, Last.running, Seconds.elapse, Seconds.cap] + +/-! ### `noChecksConfirmer` -/ + +theorem confirm_true_iff (firstSeen : FirstSeen) (now pr headRefOid : Nat) : + (confirm firstSeen now pr headRefOid).2 = true ↔ + ∃ firstSeenAt, firstSeen pr = some ⟨headRefOid, firstSeenAt⟩ ∧ firstSeenAt + 60 ≤ now := by + unfold confirm + cases hprior : firstSeen pr with + | none => simp + | some prior => + obtain ⟨head, seenAt⟩ := prior + by_cases hhead : head = headRefOid + · subst hhead + simp [NO_CHECKS_CONFIRM_SECONDS] + omega + · simp [hhead] + +/-- A first sighting or a changed head returns false and restarts the timer. -/ +theorem confirm_new_head_resets (firstSeen : FirstSeen) (now pr headRefOid : Nat) + (hnew : ∀ firstSeenAt, firstSeen pr ≠ some ⟨headRefOid, firstSeenAt⟩) : + (confirm firstSeen now pr headRefOid).2 = false ∧ + (confirm firstSeen now pr headRefOid).1 pr = some ⟨headRefOid, now⟩ := by + unfold confirm + cases hprior : firstSeen pr with + | none => simp [firstSeenSet] + | some prior => + obtain ⟨head, seenAt⟩ := prior + have hhead : head ≠ headRefOid := by + intro heq + subst heq + exact hnew seenAt hprior + simp [hhead, firstSeenSet] + +/-- Seeing the same head again does not restart the timer. -/ +theorem confirm_same_head_keeps_first_seen (firstSeen : FirstSeen) (now pr headRefOid firstSeenAt : Nat) + (hsame : firstSeen pr = some ⟨headRefOid, firstSeenAt⟩) : + (confirm firstSeen now pr headRefOid).1 = firstSeen := by + simp [confirm, hsame] + +theorem confirm_other_pr_untouched (firstSeen : FirstSeen) (now pr headRefOid other : Nat) + (hother : other ≠ pr) : (confirm firstSeen now pr headRefOid).1 other = firstSeen other := by + unfold confirm + split + · split + · rfl + · simp [firstSeenSet, hother] + · simp [firstSeenSet, hother] + +/-- The map holds, for each PR, its latest head and when that head's run began. -/ +theorem firstSeenAfter_spec (past : List Call) (pr headRefOid firstSeenAt : Nat) : + firstSeenAfter past pr = some ⟨headRefOid, firstSeenAt⟩ ↔ runStart pr headRefOid past = some firstSeenAt := by + induction past generalizing pr headRefOid firstSeenAt with + | nil => simp [firstSeenAfter, runStart] + | cons c older ih => + by_cases hpr : c.pr = pr + · subst hpr + by_cases hsame : ∃ earlier, firstSeenAfter older c.pr = some ⟨c.headRefOid, earlier⟩ + · -- The latest head is seen again: the map and the run start stay as they were. + obtain ⟨earlier, hearlier⟩ := hsame + have hrun := (ih c.pr c.headRefOid earlier).mp hearlier + have hkeep := confirm_same_head_keeps_first_seen (firstSeenAfter older) c.now c.pr c.headRefOid + earlier hearlier + by_cases hhead : c.headRefOid = headRefOid + · subst hhead + simp [firstSeenAfter, runStart, hkeep, hearlier, hrun] + · simp [firstSeenAfter, runStart, hkeep, hearlier, hhead] + · -- A first sighting or a changed head: both restart at this call. + have hnew : ∀ earlier, firstSeenAfter older c.pr ≠ some ⟨c.headRefOid, earlier⟩ := + fun earlier h => hsame ⟨earlier, h⟩ + have hrun : runStart c.pr c.headRefOid older = none := by + cases hrs : runStart c.pr c.headRefOid older with + | none => rfl + | some earlier => exact absurd ((ih c.pr c.headRefOid earlier).mpr hrs) (hnew earlier) + have hreset := (confirm_new_head_resets (firstSeenAfter older) c.now c.pr c.headRefOid hnew).2 + by_cases hhead : c.headRefOid = headRefOid + · subst hhead + simp [firstSeenAfter, runStart, hreset, hrun] + · simp [firstSeenAfter, runStart, hreset, hhead] + · have hkeep := confirm_other_pr_untouched (firstSeenAfter older) c.now c.pr c.headRefOid pr + (fun h => hpr h.symm) + simp only [firstSeenAfter, runStart, hkeep, hpr, ↓reduceIte] + exact ih pr headRefOid firstSeenAt + +theorem confirmAfter_true_iff (past : List Call) (c : Call) : + confirmAfter past c = true ↔ + ∃ firstSeenAt, runStart c.pr c.headRefOid past = some firstSeenAt ∧ firstSeenAt + 60 ≤ c.now := by + unfold confirmAfter + rw [confirm_true_iff] + constructor + · intro ⟨firstSeenAt, hmap, hold⟩ + exact ⟨firstSeenAt, (firstSeenAfter_spec past c.pr c.headRefOid firstSeenAt).mp hmap, hold⟩ + · intro ⟨firstSeenAt, hrun, hold⟩ + exact ⟨firstSeenAt, (firstSeenAfter_spec past c.pr c.headRefOid firstSeenAt).mpr hrun, hold⟩ + +theorem confirmAfter_spec (past : List Call) (c : Call) : ConfirmSpec past c := by + unfold ConfirmSpec + have hiff := confirmAfter_true_iff past c + cases hrun : runStart c.pr c.headRefOid past with + | none => + cases hresult : confirmAfter past c with + | false => rfl + | true => + obtain ⟨firstSeenAt, hsome, _⟩ := hiff.mp hresult + rw [hrun] at hsome + cases hsome + | some firstSeenAt => + rw [hrun] at hiff + cases hresult : confirmAfter past c with + | false => + have hnot : ¬ firstSeenAt + 60 ≤ c.now := fun hold => + absurd (hiff.mpr ⟨firstSeenAt, rfl, hold⟩) (by simp [hresult]) + simp [hnot] + | true => + obtain ⟨other, hsome, hold⟩ := hiff.mp hresult + cases hsome + simp [hold] + +theorem runStart_some_mem (pr headRefOid firstSeenAt : Nat) (past : List Call) + (hrun : runStart pr headRefOid past = some firstSeenAt) : + ∃ p ∈ past, p.pr = pr ∧ p.headRefOid = headRefOid ∧ p.now = firstSeenAt := by + induction past with + | nil => simp [runStart] at hrun + | cons c older ih => + simp only [runStart] at hrun + by_cases hpr : c.pr = pr + · by_cases hhead : c.headRefOid = headRefOid + · simp only [hpr, hhead, ↓reduceIte] at hrun + cases holder : runStart pr headRefOid older with + | none => + simp only [holder] at hrun + exact ⟨c, List.mem_cons_self, hpr, hhead, Option.some.inj hrun⟩ + | some earlier => + simp only [holder] at hrun + obtain ⟨p, hp, hfound⟩ := ih (hrun ▸ holder) + exact ⟨p, List.mem_cons_of_mem c hp, hfound⟩ + · simp [hpr, hhead] at hrun + · simp only [hpr, ↓reduceIte] at hrun + obtain ⟨p, hp, hfound⟩ := ih hrun + exact ⟨p, List.mem_cons_of_mem c hp, hfound⟩ + +theorem confirm_sound (past : List Call) (c : Call) : ConfirmSound past c := by + intro hresult + obtain ⟨firstSeenAt, hrun, hold⟩ := (confirmAfter_true_iff past c).mp hresult + obtain ⟨p, hp, hpr, hhead, hnow⟩ := runStart_some_mem c.pr c.headRefOid firstSeenAt past hrun + exact ⟨p, hp, hpr, hhead, by omega⟩ + +theorem confirm_first_sighting_false (past : List Call) (c : Call) : FirstSightingFalse past c := by + intro hfirst + cases hresult : confirmAfter past c with + | false => rfl + | true => + obtain ⟨p, hp, hpr, _⟩ := confirm_sound past c hresult + exact absurd hpr (hfirst p hp) + +theorem runStart_none_of_head_change (pr headRefOid : Nat) (past : List Call) (latest : Call) + (hfind : (past.find? fun p => p.pr = pr) = some latest) (hne : latest.headRefOid ≠ headRefOid) : + runStart pr headRefOid past = none := by + induction past with + | nil => simp at hfind + | cons c older ih => + by_cases hpr : c.pr = pr + · simp [List.find?, hpr] at hfind + subst hfind + simp [runStart, hpr, hne] + · simp [List.find?, hpr] at hfind + simp only [runStart, hpr, ↓reduceIte] + exact ih hfind + +theorem confirm_head_change_false (past : List Call) (c : Call) : HeadChangeFalse past c := by + unfold HeadChangeFalse + split + · rename_i latest hfind + intro hne + cases hresult : confirmAfter past c with + | false => rfl + | true => + obtain ⟨firstSeenAt, hrun, _⟩ := (confirmAfter_true_iff past c).mp hresult + rw [runStart_none_of_head_change c.pr c.headRefOid past latest hfind hne] at hrun + cases hrun + · trivial + +theorem confirm_clock_must_advance (past : List Call) (c : Call) : ClockMustAdvance past c := by + intro hrecent + cases hresult : confirmAfter past c with + | false => rfl + | true => + obtain ⟨p, hp, _, _, hold⟩ := confirm_sound past c hresult + have := hrecent p hp + omega + +/-- Head A, then B, then A: the second run of A counts from its own first +sighting, whatever came before B. -/ +theorem confirm_aba_restarts (older : List Call) (pr a b t1 t2 now : Nat) (hab : a ≠ b) : + confirmAfter (⟨t2, pr, a⟩ :: ⟨t1, pr, b⟩ :: older) ⟨now, pr, a⟩ = decide (t2 + 60 ≤ now) := by + have hrun : runStart pr a (⟨t2, pr, a⟩ :: ⟨t1, pr, b⟩ :: older) = some t2 := by + simp [runStart, Ne.symm hab] + have hspec := confirmAfter_spec (⟨t2, pr, a⟩ :: ⟨t1, pr, b⟩ :: older) ⟨now, pr, a⟩ + unfold ConfirmSpec at hspec + simpa [hrun] using hspec + +/-! ## Differential vectors -/ + +/-- `(interval, failures)` inputs for `queryBackoffSeconds`. `failures = 0` is in +the list because the production function accepts it. -/ +def backoffVectors : List (Nat × Nat) := + [0, 1, 30, 59, 60, 61, 74, 75, 76, 100, 119, 120, 149, 150, 151, 299, 300, 301, 600, 601, 3600, + 9007199254740991].flatMap fun interval => + [0, 1, 2, 3, 4, 5, 10, 64, 1024, 1025, 1100].map fun failures => (interval, failures) + +/-- Call sequences for one `noChecksConfirmer` closure, oldest first. -/ +def confirmVectors : List (List Call) := + [[⟨0, 1, 0⟩], + [⟨0, 1, 0⟩, ⟨59, 1, 0⟩, ⟨60, 1, 0⟩, ⟨61, 1, 0⟩], + [⟨1000, 1, 0⟩, ⟨1059, 1, 0⟩, ⟨1060, 1, 0⟩], + [⟨5, 1, 0⟩, ⟨5, 1, 0⟩, ⟨5, 1, 0⟩, ⟨5, 1, 0⟩], + [⟨0, 1, 0⟩, ⟨10, 1, 1⟩, ⟨100, 1, 0⟩, ⟨159, 1, 0⟩, ⟨160, 1, 0⟩], + [⟨0, 1, 0⟩, ⟨30, 2, 0⟩, ⟨60, 1, 0⟩, ⟨60, 2, 0⟩, ⟨90, 2, 0⟩], + [⟨100, 1, 0⟩, ⟨50, 1, 0⟩, ⟨110, 1, 0⟩, ⟨160, 1, 0⟩], + [⟨0, 1, 0⟩, ⟨59, 1, 0⟩, ⟨118, 1, 0⟩], + [⟨0, 1, 0⟩, ⟨60, 1, 0⟩, ⟨61, 1, 1⟩, ⟨120, 1, 1⟩, ⟨121, 1, 1⟩], + [⟨0, 1, 0⟩, ⟨60, 2, 0⟩, ⟨60, 1, 1⟩, ⟨120, 2, 0⟩, ⟨120, 1, 1⟩], + [⟨0, 1, 0⟩, ⟨0, 1, 1⟩, ⟨0, 1, 2⟩, ⟨60, 1, 2⟩, ⟨60, 1, 0⟩]] + +/-- A half-second count as a JSON number of seconds. -/ +def secondsJson (halfSeconds : Nat) : String := + if halfSeconds % 2 = 0 then toString (halfSeconds / 2) else toString (halfSeconds / 2) ++ ".5" + +/-- Head 0 is "A", 1 is "B", 2 is "C". -/ +def headJson (headRefOid : Nat) : String := "\"" ++ String.singleton (Char.ofNat (65 + headRefOid)) ++ "\"" + +-- One JSON object per line after `VECTOR `. A `calls` entry is `[now, pr, head]`. +#eval do + for (interval, failures) in backoffVectors do + let expect := + if failures = 0 then secondsJson (queryBackoffHalfSeconds interval 0) + else toString (queryBackoffSeconds interval failures) + IO.println ("VECTOR {\"fn\":\"queryBackoffSeconds\",\"interval\":" ++ toString interval + ++ ",\"failures\":" ++ toString failures ++ ",\"expect\":" ++ expect ++ "}") + for calls in confirmVectors do + let callsJson := ",".intercalate (calls.map fun (c : Call) => + "[" ++ toString c.now ++ "," ++ toString c.pr ++ "," ++ headJson c.headRefOid ++ "]") + let expect := ",".intercalate ((confirmAll calls).map toString) + IO.println ("VECTOR {\"fn\":\"noChecksConfirmer\",\"calls\":[" ++ callsJson ++ "],\"expect\":[" + ++ expect ++ "]}") + +end WatchPoll diff --git a/lean/Model/WatchPoll.mutations b/lean/Model/WatchPoll.mutations new file mode 100644 index 000000000..805a4abdb --- /dev/null +++ b/lean/Model/WatchPoll.mutations @@ -0,0 +1,103 @@ +# Mutations for mutate.py. Each one swaps a piece of WatchPoll.lean for a +# plausible bug. The text after `- ` must occur exactly once in the model; +# `+ ` lines replace it. mutate.py reports the declarations that fail on each. + +# queryBackoffSeconds, policy.ts:399-402 + +mutation backoff floor lowered to 30 seconds +- min (max interval 60 * 2 ^ (failures - 1)) 300 ++ min (max interval 30 * 2 ^ (failures - 1)) 300 + +mutation backoff cap raised to 600 seconds +- min (max interval 60 * 2 ^ (failures - 1)) 300 ++ min (max interval 60 * 2 ^ (failures - 1)) 600 + +mutation backoff exponent off by one +- min (max interval 60 * 2 ^ (failures - 1)) 300 ++ min (max interval 60 * 2 ^ failures) 300 + +mutation backoff ignores the poll interval +- min (max interval 60 * 2 ^ (failures - 1)) 300 ++ min (60 * 2 ^ (failures - 1)) 300 + +mutation backoff grows linearly instead of doubling +- min (max interval 60 * 2 ^ (failures - 1)) 300 ++ min (max interval 60 * failures) 300 + +# WatchDeadline, deadline.ts:9-20 + +mutation deadline stays finite when the timeout is 0 +- ⟨if timeout > 0 then .finite (now + timeout) else .infinity⟩ ++ ⟨if timeout ≥ 0 then .finite (now + timeout) else .infinity⟩ + +mutation remaining budget does not shrink as the clock advances +- | .finite r => .finite (r - seconds) ++ | .finite r => .finite r + +# pollUntilTerminal, policy.ts:509-562 + +mutation loop runs with no budget left +- | .finite remaining => decide (0 < remaining) ++ | .finite remaining => decide (0 ≤ remaining) + +mutation one retry more than the error budget +- if retryable = false ∨ failures ≥ o.maxQueryErrors then ++ if retryable = false ∨ failures > o.maxQueryErrors then + +mutation failure count is not incremented +- let failures := s.failures + 1 ++ let failures := s.failures + +mutation a successful step keeps the failure count +- some { s with failures := 0, retries := 0, remaining := remaining, last := .continued } ++ some { s with retries := 0, remaining := remaining, last := .continued } + +mutation poll sleep ignores the deadline +- let requested := remaining.cap seconds ++ let requested := seconds + +mutation poll sleep capped by the budget from before the step +- let requested := remaining.cap seconds ++ let requested := s.remaining.cap seconds + +mutation retry sleep ignores the deadline +- let retryInSeconds := remaining.cap (queryBackoffSeconds o.interval failures) ++ let retryInSeconds := queryBackoffSeconds o.interval failures + +mutation backoff uses the failure count from before the increment +- let retryInSeconds := remaining.cap (queryBackoffSeconds o.interval failures) ++ let retryInSeconds := remaining.cap (queryBackoffSeconds o.interval s.failures) + +mutation backoff arguments swapped +- let retryInSeconds := remaining.cap (queryBackoffSeconds o.interval failures) ++ let retryInSeconds := remaining.cap (queryBackoffSeconds failures o.interval) + +# noChecksConfirmer, policy.ts:63-85 + +mutation confirmation window shortened to 30 seconds +- def NO_CHECKS_CONFIRM_SECONDS : Nat := 60 ++ def NO_CHECKS_CONFIRM_SECONDS : Nat := 30 + +mutation confirmation needs more than 60 seconds +- (firstSeen, decide (now - prior.seenAt ≥ NO_CHECKS_CONFIRM_SECONDS)) ++ (firstSeen, decide (now - prior.seenAt > NO_CHECKS_CONFIRM_SECONDS)) + +mutation seeing the same head again restarts the timer +- (firstSeen, decide (now - prior.seenAt ≥ NO_CHECKS_CONFIRM_SECONDS)) ++ (firstSeenSet firstSeen pr ⟨headRefOid, now⟩, decide (now - prior.seenAt ≥ NO_CHECKS_CONFIRM_SECONDS)) + +mutation a changed head keeps the old timer +- else (firstSeenSet firstSeen pr ⟨headRefOid, now⟩, false) ++ else (firstSeenSet firstSeen pr ⟨headRefOid, prior.seenAt⟩, false) + +mutation a changed head confirms on the old timer +- else (firstSeenSet firstSeen pr ⟨headRefOid, now⟩, false) ++ else (firstSeenSet firstSeen pr ⟨headRefOid, now⟩, decide (now - prior.seenAt ≥ NO_CHECKS_CONFIRM_SECONDS)) + +mutation a first sighting is not recorded +- | none => (firstSeenSet firstSeen pr ⟨headRefOid, now⟩, false) ++ | none => (firstSeen, false) + +mutation one timer shared by every pull request +- fun n => if n = pr then some sighting else firstSeen n ++ fun _ => some sighting diff --git a/lean/Model/WatchPoll.statements b/lean/Model/WatchPoll.statements new file mode 100644 index 000000000..dc35ef3b8 --- /dev/null +++ b/lean/Model/WatchPoll.statements @@ -0,0 +1,2758 @@ +{ + "format": 1, + "toolchain": "leanprover/lean4:v4.34.1", + "module": "Model.WatchPoll", + "declarations": [ + { + "name": "WatchPoll.BackoffSpec", + "kind": "definition", + "type": "Nat → Nat → Prop", + "typeHash": "0dae82c6f1aa4ce803efc560501e2281aba6ecedadabcc41a212033d7dc97223", + "valueHash": "2e833d4c54023bf61b7009ec35bbe493cd91f009b6a65213a7c87e8f75f15520" + }, + { + "name": "WatchPoll.Call", + "kind": "inductive", + "type": "Type", + "typeHash": "7051ab654d793c216d744f7920d98d672357a0efa67248ee75d146d668f60997", + "valueHash": null + }, + { + "name": "WatchPoll.Call.casesOn", + "kind": "definition", + "type": "{motive : WatchPoll.Call → Sort u} → (t : WatchPoll.Call) → ((now pr headRefOid : Nat) → motive { now := now, pr := pr, headRefOid := headRefOid }) → motive t", + "typeHash": "b4363b5b01c54f98f69312cf255e2aea42ff0a2190633293ed98c37ba7ba969d", + "valueHash": "05ca26ca4c974c50313bd520413bc199c20b50ea73470cf75e085c108c019fc3" + }, + { + "name": "WatchPoll.Call.ctorIdx", + "kind": "definition", + "type": "WatchPoll.Call → Nat", + "typeHash": "bc7cb18601f96f72d3ee15ab15684d99bd303a2b988c537e4228645409a1d6b8", + "valueHash": "bb250933ead0a758c3b3f7e19759f49c6646ac3d600b50e7f9b9815bff0ade25" + }, + { + "name": "WatchPoll.Call.headRefOid", + "kind": "definition", + "type": "WatchPoll.Call → Nat", + "typeHash": "d2420d36e0a6efa6196fa981834368126db1a010b09da26b929ba66b75261d76", + "valueHash": "c7efb54489fc93d7e41222365ede52d2a14d11da1777cc5bdeb2eb8db2765017" + }, + { + "name": "WatchPoll.Call.mk", + "kind": "constructor", + "type": "Nat → Nat → Nat → WatchPoll.Call", + "typeHash": "fd46628f53e4014d36239ce15d00a9b16acc7ded580f4cb5c13bc685cdcb1081", + "valueHash": null + }, + { + "name": "WatchPoll.Call.mk.inj", + "kind": "theorem", + "type": "∀ {now pr headRefOid now_1 pr_1 headRefOid_1 : Nat}, { now := now, pr := pr, headRefOid := headRefOid } = { now := now_1, pr := pr_1, headRefOid := headRefOid_1 } → now = now_1 ∧ pr = pr_1 ∧ headRefOid = headRefOid_1", + "typeHash": "374356fa12948f1cee29c922f7903e3a7c1d158f515d3bc48268164490164e15", + "valueHash": null + }, + { + "name": "WatchPoll.Call.mk.injEq", + "kind": "theorem", + "type": "∀ (now pr headRefOid now_1 pr_1 headRefOid_1 : Nat), ({ now := now, pr := pr, headRefOid := headRefOid } = { now := now_1, pr := pr_1, headRefOid := headRefOid_1 }) = (now = now_1 ∧ pr = pr_1 ∧ headRefOid = headRefOid_1)", + "typeHash": "a3c2fc962b029f1c546951bdb6ecc7a0010f80e2e9623ec3f13fa45437b42eb8", + "valueHash": null + }, + { + "name": "WatchPoll.Call.mk.noConfusion", + "kind": "definition", + "type": "{P : Sort u} → {now pr headRefOid now' pr' headRefOid' : Nat} → { now := now, pr := pr, headRefOid := headRefOid } = { now := now', pr := pr', headRefOid := headRefOid' } → (now = now' → pr = pr' → headRefOid = headRefOid' → P) → P", + "typeHash": "923e68b32bc8556312de5f7b60f126a60af954879fae13cc7f0b7866889e702d", + "valueHash": "08ce78d0441e4175fea5dbd95053e992d29db18c6dcb4308af57b8cf82379d0e" + }, + { + "name": "WatchPoll.Call.mk.sizeOf_spec", + "kind": "theorem", + "type": "∀ (now pr headRefOid : Nat), sizeOf { now := now, pr := pr, headRefOid := headRefOid } = 1 + sizeOf now + sizeOf pr + sizeOf headRefOid", + "typeHash": "9ffd5b025175463b1a1ab9e086a100d865f724ba8df221ef73ed7e1937b2def4", + "valueHash": null + }, + { + "name": "WatchPoll.Call.noConfusion", + "kind": "definition", + "type": "{P : Sort u} → {t t' : WatchPoll.Call} → t = t' → WatchPoll.Call.noConfusionType P t t'", + "typeHash": "05e3703f5564c8b38368f641236abe8d8aee975e705f6717e15881bf89d4a522", + "valueHash": "9d0ed728773d79bdf45c0eb047afb5bd92f825a05b5853e7b7b6d80b05c2059e" + }, + { + "name": "WatchPoll.Call.noConfusionType", + "kind": "definition", + "type": "Sort u → WatchPoll.Call → WatchPoll.Call → Sort u", + "typeHash": "f187009dfe119ee03f321acd6a53ba8ea5d73b2f8bd91ceeb9acd651a9e5da29", + "valueHash": "33584bcd1211e3fea756d1bc5290bb94e93231f8eabfaafd31ae2094ac98c015" + }, + { + "name": "WatchPoll.Call.now", + "kind": "definition", + "type": "WatchPoll.Call → Nat", + "typeHash": "d2420d36e0a6efa6196fa981834368126db1a010b09da26b929ba66b75261d76", + "valueHash": "d407d01883baf477d1643034cb2f2d13333bcbcfb08b82878aabc305628455ee" + }, + { + "name": "WatchPoll.Call.pr", + "kind": "definition", + "type": "WatchPoll.Call → Nat", + "typeHash": "d2420d36e0a6efa6196fa981834368126db1a010b09da26b929ba66b75261d76", + "valueHash": "6e75276921efb91e4d32fa6671cee9fff95e9a84b99fcc3be9e967a7bcb705e4" + }, + { + "name": "WatchPoll.Call.rec", + "kind": "recursor", + "type": "{motive : WatchPoll.Call → Sort u} → ((now pr headRefOid : Nat) → motive { now := now, pr := pr, headRefOid := headRefOid }) → (t : WatchPoll.Call) → motive t", + "typeHash": "dec2013366fe59350c5fe3601f50d5147952cfefc7a4f9b7ab2ad59c1502ae62", + "valueHash": null + }, + { + "name": "WatchPoll.Call.recOn", + "kind": "definition", + "type": "{motive : WatchPoll.Call → Sort u} → (t : WatchPoll.Call) → ((now pr headRefOid : Nat) → motive { now := now, pr := pr, headRefOid := headRefOid }) → motive t", + "typeHash": "b4363b5b01c54f98f69312cf255e2aea42ff0a2190633293ed98c37ba7ba969d", + "valueHash": "dd6f39e2784a6f24ce63787ea678885a99d8513971bb1b900fa5f3a7cbc9c1cf" + }, + { + "name": "WatchPoll.ClockMustAdvance", + "kind": "definition", + "type": "List WatchPoll.Call → WatchPoll.Call → Prop", + "typeHash": "83ce7891a040b224adc8624057bed5f87dd68396bdb0533616800bb394187f16", + "valueHash": "69ecce0d41492588ae94cbbac0cfacca11fedde1b7dea62d0fde84e58e33d91a" + }, + { + "name": "WatchPoll.ConfirmSound", + "kind": "definition", + "type": "List WatchPoll.Call → WatchPoll.Call → Prop", + "typeHash": "83ce7891a040b224adc8624057bed5f87dd68396bdb0533616800bb394187f16", + "valueHash": "963d8b08cccc8b3b00a6690c3c961220dc2d10a60f1d7803d3c224111218f969" + }, + { + "name": "WatchPoll.ConfirmSpec", + "kind": "definition", + "type": "List WatchPoll.Call → WatchPoll.Call → Prop", + "typeHash": "83ce7891a040b224adc8624057bed5f87dd68396bdb0533616800bb394187f16", + "valueHash": "c07905aba3a85166af4bd9695e8161b9c92de39ce27d2b8d154c1056cdf30bf7" + }, + { + "name": "WatchPoll.Event", + "kind": "inductive", + "type": "Type", + "typeHash": "7051ab654d793c216d744f7920d98d672357a0efa67248ee75d146d668f60997", + "valueHash": null + }, + { + "name": "WatchPoll.Event.casesOn", + "kind": "definition", + "type": "{motive : WatchPoll.Event → Sort u} → (t : WatchPoll.Event) → ((outcome : WatchPoll.StepOutcome) → (stepElapsed sleepElapsed : Nat) → motive { outcome := outcome, stepElapsed := stepElapsed, sleepElapsed := sleepElapsed }) → motive t", + "typeHash": "db985f919ff1f330488246e520d8e4cae007337e195869df6f9eaa8a50209c3b", + "valueHash": "f3a6db032df850b7f6213e8e6d34b7e57a9cb8b57fe96cd8836814af864c7e15" + }, + { + "name": "WatchPoll.Event.ctorIdx", + "kind": "definition", + "type": "WatchPoll.Event → Nat", + "typeHash": "44e365ee103cce197cf5697da89eef8bf974214e47896921900356e5c6c17949", + "valueHash": "595275faf926c6e3c79c515b99958850a382cb9c9928bd4b915cc815ebcde93b" + }, + { + "name": "WatchPoll.Event.mk", + "kind": "constructor", + "type": "WatchPoll.StepOutcome → Nat → Nat → WatchPoll.Event", + "typeHash": "688a547e5811030a1d7d892d2046da2d6a85c5e1c11fc4168fb2b22adfe99017", + "valueHash": null + }, + { + "name": "WatchPoll.Event.mk.inj", + "kind": "theorem", + "type": "∀ {outcome : WatchPoll.StepOutcome} {stepElapsed sleepElapsed : Nat} {outcome_1 : WatchPoll.StepOutcome} {stepElapsed_1 sleepElapsed_1 : Nat}, { outcome := outcome, stepElapsed := stepElapsed, sleepElapsed := sleepElapsed } = { outcome := outcome_1, stepElapsed := stepElapsed_1, sleepElapsed := sleepElapsed_1 } → outcome = outcome_1 ∧ stepElapsed = stepElapsed_1 ∧ sleepElapsed = sleepElapsed_1", + "typeHash": "6786ea41386160305171b09776b8ea4b75f7932d2c991bd1fcf0dc26bcd56ac3", + "valueHash": null + }, + { + "name": "WatchPoll.Event.mk.injEq", + "kind": "theorem", + "type": "∀ (outcome : WatchPoll.StepOutcome) (stepElapsed sleepElapsed : Nat) (outcome_1 : WatchPoll.StepOutcome) (stepElapsed_1 sleepElapsed_1 : Nat), ({ outcome := outcome, stepElapsed := stepElapsed, sleepElapsed := sleepElapsed } = { outcome := outcome_1, stepElapsed := stepElapsed_1, sleepElapsed := sleepElapsed_1 }) = (outcome = outcome_1 ∧ stepElapsed = stepElapsed_1 ∧ sleepElapsed = sleepElapsed_1)", + "typeHash": "d5dfcea0120f3b4fb9ebf6adf1886e3397682c3c9d3dc1a93207184ad044fe4d", + "valueHash": null + }, + { + "name": "WatchPoll.Event.mk.noConfusion", + "kind": "definition", + "type": "{P : Sort u} → {outcome : WatchPoll.StepOutcome} → {stepElapsed sleepElapsed : Nat} → {outcome' : WatchPoll.StepOutcome} → {stepElapsed' sleepElapsed' : Nat} → { outcome := outcome, stepElapsed := stepElapsed, sleepElapsed := sleepElapsed } = { outcome := outcome', stepElapsed := stepElapsed', sleepElapsed := sleepElapsed' } → (outcome = outcome' → stepElapsed = stepElapsed' → sleepElapsed = sleepElapsed' → P) → P", + "typeHash": "38132b8482cddf30b2b7f494d5fecfdbfaf28ea33cb43c18e8c2c3d7ec7f3398", + "valueHash": "c46068417a998c6c968afd07b121b3a27a6ee3fc3eaed995260986fdcf2ada27" + }, + { + "name": "WatchPoll.Event.mk.sizeOf_spec", + "kind": "theorem", + "type": "∀ (outcome : WatchPoll.StepOutcome) (stepElapsed sleepElapsed : Nat), sizeOf { outcome := outcome, stepElapsed := stepElapsed, sleepElapsed := sleepElapsed } = 1 + sizeOf outcome + sizeOf stepElapsed + sizeOf sleepElapsed", + "typeHash": "a1f6b0c3089b2daa2823afa6590002a058980b46c8e5cd28237fcb033443e9ec", + "valueHash": null + }, + { + "name": "WatchPoll.Event.noConfusion", + "kind": "definition", + "type": "{P : Sort u} → {t t' : WatchPoll.Event} → t = t' → WatchPoll.Event.noConfusionType P t t'", + "typeHash": "401896adaba7a0589b74ddeb58065ac7ee537b9a61a1419bddec8492ca5614da", + "valueHash": "653af2916ddcac6233f85eac5ee2c6f005f70d5160cfa61b019473ca968adc8d" + }, + { + "name": "WatchPoll.Event.noConfusionType", + "kind": "definition", + "type": "Sort u → WatchPoll.Event → WatchPoll.Event → Sort u", + "typeHash": "d364dd484faf989769bd6a2d2ca896a46675d85870009ddede3a97eb78dfd9d4", + "valueHash": "5eba754efd9998cd9427837923c1b4f506fbe27aedbeb7641321e26a508ac2b6" + }, + { + "name": "WatchPoll.Event.outcome", + "kind": "definition", + "type": "WatchPoll.Event → WatchPoll.StepOutcome", + "typeHash": "68a88576a649bbbec0ba68488c81af616c6aba245f8c81b0d93b826e2a41afa9", + "valueHash": "49b85e3b6461e370b5d8a3a78b3e16c8bbe240cb0c8d39273c1eaf61368e3986" + }, + { + "name": "WatchPoll.Event.rec", + "kind": "recursor", + "type": "{motive : WatchPoll.Event → Sort u} → ((outcome : WatchPoll.StepOutcome) → (stepElapsed sleepElapsed : Nat) → motive { outcome := outcome, stepElapsed := stepElapsed, sleepElapsed := sleepElapsed }) → (t : WatchPoll.Event) → motive t", + "typeHash": "ff4bac9874f21d5ae23240c2086d118535400473951da962532d16d7aecf0c59", + "valueHash": null + }, + { + "name": "WatchPoll.Event.recOn", + "kind": "definition", + "type": "{motive : WatchPoll.Event → Sort u} → (t : WatchPoll.Event) → ((outcome : WatchPoll.StepOutcome) → (stepElapsed sleepElapsed : Nat) → motive { outcome := outcome, stepElapsed := stepElapsed, sleepElapsed := sleepElapsed }) → motive t", + "typeHash": "db985f919ff1f330488246e520d8e4cae007337e195869df6f9eaa8a50209c3b", + "valueHash": "29378c045db8f3b0f7d302eb8d8600e7c105374a98c9083cf67907dffcb4f250" + }, + { + "name": "WatchPoll.Event.sleepElapsed", + "kind": "definition", + "type": "WatchPoll.Event → Nat", + "typeHash": "48c7e4b970ab96069032dacee319c261d69fea8b832c840c14e84822aa0c028e", + "valueHash": "4a1ba7d64ec6050bbf6da083e5b652b6c3b6b46433297facfde5b036eec7a671" + }, + { + "name": "WatchPoll.Event.stepElapsed", + "kind": "definition", + "type": "WatchPoll.Event → Nat", + "typeHash": "48c7e4b970ab96069032dacee319c261d69fea8b832c840c14e84822aa0c028e", + "valueHash": "9bc6cee40f9ddfd2fd2275750b933784174e809b9fc81ed7c1571e5ac5359064" + }, + { + "name": "WatchPoll.FirstSeen", + "kind": "definition", + "type": "Type", + "typeHash": "7051ab654d793c216d744f7920d98d672357a0efa67248ee75d146d668f60997", + "valueHash": "cd57d6be0ae4dc87942064f509cc5d60c14f11c8a819c3aaa86cb121cc1b68f4" + }, + { + "name": "WatchPoll.FirstSightingFalse", + "kind": "definition", + "type": "List WatchPoll.Call → WatchPoll.Call → Prop", + "typeHash": "83ce7891a040b224adc8624057bed5f87dd68396bdb0533616800bb394187f16", + "valueHash": "5e0f8b7f5342d1ec69685207008b375b7de14b1d6af3a1f63792916cd2cfc0d9" + }, + { + "name": "WatchPoll.HeadChangeFalse", + "kind": "definition", + "type": "List WatchPoll.Call → WatchPoll.Call → Prop", + "typeHash": "83ce7891a040b224adc8624057bed5f87dd68396bdb0533616800bb394187f16", + "valueHash": "2d48f695155913ea847ffbf45122b5c9ecba415a4de01aac90aec230624214aa" + }, + { + "name": "WatchPoll.HeadChangeFalse.match_1", + "kind": "definition", + "type": "(motive : Option WatchPoll.Call → Sort u_1) → (x : Option WatchPoll.Call) → ((latest : WatchPoll.Call) → motive (some latest)) → (Unit → motive none) → motive x", + "typeHash": "466ff817b93608d9f0e0691c9bd2538b48a4e6d010cd18edfd2db3faf99600c3", + "valueHash": "1222be1a4ebee0d0e5237b1b58fe3d876b4298adc32ed381e0b1dde9680588fd" + }, + { + "name": "WatchPoll.HeadChangeFalse.match_1.eq_1", + "kind": "theorem", + "type": "∀ (motive : Option WatchPoll.Call → Sort u_1) (latest : WatchPoll.Call) (h_1 : (latest : WatchPoll.Call) → motive (some latest)) (h_2 : Unit → motive none), (match some latest with | some latest => h_1 latest | none => h_2 ()) = h_1 latest", + "typeHash": "f6cdb03b6376a350616c9edce0919da7d8c50e8cd979dee123c54392f349f057", + "valueHash": null + }, + { + "name": "WatchPoll.HeadChangeFalse.match_1.eq_2", + "kind": "theorem", + "type": "∀ (motive : Option WatchPoll.Call → Sort u_1) (h_1 : (latest : WatchPoll.Call) → motive (some latest)) (h_2 : Unit → motive none), (match none with | some latest => h_1 latest | none => h_2 ()) = h_2 ()", + "typeHash": "d6203fdfa0177e8189c05b75a21827be76ff560d27b8b7eb07d4a1c7093bc22d", + "valueHash": null + }, + { + "name": "WatchPoll.HeadChangeFalse.match_1.splitter", + "kind": "definition", + "type": "(motive : Option WatchPoll.Call → Sort u_1) → (x : Option WatchPoll.Call) → ((latest : WatchPoll.Call) → motive (some latest)) → (Unit → motive none) → motive x", + "typeHash": "466ff817b93608d9f0e0691c9bd2538b48a4e6d010cd18edfd2db3faf99600c3", + "valueHash": "cbc1ef6084130211c55240d5036daa274f3eaea6c7cdb3a230998e92fd0a13a7" + }, + { + "name": "WatchPoll.HonestClock", + "kind": "definition", + "type": "WatchPoll.Event → WatchPoll.State → Prop", + "typeHash": "f2fe8d1b0b79f37f73a13df2dd8632996e4d814040bbb5c6737ba65cbb6bfd9f", + "valueHash": "0772c6b1c56e4471c557d0d8e9390d6838b303dd46bd8b28907afe194458c1c2" + }, + { + "name": "WatchPoll.HonestRun", + "kind": "definition", + "type": "WatchPoll.PollingOptions → WatchPoll.State → List WatchPoll.Event → Prop", + "typeHash": "a823f839c23d4c05bf14631e3dd0fd6e39001ae1fa45e8b090cb3e8cf3d6c6e4", + "valueHash": "f20851c8682f87256c9316c5fa127292d6ab934ff708e5515443d9fa95667b87" + }, + { + "name": "WatchPoll.Invariant", + "kind": "definition", + "type": "WatchPoll.PollingOptions → WatchPoll.State → Prop", + "typeHash": "806abaf0994e23552112ca5fc0e9baaf70774e7ef4e647f7f68a33b9ee753ddc", + "valueHash": "73080e8ac86a15989c8934d7deef6bda3b39697fca2d33ced68884c40a258390" + }, + { + "name": "WatchPoll.Last", + "kind": "inductive", + "type": "Type", + "typeHash": "7051ab654d793c216d744f7920d98d672357a0efa67248ee75d146d668f60997", + "valueHash": null + }, + { + "name": "WatchPoll.Last.casesOn", + "kind": "definition", + "type": "{motive : WatchPoll.Last → Sort u} → (t : WatchPoll.Last) → motive WatchPoll.Last.start → ((seconds : Nat) → motive (WatchPoll.Last.sleptPoll seconds)) → ((seconds : Nat) → motive (WatchPoll.Last.sleptRetry seconds)) → motive WatchPoll.Last.continued → motive WatchPoll.Last.returnedVerdict → ((failures : Nat) → motive (WatchPoll.Last.returnedStatusQuery failures)) → motive WatchPoll.Last.returnedOnDeadline → motive WatchPoll.Last.threw → motive t", + "typeHash": "cf58ac1b6f4a63e35359ee9328eb9f7a453a1035e6d4264db84bee3599502778", + "valueHash": "566192517f433b1f5a38c06498f4a3c4e93dc827eca1056a179b4a2cdc78cc43" + }, + { + "name": "WatchPoll.Last.continued", + "kind": "constructor", + "type": "WatchPoll.Last", + "typeHash": "3120367cac7d19428f10ba958934db8caf91fcd329c508c64099d26517728cf5", + "valueHash": null + }, + { + "name": "WatchPoll.Last.continued.elim", + "kind": "definition", + "type": "{motive : WatchPoll.Last → Sort u} → (t : WatchPoll.Last) → t.ctorIdx = 3 → motive WatchPoll.Last.continued → motive t", + "typeHash": "ee6b9c3bacb6eb06b11e5650236ab4b14d9820caf91255e13ed0ad9480982789", + "valueHash": "04b3d7b1b515b4ba7c8e905edc37252913504a7bad39d111f75eec489ae0db50" + }, + { + "name": "WatchPoll.Last.continued.sizeOf_spec", + "kind": "theorem", + "type": "sizeOf WatchPoll.Last.continued = 1", + "typeHash": "d5b4357fc23c37a2cde7f07b522fc81022c7e029ab299bf3fee7b03895528d31", + "valueHash": null + }, + { + "name": "WatchPoll.Last.ctorElim", + "kind": "definition", + "type": "{motive : WatchPoll.Last → Sort u} → (ctorIdx : Nat) → (t : WatchPoll.Last) → ctorIdx = t.ctorIdx → WatchPoll.Last.ctorElimType ctorIdx → motive t", + "typeHash": "07d1569057066980b19c2a82644b8e0eea81bb6f9cba03ebe38c4e1e6c8c8748", + "valueHash": "2c8afde2e19e2461bdc5a9072dd0e806b6776d414e2ba6434157793da17d3f75" + }, + { + "name": "WatchPoll.Last.ctorElimType", + "kind": "definition", + "type": "{motive : WatchPoll.Last → Sort u} → Nat → Sort (max 1 u)", + "typeHash": "9fe1f3f5298b5d6c9548bcb433f8252f5f7df1651adda0fae33b3d90160eeb49", + "valueHash": "97824acf24ae34e2f7584d6a5dff9172d6615544821fa65bd50a36628c5605dd" + }, + { + "name": "WatchPoll.Last.ctorIdx", + "kind": "definition", + "type": "WatchPoll.Last → Nat", + "typeHash": "365510ba2576c32956c415e9b371c3c6248bd5c2b610ee3eb547c7bd5496370e", + "valueHash": "9ff8ba758b44e3512d4262d6f60f7475f54ca158b2d63e5761a44e245a09a7dd" + }, + { + "name": "WatchPoll.Last.noConfusion", + "kind": "definition", + "type": "{P : Sort u} → {t t' : WatchPoll.Last} → t = t' → WatchPoll.Last.noConfusionType P t t'", + "typeHash": "bf749c9aff9e64f609a8b69d13cae1526f7c08ac56e84c44a085ecd15ea05fa8", + "valueHash": "2ecc7569c441312ea03ea04a6831a2088808469a4f9d433cd1e57905433f873a" + }, + { + "name": "WatchPoll.Last.noConfusionType", + "kind": "definition", + "type": "Sort u → WatchPoll.Last → WatchPoll.Last → Sort u", + "typeHash": "62406b894761875a6d2392a8816778edd99025229abc3631ed465bcf1d9063f8", + "valueHash": "c4b71db873b66644bdc30eff93f134a9c0364c01b0d9f8431c5b73dec832d740" + }, + { + "name": "WatchPoll.Last.rec", + "kind": "recursor", + "type": "{motive : WatchPoll.Last → Sort u} → motive WatchPoll.Last.start → ((seconds : Nat) → motive (WatchPoll.Last.sleptPoll seconds)) → ((seconds : Nat) → motive (WatchPoll.Last.sleptRetry seconds)) → motive WatchPoll.Last.continued → motive WatchPoll.Last.returnedVerdict → ((failures : Nat) → motive (WatchPoll.Last.returnedStatusQuery failures)) → motive WatchPoll.Last.returnedOnDeadline → motive WatchPoll.Last.threw → (t : WatchPoll.Last) → motive t", + "typeHash": "4a8f9a27f5cc2403420b344baec7eecfcb7d444545a5578c41b0e0670f8bab45", + "valueHash": null + }, + { + "name": "WatchPoll.Last.recOn", + "kind": "definition", + "type": "{motive : WatchPoll.Last → Sort u} → (t : WatchPoll.Last) → motive WatchPoll.Last.start → ((seconds : Nat) → motive (WatchPoll.Last.sleptPoll seconds)) → ((seconds : Nat) → motive (WatchPoll.Last.sleptRetry seconds)) → motive WatchPoll.Last.continued → motive WatchPoll.Last.returnedVerdict → ((failures : Nat) → motive (WatchPoll.Last.returnedStatusQuery failures)) → motive WatchPoll.Last.returnedOnDeadline → motive WatchPoll.Last.threw → motive t", + "typeHash": "cf58ac1b6f4a63e35359ee9328eb9f7a453a1035e6d4264db84bee3599502778", + "valueHash": "440560cf4324111f1d770918692e41e92b65ef4b5956607f5084f4c4332aba75" + }, + { + "name": "WatchPoll.Last.returnedOnDeadline", + "kind": "constructor", + "type": "WatchPoll.Last", + "typeHash": "3120367cac7d19428f10ba958934db8caf91fcd329c508c64099d26517728cf5", + "valueHash": null + }, + { + "name": "WatchPoll.Last.returnedOnDeadline.elim", + "kind": "definition", + "type": "{motive : WatchPoll.Last → Sort u} → (t : WatchPoll.Last) → t.ctorIdx = 6 → motive WatchPoll.Last.returnedOnDeadline → motive t", + "typeHash": "26ad8ec6f0904fa5af7af166bab486ee0c1aa7c9be57fe59f3ccdfb66e65af6c", + "valueHash": "1488fb61dd4e57b5477d61ca074999c405913afe4a3deb92eeda0a842ba38495" + }, + { + "name": "WatchPoll.Last.returnedOnDeadline.sizeOf_spec", + "kind": "theorem", + "type": "sizeOf WatchPoll.Last.returnedOnDeadline = 1", + "typeHash": "91edd39717c010012e8420ff2ba492697ca11d84545d34ec455451c56bce0521", + "valueHash": null + }, + { + "name": "WatchPoll.Last.returnedStatusQuery", + "kind": "constructor", + "type": "Nat → WatchPoll.Last", + "typeHash": "4aa8b4a3d40e4ea0bd47d78db56aebe50aac681400dac0ab9912bb277ea3353e", + "valueHash": null + }, + { + "name": "WatchPoll.Last.returnedStatusQuery.elim", + "kind": "definition", + "type": "{motive : WatchPoll.Last → Sort u} → (t : WatchPoll.Last) → t.ctorIdx = 5 → ((failures : Nat) → motive (WatchPoll.Last.returnedStatusQuery failures)) → motive t", + "typeHash": "4cdd2c93e7b9a3665297ab3cba2918be4f75e7969004f483097610d13fc71338", + "valueHash": "63e2c8b78868a8d1331790ce5754a2534a13d97a8d179b28fdd8ca5e53082f74" + }, + { + "name": "WatchPoll.Last.returnedStatusQuery.inj", + "kind": "theorem", + "type": "∀ {failures failures_1 : Nat}, WatchPoll.Last.returnedStatusQuery failures = WatchPoll.Last.returnedStatusQuery failures_1 → failures = failures_1", + "typeHash": "299934fcc46f24295c756d743d8350067bccfb9a2494b305abf1f695037464a9", + "valueHash": null + }, + { + "name": "WatchPoll.Last.returnedStatusQuery.injEq", + "kind": "theorem", + "type": "∀ (failures failures_1 : Nat), (WatchPoll.Last.returnedStatusQuery failures = WatchPoll.Last.returnedStatusQuery failures_1) = (failures = failures_1)", + "typeHash": "c1efb2547ec0b1fe31f71569a7a1f20cd3854b0a466b661e5be6cbca71b9a485", + "valueHash": null + }, + { + "name": "WatchPoll.Last.returnedStatusQuery.noConfusion", + "kind": "definition", + "type": "{P : Sort u} → {failures failures' : Nat} → WatchPoll.Last.returnedStatusQuery failures = WatchPoll.Last.returnedStatusQuery failures' → (failures = failures' → P) → P", + "typeHash": "221ee0cb5d266cf92442e026750faa0aeb260674c1cc0a622086a808bc74e38b", + "valueHash": "4fe0172991b3da422b28dc0f4cf46cce49ea622d09f7c8feba20010c5303e07c" + }, + { + "name": "WatchPoll.Last.returnedStatusQuery.sizeOf_spec", + "kind": "theorem", + "type": "∀ (failures : Nat), sizeOf (WatchPoll.Last.returnedStatusQuery failures) = 1 + sizeOf failures", + "typeHash": "c218575f4a43f1033bf3457e864caec08e757ee0c6a18d1124747ca0c85f641f", + "valueHash": null + }, + { + "name": "WatchPoll.Last.returnedVerdict", + "kind": "constructor", + "type": "WatchPoll.Last", + "typeHash": "3120367cac7d19428f10ba958934db8caf91fcd329c508c64099d26517728cf5", + "valueHash": null + }, + { + "name": "WatchPoll.Last.returnedVerdict.elim", + "kind": "definition", + "type": "{motive : WatchPoll.Last → Sort u} → (t : WatchPoll.Last) → t.ctorIdx = 4 → motive WatchPoll.Last.returnedVerdict → motive t", + "typeHash": "b118220517d057669b90ad6c3cff7d5e81d79599518c7ffa73f743cf85c06840", + "valueHash": "c566acecbe7daf7e6e63395bd9f2c9c46d9fb305a2f112935a8c2ec2a15a2846" + }, + { + "name": "WatchPoll.Last.returnedVerdict.sizeOf_spec", + "kind": "theorem", + "type": "sizeOf WatchPoll.Last.returnedVerdict = 1", + "typeHash": "ffc4763650967727f962f4101ba7812edb6e2281a3502a05444b2c2b1b4dd120", + "valueHash": null + }, + { + "name": "WatchPoll.Last.running", + "kind": "definition", + "type": "WatchPoll.Last → Bool", + "typeHash": "5f6dc32c00578e22097c48d2aefc107f1b9c3b5380e0d5ba987c3868027469ea", + "valueHash": "25dd504b6e743e978fff22a17d85da80190a3e25dbc6e374eaa6521ad5520180" + }, + { + "name": "WatchPoll.Last.running.match_1", + "kind": "definition", + "type": "(motive : WatchPoll.Last → Sort u_1) → (x : WatchPoll.Last) → (Unit → motive WatchPoll.Last.start) → ((seconds : Nat) → motive (WatchPoll.Last.sleptPoll seconds)) → ((seconds : Nat) → motive (WatchPoll.Last.sleptRetry seconds)) → (Unit → motive WatchPoll.Last.continued) → ((x : WatchPoll.Last) → motive x) → motive x", + "typeHash": "f39da8b5757a5c5113973f40f097ff060b1fbd5eb32f50071b6747bfdb1d06bc", + "valueHash": "f9431b6e50a96877adcf543bbd5383744ff7a0474d8203a689936827c15d37d4" + }, + { + "name": "WatchPoll.Last.sleepSeconds", + "kind": "definition", + "type": "WatchPoll.Last → Option Nat", + "typeHash": "2998cacf032e6b242c44e5a50a01412afcb080e8d2b786423d76e6692a39143c", + "valueHash": "28a04cee31a28cbf153a236c4fd83203ad49bf8bf4354e887cfab8fa6289c027" + }, + { + "name": "WatchPoll.Last.sleepSeconds.match_1", + "kind": "definition", + "type": "(motive : WatchPoll.Last → Sort u_1) → (x : WatchPoll.Last) → ((seconds : Nat) → motive (WatchPoll.Last.sleptPoll seconds)) → ((seconds : Nat) → motive (WatchPoll.Last.sleptRetry seconds)) → ((x : WatchPoll.Last) → motive x) → motive x", + "typeHash": "a165f906b220eb3b98c8a1822ae169f4d358b73d6e641fbd5dcec3682babac17", + "valueHash": "0d290a6e90d54a3557140e99ab3da8de93a2416a6b1d21f59722583cc57fe721" + }, + { + "name": "WatchPoll.Last.sleptPoll", + "kind": "constructor", + "type": "Nat → WatchPoll.Last", + "typeHash": "68170f6140999251b378fa9bbf03f656e198815acbbef45a52ecabecbb127c5e", + "valueHash": null + }, + { + "name": "WatchPoll.Last.sleptPoll.elim", + "kind": "definition", + "type": "{motive : WatchPoll.Last → Sort u} → (t : WatchPoll.Last) → t.ctorIdx = 1 → ((seconds : Nat) → motive (WatchPoll.Last.sleptPoll seconds)) → motive t", + "typeHash": "7f751f46457da2706bda9e2a23a418b8a5a0a3d2ee27d8d6a11d9f0ef11484e3", + "valueHash": "29c7b620dd01bb323c8bd4c9afd9ef7eb2a4dfff232f30754c636ae3e5095c81" + }, + { + "name": "WatchPoll.Last.sleptPoll.inj", + "kind": "theorem", + "type": "∀ {seconds seconds_1 : Nat}, WatchPoll.Last.sleptPoll seconds = WatchPoll.Last.sleptPoll seconds_1 → seconds = seconds_1", + "typeHash": "6e0125ccb095a12d60ccb55247960b4ce82c8a94e25eba3c876dcf624eaccf7a", + "valueHash": null + }, + { + "name": "WatchPoll.Last.sleptPoll.injEq", + "kind": "theorem", + "type": "∀ (seconds seconds_1 : Nat), (WatchPoll.Last.sleptPoll seconds = WatchPoll.Last.sleptPoll seconds_1) = (seconds = seconds_1)", + "typeHash": "f2047265013fdca5b70e37857ebef24d11df638834fe87319112f4ec98a3103a", + "valueHash": null + }, + { + "name": "WatchPoll.Last.sleptPoll.noConfusion", + "kind": "definition", + "type": "{P : Sort u} → {seconds seconds' : Nat} → WatchPoll.Last.sleptPoll seconds = WatchPoll.Last.sleptPoll seconds' → (seconds = seconds' → P) → P", + "typeHash": "c79cf77cded0c82391a347e461bb5ff0a6ed8b1c5f3fd525d137377d3d40c585", + "valueHash": "ccc286b92bc2028b3d62aa2ece48faf2b893c0ed0271ba83e0bb962149d1fea7" + }, + { + "name": "WatchPoll.Last.sleptPoll.sizeOf_spec", + "kind": "theorem", + "type": "∀ (seconds : Nat), sizeOf (WatchPoll.Last.sleptPoll seconds) = 1 + sizeOf seconds", + "typeHash": "afe725b2ccbc64879cc0302b959e2acd4b9a714aa73e1fbfead1fa57dea25de0", + "valueHash": null + }, + { + "name": "WatchPoll.Last.sleptRetry", + "kind": "constructor", + "type": "Nat → WatchPoll.Last", + "typeHash": "68170f6140999251b378fa9bbf03f656e198815acbbef45a52ecabecbb127c5e", + "valueHash": null + }, + { + "name": "WatchPoll.Last.sleptRetry.elim", + "kind": "definition", + "type": "{motive : WatchPoll.Last → Sort u} → (t : WatchPoll.Last) → t.ctorIdx = 2 → ((seconds : Nat) → motive (WatchPoll.Last.sleptRetry seconds)) → motive t", + "typeHash": "d9f3e3cd5fc00c0054f1dd2b2d638ea101ec8ffd70640d28c6f818b3970d1a5d", + "valueHash": "353379747d930906445c10b175c0935afb50b5974adf740d0e213fd21de5188f" + }, + { + "name": "WatchPoll.Last.sleptRetry.inj", + "kind": "theorem", + "type": "∀ {seconds seconds_1 : Nat}, WatchPoll.Last.sleptRetry seconds = WatchPoll.Last.sleptRetry seconds_1 → seconds = seconds_1", + "typeHash": "46932e39fbdb2d9aad4c7f238cfe5d7e9afb39238657d2ca2b9b64a0ad39d896", + "valueHash": null + }, + { + "name": "WatchPoll.Last.sleptRetry.injEq", + "kind": "theorem", + "type": "∀ (seconds seconds_1 : Nat), (WatchPoll.Last.sleptRetry seconds = WatchPoll.Last.sleptRetry seconds_1) = (seconds = seconds_1)", + "typeHash": "cdd33eb094f9df7d703ba24dafd259df4f37184551a9fa26f04c4164418bf1cd", + "valueHash": null + }, + { + "name": "WatchPoll.Last.sleptRetry.noConfusion", + "kind": "definition", + "type": "{P : Sort u} → {seconds seconds' : Nat} → WatchPoll.Last.sleptRetry seconds = WatchPoll.Last.sleptRetry seconds' → (seconds = seconds' → P) → P", + "typeHash": "3292fc5201adb8752ae9fa810aa5ef73a642afcbc2e908776ba33b808dcf172d", + "valueHash": "fb09195713c2404f0181df772f200b676fb4abce11c86d17fd4068fc91d6ae4c" + }, + { + "name": "WatchPoll.Last.sleptRetry.sizeOf_spec", + "kind": "theorem", + "type": "∀ (seconds : Nat), sizeOf (WatchPoll.Last.sleptRetry seconds) = 1 + sizeOf seconds", + "typeHash": "4e098c166d047dd26369eb085e438f2bb1dc70d66c2fc9224af78a2b1d8068bd", + "valueHash": null + }, + { + "name": "WatchPoll.Last.start", + "kind": "constructor", + "type": "WatchPoll.Last", + "typeHash": "3120367cac7d19428f10ba958934db8caf91fcd329c508c64099d26517728cf5", + "valueHash": null + }, + { + "name": "WatchPoll.Last.start.elim", + "kind": "definition", + "type": "{motive : WatchPoll.Last → Sort u} → (t : WatchPoll.Last) → t.ctorIdx = 0 → motive WatchPoll.Last.start → motive t", + "typeHash": "05371ea8dfd206acf1439d4b373dbfbe1383ed9aa581084664de84dd2ad05a7b", + "valueHash": "100c627e12766b7e1ed814a6859f3317132e8ca1a617336b81719462a79111c5" + }, + { + "name": "WatchPoll.Last.start.sizeOf_spec", + "kind": "theorem", + "type": "sizeOf WatchPoll.Last.start = 1", + "typeHash": "74b7bcea68eb10efe3c46bb4b964d717850214c08d95ad8c06ce71465d50ec4b", + "valueHash": null + }, + { + "name": "WatchPoll.Last.threw", + "kind": "constructor", + "type": "WatchPoll.Last", + "typeHash": "3120367cac7d19428f10ba958934db8caf91fcd329c508c64099d26517728cf5", + "valueHash": null + }, + { + "name": "WatchPoll.Last.threw.elim", + "kind": "definition", + "type": "{motive : WatchPoll.Last → Sort u} → (t : WatchPoll.Last) → t.ctorIdx = 7 → motive WatchPoll.Last.threw → motive t", + "typeHash": "397b8ed3188ffc5d61ce138f10408eafd9775468bc3e1a0aec85ebb9a0a16c78", + "valueHash": "952033fed208fd0894b31bbde4a2a5f109deec9103842e0d2c23bb0b70de7574" + }, + { + "name": "WatchPoll.Last.threw.sizeOf_spec", + "kind": "theorem", + "type": "sizeOf WatchPoll.Last.threw = 1", + "typeHash": "e8634bd6cfad2bcd2bf653bcb834a4b2977a3d13002b25a9b3d2acb948f48116", + "valueHash": null + }, + { + "name": "WatchPoll.NO_CHECKS_CONFIRM_SECONDS", + "kind": "definition", + "type": "Nat", + "typeHash": "d38ea4c0b755428100ea00487f1413e32d3d5dd05a51e0e78c9de5d27dd99e38", + "valueHash": "47a9c095bcb043915cf80f607e46e41a1134aa0faedbc01217ef03151d457a60" + }, + { + "name": "WatchPoll.PollingOptions", + "kind": "inductive", + "type": "Type", + "typeHash": "7051ab654d793c216d744f7920d98d672357a0efa67248ee75d146d668f60997", + "valueHash": null + }, + { + "name": "WatchPoll.PollingOptions.casesOn", + "kind": "definition", + "type": "{motive : WatchPoll.PollingOptions → Sort u} → (t : WatchPoll.PollingOptions) → ((interval maxQueryErrors : Nat) → motive { interval := interval, maxQueryErrors := maxQueryErrors }) → motive t", + "typeHash": "4ce2f72d727a515e006958eb9b0c62378c7876e8f9356f9c163ffb0b804e0ce4", + "valueHash": "d12bf740b643904affd35c85d704a73d947447a3806d3a220baf1d32dafc3203" + }, + { + "name": "WatchPoll.PollingOptions.ctorIdx", + "kind": "definition", + "type": "WatchPoll.PollingOptions → Nat", + "typeHash": "19d8e49a963b3e2084de852925601d4cab9bc5d41031e1cfa26404599e49bc93", + "valueHash": "1e163787844fc0f869c2ec1f85a564ad515f0cee5026af31dad6408ac27ffb6b" + }, + { + "name": "WatchPoll.PollingOptions.interval", + "kind": "definition", + "type": "WatchPoll.PollingOptions → Nat", + "typeHash": "8756e90465bbd9157964eaf568593554488b17d223202940a757eda702850988", + "valueHash": "4308544074776e83c0587e065c56fd4c23f0540908fbdb6265969c968e641d48" + }, + { + "name": "WatchPoll.PollingOptions.maxQueryErrors", + "kind": "definition", + "type": "WatchPoll.PollingOptions → Nat", + "typeHash": "8756e90465bbd9157964eaf568593554488b17d223202940a757eda702850988", + "valueHash": "372d5ed8d4551e5fad9eed1b3b74594fbf9045a7bccf324523ca0715c78ba20f" + }, + { + "name": "WatchPoll.PollingOptions.mk", + "kind": "constructor", + "type": "Nat → Nat → WatchPoll.PollingOptions", + "typeHash": "07c3f0a3424063c6b1e4910ed71d9182c530e2cac8ab4371a39a61cfdfa23420", + "valueHash": null + }, + { + "name": "WatchPoll.PollingOptions.mk.inj", + "kind": "theorem", + "type": "∀ {interval maxQueryErrors interval_1 maxQueryErrors_1 : Nat}, { interval := interval, maxQueryErrors := maxQueryErrors } = { interval := interval_1, maxQueryErrors := maxQueryErrors_1 } → interval = interval_1 ∧ maxQueryErrors = maxQueryErrors_1", + "typeHash": "fb74f80e2d0d301e9bc893d40ef9030f8aa8f076bf3e5dcb48757f594e144f2e", + "valueHash": null + }, + { + "name": "WatchPoll.PollingOptions.mk.injEq", + "kind": "theorem", + "type": "∀ (interval maxQueryErrors interval_1 maxQueryErrors_1 : Nat), ({ interval := interval, maxQueryErrors := maxQueryErrors } = { interval := interval_1, maxQueryErrors := maxQueryErrors_1 }) = (interval = interval_1 ∧ maxQueryErrors = maxQueryErrors_1)", + "typeHash": "653dbce8bfa116f1d0d51d178e715286fed98b74d9e43c03c0e303c09cf828b9", + "valueHash": null + }, + { + "name": "WatchPoll.PollingOptions.mk.noConfusion", + "kind": "definition", + "type": "{P : Sort u} → {interval maxQueryErrors interval' maxQueryErrors' : Nat} → { interval := interval, maxQueryErrors := maxQueryErrors } = { interval := interval', maxQueryErrors := maxQueryErrors' } → (interval = interval' → maxQueryErrors = maxQueryErrors' → P) → P", + "typeHash": "ff2185715534a4f08181591184f538b1f2177858e3f29f22558e486dd25e376f", + "valueHash": "5f3e8978b578931fab5cfc009b497a7894b2bc03542e4e79fc9841b351ebf9f4" + }, + { + "name": "WatchPoll.PollingOptions.mk.sizeOf_spec", + "kind": "theorem", + "type": "∀ (interval maxQueryErrors : Nat), sizeOf { interval := interval, maxQueryErrors := maxQueryErrors } = 1 + sizeOf interval + sizeOf maxQueryErrors", + "typeHash": "83711364218edc075fb23929366b7cd56f40c5a54509374956ee9f5d610b839c", + "valueHash": null + }, + { + "name": "WatchPoll.PollingOptions.noConfusion", + "kind": "definition", + "type": "{P : Sort u} → {t t' : WatchPoll.PollingOptions} → t = t' → WatchPoll.PollingOptions.noConfusionType P t t'", + "typeHash": "3f961fa303d86b0f87996d2b88c082e19920752cc514629c8c06379efed081c2", + "valueHash": "60e6ea28d3c37c5b7099836d67ba57741ebd9411f8d878fca08b9665981f3e9b" + }, + { + "name": "WatchPoll.PollingOptions.noConfusionType", + "kind": "definition", + "type": "Sort u → WatchPoll.PollingOptions → WatchPoll.PollingOptions → Sort u", + "typeHash": "01fd6ecc7b972316b29a8b32f877f62202e56c6c1a8d3d473ad75cafd113f2ac", + "valueHash": "816d0f65518c61b2d7846afc98d394e3d2b8c76da7be7a428e67cc90a8e5a6f2" + }, + { + "name": "WatchPoll.PollingOptions.rec", + "kind": "recursor", + "type": "{motive : WatchPoll.PollingOptions → Sort u} → ((interval maxQueryErrors : Nat) → motive { interval := interval, maxQueryErrors := maxQueryErrors }) → (t : WatchPoll.PollingOptions) → motive t", + "typeHash": "04a4c73cb7989d99c732c9d7e8adf32c6be874d1705ebe893de7e16d59e4cd3c", + "valueHash": null + }, + { + "name": "WatchPoll.PollingOptions.recOn", + "kind": "definition", + "type": "{motive : WatchPoll.PollingOptions → Sort u} → (t : WatchPoll.PollingOptions) → ((interval maxQueryErrors : Nat) → motive { interval := interval, maxQueryErrors := maxQueryErrors }) → motive t", + "typeHash": "4ce2f72d727a515e006958eb9b0c62378c7876e8f9356f9c163ffb0b804e0ce4", + "valueHash": "2924b645605146c22e1a7f1f02341a524fa77ea14d56b79669a5681eb1e48189" + }, + { + "name": "WatchPoll.ResetsFailures", + "kind": "definition", + "type": "WatchPoll.State → WatchPoll.Event → WatchPoll.State → Prop", + "typeHash": "adea0fde04e4356b5dabed5e2f643f775e7b570553307a3b9c99d868c6faeefc", + "valueHash": "cfdf7f4d1bfdb74b5f3da2042f5a211c42539db8c84aeed4f92a8c14c9e2f7e8" + }, + { + "name": "WatchPoll.RetriesBelowMax", + "kind": "definition", + "type": "WatchPoll.PollingOptions → WatchPoll.State → Prop", + "typeHash": "806abaf0994e23552112ca5fc0e9baaf70774e7ef4e647f7f68a33b9ee753ddc", + "valueHash": "7a3c9961dd068410081fef53787c99577dd82d526300e533af48e65fc1ca7f6e" + }, + { + "name": "WatchPoll.RetryUsesBackoff", + "kind": "definition", + "type": "WatchPoll.PollingOptions → WatchPoll.State → WatchPoll.Event → WatchPoll.State → Prop", + "typeHash": "f48e3d974828b3bf9980945ba23d46a408f2b4a36bc16c232b65ff83fa1969f6", + "valueHash": "32289000dc29b0a1eb4eb4ffed0d12d6132e190ef8c5f806909eb8a7f460318f" + }, + { + "name": "WatchPoll.RetryUsesBackoff.match_1", + "kind": "definition", + "type": "(motive : WatchPoll.Last → Sort u_1) → (x : WatchPoll.Last) → ((seconds : Nat) → motive (WatchPoll.Last.sleptRetry seconds)) → ((x : WatchPoll.Last) → motive x) → motive x", + "typeHash": "6774e8f0a20040847861b6a2cc6a9e0c417ae530a2799a35a8b4be1dde3405f8", + "valueHash": "eb4bec73d879562749114284966580749112e8de6190b4308d3d91805436c39f" + }, + { + "name": "WatchPoll.RetryUsesBackoff.match_1.eq_1", + "kind": "theorem", + "type": "∀ (motive : WatchPoll.Last → Sort u_1) (seconds : Nat) (h_1 : (seconds : Nat) → motive (WatchPoll.Last.sleptRetry seconds)) (h_2 : (x : WatchPoll.Last) → motive x), (match WatchPoll.Last.sleptRetry seconds with | WatchPoll.Last.sleptRetry seconds => h_1 seconds | x => h_2 x) = h_1 seconds", + "typeHash": "8b79d843fc6f903791c91bb4e0a6668afe1220a909fac5351a62e88360bb5c88", + "valueHash": null + }, + { + "name": "WatchPoll.RetryUsesBackoff.match_1.eq_2", + "kind": "theorem", + "type": "∀ (motive : WatchPoll.Last → Sort u_1) (x : WatchPoll.Last) (h_1 : (seconds : Nat) → motive (WatchPoll.Last.sleptRetry seconds)) (h_2 : (x : WatchPoll.Last) → motive x), (∀ (seconds : Nat), x = WatchPoll.Last.sleptRetry seconds → False) → (match x with | WatchPoll.Last.sleptRetry seconds => h_1 seconds | x => h_2 x) = h_2 x", + "typeHash": "ca55c378024e6543ef487f1572362f026bcfce1a414b5eb4fa4b9583dde58bba", + "valueHash": null + }, + { + "name": "WatchPoll.RetryUsesBackoff.match_1.splitter", + "kind": "definition", + "type": "(motive : WatchPoll.Last → Sort u_1) → (x : WatchPoll.Last) → ((seconds : Nat) → motive (WatchPoll.Last.sleptRetry seconds)) → ((x : WatchPoll.Last) → (∀ (seconds : Nat), x = WatchPoll.Last.sleptRetry seconds → False) → motive x) → motive x", + "typeHash": "55953a1264c78d1c8c27c50512044bb15b3c4bc0696af74667e92beacb0459fa", + "valueHash": "64a87e2bab3cf6defdc0a9e9cca188ee7bff2780a5ed3febf55a5055d59cb377" + }, + { + "name": "WatchPoll.Seconds", + "kind": "inductive", + "type": "Type", + "typeHash": "7051ab654d793c216d744f7920d98d672357a0efa67248ee75d146d668f60997", + "valueHash": null + }, + { + "name": "WatchPoll.Seconds.cap", + "kind": "definition", + "type": "WatchPoll.Seconds → Nat → Nat", + "typeHash": "d0cc204d25b2efea268eb346e4b0a7539523213688430c4a19435a324383ffe1", + "valueHash": "e028448fd46da9cd50813605d981d732545b1e481d0413a76ba878b3e0bd9e91" + }, + { + "name": "WatchPoll.Seconds.casesOn", + "kind": "definition", + "type": "{motive : WatchPoll.Seconds → Sort u} → (t : WatchPoll.Seconds) → ((seconds : Nat) → motive (WatchPoll.Seconds.finite seconds)) → motive WatchPoll.Seconds.infinity → motive t", + "typeHash": "445040cb426c3f653db0462bff027adaf7248bd3a23dee43e0898443ce41912d", + "valueHash": "ec3a96201ef19ba0a5de136a422eb832d8f37073ac4c14854e176d49d9947afb" + }, + { + "name": "WatchPoll.Seconds.covers", + "kind": "definition", + "type": "WatchPoll.Seconds → Nat → Prop", + "typeHash": "f1892df9d45103bf28372ea6c63d7bc5136f42d3539f24c47a4b05ee1a3a5d72", + "valueHash": "0181a8a8240ccd9c273f1e432e62eaaa4ccc26a3a332b496689054d3553445b4" + }, + { + "name": "WatchPoll.Seconds.ctorElim", + "kind": "definition", + "type": "{motive : WatchPoll.Seconds → Sort u} → (ctorIdx : Nat) → (t : WatchPoll.Seconds) → ctorIdx = t.ctorIdx → WatchPoll.Seconds.ctorElimType ctorIdx → motive t", + "typeHash": "f378fd7a77c08a3186fe4993954797bc52d3aa4bacad1bfb9ac2f728b85d358a", + "valueHash": "ee8761d487f0e814396a3d9a6f946fbde995f28c8bd3f93e9f63df0cba887dad" + }, + { + "name": "WatchPoll.Seconds.ctorElimType", + "kind": "definition", + "type": "{motive : WatchPoll.Seconds → Sort u} → Nat → Sort (max 1 u)", + "typeHash": "dfda63133bc562efbbbf0ca3814a9bea83e1832ea46391a1e760f42d10442cd4", + "valueHash": "7d865d4fbea0e12059e5637f642de6014f650a8668c5f9c518e79a712335fabd" + }, + { + "name": "WatchPoll.Seconds.ctorIdx", + "kind": "definition", + "type": "WatchPoll.Seconds → Nat", + "typeHash": "1cf16ea9d732ed6435f67e2996240ac2d9db021d7b3a0aee6130455397f14682", + "valueHash": "85ed3e459922d2546c665e03301237d5aab233b8475b3c81f31da515a144a9ac" + }, + { + "name": "WatchPoll.Seconds.elapse", + "kind": "definition", + "type": "WatchPoll.Seconds → Nat → WatchPoll.Seconds", + "typeHash": "7dcc3245bce6d91c1f8259068daa7623d4c02ebadc44da7ed79ae6f1f6544999", + "valueHash": "fbcd057bfcf2d5c52d7da20ea73dcd7a27d9d45af9bb52bb5b58a261c5a67455" + }, + { + "name": "WatchPoll.Seconds.finite", + "kind": "constructor", + "type": "Nat → WatchPoll.Seconds", + "typeHash": "bfaf9655ceea623ea57a2b37262ea763cb5f1e3abbe43c1436dbb6835393fda3", + "valueHash": null + }, + { + "name": "WatchPoll.Seconds.finite.elim", + "kind": "definition", + "type": "{motive : WatchPoll.Seconds → Sort u} → (t : WatchPoll.Seconds) → t.ctorIdx = 0 → ((seconds : Nat) → motive (WatchPoll.Seconds.finite seconds)) → motive t", + "typeHash": "19b632026cda4e8f09b58d374dfa5205048778f4fc125d8adacdf629c3f1f0cc", + "valueHash": "737d26cf6d01c870a8867a48fcda245ac29a81f036fd3e7750c9ef7263b35288" + }, + { + "name": "WatchPoll.Seconds.finite.inj", + "kind": "theorem", + "type": "∀ {seconds seconds_1 : Nat}, WatchPoll.Seconds.finite seconds = WatchPoll.Seconds.finite seconds_1 → seconds = seconds_1", + "typeHash": "bc01762f22d1153133bd3191c8957246680e762acca0e6fe89c5a0ef8e320fd8", + "valueHash": null + }, + { + "name": "WatchPoll.Seconds.finite.injEq", + "kind": "theorem", + "type": "∀ (seconds seconds_1 : Nat), (WatchPoll.Seconds.finite seconds = WatchPoll.Seconds.finite seconds_1) = (seconds = seconds_1)", + "typeHash": "05d5007a3348fa3b0bfdcad35b05e478580e5a5d77f452ef6ccf7faae9b4913c", + "valueHash": null + }, + { + "name": "WatchPoll.Seconds.finite.noConfusion", + "kind": "definition", + "type": "{P : Sort u} → {seconds seconds' : Nat} → WatchPoll.Seconds.finite seconds = WatchPoll.Seconds.finite seconds' → (seconds = seconds' → P) → P", + "typeHash": "275adc055e75aac88488560efd1f7f2b90f28abebf358c211a0f3e3433dec7ea", + "valueHash": "ce9b551e65441ecf2b09245df1ba5e2fe7c313d5c3f99535bba8f28655129673" + }, + { + "name": "WatchPoll.Seconds.finite.sizeOf_spec", + "kind": "theorem", + "type": "∀ (seconds : Nat), sizeOf (WatchPoll.Seconds.finite seconds) = 1 + sizeOf seconds", + "typeHash": "934e4fbd664d736e8eb6efdc1917f7caec7cafbaf237a35ae19bb1b8c4c42dfa", + "valueHash": null + }, + { + "name": "WatchPoll.Seconds.infinity", + "kind": "constructor", + "type": "WatchPoll.Seconds", + "typeHash": "8db42a2f42ef3e50dcc25118167715f61f09a04c6d0d4a7e51ee3fa2007c6613", + "valueHash": null + }, + { + "name": "WatchPoll.Seconds.infinity.elim", + "kind": "definition", + "type": "{motive : WatchPoll.Seconds → Sort u} → (t : WatchPoll.Seconds) → t.ctorIdx = 1 → motive WatchPoll.Seconds.infinity → motive t", + "typeHash": "99de191adae8914b0a3720e29d56965fe14a89f6876fc15205fc5d845bfe960a", + "valueHash": "b5b1dc91ae56dfbdfceae0fc14aad60023e2f2bd66cbbc15dce6c377d00af868" + }, + { + "name": "WatchPoll.Seconds.infinity.sizeOf_spec", + "kind": "theorem", + "type": "sizeOf WatchPoll.Seconds.infinity = 1", + "typeHash": "ed10bd39b21f853b016df913fa87090cca0da4c186315e7bb0bc915455207417", + "valueHash": null + }, + { + "name": "WatchPoll.Seconds.noConfusion", + "kind": "definition", + "type": "{P : Sort u} → {t t' : WatchPoll.Seconds} → t = t' → WatchPoll.Seconds.noConfusionType P t t'", + "typeHash": "39ba043d8af4aa00194ca30e9bbdca06e5591c99a81ecf0d2755c012cb49dec0", + "valueHash": "88bb20caed9a314c6b42d56bf7df2fc494678cb44e7a20d4c3c5560770cb8779" + }, + { + "name": "WatchPoll.Seconds.noConfusionType", + "kind": "definition", + "type": "Sort u → WatchPoll.Seconds → WatchPoll.Seconds → Sort u", + "typeHash": "37fbace67f7af355354486c7b99ce872a94e95a89301848ff4ecd9fd34b1bfa3", + "valueHash": "1d3cfc270d48a0a0f4f3bcad239c6cf3c80484c088f21a810468a2818556c6af" + }, + { + "name": "WatchPoll.Seconds.positive", + "kind": "definition", + "type": "WatchPoll.Seconds → Bool", + "typeHash": "593fc83dfb8d4875915b30821f94ad44b6b98d838c06e97eaad050c12c2a6d64", + "valueHash": "9aa1343ed92cc24d051495f294313e1a1bae8ab69d7724b35e90e869226ba34e" + }, + { + "name": "WatchPoll.Seconds.rec", + "kind": "recursor", + "type": "{motive : WatchPoll.Seconds → Sort u} → ((seconds : Nat) → motive (WatchPoll.Seconds.finite seconds)) → motive WatchPoll.Seconds.infinity → (t : WatchPoll.Seconds) → motive t", + "typeHash": "3870b58b0ff14d7e71e459e772fd3e4be0cf0bf3805d2a6a6c9531f4f5e618a5", + "valueHash": null + }, + { + "name": "WatchPoll.Seconds.recOn", + "kind": "definition", + "type": "{motive : WatchPoll.Seconds → Sort u} → (t : WatchPoll.Seconds) → ((seconds : Nat) → motive (WatchPoll.Seconds.finite seconds)) → motive WatchPoll.Seconds.infinity → motive t", + "typeHash": "445040cb426c3f653db0462bff027adaf7248bd3a23dee43e0898443ce41912d", + "valueHash": "14379873306e7783d26a9081a3e844c048d275626970696ba420d3f64a2e0b8e" + }, + { + "name": "WatchPoll.Sighting", + "kind": "inductive", + "type": "Type", + "typeHash": "7051ab654d793c216d744f7920d98d672357a0efa67248ee75d146d668f60997", + "valueHash": null + }, + { + "name": "WatchPoll.Sighting.casesOn", + "kind": "definition", + "type": "{motive : WatchPoll.Sighting → Sort u} → (t : WatchPoll.Sighting) → ((headRefOid seenAt : Nat) → motive { headRefOid := headRefOid, seenAt := seenAt }) → motive t", + "typeHash": "04c20ab319a929fe711a70edbcf1846be8237ea3fd429a7c878f77c230756aaf", + "valueHash": "f9d68e861082135c414a54db738859dabdd3a005a7715a58f12423247192dcde" + }, + { + "name": "WatchPoll.Sighting.ctorIdx", + "kind": "definition", + "type": "WatchPoll.Sighting → Nat", + "typeHash": "94f799bab78a9a40a4054c45f3c7d304347e14abb5487e4a19795b0dfe64ec8e", + "valueHash": "62bb22eb2d49dc7a26d0a57c6de73459ebe1971f38f0b703d86367d3ddd772e2" + }, + { + "name": "WatchPoll.Sighting.headRefOid", + "kind": "definition", + "type": "WatchPoll.Sighting → Nat", + "typeHash": "8f808618c6f531f02d6bcb0dcbe5d56fd1862fd356485be73e20e3e36d81f8a8", + "valueHash": "9f61ad1635e1847d4fc3a0d67ce08bb2ca9d6ee01461506653dff5fba30e4829" + }, + { + "name": "WatchPoll.Sighting.mk", + "kind": "constructor", + "type": "Nat → Nat → WatchPoll.Sighting", + "typeHash": "937bca33510c6de9bff109b4100c8abe31a2204b1983812bac5d4aaedcc59f3f", + "valueHash": null + }, + { + "name": "WatchPoll.Sighting.mk.inj", + "kind": "theorem", + "type": "∀ {headRefOid seenAt headRefOid_1 seenAt_1 : Nat}, { headRefOid := headRefOid, seenAt := seenAt } = { headRefOid := headRefOid_1, seenAt := seenAt_1 } → headRefOid = headRefOid_1 ∧ seenAt = seenAt_1", + "typeHash": "f56a46d47caf5f1747edf5e0afaafefa6c0100f51fc6e167881975dc54203fe7", + "valueHash": null + }, + { + "name": "WatchPoll.Sighting.mk.injEq", + "kind": "theorem", + "type": "∀ (headRefOid seenAt headRefOid_1 seenAt_1 : Nat), ({ headRefOid := headRefOid, seenAt := seenAt } = { headRefOid := headRefOid_1, seenAt := seenAt_1 }) = (headRefOid = headRefOid_1 ∧ seenAt = seenAt_1)", + "typeHash": "012971717f098fbcafa9c4f68dc8d8c7be6dd87db2424eb2726d03ac7ba8370e", + "valueHash": null + }, + { + "name": "WatchPoll.Sighting.mk.noConfusion", + "kind": "definition", + "type": "{P : Sort u} → {headRefOid seenAt headRefOid' seenAt' : Nat} → { headRefOid := headRefOid, seenAt := seenAt } = { headRefOid := headRefOid', seenAt := seenAt' } → (headRefOid = headRefOid' → seenAt = seenAt' → P) → P", + "typeHash": "cc69a58c9e5bcb8be4ac5c8e0ab31639984e638b83c4b255a8f8c31811c81ecf", + "valueHash": "a839efab40d945741bdae4f17ef1e0ad7cc52b0e2ccef877e168091f1ae6dfc2" + }, + { + "name": "WatchPoll.Sighting.mk.sizeOf_spec", + "kind": "theorem", + "type": "∀ (headRefOid seenAt : Nat), sizeOf { headRefOid := headRefOid, seenAt := seenAt } = 1 + sizeOf headRefOid + sizeOf seenAt", + "typeHash": "a69f193e9416a9699081e4900ba8626df13ea6fc1299e44cd25d2115c02759b7", + "valueHash": null + }, + { + "name": "WatchPoll.Sighting.noConfusion", + "kind": "definition", + "type": "{P : Sort u} → {t t' : WatchPoll.Sighting} → t = t' → WatchPoll.Sighting.noConfusionType P t t'", + "typeHash": "635ac385599a9770d45b9ffae276bed940349bfcd41f3945e4434c50d5e5959b", + "valueHash": "fa41d109e775043d40187fe0622dcbdf57790dd2e3416efb72656601a5083053" + }, + { + "name": "WatchPoll.Sighting.noConfusionType", + "kind": "definition", + "type": "Sort u → WatchPoll.Sighting → WatchPoll.Sighting → Sort u", + "typeHash": "4acdad30be4d2ac0733f9a232f20e1fb31ccb6b4533b354250625a98ff8a964a", + "valueHash": "52ae9149f14741c06d9a10454904ed36efdfe439a2690baecc2d92216da7c343" + }, + { + "name": "WatchPoll.Sighting.rec", + "kind": "recursor", + "type": "{motive : WatchPoll.Sighting → Sort u} → ((headRefOid seenAt : Nat) → motive { headRefOid := headRefOid, seenAt := seenAt }) → (t : WatchPoll.Sighting) → motive t", + "typeHash": "15e9b453fe8b737994bd1e869c111ac43733ed3e5eb6a71f9dc9f0921abf1ed6", + "valueHash": null + }, + { + "name": "WatchPoll.Sighting.recOn", + "kind": "definition", + "type": "{motive : WatchPoll.Sighting → Sort u} → (t : WatchPoll.Sighting) → ((headRefOid seenAt : Nat) → motive { headRefOid := headRefOid, seenAt := seenAt }) → motive t", + "typeHash": "04c20ab319a929fe711a70edbcf1846be8237ea3fd429a7c878f77c230756aaf", + "valueHash": "fe6c75f5071e34df8f4aafb202514afa2c97fdd078c14b696cbacdce47d4b72f" + }, + { + "name": "WatchPoll.Sighting.seenAt", + "kind": "definition", + "type": "WatchPoll.Sighting → Nat", + "typeHash": "8f808618c6f531f02d6bcb0dcbe5d56fd1862fd356485be73e20e3e36d81f8a8", + "valueHash": "53966f574603c8bb15086c9fa11193d947ef7cdfc9f05070889fb43eaf4ee932" + }, + { + "name": "WatchPoll.SleepWithinBudget", + "kind": "definition", + "type": "WatchPoll.State → WatchPoll.Event → WatchPoll.State → Prop", + "typeHash": "adea0fde04e4356b5dabed5e2f643f775e7b570553307a3b9c99d868c6faeefc", + "valueHash": "3570125c58005c20150146b716d3ee078e966c4b2bdd18fe632bcd9f548a06f8" + }, + { + "name": "WatchPoll.SleepWithinBudget.match_1", + "kind": "definition", + "type": "(motive : Option Nat → Sort u_1) → (x : Option Nat) → ((seconds : Nat) → motive (some seconds)) → (Unit → motive none) → motive x", + "typeHash": "937e4345bcea644a15d9a4f2e8cd3f54b2f67921aff90de2f461f52d4b6abdeb", + "valueHash": "cffb696ce853615d87c7e090eac29136ee2212c874ac438a93b9e7f2005e9f99" + }, + { + "name": "WatchPoll.SleepWithinBudget.match_1.eq_1", + "kind": "theorem", + "type": "∀ (motive : Option Nat → Sort u_1) (seconds : Nat) (h_1 : (seconds : Nat) → motive (some seconds)) (h_2 : Unit → motive none), (match some seconds with | some seconds => h_1 seconds | none => h_2 ()) = h_1 seconds", + "typeHash": "459be37cb37a28b3fb2a152a61e1d2207d0142cb131589b0cac265ec8d882af6", + "valueHash": null + }, + { + "name": "WatchPoll.SleepWithinBudget.match_1.eq_2", + "kind": "theorem", + "type": "∀ (motive : Option Nat → Sort u_1) (h_1 : (seconds : Nat) → motive (some seconds)) (h_2 : Unit → motive none), (match none with | some seconds => h_1 seconds | none => h_2 ()) = h_2 ()", + "typeHash": "ed562fb89469f6ad0e4db7ffeca0c994bc20e3acc388b57fe54911e41f0094b2", + "valueHash": null + }, + { + "name": "WatchPoll.SleepWithinBudget.match_1.splitter", + "kind": "definition", + "type": "(motive : Option Nat → Sort u_1) → (x : Option Nat) → ((seconds : Nat) → motive (some seconds)) → (Unit → motive none) → motive x", + "typeHash": "937e4345bcea644a15d9a4f2e8cd3f54b2f67921aff90de2f461f52d4b6abdeb", + "valueHash": "a7f04c49a199983bc6aaaa37b121554ac181627bb4d6fa4d795f6a032851fc06" + }, + { + "name": "WatchPoll.Stalls", + "kind": "definition", + "type": "WatchPoll.Event → WatchPoll.State → Prop", + "typeHash": "f2fe8d1b0b79f37f73a13df2dd8632996e4d814040bbb5c6737ba65cbb6bfd9f", + "valueHash": "5a125479c778297682f56ef2932e55fcff4dcd2c92f50239d4dbd2f979d51073" + }, + { + "name": "WatchPoll.State", + "kind": "inductive", + "type": "Type", + "typeHash": "7051ab654d793c216d744f7920d98d672357a0efa67248ee75d146d668f60997", + "valueHash": null + }, + { + "name": "WatchPoll.State.casesOn", + "kind": "definition", + "type": "{motive : WatchPoll.State → Sort u} → (t : WatchPoll.State) → ((failures : Nat) → (remaining : WatchPoll.Seconds) → (last : WatchPoll.Last) → (retries slept : Nat) → motive { failures := failures, remaining := remaining, last := last, retries := retries, slept := slept }) → motive t", + "typeHash": "31fd09b5cfd05f23a98e4ff1ddd682626ba07651feccf7521fcbc42fe505c93a", + "valueHash": "2a81f67578213a778213d9db932d204c89a7fb716b69ba64d77736714198227e" + }, + { + "name": "WatchPoll.State.ctorIdx", + "kind": "definition", + "type": "WatchPoll.State → Nat", + "typeHash": "e686339d3077761ffc9cef3e37fde42edc131dcca74992c2d8c0c73ac940293a", + "valueHash": "86ff573214ea20c3b0e774834c47052e0f54ee9030391a39de05a50a82aa0763" + }, + { + "name": "WatchPoll.State.failures", + "kind": "definition", + "type": "WatchPoll.State → Nat", + "typeHash": "fb865785b526b1c00c7137588c0b3c987e264802b36a30ef59df9ca640cfb76a", + "valueHash": "25cc3e790fb14cbe004d8429ed02ce328aa87d8a56562fbb50e30981ee97475c" + }, + { + "name": "WatchPoll.State.last", + "kind": "definition", + "type": "WatchPoll.State → WatchPoll.Last", + "typeHash": "701d83fa77ceaab826283883beff00861cb5f258a2ced66d0abf106bc0fefe97", + "valueHash": "c831c7967d101ff59dc4845da9c4d9dae76b4882eef14a44c7d51fa600662faf" + }, + { + "name": "WatchPoll.State.mk", + "kind": "constructor", + "type": "Nat → WatchPoll.Seconds → WatchPoll.Last → Nat → Nat → WatchPoll.State", + "typeHash": "5052941af781f1b9e01f6148f5e79d682c7717997efe271f31e387d1d3e83a7d", + "valueHash": null + }, + { + "name": "WatchPoll.State.mk.inj", + "kind": "theorem", + "type": "∀ {failures : Nat} {remaining : WatchPoll.Seconds} {last : WatchPoll.Last} {retries slept failures_1 : Nat} {remaining_1 : WatchPoll.Seconds} {last_1 : WatchPoll.Last} {retries_1 slept_1 : Nat}, { failures := failures, remaining := remaining, last := last, retries := retries, slept := slept } = { failures := failures_1, remaining := remaining_1, last := last_1, retries := retries_1, slept := slept_1 } → failures = failures_1 ∧ remaining = remaining_1 ∧ last = last_1 ∧ retries = retries_1 ∧ slept = slept_1", + "typeHash": "56df4fc0aa6310fc197a7eaf3a4fdce8fe4a7591f6f3679f01a039736af7ebd3", + "valueHash": null + }, + { + "name": "WatchPoll.State.mk.injEq", + "kind": "theorem", + "type": "∀ (failures : Nat) (remaining : WatchPoll.Seconds) (last : WatchPoll.Last) (retries slept failures_1 : Nat) (remaining_1 : WatchPoll.Seconds) (last_1 : WatchPoll.Last) (retries_1 slept_1 : Nat), ({ failures := failures, remaining := remaining, last := last, retries := retries, slept := slept } = { failures := failures_1, remaining := remaining_1, last := last_1, retries := retries_1, slept := slept_1 }) = (failures = failures_1 ∧ remaining = remaining_1 ∧ last = last_1 ∧ retries = retries_1 ∧ slept = slept_1)", + "typeHash": "913b31fd24001a1a957309340f3bc4b92e5608cf3bd554b148bbecb5f3677c40", + "valueHash": null + }, + { + "name": "WatchPoll.State.mk.noConfusion", + "kind": "definition", + "type": "{P : Sort u} → {failures : Nat} → {remaining : WatchPoll.Seconds} → {last : WatchPoll.Last} → {retries slept failures' : Nat} → {remaining' : WatchPoll.Seconds} → {last' : WatchPoll.Last} → {retries' slept' : Nat} → { failures := failures, remaining := remaining, last := last, retries := retries, slept := slept } = { failures := failures', remaining := remaining', last := last', retries := retries', slept := slept' } → (failures = failures' → remaining = remaining' → last = last' → retries = retries' → slept = slept' → P) → P", + "typeHash": "85021eb3b06338351b38a6d6f3923b5e9d5f7ff21376454d992de27155b9f644", + "valueHash": "f9751e6929b6f90555b5e267197a19e582b5af9332ad29332f14754d02cf8e82" + }, + { + "name": "WatchPoll.State.mk.sizeOf_spec", + "kind": "theorem", + "type": "∀ (failures : Nat) (remaining : WatchPoll.Seconds) (last : WatchPoll.Last) (retries slept : Nat), sizeOf { failures := failures, remaining := remaining, last := last, retries := retries, slept := slept } = 1 + sizeOf failures + sizeOf remaining + sizeOf last + sizeOf retries + sizeOf slept", + "typeHash": "6cdb5740b0348e0a9235020005c67cf3cb02cf8e54c2cbfe1d94e9c169713104", + "valueHash": null + }, + { + "name": "WatchPoll.State.noConfusion", + "kind": "definition", + "type": "{P : Sort u} → {t t' : WatchPoll.State} → t = t' → WatchPoll.State.noConfusionType P t t'", + "typeHash": "977ba4d226c0233845f7ef3829fe03af348eb5b5f8daf46a64fbde18a7981a8e", + "valueHash": "b4f31046d9276e2bbf0f9d8b85b5cc6eadf6fa5fa0feeb4e734079149881fbed" + }, + { + "name": "WatchPoll.State.noConfusionType", + "kind": "definition", + "type": "Sort u → WatchPoll.State → WatchPoll.State → Sort u", + "typeHash": "e88e541d095a466b415aec1436f830ebfecc3894ffe1dfcdf2aef3dbb4d3ebe7", + "valueHash": "c2c0fcf8d844ba2d354f689dce9761df3dc96f473397f8308436375ddaf4244a" + }, + { + "name": "WatchPoll.State.rec", + "kind": "recursor", + "type": "{motive : WatchPoll.State → Sort u} → ((failures : Nat) → (remaining : WatchPoll.Seconds) → (last : WatchPoll.Last) → (retries slept : Nat) → motive { failures := failures, remaining := remaining, last := last, retries := retries, slept := slept }) → (t : WatchPoll.State) → motive t", + "typeHash": "638c076a8c03246a5681c778bf92ae3af0e7aab72fe0a56e16784c60e7fd7b34", + "valueHash": null + }, + { + "name": "WatchPoll.State.recOn", + "kind": "definition", + "type": "{motive : WatchPoll.State → Sort u} → (t : WatchPoll.State) → ((failures : Nat) → (remaining : WatchPoll.Seconds) → (last : WatchPoll.Last) → (retries slept : Nat) → motive { failures := failures, remaining := remaining, last := last, retries := retries, slept := slept }) → motive t", + "typeHash": "31fd09b5cfd05f23a98e4ff1ddd682626ba07651feccf7521fcbc42fe505c93a", + "valueHash": "3305025c9becfa962f46a3819cade327b42d89ca00746121b60ea428ac86f5e9" + }, + { + "name": "WatchPoll.State.remaining", + "kind": "definition", + "type": "WatchPoll.State → WatchPoll.Seconds", + "typeHash": "8a8563c336453381427c5493b0793d2daad8016554579d851508ff1638e6157d", + "valueHash": "d0e9fa062b106c3f51c7ea5e309170b7724e1aea453bded81270a5a9bfa14d81" + }, + { + "name": "WatchPoll.State.retries", + "kind": "definition", + "type": "WatchPoll.State → Nat", + "typeHash": "fb865785b526b1c00c7137588c0b3c987e264802b36a30ef59df9ca640cfb76a", + "valueHash": "e8d67d5406604c438a77b9c36c9d95c099cba5a8415d7b4f1c79e58579a10d9b" + }, + { + "name": "WatchPoll.State.slept", + "kind": "definition", + "type": "WatchPoll.State → Nat", + "typeHash": "fb865785b526b1c00c7137588c0b3c987e264802b36a30ef59df9ca640cfb76a", + "valueHash": "d868553df711d7dc41cfe114b1d3ed7feaf88378107d4012a53e1051bf7f1f7c" + }, + { + "name": "WatchPoll.StepOutcome", + "kind": "inductive", + "type": "Type", + "typeHash": "7051ab654d793c216d744f7920d98d672357a0efa67248ee75d146d668f60997", + "valueHash": null + }, + { + "name": "WatchPoll.StepOutcome.casesOn", + "kind": "definition", + "type": "{motive : WatchPoll.StepOutcome → Sort u} → (t : WatchPoll.StepOutcome) → motive WatchPoll.StepOutcome.terminal → ((seconds : Nat) → motive (WatchPoll.StepOutcome.sleep seconds)) → motive WatchPoll.StepOutcome.continue_ → ((retryable : Bool) → motive (WatchPoll.StepOutcome.queryError retryable)) → motive WatchPoll.StepOutcome.deadlineExceeded → motive WatchPoll.StepOutcome.otherError → motive t", + "typeHash": "c37eaf10ebe0ae1147c1253e7d299341d1bde4fb87c4d0f21b0803cc401d89ce", + "valueHash": "bae96b72aa0e03ae83b1214d5f6084a0ca42b199b75e57f843aaabc0b882caec" + }, + { + "name": "WatchPoll.StepOutcome.continue_", + "kind": "constructor", + "type": "WatchPoll.StepOutcome", + "typeHash": "d7acffb592e54431c7977b89619e6b1389d32a22a4dfaa92302b6476e4ca95b8", + "valueHash": null + }, + { + "name": "WatchPoll.StepOutcome.continue_.elim", + "kind": "definition", + "type": "{motive : WatchPoll.StepOutcome → Sort u} → (t : WatchPoll.StepOutcome) → t.ctorIdx = 2 → motive WatchPoll.StepOutcome.continue_ → motive t", + "typeHash": "3e5e71319336a26d78c10a7221121945e50ea06e1593c44cb8524e4e21e9276f", + "valueHash": "5c8633e4f7aedd00c9b280ec45187c914ba58592af1b51f04231ca8ceb65e2c9" + }, + { + "name": "WatchPoll.StepOutcome.continue_.sizeOf_spec", + "kind": "theorem", + "type": "sizeOf WatchPoll.StepOutcome.continue_ = 1", + "typeHash": "54f94bb6c0d67c3a6e0b84b40d4cfe9a3df15bc7b59d110ea2ef654a36af2d1e", + "valueHash": null + }, + { + "name": "WatchPoll.StepOutcome.ctorElim", + "kind": "definition", + "type": "{motive : WatchPoll.StepOutcome → Sort u} → (ctorIdx : Nat) → (t : WatchPoll.StepOutcome) → ctorIdx = t.ctorIdx → WatchPoll.StepOutcome.ctorElimType ctorIdx → motive t", + "typeHash": "e84e52c7366eb7c8b13ac43ddbbfe3eac87073f5a891deeb9a49f7688a34869a", + "valueHash": "52b0281e2db07131e02d91d711882b39535c18b6110cb0954c178ef7629d4a14" + }, + { + "name": "WatchPoll.StepOutcome.ctorElimType", + "kind": "definition", + "type": "{motive : WatchPoll.StepOutcome → Sort u} → Nat → Sort (max 1 u)", + "typeHash": "b70af6570f6b966d7e11a62bd901ad6a4ea9d0a5224dfdede6a7b036d378ad10", + "valueHash": "ebd07c53cec4acbfb29e137154936fd862480e990e55205d07ab60da49961be1" + }, + { + "name": "WatchPoll.StepOutcome.ctorIdx", + "kind": "definition", + "type": "WatchPoll.StepOutcome → Nat", + "typeHash": "19af1da29d54b3a13b6b3db61b332b9b57d71d36854c1cb65bc290d43a33c1af", + "valueHash": "5bc166f5ecf9fc9c05499ab28402104fd73f1ecf48f7c45e3151673d6dc6588b" + }, + { + "name": "WatchPoll.StepOutcome.deadlineExceeded", + "kind": "constructor", + "type": "WatchPoll.StepOutcome", + "typeHash": "d7acffb592e54431c7977b89619e6b1389d32a22a4dfaa92302b6476e4ca95b8", + "valueHash": null + }, + { + "name": "WatchPoll.StepOutcome.deadlineExceeded.elim", + "kind": "definition", + "type": "{motive : WatchPoll.StepOutcome → Sort u} → (t : WatchPoll.StepOutcome) → t.ctorIdx = 4 → motive WatchPoll.StepOutcome.deadlineExceeded → motive t", + "typeHash": "179845b64babddea935eff58bca79b4f3ee5eb531d68e4ccdd83db26afafb61c", + "valueHash": "a36742b8991e9c576e5db68d84a2207c1b8bfe321977bd04f2baa717a1c93f6c" + }, + { + "name": "WatchPoll.StepOutcome.deadlineExceeded.sizeOf_spec", + "kind": "theorem", + "type": "sizeOf WatchPoll.StepOutcome.deadlineExceeded = 1", + "typeHash": "901cbed333d2d33f51411fa6792683626172b9b6213d75097e9828cdd5319bca", + "valueHash": null + }, + { + "name": "WatchPoll.StepOutcome.noConfusion", + "kind": "definition", + "type": "{P : Sort u} → {t t' : WatchPoll.StepOutcome} → t = t' → WatchPoll.StepOutcome.noConfusionType P t t'", + "typeHash": "0fb77dc03dd53de6cb7b8bbbe4382101f2b73468353ff69c950cc64688ddf5a4", + "valueHash": "2f965f163813518e1989c02cd19964f5a38d50eb6764057bb49a908f04e1b1d5" + }, + { + "name": "WatchPoll.StepOutcome.noConfusionType", + "kind": "definition", + "type": "Sort u → WatchPoll.StepOutcome → WatchPoll.StepOutcome → Sort u", + "typeHash": "d62ba1cc8cb96fe3b1e7a59eb03d40d93fb291366cfb271004843cd94c20c720", + "valueHash": "3e5ccb443f69eb27f3bff79e0f50139cac7d6b9bddc0e740a1c7c90cd2072714" + }, + { + "name": "WatchPoll.StepOutcome.otherError", + "kind": "constructor", + "type": "WatchPoll.StepOutcome", + "typeHash": "d7acffb592e54431c7977b89619e6b1389d32a22a4dfaa92302b6476e4ca95b8", + "valueHash": null + }, + { + "name": "WatchPoll.StepOutcome.otherError.elim", + "kind": "definition", + "type": "{motive : WatchPoll.StepOutcome → Sort u} → (t : WatchPoll.StepOutcome) → t.ctorIdx = 5 → motive WatchPoll.StepOutcome.otherError → motive t", + "typeHash": "343625010cac62ba2fce7752e52c0068b22183ba226b5d804c41546340f8fe9e", + "valueHash": "22411655e1dee7ef2ffbded5555f36c04da0948476771a9d57827ff991d03220" + }, + { + "name": "WatchPoll.StepOutcome.otherError.sizeOf_spec", + "kind": "theorem", + "type": "sizeOf WatchPoll.StepOutcome.otherError = 1", + "typeHash": "3d3a4c26ef369930b7b57531a31b6a21d66c64836a0de43157d2067023dbab59", + "valueHash": null + }, + { + "name": "WatchPoll.StepOutcome.queryError", + "kind": "constructor", + "type": "Bool → WatchPoll.StepOutcome", + "typeHash": "28e1d12b5bf67bd0a687dbc2938246ab2e21f9ee7ebe843e547db5ab1d00c30f", + "valueHash": null + }, + { + "name": "WatchPoll.StepOutcome.queryError.elim", + "kind": "definition", + "type": "{motive : WatchPoll.StepOutcome → Sort u} → (t : WatchPoll.StepOutcome) → t.ctorIdx = 3 → ((retryable : Bool) → motive (WatchPoll.StepOutcome.queryError retryable)) → motive t", + "typeHash": "1f2d48e4212da5a8d919783b5dfbf9050934c8cef8ca605def061345f57b5133", + "valueHash": "18823f9a768e978dc38363442ce7fdca0cc2c973047dde06b5a02e4f308e3f7f" + }, + { + "name": "WatchPoll.StepOutcome.queryError.inj", + "kind": "theorem", + "type": "∀ {retryable retryable_1 : Bool}, WatchPoll.StepOutcome.queryError retryable = WatchPoll.StepOutcome.queryError retryable_1 → retryable = retryable_1", + "typeHash": "166028c42eede4cc1d088ece67e092f8f3003b7339dae132d91833dd65f2138a", + "valueHash": null + }, + { + "name": "WatchPoll.StepOutcome.queryError.injEq", + "kind": "theorem", + "type": "∀ (retryable retryable_1 : Bool), (WatchPoll.StepOutcome.queryError retryable = WatchPoll.StepOutcome.queryError retryable_1) = (retryable = retryable_1)", + "typeHash": "820d23ae665a9c464fa26a824cd9b35580d7a669eba0178e1094c0a28eae76ea", + "valueHash": null + }, + { + "name": "WatchPoll.StepOutcome.queryError.noConfusion", + "kind": "definition", + "type": "{P : Sort u} → {retryable retryable' : Bool} → WatchPoll.StepOutcome.queryError retryable = WatchPoll.StepOutcome.queryError retryable' → (retryable = retryable' → P) → P", + "typeHash": "cdd122683adb02f6d7e2e71ee10cef3b092a125f134e18802b20ce2f50085c50", + "valueHash": "69bf4e1f7a9c855ed94a4ab89aeb547aa5c4d9d080536f8dcad2e64ff20d5925" + }, + { + "name": "WatchPoll.StepOutcome.queryError.sizeOf_spec", + "kind": "theorem", + "type": "∀ (retryable : Bool), sizeOf (WatchPoll.StepOutcome.queryError retryable) = 1 + sizeOf retryable", + "typeHash": "8b4a2f99c2d086ccedad57cb078f90ed113534cd8b84d653324e9f3aa439ad9c", + "valueHash": null + }, + { + "name": "WatchPoll.StepOutcome.rec", + "kind": "recursor", + "type": "{motive : WatchPoll.StepOutcome → Sort u} → motive WatchPoll.StepOutcome.terminal → ((seconds : Nat) → motive (WatchPoll.StepOutcome.sleep seconds)) → motive WatchPoll.StepOutcome.continue_ → ((retryable : Bool) → motive (WatchPoll.StepOutcome.queryError retryable)) → motive WatchPoll.StepOutcome.deadlineExceeded → motive WatchPoll.StepOutcome.otherError → (t : WatchPoll.StepOutcome) → motive t", + "typeHash": "8a5651b0e077516519bc9d87f11df40e0145f9ed143a6bec79f63a22fa467245", + "valueHash": null + }, + { + "name": "WatchPoll.StepOutcome.recOn", + "kind": "definition", + "type": "{motive : WatchPoll.StepOutcome → Sort u} → (t : WatchPoll.StepOutcome) → motive WatchPoll.StepOutcome.terminal → ((seconds : Nat) → motive (WatchPoll.StepOutcome.sleep seconds)) → motive WatchPoll.StepOutcome.continue_ → ((retryable : Bool) → motive (WatchPoll.StepOutcome.queryError retryable)) → motive WatchPoll.StepOutcome.deadlineExceeded → motive WatchPoll.StepOutcome.otherError → motive t", + "typeHash": "c37eaf10ebe0ae1147c1253e7d299341d1bde4fb87c4d0f21b0803cc401d89ce", + "valueHash": "514845f0f54aa2253079fb594298a5a39b3f8805579466114f795907ae9ef7b4" + }, + { + "name": "WatchPoll.StepOutcome.sleep", + "kind": "constructor", + "type": "Nat → WatchPoll.StepOutcome", + "typeHash": "fd91854da07e1ef1f12c2ccf456c86527a4afeee6765e7a49fb3883d50773a7e", + "valueHash": null + }, + { + "name": "WatchPoll.StepOutcome.sleep.elim", + "kind": "definition", + "type": "{motive : WatchPoll.StepOutcome → Sort u} → (t : WatchPoll.StepOutcome) → t.ctorIdx = 1 → ((seconds : Nat) → motive (WatchPoll.StepOutcome.sleep seconds)) → motive t", + "typeHash": "610071f1d9ca26a8f5b5c1a22504767317725e13f0982b562a5539ea4227356f", + "valueHash": "0bf6cc13f449016ab0ca1022384d598e2e2b9999ee78271c93f16469de7ed2df" + }, + { + "name": "WatchPoll.StepOutcome.sleep.inj", + "kind": "theorem", + "type": "∀ {seconds seconds_1 : Nat}, WatchPoll.StepOutcome.sleep seconds = WatchPoll.StepOutcome.sleep seconds_1 → seconds = seconds_1", + "typeHash": "a3c1c726358a09a92bfb626673c8aee5515ded9ec5b3294baaba9ddc0ceb6ad1", + "valueHash": null + }, + { + "name": "WatchPoll.StepOutcome.sleep.injEq", + "kind": "theorem", + "type": "∀ (seconds seconds_1 : Nat), (WatchPoll.StepOutcome.sleep seconds = WatchPoll.StepOutcome.sleep seconds_1) = (seconds = seconds_1)", + "typeHash": "0ad7a94e3ee09f7fdb484e7d0c256e571c7c79a8e2ea247e5b512f2e2de80795", + "valueHash": null + }, + { + "name": "WatchPoll.StepOutcome.sleep.noConfusion", + "kind": "definition", + "type": "{P : Sort u} → {seconds seconds' : Nat} → WatchPoll.StepOutcome.sleep seconds = WatchPoll.StepOutcome.sleep seconds' → (seconds = seconds' → P) → P", + "typeHash": "7a1f9673e4d67335ae6a46c6f6098292c2cab70ad4e3099b5217547164662ecd", + "valueHash": "07bc4200d3b32ff3080ae668019ae8392477d2fa58d38823ef75b35b109f17c4" + }, + { + "name": "WatchPoll.StepOutcome.sleep.sizeOf_spec", + "kind": "theorem", + "type": "∀ (seconds : Nat), sizeOf (WatchPoll.StepOutcome.sleep seconds) = 1 + sizeOf seconds", + "typeHash": "8ef5b6755ca921eab74007e7d861251c2cb61264701738960e1966c8377838c6", + "valueHash": null + }, + { + "name": "WatchPoll.StepOutcome.succeeded", + "kind": "definition", + "type": "WatchPoll.StepOutcome → Bool", + "typeHash": "fddd083e982f5949643330747c7febf9c81871ac291a5667c3ae94f1e4837d28", + "valueHash": "14fe51b90c6b4e3afd55534f128e3893de5cecf246cfd8a48accf8e8ff44e54a" + }, + { + "name": "WatchPoll.StepOutcome.succeeded.match_1", + "kind": "definition", + "type": "(motive : WatchPoll.StepOutcome → Sort u_1) → (x : WatchPoll.StepOutcome) → (Unit → motive WatchPoll.StepOutcome.terminal) → ((seconds : Nat) → motive (WatchPoll.StepOutcome.sleep seconds)) → (Unit → motive WatchPoll.StepOutcome.continue_) → ((x : WatchPoll.StepOutcome) → motive x) → motive x", + "typeHash": "111f1a7e37c71056d35fe8f64716862756ca9c98b58949be6f3c5b5f95263615", + "valueHash": "11b0e05a8703c69a6b0f5b8ca3a36b274fd86049620bc1499e82f0186c4fb6f1" + }, + { + "name": "WatchPoll.StepOutcome.terminal", + "kind": "constructor", + "type": "WatchPoll.StepOutcome", + "typeHash": "d7acffb592e54431c7977b89619e6b1389d32a22a4dfaa92302b6476e4ca95b8", + "valueHash": null + }, + { + "name": "WatchPoll.StepOutcome.terminal.elim", + "kind": "definition", + "type": "{motive : WatchPoll.StepOutcome → Sort u} → (t : WatchPoll.StepOutcome) → t.ctorIdx = 0 → motive WatchPoll.StepOutcome.terminal → motive t", + "typeHash": "c45260a6ebf42c6bac7ed0af11c322fcbcaa035651521c8cf176b6f02e9e8512", + "valueHash": "9dfbd5efbcc92c6d1d741bdbba11f71d669d7909367d8fdb7bcbcaff9a034790" + }, + { + "name": "WatchPoll.StepOutcome.terminal.sizeOf_spec", + "kind": "theorem", + "type": "sizeOf WatchPoll.StepOutcome.terminal = 1", + "typeHash": "6213a25c9b0c37464ac64e5445681bc21febe9c9f4133dc6623e2c2cd50ccd86", + "valueHash": null + }, + { + "name": "WatchPoll.Transition", + "kind": "inductive", + "type": "Type", + "typeHash": "7051ab654d793c216d744f7920d98d672357a0efa67248ee75d146d668f60997", + "valueHash": null + }, + { + "name": "WatchPoll.Transition.after", + "kind": "definition", + "type": "WatchPoll.Transition → WatchPoll.State", + "typeHash": "5f89684429c49309443ae98df121ec810b74d7499ff1b8d8d6884d7c82ccaedb", + "valueHash": "44d64ad4aa985c3298069b2b1d957eff2a5a9542ff7e1c5119fd1c97f6461ee9" + }, + { + "name": "WatchPoll.Transition.before", + "kind": "definition", + "type": "WatchPoll.Transition → WatchPoll.State", + "typeHash": "5f89684429c49309443ae98df121ec810b74d7499ff1b8d8d6884d7c82ccaedb", + "valueHash": "61caccfa2bbc8619459590f7f56989e06342910916525f2e0ac35e6631963343" + }, + { + "name": "WatchPoll.Transition.casesOn", + "kind": "definition", + "type": "{motive : WatchPoll.Transition → Sort u} → (t : WatchPoll.Transition) → ((options : WatchPoll.PollingOptions) → (start : WatchPoll.Seconds) → (before : WatchPoll.State) → (event : WatchPoll.Event) → (after : WatchPoll.State) → motive { options := options, start := start, before := before, event := event, after := after }) → motive t", + "typeHash": "db8405809be802065d1c3671060cdc31695743dd7cf174c21d3534836b9fea59", + "valueHash": "2ee5ec2e79172371c0ca3498e575644efbb903aac27f7ee4891431b27530e7c6" + }, + { + "name": "WatchPoll.Transition.ctorIdx", + "kind": "definition", + "type": "WatchPoll.Transition → Nat", + "typeHash": "59d30f9966ee6ee63bdda1efcbcaeffaa8f7d184110efe1b4fdd8d8c3f68b666", + "valueHash": "051b955a3f8ee8eb98cb8dc3980e14963c357b9270505063bb014a0e3319c07d" + }, + { + "name": "WatchPoll.Transition.event", + "kind": "definition", + "type": "WatchPoll.Transition → WatchPoll.Event", + "typeHash": "1d154d1d96c06d85365e6d87e91c2e76598d076fa33265380f89e784eb66e615", + "valueHash": "eac95a8936c0464614662d1d1ca3a49ebc23b4944593abfb9a75c841d0109829" + }, + { + "name": "WatchPoll.Transition.mk", + "kind": "constructor", + "type": "WatchPoll.PollingOptions → WatchPoll.Seconds → WatchPoll.State → WatchPoll.Event → WatchPoll.State → WatchPoll.Transition", + "typeHash": "5b16fa8e2b769d2478b00313d363ae7687f474eaf8adda552bb376cbbae040f4", + "valueHash": null + }, + { + "name": "WatchPoll.Transition.mk.inj", + "kind": "theorem", + "type": "∀ {options : WatchPoll.PollingOptions} {start : WatchPoll.Seconds} {before : WatchPoll.State} {event : WatchPoll.Event} {after : WatchPoll.State} {options_1 : WatchPoll.PollingOptions} {start_1 : WatchPoll.Seconds} {before_1 : WatchPoll.State} {event_1 : WatchPoll.Event} {after_1 : WatchPoll.State}, { options := options, start := start, before := before, event := event, after := after } = { options := options_1, start := start_1, before := before_1, event := event_1, after := after_1 } → options = options_1 ∧ start = start_1 ∧ before = before_1 ∧ event = event_1 ∧ after = after_1", + "typeHash": "2a99a3184e45b4d215e293ce41f237c63c9c18918722b7d676ec7e41a034001c", + "valueHash": null + }, + { + "name": "WatchPoll.Transition.mk.injEq", + "kind": "theorem", + "type": "∀ (options : WatchPoll.PollingOptions) (start : WatchPoll.Seconds) (before : WatchPoll.State) (event : WatchPoll.Event) (after : WatchPoll.State) (options_1 : WatchPoll.PollingOptions) (start_1 : WatchPoll.Seconds) (before_1 : WatchPoll.State) (event_1 : WatchPoll.Event) (after_1 : WatchPoll.State), ({ options := options, start := start, before := before, event := event, after := after } = { options := options_1, start := start_1, before := before_1, event := event_1, after := after_1 }) = (options = options_1 ∧ start = start_1 ∧ before = before_1 ∧ event = event_1 ∧ after = after_1)", + "typeHash": "1dccd6b83a12b799299a402cd13cb99d2f7e76e32c634afdf9a5430b72285cf8", + "valueHash": null + }, + { + "name": "WatchPoll.Transition.mk.noConfusion", + "kind": "definition", + "type": "{P : Sort u} → {options : WatchPoll.PollingOptions} → {start : WatchPoll.Seconds} → {before : WatchPoll.State} → {event : WatchPoll.Event} → {after : WatchPoll.State} → {options' : WatchPoll.PollingOptions} → {start' : WatchPoll.Seconds} → {before' : WatchPoll.State} → {event' : WatchPoll.Event} → {after' : WatchPoll.State} → { options := options, start := start, before := before, event := event, after := after } = { options := options', start := start', before := before', event := event', after := after' } → (options = options' → start = start' → before = before' → event = event' → after = after' → P) → P", + "typeHash": "0cb0d2ec0e69899df3d2d428c727413d5420efb5b39ad1721f22c05692c98b32", + "valueHash": "94282cd63ab0eb45f466376089cb5b7f82ebd669771ee0ff753cf89c0bc5f37d" + }, + { + "name": "WatchPoll.Transition.mk.sizeOf_spec", + "kind": "theorem", + "type": "∀ (options : WatchPoll.PollingOptions) (start : WatchPoll.Seconds) (before : WatchPoll.State) (event : WatchPoll.Event) (after : WatchPoll.State), sizeOf { options := options, start := start, before := before, event := event, after := after } = 1 + sizeOf options + sizeOf start + sizeOf before + sizeOf event + sizeOf after", + "typeHash": "9197641cc9055a7cc21eb4b720b921b32cf6948b26f5855a9d0f432dcd728043", + "valueHash": null + }, + { + "name": "WatchPoll.Transition.noConfusion", + "kind": "definition", + "type": "{P : Sort u} → {t t' : WatchPoll.Transition} → t = t' → WatchPoll.Transition.noConfusionType P t t'", + "typeHash": "40331328ffb96ac269451809d16fdda0f564c8c597d2c8a477bbdffd13875452", + "valueHash": "8fe9a763db61e21709fbaa661c8e15956bd3e0420171e6d354ef3ac4520d9066" + }, + { + "name": "WatchPoll.Transition.noConfusionType", + "kind": "definition", + "type": "Sort u → WatchPoll.Transition → WatchPoll.Transition → Sort u", + "typeHash": "ee5c1ab8f2a134883657b0980f4473514cf909f1c4e7c68bffcc4ba3c6501f6e", + "valueHash": "99607774d8bc394b7a6b77f1aad0248e7905b1df7e085b367df66352e9de102a" + }, + { + "name": "WatchPoll.Transition.options", + "kind": "definition", + "type": "WatchPoll.Transition → WatchPoll.PollingOptions", + "typeHash": "5e876f43d34ba39c7aba3298fbcbcd5b6fdcc8b1f2b4a84f2ae72fbb889089ef", + "valueHash": "1e1eb5f8f80a75fc3b298306f30e2978cf52aa30bad4b2207f23d83dd1f85970" + }, + { + "name": "WatchPoll.Transition.rec", + "kind": "recursor", + "type": "{motive : WatchPoll.Transition → Sort u} → ((options : WatchPoll.PollingOptions) → (start : WatchPoll.Seconds) → (before : WatchPoll.State) → (event : WatchPoll.Event) → (after : WatchPoll.State) → motive { options := options, start := start, before := before, event := event, after := after }) → (t : WatchPoll.Transition) → motive t", + "typeHash": "fffb4ddaa11741540061190e550fe5f381c0194295d67eaf757f556f1d35177f", + "valueHash": null + }, + { + "name": "WatchPoll.Transition.recOn", + "kind": "definition", + "type": "{motive : WatchPoll.Transition → Sort u} → (t : WatchPoll.Transition) → ((options : WatchPoll.PollingOptions) → (start : WatchPoll.Seconds) → (before : WatchPoll.State) → (event : WatchPoll.Event) → (after : WatchPoll.State) → motive { options := options, start := start, before := before, event := event, after := after }) → motive t", + "typeHash": "db8405809be802065d1c3671060cdc31695743dd7cf174c21d3534836b9fea59", + "valueHash": "ff1b2f51ebd981aa90efac51b8d10e41e183b4cb8925a182ca5a44a4ed3caace" + }, + { + "name": "WatchPoll.Transition.start", + "kind": "definition", + "type": "WatchPoll.Transition → WatchPoll.Seconds", + "typeHash": "542377b62fb7884e34c369952642bd32efaccd753e61bd4d24b545b50347abf5", + "valueHash": "1f4b5443955b59141304a3d3578bc0ca1b335b5ff9e2c393fee5759de17f7f9c" + }, + { + "name": "WatchPoll.Transition.timeout", + "kind": "definition", + "type": "WatchPoll.Transition → Nat", + "typeHash": "2c50c9340c2d4dfc4ccdea7aeb8854fca6c9b1352574232fe3412f42977f0949", + "valueHash": "84ac70169b92bf46f1c98568485dc397a3c3adfa0fa3403e2d8621be3affe393" + }, + { + "name": "WatchPoll.WatchDeadline", + "kind": "inductive", + "type": "Type", + "typeHash": "7051ab654d793c216d744f7920d98d672357a0efa67248ee75d146d668f60997", + "valueHash": null + }, + { + "name": "WatchPoll.WatchDeadline.casesOn", + "kind": "definition", + "type": "{motive : WatchPoll.WatchDeadline → Sort u} → (t : WatchPoll.WatchDeadline) → ((expiresAt : WatchPoll.Seconds) → motive { expiresAt := expiresAt }) → motive t", + "typeHash": "bbe12b6143258bdb3740ddf5558b6ace0d58c84ee4de9d537b360bd5b6e468ea", + "valueHash": "c66b6edd85a5a87ca8f3f3aea5e255d6cab58f290c7530594b257aea388a134a" + }, + { + "name": "WatchPoll.WatchDeadline.create", + "kind": "definition", + "type": "Nat → Nat → WatchPoll.WatchDeadline", + "typeHash": "ef9fa035e4e980b0ea2e67a315f6310280e3fd9590590a0d99b98a9b3fedcbc9", + "valueHash": "aa2066507604867fe2e41e855f149d1b27dd65abeb839460d4816de64a40b4d8" + }, + { + "name": "WatchPoll.WatchDeadline.ctorIdx", + "kind": "definition", + "type": "WatchPoll.WatchDeadline → Nat", + "typeHash": "3729c325b068c41848c3f843a60bd732fdef890dbb6160840e541105dc236942", + "valueHash": "c21e0cd183992e9a40db29749e81aeaba6175a72a5e04af52500ff6e9eae0b27" + }, + { + "name": "WatchPoll.WatchDeadline.expiresAt", + "kind": "definition", + "type": "WatchPoll.WatchDeadline → WatchPoll.Seconds", + "typeHash": "cefd7337f7b029f20a0a5ff66555173a50f5997462f34cfcc0bdb39e39a3d14b", + "valueHash": "544c5223bac579de9af6599a4d326d67cc34646cf0381f6c2aab96f6b777e9e8" + }, + { + "name": "WatchPoll.WatchDeadline.mk", + "kind": "constructor", + "type": "WatchPoll.Seconds → WatchPoll.WatchDeadline", + "typeHash": "9703eae3d12b4b2ceecbac729be5af0309563e0e863e112987924bb9e3ca5bda", + "valueHash": null + }, + { + "name": "WatchPoll.WatchDeadline.mk.inj", + "kind": "theorem", + "type": "∀ {expiresAt expiresAt_1 : WatchPoll.Seconds}, { expiresAt := expiresAt } = { expiresAt := expiresAt_1 } → expiresAt = expiresAt_1", + "typeHash": "719fbeb354717a0627ee9ec2a701116fd8cc86e5833e7fc7750ef325a93c0c0f", + "valueHash": null + }, + { + "name": "WatchPoll.WatchDeadline.mk.injEq", + "kind": "theorem", + "type": "∀ (expiresAt expiresAt_1 : WatchPoll.Seconds), ({ expiresAt := expiresAt } = { expiresAt := expiresAt_1 }) = (expiresAt = expiresAt_1)", + "typeHash": "f4093570920bbec1ff6c6657c83ef6ee98c81519dc42f7142570dfe510464a59", + "valueHash": null + }, + { + "name": "WatchPoll.WatchDeadline.mk.noConfusion", + "kind": "definition", + "type": "{P : Sort u} → {expiresAt expiresAt' : WatchPoll.Seconds} → { expiresAt := expiresAt } = { expiresAt := expiresAt' } → (expiresAt = expiresAt' → P) → P", + "typeHash": "01844067ff4d2c7e3c036ae6455045db1d71ed70b369edfa425a24ade3c4e06d", + "valueHash": "e92e3470d5acf826e7e2fe4e8a1661e134df7ae0eaf025015941c8437fd76866" + }, + { + "name": "WatchPoll.WatchDeadline.mk.sizeOf_spec", + "kind": "theorem", + "type": "∀ (expiresAt : WatchPoll.Seconds), sizeOf { expiresAt := expiresAt } = 1 + sizeOf expiresAt", + "typeHash": "4910ba1788310b98ff9e161eaba87e172d4b8b7d22e17e98a556bd55838dfbdf", + "valueHash": null + }, + { + "name": "WatchPoll.WatchDeadline.noConfusion", + "kind": "definition", + "type": "{P : Sort u} → {t t' : WatchPoll.WatchDeadline} → t = t' → WatchPoll.WatchDeadline.noConfusionType P t t'", + "typeHash": "24da8534837d90abc4645fceb67774237ec1d6b3b3423609ff3c345bcba0856d", + "valueHash": "87ad17bf961e36acf7116f06bdef9b50de112c3ffb1830a6b5bbcd1431ac36a3" + }, + { + "name": "WatchPoll.WatchDeadline.noConfusionType", + "kind": "definition", + "type": "Sort u → WatchPoll.WatchDeadline → WatchPoll.WatchDeadline → Sort u", + "typeHash": "0d8a638c444e0a28d7ec722bebd0b00f2e28e2c326dbc2b908d1d62ee3d8c4a6", + "valueHash": "5da5ebbe76d8154a2ec26fe18bac88fe604e7e123881c9866e7d55a0097838c7" + }, + { + "name": "WatchPoll.WatchDeadline.rec", + "kind": "recursor", + "type": "{motive : WatchPoll.WatchDeadline → Sort u} → ((expiresAt : WatchPoll.Seconds) → motive { expiresAt := expiresAt }) → (t : WatchPoll.WatchDeadline) → motive t", + "typeHash": "bd03ac181ee15bcc898c4215a7290bf75e017ff83962ec1f79be6a97cddefd28", + "valueHash": null + }, + { + "name": "WatchPoll.WatchDeadline.recOn", + "kind": "definition", + "type": "{motive : WatchPoll.WatchDeadline → Sort u} → (t : WatchPoll.WatchDeadline) → ((expiresAt : WatchPoll.Seconds) → motive { expiresAt := expiresAt }) → motive t", + "typeHash": "bbe12b6143258bdb3740ddf5558b6ace0d58c84ee4de9d537b360bd5b6e468ea", + "valueHash": "7df2b7f29af8a6398deb5148d74476d29d857fd70f1b2c4487faf37c9369cb7e" + }, + { + "name": "WatchPoll.WatchDeadline.remaining", + "kind": "definition", + "type": "WatchPoll.WatchDeadline → Nat → WatchPoll.Seconds", + "typeHash": "1a5365bcf05e7a8bc02d162897bf38a38bda344d7df16ac9fe48894f9903b99f", + "valueHash": "c671ea6ee90a407f44f11fc3fff9eb0fde4e2c117e351c80b7ecd5f0370e3177" + }, + { + "name": "WatchPoll.WithinTimeout", + "kind": "definition", + "type": "Nat → WatchPoll.State → Prop", + "typeHash": "7d781158993a7bb099c72710e5a3c30ed337a057a6dc7c9120bf62c97f7a8cc5", + "valueHash": "2136af33bf556a064f341c4407e8360eaa1a1639e7b3ad9024c9de3df838d073" + }, + { + "name": "WatchPoll.backoffFailures", + "kind": "definition", + "type": "List Nat", + "typeHash": "80b301ef6c9d59c7d03cad6e3c50ce9efd0b17f8713597f3eed7c178ee58ff1a", + "valueHash": "3a3724f43a22467a6a2deae1ebbc3fd45533aae20c818aed17f892afb04840b2" + }, + { + "name": "WatchPoll.backoffIntervals", + "kind": "definition", + "type": "List Nat", + "typeHash": "80b301ef6c9d59c7d03cad6e3c50ce9efd0b17f8713597f3eed7c178ee58ff1a", + "valueHash": "baa3d698e3ea7431c846ca2f909d7b838c11b88737510a2438ec3d9b76247f77" + }, + { + "name": "WatchPoll.backoffSpec", + "kind": "theorem", + "type": "∀ (interval failures : Nat), 1 ≤ failures → WatchPoll.BackoffSpec interval failures", + "typeHash": "5c99f3706aff25f009698022a23de26f5deb8f2c77f6c529967a576c904ce186", + "valueHash": null + }, + { + "name": "WatchPoll.backoffVectors", + "kind": "definition", + "type": "List (Nat × Nat)", + "typeHash": "f36ec9eb1a6a03a4acca1f7bda790d433ce7445fb9242f15bbbbd83596b845d6", + "valueHash": "dccc53c8f54487c65b0c11fda697a71db90e555d8d7645c8f8cf0092e697148b" + }, + { + "name": "WatchPoll.badBackoff", + "kind": "definition", + "type": "List (Nat × Nat)", + "typeHash": "f36ec9eb1a6a03a4acca1f7bda790d433ce7445fb9242f15bbbbd83596b845d6", + "valueHash": "7c1abec4b591adb33ad2bbbce26ddf40f4bb852c7cf6d5bd3789ca9f0e74e891" + }, + { + "name": "WatchPoll.badBackoff.match_1", + "kind": "definition", + "type": "(motive : Nat × Nat → Sort u_1) → (x : Nat × Nat) → ((interval failures : Nat) → motive (interval, failures)) → motive x", + "typeHash": "e115785e52bac2501b452d95eaf78acc89e6ca079e55f85f9651b4684e05019c", + "valueHash": "a400c9b821ccbf21adbc7ab27cb3651c4c3f8c93082e9f323faac110c5444933" + }, + { + "name": "WatchPoll.badConfirms", + "kind": "definition", + "type": "List (String × List WatchPoll.Call × WatchPoll.Call)", + "typeHash": "7d85403b2e235e9eaab2b5c66c3d291e02adee097f291e9701eca035fdb52f22", + "valueHash": "6ca0b4043d332fbade7b45fab358dfa42f9ec52407a9a5f0670f586c62808a0a" + }, + { + "name": "WatchPoll.badConfirms.match_1", + "kind": "definition", + "type": "(motive : String × (List WatchPoll.Call → WatchPoll.Call → Bool) → Sort u_1) → (x : String × (List WatchPoll.Call → WatchPoll.Call → Bool)) → ((fst : String) → (p : List WatchPoll.Call → WatchPoll.Call → Bool) → motive (fst, p)) → motive x", + "typeHash": "9d49519410fff88c573d3806e53a6b6a39cc5b6fd06b634da52fa33758cd3df0", + "valueHash": "4a2e385fadf32ed93fbaba5e46bca939826fa723347b07441106bee97443c4b8" + }, + { + "name": "WatchPoll.badConfirms.match_3", + "kind": "definition", + "type": "(motive : List WatchPoll.Call × WatchPoll.Call → Sort u_1) → (x : List WatchPoll.Call × WatchPoll.Call) → ((past : List WatchPoll.Call) → (c : WatchPoll.Call) → motive (past, c)) → motive x", + "typeHash": "bdae02eb7e5b70eb0aaf5ee39a13a1a29cdbcf575edc691816560aafa9e97903", + "valueHash": "ffdeec75c556aa12dbb52506d9e03217b175bec1b5e1347917f2b7a34183724d" + }, + { + "name": "WatchPoll.badSteps", + "kind": "definition", + "type": "List (String × WatchPoll.Transition)", + "typeHash": "08b6a4e81417648ab668ffcdd5c361210a0d3e4aa6935a380e8f9df793bcbbc2", + "valueHash": "03027ad161d4b190a250247065865378b7eb687568ca60f68419178a70e03ab7" + }, + { + "name": "WatchPoll.badSteps.match_1", + "kind": "definition", + "type": "(motive : String × (WatchPoll.Transition → Bool) → Sort u_1) → (x : String × (WatchPoll.Transition → Bool)) → ((fst : String) → (p : WatchPoll.Transition → Bool) → motive (fst, p)) → motive x", + "typeHash": "f5511c7dc434f25e1da25293bd6439aa0bda87657fbf5d707c4068cf6a01eb15", + "valueHash": "c04828ce8ad2cca1ac4a7465fbf849a52afbc00b37eb04b34fed2d26198049fd" + }, + { + "name": "WatchPoll.boundedBudgets", + "kind": "definition", + "type": "List WatchPoll.Seconds", + "typeHash": "63f99823149c95f496e692d1160fc63d528c8b205583befd4313c743e1acd4ce", + "valueHash": "44a5383a694a1dee0227270c92c62fbdd3582069281a533d1c3be13974d21500" + }, + { + "name": "WatchPoll.boundedCalls", + "kind": "definition", + "type": "List WatchPoll.Call", + "typeHash": "0ff773ada97755cb188d0da36849d6cc273d8f29425a77a3ee8fb6db7b2dff15", + "valueHash": "07a0e700da39c4c1f0e044278566c8a2671f3a6373ad7d01b100177a2fb52596" + }, + { + "name": "WatchPoll.boundedEvents", + "kind": "definition", + "type": "WatchPoll.PollingOptions → List WatchPoll.Event", + "typeHash": "c5bd0ed03bd6770d91f74166ee14ee492f9df9d982696dfa062c4b303fdb4f60", + "valueHash": "a2fd945a99ba3c634393e654c9ff5a8067eebf489d02b2352657217f5e3948a1" + }, + { + "name": "WatchPoll.boundedOptions", + "kind": "definition", + "type": "List WatchPoll.PollingOptions", + "typeHash": "9e9c8e3f84119f2a4a8040e84092d1dc67209fa6aeab35aca2633d95b41f0fed", + "valueHash": "05670476ffa8dbc62f554d57da97412cf1087fd8384f3a088abf6d775654801b" + }, + { + "name": "WatchPoll.confirm", + "kind": "definition", + "type": "WatchPoll.FirstSeen → Nat → Nat → Nat → WatchPoll.FirstSeen × Bool", + "typeHash": "db8ec329f5ae74bca6e69dca55f1fa547e45111a240e2cd19e7035297ce9dedf", + "valueHash": "1a7e721f5e55af123d2d9bd567e6b91626a872a256407a48f6282d3ee15c9e31" + }, + { + "name": "WatchPoll.confirm.match_1", + "kind": "definition", + "type": "(motive : Option WatchPoll.Sighting → Sort u_1) → (x : Option WatchPoll.Sighting) → ((prior : WatchPoll.Sighting) → motive (some prior)) → (Unit → motive none) → motive x", + "typeHash": "0d0147d7054ef229ca744607a70997ab2dffc770df8e9bde8aa80c76e252a4c2", + "valueHash": "46794cede32f83675da4cc96377150e407d1ced8f81571e0548808e3d6b4d98f" + }, + { + "name": "WatchPoll.confirmAfter", + "kind": "definition", + "type": "List WatchPoll.Call → WatchPoll.Call → Bool", + "typeHash": "adabd3caed81ce05b49fad14590241b28ae2ebb5d291c0b14e59c37eb41bb6c5", + "valueHash": "cbcc6686d2c5ef4de7635d10e976f239a5baa040b3169942611797f3be3eab03" + }, + { + "name": "WatchPoll.confirmAfter_spec", + "kind": "theorem", + "type": "∀ (past : List WatchPoll.Call) (c : WatchPoll.Call), WatchPoll.ConfirmSpec past c", + "typeHash": "88f0f19fd023700bccfab03ce00105349bef232fa4c8ea3c9edd31c64ffc360e", + "valueHash": null + }, + { + "name": "WatchPoll.confirmAll", + "kind": "definition", + "type": "List WatchPoll.Call → List Bool", + "typeHash": "8f7dcbb36d80698567e1ff75a6a2ffc5b162652292edd221d66f28bf74db7290", + "valueHash": "6273329aa88250ea0626bc56341bfad523d2bcb647eb36eba72ed78c2cbf3e38" + }, + { + "name": "WatchPoll.confirmCases", + "kind": "definition", + "type": "List (List WatchPoll.Call × WatchPoll.Call)", + "typeHash": "03c65282f86bb28369bf2fac70f67727d87534b0e188212f6486ce8b89e80921", + "valueHash": "664c221cb77b7a0c63c965095c59432b5678e96a2633721b50bd660286b4eef4" + }, + { + "name": "WatchPoll.confirmProperties", + "kind": "definition", + "type": "List (String × (List WatchPoll.Call → WatchPoll.Call → Bool))", + "typeHash": "2ac1fd337b5b3bfc8a2119abd3c8ece261d4fb2d1d4d6a361f15158e28663e57", + "valueHash": "3cfd842022de589ed182eba56e00c236960d13a0d2c691e535427e542a90c67b" + }, + { + "name": "WatchPoll.confirmTargets", + "kind": "definition", + "type": "List (String × (List WatchPoll.Call → WatchPoll.Call → Bool))", + "typeHash": "2ac1fd337b5b3bfc8a2119abd3c8ece261d4fb2d1d4d6a361f15158e28663e57", + "valueHash": "1774776dc5345daf83cc67492934b0b2815fd9fdeb1bb57c3fa6383c2a61f1d9" + }, + { + "name": "WatchPoll.confirmVectors", + "kind": "definition", + "type": "List (List WatchPoll.Call)", + "typeHash": "c29bb3252e2c8a645d3e1b87fbb5569f06e710f67ab18dbce020fe05a578b465", + "valueHash": "a2f63f349d7831933ea25cdbecc04b310a39128e9fa0f9ff19613a957b4806f2" + }, + { + "name": "WatchPoll.confirm_aba_restarts", + "kind": "theorem", + "type": "∀ (older : List WatchPoll.Call) (pr a b t1 t2 now : Nat), a ≠ b → WatchPoll.confirmAfter ({ now := t2, pr := pr, headRefOid := a } :: { now := t1, pr := pr, headRefOid := b } :: older) { now := now, pr := pr, headRefOid := a } = decide (t2 + 60 ≤ now)", + "typeHash": "7b1a579c16ae02b0a4699ad67e3f39ee223b57b83cdee4376b7162b7d6f08b48", + "valueHash": null + }, + { + "name": "WatchPoll.confirm_clock_must_advance", + "kind": "theorem", + "type": "∀ (past : List WatchPoll.Call) (c : WatchPoll.Call), WatchPoll.ClockMustAdvance past c", + "typeHash": "3f253763407f05aa1cbcac3392424a4696c3553d930ebfa311f16c112c5474a8", + "valueHash": null + }, + { + "name": "WatchPoll.confirm_first_sighting_false", + "kind": "theorem", + "type": "∀ (past : List WatchPoll.Call) (c : WatchPoll.Call), WatchPoll.FirstSightingFalse past c", + "typeHash": "25caf64553371f7bb50c6ac6f0d4d5a405a0810924f7e05af736b3cc17a117b2", + "valueHash": null + }, + { + "name": "WatchPoll.confirm_head_change_false", + "kind": "theorem", + "type": "∀ (past : List WatchPoll.Call) (c : WatchPoll.Call), WatchPoll.HeadChangeFalse past c", + "typeHash": "6bf2098b0c607162fc45de1831fdedab3a26a234dd948c4fa9b036c596b629ac", + "valueHash": null + }, + { + "name": "WatchPoll.confirm_new_head_resets", + "kind": "theorem", + "type": "∀ (firstSeen : WatchPoll.FirstSeen) (now pr headRefOid : Nat), (∀ (firstSeenAt : Nat), firstSeen pr ≠ some { headRefOid := headRefOid, seenAt := firstSeenAt }) → (WatchPoll.confirm firstSeen now pr headRefOid).snd = false ∧ (WatchPoll.confirm firstSeen now pr headRefOid).fst pr = some { headRefOid := headRefOid, seenAt := now }", + "typeHash": "9eb73b3d99ef2db21d712faf050d8b7e112975c7785e3c4aa2f412fd8bca5768", + "valueHash": null + }, + { + "name": "WatchPoll.confirm_other_pr_untouched", + "kind": "theorem", + "type": "∀ (firstSeen : WatchPoll.FirstSeen) (now pr headRefOid other : Nat), other ≠ pr → (WatchPoll.confirm firstSeen now pr headRefOid).fst other = firstSeen other", + "typeHash": "3a42caec0f5a1b33251b9e0b07921b4cd2e0d52ca10c84f30c2eb3fbf7531139", + "valueHash": null + }, + { + "name": "WatchPoll.confirm_same_head_keeps_first_seen", + "kind": "theorem", + "type": "∀ (firstSeen : WatchPoll.FirstSeen) (now pr headRefOid firstSeenAt : Nat), firstSeen pr = some { headRefOid := headRefOid, seenAt := firstSeenAt } → (WatchPoll.confirm firstSeen now pr headRefOid).fst = firstSeen", + "typeHash": "002fb503e7083bcec5a220b98995ddda99de1ef4f1edbeed3d6f60f338807780", + "valueHash": null + }, + { + "name": "WatchPoll.confirm_sound", + "kind": "theorem", + "type": "∀ (past : List WatchPoll.Call) (c : WatchPoll.Call), WatchPoll.ConfirmSound past c", + "typeHash": "29058cc9713653234e6762fddb423675e26aaab112cd202da1c23bb92ba3ba4e", + "valueHash": null + }, + { + "name": "WatchPoll.confirm_true_iff", + "kind": "theorem", + "type": "∀ (firstSeen : WatchPoll.FirstSeen) (now pr headRefOid : Nat), (WatchPoll.confirm firstSeen now pr headRefOid).snd = true ↔ ∃ firstSeenAt, firstSeen pr = some { headRefOid := headRefOid, seenAt := firstSeenAt } ∧ firstSeenAt + 60 ≤ now", + "typeHash": "0c144785176748d5fd3b87be930dc6175d0cd4533ae2b7c19bad78427c6c4850", + "valueHash": null + }, + { + "name": "WatchPoll.continue_without_clock_repeats", + "kind": "theorem", + "type": "∀ (o : WatchPoll.PollingOptions) (s : WatchPoll.State) (sleepElapsed : Nat), s.last = WatchPoll.Last.continued → s.remaining.positive = true → s.failures = 0 → s.retries = 0 → WatchPoll.pollStep o s { outcome := WatchPoll.StepOutcome.continue_, stepElapsed := 0, sleepElapsed := sleepElapsed } = some s", + "typeHash": "cc429765a8ebb5533feb86ed8a08fdf48dc5dc2375c5e85ff22fbd2e80b3d86b", + "valueHash": null + }, + { + "name": "WatchPoll.firstSeenAfter", + "kind": "definition", + "type": "List WatchPoll.Call → WatchPoll.FirstSeen", + "typeHash": "dc310c2d950d771c81ca17346c7a778b481577d0d102b3491f270e6de6abb2b0", + "valueHash": "879a788e36a6831ae060a88e20d5944f767da751c589c65a06aa1a9bd862bbaa" + }, + { + "name": "WatchPoll.firstSeenAfter.match_1", + "kind": "definition", + "type": "(motive : List WatchPoll.Call → Sort u_1) → (x : List WatchPoll.Call) → (Unit → motive []) → ((c : WatchPoll.Call) → (older : List WatchPoll.Call) → motive (c :: older)) → motive x", + "typeHash": "878d24c3a88507f5063bf4180d2cfc5d14ebd2db8f13c11188217e0ba20476ed", + "valueHash": "f8303eea7858d1b8a07e7a8b011946deb8a5e34ec9a60189cdcd9d4977e931a7" + }, + { + "name": "WatchPoll.firstSeenAfter_spec", + "kind": "theorem", + "type": "∀ (past : List WatchPoll.Call) (pr headRefOid firstSeenAt : Nat), WatchPoll.firstSeenAfter past pr = some { headRefOid := headRefOid, seenAt := firstSeenAt } ↔ WatchPoll.runStart pr headRefOid past = some firstSeenAt", + "typeHash": "f9c5b3f09712d26ac9c2963dd0ffb050dbeea1c2b04dc6f0065ade4c97614aab", + "valueHash": null + }, + { + "name": "WatchPoll.firstSeenSet", + "kind": "definition", + "type": "WatchPoll.FirstSeen → Nat → WatchPoll.Sighting → WatchPoll.FirstSeen", + "typeHash": "1bacd697602e24817f862eb45bf97573d591a234f6f033211f9107203cf73e5c", + "valueHash": "c6e2a489508061206dc7231e0daddfb67752e42572bfd6b0a18c59dd4408450b" + }, + { + "name": "WatchPoll.frontier", + "kind": "definition", + "type": "WatchPoll.PollingOptions → WatchPoll.Seconds → Nat → List WatchPoll.State", + "typeHash": "f243d716a1060f41b7c7a7d2423f9217fd13fd7ce69e18d553e02b386c79e666", + "valueHash": "b3828d545aabd4173627d755fa95a2fc6f6e03f5cebc8a2592ea991d88c5cb86" + }, + { + "name": "WatchPoll.frontier.match_1", + "kind": "definition", + "type": "(motive : Nat → Sort u_1) → (x : Nat) → (Unit → motive 0) → ((depth : Nat) → motive depth.succ) → motive x", + "typeHash": "04f0d092f6ae5801bb88e69a95d609947162ea69e6ad35a930a4020b9bc3cebc", + "valueHash": "4d669dbec0cdae014a42200b0fc9b8f411ef600432cdccb8be171dda38311272" + }, + { + "name": "WatchPoll.headJson", + "kind": "definition", + "type": "Nat → String", + "typeHash": "faacb663ba282bfb7b22ced9bc6cbd1350916362e5fd756dde0ab7dc6eb2621e", + "valueHash": "30f8460974cbf5d25d6359bd4d3647eae3a3e1bc77473af6c33fe2d199a63cad" + }, + { + "name": "WatchPoll.histories", + "kind": "definition", + "type": "Nat → List (List WatchPoll.Call)", + "typeHash": "50f2023f2b62cd0dcbec9eaccb782c4112c8630c4e1bb7e136afd8901e9ad16b", + "valueHash": "d44c2f6af36a8127ed63464f79fb19769344f3084c427aff87afd412726e79ec" + }, + { + "name": "WatchPoll.init", + "kind": "definition", + "type": "WatchPoll.Seconds → WatchPoll.State", + "typeHash": "d824f35ce51a4352f26c8f62b7cf562243ba661301b540312419c3fef4a28c1d", + "valueHash": "e07aa37480f7f6652faa83a00d5e64769a9536b6a6ee02cedf8c9f249515e004" + }, + { + "name": "WatchPoll.instDecidableBackoffSpec", + "kind": "definition", + "type": "(interval failures : Nat) → Decidable (WatchPoll.BackoffSpec interval failures)", + "typeHash": "67361222c70dd7e9bbf79829ceec09f3386e4d8fd36da8177651759dbefd1ecc", + "valueHash": "6d0336821c73ceb2d6815952b7fd53c88d9be5bf78f92fb537783317353c3ae5" + }, + { + "name": "WatchPoll.instDecidableClockMustAdvance", + "kind": "definition", + "type": "(past : List WatchPoll.Call) → (c : WatchPoll.Call) → Decidable (WatchPoll.ClockMustAdvance past c)", + "typeHash": "11eb93ae036245706215753a8a0265e09ce18658071a4a1dda5c65f580c0889f", + "valueHash": "24bfbb55aae5ab19eb82090857452542fe0ad3f6aa46ac539901f4d50851fc01" + }, + { + "name": "WatchPoll.instDecidableConfirmSound", + "kind": "definition", + "type": "(past : List WatchPoll.Call) → (c : WatchPoll.Call) → Decidable (WatchPoll.ConfirmSound past c)", + "typeHash": "d58c503f409580264b6c19e425d1e8ae95a94529c3ad2f7f584cb062cfd14e7f", + "valueHash": "2a21e7933596612e21050acf2ec9f5dfe3163965e099209602d5c600bbf1f7f3" + }, + { + "name": "WatchPoll.instDecidableConfirmSpec", + "kind": "definition", + "type": "(past : List WatchPoll.Call) → (c : WatchPoll.Call) → Decidable (WatchPoll.ConfirmSpec past c)", + "typeHash": "b29d98b4f37b39306d650956521f6e9e9d158bfe543a858997220163e8ef506b", + "valueHash": "d8ad2d3506b87dde8adf11dff56981baf0aad6d5e7f97f5e88fa013fcff54fba" + }, + { + "name": "WatchPoll.instDecidableCovers", + "kind": "definition", + "type": "(remaining : WatchPoll.Seconds) → (seconds : Nat) → Decidable (remaining.covers seconds)", + "typeHash": "30c4a2aac5dec418e35c6c62cd4f46a3ed7d9752dae8e9a2c26682aac95a0ece", + "valueHash": "0c8efebf8de4d102109c53aebc0904369a4748bf633201ad200c37889f44cc70" + }, + { + "name": "WatchPoll.instDecidableCovers.match_4", + "kind": "definition", + "type": "(motive : WatchPoll.Seconds → Nat → Sort u_1) → (x : WatchPoll.Seconds) → (x_1 : Nat) → ((r seconds : Nat) → motive (WatchPoll.Seconds.finite r) seconds) → ((x : Nat) → motive WatchPoll.Seconds.infinity x) → motive x x_1", + "typeHash": "605eec39aced62c1f994481d3c9c3ea5f34c59bc634b62da9eb60ddf9abaac11", + "valueHash": "90065490e2fa2fd6fb8a07c8d889b8d1c94c5d3aa9aa64012f1ec3d99f10ae63" + }, + { + "name": "WatchPoll.instDecidableEqCall", + "kind": "definition", + "type": "DecidableEq WatchPoll.Call", + "typeHash": "e96db09358f33ffd7f904b6c6d1d5f8328ea5f568e6f45cd1893a58cc26fc4dd", + "valueHash": "80bd1778ad9c323026806e15b2e93051d0a11e1d671212f3900a200e30b4ae59" + }, + { + "name": "WatchPoll.instDecidableEqCall.decEq", + "kind": "definition", + "type": "(x x_1 : WatchPoll.Call) → Decidable (x = x_1)", + "typeHash": "1856153c2be08dd13c9ee2e453fa09b2082831522875ee09793d37631db2b054", + "valueHash": "6b1c9f1ad2f19fe07473bd3a9145a56b9fc38fbd56c542137b461dc458fead21" + }, + { + "name": "WatchPoll.instDecidableEqCall.decEq.match_1", + "kind": "definition", + "type": "(motive : WatchPoll.Call → WatchPoll.Call → Sort u_1) → (x x_1 : WatchPoll.Call) → ((a a_1 a_2 b b_1 b_2 : Nat) → motive { now := a, pr := a_1, headRefOid := a_2 } { now := b, pr := b_1, headRefOid := b_2 }) → motive x x_1", + "typeHash": "f46afd402399ad4af61997417ffb546e5dae801a9ac30aee818677dd65a1df7d", + "valueHash": "e29649a0e85ee163cbfb5714aad70d4c26ff91ea748f0a7534fcfc71d685d330" + }, + { + "name": "WatchPoll.instDecidableEqEvent", + "kind": "definition", + "type": "DecidableEq WatchPoll.Event", + "typeHash": "c3ae976d5da1c686e50f6b06bf7e9f8eaf75e439d8d5fadce9ca962092e7342e", + "valueHash": "3a1ab179bbc1abb12ab6360ece3c0e75f192159f0c096887f50b9c7304061514" + }, + { + "name": "WatchPoll.instDecidableEqEvent.decEq", + "kind": "definition", + "type": "(x x_1 : WatchPoll.Event) → Decidable (x = x_1)", + "typeHash": "be2f3a4b4d83acf637f6fb54322806160c016059261539d61a6f0bdd5673c802", + "valueHash": "dd3c1b36c60c2fbd48bf4bd36db68729ae5af7940eee2c8d4ec17a2a2fbc6c94" + }, + { + "name": "WatchPoll.instDecidableEqEvent.decEq.match_1", + "kind": "definition", + "type": "(motive : WatchPoll.Event → WatchPoll.Event → Sort u_1) → (x x_1 : WatchPoll.Event) → ((a : WatchPoll.StepOutcome) → (a_1 a_2 : Nat) → (b : WatchPoll.StepOutcome) → (b_1 b_2 : Nat) → motive { outcome := a, stepElapsed := a_1, sleepElapsed := a_2 } { outcome := b, stepElapsed := b_1, sleepElapsed := b_2 }) → motive x x_1", + "typeHash": "1a118aef25dce53eba0fcca02c09c6ece1f3c98d502940650c74d3eae8a4d86b", + "valueHash": "2cc2f3e35a8f3f32976a8c9d864eafbf6f411bc598da1931c3c3566d904c690d" + }, + { + "name": "WatchPoll.instDecidableEqLast", + "kind": "definition", + "type": "DecidableEq WatchPoll.Last", + "typeHash": "58f9bf95f07dafbd9861cf1cdd8872aa3eecf7b29bc5ca71ca1dcc2e3b71f48f", + "valueHash": "713180fbcb31a2a79fc35783e782d4704eaa483ec584517d6cfddb8df415c9f6" + }, + { + "name": "WatchPoll.instDecidableEqLast.decEq", + "kind": "definition", + "type": "(x x_1 : WatchPoll.Last) → Decidable (x = x_1)", + "typeHash": "3c79a69bb68e07c591edf89f39e73e3c07ec7c474ed51d4f69bd0f784e390725", + "valueHash": "8c4a4c09254001eee18e07b14ac0db842985c1ca1d4dd937cca6869795cf0827" + }, + { + "name": "WatchPoll.instDecidableEqLast.decEq.match_1", + "kind": "definition", + "type": "(motive : WatchPoll.Last → WatchPoll.Last → Sort u_1) → (x x_1 : WatchPoll.Last) → (Unit → motive WatchPoll.Last.start WatchPoll.Last.start) → ((seconds : Nat) → motive WatchPoll.Last.start (WatchPoll.Last.sleptPoll seconds)) → ((seconds : Nat) → motive WatchPoll.Last.start (WatchPoll.Last.sleptRetry seconds)) → (Unit → motive WatchPoll.Last.start WatchPoll.Last.continued) → (Unit → motive WatchPoll.Last.start WatchPoll.Last.returnedVerdict) → ((failures : Nat) → motive WatchPoll.Last.start (WatchPoll.Last.returnedStatusQuery failures)) → (Unit → motive WatchPoll.Last.start WatchPoll.Last.returnedOnDeadline) → (Unit → motive WatchPoll.Last.start WatchPoll.Last.threw) → ((seconds : Nat) → motive (WatchPoll.Last.sleptPoll seconds) WatchPoll.Last.start) → ((a b : Nat) → motive (WatchPoll.Last.sleptPoll a) (WatchPoll.Last.sleptPoll b)) → ((seconds seconds_1 : Nat) → motive (WatchPoll.Last.sleptPoll seconds) (WatchPoll.Last.sleptRetry seconds_1)) → ((seconds : Nat) → motive (WatchPoll.Last.sleptPoll seconds) WatchPoll.Last.continued) → ((seconds : Nat) → motive (WatchPoll.Last.sleptPoll seconds) WatchPoll.Last.returnedVerdict) → ((seconds failures : Nat) → motive (WatchPoll.Last.sleptPoll seconds) (WatchPoll.Last.returnedStatusQuery failures)) → ((seconds : Nat) → motive (WatchPoll.Last.sleptPoll seconds) WatchPoll.Last.returnedOnDeadline) → ((seconds : Nat) → motive (WatchPoll.Last.sleptPoll seconds) WatchPoll.Last.threw) → ((seconds : Nat) → motive (WatchPoll.Last.sleptRetry seconds) WatchPoll.Last.start) → ((seconds seconds_1 : Nat) → motive (WatchPoll.Last.sleptRetry seconds) (WatchPoll.Last.sleptPoll seconds_1)) → ((a b : Nat) → motive (WatchPoll.Last.sleptRetry a) (WatchPoll.Last.sleptRetry b)) → ((seconds : Nat) → motive (WatchPoll.Last.sleptRetry seconds) WatchPoll.Last.continued) → ((seconds : Nat) → motive (WatchPoll.Last.sleptRetry seconds) WatchPoll.Last.returnedVerdict) → ((seconds failures : Nat) → motive (WatchPoll.Last.sleptRetry seconds) (WatchPoll.Last.returnedStatusQuery failures)) → ((seconds : Nat) → motive (WatchPoll.Last.sleptRetry seconds) WatchPoll.Last.returnedOnDeadline) → ((seconds : Nat) → motive (WatchPoll.Last.sleptRetry seconds) WatchPoll.Last.threw) → (Unit → motive WatchPoll.Last.continued WatchPoll.Last.start) → ((seconds : Nat) → motive WatchPoll.Last.continued (WatchPoll.Last.sleptPoll seconds)) → ((seconds : Nat) → motive WatchPoll.Last.continued (WatchPoll.Last.sleptRetry seconds)) → (Unit → motive WatchPoll.Last.continued WatchPoll.Last.continued) → (Unit → motive WatchPoll.Last.continued WatchPoll.Last.returnedVerdict) → ((failures : Nat) → motive WatchPoll.Last.continued (WatchPoll.Last.returnedStatusQuery failures)) → (Unit → motive WatchPoll.Last.continued WatchPoll.Last.returnedOnDeadline) → (Unit → motive WatchPoll.Last.continued WatchPoll.Last.threw) → (Unit → motive WatchPoll.Last.returnedVerdict WatchPoll.Last.start) → ((seconds : Nat) → motive WatchPoll.Last.returnedVerdict (WatchPoll.Last.sleptPoll seconds)) → ((seconds : Nat) → motive WatchPoll.Last.returnedVerdict (WatchPoll.Last.sleptRetry seconds)) → (Unit → motive WatchPoll.Last.returnedVerdict WatchPoll.Last.continued) → (Unit → motive WatchPoll.Last.returnedVerdict WatchPoll.Last.returnedVerdict) → ((failures : Nat) → motive WatchPoll.Last.returnedVerdict (WatchPoll.Last.returnedStatusQuery failures)) → (Unit → motive WatchPoll.Last.returnedVerdict WatchPoll.Last.returnedOnDeadline) → (Unit → motive WatchPoll.Last.returnedVerdict WatchPoll.Last.threw) → ((failures : Nat) → motive (WatchPoll.Last.returnedStatusQuery failures) WatchPoll.Last.start) → ((failures seconds : Nat) → motive (WatchPoll.Last.returnedStatusQuery failures) (WatchPoll.Last.sleptPoll seconds)) → ((failures seconds : Nat) → motive (WatchPoll.Last.returnedStatusQuery failures) (WatchPoll.Last.sleptRetry seconds)) → ((failures : Nat) → motive (WatchPoll.Last.returnedStatusQuery failures) WatchPoll.Last.continued) → ((failures : Nat) → motive (WatchPoll.Last.returnedStatusQuery failures) WatchPoll.Last.returnedVerdict) → ((a b : Nat) → motive (WatchPoll.Last.returnedStatusQuery a) (WatchPoll.Last.returnedStatusQuery b)) → ((failures : Nat) → motive (WatchPoll.Last.returnedStatusQuery failures) WatchPoll.Last.returnedOnDeadline) → ((failures : Nat) → motive (WatchPoll.Last.returnedStatusQuery failures) WatchPoll.Last.threw) → (Unit → motive WatchPoll.Last.returnedOnDeadline WatchPoll.Last.start) → ⋯", + "typeHash": "217a4970721a5d72e5215b0f967dd2f3ce2f2fe3bc921b95821cae28410b5b80", + "valueHash": "2f741f5fb3d91f6f90cfff123f5cdee54d95968900dd4dd63257c6b9d7dc1815" + }, + { + "name": "WatchPoll.instDecidableEqPollingOptions", + "kind": "definition", + "type": "DecidableEq WatchPoll.PollingOptions", + "typeHash": "1c665bd743d2be4c655c639c98ac6dc85dff44e9ab8f46d96d499bc1e1074674", + "valueHash": "cf8c7af1791efa859962ec080215593bfeeb23c3b9e3a588d838a87117f4990c" + }, + { + "name": "WatchPoll.instDecidableEqPollingOptions.decEq", + "kind": "definition", + "type": "(x x_1 : WatchPoll.PollingOptions) → Decidable (x = x_1)", + "typeHash": "0a500fa4592cae43922cd447c88813178219e850b7547d95ed4d9a4ef74154f7", + "valueHash": "7a0216b24c37b8ac2a0e3f916434d76e8e85fd85aa7976a498025b0bbaa392f9" + }, + { + "name": "WatchPoll.instDecidableEqPollingOptions.decEq.match_1", + "kind": "definition", + "type": "(motive : WatchPoll.PollingOptions → WatchPoll.PollingOptions → Sort u_1) → (x x_1 : WatchPoll.PollingOptions) → ((a a_1 b b_1 : Nat) → motive { interval := a, maxQueryErrors := a_1 } { interval := b, maxQueryErrors := b_1 }) → motive x x_1", + "typeHash": "92b26f28918dd71a095c14522d152219ffa5d0867fd6e9153b3a45dabc7c297d", + "valueHash": "a2d1badc82706d70e425ca7c967af7bf0d3b4ce56317f189a3e5f448a6e3f112" + }, + { + "name": "WatchPoll.instDecidableEqSeconds", + "kind": "definition", + "type": "DecidableEq WatchPoll.Seconds", + "typeHash": "19d59640c7a0bce550a6d86f0798a3022f6ce640aacca3950ee4593d5db6f1b0", + "valueHash": "3f6c7b2772a1729acb1423e0d8d5d979f2dbc4bb86e0281dd1768ba7e8c6a852" + }, + { + "name": "WatchPoll.instDecidableEqSeconds.decEq", + "kind": "definition", + "type": "(x x_1 : WatchPoll.Seconds) → Decidable (x = x_1)", + "typeHash": "c6ccc94dd42afee932caaf511fcfa5cad359f81d69436d134238ce8703289e45", + "valueHash": "780d1be94d58d142be05b3e79203b8e8ef0ecb3bf9c3f095780237ba1e8f2d67" + }, + { + "name": "WatchPoll.instDecidableEqSeconds.decEq.match_1", + "kind": "definition", + "type": "(motive : WatchPoll.Seconds → WatchPoll.Seconds → Sort u_1) → (x x_1 : WatchPoll.Seconds) → ((a b : Nat) → motive (WatchPoll.Seconds.finite a) (WatchPoll.Seconds.finite b)) → ((seconds : Nat) → motive (WatchPoll.Seconds.finite seconds) WatchPoll.Seconds.infinity) → ((seconds : Nat) → motive WatchPoll.Seconds.infinity (WatchPoll.Seconds.finite seconds)) → (Unit → motive WatchPoll.Seconds.infinity WatchPoll.Seconds.infinity) → motive x x_1", + "typeHash": "9e8126710e76bc59b9a752236f767bbe7fc17a32e436e13d71d12bd3956bcab4", + "valueHash": "9622610069e24ac30ed798d02fbd554ce9a3d92ef285c44b7d36227f7c51cae9" + }, + { + "name": "WatchPoll.instDecidableEqSighting", + "kind": "definition", + "type": "DecidableEq WatchPoll.Sighting", + "typeHash": "f0b6aa7bb6aaaae1dfe41f6099a4eb9154d8898bdb01db74664c62c40d85393b", + "valueHash": "b59bf3731a55f4aefaa1df45a166158d64e82e79ea815fc2296f5fa91a268a2d" + }, + { + "name": "WatchPoll.instDecidableEqSighting.decEq", + "kind": "definition", + "type": "(x x_1 : WatchPoll.Sighting) → Decidable (x = x_1)", + "typeHash": "da7bafc040ee92c4417a7f213a61edb22528743757ae902f745a07af052f27df", + "valueHash": "f295f1a0e712503c5f5e4e6581fcfc514b79ca2b23c1307246088929e0f512ec" + }, + { + "name": "WatchPoll.instDecidableEqSighting.decEq.match_1", + "kind": "definition", + "type": "(motive : WatchPoll.Sighting → WatchPoll.Sighting → Sort u_1) → (x x_1 : WatchPoll.Sighting) → ((a a_1 b b_1 : Nat) → motive { headRefOid := a, seenAt := a_1 } { headRefOid := b, seenAt := b_1 }) → motive x x_1", + "typeHash": "9b693ea81c8322b4b9557acc24f20918c6724d20ee8e71e8bbfdd775640f3bc9", + "valueHash": "67fbba5f105ced8ce51f854824c347c70eebbb9ee546d37442f072df3afaa448" + }, + { + "name": "WatchPoll.instDecidableEqState", + "kind": "definition", + "type": "DecidableEq WatchPoll.State", + "typeHash": "4773ee7f53f9404583abb9fa932bd0b4e859cea416797c7585f334825d450509", + "valueHash": "fc109430a6e0fa6a5ddd0ade54398afc335ea8b7bd4454cf6556eae6f431f58c" + }, + { + "name": "WatchPoll.instDecidableEqState.decEq", + "kind": "definition", + "type": "(x x_1 : WatchPoll.State) → Decidable (x = x_1)", + "typeHash": "b97382511bd496375e662b5e9dd19f768023146728d02b3fc42f3f14b1f6d4d9", + "valueHash": "13e307688b060ef7a1f7b9aaa2727f9c8541f9117a01d855726a8d221600f1dc" + }, + { + "name": "WatchPoll.instDecidableEqState.decEq.match_1", + "kind": "definition", + "type": "(motive : WatchPoll.State → WatchPoll.State → Sort u_1) → (x x_1 : WatchPoll.State) → ((a : Nat) → (a_1 : WatchPoll.Seconds) → (a_2 : WatchPoll.Last) → (a_3 a_4 b : Nat) → (b_1 : WatchPoll.Seconds) → (b_2 : WatchPoll.Last) → (b_3 b_4 : Nat) → motive { failures := a, remaining := a_1, last := a_2, retries := a_3, slept := a_4 } { failures := b, remaining := b_1, last := b_2, retries := b_3, slept := b_4 }) → motive x x_1", + "typeHash": "3dee918d953f936d22b32507f5c811ce200ebd722c500c00a2b424737484743b", + "valueHash": "0ec1141f3dfb3be73a81b06746e36619ce859f468e993265a1f5fc794e19af17" + }, + { + "name": "WatchPoll.instDecidableEqStepOutcome", + "kind": "definition", + "type": "DecidableEq WatchPoll.StepOutcome", + "typeHash": "212ca80448d38c22368f7cced9e1f8e10a01a81d33d83eb2e3a22e42bf8c08e3", + "valueHash": "8261a48373ae507f97cba4b69db83603b4070fc5a133d5e0dbd2a0d436c034dc" + }, + { + "name": "WatchPoll.instDecidableEqStepOutcome.decEq", + "kind": "definition", + "type": "(x x_1 : WatchPoll.StepOutcome) → Decidable (x = x_1)", + "typeHash": "938bff0a454a200ae945a14d90bcf72ee005cff8b465ae6c271850db65e07295", + "valueHash": "7c61d4e7f874b18975abea1bbf738ef2c5cf82e54d887649a7854ac411a25e10" + }, + { + "name": "WatchPoll.instDecidableEqStepOutcome.decEq.match_1", + "kind": "definition", + "type": "(motive : WatchPoll.StepOutcome → WatchPoll.StepOutcome → Sort u_1) → (x x_1 : WatchPoll.StepOutcome) → (Unit → motive WatchPoll.StepOutcome.terminal WatchPoll.StepOutcome.terminal) → ((seconds : Nat) → motive WatchPoll.StepOutcome.terminal (WatchPoll.StepOutcome.sleep seconds)) → (Unit → motive WatchPoll.StepOutcome.terminal WatchPoll.StepOutcome.continue_) → ((retryable : Bool) → motive WatchPoll.StepOutcome.terminal (WatchPoll.StepOutcome.queryError retryable)) → (Unit → motive WatchPoll.StepOutcome.terminal WatchPoll.StepOutcome.deadlineExceeded) → (Unit → motive WatchPoll.StepOutcome.terminal WatchPoll.StepOutcome.otherError) → ((seconds : Nat) → motive (WatchPoll.StepOutcome.sleep seconds) WatchPoll.StepOutcome.terminal) → ((a b : Nat) → motive (WatchPoll.StepOutcome.sleep a) (WatchPoll.StepOutcome.sleep b)) → ((seconds : Nat) → motive (WatchPoll.StepOutcome.sleep seconds) WatchPoll.StepOutcome.continue_) → ((seconds : Nat) → (retryable : Bool) → motive (WatchPoll.StepOutcome.sleep seconds) (WatchPoll.StepOutcome.queryError retryable)) → ((seconds : Nat) → motive (WatchPoll.StepOutcome.sleep seconds) WatchPoll.StepOutcome.deadlineExceeded) → ((seconds : Nat) → motive (WatchPoll.StepOutcome.sleep seconds) WatchPoll.StepOutcome.otherError) → (Unit → motive WatchPoll.StepOutcome.continue_ WatchPoll.StepOutcome.terminal) → ((seconds : Nat) → motive WatchPoll.StepOutcome.continue_ (WatchPoll.StepOutcome.sleep seconds)) → (Unit → motive WatchPoll.StepOutcome.continue_ WatchPoll.StepOutcome.continue_) → ((retryable : Bool) → motive WatchPoll.StepOutcome.continue_ (WatchPoll.StepOutcome.queryError retryable)) → (Unit → motive WatchPoll.StepOutcome.continue_ WatchPoll.StepOutcome.deadlineExceeded) → (Unit → motive WatchPoll.StepOutcome.continue_ WatchPoll.StepOutcome.otherError) → ((retryable : Bool) → motive (WatchPoll.StepOutcome.queryError retryable) WatchPoll.StepOutcome.terminal) → ((retryable : Bool) → (seconds : Nat) → motive (WatchPoll.StepOutcome.queryError retryable) (WatchPoll.StepOutcome.sleep seconds)) → ((retryable : Bool) → motive (WatchPoll.StepOutcome.queryError retryable) WatchPoll.StepOutcome.continue_) → ((a b : Bool) → motive (WatchPoll.StepOutcome.queryError a) (WatchPoll.StepOutcome.queryError b)) → ((retryable : Bool) → motive (WatchPoll.StepOutcome.queryError retryable) WatchPoll.StepOutcome.deadlineExceeded) → ((retryable : Bool) → motive (WatchPoll.StepOutcome.queryError retryable) WatchPoll.StepOutcome.otherError) → (Unit → motive WatchPoll.StepOutcome.deadlineExceeded WatchPoll.StepOutcome.terminal) → ((seconds : Nat) → motive WatchPoll.StepOutcome.deadlineExceeded (WatchPoll.StepOutcome.sleep seconds)) → (Unit → motive WatchPoll.StepOutcome.deadlineExceeded WatchPoll.StepOutcome.continue_) → ((retryable : Bool) → motive WatchPoll.StepOutcome.deadlineExceeded (WatchPoll.StepOutcome.queryError retryable)) → (Unit → motive WatchPoll.StepOutcome.deadlineExceeded WatchPoll.StepOutcome.deadlineExceeded) → (Unit → motive WatchPoll.StepOutcome.deadlineExceeded WatchPoll.StepOutcome.otherError) → (Unit → motive WatchPoll.StepOutcome.otherError WatchPoll.StepOutcome.terminal) → ((seconds : Nat) → motive WatchPoll.StepOutcome.otherError (WatchPoll.StepOutcome.sleep seconds)) → (Unit → motive WatchPoll.StepOutcome.otherError WatchPoll.StepOutcome.continue_) → ((retryable : Bool) → motive WatchPoll.StepOutcome.otherError (WatchPoll.StepOutcome.queryError retryable)) → (Unit → motive WatchPoll.StepOutcome.otherError WatchPoll.StepOutcome.deadlineExceeded) → (Unit → motive WatchPoll.StepOutcome.otherError WatchPoll.StepOutcome.otherError) → motive x x_1", + "typeHash": "6cdcd6736a6e804ca26c0153fb8045a8d25fcda24a693a68d0a12c80407c04ee", + "valueHash": "6d7fa25a883ba909f9ccd29435eb1fd452bb61987a494c194b7ec00cce2919dc" + }, + { + "name": "WatchPoll.instDecidableEqWatchDeadline", + "kind": "definition", + "type": "DecidableEq WatchPoll.WatchDeadline", + "typeHash": "54d9195f75aa5637d02c432e16ffbba6e3e3182993cd8babb3bcdfa04ca21e27", + "valueHash": "f3060104d0dca23f8385602c9ef9e59081d8abeaa4ce04dd9ed55770216c3918" + }, + { + "name": "WatchPoll.instDecidableEqWatchDeadline.decEq", + "kind": "definition", + "type": "(x x_1 : WatchPoll.WatchDeadline) → Decidable (x = x_1)", + "typeHash": "04302b920f2701f4d10da3623f710124f570e4dc39ad4325ce75c87bb89121b4", + "valueHash": "dc9f4cda998b0d64d23b481fb62c02eea518f3edec767e3efe73931cba2e64e8" + }, + { + "name": "WatchPoll.instDecidableEqWatchDeadline.decEq.match_1", + "kind": "definition", + "type": "(motive : WatchPoll.WatchDeadline → WatchPoll.WatchDeadline → Sort u_1) → (x x_1 : WatchPoll.WatchDeadline) → ((a b : WatchPoll.Seconds) → motive { expiresAt := a } { expiresAt := b }) → motive x x_1", + "typeHash": "f71a0cb291c31f580284f0251c4cc27ab0ecf4e644e62d4e744185c1cf6527f4", + "valueHash": "fcad1a5539d4217f1ed69e286da413434c78a72fbdf993864b22fccf1ce7fc58" + }, + { + "name": "WatchPoll.instDecidableFirstSightingFalse", + "kind": "definition", + "type": "(past : List WatchPoll.Call) → (c : WatchPoll.Call) → Decidable (WatchPoll.FirstSightingFalse past c)", + "typeHash": "27994754486b339273dbafe1463720699f9c4141f6ee69a02a36e7ee68906671", + "valueHash": "74d5896027191d63b8dd307ad14e9637badd4df53d8b3fd620436cb14d086ed5" + }, + { + "name": "WatchPoll.instDecidableHeadChangeFalse", + "kind": "definition", + "type": "(past : List WatchPoll.Call) → (c : WatchPoll.Call) → Decidable (WatchPoll.HeadChangeFalse past c)", + "typeHash": "4707e2719b42b0a818df7d0fa982e8ddded0435c1b57bf3b499ea07593deeb84", + "valueHash": "0f064b814a079e18c8aedc44b48923e5a0f8505a8b80866e939c2b1a9f013a89" + }, + { + "name": "WatchPoll.instDecidableHonestClock", + "kind": "definition", + "type": "(e : WatchPoll.Event) → (t : WatchPoll.State) → Decidable (WatchPoll.HonestClock e t)", + "typeHash": "ac46e39dbaead496b59bff59920dc93b3fee1ccce68cabf5b7775cb7ea8cb7ff", + "valueHash": "f0704e2fc1261dcb8f7ffe2eb27fe2b751078548f6eec7aecb29cf6719fa2cd5" + }, + { + "name": "WatchPoll.instDecidableInvariant", + "kind": "definition", + "type": "(o : WatchPoll.PollingOptions) → (s : WatchPoll.State) → Decidable (WatchPoll.Invariant o s)", + "typeHash": "75aae2f6e1706b3f68579a751623b5b04cfdccf8364bea6a2d2e2442aa974008", + "valueHash": "235d74c10c28f51cc17cc3fc5e7218f5cb66d3743e43aa20a34157bfc785dd18" + }, + { + "name": "WatchPoll.instDecidableResetsFailures", + "kind": "definition", + "type": "(s : WatchPoll.State) → (e : WatchPoll.Event) → (t : WatchPoll.State) → Decidable (WatchPoll.ResetsFailures s e t)", + "typeHash": "c46a502fa793dea1835d25533320d384e1c33d20939bf6df9d63640dca4bc944", + "valueHash": "091c99f6afd5728915e1eb7ae1200f35d74c5bc6b31d8a7c529fd00a23741451" + }, + { + "name": "WatchPoll.instDecidableRetriesBelowMax", + "kind": "definition", + "type": "(o : WatchPoll.PollingOptions) → (s : WatchPoll.State) → Decidable (WatchPoll.RetriesBelowMax o s)", + "typeHash": "cf25f846852370ab010666a531a78eb65214f7395238d1ffb6fa0db9169b5f86", + "valueHash": "af22452ebe28eb3b9475b224b614e54edad9024fd06c1599f57380ec1f44700d" + }, + { + "name": "WatchPoll.instDecidableRetryUsesBackoff", + "kind": "definition", + "type": "(o : WatchPoll.PollingOptions) → (s : WatchPoll.State) → (e : WatchPoll.Event) → (t : WatchPoll.State) → Decidable (WatchPoll.RetryUsesBackoff o s e t)", + "typeHash": "50b60ca1702a1bd44f13efbcb8e050324596182cf99d7a9e137336dde3ea6bec", + "valueHash": "670a094422b8ca2c0099fe0a890521be752667e06e38bc37b55f772f0261f253" + }, + { + "name": "WatchPoll.instDecidableSleepWithinBudget", + "kind": "definition", + "type": "(s : WatchPoll.State) → (e : WatchPoll.Event) → (t : WatchPoll.State) → Decidable (WatchPoll.SleepWithinBudget s e t)", + "typeHash": "d28a4590540914d5c90077bab9e33ab23c8699e35d190fa4c5707230f7e9c9fa", + "valueHash": "ffbec3c571af7861a21b693a8353b537bf38c9387cb9f89840ade833e1c1a452" + }, + { + "name": "WatchPoll.instDecidableStalls", + "kind": "definition", + "type": "(e : WatchPoll.Event) → (t : WatchPoll.State) → Decidable (WatchPoll.Stalls e t)", + "typeHash": "2af1254a1424eb32aaf1269744a317f1bb65b1be38106bf9e8738a707606ad5d", + "valueHash": "317d97c53456c3881d7f83f7fa542256fb1584345e54cf2a46c6a71a14282053" + }, + { + "name": "WatchPoll.instDecidableWithinTimeout", + "kind": "definition", + "type": "(timeout : Nat) → (s : WatchPoll.State) → Decidable (WatchPoll.WithinTimeout timeout s)", + "typeHash": "6a51531e8d933a068db727aa672a1924c87800783861bd0b62937b574a148995", + "valueHash": "5f6dc8ac00898c4df33788cf79cf35a094739010da1f1c7bd4af6fe1b7b50357" + }, + { + "name": "WatchPoll.instReprCall", + "kind": "definition", + "type": "Repr WatchPoll.Call", + "typeHash": "2b3f89614280302d71f9741f2f5aa3adb306ab907c85cb56294ca8cf58d9c923", + "valueHash": "aa2d88d29f7ef32337881fe41ae87121b0f84b4bb0f9506036c2e0bfd1a91947" + }, + { + "name": "WatchPoll.instReprCall.repr", + "kind": "definition", + "type": "WatchPoll.Call → Nat → Format", + "typeHash": "8a5e2696c4f3b027f666d20a380bf2fc0aa0181f03c9f912aacfe9063c382815", + "valueHash": "820349dbea000e5893f6019967c786ec7b71362883157dfa0b93e2a0ae615c62" + }, + { + "name": "WatchPoll.instReprEvent", + "kind": "definition", + "type": "Repr WatchPoll.Event", + "typeHash": "eb97e1f0dd321893573849b5eeb1f0c4ad28112a98ac48be36541e36ed4d32e9", + "valueHash": "8046e99d342f252b23123ab9c10678e87d262f21f96e836af1f2329d1e6af174" + }, + { + "name": "WatchPoll.instReprEvent.repr", + "kind": "definition", + "type": "WatchPoll.Event → Nat → Format", + "typeHash": "444eb84a50f0e80fa932aa188540c1e040dbc39f4323d8328a78837b7b2f9d1f", + "valueHash": "118fd11c0aba431e67e2ab69ac7f28c9386a75e5ac37c5dbb8663c15c1fb3b23" + }, + { + "name": "WatchPoll.instReprLast", + "kind": "definition", + "type": "Repr WatchPoll.Last", + "typeHash": "6fed6a0f817a0eeed415223d63b7bb9a468a684341109e34b1c3aeae015f4944", + "valueHash": "85cf0423e5197614580230966c21c175c592811b2c85d3accf00a7ecb4fe0816" + }, + { + "name": "WatchPoll.instReprLast.repr", + "kind": "definition", + "type": "WatchPoll.Last → Nat → Format", + "typeHash": "49f63de8f1aa88732defeb042074d779a8f03ca766bc5a7ba63dd9751d7dcb97", + "valueHash": "51ad52d8544124ed1c79a3835b1bf3d7b52a75a04662f314aa11aa17de084546" + }, + { + "name": "WatchPoll.instReprLast.repr.match_1", + "kind": "definition", + "type": "(motive : WatchPoll.Last → Sort u_1) → (x : WatchPoll.Last) → (Unit → motive WatchPoll.Last.start) → ((a : Nat) → motive (WatchPoll.Last.sleptPoll a)) → ((a : Nat) → motive (WatchPoll.Last.sleptRetry a)) → (Unit → motive WatchPoll.Last.continued) → (Unit → motive WatchPoll.Last.returnedVerdict) → ((a : Nat) → motive (WatchPoll.Last.returnedStatusQuery a)) → (Unit → motive WatchPoll.Last.returnedOnDeadline) → (Unit → motive WatchPoll.Last.threw) → motive x", + "typeHash": "b5cc61e12f0fd15abb8deb414d61ae9eed599bb2b299b57908c440cd9d011fe4", + "valueHash": "815b687739a206cf50df0ae70d96f295c53aae266c268e3ee47beb6b199e7b0d" + }, + { + "name": "WatchPoll.instReprPollingOptions", + "kind": "definition", + "type": "Repr WatchPoll.PollingOptions", + "typeHash": "b0b3d7e47766f48f2e38a3e9333629df00ef485ac56603d2b7b0c04a2870d39a", + "valueHash": "ca96d04f13464f35c76b365078b8e0c54c7642a36994678c6bf05092d401fb14" + }, + { + "name": "WatchPoll.instReprPollingOptions.repr", + "kind": "definition", + "type": "WatchPoll.PollingOptions → Nat → Format", + "typeHash": "b4585af3333fe833839b0254378c22a3c2ff6e72ef5791542cee332d2ac8b247", + "valueHash": "fbc97d94ebdc627f5c5fab70bd9977b67b9f3b156e0a336e2deecf0a69457dc9" + }, + { + "name": "WatchPoll.instReprSeconds", + "kind": "definition", + "type": "Repr WatchPoll.Seconds", + "typeHash": "672a6dd7c12f72cae5b06a3f6429f0e1f88861db8f3b1cb003a9bcc818bf1704", + "valueHash": "61355da861053f78a79a6270de92aa91fcd3ba7ae28680d8e906afcba554e45e" + }, + { + "name": "WatchPoll.instReprSeconds.repr", + "kind": "definition", + "type": "WatchPoll.Seconds → Nat → Format", + "typeHash": "3d819e316f2dca02ee72b44907ba63961f4a88fc945dcc00b78313c5008791a5", + "valueHash": "198bb937ec704dccbb1be381075b69fe7ff47aa0e562e3d9bb1a7b3cfb3014ea" + }, + { + "name": "WatchPoll.instReprSeconds.repr.match_1", + "kind": "definition", + "type": "(motive : WatchPoll.Seconds → Sort u_1) → (x : WatchPoll.Seconds) → ((a : Nat) → motive (WatchPoll.Seconds.finite a)) → (Unit → motive WatchPoll.Seconds.infinity) → motive x", + "typeHash": "4acf454f795e573f1577832d746549a3cee0ea22e7974245a9ac7603049b35d4", + "valueHash": "5a584ece15061c1d55f73d2bd0de34634d1110b8823def917061f9308994d01c" + }, + { + "name": "WatchPoll.instReprSeconds.repr.match_1.eq_1", + "kind": "theorem", + "type": "∀ (motive : WatchPoll.Seconds → Sort u_1) (a : Nat) (h_1 : (a : Nat) → motive (WatchPoll.Seconds.finite a)) (h_2 : Unit → motive WatchPoll.Seconds.infinity), (match WatchPoll.Seconds.finite a with | WatchPoll.Seconds.finite a => h_1 a | WatchPoll.Seconds.infinity => h_2 ()) = h_1 a", + "typeHash": "23c1e20487437cc78e01ab18c1e349e65ef84b0bc41d08c9c69bd46ba2cc0473", + "valueHash": null + }, + { + "name": "WatchPoll.instReprSeconds.repr.match_1.eq_2", + "kind": "theorem", + "type": "∀ (motive : WatchPoll.Seconds → Sort u_1) (h_1 : (a : Nat) → motive (WatchPoll.Seconds.finite a)) (h_2 : Unit → motive WatchPoll.Seconds.infinity), (match WatchPoll.Seconds.infinity with | WatchPoll.Seconds.finite a => h_1 a | WatchPoll.Seconds.infinity => h_2 ()) = h_2 ()", + "typeHash": "0d38636a4887a17b9769177f849fa1d50b8e039cc2f43db76d61f3e424de51b6", + "valueHash": null + }, + { + "name": "WatchPoll.instReprSeconds.repr.match_1.splitter", + "kind": "definition", + "type": "(motive : WatchPoll.Seconds → Sort u_1) → (x : WatchPoll.Seconds) → ((a : Nat) → motive (WatchPoll.Seconds.finite a)) → (Unit → motive WatchPoll.Seconds.infinity) → motive x", + "typeHash": "4acf454f795e573f1577832d746549a3cee0ea22e7974245a9ac7603049b35d4", + "valueHash": "42c340f061064e8027eec975211d34bae0adb0bf1b2288c2f1cfe7823ef782fa" + }, + { + "name": "WatchPoll.instReprSighting", + "kind": "definition", + "type": "Repr WatchPoll.Sighting", + "typeHash": "ec87e20562f71b3b555218cbff32f29e85c4ca46f5077e8a4c4b46f8181af0c7", + "valueHash": "631101a02a2ef70c3fa0d97f332608546cb66224a2479610779cab7ce9ef698a" + }, + { + "name": "WatchPoll.instReprSighting.repr", + "kind": "definition", + "type": "WatchPoll.Sighting → Nat → Format", + "typeHash": "f423777d006a3ad7a21e9bb9769afa06c61ae54cc868e8c630e0db8d9d069678", + "valueHash": "87c40cba3f4a6106bad12b5acdb24eaa8d300f89ae64c974cd71a3d653ea7bcf" + }, + { + "name": "WatchPoll.instReprState", + "kind": "definition", + "type": "Repr WatchPoll.State", + "typeHash": "f2bb1573de4cbb72d692d32a7a190748f7e4976bde43cee88fdba260362f4209", + "valueHash": "3c4d72dd43e5b813010c7ffa5f196835f0623ddb28fa4a741c3677b7cb6e0d96" + }, + { + "name": "WatchPoll.instReprState.repr", + "kind": "definition", + "type": "WatchPoll.State → Nat → Format", + "typeHash": "af19d97459b8870eb182d70fc0cf97915a37db061d0c4a5ed70f0e1fb11e30da", + "valueHash": "b9ced07e25a11bb5efce70304d4ff06302bbc58d4284ba3460190cbca1e81537" + }, + { + "name": "WatchPoll.instReprStepOutcome", + "kind": "definition", + "type": "Repr WatchPoll.StepOutcome", + "typeHash": "075deece719da945d096b43b61555e9ef35f190104af19503a31c064502ac85c", + "valueHash": "b8440c55cd3ea16ed0ff84fce0440e76566f9f348a92cf2deb64c2979a7c8466" + }, + { + "name": "WatchPoll.instReprStepOutcome.repr", + "kind": "definition", + "type": "WatchPoll.StepOutcome → Nat → Format", + "typeHash": "3483edd30541309866d86d3207877f2c26802805e88d8d94d485203a0b32da5d", + "valueHash": "9df84f40ad33ce4190b47670d6328f081cc9ebf9a138e946b6435449d641f3e0" + }, + { + "name": "WatchPoll.instReprStepOutcome.repr.match_1", + "kind": "definition", + "type": "(motive : WatchPoll.StepOutcome → Sort u_1) → (x : WatchPoll.StepOutcome) → (Unit → motive WatchPoll.StepOutcome.terminal) → ((a : Nat) → motive (WatchPoll.StepOutcome.sleep a)) → (Unit → motive WatchPoll.StepOutcome.continue_) → ((a : Bool) → motive (WatchPoll.StepOutcome.queryError a)) → (Unit → motive WatchPoll.StepOutcome.deadlineExceeded) → (Unit → motive WatchPoll.StepOutcome.otherError) → motive x", + "typeHash": "d374f0e69b5b93ccd1b25cdcba35ffe95c031a51aad5e82a8a85aec448f26f55", + "valueHash": "bc88611105448fcefa44b952e9f213f2195c76874a995ecff0ff35cc85fb0ea3" + }, + { + "name": "WatchPoll.instReprTransition", + "kind": "definition", + "type": "Repr WatchPoll.Transition", + "typeHash": "ba25730b15f354f96037f3d66c97c3ce5d2e8a9b21d3d7a1a54e4d7ef78513ae", + "valueHash": "af0e038fd41c1d1ad2ddd29d0bc1157666317f9c402b1618a0596d74fcb098bc" + }, + { + "name": "WatchPoll.instReprTransition.repr", + "kind": "definition", + "type": "WatchPoll.Transition → Nat → Format", + "typeHash": "8a11760c5a4ce141ea9117050a4416776b6885ec7425e13f23446bd9225f60c1", + "valueHash": "7997215440807658f71b5bdc90fc37b7c704d47b7f22f1ee77e079b2c365b7ee" + }, + { + "name": "WatchPoll.instReprWatchDeadline", + "kind": "definition", + "type": "Repr WatchPoll.WatchDeadline", + "typeHash": "79224db6b0a4fa74442742ac29422876f5b0014b4d81ffe33a1c914decc200c4", + "valueHash": "82507217e0dacfc23f3bb7c59476cdb84276f93689eb2fcae7477ce417bf1e4a" + }, + { + "name": "WatchPoll.instReprWatchDeadline.repr", + "kind": "definition", + "type": "WatchPoll.WatchDeadline → Nat → Format", + "typeHash": "06990e934d055da4f218653146266d5f1969313325d23c753ea20356142b3a46", + "valueHash": "33f227fbf474a662d5920e344a4fe2f86ec895ffc7f6a379cb7704a667bac9f9" + }, + { + "name": "WatchPoll.invariant_init", + "kind": "theorem", + "type": "∀ (o : WatchPoll.PollingOptions) (remaining : WatchPoll.Seconds), WatchPoll.Invariant o (WatchPoll.init remaining)", + "typeHash": "f7f1bc39f61f0294127fb3c98adcaee4423e6010159704ea9d90a6949bfda298", + "valueHash": null + }, + { + "name": "WatchPoll.pollStep", + "kind": "definition", + "type": "WatchPoll.PollingOptions → WatchPoll.State → WatchPoll.Event → Option WatchPoll.State", + "typeHash": "2340df5f2caffa6798ab97f189433e6305f2370f6e078785deaf44b2eda9ac1e", + "valueHash": "954a9062e746f7bf1cf19d1563109bdfa370e17557c7f0692378b077e4f840ed" + }, + { + "name": "WatchPoll.pollStep.match_1", + "kind": "definition", + "type": "(motive : WatchPoll.StepOutcome → Sort u_1) → (x : WatchPoll.StepOutcome) → (Unit → motive WatchPoll.StepOutcome.terminal) → ((seconds : Nat) → motive (WatchPoll.StepOutcome.sleep seconds)) → (Unit → motive WatchPoll.StepOutcome.continue_) → (Unit → motive WatchPoll.StepOutcome.deadlineExceeded) → (Unit → motive WatchPoll.StepOutcome.otherError) → ((retryable : Bool) → motive (WatchPoll.StepOutcome.queryError retryable)) → motive x", + "typeHash": "8fd46ca8061c4f6ebe2b1cc48ce84ae3667266a59eb433e04ae5d0cac48dee07", + "valueHash": "181575057ac01be6ab59edfa3a6ab61542c10b1a4429b86dbe5dbf033d3d8b67" + }, + { + "name": "WatchPoll.pollStep_keeps_invariant", + "kind": "theorem", + "type": "∀ (o : WatchPoll.PollingOptions) (s t : WatchPoll.State) (e : WatchPoll.Event), WatchPoll.Invariant o s → WatchPoll.pollStep o s e = some t → WatchPoll.Invariant o t", + "typeHash": "dea516aab50c6f3e5fa6254a76ebe4490bf9c9709e72534c25665301aaefdb40", + "valueHash": null + }, + { + "name": "WatchPoll.pollStep_keeps_timeout", + "kind": "theorem", + "type": "∀ (timeout : Nat) (o : WatchPoll.PollingOptions) (s t : WatchPoll.State) (e : WatchPoll.Event), WatchPoll.WithinTimeout timeout s → WatchPoll.pollStep o s e = some t → WatchPoll.HonestClock e t → WatchPoll.WithinTimeout timeout t", + "typeHash": "f07e4dfcfe1e57e2486e58a2e0d59cd87f76720a0ee0880f111e643794564d3a", + "valueHash": null + }, + { + "name": "WatchPoll.pollStep_resets_failures", + "kind": "theorem", + "type": "∀ (o : WatchPoll.PollingOptions) (s t : WatchPoll.State) (e : WatchPoll.Event), WatchPoll.pollStep o s e = some t → WatchPoll.ResetsFailures s e t", + "typeHash": "80c04a2989042f5ce26a335322a69c584b504617e40e88a025770497e51d96ee", + "valueHash": null + }, + { + "name": "WatchPoll.pollStep_retry_uses_backoff", + "kind": "theorem", + "type": "∀ (o : WatchPoll.PollingOptions) (s t : WatchPoll.State) (e : WatchPoll.Event), WatchPoll.pollStep o s e = some t → WatchPoll.RetryUsesBackoff o s e t", + "typeHash": "9aeb54c270911c1846e90fab164d20f15f166f06eba1b1e1f406be8f3ffffe92", + "valueHash": null + }, + { + "name": "WatchPoll.pollStep_sleep_within_budget", + "kind": "theorem", + "type": "∀ (o : WatchPoll.PollingOptions) (s t : WatchPoll.State) (e : WatchPoll.Event), WatchPoll.pollStep o s e = some t → WatchPoll.SleepWithinBudget s e t", + "typeHash": "661cf1fb491c212ca684231daf9f37f425987c05053a30fa5569b5ea2ae393c1", + "valueHash": null + }, + { + "name": "WatchPoll.pollUntilTerminal_slept_le_timeout", + "kind": "theorem", + "type": "∀ (o : WatchPoll.PollingOptions) (timeout t0 elapsed : Nat) (es : List WatchPoll.Event), 0 < timeout → WatchPoll.HonestRun o (WatchPoll.init ((WatchPoll.WatchDeadline.create timeout t0).remaining (t0 + elapsed))) es → (WatchPoll.run o (WatchPoll.init ((WatchPoll.WatchDeadline.create timeout t0).remaining (t0 + elapsed))) es).slept ≤ timeout", + "typeHash": "8147547dc10a1b7fb17d537395ce712dd914ceb3b92d1d3171c295d20813bcba", + "valueHash": null + }, + { + "name": "WatchPoll.queryBackoffHalfSeconds", + "kind": "definition", + "type": "Nat → Nat → Nat", + "typeHash": "94b9a6a83b4de49ceaccbbb340e4b2ebf92d42dc5f64dea3e8b246e159a30fd5", + "valueHash": "c8b0ca1e3319ffc91fa973a81601bd3a1a3c02a88a5c27af83bbf46160a1843e" + }, + { + "name": "WatchPoll.queryBackoffHalfSeconds_eq", + "kind": "theorem", + "type": "∀ (interval failures : Nat), 1 ≤ failures → WatchPoll.queryBackoffHalfSeconds interval failures = 2 * WatchPoll.queryBackoffSeconds interval failures", + "typeHash": "9488cfee81c2100202aecab21d933d22662c2eb54957df0a269d6c3b2318d557", + "valueHash": null + }, + { + "name": "WatchPoll.queryBackoffHalfSeconds_zero", + "kind": "theorem", + "type": "∀ (interval : Nat), WatchPoll.queryBackoffHalfSeconds interval 0 = min (max interval 60) 600", + "typeHash": "5c2f0c649700784ca97ac11bbb3325376914cfee2875559fbc38ba7e95f6d5cb", + "valueHash": null + }, + { + "name": "WatchPoll.queryBackoffSeconds", + "kind": "definition", + "type": "Nat → Nat → Nat", + "typeHash": "94b9a6a83b4de49ceaccbbb340e4b2ebf92d42dc5f64dea3e8b246e159a30fd5", + "valueHash": "c994e4a86bf7fc3c531631780c7d843c0e03390877983f0fa938905a3d732e82" + }, + { + "name": "WatchPoll.queryBackoff_doubles", + "kind": "theorem", + "type": "∀ (interval failures : Nat), 1 ≤ failures → WatchPoll.queryBackoffSeconds interval (failures + 1) = min (2 * WatchPoll.queryBackoffSeconds interval failures) 300", + "typeHash": "f90af4f7a707dd46cbd65c8b217c39aa50abf19dde421ce960bd9dd260643003", + "valueHash": null + }, + { + "name": "WatchPoll.queryBackoff_eq_300", + "kind": "theorem", + "type": "∀ (interval failures : Nat), 4 ≤ failures → WatchPoll.queryBackoffSeconds interval failures = 300", + "typeHash": "f7b74dc74e4bc083f60d6b3545d7a1163e00f666bc65e29503407473d0fbf846", + "valueHash": null + }, + { + "name": "WatchPoll.queryBackoff_first", + "kind": "theorem", + "type": "∀ (interval : Nat), WatchPoll.queryBackoffSeconds interval 1 = min (max interval 60) 300", + "typeHash": "85c5620679b14f1f312b88353acf170efd8701e86f857394b0dddf1d40869ff3", + "valueHash": null + }, + { + "name": "WatchPoll.queryBackoff_ge_60", + "kind": "theorem", + "type": "∀ (interval failures : Nat), 1 ≤ failures → 60 ≤ WatchPoll.queryBackoffSeconds interval failures", + "typeHash": "1a9c94ee2e8d7b14a735a9cab0102583918395352fb17888357d8d6efd1bcd71", + "valueHash": null + }, + { + "name": "WatchPoll.queryBackoff_le_300", + "kind": "theorem", + "type": "∀ (interval failures : Nat), WatchPoll.queryBackoffSeconds interval failures ≤ 300", + "typeHash": "50aaf557b371108ffe464d600d2bf057d4b86bff2aff88f3c6402d739741c2ee", + "valueHash": null + }, + { + "name": "WatchPoll.queryBackoff_mono", + "kind": "theorem", + "type": "∀ (interval f g : Nat), 1 ≤ f → f ≤ g → WatchPoll.queryBackoffSeconds interval f ≤ WatchPoll.queryBackoffSeconds interval g", + "typeHash": "50c2542310099b5f974fb4cbc865e0d7c8a8f65024789707f10d92f10c7e352b", + "valueHash": null + }, + { + "name": "WatchPoll.queryBackoff_zero_below_floor", + "kind": "theorem", + "type": "∀ (interval : Nat), WatchPoll.queryBackoffHalfSeconds interval 0 < 2 * 60 ↔ interval < 120", + "typeHash": "c8090b9fea5a4ce4f28cc66c991911e2bd562bc27cd5e52bde28696fbc941701", + "valueHash": null + }, + { + "name": "WatchPoll.reachTargets", + "kind": "definition", + "type": "List (String × (WatchPoll.Transition → Bool))", + "typeHash": "fdfe3e056397b87e9e3678ca0a64990aa87db4c014c1ad0c7eda7d247e0b837a", + "valueHash": "07b3ec4de0cca047848e56e8faf043a2dae15367c6862c22d139af527cec26d3" + }, + { + "name": "WatchPoll.reachTargets.match_1", + "kind": "definition", + "type": "(motive : WatchPoll.StepOutcome → WatchPoll.Last → Sort u_1) → (x : WatchPoll.StepOutcome) → (x_1 : WatchPoll.Last) → ((asked seconds : Nat) → motive (WatchPoll.StepOutcome.sleep asked) (WatchPoll.Last.sleptPoll seconds)) → ((x : WatchPoll.StepOutcome) → (x_2 : WatchPoll.Last) → motive x x_2) → motive x x_1", + "typeHash": "583584a04cb3bc8ceaf15b6da9ba192e079ea15f047a6035dd8eeefd99149ebf", + "valueHash": "8c9bc60795132700e24ddbbdf6f01c6dc65bae68dea734500f64f27a8f8555f5" + }, + { + "name": "WatchPoll.reachable", + "kind": "definition", + "type": "WatchPoll.PollingOptions → WatchPoll.Seconds → Nat → List WatchPoll.State", + "typeHash": "acbc50f6633254e3487dc00498656cf6a2b3e9d94a7aedb972d3add2bae6e9ec", + "valueHash": "a92945bc4d4decc21fa4babec73954356e1c52b638657ccd62043a4e02a8daaa" + }, + { + "name": "WatchPoll.remaining_create_finite", + "kind": "theorem", + "type": "∀ (timeout t0 elapsed : Nat), 0 < timeout → (WatchPoll.WatchDeadline.create timeout t0).remaining (t0 + elapsed) = WatchPoll.Seconds.finite (timeout - elapsed)", + "typeHash": "6f9a32f0e510f018eb93762b7e7e8cf7427e2871a422af80b3ccf849a73c1938", + "valueHash": null + }, + { + "name": "WatchPoll.remaining_create_infinity", + "kind": "theorem", + "type": "∀ (t0 now : Nat), (WatchPoll.WatchDeadline.create 0 t0).remaining now = WatchPoll.Seconds.infinity", + "typeHash": "7dc9cd6cd603b8d335084697155334e7a7302b88ec2e3154d6b3f27ee75dba88", + "valueHash": null + }, + { + "name": "WatchPoll.remaining_elapse", + "kind": "theorem", + "type": "∀ (d : WatchPoll.WatchDeadline) (now seconds : Nat), d.remaining (now + seconds) = (d.remaining now).elapse seconds", + "typeHash": "4503894aee2530603b23ed4ab7b33c43149a581d762009ef8f3d9b934f4f14e2", + "valueHash": null + }, + { + "name": "WatchPoll.run", + "kind": "definition", + "type": "WatchPoll.PollingOptions → WatchPoll.State → List WatchPoll.Event → WatchPoll.State", + "typeHash": "438e5f926cbac1f7d7cd30a896f384370d386c07e2cf93864a6cffb4cbe05a92", + "valueHash": "7d87b4094451083ecf97e7947d8b6642e37057876cf80a07300f4daeba4ed3f0" + }, + { + "name": "WatchPoll.run.match_1", + "kind": "definition", + "type": "(motive : Option WatchPoll.State → Sort u_1) → (x : Option WatchPoll.State) → ((t : WatchPoll.State) → motive (some t)) → (Unit → motive none) → motive x", + "typeHash": "1bc30e6d20b76786e39adca4cc22f9cf285e45b37cdf4e15758734d229ec5042", + "valueHash": "0c36549b657ebb7d92a775a328e7cdcb1bd5db26c5c54bc3dd0485f996f48689" + }, + { + "name": "WatchPoll.run.match_3", + "kind": "definition", + "type": "(motive : List WatchPoll.Event → Sort u_1) → (x : List WatchPoll.Event) → (Unit → motive []) → ((e : WatchPoll.Event) → (es : List WatchPoll.Event) → motive (e :: es)) → motive x", + "typeHash": "579d7b4ed8bec1c5dfe391b4b905ea66e82913a13bbf55d571bfdbae5609a1d8", + "valueHash": "2e42f8a029e70772916302d3d5865a3617e81d0509a256f1e19887bbb565e80e" + }, + { + "name": "WatchPoll.runStart", + "kind": "definition", + "type": "Nat → Nat → List WatchPoll.Call → Option Nat", + "typeHash": "243611db33d6352bf2d87596f1f1e9526bde1d0deecb30f39930eca8d3c437a3", + "valueHash": "3fde59a1e08545a794655a6bca17a7b3864e1b0f67521f12f628d7e19c4d54e2" + }, + { + "name": "WatchPoll.run_keeps_invariant", + "kind": "theorem", + "type": "∀ (o : WatchPoll.PollingOptions) (s : WatchPoll.State) (es : List WatchPoll.Event), WatchPoll.Invariant o s → WatchPoll.Invariant o (WatchPoll.run o s es)", + "typeHash": "e225667eba79d9448c81f9c89d8e296be967a86a51c26557d975f49db2b708e6", + "valueHash": null + }, + { + "name": "WatchPoll.run_keeps_timeout", + "kind": "theorem", + "type": "∀ (timeout : Nat) (o : WatchPoll.PollingOptions) (s : WatchPoll.State) (es : List WatchPoll.Event), WatchPoll.WithinTimeout timeout s → WatchPoll.HonestRun o s es → WatchPoll.WithinTimeout timeout (WatchPoll.run o s es)", + "typeHash": "2218920ea73f2ee9bbe9bad83ad1e6fb4fc31728c12b2ed8e3556c0d1b0947b4", + "valueHash": null + }, + { + "name": "WatchPoll.run_retries_below_max", + "kind": "theorem", + "type": "∀ (o : WatchPoll.PollingOptions) (remaining : WatchPoll.Seconds) (es : List WatchPoll.Event), 0 < o.maxQueryErrors → WatchPoll.RetriesBelowMax o (WatchPoll.run o (WatchPoll.init remaining) es)", + "typeHash": "8b71114f8e13cddefce418e70c74ebda88ca036f00c9b0c4d2336f943c508f06", + "valueHash": null + }, + { + "name": "WatchPoll.secondsJson", + "kind": "definition", + "type": "Nat → String", + "typeHash": "8480599605e2fb8bce4345c14667997d8a76b4fbc6eeb9f15a938a347374fc71", + "valueHash": "cce06d8caead6ad82bcabed974fda9a2a4539a273ae932a74d2ed354c19bfdf2" + }, + { + "name": "WatchPoll.stallOutcomes", + "kind": "definition", + "type": "List WatchPoll.StepOutcome", + "typeHash": "3bc0ca8158679f528eecb09acce134ac4d7e678736bbbc302397aa178643754e", + "valueHash": "bc028700a2a4591d4a8a17dff0b5d5601fd251f9b07d247b39af756fc42243ff" + }, + { + "name": "WatchPoll.stall_needs_continue_or_zero_sleep", + "kind": "theorem", + "type": "∀ (o : WatchPoll.PollingOptions) (s t : WatchPoll.State) (e : WatchPoll.Event), WatchPoll.pollStep o s e = some t → WatchPoll.HonestClock e t → WatchPoll.Stalls e t → e.outcome = WatchPoll.StepOutcome.continue_ ∨ e.outcome = WatchPoll.StepOutcome.sleep 0", + "typeHash": "34d42e1d4f573bb959ca17904ac1c91172655f67816c4701a3bd7f97880ff12e", + "valueHash": null + }, + { + "name": "WatchPoll.stepProperties", + "kind": "definition", + "type": "List (String × (WatchPoll.Transition → Bool))", + "typeHash": "fdfe3e056397b87e9e3678ca0a64990aa87db4c014c1ad0c7eda7d247e0b837a", + "valueHash": "5fc27cf1e2ddadc92af1bc28c2a9a218e3a4860ceeae161a6641303b41690ebf" + }, + { + "name": "WatchPoll.transitions", + "kind": "definition", + "type": "List WatchPoll.Transition", + "typeHash": "672280a7d50e2ae0753c400cd5b5e553d2949645fcca44131585912480a4bfa1", + "valueHash": "7373d6b66ff7729d29233db07606e6be11b69490a5bd725ab34a366c7669a7ca" + }, + { + "name": "WatchPoll.unreachedConfirms", + "kind": "definition", + "type": "List String", + "typeHash": "9530863598172fb42fb2852fa1a55bb0008754cdf21aff8d80fdeb1067897af9", + "valueHash": "8f5e82593e76aff9881b7caff651bb89b0a2bbbd9470ea34edd7a9a62c649e01" + }, + { + "name": "WatchPoll.unreachedSteps", + "kind": "definition", + "type": "List String", + "typeHash": "9530863598172fb42fb2852fa1a55bb0008754cdf21aff8d80fdeb1067897af9", + "valueHash": "8002cb327a1d1df8d203dfbdbdb2cef817661bbf47e581fc8b6824f267fd81d5" + }, + { + "name": "WatchPoll.zero_sleep_without_clock_repeats", + "kind": "theorem", + "type": "∀ (o : WatchPoll.PollingOptions) (s : WatchPoll.State), s.last = WatchPoll.Last.sleptPoll 0 → s.remaining.positive = true → s.failures = 0 → s.retries = 0 → WatchPoll.pollStep o s { outcome := WatchPoll.StepOutcome.sleep 0, stepElapsed := 0, sleepElapsed := 0 } = some s", + "typeHash": "ff9ec5a48d4dc9034ad487c5439063746f5563b222fa00632cd61d99d67313d2", + "valueHash": null + } + ] +} diff --git a/lean/lake-manifest.json b/lean/lake-manifest.json new file mode 100644 index 000000000..4fbecf563 --- /dev/null +++ b/lean/lake-manifest.json @@ -0,0 +1,6 @@ +{"version": "1.2.0", + "packagesDir": ".lake/packages", + "packages": [], + "name": "model", + "lakeDir": ".lake", + "fixedToolchain": false} diff --git a/lean/lakefile.toml b/lean/lakefile.toml new file mode 100644 index 000000000..4c53dfc35 --- /dev/null +++ b/lean/lakefile.toml @@ -0,0 +1,6 @@ +name = "model" +defaultTargets = ["Model"] + +[[lean_lib]] +name = "Model" +globs = ["Model.+"] diff --git a/lean/lean-toolchain b/lean/lean-toolchain new file mode 100644 index 000000000..ba8ebf2db --- /dev/null +++ b/lean/lean-toolchain @@ -0,0 +1 @@ +leanprover/lean4:v4.34.1 diff --git a/plugins/pstack/skills/poteto-mode/scripts/watch-pr/cli.test.ts b/plugins/pstack/skills/poteto-mode/scripts/watch-pr/cli.test.ts index 31a2a624c..e7654c617 100644 --- a/plugins/pstack/skills/poteto-mode/scripts/watch-pr/cli.test.ts +++ b/plugins/pstack/skills/poteto-mode/scripts/watch-pr/cli.test.ts @@ -217,6 +217,18 @@ describe("main", () => { ); }); + it("rejects an interval longer than one timer can sleep, and accepts the longest one", async () => { + const harness = testRuntime(fakeReader()); + expect(await main(["--interval", "2147484"], harness.runtime)).toBe(64); + expect(harness.stdout).toEqual([]); + expect(harness.stderr.join("")).toContain( + "option '--interval <seconds>' argument '2147484' is invalid. must be at most 2147483.647" + ); + expect( + parseArgs(["--interval", "2147483.647"], silentIo).polling.interval + ).toBe(2147483.647); + }); + it("bypasses the queue machine for queued-stack status-only", async () => { const reader = fakeReader(); const harness = testRuntime(reader); diff --git a/plugins/pstack/skills/poteto-mode/scripts/watch-pr/cli.ts b/plugins/pstack/skills/poteto-mode/scripts/watch-pr/cli.ts index 8de672833..3b937f12b 100644 --- a/plugins/pstack/skills/poteto-mode/scripts/watch-pr/cli.ts +++ b/plugins/pstack/skills/poteto-mode/scripts/watch-pr/cli.ts @@ -40,6 +40,15 @@ function positiveNumber(value: string): number { throw new InvalidArgumentError("must be greater than zero"); return parsed; } +// A timer delay above 2^31 - 1 ms is replaced with 1 ms, so a longer sleep +// would return at once and the loop would poll without pause. +const MAX_SLEEP_SECONDS = (2 ** 31 - 1) / 1_000; +function sleepSeconds(value: string): number { + const parsed = positiveNumber(value); + if (parsed > MAX_SLEEP_SECONDS) + throw new InvalidArgumentError(`must be at most ${MAX_SLEEP_SECONDS}`); + return parsed; +} function nonNegativeNumber(value: string): number { const parsed = Number(value); if (!Number.isFinite(parsed) || parsed < 0) @@ -113,7 +122,7 @@ export function parseArgs( .option( "--interval <seconds>", `poll interval; a PR with no checks takes ${NO_CHECKS_CONFIRM_SECONDS} seconds to confirm whatever this is`, - positiveNumber, + sleepSeconds, 60 ) .option( diff --git a/tools/forks.json b/tools/forks.json index 90075cf76..df88dabcd 100644 --- a/tools/forks.json +++ b/tools/forks.json @@ -146,13 +146,13 @@ }, "plugins/pstack/skills/poteto-mode/scripts/watch-pr/cli.test.ts": { "kind": "policy", - "why": "Supplies the shared deadline and landing revision the port's watcher requires, and checks that a PR with no checks configured gets a first-sighting status cell and, after one WAITING poll, a READY verdict that names the absence, and that mergeability GitHub has not computed is named in the status table, the WAITING line, and the TIMEOUT line, that a retry after a mid-read change is not called a failed query, that a command that could not run gets an install action, and the two help lines the port rewrote.", + "why": "Supplies the shared deadline and landing revision the port's watcher requires, and checks that a PR with no checks configured gets a first-sighting status cell and, after one WAITING poll, a READY verdict that names the absence, and that mergeability GitHub has not computed is named in the status table, the WAITING line, and the TIMEOUT line, that a retry after a mid-read change is not called a failed query, that a command that could not run gets an install action, that an --interval longer than one timer can sleep is refused, and the two help lines the port rewrote.", "since": "0.9.34", "upstream": "not-proposed" }, "plugins/pstack/skills/poteto-mode/scripts/watch-pr/cli.ts": { "kind": "policy", - "why": "Threads one WatchDeadline through discovery, commands, and polling, reports a deadline verdict instead of throwing, and says in its help that --interval does not shorten the 60 second no-checks confirmation and that exit 0 from --status-only does not mean ready.", + "why": "Threads one WatchDeadline through discovery, commands, and polling, reports a deadline verdict instead of throwing, rejects an --interval longer than one timer can sleep, and says in its help that --interval does not shorten the 60 second no-checks confirmation and that exit 0 from --status-only does not mean ready.", "since": "0.9.24", "upstream": "not-proposed" }, From f807105d217fa90281273597c5496e91a80ef69f Mon Sep 17 00:00:00 2001 From: Michael Denyer <97485362+michael-denyer@users.noreply.github.com> Date: Thu, 8 Oct 2026 22:05:06 +0100 Subject: [PATCH 06/11] fix(pi): seal the wakeup slot only against a run in flight (#237) /loop treated any non-idle session as a run in flight. Pi is also not idle during a manual compaction, and it emits agent_settled only when a run ends, so a /loop typed during a compaction left the slot sealed and the next run was refused every wakeup. The scheduler now tracks a run from agent_start to agent_settled and seals only then. The TLA+ model of the scheduler comes with the change; two of its runs still fail by design and record narrower findings this does not fix. --- plugins/pstack/pi/schedule.ts | 17 +- tests/pi/interaction.test.mjs | 31 ++- tla/PiScheduler.matrix | 70 +++++++ tla/PiScheduler.mutations | 127 +++++++++++++ tla/PiScheduler.tla | 343 ++++++++++++++++++++++++++++++++++ 5 files changed, 583 insertions(+), 5 deletions(-) create mode 100644 tla/PiScheduler.matrix create mode 100644 tla/PiScheduler.mutations create mode 100644 tla/PiScheduler.tla diff --git a/plugins/pstack/pi/schedule.ts b/plugins/pstack/pi/schedule.ts index fa0d25676..86a471ee0 100644 --- a/plugins/pstack/pi/schedule.ts +++ b/plugins/pstack/pi/schedule.ts @@ -31,6 +31,9 @@ export class Scheduler { // nothing tells that run's re-arm of the old loop from any other wakeup. It // is refused them all, or the loop the user just ended would come back. sealed = false; + // A run is in flight: set by agent_start, cleared by agent_settled. Not + // ctx.isIdle(), which is also false during a manual compaction. + running = false; constructor(private readonly pi: ExtensionAPI) {} @@ -163,7 +166,11 @@ export function registerSchedule(pi: ExtensionAPI, scheduler: Scheduler, oneShot }, }); + pi.on("agent_start", () => { + scheduler.running = true; + }); pi.on("agent_settled", () => { + scheduler.running = false; scheduler.sealed = false; }); @@ -171,6 +178,8 @@ export function registerSchedule(pi: ExtensionAPI, scheduler: Scheduler, oneShot description: "Run a prompt on an interval (/loop 5m <prompt>), self-paced (/loop <prompt>), or stop (/loop stop)", async handler(args, ctx) { const cmd = parseLoop(args); + // Also true during a manual compaction, which no run is behind: the first + // prompt then waits for idle, but only a run in flight seals the slot. const midRun = !ctx.isIdle(); // sendUserMessage only starts the run, and a one-shot run disposes the // session as soon as the command returns, so there the command waits it out. @@ -180,7 +189,7 @@ export function registerSchedule(pi: ExtensionAPI, scheduler: Scheduler, oneShot // re-arm then comes from a later run and is not refused. if (midRun) { scheduler.scheduleWakeup(0, prompt, ctx); - ctx.ui.notify("The loop starts when the current run ends.", "info"); + ctx.ui.notify("The loop starts once the session is idle.", "info"); return; } if (!oneShot.exits(ctx)) { @@ -197,18 +206,18 @@ export function registerSchedule(pi: ExtensionAPI, scheduler: Scheduler, oneShot ctx.ui.notify(`${cmd.reason ? `${cmd.reason} ` : ""}Usage: /loop [interval like 5m or 1h] <prompt>, or /loop stop`, "info"); return; case "stop": - ctx.ui.notify(scheduler.stopAll(midRun) ? "Loop stopped." : "No loop was running.", "info"); + ctx.ui.notify(scheduler.stopAll(scheduler.running) ? "Loop stopped." : "No loop was running.", "info"); return; case "fixed": { const seconds = Math.max(MIN_DELAY_S, cmd.seconds); - scheduler.stopAll(midRun); + scheduler.stopAll(scheduler.running); scheduler.startLoop(seconds, cmd.prompt, ctx); ctx.ui.notify(`Looping every ${seconds}s. /loop stop ends it.`, "info"); await fire(cmd.prompt); return; } case "dynamic": - scheduler.stopAll(midRun); + scheduler.stopAll(scheduler.running); scheduler.startSelfPaced(); await fire(dynamicPrompt(cmd.prompt)); } diff --git a/tests/pi/interaction.test.mjs b/tests/pi/interaction.test.mjs index a6fa79d69..09ff73686 100644 --- a/tests/pi/interaction.test.mjs +++ b/tests/pi/interaction.test.mjs @@ -357,6 +357,7 @@ describe("/loop", () => { const { pi, ctx, run, ui } = loop({ idle: () => idle }); await run("watch PR 42"); idle = false; + await pi.emit("agent_start", {}, ctx); await run("stop"); expect(ui.calls.at(-1).message).toBe("Loop stopped."); const refused = await pi.call("schedule_wakeup", { delaySeconds: 60, prompt: rearm }, { ...ctx, ui }); @@ -373,6 +374,7 @@ describe("/loop", () => { let idle = false; const { pi, ctx, run, ui } = loop({ idle: () => idle }); const wake = () => pi.call("schedule_wakeup", { delaySeconds: 60, prompt: "later" }, { ...ctx, ui }); + await pi.emit("agent_start", {}, ctx); await run("stop"); expect(ui.calls.at(-1).message).toBe("No loop was running."); expect((await wake()).details).toEqual({ refused: true }); @@ -389,8 +391,9 @@ describe("/loop", () => { const wake = (params) => pi.call("schedule_wakeup", params, { ...ctx, ui }); await run("watch old"); idle = false; + await pi.emit("agent_start", {}, ctx); await run("watch new"); - expect(ui.calls.at(-1).message).toBe("The loop starts when the current run ends."); + expect(ui.calls.at(-1).message).toBe("The loop starts once the session is idle."); expect((await wake({ delaySeconds: 60, prompt: "/loop watch old" })).details).toEqual({ refused: true }); expect((await wake({ stop: true })).details).toEqual({ cancelled: false }); jest.advanceTimersByTime(60_000); @@ -412,6 +415,7 @@ describe("/loop", () => { test("a fixed loop asked for during a run sends its first prompt once that run settles", async () => { let idle = false; const { pi, ctx, run } = loop({ idle: () => idle }); + await pi.emit("agent_start", {}, ctx); await run("5m check the deploy"); jest.advanceTimersByTime(30_000); expect(pi.userMessages).toEqual([]); @@ -421,6 +425,31 @@ describe("/loop", () => { expect(pi.userMessages.map((m) => m.content)).toEqual(["check the deploy"]); }); + // During a manual compaction isIdle() is false with no run in flight, and + // no agent_settled follows when it ends. + test("a self-paced loop started during a manual compaction waits for idle, then re-arms after its first iteration", async () => { + let idle = false; + const { pi, ctx, run, ui } = loop({ idle: () => idle }); + await run("watch the deploy"); + expect(ui.calls.at(-1).message).toBe("The loop starts once the session is idle."); + idle = true; + jest.advanceTimersByTime(1_000); + expect(pi.userMessages).toHaveLength(1); + idle = false; + await pi.emit("agent_start", {}, ctx); + const rearm = await pi.call("schedule_wakeup", { delaySeconds: 60, prompt: "/loop watch the deploy" }, { ...ctx, ui }); + expect(resultText(rearm)).toContain("Wakeup scheduled"); + }); + + test("/loop stop during a manual compaction does not refuse the next run's wakeup", async () => { + let idle = false; + const { pi, ctx, run, ui } = loop({ idle: () => idle }); + await run("stop"); + await pi.emit("agent_start", {}, ctx); + const wake = await pi.call("schedule_wakeup", { delaySeconds: 60, prompt: "later" }, { ...ctx, ui }); + expect(resultText(wake)).toContain("Wakeup scheduled"); + }); + for (const [what, started, prompt] of [ ["a live loop's re-arm in other words than its prompt", "watch PR 42", "/loop Watch PR 42."], ["a /loop prompt with no loop started", null, "/loop babysit PR 42"], diff --git a/tla/PiScheduler.matrix b/tla/PiScheduler.matrix new file mode 100644 index 000000000..45d787dac --- /dev/null +++ b/tla/PiScheduler.matrix @@ -0,0 +1,70 @@ +# Matrix for tlc-matrix.sh. w is MaxWakeups (scheduleWakeup calls), l is +# MaxLoops (startLoop calls). The code has exactly one wakeup slot and one +# event loop, so there is no slot count or worker count to vary. +# +# "strict" is the host the comments in schedule.ts assume: not idle means a +# run is in flight, a fired prompt is a run at once, and nothing else starts a +# run while a /loop start waits in the slot. Every property holds there. + +spec PiScheduler.tla +check INVARIANTS TypeOK OneWakeup TimersOwned NoCancelledDelivery NoRearmByCutRun SealedOnlyAgainstCutRun FreshRunNotRefused StartSurvivesRuns OneLoopKind StoppedIsTerminal +check PROPERTIES IdleDelivery NothingFiresAfterStop +reach ParkedWakeup | strict w=2 +reach DueWhileIdle | strict w=2 +reach TickWhileBusy | strict w=2 +reach StartBehindCutRun | strict w=2 +reach LoopAndWakeup | strict w=2 +reach SelfPacedRearm | strict w=2 +reach StoppedAfterDelivery | strict w=2 +reach ShutdownCancelled | strict w=2 +reach AllDelivered | strict w=2 +run strict w=0 l=1 | MaxWakeups=0 MaxLoops=1 CompactCmd=FALSE StartGap=FALSE StartRace=FALSE +run strict w=1 l=0 | MaxWakeups=1 MaxLoops=0 CompactCmd=FALSE StartGap=FALSE StartRace=FALSE +run strict w=1 l=1 | MaxWakeups=1 MaxLoops=1 CompactCmd=FALSE StartGap=FALSE StartRace=FALSE +run strict w=2 l=1 | MaxWakeups=2 MaxLoops=1 CompactCmd=FALSE StartGap=FALSE StartRace=FALSE +run strict w=3 l=2 | MaxWakeups=3 MaxLoops=2 CompactCmd=FALSE StartGap=FALSE StartRace=FALSE +run strict w=5 l=2 | MaxWakeups=5 MaxLoops=2 CompactCmd=FALSE StartGap=FALSE StartRace=FALSE + +# Liveness, under the fairness in Spec. It holds on every host: a /loop start +# that a run replaces counts as cancelled. +spec PiScheduler.tla +check PROPERTIES WakeupDelivered +run strict w=1 l=1 liveness | MaxWakeups=1 MaxLoops=1 CompactCmd=FALSE StartGap=FALSE StartRace=FALSE +run strict w=2 l=1 liveness | MaxWakeups=2 MaxLoops=1 CompactCmd=FALSE StartGap=FALSE StartRace=FALSE +run host=all w=2 l=1 liveness | MaxWakeups=2 MaxLoops=1 CompactCmd=TRUE StartGap=TRUE StartRace=TRUE + +# All three host behaviours on. The timer, delivery, stop and sealed +# properties do not depend on the host. +spec PiScheduler.tla +check INVARIANTS TypeOK OneWakeup TimersOwned NoCancelledDelivery SealedOnlyAgainstCutRun FreshRunNotRefused OneLoopKind StoppedIsTerminal +check PROPERTIES IdleDelivery NothingFiresAfterStop +run host=all w=3 l=2 | MaxWakeups=3 MaxLoops=2 CompactCmd=TRUE StartGap=TRUE StartRace=TRUE + +# One host behaviour at a time. First the sealed properties that still hold +# with it on, then the ones that fail. The failing runs are the record of each +# finding and are expected to FAIL; do not weaken the property. + +# /loop during a manual compaction. Every property holds: the seal follows the +# extension's own run flag, not isIdle(), so a loop started during a +# compaction runs and re-arms. +spec PiScheduler.tla +check INVARIANTS TypeOK NoRearmByCutRun SealedOnlyAgainstCutRun FreshRunNotRefused StartSurvivesRuns +reach CompactionLoopRearmed | host=compact +run host=compact holds w=3 l=2 | MaxWakeups=3 MaxLoops=2 CompactCmd=TRUE StartGap=FALSE StartRace=FALSE + +# A gap between fire() and the run becoming active. +spec PiScheduler.tla +check INVARIANTS TypeOK SealedOnlyAgainstCutRun FreshRunNotRefused StartSurvivesRuns +reach StoppedLoopRearmed | host=gap +run host=gap holds w=3 l=2 | MaxWakeups=3 MaxLoops=2 CompactCmd=FALSE StartGap=TRUE StartRace=FALSE +spec PiScheduler.tla +check INVARIANTS NoRearmByCutRun +run finding rearm-after-stop-in-gap w=2 l=1 | MaxWakeups=2 MaxLoops=1 CompactCmd=FALSE StartGap=TRUE StartRace=FALSE + +# Another run starts while a /loop start waits in the slot. +spec PiScheduler.tla +check INVARIANTS TypeOK NoRearmByCutRun SealedOnlyAgainstCutRun FreshRunNotRefused +run host=race holds w=3 l=2 | MaxWakeups=3 MaxLoops=2 CompactCmd=FALSE StartGap=FALSE StartRace=TRUE +spec PiScheduler.tla +check INVARIANTS StartSurvivesRuns +run finding loop-start-replaced w=2 l=1 | MaxWakeups=2 MaxLoops=1 CompactCmd=FALSE StartGap=FALSE StartRace=TRUE diff --git a/tla/PiScheduler.mutations b/tla/PiScheduler.mutations new file mode 100644 index 000000000..9f244df42 --- /dev/null +++ b/tla/PiScheduler.mutations @@ -0,0 +1,127 @@ +# Mutations for mutate.py. Each one swaps a piece of PiScheduler.tla for a +# plausible bug in schedule.ts and names the one property that must fail on +# it. Each mutation runs on one host where the unmutated spec passes every +# property it checks, strict unless the bug needs CompactCmd, so a DETECTED +# line is the mutation's doing. + +mutation scheduleWakeup does not cancel the pending wakeup first +detects OneWakeup +only strict w=2 +- /\ wtimer' = [Cleared EXCEPT ![nextW] = "armed"] ++ /\ wtimer' = [wtimer EXCEPT ![nextW] = "armed"] + +mutation the idle retry does not keep its new timer handle +detects TimersOwned +only strict w=2 +- /\ slot' = w ++ /\ slot' = None + +mutation cancel does not stop a timer whose delay has already passed +detects NoCancelledDelivery +only strict w=2 +- Cleared == [w \in W |-> IF w = slot THEN "off" ELSE wtimer[w]] ++ Cleared == [w \in W |-> IF w = slot /\ wtimer[w] = "armed" THEN "off" ELSE wtimer[w]] + +mutation schedule_wakeup ignores sealed +detects NoRearmByCutRun +only strict w=2 +- LET refused == sealed IN ++ LET refused == FALSE IN + +mutation agent_settled does not clear sealed +detects SealedOnlyAgainstCutRun +only strict w=2 +- /\ sealed' = FALSE ++ /\ sealed' = sealed + +mutation stopAll seals whether or not a run is in flight +detects FreshRunNotRefused +only strict w=2 +- /\ sealed' = (sealed \/ running) ++ /\ sealed' = TRUE + +# The bug before the fix: a /loop during a manual compaction sealed the slot +# against a run that was not there, and the next run was refused. +mutation /loop seals whenever the session is not idle, compaction included +detects FreshRunNotRefused +only host=compact +- /\ sealed' = (sealed \/ running) ++ /\ sealed' = (sealed \/ midRun) + +mutation /loop seals during a compaction, so the loop it starts cannot re-arm +detects reach:CompactionLoopRearmed +only host=compact +- /\ sealed' = (sealed \/ running) ++ /\ sealed' = (sealed \/ midRun) + +mutation agent_settled does not clear the running flag +detects SealedOnlyAgainstCutRun +only host=compact +- /\ running' = FALSE ++ /\ running' = running + +mutation stop: true cancels a sealed slot +detects StartSurvivesRuns +only strict w=2 +- LET guarded == sealed IN ++ LET guarded == FALSE IN + +mutation a fixed /loop does not end the self-paced loop +detects OneLoopKind +only strict w=2 +- /\ selfPaced' = (c = "dynamic") ++ /\ selfPaced' = (c = "dynamic" \/ (c = "fixed" /\ selfPaced)) + +mutation session_shutdown leaves the interval running +detects StoppedIsTerminal +only strict w=2 +- /\ Cancel /\ StopLoop ++ /\ Cancel /\ UNCHANGED loopVars + +mutation deliver does not check that the session is idle +detects IdleDelivery +only strict w=2 +- /\ ~down /\ wtimer[w] = "due" /\ Idle ++ /\ ~down /\ wtimer[w] = "due" + +mutation stopLoop clears the field but not the interval +detects NothingFiresAfterStop +only strict w=2 +- StoppedLoop == [l \in L |-> IF l = loop THEN "off" ELSE ltimer[l]] ++ StoppedLoop == ltimer + +mutation a wakeup that finds the session busy is dropped, not re-armed +detects WakeupDelivered +only strict w=2 +- /\ wtimer' = [wtimer EXCEPT ![w] = "armed"] ++ /\ wtimer' = [wtimer EXCEPT ![w] = "off"] + +# The next two are not code bugs. They weaken assumption A8 and show that +# each strong-fairness conjunct is needed: with weak fairness a session that +# is busy at every expiry, or that turns busy before every callback, starves +# the wakeup. +mutation only weak fairness on delivery +detects WakeupDelivered +only strict w=2 +- /\ SF_vars(DeliverFire(w)) ++ /\ WF_vars(DeliverFire(w)) + +mutation only weak fairness on expiry while idle +detects WakeupDelivered +only strict w=2 +- /\ SF_vars(Idle /\ Expire(w)) ++ /\ WF_vars(Idle /\ Expire(w)) + +# Not a code bug: it removes the model's bound on scheduleWakeup calls, to +# show that TypeOK constrains the state. +mutation the bound on wakeup ids is not enforced +detects TypeOK +only strict w=2 +- /\ nextW <= MaxWakeups ++ /\ TRUE + +mutation /loop during a run fires at once and does not take the slot +detects reach:StartBehindCutRun +only strict w=2 +- /\ IF c # "stop" /\ midRun THEN Arm(IF running THEN "cmd" ELSE "compact", "plain") ELSE Cancel ++ /\ IF FALSE THEN Arm("cmd", "plain") ELSE Cancel diff --git a/tla/PiScheduler.tla b/tla/PiScheduler.tla new file mode 100644 index 000000000..0fb0a6011 --- /dev/null +++ b/tla/PiScheduler.tla @@ -0,0 +1,343 @@ +---- MODULE PiScheduler ---- +(* The Pi wakeup and /loop scheduler: one pending wakeup, one fixed-interval + loop and one self-paced loop per session, and the `sealed` flag that stops + a run the user cut off from re-arming the loop. + + Source followed: + plugins/pstack/pi/schedule.ts:23-93 class Scheduler + plugins/pstack/pi/schedule.ts:131-226 registerSchedule: the + schedule_wakeup tool, the + agent_start and agent_settled + handlers, /loop + plugins/pstack/pi/index.ts:29-34 session_shutdown calls stopAll() + + One Node.js event loop runs every task to completion, so each action below + is one task: a timer callback, a tool call, a command handler, an event + handler. There is no lock and no program counter per thread. The state a + thread model would keep in a pc is in `run` (the session's run) and in the + timer tables. + + A wakeup that finds the session busy re-arms itself (schedule.ts:49-52). + It waits in `wtimer` as "armed" or "due" and only its own timer wakes it. + The user can always type another command, so a wakeup that is dropped while + it waits never shows as a TLC deadlock. It shows as a violation of the + liveness property WakeupDelivered. + + Assumptions. The code does not guarantee these; the Pi host or Node does. + A1 clearTimeout and clearInterval stop a timer whose delay has passed but + whose callback has not started (Node's timer lists). + A2 schedule_wakeup runs only while a run is active, and the active run is + the only caller. + A3 ctx.isIdle() is false exactly while a run is active or a manual + compaction is in progress (Pi 1.0.0, agent-session.js:1038-1039). + A4 Every active run ends, and Pi then clears the run and calls the + agent_settled handler with no timer or input task between the two + (agent-session.js:671-677, 1369-1377). + A5 Not one-shot mode: oneShot.exits(ctx) is false, so schedule.ts:155-157 + and 199-202 are not modelled. + A6 A wakeup or loop prompt is plain text or "/loop <prompt>". A prompt + that is itself "/loop stop" or "/loop 5m ..." is not modelled. + A7 No scheduler call follows session_shutdown. + A8 Strong fairness, for WakeupDelivered only: the session is idle at some + expiry of the wakeup's timer, and the callback then runs before a new + run starts. In real time: the session stays idle for IDLE_RETRY_MS. + Weak fairness is not enough. The mutation "only weak fairness" in + PiScheduler.mutations gives the starvation trace. + A9 Pi emits agent_start to extensions in the same task chain that sets + its run-active flag (agent-session.js:1352, then pi-agent-core + agent-loop.js:50 before any model call), and agent_settled follows + every run, aborted or failed (the finally at agent-session.js:1369- + 1377). So the extension's `running` flag is TRUE exactly while + run = "active", and no schedule_wakeup call precedes agent_start. + + Three constants switch on host behaviour that the comments in schedule.ts + assume away. With all three FALSE every property holds. + CompactCmd the user can run /loop while a manual compaction is in + progress (Pi interactive-mode.js:2652-2658 does this). + StartGap a prompt that fire() sends becomes an active run in a later + task, so isIdle() stays true in between (Pi's prompt() awaits + hooks before agent-session.js:1352). + StartRace another run can start while a /loop start waits in the slot: + the user types a prompt, or an interval tick lands, before the + wakeup's next retry. *) +EXTENDS Naturals, FiniteSets + +CONSTANTS MaxWakeups, MaxLoops, CompactCmd, StartGap, StartRace + +None == 0 +W == 1..MaxWakeups \* one id per scheduleWakeup call, which is one deliver closure +L == 1..MaxLoops \* one id per startLoop call + +VARIABLES + slot, loop, selfPaced, sealed, \* Scheduler fields, schedule.ts:24-36 + running, \* Scheduler.running: a run is in flight, schedule.ts:36 + wtimer, ltimer, \* Node's timers: "off", "armed", or "due" (delay passed, callback not yet run) + run, compacting, down, \* the Pi session + startedBy, \* the wakeup whose prompt started the current run, else None + nextW, nextL, wby, wkind, \* next free id; who scheduled each wakeup ("cmd": /loop during a run, + \* "compact": /loop during a compaction) and what its prompt is + fired, cancelled, \* history: wakeups delivered; wakeups cancelled or replaced + cut, \* history: the user ran /loop while the current run was in flight + quiet, \* history: the last /loop was "stop" and no run was in flight + refusedFresh, startStolen \* history: a run that was not cut was refused; a run cancelled a /loop start + +slotVars == <<slot, wtimer, nextW, wby, wkind, cancelled>> +loopVars == <<loop, ltimer, nextL>> +session == <<run, compacting, down, startedBy>> +history == <<fired, cut, quiet, refusedFresh, startStolen>> +vars == <<slotVars, loopVars, selfPaced, sealed, running, session, history>> + +Init == /\ slot = None /\ loop = None /\ selfPaced = FALSE /\ sealed = FALSE /\ running = FALSE + /\ wtimer = [w \in W |-> "off"] /\ ltimer = [l \in L |-> "off"] + /\ run = "none" /\ compacting = FALSE /\ down = FALSE /\ startedBy = None + /\ nextW = 1 /\ nextL = 1 + /\ wby = [w \in W |-> "tool"] /\ wkind = [w \in W |-> "plain"] + /\ fired = {} /\ cancelled = {} + /\ cut = FALSE /\ quiet = FALSE /\ refusedFresh = FALSE /\ startStolen = FALSE + +Idle == run # "active" /\ ~compacting \* ctx.isIdle() +Started == IF StartGap THEN "transit" ELSE "active" \* the run state after fire() on an idle session +Pending == IF slot = None THEN {} ELSE {slot} +StartPending == slot # None /\ wby[slot] # "tool" \* a /loop start waits in the slot +LiveW == {w \in W : wtimer[w] # "off"} +LiveL == {l \in L : ltimer[l] # "off"} + +\* clearTimeout(this.wakeup), schedule.ts:62, and clearInterval(this.loop), :75. +Cleared == [w \in W |-> IF w = slot THEN "off" ELSE wtimer[w]] +StoppedLoop == [l \in L |-> IF l = loop THEN "off" ELSE ltimer[l]] + +\* cancelWakeup, schedule.ts:60-65. +Cancel == /\ wtimer' = Cleared /\ slot' = None + /\ cancelled' = cancelled \cup Pending + /\ UNCHANGED <<nextW, wby, wkind>> + +\* scheduleWakeup, schedule.ts:44-58: cancel, arm a new timer, keep its handle. +Arm(by, kind) == + /\ nextW <= MaxWakeups + /\ wtimer' = [Cleared EXCEPT ![nextW] = "armed"] + /\ slot' = nextW /\ nextW' = nextW + 1 + /\ wby' = [wby EXCEPT ![nextW] = by] /\ wkind' = [wkind EXCEPT ![nextW] = kind] + /\ cancelled' = cancelled \cup Pending + +\* stopLoop, schedule.ts:73-78, and startLoop after it, :67-71. +StopLoop == ltimer' = StoppedLoop /\ loop' = None /\ UNCHANGED nextL +StartLoop == /\ nextL <= MaxLoops + /\ ltimer' = [StoppedLoop EXCEPT ![nextL] = "armed"] + /\ loop' = nextL /\ nextL' = nextL + 1 + +\* fire, schedule.ts:40-42, on an idle session. With no gap the run is active +\* at once and Pi emits agent_start (A9), schedule.ts:169-171. +Fire(by) == run' = Started /\ running' = (Started = "active") /\ startedBy' = by + +\* The user types /loop stop, /loop <interval> <prompt> ("fixed") or +\* /loop <prompt> ("dynamic"): schedule.ts:181-224. The first prompt waits +\* whenever the session is not idle; the seal needs a run in flight. +Cmd(c) == + LET midRun == ~Idle IN \* :183 + /\ ~down + /\ compacting => CompactCmd + /\ sealed' = (sealed \/ running) \* :90, :209 + /\ selfPaced' = (c = "dynamic") \* :89, :81 + /\ IF c = "fixed" THEN StartLoop ELSE StopLoop \* :91, :214 + /\ IF c # "stop" /\ midRun THEN Arm(IF running THEN "cmd" ELSE "compact", "plain") ELSE Cancel \* :87, :190-191 + /\ IF c # "stop" /\ ~midRun THEN Fire(None) ELSE UNCHANGED <<run, running, startedBy>> \* :195-197 + \* A start that lands in the StartGap window sends its first prompt into the + \* run in transit. That run's re-arm may then be the new loop's own, so only + \* a stop marks the run in transit as cut. + /\ cut' = (cut \/ run = "active" \/ (run = "transit" /\ c = "stop")) + /\ quiet' = (c = "stop" /\ run = "none") + /\ UNCHANGED <<compacting, down, fired, refusedFresh, startStolen>> + +\* The active run calls schedule_wakeup with a delay and a prompt: schedule.ts:145-165. +ToolSchedule(kind) == + LET refused == sealed IN \* :148 + /\ run = "active" /\ ~down + /\ IF refused + THEN /\ refusedFresh' = (refusedFresh \/ ~cut) + /\ UNCHANGED <<slotVars, startStolen>> + ELSE /\ Arm("tool", kind) \* :159 + /\ startStolen' = (startStolen \/ StartPending) + /\ UNCHANGED refusedFresh + /\ UNCHANGED <<loopVars, selfPaced, sealed, running, session, fired, cut, quiet>> + +\* The active run calls schedule_wakeup with stop: true: schedule.ts:139-144. +ToolStop == + LET guarded == sealed IN \* :141 + /\ run = "active" /\ ~down + /\ IF guarded + THEN /\ refusedFresh' = (refusedFresh \/ (~cut /\ slot # None)) + /\ UNCHANGED <<slotVars, startStolen>> + ELSE /\ Cancel + /\ startStolen' = (startStolen \/ StartPending) + /\ UNCHANGED refusedFresh + /\ UNCHANGED <<loopVars, selfPaced, sealed, running, session, fired, cut, quiet>> + +\* Node: the wakeup timer's delay passes and its callback is queued. +Expire(w) == + /\ ~down /\ wtimer[w] = "armed" + /\ wtimer' = [wtimer EXCEPT ![w] = "due"] + /\ UNCHANGED <<slot, nextW, wby, wkind, cancelled, loopVars, selfPaced, sealed, running, session, history>> + +\* deliver on a busy session, schedule.ts:49-52: re-arm and keep the new handle. +Retry(w) == + /\ ~down /\ wtimer[w] = "due" /\ ~Idle + /\ wtimer' = [wtimer EXCEPT ![w] = "armed"] + /\ slot' = w + /\ UNCHANGED <<nextW, wby, wkind, cancelled, loopVars, selfPaced, sealed, running, session, history>> + +\* deliver on an idle session, schedule.ts:53-54. A "/loop <prompt>" prompt is +\* the self-paced re-arm: Pi runs the /loop handler in place, :219-222. +DeliverFire(w) == + /\ ~down /\ wtimer[w] = "due" /\ Idle + /\ wtimer' = [wtimer EXCEPT ![w] = "off"] + /\ slot' = None + /\ fired' = fired \cup {w} + /\ Fire(w) + /\ IF wkind[w] = "loop" THEN selfPaced' = TRUE /\ StopLoop + ELSE UNCHANGED <<selfPaced, loopVars>> + /\ quiet' = FALSE + /\ UNCHANGED <<nextW, wby, wkind, cancelled, sealed, compacting, down, cut, refusedFresh, startStolen>> + +\* Node: the interval's delay passes; then its callback, schedule.ts:69, which +\* fires on an idle session and drops the tick on a busy one. +LExpire(l) == + /\ ~down /\ ltimer[l] = "armed" + /\ ltimer' = [ltimer EXCEPT ![l] = "due"] + /\ UNCHANGED <<loop, nextL, slotVars, selfPaced, sealed, running, session, history>> + +TickFire(l) == + /\ ~down /\ ltimer[l] = "due" /\ Idle + /\ StartRace \/ ~StartPending + /\ ltimer' = [ltimer EXCEPT ![l] = "armed"] + /\ Fire(None) + /\ quiet' = FALSE + /\ UNCHANGED <<loop, nextL, slotVars, selfPaced, sealed, compacting, down, fired, cut, refusedFresh, startStolen>> + +TickDrop(l) == + /\ ~down /\ ltimer[l] = "due" /\ ~Idle + /\ ltimer' = [ltimer EXCEPT ![l] = "armed"] + /\ UNCHANGED <<loop, nextL, slotVars, selfPaced, sealed, running, session, history>> + +\* The user types a prompt on an idle session. +UserPrompt == + /\ ~down /\ run = "none" /\ ~compacting + /\ StartRace \/ ~StartPending + /\ Fire(None) + /\ quiet' = FALSE + /\ UNCHANGED <<slotVars, loopVars, selfPaced, sealed, compacting, down, fired, cut, refusedFresh, startStolen>> + +\* With StartGap: the prompt in transit becomes the active run and Pi emits +\* agent_start: schedule.ts:169-171. +RunStart == + /\ ~down /\ run = "transit" + /\ run' = "active" + /\ running' = TRUE + /\ UNCHANGED <<slotVars, loopVars, selfPaced, sealed, compacting, down, startedBy, history>> + +\* The run settles and Pi emits agent_settled: schedule.ts:172-175. +RunSettle == + /\ ~down /\ run = "active" + /\ run' = "none" /\ startedBy' = None + /\ running' = FALSE + /\ sealed' = FALSE + /\ cut' = FALSE + /\ UNCHANGED <<slotVars, loopVars, selfPaced, compacting, down, fired, quiet, refusedFresh, startStolen>> + +\* Manual compaction. Pi aborts the run first, so none is in flight. +CompactStart == + /\ ~down /\ run = "none" /\ ~compacting + /\ compacting' = TRUE + /\ UNCHANGED <<slotVars, loopVars, selfPaced, sealed, running, run, down, startedBy, history>> + +CompactEnd == + /\ ~down /\ compacting + /\ compacting' = FALSE + /\ UNCHANGED <<slotVars, loopVars, selfPaced, sealed, running, run, down, startedBy, history>> + +\* session_shutdown: stopAll() with midRun false, index.ts:29-34. +Shutdown == + /\ ~down + /\ down' = TRUE + /\ Cancel /\ StopLoop + /\ selfPaced' = FALSE + /\ UNCHANGED <<sealed, running, run, compacting, startedBy, history>> + +\* Stutter after shutdown so that the end is not reported as a deadlock. +Done == down /\ UNCHANGED vars + +Next == \/ \E c \in {"stop", "fixed", "dynamic"} : Cmd(c) + \/ \E k \in {"plain", "loop"} : ToolSchedule(k) + \/ ToolStop + \/ \E w \in W : Expire(w) \/ Retry(w) \/ DeliverFire(w) + \/ \E l \in L : LExpire(l) \/ TickFire(l) \/ TickDrop(l) + \/ UserPrompt \/ RunStart \/ RunSettle + \/ CompactStart \/ CompactEnd \/ Shutdown \/ Done + +\* Runs start and end and compaction ends. No fairness on anything the user or +\* the agent chooses to do. The two strong conjuncts are assumption A8. +Fairness == /\ WF_vars(RunStart) /\ WF_vars(RunSettle) /\ WF_vars(CompactEnd) + /\ \A w \in W : /\ WF_vars(Retry(w)) + /\ SF_vars(Idle /\ Expire(w)) + /\ SF_vars(DeliverFire(w)) + +Spec == Init /\ [][Next]_vars /\ Fairness + +TypeOK == + /\ slot \in W \cup {None} /\ loop \in L \cup {None} + /\ selfPaced \in BOOLEAN /\ sealed \in BOOLEAN /\ running \in BOOLEAN + /\ wtimer \in [W -> {"off", "armed", "due"}] /\ ltimer \in [L -> {"off", "armed", "due"}] + /\ run \in {"none", "transit", "active"} /\ (run = "transit" => StartGap) + /\ running = (run = "active") \* A9 + /\ compacting \in BOOLEAN /\ down \in BOOLEAN + /\ startedBy \in W \cup {None} /\ (run = "none" => startedBy = None) + /\ nextW \in 1..(MaxWakeups + 1) /\ nextL \in 1..(MaxLoops + 1) + /\ wby \in [W -> {"tool", "cmd", "compact"}] /\ wkind \in [W -> {"plain", "loop"}] + /\ fired \subseteq W /\ cancelled \subseteq W + /\ cut \in BOOLEAN /\ quiet \in BOOLEAN + /\ refusedFresh \in BOOLEAN /\ startStolen \in BOOLEAN + +\* (1) At most one wakeup timer is alive. +OneWakeup == Cardinality(LiveW) <= 1 +\* Ownership: a timer is alive exactly when the Scheduler field holds its handle. +TimersOwned == LiveW = Pending /\ LiveL = (IF loop = None THEN {} ELSE {loop}) +\* (2) A wakeup that was cancelled or replaced never delivers its prompt. +NoCancelledDelivery == fired \cap cancelled = {} +\* (3) A run the user cut off with /loop never has a wakeup of its own pending. +NoRearmByCutRun == cut => (slot = None \/ wby[slot] = "cmd") +\* (3) sealed is set only against a run that /loop cut off, and is clear once that run settles... +SealedOnlyAgainstCutRun == sealed => (run = "active" /\ cut) +\* ...so a run that started after the /loop command is never refused. +FreshRunNotRefused == ~refusedFresh +\* The /loop start that waits in the slot is cancelled only by /loop or shutdown, never by a run. +StartSurvivesRuns == ~startStolen +\* The fixed loop and the self-paced loop are never both live. +OneLoopKind == ~(loop # None /\ selfPaced) +\* (5), terminal state: after /loop stop with no run in flight, and after +\* shutdown, no timer is pending and no loop is live. +Terminal == slot = None /\ loop = None /\ ~selfPaced /\ LiveW = {} /\ LiveL = {} +StoppedIsTerminal == (quiet \/ down) => Terminal +\* (4) The wakeup path delivers only to an idle session. +IdleDelivery == [][fired' # fired => Idle]_vars +\* (5) After /loop stop with no run in flight, nothing fires again. +Fires == fired' # fired \/ \E l \in L : TickFire(l) +NothingFiresAfterStop == [][quiet => ~Fires]_vars +\* (6) A pending wakeup is delivered unless it is cancelled or replaced. +WakeupDelivered == \A w \in W : (wtimer[w] # "off") ~> (w \in fired \/ w \in cancelled) + +\* Reachable states the properties depend on (reach lines in PiScheduler.matrix). +ParkedWakeup == \E w \in W : wtimer[w] = "due" /\ ~Idle \* a due wakeup waits for idle +DueWhileIdle == \E w \in W : wtimer[w] = "due" /\ Idle \* a stop can land before the callback +TickWhileBusy == \E l \in L : ltimer[l] = "due" /\ ~Idle \* a tick about to be dropped +StartBehindCutRun == sealed /\ cut /\ StartPending \* a /loop start waits behind the run it cut +LoopAndWakeup == loop # None /\ slot # None \* the interval and a wakeup both alive +SelfPacedRearm == selfPaced /\ slot # None /\ wkind[slot] = "loop" \* the self-paced loop's own re-arm pending +StoppedAfterDelivery == quiet /\ fired # {} \* /loop stop on an idle session that had fired +ShutdownCancelled == down /\ cancelled # {} \* shutdown cancelled a pending wakeup +AllDelivered == W # {} /\ fired = W \* every wakeup the bound allows was delivered +\* The consequence of each finding on the host that allows it. +\* CompactCmd: a self-paced loop started during a compaction is in its first +\* iteration and that run's own re-arm is pending. +CompactionLoopRearmed == SelfPacedRearm /\ startedBy # None /\ wby[startedBy] = "compact" +\* StartGap: a wakeup fired, the user stopped the loop, and the run re-armed it. +StoppedLoopRearmed == cut /\ fired # {} /\ slot # None /\ wby[slot] = "tool" /\ wkind[slot] = "loop" +==== From de804764083769e5ed6c039b93abdfee167c40ca Mon Sep 17 00:00:00 2001 From: Michael Denyer <97485362+michael-denyer@users.noreply.github.com> Date: Thu, 8 Oct 2026 22:05:15 +0100 Subject: [PATCH 07/11] fix(orch): release the store lock behind the takeover claim (#238) The release read the lock, saw its own pid, and unlinked the lock by path as a separate step. A forced takeover between the two replaced the lock, and the release deleted the replacement, which left the forced writer with no lock on disk. The release now runs its read and unlink behind the claim a takeover holds, and waits for a live claimant to finish. The TLA+ model of the lock comes with the change; two of its runs still fail by design and record the empty lock a filesystem without hard links allows. --- .../poteto-mode/scripts/orch/orch.test.ts | 34 ++ .../skills/poteto-mode/scripts/orch/store.ts | 52 ++- tla/OrchStoreLock.matrix | 84 +++++ tla/OrchStoreLock.mutations | 119 ++++++ tla/OrchStoreLock.tla | 351 ++++++++++++++++++ tools/forks.json | 4 +- 6 files changed, 627 insertions(+), 17 deletions(-) create mode 100644 tla/OrchStoreLock.matrix create mode 100644 tla/OrchStoreLock.mutations create mode 100644 tla/OrchStoreLock.tla diff --git a/plugins/pstack/skills/poteto-mode/scripts/orch/orch.test.ts b/plugins/pstack/skills/poteto-mode/scripts/orch/orch.test.ts index 8faa38159..2b476c33d 100644 --- a/plugins/pstack/skills/poteto-mode/scripts/orch/orch.test.ts +++ b/plugins/pstack/skills/poteto-mode/scripts/orch/orch.test.ts @@ -588,6 +588,40 @@ await store.close(); expect(await lockFiles(directory)).toEqual([]); }); + it("refuses a forced writer that arrives while the holder's release is between its read and its unlink", async () => { + const { directory, store } = await initializedStore(); + await store.close(); + const flags = await makeDirectory(); + + const a = spawnWriter({ + directory, + flags, + name: "A", + atLockUnlink: `writeFileSync(flag("releasing"), ""); + spin(() => peer("F.held") || peer("F.refused"));`, + }); + const f = spawnWriter({ + directory, + flags, + name: "F", + force: true, + before: `spin(() => peer("A.releasing"));`, + }); + expect(await Promise.all([a.exited, f.exited])).toEqual([0, 0]); + expect(await new Response(a.stderr).text()).toBe(""); + expect(await new Response(f.stderr).text()).toBe(""); + + expect(await readFlags(flags)).toEqual({ + "A.releasing": "", + "A.held": "", + "F.refused": `store lock held by pid ${a.pid} is being replaced by another writer; retry`, + }); + expect(await readFile(join(directory, "units.tsv"), "utf8")).toBe( + "id\ttrack\tstate\tbranch\tpr\tsha\tbrief\nA-unit\trace\tpending\t\t\t\t\n" + ); + expect(await lockFiles(directory)).toEqual([]); + }); + it("replaces a stale lock after a writer was killed while replacing it", async () => { const { directory, store } = await initializedStore(); await store.close(); diff --git a/plugins/pstack/skills/poteto-mode/scripts/orch/store.ts b/plugins/pstack/skills/poteto-mode/scripts/orch/store.ts index 1c7be01e6..439c85331 100644 --- a/plugins/pstack/skills/poteto-mode/scripts/orch/store.ts +++ b/plugins/pstack/skills/poteto-mode/scripts/orch/store.ts @@ -441,12 +441,14 @@ async function acquireLock( } }; - // POSIX cannot unlink a file only if its content still matches, so the - // re-read and replace run behind a claim: a directory that holds one file + // POSIX cannot unlink a file only if its content still matches, so each + // read-and-unlink runs behind a claim: a directory that holds one file // named for the claimant's pid. rename cannot replace a directory that // holds a file, so a claim excludes every other writer, and a dead // claimant's file is removed by name, which cannot remove a live claimant's. - const takeOver = async (holder: string): Promise<void> => { + // Resolves to false, without running the body, when a live writer holds + // the claim. + const claimed = async (body: () => Promise<void>): Promise<boolean> => { const staged = `${takeover}.${pid}.${randomUUID()}`; const discard = (): Promise<void> => rm(staged, { recursive: true, force: true }).catch(() => {}); @@ -465,11 +467,19 @@ async function acquireLock( if (code !== "ENOTEMPTY" && code !== "EEXIST") { throw error; } - throw new UserError( - `store lock held by pid ${holder} is being replaced by another writer; retry` - ); + return false; } try { + await body(); + } finally { + await rename(takeover, staged).catch(() => {}); + await discard(); + } + return true; + }; + + const takeOver = async (holder: string): Promise<void> => { + const replaced = await claimed(async () => { try { const current = (await readFile(path, "utf8")).trim() || "unknown"; if (current !== holder) { @@ -485,9 +495,11 @@ async function acquireLock( if (blocker !== null) { throw new UserError(`store lock held by pid ${blocker}`); } - } finally { - await rename(takeover, staged).catch(() => {}); - await discard(); + }); + if (!replaced) { + throw new UserError( + `store lock held by pid ${holder} is being replaced by another writer; retry` + ); } }; @@ -504,15 +516,25 @@ async function acquireLock( } } + // A claimant that refuses the release is replacing the lock. It finishes or + // dies, and the next claim sweeps a dead one, so the release tries again. return async (): Promise<void> => { - try { - if ((await readFile(path, "utf8")).trim() === pid) { - await unlink(path); + const removeOwn = async (): Promise<void> => { + try { + if ((await readFile(path, "utf8")).trim() === pid) { + await unlink(path); + } + } catch (error) { + if (errorCode(error) !== "ENOENT") { + throw error; + } } - } catch (error) { - if (errorCode(error) !== "ENOENT") { - throw error; + }; + for (;;) { + if (await claimed(removeOwn)) { + return; } + await new Promise((resolve) => setTimeout(resolve, 10)); } }; } diff --git a/tla/OrchStoreLock.matrix b/tla/OrchStoreLock.matrix new file mode 100644 index 000000000..d59aa636f --- /dev/null +++ b/tla/OrchStoreLock.matrix @@ -0,0 +1,84 @@ +# Matrix for OrchStoreLock.tla, the pid lock of the orch store (store.ts +# acquireLock, lines 394-540). Run it with tlc-matrix.sh; OrchStoreLock.mutations +# uses the same checks and runs. +# +# The two sections at the end are counter-examples and are expected to FAIL. +# Each one keeps a property that the shipped code violates without hard +# links, in its own section so that TLC prints one trace per property. The +# sections above them must PASS. + +# Writers without --force on a filesystem with hard links: every property. +spec OrchStoreLock.tla +check INVARIANTS TypeOK OneHolder LockNamesHolder ClaimExclusive NoResidue +check PROPERTIES TakeoverRemovesNamedLock TakeoverSparesLiveHolder ReleaseRemovesOwnLock +check PROPERTIES Terminates CloseTerminates LateWriterAcquires +reach ReleaseWindow | n=1 +reach StaleLock | n=2 +reach StaleLockReplaced | n=2 +reach LockVanished | n=2 +reach DeadClaimant | n=3 +reach EmptyTakeoverDir | n=3 +reach ClaimRefused | n=3 +reach LateWriterMeetsWreck | n=3 +run noforce hardlink n=1 | Procs={p1} Forcers={} HardLinks=TRUE +run noforce hardlink n=2 | Procs={p1,p2} Forcers={} HardLinks=TRUE +run noforce hardlink n=3 | Procs={p1,p2,p3} Forcers={} HardLinks=TRUE + +# Four writers reach the third ENOENT of lockOrHolder and a late writer that +# finds an empty takeover directory. Safety only: the liveness graph of four +# writers has 81 million nodes. +spec OrchStoreLock.tla +check INVARIANTS TypeOK OneHolder LockNamesHolder ClaimExclusive NoResidue +check PROPERTIES TakeoverRemovesNamedLock TakeoverSparesLiveHolder ReleaseRemovesOwnLock +reach RetriesExhausted | n=4 +reach LateWriterMeetsEmptyDir | n=4 +run noforce hardlink safety n=4 | Procs={p1,p2,p3,p4} Forcers={} HardLinks=TRUE + +# No hard links (the exclusive open of line 415): safety and termination. +spec OrchStoreLock.tla +check INVARIANTS TypeOK OneHolder LockNamesHolder ClaimExclusive NoResidue +check PROPERTIES TakeoverRemovesNamedLock TakeoverSparesLiveHolder ReleaseRemovesOwnLock +check PROPERTIES Terminates CloseTerminates +reach OrphanBlankLock | n=2 +run noforce excl n=1 | Procs={p1} Forcers={} HardLinks=FALSE +run noforce excl n=2 | Procs={p1,p2} Forcers={} HardLinks=FALSE +run noforce excl n=3 | Procs={p1,p2,p3} Forcers={} HardLinks=FALSE + +# Writers with --force on a filesystem with hard links: every property. The +# release's read and unlink run behind the claim, so a forced takeover cannot +# sit between them, and no release can sit between a takeover's re-read and +# its unlink. A release that finds the claim taken stages a new one until the +# claimant finishes or dies. +spec OrchStoreLock.tla +check INVARIANTS TypeOK OneHolder LockNamesHolder ClaimExclusive NoResidue +check PROPERTIES TakeoverRemovesNamedLock TakeoverSparesLiveHolder ReleaseRemovesOwnLock +check PROPERTIES Terminates CloseTerminates LateWriterAcquires +reach ForcedSteal | force one n=2 +reach ReleaseBlocked | force one n=2 +run force one n=2 | Procs={p1,p2} Forcers={p2} HardLinks=TRUE +run force one n=3 | Procs={p1,p2,p3} Forcers={p3} HardLinks=TRUE +run force all n=3 | Procs={p1,p2,p3} Forcers={p1,p2,p3} HardLinks=TRUE + +# Writers with --force and no hard links: the properties that hold. The empty +# lock of line 415 reads as "unknown", which a forced writer takes over; see +# the second expected FAIL. +spec OrchStoreLock.tla +check INVARIANTS TypeOK ClaimExclusive NoResidue +check PROPERTIES TakeoverSparesLiveHolder ReleaseRemovesOwnLock +check PROPERTIES Terminates CloseTerminates LateWriterAcquires +run force all excl n=2 | Procs={p1,p2} Forcers={p1,p2} HardLinks=FALSE + +# Expected FAIL 1: without hard links, a writer that dies between the exclusive +# open and the write of line 415 leaves an empty lock. No writer without +# --force can replace it. +spec OrchStoreLock.tla +check PROPERTIES LateWriterAcquires +run finding empty lock, excl n=2 | Procs={p1,p2} Forcers={} HardLinks=FALSE + +# Expected FAIL 2, the same limitation under --force: a forced writer reads the +# lock between the exclusive open and the write of line 415, takes over the +# holder "unknown", re-reads the lock still empty (line 484), and its unlink +# (line 488) removes the pid the holder has written by then. +spec OrchStoreLock.tla +check INVARIANTS LockNamesHolder +run finding forced empty lock, excl n=2 | Procs={p1,p2} Forcers={p1,p2} HardLinks=FALSE diff --git a/tla/OrchStoreLock.mutations b/tla/OrchStoreLock.mutations new file mode 100644 index 000000000..af42914a2 --- /dev/null +++ b/tla/OrchStoreLock.mutations @@ -0,0 +1,119 @@ +# Mutations for mutate.py. Each one swaps a piece of OrchStoreLock.tla for a +# plausible bug in store.ts and names the one property that must fail on it. +# Every `only` line picks a run that passes on the unmutated spec, so a +# DETECTED line comes from the mutation, not from the expected-FAIL run. + +mutation create overwrites an existing lock (line 410 without EEXIST) +detects OneHolder +only noforce hardlink n=2 +- /\ IF lock # NoPid ++ /\ IF FALSE + +mutation takeOver skips the comparison with the holder (line 485) +detects LockNamesHolder +only noforce hardlink n=3 +- /\ Goto(p, IF LockText = holder[p] THEN "unlink" ELSE "unclaim") ++ /\ Goto(p, "unlink") + +mutation the sweep removes a live claimant's file (line 459 without holderIsDead) +detects ClaimExclusive +only noforce hardlink n=3 +- /\ claimants' = IF c \in dead THEN claimants \ {c} ELSE claimants ++ /\ claimants' = claimants \ {c} + +mutation a blocked takeOver keeps its claim (line 475 skipped) +detects NoResidue +only noforce hardlink n=3 +- THEN Goto(p, "unclaim") /\ UNCHANGED holder ++ THEN Goto(p, "failed") /\ UNCHANGED holder + +mutation rename replaces a claimed takeover directory (line 463 never fails) +detects TakeoverRemovesNamedLock +only noforce hardlink n=3 +- /\ IF claimants = {} ++ /\ IF TRUE + +mutation an empty lock counts as stale (holderIsDead true for "unknown") +detects TakeoverSparesLiveHolder +only noforce excl n=2 +- /\ Goto(p, IF holder[p] \in dead \/ p \in Forcers THEN "mkstaged" ELSE "failed") ++ /\ Goto(p, IF holder[p] \in dead \cup {Unknown} \/ p \in Forcers THEN "mkstaged" ELSE "failed") + +mutation every holder counts as stale (line 508 always true) +detects TakeoverSparesLiveHolder +only noforce hardlink n=2 +- /\ Goto(p, IF holder[p] \in dead \/ p \in Forcers THEN "mkstaged" ELSE "failed") ++ /\ Goto(p, "mkstaged") + +mutation takeOver's lockOrHolder starts with three retries (line 425) +detects TypeOK +only noforce hardlink n=2 +- StartCreate(p) == Goto(p, "mkpid") /\ retries' = [retries EXCEPT ![p] = 2] ++ StartCreate(p) == Goto(p, "mkpid") /\ retries' = [retries EXCEPT ![p] = 3] + +mutation an ENOENT retry reads again instead of creating (line 425 loop) +detects Terminates +only noforce hardlink n=2 +- THEN /\ retries' = [retries EXCEPT ![p] = @ - 1] +- /\ Goto(p, "mkpid") ++ THEN /\ retries' = retries ++ /\ Goto(p, "read") + +mutation release waits for the lock instead of unlinking it (line 525) +detects CloseTerminates +only noforce hardlink n=1 +- THEN Goto(p, "relunlink") /\ UNCHANGED got ++ THEN Goto(p, "relread") /\ UNCHANGED got + +# The release window: the read and unlink of lines 524-525 outside the claim. +mutation release reads and unlinks without a claim (close before this fix) +detects ReleaseRemovesOwnLock +only force one n=2 +- /\ Goto(p, "mkstaged") +- /\ UNCHANGED <<lockfile, claim, temps, holder, retries, seen, got, env>> ++ /\ Goto(p, "relread") ++ /\ UNCHANGED <<lockfile, claim, temps, holder, retries, seen, got, env>> + +# The takeover window: a takeover that ignores a release's claim. +mutation a takeover renames over a release's claim (line 463 ignores a releasing claimant) +detects ReleaseRemovesOwnLock +only force one n=2 +- /\ IF claimants = {} ++ /\ IF \A q \in claimants: closing[q] + +mutation a release refused the claim gives up and leaves its lock (line 533 without the loop) +detects NoResidue +only force one n=2 +- THEN IF pc[p] = "busy" THEN "mkstaged" ELSE "released" ++ THEN "released" + +mutation the sweep leaves a dead claimant's file (line 460 skipped), release +detects CloseTerminates +only force one n=2 +- /\ claimants' = IF c \in dead THEN claimants \ {c} ELSE claimants ++ /\ claimants' = claimants + +mutation the sweep leaves a dead claimant's file (line 460 skipped) +detects LateWriterAcquires +only noforce hardlink n=3 +- /\ claimants' = IF c \in dead THEN claimants \ {c} ELSE claimants ++ /\ claimants' = claimants + +# The assumption that rename replaces an empty directory. +mutation rename cannot replace an empty takeover directory (not POSIX) +detects LateWriterAcquires +only noforce hardlink n=3 +- /\ IF claimants = {} ++ /\ IF ~tdir + +mutation a crash leaves no dead pid +detects reach:StaleLock +only noforce hardlink n=2 +- /\ dead' = dead \cup {p} ++ /\ dead' = dead + +mutation the sweep leaves a dead claimant's file (line 460 skipped), reach +detects reach:EmptyTakeoverDir +only noforce hardlink n=3 +- /\ claimants' = IF c \in dead THEN claimants \ {c} ELSE claimants ++ /\ claimants' = claimants diff --git a/tla/OrchStoreLock.tla b/tla/OrchStoreLock.tla new file mode 100644 index 000000000..869c5bf7c --- /dev/null +++ b/tla/OrchStoreLock.tla @@ -0,0 +1,351 @@ +---- MODULE OrchStoreLock ---- +(* The pid lock of the orch store, as N writer processes run it. + + Source: plugins/pstack/skills/poteto-mode/scripts/orch/store.ts + holderIsDead lines 381-392 + acquireLock lines 394-540 + create lines 406-419 + lockOrHolder lines 424-442 + claimed lines 451-479 + takeOver lines 481-504 + the release function it returns lines 521-539 + openStore: ensureLock, beginWrite lines 1352-1378 + openStore: close lines 1729-1746 + and plugins/pstack/skills/poteto-mode/scripts/orch/orch.ts lines 219-240, + where one CLI process opens the store, runs one operation and closes it. + + Each action is one filesystem call, labelled with its line. A writer never + waits for the lock: a writer that cannot get it ends at "failed", which is + the UserError of lines 486, 496, 500 or 515. The release does wait for the + claim: it stages a new one (line 533) until the claimant finishes or dies. + Crash stops a writer at any step and leaves its files in place. A crash is + the only way to leave a step without running the finally blocks of lines + 417 and 474-477. + + `lock` is the lock file's actual owner. `got` is each writer's belief that + it holds the lock. The two can differ, and the properties compare them. + + Assumptions the code does not guarantee: + - A pid is never reused. A dead writer's pid stays dead, and no live + process carries a pid found in a stale lock or claim. + - process.kill(pid, 0) reports ESRCH exactly for a dead writer. A writer + of another user (EPERM) counts as live, as in line 390. + - Each filesystem call is atomic and every writer sees its effect at once, + as on a local POSIX filesystem. NFS and other caching filesystems are + outside the model. + - rename replaces an empty directory and fails with ENOTEMPTY or EEXIST on + one that holds a file (POSIX). The mutations file shows that property + LateWriterAcquires depends on the first half. + - With HardLinks = FALSE, link always fails with a code other than EEXIST + and line 415 runs as two steps: the exclusive open, then the write. + - No call fails for a reason outside the protocol (EACCES, ENOSPC, EIO). + - Each process calls acquireLock once and opens one store on the directory. + orch.ts does this. openStore itself lets a failed write retry (line 1365). + - The randomUUID names of lines 407 and 452 never collide. + - A live claimant keeps running. A claimant stopped for good (SIGSTOP) + would hold the release in its retry loop; weak fairness rules that out. + + Merged steps. Lines 455 and 457 touch only the private staged directory + and are one step. The liveness test and the rm of lines 459-460 are one + step: a dead pid stays dead, so the test cannot go stale before the rm. + The comparisons of lines 485 and 524 are part of the read that feeds them. + + `stolen` is a ghost variable: the owners of the locks that a takeover + removed while the lock still named the holder it was called for. Such a + writer was reported through onStaleLock or onLockStolen (lines 509, 512). *) +EXTENDS Naturals, FiniteSets + +CONSTANTS Procs, \* the writer pids + Forcers, \* the writers run with --force (options.force) + HardLinks \* FALSE: the filesystem has no hard links (line 415) + +NoPid == "none" \* no lock file; no holder read yet +Unknown == "unknown" \* the text read from an empty or vanished lock + +VARIABLES lock, blank, \* .orch.lock: its creator, and whether it is still empty + tdir, claimants, \* .orch.lock.takeover: exists, and the pid files in it + pidfile, staged, \* the writers whose private pid file / staged directory exists + pc, holder, retries, seen, got, closing, + dead, stolen + +lockfile == <<lock, blank>> +claim == <<tdir, claimants>> +temps == <<pidfile, staged>> +locals == <<holder, retries, seen, got, closing>> +env == <<dead, stolen>> +vars == <<lockfile, claim, temps, pc, locals, env>> + +Init == /\ lock = NoPid /\ blank = FALSE + /\ tdir = FALSE /\ claimants = {} + /\ pidfile = {} /\ staged = {} + /\ pc = [p \in Procs |-> "mkpid"] + /\ holder = [p \in Procs |-> NoPid] + /\ retries = [p \in Procs |-> 2] + /\ seen = [p \in Procs |-> {}] + /\ got = [p \in Procs |-> FALSE] + /\ closing = [p \in Procs |-> FALSE] + /\ dead = {} /\ stolen = {} + +At(p, label) == p \notin dead /\ pc[p] = label +Goto(p, label) == pc' = [pc EXCEPT ![p] = label] +\* What readFile returns for a lock file that exists (lines 435, 474). +LockText == IF blank THEN Unknown ELSE lock +\* takeOver is running: its holder argument is set. +InTake(p) == holder[p] # NoPid +\* takeOver enters lockOrHolder with a fresh retry count (line 494). +StartCreate(p) == Goto(p, "mkpid") /\ retries' = [retries EXCEPT ![p] = 2] + +\* create, line 408: write the private pid file. +MkPid(p) == /\ At(p, "mkpid") + /\ pidfile' = pidfile \cup {p} + /\ Goto(p, "link") + /\ UNCHANGED <<lockfile, claim, staged, locals, env>> + +\* create, line 410: link the pid file into place, or line 415: open the +\* lock exclusively. Both fail with EEXIST when the lock exists. +Link(p) == /\ At(p, "link") + /\ IF lock # NoPid + THEN Goto(p, "rmpid") /\ UNCHANGED <<lockfile, got>> + ELSE /\ lock' = p + /\ blank' = ~HardLinks + /\ got' = [got EXCEPT ![p] = TRUE] + /\ Goto(p, IF HardLinks THEN "rmpid" ELSE "write") + /\ UNCHANGED <<claim, temps, holder, retries, seen, closing, env>> + +\* create, line 415 after the open: write the pid through the open file. The +\* write reaches the lock only if the lock is still the file this writer made. +Write(p) == /\ At(p, "write") + /\ blank' = (blank /\ lock # p) + /\ Goto(p, "rmpid") + /\ UNCHANGED <<lock, claim, temps, locals, env>> + +\* create, line 417: unlink the pid file. Then lockOrHolder returns null +\* (line 428) or goes on to read the lock. +RmPid(p) == /\ At(p, "rmpid") + /\ pidfile' = pidfile \ {p} + /\ Goto(p, IF ~got[p] THEN "read" ELSE IF InTake(p) THEN "unclaim" ELSE "held") + /\ UNCHANGED <<lockfile, claim, staged, locals, env>> + +\* lockOrHolder, lines 435-439: read the lock. ENOENT retries create twice. +\* In acquireLock the text is the holder (line 506); in takeOver any text is +\* a blocker (lines 494-496). +Read(p) == /\ At(p, "read") + /\ IF lock = NoPid /\ retries[p] > 0 + THEN /\ retries' = [retries EXCEPT ![p] = @ - 1] + /\ Goto(p, "mkpid") + /\ UNCHANGED holder + ELSE /\ IF InTake(p) + THEN Goto(p, "unclaim") /\ UNCHANGED holder + ELSE /\ holder' = [holder EXCEPT ![p] = IF lock = NoPid THEN Unknown ELSE LockText] + /\ Goto(p, "judge") + /\ UNCHANGED retries + /\ UNCHANGED <<lockfile, claim, temps, seen, got, closing, env>> + +\* acquireLock, lines 507-516: take over a dead holder's lock, or any lock +\* with --force; otherwise fail. +Judge(p) == /\ At(p, "judge") + /\ Goto(p, IF holder[p] \in dead \/ p \in Forcers THEN "mkstaged" ELSE "failed") + /\ UNCHANGED <<lockfile, claim, temps, locals, env>> + +\* claimed, lines 455-457: make the staged directory and its pid file. +MkStaged(p) == /\ At(p, "mkstaged") + /\ staged' = staged \cup {p} + /\ Goto(p, "readdir") + /\ UNCHANGED <<lockfile, claim, pidfile, locals, env>> + +\* claimed, line 458: list the claimants. +ReadDir(p) == /\ At(p, "readdir") + /\ seen' = [seen EXCEPT ![p] = claimants] + /\ Goto(p, IF claimants = {} THEN "rename" ELSE "sweep") + /\ UNCHANGED <<lockfile, claim, temps, holder, retries, got, closing, env>> + +\* claimed, lines 459-460: remove one listed claimant's file if it is dead. +Sweep(p) == /\ At(p, "sweep") + /\ \E c \in seen[p]: + /\ seen' = [seen EXCEPT ![p] = @ \ {c}] + /\ claimants' = IF c \in dead THEN claimants \ {c} ELSE claimants + /\ Goto(p, IF seen[p] = {c} THEN "rename" ELSE "sweep") + /\ UNCHANGED <<lockfile, tdir, temps, holder, retries, got, closing, env>> + +\* claimed, line 463: rename the staged directory onto the takeover +\* directory. It fails when that directory holds a file (lines 464-470). +Rename(p) == /\ At(p, "rename") + /\ IF claimants = {} + THEN /\ tdir' = TRUE + /\ claimants' = {p} + /\ staged' = staged \ {p} + /\ Goto(p, IF closing[p] THEN "relread" ELSE "reread") + ELSE Goto(p, "busy") /\ UNCHANGED <<claim, staged>> + /\ UNCHANGED <<lockfile, pidfile, locals, env>> + +\* takeOver, lines 484-486: read the lock again behind the claim. +Reread(p) == /\ At(p, "reread") + /\ IF lock = NoPid + THEN StartCreate(p) + ELSE /\ Goto(p, IF LockText = holder[p] THEN "unlink" ELSE "unclaim") + /\ UNCHANGED retries + /\ UNCHANGED <<lockfile, claim, temps, holder, seen, got, closing, env>> + +\* takeOver, line 488: unlink the lock, whatever it holds by now. +TUnlink(p) == /\ At(p, "unlink") + /\ stolen' = IF lock # NoPid /\ LockText = holder[p] THEN stolen \cup {lock} ELSE stolen + /\ lock' = NoPid + /\ blank' = FALSE + /\ StartCreate(p) + /\ UNCHANGED <<claim, temps, holder, seen, got, closing, dead>> + +\* claimed, line 475: rename the takeover directory back to the staged path. +Unclaim(p) == /\ At(p, "unclaim") + /\ IF tdir + THEN tdir' = FALSE /\ claimants' = {} /\ staged' = staged \cup {p} + ELSE UNCHANGED <<claim, staged>> + /\ Goto(p, "discard") + /\ UNCHANGED <<lockfile, pidfile, locals, env>> + +\* claimed, lines 465 and 476: remove the staged directory. A refused claim +\* ends takeOver (line 500) but makes the release try again (line 533). +Discard(p) == /\ p \notin dead + /\ pc[p] \in {"busy", "discard"} + /\ staged' = staged \ {p} + /\ Goto(p, IF closing[p] + THEN IF pc[p] = "busy" THEN "mkstaged" ELSE "released" + ELSE IF got[p] THEN "held" ELSE "failed") + /\ UNCHANGED <<lockfile, claim, pidfile, locals, env>> + +\* store.close, lines 1729-1745: the holder chooses when to release. The +\* release runs behind the claim, so it starts by staging one (line 534). +Close(p) == /\ At(p, "held") + /\ closing' = [closing EXCEPT ![p] = TRUE] + /\ Goto(p, "mkstaged") + /\ UNCHANGED <<lockfile, claim, temps, holder, retries, seen, got, env>> + +\* release, line 524: read the lock and compare it with the own pid. +RelRead(p) == /\ At(p, "relread") + /\ IF lock = p /\ ~blank + THEN Goto(p, "relunlink") /\ UNCHANGED got + ELSE Goto(p, "unclaim") /\ got' = [got EXCEPT ![p] = FALSE] + /\ UNCHANGED <<lockfile, claim, temps, holder, retries, seen, closing, env>> + +\* release, line 525: unlink the lock, whatever it holds by now. +RelUnlink(p) == /\ At(p, "relunlink") + /\ lock' = NoPid + /\ blank' = FALSE + /\ got' = [got EXCEPT ![p] = FALSE] + /\ Goto(p, "unclaim") + /\ UNCHANGED <<claim, temps, holder, retries, seen, closing, env>> + +\* The process dies. Its files stay. +Crash(p) == /\ p \notin dead + /\ pc[p] \notin {"failed", "released"} + /\ dead' = dead \cup {p} + /\ UNCHANGED <<lockfile, claim, temps, pc, locals, stolen>> + +Step(p) == \/ MkPid(p) \/ Link(p) \/ Write(p) \/ RmPid(p) \/ Read(p) \/ Judge(p) + \/ MkStaged(p) \/ ReadDir(p) \/ Sweep(p) \/ Rename(p) \/ Reread(p) + \/ TUnlink(p) \/ Unclaim(p) \/ Discard(p) \/ RelRead(p) \/ RelUnlink(p) + +Ended(p) == p \in dead \/ pc[p] \in {"held", "failed", "released"} +\* Stutter at the end so that termination is not reported as a deadlock. +Done == (\A p \in Procs: Ended(p)) /\ UNCHANGED vars + +Next == (\E p \in Procs: Step(p) \/ Close(p) \/ Crash(p)) \/ Done + +\* Weak fairness on each writer's protocol steps. Close is the holder's +\* choice and Crash is the environment's, so neither is fair. +Spec == Init /\ [][Next]_vars /\ \A p \in Procs: WF_vars(Step(p)) + +---- +TypeOK == /\ lock \in Procs \cup {NoPid} + /\ blank \in BOOLEAN + /\ blank => lock # NoPid /\ ~HardLinks + /\ tdir \in BOOLEAN + /\ claimants \subseteq Procs + /\ claimants # {} => tdir + /\ pidfile \subseteq Procs /\ staged \subseteq Procs + /\ pc \in [Procs -> {"mkpid", "link", "write", "rmpid", "read", "judge", + "mkstaged", "readdir", "sweep", "rename", "busy", + "reread", "unlink", "unclaim", "discard", "held", + "relread", "relunlink", "released", "failed"}] + /\ holder \in [Procs -> Procs \cup {NoPid, Unknown}] + /\ retries \in [Procs -> 0..2] + /\ seen \in [Procs -> SUBSET Procs] + /\ got \in [Procs -> BOOLEAN] + /\ closing \in [Procs -> BOOLEAN] + /\ dead \subseteq Procs /\ stolen \subseteq Procs + +\* The live writers that believe they hold the store lock. +Holders == {p \in Procs \ dead: got[p]} + +\* (1) Two live writers never both believe they hold the lock, apart from a +\* writer whose lock a takeover removed by name. +OneHolder == Cardinality(Holders \ stolen) <= 1 + +\* The lock file names the live writer that believes it holds the lock. +LockNamesHolder == \A p \in Holders \ stolen: lock = p + +\* A writer between its claim (line 463) and its release of it (line 475): +\* a takeover replacing the lock, or a release removing its own. +Claiming(p) == \/ InTake(p) /\ pc[p] \in {"reread", "unlink", "mkpid", "link", "write", + "rmpid", "read", "unclaim"} + \/ closing[p] /\ pc[p] \in {"relread", "relunlink", "unclaim"} +\* The takeover directory holds at most one claim, and it is the claim of +\* each live writer that believes it has one. +ClaimExclusive == /\ Cardinality(claimants) <= 1 + /\ \A p \in Procs \ dead: Claiming(p) => claimants = {p} + +\* A writer that returned from acquireLock or release without crashing left +\* no private file and no claim, and one that holds nothing left no lock. +\* The release's read and unlink run behind its own claim. +NoResidue == \A p \in Procs \ dead: + /\ pc[p] \in {"held", "relread", "relunlink", "failed", "released"} + => p \notin pidfile \cup staged + /\ pc[p] \in {"held", "failed", "released"} => p \notin claimants + /\ pc[p] \in {"failed", "released"} => lock # p + +Moved(p, from, to) == pc[p] = from /\ pc'[p] = to + +\* (2) The unlink of line 488 removes the lock that line 484 read, the lock +\* of the holder takeOver was called for. +TakeoverRemovesNamedLock == + [][\A p \in Procs: Moved(p, "unlink", "mkpid") /\ lock # NoPid => LockText = holder[p]]_vars + +\* (2) A writer without --force never removes a live writer's lock. +TakeoverSparesLiveHolder == + [][\A p \in Procs \ Forcers: Moved(p, "unlink", "mkpid") => lock \in dead \cup {NoPid}]_vars + +\* (3) The unlink of line 525 removes only the releasing writer's own lock. +ReleaseRemovesOwnLock == + [][\A p \in Procs: Moved(p, "relunlink", "unclaim") => lock \in {p, NoPid}]_vars + +\* No writer waits: each one ends holding the lock, failed, released or dead. +Terminates == \A p \in Procs: <>Ended(p) + +\* close returns once it is called. +CloseTerminates == \A p \in Procs: closing[p] ~> (pc[p] = "released" \/ p \in dead) + +\* A writer that has not started while every other writer is dead or has +\* ended without the lock. +Late(p) == /\ At(p, "mkpid") + /\ holder[p] = NoPid + /\ \A q \in Procs \ {p}: q \in dead \/ pc[q] \in {"failed", "released"} + +\* (4) Whatever crashed holders and crashed claimants left behind, a later +\* writer acquires the lock unless it crashes too. +LateWriterAcquires == \A p \in Procs: Late(p) ~> (pc[p] = "held" \/ p \in dead) + +---- +\* States the properties depend on (reach checks in OrchStoreLock.matrix). +StaleLock == lock \in dead /\ ~blank +DeadClaimant == claimants \cap dead # {} +EmptyTakeoverDir == tdir /\ claimants = {} +StaleLockReplaced == \E p \in Procs \ dead: pc[p] = "held" /\ holder[p] \in dead +ClaimRefused == \E p \in Procs \ dead: pc[p] = "busy" +ReleaseBlocked == \E p \in Procs \ dead: closing[p] /\ pc[p] = "busy" +LockVanished == \E p \in Procs \ dead: retries[p] < 2 /\ ~InTake(p) +RetriesExhausted == \E p \in Procs \ dead: retries[p] = 0 /\ holder[p] = Unknown +ReleaseWindow == \E p \in Procs \ dead: pc[p] = "relunlink" +LateWriterMeetsWreck == \E p \in Procs: Late(p) /\ StaleLock /\ DeadClaimant +LateWriterMeetsEmptyDir == \E p \in Procs: Late(p) /\ StaleLock /\ EmptyTakeoverDir +OrphanBlankLock == blank /\ lock \in dead +ForcedSteal == \E p \in Forcers \ dead: pc[p] = "held" /\ holder[p] \in Procs \ dead +==== diff --git a/tools/forks.json b/tools/forks.json index df88dabcd..b0459165c 100644 --- a/tools/forks.json +++ b/tools/forks.json @@ -128,13 +128,13 @@ }, "plugins/pstack/skills/poteto-mode/scripts/orch/orch.test.ts": { "kind": "policy", - "why": "Covers the guards the port adds to orch/store.ts: nested-paren status rows, leading-dash branch names, single-line table cells, frontier SHAs from exact branch refs, and colour and hyperlink codes in gt output, injecting the fake gt through the store's gt option instead of rewriting PATH and giving the CLI only PATH and ORCH_STORE instead of the whole environment. For the stale-lock takeover it races two processes for a stale lock to show only one replaces it, refuses every writer, forced or not, while a live writer holds the takeover claim, pauses a second process at its unlink of the lock to show a forced writer is refused mid-takeover, kills one there to show the next writer clears its claim, removes the lock there to show the takeover carries on, and pauses one after it has listed a dead claimant to show it leaves alone a claim made since. Kills a writer after every lock call, with and without a stale lock, to show the next writer gets in, fails the hard link to show the exclusive open still refuses a live lock, plants and removes a rival lock between a failed create and the read to show the create is retried, and points the lock at a missing file to show the retry ends. Expects unit add to return ids and tracks as written, round-trips unit cells and inbox pointers that start with a formula or quote character and pins their quoted form on disk, reads rows written before the quote was stripped without dropping a leading quote, and times out gt log and gt info against a gt that ignores SIGTERM, with a budget a saturated machine can meet.", + "why": "Covers the guards the port adds to orch/store.ts: nested-paren status rows, leading-dash branch names, single-line table cells, frontier SHAs from exact branch refs, and colour and hyperlink codes in gt output, injecting the fake gt through the store's gt option instead of rewriting PATH and giving the CLI only PATH and ORCH_STORE instead of the whole environment. For the stale-lock takeover it races two processes for a stale lock to show only one replaces it, refuses every writer, forced or not, while a live writer holds the takeover claim, pauses a second process at its unlink of the lock to show a forced writer is refused mid-takeover, pauses a holder at the unlink of its release to show a forced writer is refused there too, kills one there to show the next writer clears its claim, removes the lock there to show the takeover carries on, and pauses one after it has listed a dead claimant to show it leaves alone a claim made since. Kills a writer after every lock call, with and without a stale lock, to show the next writer gets in, fails the hard link to show the exclusive open still refuses a live lock, plants and removes a rival lock between a failed create and the read to show the create is retried, and points the lock at a missing file to show the retry ends. Expects unit add to return ids and tracks as written, round-trips unit cells and inbox pointers that start with a formula or quote character and pins their quoted form on disk, reads rows written before the quote was stripped without dropping a leading quote, and times out gt log and gt info against a gt that ignores SIGTERM, with a budget a saturated machine can meet.", "since": "0.9.47", "upstream": "not-proposed" }, "plugins/pstack/skills/poteto-mode/scripts/orch/store.ts": { "kind": "policy", - "why": "Adds guards for temp-file cleanup, single-line status table cells, nested-paren gt status rows, leading-dash branch names, and frontier SHAs resolved from exact local branch refs, runs gt and git without an env override and strips terminal control sequences from gt output instead of setting NO_COLOR, and takes the gt path as an open option so tests can inject a fake. Serializes stale-lock takeovers behind a claim directory that holds one file named for the claimant's pid, staged under a private name and renamed into place. A live claim refuses every other writer, forced or not, and the next writer removes a dead claimant's file by name before it claims. Inside the claim the takeover re-reads the lock, refuses when the holder changed, and carries on when the lock is already gone. Creates the lock by writing the pid to a private file and hard-linking it into place, so a writer killed mid-create cannot leave a lock with no pid in it, and falls back to an exclusive open where the link fails for any reason other than an existing lock. Creates the lock again, twice at most, when it vanishes between a failed create and the read of its holder, and reports a lock it cannot read as held by an unknown pid. Quotes a cell that starts with a spreadsheet formula character or a quote only when writing a TSV or an inbox pointer, and strips that quote when reading one, so ids and branches round-trip unchanged. A row written before this change keeps a leading quote unless another quote or a formula character follows it; that is the shape the store writes, so such a row loses its first quote on read. Bounds each gt call with a timeout that kills gt with SIGKILL and that the gtTimeoutMs open option overrides.", + "why": "Adds guards for temp-file cleanup, single-line status table cells, nested-paren gt status rows, leading-dash branch names, and frontier SHAs resolved from exact local branch refs, runs gt and git without an env override and strips terminal control sequences from gt output instead of setting NO_COLOR, and takes the gt path as an open option so tests can inject a fake. Serializes stale-lock takeovers behind a claim directory that holds one file named for the claimant's pid, staged under a private name and renamed into place. A live claim refuses every other writer, forced or not, and the next writer removes a dead claimant's file by name before it claims. Inside the claim the takeover re-reads the lock, refuses when the holder changed, and carries on when the lock is already gone. The release reads and unlinks its own lock behind the same claim, so a forced takeover cannot replace the lock between the two, and stages a new claim until a live claimant finishes or dies. Creates the lock by writing the pid to a private file and hard-linking it into place, so a writer killed mid-create cannot leave a lock with no pid in it, and falls back to an exclusive open where the link fails for any reason other than an existing lock. Creates the lock again, twice at most, when it vanishes between a failed create and the read of its holder, and reports a lock it cannot read as held by an unknown pid. Quotes a cell that starts with a spreadsheet formula character or a quote only when writing a TSV or an inbox pointer, and strips that quote when reading one, so ids and branches round-trip unchanged. A row written before this change keeps a leading quote unless another quote or a formula character follows it; that is the shape the store writes, so such a row loses its first quote on read. Bounds each gt call with a timeout that kills gt with SIGKILL and that the gtTimeoutMs open option overrides.", "since": "0.9.13", "upstream": "not-proposed" }, From fee271b995a0885095b413557c22cb8aea22a08f Mon Sep 17 00:00:00 2001 From: alex-overcyai <alex@overcy.ai> Date: Thu, 8 Oct 2026 14:25:17 -0700 Subject: [PATCH 08/11] fix(shipping): use GitHub dequeue input id (#239) Co-authored-by: Alex OG Dev <Alexdev@Overcy-Dev-1.local> --- .../skills/poteto-mode/scripts/watch-pr/shipping-cli.test.ts | 4 ++++ .../skills/poteto-mode/scripts/watch-pr/shipping.test.ts | 1 + .../pstack/skills/poteto-mode/scripts/watch-pr/shipping.ts | 2 +- 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/plugins/pstack/skills/poteto-mode/scripts/watch-pr/shipping-cli.test.ts b/plugins/pstack/skills/poteto-mode/scripts/watch-pr/shipping-cli.test.ts index 8600f6e97..70bb8afd8 100644 --- a/plugins/pstack/skills/poteto-mode/scripts/watch-pr/shipping-cli.test.ts +++ b/plugins/pstack/skills/poteto-mode/scripts/watch-pr/shipping-cli.test.ts @@ -49,6 +49,10 @@ if (query.includes('disablePullRequestAutoMerge')) { state.autoMergeRequest = null; result = { disablePullRequestAutoMerge: { clientMutationId: null } }; } else if (query.includes('dequeuePullRequest')) { + if (!query.includes('dequeuePullRequest(input:{id:$id})')) { + console.error("DequeuePullRequestInput requires id"); + process.exit(1); + } state.mergeQueueEntry = null; result = { dequeuePullRequest: { clientMutationId: null } }; } else result = { repository: { pullRequest: state } }; diff --git a/plugins/pstack/skills/poteto-mode/scripts/watch-pr/shipping.test.ts b/plugins/pstack/skills/poteto-mode/scripts/watch-pr/shipping.test.ts index 84c638c48..6d6983354 100644 --- a/plugins/pstack/skills/poteto-mode/scripts/watch-pr/shipping.test.ts +++ b/plugins/pstack/skills/poteto-mode/scripts/watch-pr/shipping.test.ts @@ -239,6 +239,7 @@ describe("shipping GitHub boundary", () => { }; } if (query.includes("dequeuePullRequest")) { + expect(query).toContain("dequeuePullRequest(input:{id:$id})"); expect(args).toContain("id=pr-id"); mergeQueueEntry = null; return { data: { dequeuePullRequest: { clientMutationId: null } } }; diff --git a/plugins/pstack/skills/poteto-mode/scripts/watch-pr/shipping.ts b/plugins/pstack/skills/poteto-mode/scripts/watch-pr/shipping.ts index e0249cb38..2ab22635c 100644 --- a/plugins/pstack/skills/poteto-mode/scripts/watch-pr/shipping.ts +++ b/plugins/pstack/skills/poteto-mode/scripts/watch-pr/shipping.ts @@ -211,7 +211,7 @@ export class GhShippingService implements ShippingService { "api", "graphql", "-f", - "query=mutation($id:ID!) { dequeuePullRequest(input:{pullRequestId:$id}) { clientMutationId } }", + "query=mutation($id:ID!) { dequeuePullRequest(input:{id:$id}) { clientMutationId } }", "-f", `id=${id}`, ]) From f0c752c841dbde6f5b2f405cd67522029b383a5c Mon Sep 17 00:00:00 2001 From: Michael Denyer <97485362+michael-denyer@users.noreply.github.com> Date: Thu, 8 Oct 2026 22:32:44 +0100 Subject: [PATCH 09/11] test(shipping): answer the shipping tests from one fake GitHub (#240) * test(shipping): answer the shipping tests from one fake GitHub Both shipping test files dispatched on the query text themselves, and each compared the dequeue mutation with a fixed string. That check failed a query that differed only in spacing and passed a wrong input field on disablePullRequestAutoMerge. fakeGitHub holds each mutation's required input field in one table and answers the way the endpoint does: a query gets back only the fields it selects, and a mutation needs the right name, input field, variable type, and pull request id. * test(shipping): match only top-level fields and allow a named mutation The field filter matched the nested id in mergeQueueEntry { id }, so the fake still returned the pull request id to a query that no longer selected it. The mutation pattern also refused a named operation, which GitHub accepts. --- .../scripts/watch-pr/shipping-cli.test.ts | 21 ++------ .../scripts/watch-pr/shipping.test-helper.ts | 48 +++++++++++++++++++ .../scripts/watch-pr/shipping.test.ts | 28 ++++------- 3 files changed, 61 insertions(+), 36 deletions(-) create mode 100644 plugins/pstack/skills/poteto-mode/scripts/watch-pr/shipping.test-helper.ts diff --git a/plugins/pstack/skills/poteto-mode/scripts/watch-pr/shipping-cli.test.ts b/plugins/pstack/skills/poteto-mode/scripts/watch-pr/shipping-cli.test.ts index 70bb8afd8..565fe5fef 100644 --- a/plugins/pstack/skills/poteto-mode/scripts/watch-pr/shipping-cli.test.ts +++ b/plugins/pstack/skills/poteto-mode/scripts/watch-pr/shipping-cli.test.ts @@ -40,25 +40,12 @@ function fixture( writeFileSync( gh, `#!${process.execPath} -import { readFileSync, writeFileSync, appendFileSync } from 'node:fs'; +import { readFileSync, writeFileSync } from 'node:fs'; +import { fakeGitHub } from ${JSON.stringify(join(import.meta.dir, "shipping.test-helper.ts"))}; const state = JSON.parse(readFileSync(process.env.SHIPPING_STATE, 'utf8')); -const args = process.argv.slice(2); -const query = args.find(arg => arg.startsWith('query=')); -let result; -if (query.includes('disablePullRequestAutoMerge')) { - state.autoMergeRequest = null; - result = { disablePullRequestAutoMerge: { clientMutationId: null } }; -} else if (query.includes('dequeuePullRequest')) { - if (!query.includes('dequeuePullRequest(input:{id:$id})')) { - console.error("DequeuePullRequestInput requires id"); - process.exit(1); - } - state.mergeQueueEntry = null; - result = { dequeuePullRequest: { clientMutationId: null } }; -} else result = { repository: { pullRequest: state } }; -appendFileSync(process.env.SHIPPING_STATE + '.calls', query + '\\n'); +const response = fakeGitHub(state, process.argv.slice(2)); writeFileSync(process.env.SHIPPING_STATE, JSON.stringify(state)); -console.log(JSON.stringify({ data: result })); +console.log(JSON.stringify(response)); ` ); chmodSync(gh, 0o755); diff --git a/plugins/pstack/skills/poteto-mode/scripts/watch-pr/shipping.test-helper.ts b/plugins/pstack/skills/poteto-mode/scripts/watch-pr/shipping.test-helper.ts new file mode 100644 index 000000000..c65ca7877 --- /dev/null +++ b/plugins/pstack/skills/poteto-mode/scripts/watch-pr/shipping.test-helper.ts @@ -0,0 +1,48 @@ +// The slice of GitHub's GraphQL schema the shipping service depends on: the +// one required input field of each mutation, and the pull request field the +// mutation clears. Regenerate the input names with +// gh api graphql -f query='{ __type(name:"DequeuePullRequestInput") { inputFields { name } } }' +const MUTATIONS: Record<string, { input: string; clears: string }> = { + disablePullRequestAutoMerge: { + input: "pullRequestId", + clears: "autoMergeRequest", + }, + dequeuePullRequest: { input: "id", clears: "mergeQueueEntry" }, +}; + +const MUTATION = + /mutation\s*\w*\s*\(\s*\$id\s*:\s*ID!\s*\)\s*\{\s*(\w+)\(\s*input\s*:\s*\{\s*(\w+)\s*:\s*\$id\s*\}\s*\)/; + +/** + * Answer one `gh api graphql` call the way GitHub would. + * + * A query returns only the pull request fields it selects. A mutation is + * refused unless it declares `$id` as `ID!`, names a known mutation and its + * required input field, and carries this pull request's node id. + */ +export function fakeGitHub( + pullRequest: Record<string, unknown>, + argv: readonly string[] +): { data: Record<string, unknown> } { + const query = argv.find((arg) => arg.startsWith("query=")) ?? ""; + if (!query.includes("mutation")) { + // Drop nested selections, so `mergeQueueEntry { id }` does not count as + // selecting the pull request's own `id`. + const selection = query.replace(/\{[^{}]*\}/g, ""); + const selected = Object.entries(pullRequest).filter(([field]) => + new RegExp(`\\b${field}\\b`).test(selection) + ); + return { + data: { repository: { pullRequest: Object.fromEntries(selected) } }, + }; + } + const [, name = "", input = ""] = MUTATION.exec(query) ?? []; + const contract = MUTATIONS[name]; + if (!contract) throw new Error(`not a mutation GitHub accepts: ${query}`); + if (input !== contract.input) + throw new Error(`${name} input requires ${contract.input}, got ${input}`); + if (!argv.includes(`id=${pullRequest.id}`)) + throw new Error("Could not resolve to a node with the given id"); + pullRequest[contract.clears] = null; + return { data: { [name]: { clientMutationId: null } } }; +} diff --git a/plugins/pstack/skills/poteto-mode/scripts/watch-pr/shipping.test.ts b/plugins/pstack/skills/poteto-mode/scripts/watch-pr/shipping.test.ts index 6d6983354..cd36f6a03 100644 --- a/plugins/pstack/skills/poteto-mode/scripts/watch-pr/shipping.test.ts +++ b/plugins/pstack/skills/poteto-mode/scripts/watch-pr/shipping.test.ts @@ -8,6 +8,7 @@ import { type LandingRecord, type ShippingService, } from "./shipping.ts"; +import { fakeGitHub } from "./shipping.test-helper.ts"; const context = { owner: "owner", repo: "repo", number: parsePrNumber(1) }; const empty: LandingRecord = { @@ -227,25 +228,14 @@ describe("shipping GitHub boundary", () => { }); it("uses separate mutations for both pending mechanisms", async () => { - let autoMergeRequest: { enabledAt: string } | null = { enabledAt: "now" }; - let mergeQueueEntry: { id: string } | null = { id: "queue" }; - const service = new GhShippingService(async (args) => { - const query = args.find((arg) => arg.startsWith("query=")) ?? ""; - if (query.includes("disablePullRequestAutoMerge")) { - expect(args).toContain("id=pr-id"); - autoMergeRequest = null; - return { - data: { disablePullRequestAutoMerge: { clientMutationId: null } }, - }; - } - if (query.includes("dequeuePullRequest")) { - expect(query).toContain("dequeuePullRequest(input:{id:$id})"); - expect(args).toContain("id=pr-id"); - mergeQueueEntry = null; - return { data: { dequeuePullRequest: { clientMutationId: null } } }; - } - return response({ ...raw, autoMergeRequest, mergeQueueEntry }); - }); + const pullRequest = { + ...raw, + autoMergeRequest: { enabledAt: "now" }, + mergeQueueEntry: { id: "queue" }, + }; + const service = new GhShippingService(async (args) => + fakeGitHub(pullRequest, args) + ); const expected = await service.inspect(context); expect(await cancelPending(service, expected)).toEqual({ kind: "cancelled", From e6719ac0c4f95512e264ec069c75eb277d3e6d78 Mon Sep 17 00:00:00 2001 From: Michael Denyer <97485362+michael-denyer@users.noreply.github.com> Date: Thu, 8 Oct 2026 22:38:31 +0100 Subject: [PATCH 10/11] fix: retain unreadable activity roots and block overlapping Pi agents (#241) --- plugins/pstack/pi/agents.ts | 66 +++++++++++-------- .../skills/poteto-mode/references/pi-tools.md | 2 +- .../poteto-mode/scripts/worktree-audit.mjs | 12 +++- tests/pi/agents-session.test.mjs | 59 ++++++++++++++++- tests/worktree-audit.test.mjs | 38 ++++++++++- 5 files changed, 143 insertions(+), 34 deletions(-) diff --git a/plugins/pstack/pi/agents.ts b/plugins/pstack/pi/agents.ts index b1a26825e..8e1b9f7ef 100644 --- a/plugins/pstack/pi/agents.ts +++ b/plugins/pstack/pi/agents.ts @@ -78,7 +78,10 @@ interface Run { // A remote agent runs under the live pi process that restore left it to. type LocalAgent = { kind: "local"; record: RunningRecord; run: Run }; -type AgentState = LocalAgent | { kind: "remote"; record: RunningRecord } | { kind: "ended"; record: EndedRecord }; +type AgentState = LocalAgent + | { kind: "remote"; record: RunningRecord } + | { kind: "interrupted"; record: RunningRecord } + | { kind: "ended"; record: EndedRecord }; // How a run ended, from what its process left behind. A run that settled on a // reply completed, whatever the exit code of the shutdown after it. Stderr @@ -296,10 +299,10 @@ export class AgentRunner { private find(to: string): AgentState { const byId = this.agents.get(to); - if (byId) return byId; + if (byId) return this.refreshRestored(byId); const states = [...this.agents.values()]; const matches = states.filter((state) => state.record.agent.description === to); - if (matches.length === 1) return matches[0]; + if (matches.length === 1) return this.refreshRestored(matches[0]); if (matches.length > 1) { throw new Error(`"${to}" matches several agents (${matches.map((state) => state.record.agent.id).join(", ")}); pass an agentId.`); } @@ -309,11 +312,14 @@ export class AgentRunner { // An agent this process can act on. One running under another pi process is // not: only that process holds its stdin and can message or stop it. - private owned(to: string): Exclude<AgentState, { kind: "remote" }> { + private owned(to: string): Exclude<AgentState, { kind: "remote" | "interrupted" }> { const state = this.find(to); if (state.kind === "remote") { throw new Error(`Agent ${state.record.agent.id} is running under another pi process (pid ${state.record.parentPid}); only that process can message or stop it.`); } + if (state.kind === "interrupted") { + throw new Error(`Agent ${state.record.agent.id}'s previous process (pid ${state.record.pid}) has not exited; it cannot resume or be stopped here until its exit is confirmed.`); + } return state; } @@ -360,18 +366,41 @@ export class AgentRunner { } list(): AgentRecord[] { - return [...this.agents.values()].map((state) => state.record); + return [...this.agents.values()].map((state) => this.refreshRestored(state).record); } private persist(record: AgentRecord): void { this.pi.appendEntry(ENTRY_TYPE, record); } - // Folds persisted snapshots. A snapshot still marked running is left to the - // live pi process that launched it while its process is still that one's - // child: a live parent pid alone may be a reused one. Any other running - // snapshot ends here, and its process is stopped when it is known to be the - // agent's. + private refreshRestored(state: AgentState): AgentState { + if (state.kind === "local" || state.kind === "ended") return state; + const { record } = state; + const fate = fateOf(record); + if (fate === "kept" && state.kind === "remote") return state; + if (fate !== "gone") { + if (state.kind === "interrupted") return state; + const interrupted: AgentState = { kind: "interrupted", record }; + this.agents.set(record.agent.id, interrupted); + reapOrphan(record, this.settings.killGraceMs); + return interrupted; + } + const stopped: EndedRecord = { + agent: record.agent, + status: "stopped", + pid: record.pid, + exitCode: null, + endedAt: now(), + finalText: "(interrupted: the session that started it ended)", + }; + const ended: AgentState = { kind: "ended", record: stopped }; + this.agents.set(record.agent.id, ended); + this.persist(stopped); + return ended; + } + + // A restored process stays unavailable until its exit is confirmed. Only a + // process whose identity matches the record can be signalled. restore(entries: readonly SessionEntry[]): void { for (const entry of entries) { if (entry.type !== "custom" || entry.customType !== ENTRY_TYPE || !Value.Check(recordSchema, entry.data)) continue; @@ -379,21 +408,6 @@ export class AgentRunner { if (this.agents.get(id)?.kind === "local") continue; this.agents.set(id, entry.data.status === "running" ? { kind: "remote", record: entry.data } : { kind: "ended", record: entry.data }); } - for (const [id, state] of this.agents) { - if (state.kind !== "remote") continue; - const { record } = state; - if (fateOf(record) === "kept") continue; - const stopped: EndedRecord = { - agent: record.agent, - status: "stopped", - pid: record.pid, - exitCode: null, - endedAt: now(), - finalText: "(interrupted: the session that started it ended)", - }; - this.agents.set(id, { kind: "ended", record: stopped }); - this.persist(stopped); - reapOrphan(record, this.settings.killGraceMs); - } + for (const state of this.agents.values()) this.refreshRestored(state); } } diff --git a/plugins/pstack/skills/poteto-mode/references/pi-tools.md b/plugins/pstack/skills/poteto-mode/references/pi-tools.md index d75ddb008..7624b9db4 100644 --- a/plugins/pstack/skills/poteto-mode/references/pi-tools.md +++ b/plugins/pstack/skills/poteto-mode/references/pi-tools.md @@ -40,7 +40,7 @@ poteto-mode's Subagents section applies on Pi through the `agent` tool: - `run_in_background: true` returns the agent id at once. The completion joins the conversation after your current tool calls finish, as on Claude Code, or starts a turn when you are idle. In print and JSON mode (`pi -p`), where Pi exits once the run settles, the extension holds the settle while a background agent runs, so each completion still arrives as a turn and the process ends after the last one. A child agent holds its settle the same way while its own background agents run, because its parent closes its stdin once it settles, and a message from the parent ends the hold. Interactive and RPC main sessions settle as usual and take the completion when it arrives. - While a background agent runs, the extension blocks a `bash` command line in which the word `sleep` is followed by a literal duration of two seconds or more, anywhere in the line, poll loops included: `sleep 30`, `npm test; sleep 30`, `until gh pr checks 1; do sleep 30; done`, `sleep infinity`. A shorter sleep, a sleep backgrounded as `sleep 30 &`, and `sleep` inside a quoted string, a comment, or a heredoc all run. The rule errs toward blocking, so `timeout 5 sleep 30` and a sleep inside a backgrounded group are blocked too. The completion notice arrives on its own, so continue other work or end your turn. - An agent's status follows its process. `completed` means the child exited, and `stop_agent` reports `stopped` only once the child has exited, so the Claude Code caveat about a `completed` agent that keeps running does not apply to the agent itself. Stopping an agent, like the session shutdown that stops every agent, ends the child pi and the `bash` command it still has running, because Pi kills that command's process tree on SIGTERM. A process that a finished `bash` command left in the background (`cmd &`, `nohup cmd`) runs in its own process group and outlives the agent; stop it yourself. A stopped agent's notice joins the conversation without starting a turn, since `stop_agent` already returned. -- Agents belong to the session that started them. Quitting, reloading, and starting, resuming, or forking a session all stop every running agent, and a parent process that exits signals its agents to stop. When a session starts, an agent still recorded as running is marked `stopped`, and its process is killed, only if the `pi` process that launched it is gone. An agent whose launching process is still alive is left running. +- Agents belong to the session that started them. Quitting, reloading, and starting, resuming, or forking a session all stop every running agent, and a parent process that exits signals its agents to stop. On restore, an agent still running under its launching Pi process is left alone. An orphan whose process identity matches the saved record is signalled to stop, and remains unavailable to `send_message` and `stop_agent` until its process exits. Without enough identity data to signal it safely, its process is left alone and continuation is refused until its exit is confirmed. Only then is the restored agent marked `stopped` and allowed to resume. - `send_message` to a running agent is an RPC `steer` on the child's stdin. The agent reads it after its current tool calls, as on Claude Code, carries on in the same run, and sends one completion notice. A message the child rejects returns an error, and the agent keeps running. A message to a finished agent resumes its session in the background with the context of its earlier runs. A message that arrives after the agent settled but before its process exited resumes the agent once the process has exited. - A role value's `@<level>` picks the same effort agent as on Claude Code, and the extension passes its level to the child as `--thinking`. `session`, or an agent with no effort, runs the child at the parent's current thinking level. - Keep the rest of the policy unchanged. Pass file pointers not inlined context, give each worker its own worktree when they write, review every subagent's diff yourself. diff --git a/plugins/pstack/skills/poteto-mode/scripts/worktree-audit.mjs b/plugins/pstack/skills/poteto-mode/scripts/worktree-audit.mjs index b189def4c..4dc6a3dbb 100755 --- a/plugins/pstack/skills/poteto-mode/scripts/worktree-audit.mjs +++ b/plugins/pstack/skills/poteto-mode/scripts/worktree-audit.mjs @@ -14,7 +14,7 @@ // Every probe yields a Fact, { known: true, value } or { known: false }. A hold // bucket needs only its own fact; `safe` needs every fact known. import { execFileSync } from "node:child_process"; -import { existsSync, readdirSync, readFileSync, realpathSync, statSync } from "node:fs"; +import { readdirSync, readFileSync, realpathSync, statSync } from "node:fs"; import { homedir } from "node:os"; import { basename, dirname, join } from "node:path"; import process from "node:process"; @@ -74,13 +74,19 @@ export function parseWorktrees(output) { return worktrees; } -export function defaultTranscriptRoots({ env = process.env, home = homedir(), exists = existsSync } = {}) { +export function defaultTranscriptRoots({ env = process.env, home = homedir(), stat = statSync } = {}) { const claude = join(env.CLAUDE_CONFIG_DIR || join(home, ".claude"), "projects"); const codex = env.CODEX_HOME || join(home, ".codex"); const piAgent = env.PI_CODING_AGENT_DIR || join(home, ".pi", "agent"); const copilot = join(env.COPILOT_HOME || join(home, ".copilot"), "session-state"); const found = [claude, join(codex, "sessions"), join(codex, "archived_sessions"), - join(piAgent, "sessions"), join(piAgent, "pstack"), copilot].filter((root) => exists(root)); + join(piAgent, "sessions"), join(piAgent, "pstack"), copilot].filter((root) => { + try { + return stat(root, { throwIfNoEntry: false }) !== undefined; + } catch (error) { + return !["ENOENT", "ENOTDIR"].includes(error.code); + } + }); return found.length ? found : [claude]; } diff --git a/tests/pi/agents-session.test.mjs b/tests/pi/agents-session.test.mjs index 6916c7a0b..efcec2594 100644 --- a/tests/pi/agents-session.test.mjs +++ b/tests/pi/agents-session.test.mjs @@ -89,8 +89,8 @@ describe("registry", () => { // The other pi: a second process that starts a background agent and prints // the entries a pi process opening the same session would read. - async function otherPi() { - const { w, ctx } = setup({ script: { default: [{ spawn: "running" }, { sleep: 30000 }] } }); + async function otherPi(options = {}) { + const { w, ctx } = setup({ script: { default: [{ spawn: "running" }, { sleep: 30000 }] }, ...options }); const host = w.spawn(process.execPath, [join(import.meta.dir, "host.mjs"), JSON.stringify(w.settings), "stay"], { stdio: ["ignore", "pipe", "inherit"], }); @@ -101,6 +101,61 @@ describe("registry", () => { return { w, ctx, host, entries: JSON.parse(printed) }; } + test("an orphan cannot resume its session while its process awaits SIGKILL", async () => { + const { w, ctx, host, entries } = await otherPi({ script: { byPrompt: { + x: [{ ignoreSigterm: true }, { spawn: "running" }, { sleep: 30000 }], + again: [{ reply: "resumed" }], + } } }); + const [first] = w.invocations(); + const exited = exitOf(host); + host.kill("SIGKILL"); + await exited; + const resumed = await restore(w, entries); + const id = entries[0].data.agent.id; + + expect(alive(first.pid)).toBe(true); + expect((await listAgents(resumed, ctx))[0].status).toBe("running"); + await expect(resumed.call("send_message", { to: id, message: "again" }, ctx)).rejects.toThrow(/process.*has not exited/); + expect(w.invocations()).toHaveLength(1); + await waitFor(() => !alive(first.pid)); + expect((await listAgents(resumed, ctx))[0].status).toBe("stopped"); + + await resumed.call("send_message", { to: id, message: "again" }, ctx); + await waitFor(() => resumed.messages.length === 1); + const second = w.invocations()[1]; + expect(flag(second, "--session-id")).toBe(flag(first, "--session-id")); + expect(second.cwd).toBe(first.cwd); + expect(alive(first.pid)).toBe(false); + }); + + test("an orphan without recorded process identity cannot resume or be signalled until it exits", async () => { + const { w, ctx, host, entries } = await otherPi(); + const [first] = w.invocations(); + delete entries[0].data.pidStart; + const exited = exitOf(host); + host.kill("SIGKILL"); + await exited; + const resumed = await restore(w, entries); + const id = entries[0].data.agent.id; + + expect((await listAgents(resumed, ctx))[0].status).toBe("running"); + for (const [tool, params] of [ + ["send_message", { to: id, message: "again" }], + ["stop_agent", { id }], + ]) await expect(resumed.call(tool, params, ctx)).rejects.toThrow(/process.*has not exited/); + await sleep(2 * w.settings.killGraceMs); + expect(alive(first.pid)).toBe(true); + expect(w.invocations()).toHaveLength(1); + + process.kill(-first.pid, "SIGTERM"); + await waitFor(() => !alive(first.pid)); + expect((await listAgents(resumed, ctx))[0].status).toBe("stopped"); + await resumed.call("send_message", { to: id, message: "again" }, ctx); + await w.until("invocation", 2); + expect(flag(w.invocations()[1], "--session-id")).toBe(flag(first, "--session-id")); + await resumed.emit("session_shutdown", {}, ctx); + }); + for (const [what, edit, env, status] of [ ["is left running, untouched", (data) => data, {}, "running"], ["is left running on a record an earlier version wrote, which has no start time", ({ pidStart, ...data }) => data, {}, "running"], diff --git a/tests/worktree-audit.test.mjs b/tests/worktree-audit.test.mjs index 543277702..3d6b8392f 100644 --- a/tests/worktree-audit.test.mjs +++ b/tests/worktree-audit.test.mjs @@ -335,6 +335,24 @@ for (const dir of ["sessions", "archived_sessions"]) { }); } +test.skipIf(noChmod)("an inaccessible default transcript root keeps a recently used worktree out of safe", () => { + const fixture = createFixture(); + const chatted = addWorktree(fixture, "codex-inaccessible"); + mkdirSync(join(fixture.root, ".claude", "projects"), { recursive: true }); + const codex = join(fixture.root, ".codex"); + const session = join(codex, "sessions", "recent.jsonl"); + mkdirSync(dirname(session), { recursive: true }); + writeFileSync(session, `${JSON.stringify({ type: "session_meta", payload: { cwd: chatted } })}\n`); + chmodSync(codex, 0o000); + locked.push(codex); + + const transcripts = defaultTranscriptRoots({ env: {}, home: fixture.root }); + const { rows, warnings } = runAudit(fixture, { transcripts }); + expect(rowFor(rows, chatted).slice(6, 8)).toEqual(["-", "review"]); + expect(warnings).toHaveLength(1); + expect(warnings[0]).toMatch(/transcript scan failed.*EACCES/); +}); + describe("lastChats matches a path as JSONL spells it, never a sibling's prefix", () => { const scan = (path, cwd, spellings = [path]) => { const root = realpathSync(mkdtempSync(join(tmpdir(), "worktree-audit-chats-"))); @@ -550,7 +568,7 @@ describe("default transcripts roots", () => { const claude = "/home/u/.claude/projects"; // These paths are POSIX literals, which path.join spells with backslashes on Windows. const roots = ({ exists, ...options }) => - defaultTranscriptRoots({ ...options, home, exists: (path) => exists(gitPath(path)) }).map(gitPath); + defaultTranscriptRoots({ ...options, home, stat: (path) => exists(gitPath(path)) ? {} : undefined }).map(gitPath); test("every runtime directory that exists, Pi's under PI_CODING_AGENT_DIR when set", () => { const present = new Set([claude, "/home/u/.codex/sessions", "/pi/sessions", "/pi/pstack", "/home/u/.pi/agent/sessions"]); @@ -587,12 +605,28 @@ describe("default transcripts roots", () => { expect(roots({ env: {}, exists: () => false })).toEqual([claude]); }); + test.each(["EACCES", "EPERM", "EIO", "ELOOP"])("a %s while discovering a root retains it for the audit", (code) => { + const inaccessible = join(home, ".codex", "sessions"); + const found = defaultTranscriptRoots({ env: {}, home, stat: (path) => { + if (path === inaccessible) throw Object.assign(new Error("cannot inspect root"), { code }); + return gitPath(path) === claude ? {} : undefined; + } }); + expect(found.map(gitPath)).toEqual([claude, gitPath(inaccessible)]); + }); + + test.each(["ENOENT", "ENOTDIR"])("a %s while discovering a root omits it", (code) => { + expect(defaultTranscriptRoots({ env: {}, home, stat: (path) => { + if (gitPath(path) === claude) return {}; + throw Object.assign(new Error("root is absent"), { code }); + } }).map(gitPath)).toEqual([claude]); + }); + test.each([ ["PI_CODING_AGENT_DIR", { PI_CODING_AGENT_DIR: "/pi" }], ["the default agent directory", {}], ])("the Pi roots are where the extension keeps sessions and agent state under %s", (_, env) => { const { agentDir } = defaultSettings(() => 0, env); - const roots = defaultTranscriptRoots({ env, home: homedir(), exists: () => true }); + const roots = defaultTranscriptRoots({ env, home: homedir(), stat: () => ({}) }); expect(roots).toEqual(expect.arrayContaining([join(agentDir, "sessions"), join(agentDir, PSTACK_STATE_DIR)])); }); }); From 60ae9e25610744bccd45bd405c211413a471aaf8 Mon Sep 17 00:00:00 2001 From: Michael Denyer <97485362+michael-denyer@users.noreply.github.com> Date: Thu, 8 Oct 2026 22:45:46 +0100 Subject: [PATCH 11/11] Release 0.9.79 (#242) --- .claude-plugin/marketplace.json | 2 +- CHANGES.md | 16 ++++++++++++++++ VERSION | 2 +- package.json | 2 +- plugins/pstack/.claude-plugin/plugin.json | 2 +- plugins/pstack/.codex-plugin/plugin.json | 2 +- plugins/pstack/.github/plugin/plugin.json | 2 +- 7 files changed, 22 insertions(+), 6 deletions(-) diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 50bfd4b17..014dfc922 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -10,7 +10,7 @@ "name": "pstack", "source": "./plugins/pstack", "description": "If you want to go fast, go deep first. pstack helps you write less, but higher quality code. rigorous agent workflows you can parallelize with confidence. Original pstack by Lauren Tan (poteto).", - "version": "0.9.78" + "version": "0.9.79" } ] } diff --git a/CHANGES.md b/CHANGES.md index bd9c36e2c..40f3d1a45 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,6 +2,22 @@ This file is the release changelog, with one `## <version> - <title>` entry per release, newest first. The Cursor-to-Claude rewrite rules live in [`tools/substitutions.json`](tools/substitutions.json), and the [sync boundary](CONTRIBUTING.md#the-sync-boundary) in `CONTRIBUTING.md` defines how a change to upstream's skill content is declared. +## 0.9.79 - dequeue a queued PR, and fixes from three models and a code review + +`ship-pr cancel-pending` can take a PR out of a merge queue. The dequeue request named the pull request in `pullRequestId`, a field GitHub's `DequeuePullRequestInput` does not have, so GitHub refused it. The request now sends `id`, and `disablePullRequestAutoMerge` keeps `pullRequestId`, which is the name its own input type uses. @alex-overcyai found and fixed this in [#239](https://github.com/michael-denyer/pstack-claude/pull/239). Both shipping test files now answer from one fake of `gh api graphql`. The fake returns only the fields a query selects and refuses a mutation with the wrong input field or variable type, so the same mistake on either mutation fails a test. + +`watch-pr` refuses an `--interval` above 2147483.647 seconds. A timer delay above 2^31 - 1 ms is replaced with 1 ms, so a longer interval made every poll sleep return at once. + +On Pi, a `/loop` typed during a manual compaction no longer leaves the wakeup slot sealed. The scheduler treated any session that was not idle as a run in flight, and Pi is not idle during a compaction, so it refused the next run at every wakeup. It now tracks a run from `agent_start` to `agent_settled` and seals the slot only between the two. + +The orchestration store's lock release can no longer delete a lock that a forced takeover has just written. The release read the lock, saw its own pid, and unlinked the lock by path as a separate step. It now does both behind the claim a takeover holds, and waits for a live claimant to finish. + +The three fixes above came from models of the code, which ship with them: `lean/Model/WatchPoll.lean` for the polling arithmetic, and `tla/PiScheduler.tla` and `tla/OrchStoreLock.tla` for the scheduler and the lock. Two runs of each TLA+ model still fail by design and record narrower findings these fixes do not close. + +worktree-audit keeps a transcript root it cannot inspect for any reason other than absence. It reports the scan failure and leaves recent activity unknown, so the worktree needs review instead of reading as idle. On Pi, a restored agent stays unavailable until its previous process is confirmed gone. Pi signals only an orphan whose identity matches, and an older record without enough identity data refuses continuation while that process is alive. + +`/poteto-help` offers "a default effort" where it offered "a reasoning budget", a step the port's `/setup-pstack` does not have. The sync rule that scrubs model slugs in `setup-pstack` no longer takes a sentence's period into the slug. + ## 0.9.78 - sync to upstream df58112 (v0.15.15) The upstream pin moves from `2cbf585` to `df58112`, upstream v0.15.15, two commits. `/poteto-help` now offers `/setup-pstack` when the model sheet is missing and the answer depends on it. It asks at most once per chat, and a user who declines is told that every role keeps its default model. Upstream also dropped one model from its default panel and lowered its Opus effort from `max` to `xhigh`. The port takes neither change, because `models.json` sets the port's panel and effort. `setup-pstack` keeps the port's default-effort step in place of upstream's reworded budget step. diff --git a/VERSION b/VERSION index 5c648ab70..937cf1cbb 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.9.78 +0.9.79 diff --git a/package.json b/package.json index fe8e5d117..23e45091b 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "pstack", "private": true, - "version": "0.9.78", + "version": "0.9.79", "description": "If you want to go fast, go deep first. pstack helps you write less, but higher quality code. rigorous agent workflows you can parallelize with confidence. Pi package of the Claude Code plugin; skills are shared, tool names resolve via skills/poteto-mode/references/pi-tools.md. Original pstack by Lauren Tan (poteto).", "author": { "name": "Michael Denyer", diff --git a/plugins/pstack/.claude-plugin/plugin.json b/plugins/pstack/.claude-plugin/plugin.json index adc4aafed..2f2573c7e 100644 --- a/plugins/pstack/.claude-plugin/plugin.json +++ b/plugins/pstack/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "pstack", "displayName": "pstack (Claude Code port)", - "version": "0.9.78", + "version": "0.9.79", "description": "If you want to go fast, go deep first. pstack helps you write less, but higher quality code. rigorous agent workflows you can parallelize with confidence. Ported from cursor/plugins/pstack to Claude Code. Original pstack by Lauren Tan (poteto).", "author": { "name": "Michael Denyer", diff --git a/plugins/pstack/.codex-plugin/plugin.json b/plugins/pstack/.codex-plugin/plugin.json index 26fa5bdfc..da489d470 100644 --- a/plugins/pstack/.codex-plugin/plugin.json +++ b/plugins/pstack/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "pstack", - "version": "0.9.78", + "version": "0.9.79", "description": "If you want to go fast, go deep first. pstack helps you write less, but higher quality code. rigorous agent workflows you can parallelize with confidence. Codex port of the Claude Code plugin; skills are shared, tool names resolve via skills/poteto-mode/references/codex-tools.md. Original pstack by Lauren Tan (poteto).", "author": { "name": "Michael Denyer", diff --git a/plugins/pstack/.github/plugin/plugin.json b/plugins/pstack/.github/plugin/plugin.json index b5a2e1699..8312b0d9a 100644 --- a/plugins/pstack/.github/plugin/plugin.json +++ b/plugins/pstack/.github/plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "pstack", "displayName": "pstack (GitHub Copilot port)", - "version": "0.9.78", + "version": "0.9.79", "description": "If you want to go fast, go deep first. pstack helps you write less, but higher quality code. rigorous agent workflows you can parallelize with confidence. Ported from cursor/plugins/pstack to GitHub Copilot CLI and app. Original pstack by Lauren Tan (poteto).", "author": { "name": "Michael Denyer",