Skip to content

fix(refresh): require explicit acknowledgement to resume paused external delivery - #4169

Merged
huangruiteng merged 3 commits into
loopx-project:mainfrom
xyx2002OvO:codex/refresh-external-delivery-resume
Sep 11, 2026
Merged

huangruiteng merged 3 commits into
loopx-project:mainfrom
xyx2002OvO:codex/refresh-external-delivery-resume

Conversation

@xyx2002OvO

@xyx2002OvO xyx2002OvO commented Sep 10, 2026 •

Copy link
Copy Markdown
Contributor

A Turn-bound refresh could temporarily suppress external sinks and then accidentally re-enable delivery when a recovery command omitted that flag. Record the pause in the existing rollout journal and require --resume-external-sinks <resume_key> to acknowledge the current pause before send-capable recovery proceeds.

The TypeScript quota owner decides admission; the Python adapter persists transitions under the existing refresh lock, and both Explore and Goal Channel use that decision. Re-pausing invalidates the previous key. Receipt-only repair stays local and dry-run persists no transition. Historical operations without pause evidence and non-Turn refreshes retain their previous behavior. The acknowledgement does not grant provider permissions; rollback to older binaries loses enforcement.

Scope excludes target migration, global-sync permission persistence, general hook changes, lock redesign, and exactly-once transport. A bounded companion cleanup moves the existing early recovery payload into its owning quota adapter to reduce growth in state_refresh.py.

Validation:

  • Updated to main after fix(change-window): accept empty Git reference transactions #4171 fixed the pre-existing Git 2.55 empty-reference-transaction failure. No separate Git-hook change is included in this PR diff.
  • Both complete Linux Python shards and the pytest and merge-gate checks passed.
  • Control-plane typecheck/tests, CLI output checks, minimum/forward Node compatibility, native Windows checks, Stage 2C end-to-end/mutant/installed suites, builds, DCO and dependency review passed.
  • The real CLI recovery matrix covers retained isolation, omitted flags, mismatched keys, explicit resume, repeated replay and a second pause invalidating the old key. No file-lock or persistence checks were bypassed.
  • Verified head: 31e5b8f2ff3bd22824e9270efd37c51276719ac3; CI run.

Signed-off-by: Tartar <xiaoyx67@mail2.sysu.edu.cn>
Signed-off-by: Tartar <xiaoyx67@mail2.sysu.edu.cn>
…l-delivery-resume

Signed-off-by: Tartar <xiaoyx67@mail2.sysu.edu.cn>
@xyx2002OvO
xyx2002OvO marked this pull request as ready for review September 10, 2026 10:35

@huangruiteng huangruiteng left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

动机

本 PR 解决的是一个真实的外部交付恢复缺口:某个 exact Turn 因人工判断而暂停 Explore Graph / Goal Channel 等外部 sink 后,后续调用如果只是省略暂停参数,旧路径会重新进入交付,没有一个可机读、可回放的“必须显式恢复”边界。受影响的是在同一操作上重试 refresh-state 的自动化与维护者;错误恢复会把原本要求人工确认的外部副作用悄悄重新打开。最小修复不能只在 CLI 层记一个布尔值,因为两个 sink 共用同一交付授权且需要跨调用、跨进程重放;把状态转换留在既有 TypeScript 决策边界、由 Python 只做持久化适配,是较小且一致的方案。非目标是新增 provider 权限、替换现有 sink 或把本地 refresh 也强制外部交付。

改动思路

入口仍是 refresh-state。CLI 把当前调用意图和该 operation 的历史事件交给 TypeScript refreshExternalDelivery,由它判定 local、paused、resume_required 或 ready;Python 适配器只在现有 refresh 锁内持久化类型化转换,并把授权结果交给 Explore Graph 与 Goal Channel 两个既有消费者。正向路径是:首次 --suppress-external-sinks 写入 pause 事件;之后省略参数会稳定返回 external_delivery_resume_required;只有携带匹配恢复 key 的 --resume-external-sinks 才进入 ready。负向路径覆盖过期 key、跨 operation key、相关历史损坏及冲突恢复;这些情况均 fail closed,且 receipt-only 修复及无外部交付的调用仍保持本地。该设计复用了既有刷新锁、事件历史和 sink 调用点,没有建立第二套权限存储;provider 可用性也不会自动等价为恢复授权。

具体改动

生产改动集中在 TypeScript 状态机、Python 桥接以及两个现有 sink 的授权接线;测试补齐了正向恢复、重放、错误 key、损坏历史与调用隔离。exact head 为 31e5b8f2ff3bd22824e9270efd37c51276719ac3,共 14 个文件,约 +498/-56。

关键代码讲解

  • refreshExternalDelivery 是唯一的策略 owner:根据历史与本次显式意图产出类型化决策,避免 Python 重新解释状态。
  • Python refresh 适配器在原有锁和事件边界内提交 pause/resume 转换,不把序列化职责扩大成第二套业务规则。
  • Explore Graph 与 Goal Channel 只消费返回的授权结果;省略参数不再绕过已有暂停状态。
  • 历史读取对相关损坏 fail closed,但 receipt-only 修复与从未暂停过的旧 operation 保留原有本地/按调用行为。

对主干的风险

主要风险是恢复状态与 operation/Turn 身份绑定错误,导致错误放行、永久卡住,或把本地 refresh 误判成外部交付。针对这些风险,我执行了 23 个 Python 回归、5 个 TypeScript 状态机测试、control-plane typecheck、Ruff 与 git diff --check,均通过;测试覆盖匹配/过期/跨 operation key、损坏历史、重复恢复、两个 sink 以及无交付调用。当前 main 虽在该 head 之后继续前进,但受影响路径没有后续改动,clean merge-tree 也成功。剩余风险主要是不同真实 provider 的网络失败重试组合,现有设计仍由各 sink 保留失败/重试职责;本 PR 没有扩大它们的权限。

我的整体评价

没有发现阻断问题。这个改动把暂停/恢复从容易被“参数缺省”绕过的瞬时调用选择,收敛为可审计的类型化转换,同时保持 TypeScript 决策权和 Python/Provider 的既有职责。范围与故障成本相称,负向与重放覆盖充分,且没有引入推测性的通用框架。未来向 refactor pass 检查了相邻的重复规则:当前少量兼容错误文本有明确适配用途,不值得在本 PR 继续抽象。结论:批准。

English verdict: APPROVE at exact head 31e5b8f2ff3bd22824e9270efd37c51276719ac3; explicit resume is now machine-enforced by the existing typed decision boundary, with 23 Python tests, 5 TypeScript tests, typecheck, Ruff, and diff hygiene passing.

@huangruiteng
huangruiteng merged commit d76d3b7 into loopx-project:main Sep 11, 2026
21 checks passed
huangruiteng pushed a commit that referenced this pull request Sep 12, 2026
* fix(cli): restore module size and manpage classification budgets

Two required public smokes fail on current `main`.

`cli-command-module-size-ownership-command-modularization-smoke.py` reports
`project_lifecycle.py has 1007 lines, above budget 1000`. The file crossed the
budget when #4169 added the mutually exclusive external-sink delivery flags.
Extract the three typed inline input codecs into
`project_lifecycle_inputs.py`, which returns the owner to 925 lines without
changing any public invocation.

`cli-help-manpage-smoke.py` reports `unclassified: ['agent-context']`. The
command shipped in #4244 without a manpage class. Add it to the existing
`MANPAGE_COMMAND_HELP_ONLY` set, which is where comparable read-only
lifecycle helpers already live.

Extraction keeps the existing ownership contract: the registration and
dispatch markers asserted by
`cli-project-lifecycle-command-modularization-smoke.py` stay in the module,
and `PROJECT_LIFECYCLE_COMMANDS` still covers all four commands.

Validation:
- `python3 examples/cli-command-module-size-ownership-command-modularization-smoke.py` -> ok
- `python3 examples/cli-help-manpage-smoke.py` -> ok
- `python3 examples/cli-project-lifecycle-command-modularization-smoke.py` -> ok
- `python3 regression/cli-command-module-contract.py` -> ok
- `python -m pytest tests/cli_commands/ tests/control_plane/test_cli_output_budget.py` -> 94 passed

Signed-off-by: song <liusongstep@gmail.com>

* test(smokes): realign stale contracts with shipped behavior

Four required public smokes assert contracts that have since moved. Each is
reproducible on a clean `main@fa57253`, so this aligns the checks with the
shipped behavior rather than changing any product path.

- `blocker-push-runtime-smoke.py` asserted the retired per-shell phrasing
  `` `LOOPX_TURN=<current_time_iso>`; reuse. ``. #4201 moved the bootstrap rule
  into the shared `HEARTBEAT_TURN_BOOTSTRAP_RULE`, whose current sentence ends
  with `reuse the value on retries`. Assert that sentence.
- `install-local-smoke.py` required the accountable refresh and spend commands
  inside the `--brief` prompt, but brief mode renders exactly one bounded guard
  block by design; those commands belong to the full and compact modes. #4201
  already realigned the adjacent thin-mode assertions and missed this one.
  Assert the brief contract, including that the pair stays out.
- `github-actions-runtime-smoke.py` rejected the `22.14` SQLite runtime and the
  Node 26 forward job, and required the pre-#4241 `merge-gate` needs order.
  Record `SQLITE_NODE_VERSION` with its finalization rationale, extend the
  `python-tests.yml` expectation, and match the current needs list.
- `repository-hygiene-smoke.py` fails because the `v1.0.3` tag exists without a
  timeline entry. Add the entry, following the existing format.

Validation (each command exits 0):
- `python3 examples/blocker-push-runtime-smoke.py`
- `python3 examples/install-local-smoke.py`
- `python3 examples/github-actions-runtime-smoke.py`
- `python3 examples/repository-hygiene-smoke.py`
- `python3 examples/release/release-readiness-doc-smoke.py`
- `python -m pytest tests/control_plane/test_heartbeat_notification_rule.py tests/control_plane/test_heartbeat_prompt_support.py tests/control_plane/test_heartbeat_receipt.py tests/control_plane/test_heartbeat_recommendation_rules.py` -> 48 passed

Signed-off-by: song <liusongstep@gmail.com>

* test(ci): qualify runtime pins per workflow lane

Signed-off-by: song <liusongstep@gmail.com>

* test(ci): ignore comments when checking qualified runtime pins

Signed-off-by: song <liusongstep@gmail.com>

---------

Signed-off-by: song <liusongstep@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants