mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-11-07 14:38:23 -10:00
docs(design): never re-target an admitted PR event key
With the recipient reselected on every drain, a key admitted to one session could be admitted again to another when a replica clear failed and the most recently active binder changed before the retry: the prompt route's admission ring is keyed by session and key, so it cannot suppress that. The drain now records admitted keys per PR for the daemon's lifetime and only retries the cleanup for them; lazy recipient selection applies to keys not yet admitted. Validation and acceptance criterion 2 cover the partial-clear/recipient-change case.
This commit is contained in:
1 parent
5b4a23f25b
commit
d93c3e70d8
2 files changed
+8
-8
No files matched your search
@@ -79,14 +79,14 @@ Each transition's key is `pr-state:<owner/repo>#<number>:<type>:<headSha or prev
|
||||
|
||||
### 4. Delivery
|
||||
|
||||
- **Target.** One recipient per PR across the whole daemon, never one per workspace. Observation stays per workspace (each sweep views the open bound PRs of its own workspace, deduplicated by freshness as §1 says), but detection and delivery are PR-scoped. When a sweep detects a transition it writes the new snapshot, the fired record and the `pending` entry to **every active sidecar bound to the PR, in every trusted workspace** — the process-global sidecar scan behind `GET /sessions/pr-bindings` enumerates them — through each sidecar's own mutation queue and lock. The recipient is resolved lazily, at delivery time: among every eligible session bound to the PR across all trusted workspaces, the most recently active one (transcript mtime, refined by the bridge's live activity), the rule the channel applies to the whole bindings snapshot today, dispatched through that session's owning runtime. Because `pending` is PR-scoped, a recipient that is archived or outranked between detection and drain loses nothing: the drain finds the same entry in a sibling sidecar and delivers it under its original key. **Baseline rule:** detection never compares the fresh view with the sweep's own sidecar alone; before comparing, the sweep reads the PR's `watch` block from every sidecar bound to it and takes the newest by `observedAt` as the baseline — the whole block of that one observation (`state`, `headSha`, `checks`, `mergeable`, and its still-pending entries), never fields mixed from several sidecars. A sibling left behind by a crash adopts the newer block silently, pending entries included — no transition, no new key. A `partial` answer from the scan is no baseline evidence: if the scan could not enumerate every sidecar bound to the PR (a draining, transitioning or blocked workspace, a failed sidecar read), the sweep skips detection for that PR this sweep instead of comparing against an unconfirmed baseline; the slim-listing `state` refresh is unaffected. **Trust and retirement:** each sibling is re-resolved immediately before its write, and the recipient immediately before dispatch; a workspace that is no longer registered or trusted, or whose runtime generation is retired (`assertGenerationOpen()`), is skipped with a log line and treated as the crash window (its sidecar catches up through the baseline rule later), and an untrusted workspace is never a delivery target. Two registered workspaces for one repository, each with a session following the same PR, therefore produce one delivery, not two, with or without a crash in between. Archived sessions are refreshed for `state` but never delivered to. Channel-owned sessions (`sourceType: 'channel'`) and machine-owned hidden-catalog sessions are never targets.
|
||||
- **Target.** One recipient per PR across the whole daemon, never one per workspace. Observation stays per workspace (each sweep views the open bound PRs of its own workspace, deduplicated by freshness as §1 says), but detection and delivery are PR-scoped. When a sweep detects a transition it writes the new snapshot, the fired record and the `pending` entry to **every active sidecar bound to the PR, in every trusted workspace** — the process-global sidecar scan behind `GET /sessions/pr-bindings` enumerates them — through each sidecar's own mutation queue and lock. The recipient is resolved lazily, at delivery time, for keys not yet admitted (an admitted key is never re-targeted, §5): among every eligible session bound to the PR across all trusted workspaces, the most recently active one (transcript mtime, refined by the bridge's live activity), the rule the channel applies to the whole bindings snapshot today, dispatched through that session's owning runtime. Because `pending` is PR-scoped, a recipient that is archived or outranked between detection and drain loses nothing: the drain finds the same entry in a sibling sidecar and delivers it under its original key. **Baseline rule:** detection never compares the fresh view with the sweep's own sidecar alone; before comparing, the sweep reads the PR's `watch` block from every sidecar bound to it and takes the newest by `observedAt` as the baseline — the whole block of that one observation (`state`, `headSha`, `checks`, `mergeable`, and its still-pending entries), never fields mixed from several sidecars. A sibling left behind by a crash adopts the newer block silently, pending entries included — no transition, no new key. A `partial` answer from the scan is no baseline evidence: if the scan could not enumerate every sidecar bound to the PR (a draining, transitioning or blocked workspace, a failed sidecar read), the sweep skips detection for that PR this sweep instead of comparing against an unconfirmed baseline; the slim-listing `state` refresh is unaffected. **Trust and retirement:** each sibling is re-resolved immediately before its write, and the recipient immediately before dispatch; a workspace that is no longer registered or trusted, or whose runtime generation is retired (`assertGenerationOpen()`), is skipped with a log line and treated as the crash window (its sidecar catches up through the baseline rule later), and an untrusted workspace is never a delivery target. Two registered workspaces for one repository, each with a session following the same PR, therefore produce one delivery, not two, with or without a crash in between. Archived sessions are refreshed for `state` but never delivered to. Channel-owned sessions (`sourceType: 'channel'`) and machine-owned hidden-catalog sessions are never targets.
|
||||
- **Mechanism.** The refresh calls the daemon's prompt admission in-process — the code behind `POST /session/:id/prompt` — with a daemon-origin classification that admission treats like the channel bound relay: not an interactive user prompt (no loop-detected rejection, no PR-binding backfill on the injected text, a "PR event" display projection), with the transition key as the `idempotencyKey` so a retry within the daemon's lifetime deduplicates into the original admission. The classification is provenance-bound, not string-bound: it is passed as a trusted in-process parameter of the admission call (the shape `context.channelPrompt` already has, which the route and the bridge re-arm only from trusted context, never from request `_meta`). The marker name `qwen.daemon.prStateEvent` appears in the display projection and in both strip lists (route and bridge) as defence in depth; a client body carrying it is stripped and gains nothing, pinned by a route test like the existing forged-`qwen.channel.prompt` cases.
|
||||
- **Dormant sessions.** A session that is bound but not live is restored through the same resume path the channel accessor uses, then delivered to; `wakeDormant` is on by default so a daemon restart does not lose events; with `wakeDormant: false` the transition stays in `pending` and is delivered on the session's next live turn (the drain also runs on resume), the 16-per-PR cap still applies and eviction logs, so nothing vanishes silently. A session that cannot be restored (record deleted, archived, worktree superseded) drops the transition with a log line; nothing is ever published anywhere.
|
||||
- **Prompt text.** The channel's bound prompt shape: an event header, the untrusted-event-data notice, PR identity (repo, number, title, URL), an `Event:` line and the summary (`ci_failure` names up to five failed checks from the rollup). The reply stays in the session.
|
||||
|
||||
### 5. Durability
|
||||
|
||||
Detection and the new snapshot are persisted by one sidecar write per sidecar bound to the PR, each with the transition appended to its `pending`; delivery then drains `pending` oldest-first, grouping identical keys across the PR's sidecars, admits each key once to the recipient resolved at that moment, and clears the key from every sidecar after admission. The four crash windows resolve as: before the first write, the transition is re-detected next sweep (no loss, nothing delivered); between sidecar writes, the sidecars left behind catch up through the baseline rule — adopting the newer block and its pending entries without detecting anything — and the next sweep drains the key once; after all writes and before delivery, the next sweep drains `pending` first; after delivery and before the clear, the next sweep re-delivers and the admission ring deduplicates it within the daemon's lifetime (at-least-once across a restart, the same trade the channel made). `pending` is capped at 16 per PR; overflow drops the oldest with a log line.
|
||||
Detection and the new snapshot are persisted by one sidecar write per sidecar bound to the PR, each with the transition appended to its `pending`; delivery then drains `pending` oldest-first, grouping identical keys across the PR's sidecars, admits each key once to the recipient resolved at that moment, records the admitted key (per `owner/repo#number`) in an in-process set for the daemon's lifetime, and clears the key from every sidecar after admission. That record, not the prompt route's admission ring, is what makes a retry safe: the ring is keyed by session and key, so it cannot suppress the same key admitted to a different session. If a replica clear fails and the most recently active binder changes before the retry, the next drain finds the key in the admitted set and only retries the cleanup — it never selects a new recipient for a key already admitted; lazy recipient selection applies only to keys not yet admitted. The four crash windows resolve as: before the first write, the transition is re-detected next sweep (no loss, nothing delivered); between sidecar writes, the sidecars left behind catch up through the baseline rule — adopting the newer block and its pending entries without detecting anything — and the next sweep drains the key once; after all writes and before delivery, the next sweep drains `pending` first; after delivery and before the clear, the next sweep only retries the clear within the daemon's lifetime (the admitted set remembers the key, whoever the recipient would be now); across a restart the set is gone and the key is re-delivered once (at-least-once, the same trade the channel made). `pending` is capped at 16 per PR; overflow drops the oldest with a log line.
|
||||
|
||||
### 6. Configuration
|
||||
|
||||
@@ -129,14 +129,14 @@ The GitHub channel's `deliverToBoundSessions` is unchanged. `prWatchEvents`, the
|
||||
|
||||
## Validation plan
|
||||
|
||||
- Unit: view mapping (`headRefOid`, `mergeable` states, rollup); transition rules including once-per-head `ci_failure` across a force-push revisit (`A failing → B failing → A failing` fires twice, not three times), edge-triggered conflict, reopen → close; selection among several binders, including binders in different workspaces; pending persistence and drain across simulated crashes, including a crash between the recipient's sidecar write and the sibling writes for a head change and for a terminal `state_change` observed through the slim listing (the sibling adopts the newer baseline by `observedAt`, detects nothing, and the pending transition is admitted exactly once under its original key); configuration parsing; the over-length key hashing; the `partial`-scan skip (no detection, no second key); `UNKNOWN` after `CONFLICTING` preserving the stored value and emitting no transition; a recipient archived or outranked between detection and drain (one admission under the original key from a sibling sidecar); `wakeDormant: false` holding the entry until the next live turn; and the setting unset → zero `gh pr view` invocations with the listing count unchanged.
|
||||
- Unit: view mapping (`headRefOid`, `mergeable` states, rollup); transition rules including once-per-head `ci_failure` across a force-push revisit (`A failing → B failing → A failing` fires twice, not three times), edge-triggered conflict, reopen → close; selection among several binders, including binders in different workspaces; pending persistence and drain across simulated crashes, including a crash between the recipient's sidecar write and the sibling writes for a head change and for a terminal `state_change` observed through the slim listing (the sibling adopts the newer baseline by `observedAt`, detects nothing, and the pending transition is admitted exactly once under its original key); configuration parsing; the over-length key hashing; the `partial`-scan skip (no detection, no second key); `UNKNOWN` after `CONFLICTING` preserving the stored value and emitting no transition; a recipient archived or outranked between detection and drain (one admission under the original key from a sibling sidecar); a partial clear followed by a recipient change (K admitted to A, B's copy not cleared, B becomes the most recently active binder → no second admission, only the cleanup is retried); `wakeDormant: false` holding the entry until the next live turn; and the setting unset → zero `gh pr view` invocations with the listing count unchanged.
|
||||
- Refresh tests (existing harness with a fake `fetchPullRequests`): events delivered once per transition, dormant session restored, archived and channel-owned sessions skipped, cap and rotation (two consecutive sweeps over 40 PRs view disjoint windows), the aggregate view budget, trust/retirement re-resolution before a sibling write, and the overlap rule (a repository owned by a configured channel watch is skipped).
|
||||
- E2E (daemon + stubbed `gh` binary): bind a PR in a session, flip the stub's rollup to failing, observe one injected prompt in that session after the next sweep and none after a daemon restart.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
1. With `sessionPrWatch.events` set, a bound session receives exactly one prompt per transition instance for CI failure, merge conflict, head change and merge/close, with the same recurrence semantics as the channel watch, without any GitHub channel configured.
|
||||
2. A daemon restart between detection and delivery delivers the transition once; a restart after delivery re-delivers at most once.
|
||||
2. A daemon restart between detection and delivery delivers the transition once; a restart after delivery re-delivers at most once; without a restart a key is admitted at most once, even if replica cleanup fails and the most recently active binder changes before the retry.
|
||||
3. The GitHub channel no longer issues any PR-state API call; a configuration with `prWatchEvents` still connects, logs exactly one deprecation warning naming `sessionPrWatch`, and runs no watch pass; from the first minor release after PR B it is rejected at `connect()` with the same message.
|
||||
4. With the setting unset, the refresh's behaviour and API call volume are unchanged.
|
||||
5. Two trusted workspaces for one repository, each with a session bound to the same PR, receive one delivery per transition, to the most recently active of those sessions, also when the daemon restarts between sidecar writes or the recipient is archived or outranked between detection and drain; and a force-push back to a head whose CI failure already fired does not fire `ci_failure` again while that head is within the PR's 16-head history.
|
||||
|
||||
@@ -79,14 +79,14 @@ watch?: {
|
||||
|
||||
### 4. 投递
|
||||
|
||||
- **目标。** 整个 daemon 范围内每个 PR 只有一个收件会话,绝不是每个 workspace 一个。观察仍按 workspace 进行(每次扫描只 view 本 workspace 的 open 绑定 PR,按 §1 的新鲜度去重),但检测与投递以 PR 为范围。扫描检测到转换时,把新快照、已触发记录与 `pending` 条目写进**该 PR 在所有受信 workspace 的每个 active 绑定 sidecar**——由 `GET /sessions/pr-bindings` 背后的进程级 sidecar 扫描枚举——各走各自的变更队列与锁。收件者在投递时才惰性决定:在所有受信 workspace 中绑定该 PR 的全部合格会话里取最近活跃的那个(transcript mtime,再以 bridge 的 live 活动修正),这正是 channel 今天对整个绑定快照应用的规则,经该会话所属的 runtime 派发。因为 `pending` 以 PR 为范围,检测与排空之间被归档或被别的会话超过的收件者不会丢任何东西:排空在兄弟 sidecar 里找到同一条目,以其原始 key 投递。**基线规则:**检测绝不只拿新鲜 view 与本次扫描自己的 sidecar 比较;比较之前,扫描读取该 PR 在每个绑定 sidecar 中的 `watch` 块,取 `observedAt` 最新的作为基线——取那一次观察的整个块(`state`、`headSha`、`checks`、`mergeable` 及其仍待投递的条目),绝不从多个 sidecar 拼字段。被崩溃落下的兄弟静默采用更新的块,连同其 pending 条目——没有转换、没有新 key。扫描的 `partial` 答案不是基线证据:若扫描无法枚举该 PR 的每个绑定 sidecar(draining、transitioning 或 blocked 的 workspace,某个 sidecar 读取失败),本次扫描对该 PR 跳过检测而不是对着未经确认的基线比较;slim 列表的 `state` 刷新不受影响。**信任与退役:**每个兄弟在写入前、收件者在派发前立即重新解析;不再注册、不再受信或 runtime 代际已退役(`assertGenerationOpen()`)的 workspace 记一行日志跳过并视同崩溃窗口(其 sidecar 之后按基线规则追平),不受信的 workspace 永远不是投递目标。因此同一仓库注册了两个 workspace、各有一个会话跟随同一个 PR 时,无论中间是否崩溃,都只投递一次而非两次。归档会话只刷新 `state`,绝不投递。channel 自有会话(`sourceType: 'channel'`)与机器自有的隐藏目录会话永远不是目标。
|
||||
- **目标。** 整个 daemon 范围内每个 PR 只有一个收件会话,绝不是每个 workspace 一个。观察仍按 workspace 进行(每次扫描只 view 本 workspace 的 open 绑定 PR,按 §1 的新鲜度去重),但检测与投递以 PR 为范围。扫描检测到转换时,把新快照、已触发记录与 `pending` 条目写进**该 PR 在所有受信 workspace 的每个 active 绑定 sidecar**——由 `GET /sessions/pr-bindings` 背后的进程级 sidecar 扫描枚举——各走各自的变更队列与锁。收件者在投递时才惰性决定,且只对尚未准入的 key(已准入的 key 绝不改投,§5):在所有受信 workspace 中绑定该 PR 的全部合格会话里取最近活跃的那个(transcript mtime,再以 bridge 的 live 活动修正),这正是 channel 今天对整个绑定快照应用的规则,经该会话所属的 runtime 派发。因为 `pending` 以 PR 为范围,检测与排空之间被归档或被别的会话超过的收件者不会丢任何东西:排空在兄弟 sidecar 里找到同一条目,以其原始 key 投递。**基线规则:**检测绝不只拿新鲜 view 与本次扫描自己的 sidecar 比较;比较之前,扫描读取该 PR 在每个绑定 sidecar 中的 `watch` 块,取 `observedAt` 最新的作为基线——取那一次观察的整个块(`state`、`headSha`、`checks`、`mergeable` 及其仍待投递的条目),绝不从多个 sidecar 拼字段。被崩溃落下的兄弟静默采用更新的块,连同其 pending 条目——没有转换、没有新 key。扫描的 `partial` 答案不是基线证据:若扫描无法枚举该 PR 的每个绑定 sidecar(draining、transitioning 或 blocked 的 workspace,某个 sidecar 读取失败),本次扫描对该 PR 跳过检测而不是对着未经确认的基线比较;slim 列表的 `state` 刷新不受影响。**信任与退役:**每个兄弟在写入前、收件者在派发前立即重新解析;不再注册、不再受信或 runtime 代际已退役(`assertGenerationOpen()`)的 workspace 记一行日志跳过并视同崩溃窗口(其 sidecar 之后按基线规则追平),不受信的 workspace 永远不是投递目标。因此同一仓库注册了两个 workspace、各有一个会话跟随同一个 PR 时,无论中间是否崩溃,都只投递一次而非两次。归档会话只刷新 `state`,绝不投递。channel 自有会话(`sourceType: 'channel'`)与机器自有的隐藏目录会话永远不是目标。
|
||||
- **机制。** 刷新在进程内调用 daemon 的 prompt 准入——`POST /session/:id/prompt` 背后的代码——并带上 daemon 来源分类,准入把它当作 channel 绑定中继那样处理:不是交互式用户 prompt(不做循环检测拒绝,不对注入文本做 PR 绑定回填,显示投影为"PR 事件"),以转换 key 作为 `idempotencyKey`,因此 daemon 生命周期内的重试会去重到原始准入。该分类绑定来源而非字符串:作为准入调用的受信进程内参数传入(即 `context.channelPrompt` 已有的形状,路由与 bridge 只从受信 context 重新装配它,绝不从请求的 `_meta`)。标记名 `qwen.daemon.prStateEvent` 只出现在显示投影和两份剥离清单(路由与 bridge)中作为纵深防御;携带它的客户端请求体会被剥离且得不到任何授权,并由一个与现有伪造 `qwen.channel.prompt` 用例同形的路由测试钉住。
|
||||
- **休眠会话。** 已绑定但不在线的会话通过 channel 访问器所用的同一恢复路径恢复后再投递;`wakeDormant` 默认开启,这样 daemon 重启不会丢事件;`wakeDormant: false` 时转换留在 `pending`,在该会话下一次 live 回合投递(排空在恢复时也会运行),每个 PR 16 条的上限仍然适用且淘汰会记日志,不会静默消失。无法恢复的会话(记录已删除、已归档、worktree 被取代)丢弃该转换并记一行日志;任何地方都不会公开发布。
|
||||
- **prompt 文本。** 沿用 channel 绑定 prompt 的形状:事件头、不可信事件数据提示、PR 身份(仓库、编号、标题、URL)、一行 `Event:` 与摘要(`ci_failure` 从 rollup 中列出最多五个失败的 check)。回复留在会话内。
|
||||
|
||||
### 5. 持久性
|
||||
|
||||
检测结果与新快照按该 PR 的每个绑定 sidecar 各一次写入持久化,每次都把转换追加到该 sidecar 的 `pending`;投递随后按最旧优先排空 `pending`,把同一 key 在该 PR 各 sidecar 中的副本合并,对当时解析出的收件者每个 key 只准入一次,准入后从每个 sidecar 清除该 key。四个崩溃窗口的处理:首次写入前崩溃,下次扫描重新检测(无丢失,也没投递过任何东西);sidecar 写入之间崩溃,被落下的 sidecar 按基线规则追平——采用更新的块连同其 pending 条目而不检测任何东西——下次扫描把该 key 排空一次;全部写入后、投递前崩溃,下次扫描先排空 `pending`;投递后、清除前崩溃,下次扫描重新投递,准入环在 daemon 生命周期内去重(跨重启是 at-least-once,与 channel 的取舍相同)。`pending` 每个 PR 上限 16 条;溢出时丢弃最旧的并记日志。
|
||||
检测结果与新快照按该 PR 的每个绑定 sidecar 各一次写入持久化,每次都把转换追加到该 sidecar 的 `pending`;投递随后按最旧优先排空 `pending`,把同一 key 在该 PR 各 sidecar 中的副本合并,对当时解析出的收件者每个 key 只准入一次,把已准入的 key(按 `owner/repo#number`)记入 daemon 生命周期内的进程内集合,准入后从每个 sidecar 清除该 key。让重试安全的是这份记录而非 prompt 路由的准入环:准入环按会话加 key 键控,挡不住同一 key 对另一会话的准入。若某个副本清除失败、重试前最近活跃的绑定者又换了人,下次排空在已准入集合里找到该 key,只重试清除——绝不为已准入的 key 再选新收件者;惰性选收件者只对尚未准入的 key 生效。四个崩溃窗口的处理:首次写入前崩溃,下次扫描重新检测(无丢失,也没投递过任何东西);sidecar 写入之间崩溃,被落下的 sidecar 按基线规则追平——采用更新的块连同其 pending 条目而不检测任何东西——下次扫描把该 key 排空一次;全部写入后、投递前崩溃,下次扫描先排空 `pending`;投递后、清除前崩溃,daemon 生命周期内下次扫描只重试清除(已准入集合记得该 key,无论此刻收件者会是谁);跨重启集合丢失,该 key 重新投递一次(at-least-once,与 channel 的取舍相同)。`pending` 每个 PR 上限 16 条;溢出时丢弃最旧的并记日志。
|
||||
|
||||
### 6. 配置
|
||||
|
||||
@@ -129,14 +129,14 @@ GitHub channel 的 `deliverToBoundSessions` 不变。`prWatchEvents`、监视通
|
||||
|
||||
## 验证计划
|
||||
|
||||
- 单元:view 映射(`headRefOid`、`mergeable` 状态、rollup);转换规则,包括 force-push 回访下每个 head 一次的 `ci_failure`(`A failing → B failing → A failing` 触发两次而非三次)、边沿触发的冲突、重开 → 关闭;多个绑定者之间的选择,含分属不同 workspace 的绑定者;模拟崩溃下的 pending 持久化与排空,包括 head 变更与经 slim 列表观察到的终态 `state_change` 两种情形下收件者 sidecar 写入与兄弟写入之间的崩溃(兄弟按 `observedAt` 采用更新的基线、不检测任何东西,pending 中的转换以原始 key 恰好准入一次);配置解析;超长 key 的哈希;`partial` 扫描的跳过(不检测、不产生第二个 key);`CONFLICTING` 之后的 `UNKNOWN` 保留已存值且不产生转换;检测与排空之间收件者被归档或被超过(从兄弟 sidecar 以原始 key 恰好准入一次);`wakeDormant: false` 把条目留到下一次 live 回合;以及未设置该项时零次 `gh pr view` 且列表调用次数不变。
|
||||
- 单元:view 映射(`headRefOid`、`mergeable` 状态、rollup);转换规则,包括 force-push 回访下每个 head 一次的 `ci_failure`(`A failing → B failing → A failing` 触发两次而非三次)、边沿触发的冲突、重开 → 关闭;多个绑定者之间的选择,含分属不同 workspace 的绑定者;模拟崩溃下的 pending 持久化与排空,包括 head 变更与经 slim 列表观察到的终态 `state_change` 两种情形下收件者 sidecar 写入与兄弟写入之间的崩溃(兄弟按 `observedAt` 采用更新的基线、不检测任何东西,pending 中的转换以原始 key 恰好准入一次);配置解析;超长 key 的哈希;`partial` 扫描的跳过(不检测、不产生第二个 key);`CONFLICTING` 之后的 `UNKNOWN` 保留已存值且不产生转换;检测与排空之间收件者被归档或被超过(从兄弟 sidecar 以原始 key 恰好准入一次);部分清除后收件者变化(K 已准入 A,B 的副本未清除,B 成为最近活跃的绑定者 → 不再二次准入,只重试清除);`wakeDormant: false` 把条目留到下一次 live 回合;以及未设置该项时零次 `gh pr view` 且列表调用次数不变。
|
||||
- 刷新测试(现有带假 `fetchPullRequests` 的测试框架):每个转换投递一次、休眠会话被恢复、归档与 channel 自有会话被跳过、上限与旋转(对 40 个 PR 的两次连续扫描 view 不相交的窗口)、聚合 view 预算、兄弟写入前的信任/退役重新解析,以及重叠规则(已配置 channel 监视的仓库被跳过)。
|
||||
- E2E(daemon + 桩 `gh` 二进制):在会话中绑定 PR,把桩的 rollup 翻成失败,下一次扫描后在该会话观察到恰好一条注入 prompt,daemon 重启后没有新的注入。
|
||||
|
||||
## 验收标准
|
||||
|
||||
1. 设置 `sessionPrWatch.events` 后,绑定会话对 CI 失败、合并冲突、head 变更、合并/关闭的每个转换实例恰好收到一条 prompt,递归语义与 channel 监视相同,且无需配置任何 GitHub channel。
|
||||
2. 检测与投递之间的 daemon 重启投递该转换一次;投递之后的重启最多重新投递一次。
|
||||
2. 检测与投递之间的 daemon 重启投递该转换一次;投递之后的重启最多重新投递一次;不重启时每个 key 最多准入一次,即使副本清除失败且重试前最近活跃的绑定者换了人。
|
||||
3. GitHub channel 不再发出任何 PR 状态 API 调用;带 `prWatchEvents` 的配置仍能连接,只记一条点名 `sessionPrWatch` 的弃用告警且不跑监视通道;PR B 之后的第一个 minor 版本起在 `connect()` 以同一条信息拒绝。
|
||||
4. 未设置该项时,刷新的行为与 API 调用量不变。
|
||||
5. 同一仓库的两个受信 workspace 各有一个会话绑定同一个 PR 时,每个转换只投递一次,给其中最近活跃的会话,daemon 在 sidecar 写入之间重启、或收件者在检测与排空之间被归档或被超过时亦然;force-push 回到一个 CI 失败已触发过的 head 时,只要该 head 仍在该 PR 的 16 个 head 历史内,`ci_failure` 不再触发。
|
||||
|
||||
Reference in new issue
Block a user