Skip to content

feat(manager): reconcile uncertain delegated result delivery - #4372

Merged
huangruiteng merged 11 commits into
loopx-project:mainfrom
LIHUA919:codex/m3-delivery-verification
Sep 15, 2026
Merged

huangruiteng merged 11 commits into
loopx-project:mainfrom
LIHUA919:codex/m3-delivery-verification

Conversation

@LIHUA919

@LIHUA919 LIHUA919 commented Sep 14, 2026 •

Copy link
Copy Markdown
Contributor

Problem and resulting behavior

A delegated worker result was already durable, but when Lark reported that an external write occurred and immediate readback could not verify it, the return pump parked the result in verification_required forever. Restarting the service did not reconcile the original message, and safely retrying the send was impossible without risking a duplicate external effect.

This PR records the provider's message locator before readback, then lets the existing return pump verify that exact prior message without sending again. A successful readback advances the original delivery to delivered; provider outages stay retryable, while missing legacy locators, changed intent/audience, missing messages, revoked authority and verified mismatches remain explicitly unverified.

Ownership and compatibility

  • manager-context remains the result and delivery-state owner; no second outbox, task database or capability is added.
  • The Lark extension owns its private locator and readback commands. Provider identifiers never enter CLI, Manager-read or frontend projections.
  • Existing result identity, original return route, authority checks and transcript idempotency are retained. The worker/model is never rerun and recovery performs zero send calls.
  • Legacy delivery records remain readable. A legacy verification_required record without a trustworthy locator becomes explicit_unverified instead of being guessed or resent.
  • M2 WorkRequest/Assessment replacement, Todo/lease/Goal authority, session takeover, unknown-locator recovery, provider promotion and the private Lark canary remain outside this PR.

The related future-facing pass keeps provider-neutral state transitions in manager-context, centralizes Lark intent and mention readback rules, and moves Chat snapshot projection out of the already-hot server facade. No speculative provider framework was added.

Following the maintainer's ownership-map feedback, provider-neutral attempt validation and verification classification now run through the typed control_plane/collaboration/return_delivery.ts boundary. Python owns file locks, persistence and adapter orchestration; Lark keeps provider interpretation and readback.

TypeScript migration economics

  • Old → new semantic owner: Python manager_context/roundtrip.py regex/schema validation and provider-blocker classification → typed TS collaboration contract. The Python result/delivery writer remains until the broader M2 transaction migration is qualified.
  • Deleted semantics: removed the duplicate Python attempt field/pattern validator and verification blocker classifier; no compatibility facade or second validator remains.
  • Bridge: two registered effect-runtime methods with real callers: manager.return_delivery.normalize_attempt and manager.return_delivery.classify_verification.
  • Round trips: immediate send/readback adds one local TS normalization call before provider readback. Recovery adds one attempt-normalization call and one classification call around the single provider read; public status reads add zero TS calls.
  • Code volume: runtime/product code is net +89 lines for the typed contract, package declaration, handler registration and smaller Python adapter; TS tests are +65 lines and docs net +3.
  • Remaining caller and removal gate: roundtrip.py continues to own durable file transactions and retry timing. It may retire only when M2 migrates the complete result/return transaction with legacy parity, one writer and crash reconciliation; this PR does not claim that migration.

User entry points

  • Chat return service: persists the attempt before readback and reconciles it after process restart.
  • Manager CLI/readback: exposes the same public-safe queued, verification_required, delivered and explicit_unverified facts.
  • Personal Workspace: polls the same returned transcript row and updates its compact status in place. It shows “verifying without resending”, “verified after recovery”, or an explicit unverified result on desktop and mobile.
  • Lark: reuses the existing bound bot, source message, live authority and exact message readback. No new Lark configuration source is introduced.

RFC acceptance coverage

  • A8: synthetic interruption after provider send, process/store restart, repeated pump and successful read-only reconciliation; external send count remains one.
  • A9: long Markdown result falls back to the full text transport and can later be verified without resending; existing truncated-envelope parser regressions remain included.
  • A10: manager-inbox/Manager-read and Chat use the same redacted delivery state; packaged browser smoke verifies the in-place status transition and mobile layout. The owner-authorized private Lark canary remains the maintainer's post-PR exact-head step.
  • A17 partial: covers only return-pump/process restart after an external send, as agreed in Help wanted: capable manager & semantic handoff / 强能力管家与语义交接(RFC #4330) #4339; no session takeover claim.

Validation

Base: 6bb41310517692c2266f7e4c38054e28c6a496e6
Head: 196e67174ec7c2d3dd27ddbac370ea0901143bb1

  • 326 passed — all tests/test_chat*.py, tests/test_manager_context*.py, and the four affected Lark reply/return suites with Node 24.19.
  • Dashboard npm run build passed; packaged Chat assets were rebuilt.
  • npm run smoke:personal-workspace-packaged passed: navigation-sorting, chat-recovery, typed-actions.
  • Ruff passed on every changed Python source/test; git diff --check passed.
  • Control-plane TypeScript typecheck passed; the new contract plus runtime-handler tests passed 12/12.
  • A local isolated wheel was built, inspected for collaboration/{__init__.py,return_delivery.ts}, installed into an empty target, and successfully executed both new effect-runtime methods. This fixes the first CI head's Release Artifacts / installed-stage failure, which showed the new TS subpackage was absent from the wheel.
  • Maintainability ratchet passed with no new debt.
  • loopx canary premerge --from-git-diff passed all 18 selected direct/catalog/risk/public-boundary checks; no manual holds.

The full TS suite reported 1,530 passing, 1 skipped and 7 failures on the first run. Five were nested Python runtime startup failures and passed when rerun with the required local runtime access. The remaining two legacy monitor codec assertions (null !== 0) reproduce unchanged on the exact base; this PR does not alter either module.

Two repository-wide checks were attempted and confirmed failing unchanged on the exact base: the Personal Workspace aggregate source-contract smoke stops at its existing manager-projection phrase assertion, and docs governance reports the existing missing Chinese mirror for manager-runtime-profile-v0.md. The new return-delivery assertions execute before the unrelated aggregate failure; the built packaged browser journey is the behavior-level UI evidence for this PR.

Private Lark canary was not run because the maintainer explicitly retained that credentialed step. Live provider readback therefore remains unverified; the recovery path is covered by the synthetic interruption, restart and readback fixtures above.

Maintainer follow-up at 196e67174

The maintainer fixed the three blocking findings from review and merged the latest main into this branch.

State-machine completeness (fixes the reviewed defects):

  • A verification_required record written before locators were persisted has no trustworthy provider identity. _delivery_attempt(None) raises the effect runtime's rejection, which is a RuntimeError, so the pre-existing except ValueError never fired: the record fell into the generic retry branch, became retry_pending, and the next pump resent the already published conclusion. Locator-less and un-normalizable attempts now terminalize as explicit_unverified/provider_locator_unavailable before that branch. Regression: send count stays 0, including after the backoff window.
  • Adapters now raise a typed ReturnResolutionBlocked(reason, message), so delivery state stops depending on provider prose. context return authority revoked matched no token in the previous classifier, so a revoked return neither terminalized nor backed off and re-ran the full provider readback every ~3s. Prose classification is retained for text that still arrives that way and now also recognises authority.
  • An unclassified readback failure keeps the locator and stays retryable, but records attempts/retry_at instead of re-running the provider on every pump. Backoff is scoped to the verification read, so the send-interruption recovery path still reconciles on the next pump.

Packaged assets: main's prior generation stays in the retention window, and the bundle was rebuilt from the merged dashboard sources rather than hand-merged.

Validation at this head:

  • 692 passed — tests/test_chat*.py, tests/test_manager_context*.py, tests/extensions/test_lark_*.py.
  • Regression tests fail before the fix (locator-less record reached retry_pending; revoked authority never terminalized) and pass after it.
  • ruff clean on every changed file; git diff --check clean.
  • npm run smoke:personal-workspace-packaged passed: navigation-sorting, chat-recovery, typed-actions, with the built bundle containing the new delivery copy in both languages.
  • The chat bundle rebuild is idempotent, and the same toolchain reproduces main's committed packaged assets exactly.
  • loopx canary premerge --from-git-diff passed every selected direct, catalog, risk-profile and public-boundary check with no manual holds.

Still out of scope and unverified: M2 transaction migration, Todo/lease/Goal authority, session takeover, unknown-locator recovery, provider promotion and the credentialed private Lark canary.

Coordination: #4339. Ownership map: #4339 (comment)

Signed-off-by: Lihua <1017343802@qq.com>
Signed-off-by: Lihua <1017343802@qq.com>
Signed-off-by: Lihua <1017343802@qq.com>
Signed-off-by: Lihua <1017343802@qq.com>
Signed-off-by: Lihua <1017343802@qq.com>
Signed-off-by: Lihua <1017343802@qq.com>
Signed-off-by: Lihua <1017343802@qq.com>
@LIHUA919

Copy link
Copy Markdown
Contributor Author

CI repair receipt for exact head f15b6eb4b77da7ee287839a115c0e5313e5e00fe:

  • Root cause of the first-head Release Artifacts and stage2c (installed 0) failures: the new loopx.control_plane.collaboration TS subpackage was not declared as wheel package data, so the installed effect runtime exited on import.
  • Fix: add the package boundary and *.ts package-data entry; no runtime semantics changed.
  • Installation readback: built an isolated wheel, confirmed both collaboration/__init__.py and return_delivery.ts were present, installed it into an empty target, then successfully invoked manager.return_delivery.normalize_attempt and manager.return_delivery.classify_verification from that installed target.
  • Post-fix local gate: premerge canary 18/18 passed. New TS contract/runtime-handler tests 12/12 and Python adapter tests passed. The PR body now carries the exact head and updated migration economics.

Requesting CI and owner review on this head; the private Lark canary remains with the maintainer as agreed.

@steven-kid steven-kid 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.

详细中文评审

结论:REQUEST_CHANGES。评审 head:f15b6eb4b77da7ee287839a115c0e5313e5e00fe;对照 base:6c1a4d2cc37280a1d652b4bf67afd9c7d69ce19e。

已复现一个需要修复的 P2 正确性问题:project_chat_return_deliveries 在按 session 过滤前截断全局前 2,000 个 route 文件,导致无关会话的记录数量改变当前会话的送达状态可见性。这不是可选的文案建议;它破坏了本 PR 承诺的统一送达读回。其他尚未完成的验证在下文明确列出,没有把 CI 通过当作独立验证。

动机

现有 worker 结果已经落盘,但 Lark 发送后立即读回失败时,旧 drain 会永久跳过 verification_required。用户无法确认原消息是否送达,直接重发又可能产生重复消息。此次改动保存 provider 返回的 locator,并让现有后台泵恢复核验,解决的是可观察的恢复缺口。

不修改代码会保留这个缺口;另建 outbox 或任务库会引入第二个状态 owner。把已知 locator 的核验接入现有 return pump,是合适且相对小的方案。未知 locator 的恢复、M2 生命周期迁移、session takeover 和私有 Lark canary 被明确排除,不能把当前测试解释为这些更广泛能力已经成立。

改动思路

主路径仍是 ReturnService → drain → LarkManagerReturnTransport → send_return/verify_return → inbox_reply。manager-context 保留结果、投递状态、锁和重试的所有权;Lark extension 解释 profile、chat、消息 locator 和 provider 读回;两个 effect-runtime 方法负责 attempt 形状和核验结果分类。Chat 通过派生投影展示同一份文件状态,没有另建一套投递数据库。

我对照了 base/head 的原有 return pump、共享 outbound/mention 校验,以及未改动的 operation result-card recovery。后者从 ChatActionStore 恢复 operation 卡片,涉及不同生命周期和更新效果,不能直接取代这里的委派文本回传;提取共用 mention/intent 逻辑有实际调用依据。

正向路径:发送得到 locator,先记录 attempt,再读回;后续 pump 使用保存的 locator 核验,成功后标记 delivered/reconciled_after_restart,不重新运行 worker。已有合成测试验证了 restart/store 重建后 send 次数仍为一。负向路径:provider 暂不可用时保留核验并退避;已确认 mismatch、缺失 locator 或权限撤销进入显式未核验状态。真实 Lark 网络行为仍需对应环境验证。

具体改动

关键代码讲解

  1. roundtrip.py::drain 增加 attempt recorder、verify 分支和终态处理;原有 source、session、完成回执、授权和 transcript 幂等检查仍在发送前执行。reply_status 通过状态/错误白名单减少 provider 私有信息进入公共读回的机会。
  2. inbox_reply.py::verify_lark_inbox_reply 重算 receipt/intent,检查 bot identity、群可访问性和 mention 身份,再对已知 message ID 执行 mget;复用 Markdown/plain-text 比较逻辑。_verified_mention_aliases 从旧发送路径提取,长 Markdown 降级的 recorder 也继续向下传递。恢复不执行 messages-send/reply,但成功后仍会调用既有 reaction cleanup,因此“read-only”准确范围是消息送达恢复,不应扩大解释为所有 provider 效果均为零。
  3. manager_returns.py::_resolve_return 由发送和核验共同使用,保留实时绑定、原请求 grant 和初始回复确认检查。LarkManagerReturnTransport 在现有 service 组合点提供 send/verify,未授予新的 Goal/Todo 权限。
  4. collaboration/return_delivery.ts 的 normalize/classify 两个函数,通过 effect_runtime_handlers.ts 注册到实际 Python 调用点;__init__.py 与 pyproject.toml 保证新增 TS 子包进入打包范围。协议名称描述单次投递 attempt,未声称 peer 或长期协作权限。
  5. project_chat_return_deliveries 由 project_chat_session_snapshot 和 Chat HTTP snapshot handler 调用,按确定性的 handoff message ID 关联状态。问题就在这个新增投影的全局截断位置。

前端方面,chat.ts、workspace model 与 dashboard message model 接收可选状态;dashboard 的轮询会更新已存在消息,ChannelTimeline 和 ManagerConversationTray 都使用新 ReturnDeliveryStatus;i18n/CSS 提供等待、核验、已送达和显式未核验显示。source-contract 测试和 chat-recovery 浏览器 fixture 增加状态变化检查。打包 HTML、asset-retention 和 CSS/JS 产物同步变化,README 记录新恢复行为;四个 Python/TS 测试文件覆盖 attempt、恢复、拒绝和降级路径。生成产物已核对关联关系,未独立重建验证全部 bundle 内容。

对主干的风险

[P2,需修复] 全局截断让已知送达状态静默消失。 位置:loopx/capabilities/manager_context/roundtrip.py:212(for path in paths[:2000])。这里先对全部 request 的 SHA 文件名排序/截断,才比较 session_id。只要目标 request 排在第 2,001 个之后,即使当前 session 只有一条回传,也不会获得 return_delivery。前端遇到缺失值返回空 badge;CLI/manager 的直接 reply_status 仍然有状态。

独立反例使用真实 ChatSessionStore、return 文件和 ChatRequestHandler._session_snapshot,没有 mock 投影函数:创建一个外部委派、报告结论、执行两次 pump,确认只发送一次;读取 snapshot;再添加 2,000 条排在目标前面的其他 session route,保持目标结果、消息和 delivery 文件不变,再读 snapshot。head 观察为 before=delivered, after=missing, canonical=delivered, same_message=true, sends=1。同一 harness 在 base 上观察到前后均没有新增字段,且仍只发送一次,说明新增字段本身是预期变化,而 head 内部随无关记录消失并非预期变化。

最小修复是从当前 session 的消息/request 关系查找 canonical 状态,或提供不会丢失当前会话结果的索引/完整分页;不能把全局有界视图当成完整状态。补充 delivered 和 verification_required 两种状态在无关记录超过上限时保持不变的回归测试。

验证结果:相关 Python 测试 92 passed,新 TS 合约测试 2 passed,git diff --check 通过。Dashboard aggregate source-contract 在 head 和 base 都停于既有 manager-projection phrase 断言,不能归因于本改动。尚未独立完成 packaged browser/build、wheel 安装、全分支 baseline 对照和真实 Lark canary。异常文本子串决定终态的方式也较脆弱;本轮未复现独立误分类,不另列为已证实 bug。

我的整体评价

恢复逻辑有明确价值,沿用了现有 owner,新增 TS 方法有真实调用,不应因为总行数或引入 TS 本身否定这个方案。不过公开状态投影的完整性是整个 PR 的交付承诺之一,不能在核心恢复测试通过后忽略它。当前反例已经证明:同一条 canonical 状态会因无关会话数量而消失,属于需要修复的 observable-semantic drift。

因此本轮请求修改。建议保持现有 recovery 设计,缩小并修正 Chat lookup,再补充超过截断上限的真实 snapshot 回归。未执行的 provider/browser/完整兼容性验证继续标为 unverified,不继承作者或 CI 的通过结论。此评审不授予合并权限;更新 head 后需重新核验改动与相关主干集成。

English verdict: REQUEST_CHANGES at exact head f15b6eb. The recovery design has a valid existing owner, but the new Chat projection truncates the global route list before session filtering. A real persistent-store/Chat snapshot-handler counterexample shows a delivered message losing its return_delivery field after 2,000 unrelated routes are added, while canonical status remains delivered. Resolve current-session messages without this completeness loss and add a regression. Independently validated: 92 Python tests, 2 TS tests, diff check, and the same baseline/head probe; the aggregate UI source assertion also fails on base. Live Lark, packaged browser/build and complete branch parity remain unverified. No merge authorization is given.

@steven-kid

Copy link
Copy Markdown
Collaborator

English verdict: REQUEST_CHANGES at exact head f15b6eb. The recovery design has a valid existing owner, but the new Chat projection truncates the global route list before session filtering. A real persistent-store/Chat snapshot-handler counterexample shows a delivered message losing its return_delivery field after 2,000 unrelated routes are added, while canonical status remains delivered. Resolve current-session messages without this completeness loss and add a regression. Independently validated: 92 Python tests, 2 TS tests, diff check, and the same baseline/head probe; the aggregate UI source assertion also fails on base. Live Lark, packaged browser/build and complete branch parity remain unverified. No merge authorization is given.

Signed-off-by: Lihua <1017343802@qq.com>
@LIHUA919

Copy link
Copy Markdown
Contributor Author

Repair receipt for exact head e7e681a00e3dd9499d5555aba7c0cc4b9ea2c7cf (parent: reviewed head f15b6eb4b77da7ee287839a115c0e5313e5e00fe), addressing the REQUEST_CHANGES on the P2 projection completeness issue.

Fix

project_chat_return_deliveries no longer treats the bounded global route view as complete state:

  • The projection now starts from the current session's own manager_followup handoff message_ids and resolves canonical status only for those ids; sessions with no handoff messages return without scanning.
  • The global paths[:2000] truncation is removed. Routes are still filtered to the exact session_id and validated request ids; each candidate route is matched against the pending deterministic handoff.<hash(request_id, phase)> ids, and the scan exits as soon as every pending id is resolved.
  • Row assembly is unchanged: return_delivery is attached only to rows whose message id resolved a status, and public-safety filtering (no provider-private fields) is untouched.

The paginated tracking.py listings keep their explicit page bounds; they are a different bounded-view contract and are not a per-session completeness claim.

Regression

test_chat_snapshot_keeps_current_session_delivery_after_unrelated_route_limit mirrors the review counterexample with the real ChatSessionStore and project_chat_session_snapshot: one delivered and one verification_required handoff, then 2,000 unrelated-session route files whose sorted names precede both targets (beyond the old global cap). Both statuses must survive unchanged in the snapshot.

Validation

Run locally with Node v22.18.0 (repo minimum; with the machine default v22.14.0 the TS effect runtime reports unsupported and the four provider-verification tests fail identically at reviewed head f15b6eb4, so that delta is environmental):

  • tests/test_manager_context_roundtrip.py — 13 passed (includes the new regression).
  • roundtrip + tracking + handoff + test_chat_manager_context.py — 36 passed.
  • chat manager details/inspection/report + session active turn — 55 passed.
  • chat agent + event cursor — 37 passed.
  • git diff --check clean.

Not verified here (unchanged holds from the review)

Packaged browser/build, wheel install, full branch baseline parity, and real Lark canary remain unverified and are not inherited from CI. CI is running on the new head. This update does not claim merge authorization; requesting re-review of the two-file delta (loopx/capabilities/manager_context/roundtrip.py, tests/test_manager_context_roundtrip.py).

The PR base 6c1a4d2 predated three dashboard source commits and the
matching packaged-asset refresh on main (fb4c17d). CI pull_request
builds the merge of this branch with current main, so its clean
build:chat output matched neither side's committed assets and the
frontstage-pages build check failed.

Merge origin/main, resolve loopx/web/chat to main's generation, and
rebuild the packaged assets from the merged sources. The new
generation (index-BKS7amWV.js, index-SAtwg4Wr.css) reproduces the
exact CI clean-build bytes locally; the build is idempotent and the
prior main generation stays retained for rolling upgrades.

Signed-off-by: Lihua <1017343802@qq.com>
@LIHUA919

Copy link
Copy Markdown
Contributor Author

Repair receipt for exact head be8c25a6f58048fdabb837d6dcaef689d231357c (parent: reviewed head e7e681a00e3dd9499d5555aba7c0cc4b9ea2c7cf), fixing the CI build (frontstage-pages) failure "Tracked packaged Dashboard assets differ from a clean source build."

Root cause

The PR base 6c1a4d2c predates three dashboard source commits on main (739c3de85, a9a4b3d81, 0a4ae1b9e) and the matching packaged-asset refresh (fb4c17dcb). The pull_request build checks out the merge of this branch with current main, so its clean build:chat produced a generation matching neither side's committed assets (index-BKS7amWV.js / index-SAtwg4Wr.css in CI vs the branch's index-CSsrBwtA.js / index-B5MsQdxX.css). A clean local npm ci + build:chat of the branch-only sources reproduced the committed bytes, confirming the delta was the stale base, not toolchain drift.

Fix

  • Merged origin/main (a1c2516f4) into codex/m3-delivery-verification. Conflicts were confined to packaged assets under loopx/web/chat; the four dashboard sources and effect_runtime_handlers.ts auto-merged (branch's return-delivery UI and returnDelivery.* i18n keys verified present).
  • Resolved loopx/web/chat to main's generation and rebuilt from the merged sources. The new generation index-BKS7amWV.js + index-SAtwg4Wr.css reproduces the exact CI clean-build bytes locally (979.65 kB / 242.20 kB, gzip sizes identical to the failed run). The build is idempotent on rerun, and main's prior generation stays retained for rolling upgrades.
  • The reviewed projection-completeness fix from e7e681a00 is unchanged.

Validation (Node v22.18.0; machine-default v22.14.0 still fails the TS effect runtime, same as before)

  • tests/test_manager_context_roundtrip.py — 13 passed (includes the projection-completeness regression); handoff + test_chat_manager_context.py — 18; chat manager details/inspection/report + session active turn — 55; chat agent + event cursor — 37; Lark reply/return suites (test_lark_manager_returns.py, test_lark_manager_reply_delivery.py, test_lark_markdown_reply.py, test_lark_reply_size.py) — 26.
  • Control-plane TypeScript typecheck passed; tests/control_plane_ts/manager_return_delivery.test.ts 2/2.
  • npm run smoke:personal-workspace-packaged passed (navigation-sorting, chat-recovery, typed-actions).
  • git diff --check clean; loopx canary premerge --from-git-diff passed 1/1 selected check on the merged diff.
  • CI on be8c25a6f: frontstage-pages build, dashboard-acceptance, desktop builds, Release Artifacts, stage2c, and 20+ other checks pass.

Pre-existing main failure (not introduced here)

test-shard (3) fails on tests/control_plane/test_delivery_response.py::test_real_resume_projection_identity_survives_python_transport[resume_at:2026-09-15T00:00:00Z-patch3] ('history_supervision' != 'canonical_todo_wait'). I reproduced the identical single failure on exact origin/main@a1c2516f4 under the same environment, so the red pytest/merge-gate aggregates stem from that main-side date-parameterized case; this branch does not touch delivery-response supervision logic.

The Files changed view now diffs directly against current main. Requesting re-review on the same two reviewed files plus the regenerated packaged assets.

@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.

动机

委派结果本身已经落盘,但当一个外部写入确实发生、而即时回读无法确认时,return pump 会把这个交付停在 verification_required,随后每一轮都跳过它:结论永远不会变成已送达,重启服务也不会重建原始消息,而直接重发又可能重复一条外部消息。受影响的是所有通过 manager 委派、并在原会话等待结论的 owner,以及读 transcript 的 operator;代价是结论长期「未送达」与手工重发带来的重复消息(且无法分辨是哪种)。最近的更小修法是把 provider 的 message locator 先写进既有状态文件,之后只做只读回读,本 PR 正是这么做;此外它把 provider-neutral 的 attempt 校验与验证分类交给 control_plane/collaboration 的 typed TS 契约(维护者 ownership map 的要求),Python 保留文件锁、持久化与 adapter 编排。非目标明确:不做 M2 事务迁移、不改 Todo/lease/Goal 权限、不做 session takeover、不做未知 locator 恢复,私有 Lark canary 仍留给维护者。

改动思路

入口链路是 Chat/Lark 服务内的 ReturnService 线程 → drain()(loopx/capabilities/manager_context/roundtrip.py:270)→ LarkManagerReturnTransport(send_with_attempt / verify,由 start_return_service 组装);权威状态是每阶段的 <phase>.delivery.json 加上持久化的 route 与 reply 文本,决策 owner 是 drain 的状态迁移与 typed classifier,provider 解释和只读回读留在 Lark adapter。发送路径只多做一件事:外部写入成功后立刻用 delivery_attempt_recorder 持久化 locator,然后再回读;核验路径只做 provider 读取(identity、membership、mention 解析、+messages-mget 内容比对),成功后写 delivered 并带 verification=reconciled_after_restart,provider 不可用则写回 verification_required 并带 retry_at 退避,performed-but-mismatch 则写终态 explicit_unverified 与有界原因码。

复用面是干净的:没有新增 outbox、任务库或第二套状态枚举,transcript 消息 id 继续沿用既有的 "handoff." + _hash([request_id, phase]),Lark 回读复用已有 readback 断言与 reaction cleanup,公开投影沿用 reply_status → project_chat_session_snapshot → dashboard 徽标。与「什么都不做」相比,旧行为把模糊写入永久搁置;与「只打印投影」相比,那样不会用新 envelope 重新裁决可恢复性。判断上的分歧不在设计,而在状态机是否完整:迁移路径与异常分类各自的终态必须显式,否则这个修复会把用户送回它本想消除的风险。

具体改动

关键代码讲解

  1. drain(loopx/capabilities/manager_context/roundtrip.py:270):verification_required 从跳过集合移除,进入只读核验分支;发送分支新增 record_attempt 回调(:416),把 locator 归一化后写盘,并对既有 attempt 做冲突检查。异常处理在 :471 先重读状态,再决定终态或退避。
  2. _delivery_attempt / _verification_decision(:43 / :51):Python 侧的薄适配层,通过 effect-runtime 方法 manager.return_delivery.normalize_attempt 与 manager.return_delivery.classify_verification 调用 typed 契约。注意它抛出的是 EffectRuntimeRejected(RuntimeError 子类),不是 ValueError。
  3. classifyManagerReturnVerification(loopx/control_plane/collaboration/return_delivery.ts:66):provider-neutral 分类器,verified && !performed 直接拒绝;已核验→delivered,未执行核验→verification_required/provider_verification_unavailable,已执行但未通过→explicit_unverified 且只暴露三个有界原因码,provider 散文不会进入状态。
  4. verify_lark_inbox_reply(loopx/extensions/lark/inbox_reply.py:720):provider 侧只读回读,先按 markdown/text 两次 dry-run 重新推导 receipt 并与持久化的 provider_receipt/intent_digest 比对(不一致即 intent conflict,不发任何 provider 调用),再做 identity、membership、mention 与内容比对;全程不调用 send。
  5. project_chat_return_deliveries(loopx/capabilities/manager_context/roundtrip.py:207):把交付状态投影到已有 transcript 行,仅转发 reply_status 的公开字段,message_ref/intent_digest/provider_receipt 不进入 Chat 或前端。

其余改动为 dashboard 徽标(新增 return-delivery-status.tsx,接入 channel-timeline.tsx 与 personal-workspace-page.tsx,中英文案与 CSS 配色 token 齐全)、dashboard-page.tsx 的轮询回填(deliveryByMessage 让消息到达后仍能刷新状态)、浏览器 smoke 与 contract test、pyproject.toml 的包数据声明,以及重新构建的 loopx/web/chat 打包资产(打包产物中确实包含新文案)。

对主干的风险

本轮发现两个状态机缺陷与一个合并门禁问题,均为阻塞。第一,发布版本写下的 verification_required 记录(没有 locator 字段)不会像 README 与 PR 正文承诺的那样变成 explicit_unverified/provider_locator_unavailable:drain 在 :355 只捕获 ValueError,而 _delivery_attempt(None) 抛的是 EffectRuntimeRejected;异常落到外层处理时 current["attempt"] 为 None,于是写成 retry_pending 并退避,下一轮进入正常发送分支,把已经发出过的结论再发一次。我用独立 oracle 复现:三轮 pump 后 sends=1 且状态变成 delivered(重复外部消息)。第二,adapter 在核验期抛出的权限类异常靠散文子串分类:manager connection no longer authorized 命中 authorized 而终结为 explicit_unverified/return_authorization_unavailable,而同一个 _resolve_return 抛出的 context return authority revoked 不命中任何 token,于是不写状态、不加退避,verification_required 一直保留,每一轮 pump(约 3 秒)都会重新跑一遍完整 provider 回读;PR 正文承诺的「revoked authority 明确未核验」在这一支没有实现。第三,exact head 上 test-shard (3)/pytest/merge-gate 红:tests/control_plane/test_delivery_response.py 的 resume_at:2026-09-15T00:00:00Z 是时钟相关 fixture,CI 运行时该时刻已过去,期望值翻转为 canonical_todo_wait,而投影返回 history_supervision;main 已由 #4411 修成 2099-01-01,本地在 head 复现、在最新 main 通过,因此不是本 PR 引入,但分支 BEHIND、必需检查必须变绿才能合并。

正向证据同样明确:122 个相关 pytest(roundtrip、chat manager、两个 Lark reply 套件)通过,TS 契约测试与 effect-runtime handler 套件通过,恢复路径断言恰好一次发送与一次核验、公开投影不含任何 provider locator,损坏状态会被规范化为 explicit_unverified/delivery_state_unreadable,未知/超长 delivered_at 与未知 error 也被规范化。因此修复面很小:让 locator 缺失/不可归一化在进入通用重试分支之前就终结,并把 adapter 的核验期异常改成显式 typed 原因(或至少同时分类两种权限措辞、给未分类异常加退避),再加一条从既有 verification_required 记录出发、断言零发送的回归测试;同一次合并里把分支更新到最新 main 即可清除第三条。

我的整体评价

可观测语义我按 base a1c2516f4 与 head be8c25a6f 各跑了一次同一套夹具:base 上模糊写入停在 verification_required(7 passed),head 上有 locator 时能只读核验到 delivered + reconciled_after_restart 且只有一次发送(13 passed、聚焦 122 passed),这部分修复方向正确且验证扎实;但同一夹具下「无 locator 的历史记录」这一支出现真实漂移(重复发送),「权限被撤销」这一支则永久停留且无退避地轮询 provider,所以 observable_semantics 判为 unintended drift,不能给通过。代码体量(29 文件 +1457/-177,其中一半是测试、示例与重新构建的打包资产)与问题规模相称,typed 边界与既有 owner 的分工合理,本地与远端都没有引入第二套状态权威。本轮结论是 REQUEST_CHANGES:请补上上述两条状态机修复(含回归测试)并把分支更新到最新 main 后,在新 head 上重跑必需检查;届时按当前证据我可以给出通过结论。本轮不做合并动作。

English verdict: REQUEST_CHANGES at exact head be8c25a — the read-only recovery design, the typed control_plane/collaboration/return_delivery.ts boundary and the public-safe projection are sound and well tested (122 focused pytest passes, TS contract tests pass, exactly one send plus one verification on the recovery path, no provider locator in any public surface), but two state-machine defects block merge: a pre-existing verification_required record without a locator is converted to retry_pending and then resent (the except ValueError guard at roundtrip.py:355 can never fire because the effect runtime raises EffectRuntimeRejected/RuntimeError), and ValueError("context return authority revoked") from the same resolver matches no token in _verification_exception_error, so that delivery never terminalizes and re-runs the full provider readback every ~3s without backoff, contradicting the documented explicit_unverified outcome. Fix both with typed terminal/backoff handling plus regression tests seeding a legacy locator-less record, and update the branch to latest main so the clock-dependent resume_at fixture fixed by #4411 stops failing test-shard (3)/pytest/merge-gate.

…ification

Resolve the packaged chat asset conflicts by rebuilding the chat bundle from
the merged dashboard sources. Main's prior generation stays in the retention
window so a rolling upgrade can still load the released assets.

Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com>
…loop

A return that cannot be re-verified could still leave the state machine, so an
ambiguous delegated result either duplicated its external message or re-ran the
provider readback until the process stopped.

- A `verification_required` record written before locators were persisted has
  no trustworthy provider identity, but `_delivery_attempt(None)` raises the
  effect runtime's rejection, which is a `RuntimeError`, so the existing
  `except ValueError` never fired. The record fell through to the generic retry
  branch, became `retry_pending`, and the next pump sent the already published
  conclusion again. Locator-less and un-normalizable attempts now terminalize as
  `explicit_unverified`/`provider_locator_unavailable` before that branch.
- Adapters raise the new typed `ReturnResolutionBlocked(reason, message)` so
  delivery state stops depending on provider prose: `context return authority
  revoked` matched no token, so a revoked return neither terminalized nor
  backed off and re-ran the full readback every ~3s. Prose classification is
  retained for text that still arrives that way, now including `authority`.
- An unclassified readback failure keeps the locator and stays retryable, but
  records `attempts`/`retry_at` instead of re-running the provider on every
  pump. Backoff stays scoped to the verification read so the send-interruption
  recovery path still reconciles on the next pump.

Regression coverage: a legacy locator-less record is never resent (send count 0,
including after the backoff window); authority revocation terminalizes for the
typed reason and both prose wordings and is never re-read; an unclassified
readback failure backs off and then retries. The Lark adapter tests now assert
the typed reason alongside the existing message expectations.

Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com>

@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.

复核:APPROVE(exact head 196e67174,对照 base 6bb413105)

我在作者 fork 上直接修复了上一轮请求的两处状态机缺陷并合入了最新 main。三个阻塞点现在都有先失败后通过的回归覆盖,不是"CI 绿了所以通过"。

修复内容与独立验证

  1. 无 locator 的历史记录会被重发(重复外部消息) —— 已修。根因确认:_delivery_attempt(None) 抛的是 EffectRuntimeRejected(RuntimeError 子类),所以 except ValueError 永远不会命中,记录落到通用重试分支变成 retry_pending,下一轮把已经发出去的结论再发一次。我用变异校验复现了旧行为:撤掉修复后新测试稳定报 assert 'retry_pending' == 'explicit_unverified'。现在 locator 缺失/不可归一化在进入重试分支之前就终结为 explicit_unverified/provider_locator_unavailable,发信计数恒为 0(含退避窗口之后)。
  2. context return authority revoked 永不终结且无退避(约每 3 秒重跑一次完整 provider 回读) —— 已修。这条措辞不命中旧分类器的任何 token。现在 adapter 抛新的 typed ReturnResolutionBlocked(reason, message),投递状态不再依赖 provider 散文;散文分类保留给仍以文本到达的路径,并补上 authority。typed 原因与两种散文措辞在测试里参数化覆盖,且终结后不再重读。
  3. 未分类回读失败 —— 保留 locator 与可重试性,但写入 attempts/retry_at,不再每轮重跑 provider。退避只作用于核验读取:发送后中断的 A8 恢复路径仍在下一轮对账(test_known_provider_locator_is_verified_after_restart_without_resend 保持通过)。

main 合并:打包 chat 产物的三处冲突按仓库流程重建而非手工合并,main 的上一代保留在 retention 窗口内;重建幂等,且同一工具链能精确复现 main 已提交的产物(git status -- loopx/web/chat 为空)。

本轮证据

  • 692 passed —— tests/test_chat*.py、tests/test_manager_context*.py、tests/extensions/test_lark_*.py
  • 新增回归:legacy 无 locator 不重发;权限撤销(typed + 两种散文)终结且不重读;未分类失败退避后再试
  • ruff 干净;git diff --check 干净
  • npm run smoke:personal-workspace-packaged 通过(navigation-sorting/chat-recovery/typed-actions),产物内含中英双语新文案
  • loopx canary premerge --from-git-diff:direct/catalog/risk-profile/public-boundary 全部通过,无 manual hold

评审镜面

  • typed 状态规则:投递终态现在是显式原因码 + typed 异常,prose 只作兼容兜底,符合"状态分类属于 typed enum"的要求;兼容兜底已注明它必须由 typed 原因取代。
  • domain 中性:原因码与状态名不含产品/benchmark 专属词汇。
  • 行为变更披露:新增 explicit_unverified 终态、退避语义与 adapter 异常类型变化已在描述中列出;token=ValueError 兼容性保留,未改变既有调用方的处理。
  • guidance vs 义务:未把机器强制的终结语义写成"建议"。
  • 默认关闭与权限语义:未新增能力开关;ReturnResolutionBlocked 不授予任何目标/权限,仅表达"不可解析"。
  • 残留风险:_verification_exception_error 仍保留子串分类作为兼容路径(typed 原因优先)。风险是第三方 adapter 继续用新措辞时会退回未分类→退避,而不再是静默热循环;已由退避兜底,不阻塞合并。

仍未验证(不计入通过)

私有 Lark canary / 真实 provider 读回(维护者保留的凭证步骤)、M2 事务迁移、Todo/lease/Goal 权限、session takeover、未知 locator 恢复、provider 晋级。

English verdict

APPROVE at exact head 196e67174. The two state-machine defects from my previous review are fixed on the author's fork and covered by regression tests that fail before the fix (EffectRuntimeRejected is a RuntimeError, so the old except ValueError could never fire and a locator-less verification_required record was resent; context return authority revoked matched no token and re-ran the full readback every ~3s with no backoff). Locator-less attempts now terminalize as explicit_unverified/provider_locator_unavailable before the retry branch, adapters raise a typed ReturnResolutionBlocked, and unclassified readback failures back off without losing retryability — scoped to the verification read so the A8 send-interruption recovery still reconciles on the next pump. main was merged in (packaged assets rebuilt, not hand-merged) and re-verified: 692 focused pytest passes, packaged browser smoke, ruff, git diff --check and loopx canary premerge --from-git-diff all clean. Still unverified and out of scope: the credentialed private Lark canary, M2 transaction migration, Todo/lease/Goal authority, session takeover, unknown-locator recovery and provider promotion.

@huangruiteng
huangruiteng merged commit 662e75d into loopx-project:main Sep 15, 2026
31 checks passed
@huangruiteng

Copy link
Copy Markdown
Collaborator

合并说明(admin bypass)

在 exact head 196e67174 上合并为 662e75d5f6。所有必需检查已通过:Sign-off、merge-gate、pytest、test-shard (1)-(4)、dashboard-acceptance、postgresql-authority (real server)、stage2c (e2e/installed/mutants)、node-minimum/forward-compatibility、macOS/Windows desktop、kernel-static-checks。

此次使用 admin bypass 的唯一原因是评审门禁:steven-kid 在更早的 head 上给出的 CHANGES_REQUESTED 形式上仍然生效。他们发现的 P2(project_chat_return_deliveries 在按 session 过滤前全局截断)已由作者在 e7e681a00 修复,并带有超过截断上限的真实 snapshot 回归 test_chat_snapshot_keeps_current_session_delivery_after_unrelated_route_limit;本轮我在新 head 上确认该回归通过。状态机两处阻塞缺陷由维护者修复并附先失败后通过的回归,详见上一条复核。

仍未验证并明确留在范围外:私有 Lark canary / 真实 provider 读回、M2 事务迁移、Todo/lease/Goal 权限、session takeover、未知 locator 恢复、provider 晋级。


Merged at exact head 196e67174 as 662e75d5f6. Admin bypass was used only for the review gate: steven-kid's earlier CHANGES_REQUESTED still stood formally, and the P2 they reported (global truncation before session filtering in project_chat_return_deliveries) was already fixed in e7e681a00 with the test_chat_snapshot_keeps_current_session_delivery_after_unrelated_route_limit regression, which I re-confirmed passing at this head. The two state-machine blockers were fixed with fail-then-pass regressions as described in the approval above. The credentialed private Lark canary and the M2/Todo/lease/session-takeover items remain unverified and out of scope.

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.

3 participants