Repository navigation
fix(lark): surface uncaptured private message sources - #6780
Conversation
Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com>
There was a problem hiding this comment.
Reviewer: model_agent · OpenAI gpt-6.1-sol · runtime_reported · effort=max
Request changes conclusion (author-owned PR; GitHub blocks formal self-review)
REQUEST_CHANGES — P2:同一 binding 的观察回执缺少并发读改写保护,会丢失已发现的缺口,并最终错误显示已核对。 精确 head 5d043efa525b1d4a16e51d503dc25e48a79ac916,对照 main 105c4bc15091afac832495167cb98a1389951a04。当前账号与作者相同,以 COMMENTED 记录代码结论与行内发现;不切换账号、不合并。
动机
本人私聊的负责人看到监听就绪且待处理为零时,需要辨别原消息是否实际进入收件记录。
旧页面只有监听状态和待处理数量;候选增加来源核对,但两个服务并发保存观察时可覆盖早先缺口,之后仅补入后一条收件记录就错误显示已核对。
单实例核对和界面展示有效,但独立原生绑定/HTTP 反例出现 verified 与零缺口,同时仍有一条已观察且未捕获的原消息,因此完整的缺口保留结果尚未成立。
本次不要求安装 Bot、调用真实模型或补跑历史任务;监听、来源核对、执行、最终投递与采用仍是不同事实。
新增来源核对方向正确:只看监听就绪和空队列,确实无法判断是否漏掉原消息。但这个增量的价值在于持续保留已发现的遗漏;只展示一次红字、后来又把未处理的原消息隐藏,仍会让负责人失去恢复线索。阻塞原因是这个当前行为缺陷,不是要求本 PR 完成整个长期 L1 项目。
改动思路
历史读取、身份核验和私有文件属于现有 Lark provider IO,Core 继续拥有受理和授权;这个放置合理,但同一 binding 的观察回执必须复用已有文件锁或 CAS 保护,atomic rename 不能代替并发读改写。
本 PR 的有用边界是既有连接页的来源缺口观测及一次自有消费者重连,不包含历史任务执行、真实 provider 根因修复或安装版资格。
现有投递扫描器预留一个有界观察任务,读取已准入原件提供的确切目的地,复用身份核验和分页 decoder。原生 capture 与历史观察保持分开;只有新发现、无本 App 回复的缺口才返回重连提示,由本服务已有 worker 的 stop Event 和原 App lease/finally/refresh 路径接手。HTTP 只读本地缓存,不读取网络,不把观察当作执行权限。
问题在于:每个服务只保证自己的一个 observer Future,没有保护多个服务共同使用的 binding 文件。网络读取期间,别的观察者可以提交新 gaps;后返回者仍使用调用前的旧字典,再原子替换整个文件。原子替换避免半截 JSON,却不能保证历史事实的合并和完整窗口的单调性。
具体改动
先读的原规格是 docs/architecture/rfcs/app-conversation-and-async-inbox-v0.md,不可变 spec_revision=105c4bc15091afac832495167cb98a1389951a04,重点为 ordinary private Chat 与本地健康读模型检查点;中文镜像和既有 Lark context-only/历史不执行协议也已核对。原条款无编号,以下为描述性映射;新增 gap 保留保证同时来自本 PR 的明确目标,不冒充既有安装验收已经完成。
- private-owner-source-authority — implemented:沿用 Core 的 App/owner/source/grant 核验。独立反例从真实原生 binding 和已准入 capture 构造,观察期间没有新增 Core admission 或 provider 写入。
- truthful-source-observation — not_met:并发回执丢掉先前缺口,实际 HTTP 后续返回 verified/0;需要修复下述 P2 才能完成可信来源核对。
- existing-consumer-and-recovery-owner — implemented:重连只设置本服务对应 worker 的 stop Event,原 App lease 和刷新生命周期拥有替代者;历史不进入 Core、回复或 failed-Turn 恢复。
- installed-provider-qualification — deferred:沿用原 installed service/L1 owner 的验收;作者真实 provider 探测超时不算恢复通过,本轮也没有安装、调用真实模型或读取私人会话。
关键代码讲解
PrivateIngressObserver.observe在第 99 行读取旧状态,经 provider IO 后第 114 行整体写回。此处没有跨进程锁、revision 或 CAS,是当前 lost update 的提交点。PrivateIngressObserver._observe正确复核 binding、限定原目的地、使用完整分页并在 IO 后重读 capture;第 199 行却只从本次调用持有的旧state.gaps继续保留历史。顺序测试证明不了另一个观察者已提交的 gap 会保留下来。_reconcile_private的 future、四槽池和 stop Event 都是服务实例内的;App 消费者 lease 保护实时 stream,不保护每个服务的 observer 回执事务。_private_conversations向原 API 增加无原消息 ID/正文的状态、时间和计数,排除群和其它 transport;现有 React 面板忠实显示这些值,所以 producer 的假 verified 会直接成为用户看到的成功状态。
P2 的独立复现:两个实际 observer 使用同一原生 binding、同一私有 receipt,均在第一笔提交前读到旧状态。A 完整观察到缺口 A 并提交;B 随后从不再包含 A 的合法历史窗口观察到 B,用旧状态覆盖文件。结果只剩 B,实际连接 GET 为 1 个 gap。再仅让 B 经真实 live admission 进入原生 capture,下一轮读空历史后,API 变成 status=verified, source_gap_count=0;A 始终没有进入 capture。这两个独立断言都在当前 head 失败,观察阶段 capture 文件及 provider 写入保持不变。
最小修复:在现有 Lark observer 内对每个 binding 的 receipt 做跨进程串行化,或提交前重读当前版本并用 CAS 合并未被原生 capture 关闭的 gaps;保护完整窗口单调前进和 reconnect 去重,不新建 Core 状态 owner。 增加两个观察者交错读取/提交、仅后一条补入 capture、前一条仍缺失的回归;最终 HTTP 必须保留 A,不能成为 verified。保留现有顺序、分页失败、撤权、单次重连和不执行历史的断言。
对主干的风险
这条竞态影响持续运行的可信状态,而非只有 JSON 文件一致性。两个 Chat 服务共用运行目录时,各自都有观察循环;其中一个实时消费者 standby,并不意味着它的 observer 已被 App lease 串行化。后写者既可以丢掉先前 gap,也可能回退覆盖时钟或重复发出新 gap 提示。当前独立反例已把缺口丢失追到真实 HTTP 假成功,不把并发风险仅停留在源码推测。
独立验证:tests/test_lark_private_ingress.py 与 tests/test_lark_private_conversations.py 共 223 passed;正常 Chat 包构建、TypeScript/Vite 和 verify --source 通过;标准 premerge 6 项直接+16 项所选检查通过。这些成功没有覆盖两个 observer 写同一 receipt 的交错,不能推翻两条新失败。新增 215 行 provider 模块和复用既有 decoder/Core owner 的方式符合范围;应在此 owner 内补事务保护,无需引入泛化框架或第二套状态权限。
实际 Ego 检查了候选打包资源上的原设置入口:无 ingress、gap、unavailable 和 verified,监听就绪/待处理零与 gap 并存,中英文、桌面/390px、键盘、减少动态效果和刷新恢复;英文 390px 的文档宽度也是 390,私聊 API 没有写入。UI 采用既有合成 API fixture,原生 producer/HTTP 在上述独立测试中另验,不把静态 fixture 当成安装版或真实 Lark 证据。全部五张公开图已目检并核对 CRC、无文本/EXIF 元数据。浏览器定位和手机导航的测试脚本错误均通过当前快照纠正,没有改产品来通过测试。
语义与 CI 对齐
IngressObservationStatus 是 provider 本地观察词汇,四值与可选 Zod API schema 一致,双语 RFC 明确了默认开启的历史只读 IO、页数/时间预算和重连边界;没有把硬执行义务称作 guidance,也没有把它变成 Core 的完成状态。群、其它 provider 和无此可选字段的旧接口保留原路径。需修复的是历史 gap 持久性,不是放宽状态/scope 条件。
必需根模块检查仍为 1 failed/5 passed,146 > 145。当前 head 与本轮固定 base 的 checker、预算、offender 及全部 146 个模块相同,完整失败只差耗时;这是已有独立 hold,不是本次新增 Lark 子模块造成的根目录增长,也未被豁免。当前 wait_for_ci=false,未查询、轮询或等待 CI。真实 provider 恢复、安装态和长期成本没有通过结论。
我的整体评价
本次目标判断为 not_yet_proven,代码结论 REQUEST_CHANGES。long_horizon=regression:反复观察能永久丢掉已知遗漏,后续扫描反而生成假成功;user_experience=regression:新的提示本身清楚且无需额外授权轮次,但错误 producer 会撤掉仍应存在的恢复提示。不能以单实例通过、截图或预合并绿灯替代这一持续性不变量。
历史读取、身份核验和私有文件属于现有 Lark provider IO,Core 继续拥有受理和授权;这个放置合理,但同一 binding 的观察回执必须复用已有文件锁或 CAS 保护,atomic rename 不能代替并发读改写。
本 PR 的有用边界是既有连接页的来源缺口观测及一次自有消费者重连,不包含历史任务执行、真实 provider 根因修复或安装版资格。
机制成本与缺口相称,当前不要求改写 Core、添加新消费者或整套语言迁移;复用现有锁或 revision/CAS 维护同一 provider 回执即可。修复后需要以新精确 head 重跑交错写入/仅后一条 capture 的真实 HTTP 反例、223 项已有 native 检查以及相关 premerge,确认缺口保持、时钟不倒退和重连不重复,再重新评审。现有根模块预算及安装采用验收仍独立保留。
English verdict: REQUEST_CHANGES at 5d043efa525b1d4a16e51d503dc25e48a79ac916. Concurrent observers can overwrite a committed source gap; after only the later source is captured, the real API incorrectly reports verified with zero gaps. Serialize or CAS-merge each binding receipt using the existing owner, and add the interleaved native/HTTP regression. The pre-existing root-module budget failure and installed/live-provider qualification remain separate holds.
| # empty history. Retain the last complete bound and every gap. | ||
| state.update(status=IngressObservationStatus.UNAVAILABLE.value, | ||
| last_checked_at=time.time()) | ||
| write_private_json_atomic(path, state) |
There was a problem hiding this comment.
[P2] 同一 binding 的 receipt 需要跨进程读改写保护。每个 Chat 服务都有自己的 observer Future,App stream lease 不串行化这里:A/B 都先读旧 state,A 保存 gap A 后 B 整体覆盖为 gap B;再仅 B 经原生 capture 补入,实际连接 HTTP 会返回 verified/0,而 A 始终未捕获。独立原生 binding+真实 HTTP 的两条交错反例已在本 head 失败,223 项顺序/现有测试仍通过。请在此 owner 复用 per-binding 文件锁,或提交前重读最新 receipt 并用 CAS 合并未被 native capture 关闭的 gaps,同时保持完整窗口单调和重连去重;补充并发观察者及只补入后一条的回归。
loopx-agent
left a comment
There was a problem hiding this comment.
Reviewer: actor_kind=model_agent; model=gpt-6.1-sol; provider=OpenAI; runtime_reported; reasoning_effort=max.
Request changes conclusion (author-owned PR; GitHub blocks formal self-review)
REQUEST_CHANGES:对 6780@5d043efa525b1d4a16e51d503dc25e48a79ac916 完整评审后,发现一个会把未核对区间隐藏为已核对的恢复缺陷。
[P1] 继续核对时不能静默跳过超过一天的未覆盖区间 — private_ingress.py:149–153。已有 last_complete_at 后停机或读取失败超过一天,max(last_complete_at, now - 86400) 会把下一轮起点直接移到最近一天。停机早期的原消息不再进入任何查询,随后 212–214 行却提交新的完整时间并返回 verified。我用按查询时间真实筛选的合成 provider 复现:先成功核对,过一小时收到未捕获消息,再停机两天;恢复后的窗口从该消息之后开始,真实 Core 绑定与 HTTP 接口仍返回 verified、source_gap_count=0,设置页因此显示“原消息已核对”。这会让需要找回的漏消息永久消失在核对视野中。请仅对首次启动应用一天回看上限;已有游标时,在现有页数和超时预算内分段推进,或明确保留不可覆盖区间并返回未完成,不能跨过它认证完整。增加上述两天中断的时间筛选回归,断言缺口被发现,或覆盖仍明确未完成,且旧游标不会被无证据推进。
动机
受影响的是使用已绑定飞书本人私聊的负责人。监听器就绪且待执行为零,并不能解释一条原消息是否进入了本机收件记录;之前的界面没有单独呈现这个差别。本 PR 让后台比较原消息和捕获回执,并在原有连接设置中显示差异,目标本身成立。
预期可观察结果是:原消息没有回执时可见缺口,读取失败时保留不确定性,历史消息不会被擅自执行。它不是自动重放、失败 Turn 重试、历史答复或真实 requester adoption 的实现。当前长中断反例没有达到第一项:看似健康的新覆盖时间掩盖了从未检查的旧消息。
真实 provider 与运行 Bot 的恢复尚未认证,当前长中断也会错误显示覆盖完整。
改动思路
复用现有后台扫描器、已核验 App、既有本人绑定、分页解码器和私有原子回执。新模块只负责 Lark 历史 IO、观察游标及缺口;Core 的绑定、授权和准入仍由原 TypeScript owner 决定,Python 没有另建通用准入规则。新缺口最多请求停止本服务已有消费者,由原 App 租约与刷新生命周期接续,不创建第二个消费者。
这是默认启用的后台行为变化,不是一个默认关闭的能力:本人已绑定目的地新增历史读取和一次性监听重连。未绑定、群聊和其他 provider 需要保持原有入口与权限;本次测试覆盖其选择边界。枚举 IngressObservationStatus 是本地 provider 观察词汇,API 是可选投影;它不授予执行权。
保留现有 owner 和有界读取是合适的架构方向。当前 PR 边界是后台观察、一次现有监听重连、API 与设置投影及对应回归;真实 Bot 安装和 provider 丢失根因没有在这里完成。后续修复只需在这个 owner 中保留连续覆盖边界,不需要放大每轮预算、增加队列或引入另一套 authority。
具体改动
规格依据:docs/architecture/rfcs/app-conversation-and-async-inbox-v0.md,spec_revision=105c4bc15091afac832495167cb98a1389951a04。以下是本次评审使用的 criterion ID,不是新增的产品状态:
owner-source-boundary:既有 Core 绑定与当前 App/owner 核验仍是入口;已检查原 owner 源码与 real native fixture。capture-not-execution:历史只比较收件,不准入 Core、不发消息、不复活失败请求;回执不变和禁止 Core admission 的反例均验证。incomplete-not-absence:来源缺失或未完成不能变成完整与零缺口;本次长中断反例失败,是阻断项。bounded-recovery:既有扫描池、App 租约、撤权与停机拥有恢复,不增加消费者;正常分页、预算耗尽、并发捕获与撤权用例通过。read-only-health:连接 GET 只读已有本地观察,返回时间和数量,不公开原消息标识或正文;基线/当前真实 HTTP 对照验证原字段不变。
关键代码讲解
PrivateIngressObserver.observe/_observe:筛选本人 Lark 绑定,核验当前身份,再按原目的地分页;读取后重查绑定和捕获回执,避免并发捕获误报。失败保留之前缺口与完成时间。缺陷在恢复窗口起点的再次一天截断,而不是分页返回为空本身。LarkGoalTopicRuntimeService._reconcile_private:在原四线程池中安排观察 Future,完成后至多停止本服务对应 worker。既有 poll/finally/refresh 释放 App 租约并接续,Core 执行不因历史读取重放。PrivateConversationRequestMixin._private_conversations:保持原连接字段,只有 Lark 本人连接添加可选的ingress;监听状态、待执行数和来源观察各自保留语义。PrivateConversationPanel:既有设置页用 alert/status 展示核对结果,缺口与不可用不是空连接。它直接信任后端verified,所以游标错误会成为用户看到的错误健康结论。
共 15 个文件、+650/-2,包括 215 行 provider 模块、334 行测试、原有运行时/API/设置的小幅接线、双语文档和 5 张合成截图。源码、完整非二进制 diff 与五张截图均检查。模块放在现有 Lark 扩展中合理;未来边界检查认为无需另起通用 abstraction。与本问题同域且值得现在完成的简化,是把首次回看和已有覆盖游标的规则明确分开,并用同一连续性不变量保护更新点。
对主干的风险
主要风险是新的默认观察给出错误的完整性结论,并通过设置页影响负责人对漏消息的判断。另有后台 IO 与共享扫描池占用、旧回执、分页不完整、绑定撤销、并发捕获及监听重连风险;对应正常和拒绝路径已审阅并验证,不能用“监听就绪”或 Bot 曾回复代替原生捕获、最终交付或采用。
另已有同一提交的并发回执缺口评审提出 P2;它与本次长中断反例不同,仍需独立处理。本评审不撤销或重复宣称验证了那份结论。
独立验证:22 项 ingress 测试通过;邻接运行时/绑定/私聊兼容验证为 337 passed;前端 typecheck、Vite 与 packaged Chat build 通过(保留已有大 chunk 警告);standard premerge 为 6 项直接检查与 16 项选中检查通过,包括完整 semantic drift、双语文档与公开/私有边界。开发期 advisory 识别出本地观察枚举,已判断其归属;advisory 本身不是等价性证明。
额外的两个隔离探针(其中一个按查询时间筛选),以及 1 项实际 Core 绑定/文件存储/HTTP 基线与当前方法对照,确认上述失败;探针成功复现不等于被测不变量通过。GET 原有字段逐项相同,当前新增错误的 verified,捕获回执不变且未执行历史准入。基线方法来自不可变提交,复用的 Core/HTTP/身份/分页 owner 源码逐字节相同。
必需的根模块预算检查仍为 1 failed / 5 passed、146 > 145;基线与当前全部根模块、预算和测试相同,已单独归因为既有问题,不要求此 PR 修改预算。该独立门禁不能被 canary 的通过覆盖。
语义与 CI 对齐
CI 按当前评审契约未咨询,也未等待或触发。未绑定、群聊、其他 provider 保持选择边界;已有 API 消费者允许没有 ingress。当前本人连接默认新增历史读取,PR 和双语 RFC 有明确披露。“需核对”是提示,自动观察和单次重连是机器行为;没有把它称作必须由用户完成的新授权流程。通用 Core 规则保持领域中立。
五张公开截图是合成数据,静态视觉审查能确认文案与布局,不能认证交互运行。当前 packaged bundle 已构建,本次没有独立执行浏览器交互、真实 provider 读取或安装到运行 Bot;作者的真实 provider 探针超时也不能证明恢复。修复后仍需补上受影响实际用户交互与安装路径证据。
我的整体评价
这个增量有独立价值,复用边界与不重放约束合理,但当前恢复规则会认证一个存在未检查历史的区间,正好违背解决漏消息不可见问题的目的,因此不能批准。请在同一 PR 修复游标连续性,补上按时间过滤的两天中断反例,随后重新检查完整 exact head、默认行为、API 和设置投影;不要只增加超时或把状态文案改得更乐观。真实 provider 与已安装 Bot 的效果、最终交付和本人采用仍分别待验,没有从测试数量或截图推出它们已完成。
English verdict: REQUEST_CHANGES at 5d043efa525b1d4a16e51d503dc25e48a79ac916. After an outage longer than one day, the rolling clamp skips an unchecked interval and the real native HTTP projection still reports verified with zero gaps. Preserve contiguous coverage within the existing page/time budget, or retain an explicit incomplete interval, and add the time-filtered restart regression. Existing local tests/build/canary do not cover this counterexample; real-provider and installed-path recovery remain unqualified.
…6786) Two findings from the review of the merged #6780: - The coverage cursor was re-clamped to "now - one day" on every pass, so an outage longer than a day skipped the unchecked interval while still committing a fresh complete time and reporting verified. Cap the lookback only for the first observation; with a cursor, resume from it and advance at most one day per pass, reporting unavailable until the live edge is reached. - Each binding receipt was read, reasoned over and rewritten without exclusion, so a concurrent observer could overwrite a gap another observer had just committed. Hold the existing per-binding file lock across the read-modify-write and re-read the receipt under it; a pass that cannot take the lock leaves the receipt untouched. Cover both with a time-filtered two-day restart regression and an interleaved observer regression; both fail without the fix. Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com>
…rd settings step loopx-project#6780 added mobile reload steps to the private-steward-scope acceptance scenario. At phone widths Goal navigation is a closed off-canvas drawer, so the Settings button is not in the DOM until the drawer opens. Both chat-bundle-browser (2) and dashboard-browser (2) therefore time out at settings() on main, and since checks requires both lanes, the required merge-gate is red for every core-tests PR. Open the drawer in the shared settings() helper when the mobile menu trigger is visible, matching the existing pattern in the other workspace scenarios, so the new mobile private-ingress readback runs. Closes loopx-project#6795. Signed-off-by: JasonBuildAI <jasonbuildai@gmail.com>
A private Lark connection can report a ready listener and zero pending requests while an original owner message never reached native capture. The existing background scanner now compares bounded, read-only source history with capture receipts and exposes that difference in the connection API and Lark settings.
The observer reuses the verified App, existing owner destination, provider pagination decoder, private atomic receipts and delivery scanner. A newly observed unanswered gap requests at most one reconnect of this service's existing live consumer through its existing lease lifecycle. It never admits history to Core, sends a historical reply, retries a failed Turn or creates another consumer. Failed/incomplete reads preserve the previous coverage clock and gaps; a later capture cannot hide an earlier hole in the initial lookback. A Bot reply is only an observation, not proof of final delivery or adoption. Groups and other providers keep their existing contracts.
IngressObservationStatusis local Lark observation vocabulary, projected as an optional API field for compatibility. It grants no execution authority. Python owns this provider IO; Core still owns binding, scope and admission decisions. The bilingual RFC describes the bounds and recovery limits.Validation at
5d043efa525b1d4a16e51d503dc25e48a79ac916:npm run build: typecheck, Vite build and normal packaged Chat bundle succeeded. Ruff, full semantic drift check, bilingual RFC and public/private scan passed.The candidate has not been installed into a running Bot. A separate read-only real-provider probe timed out during fresh identity/history reads, so it does not qualify real-provider recovery. Reconnection mitigates the future live path; it does not prove the cause of provider loss or execution of a missing historical request. Independent review and installed-path qualification remain.
Desktop, before → source gap (synthetic data, candidate packaged assets):
Narrow viewport, unavailable source observation: