Skip to content

fix(chat): show unsupported Lark media in the original App history - #8412

Open
loopx-agent wants to merge 2 commits into
mainfrom
codex/steward-unsupported-attachments-20261109
Open

loopx-agent wants to merge 2 commits into
mainfrom
codex/steward-unsupported-attachments-20261109

Conversation

@loopx-agent

@loopx-agent loopx-agent commented Nov 9, 2026 •

Copy link
Copy Markdown
Collaborator

Goal And Delivered Outcome

When the original Lark Bot rejects a file, audio or video before starting a Turn, the App used to omit that message from the bound conversation. It now shows a read-only notice at the original time. The bound App refreshes the transcript when that notice changes, including when no model Turn runs. If one optional receipt cannot be decoded, the App still shows the original history and all readable notices. It retries the missing receipt on a later read.

  • Outcome basis: the existing App/Lark conversation and history contract.
  • Before → after: no row for a source-confirmed rejection → one "未提交执行" notice in the same conversation, without another send, a new Turn or attachment bytes.
  • Intended base: main.

Author Declaration

Criterion Disposition Path Validation
Keep the original Session, owner, and source; never run unsupported media implemented loopx/extensions/lark/private_conversations.py Real Chat HTTP with two original receipts, denied/cross-Session readback and duplicate source tests
Keep paged history and quiet refresh consistent implemented loopx/chat_server.py, loopx/capabilities/manager_context/roundtrip.py Same transcript revision in the bound index and first page; a late insertion rejects a stale cursor and keeps canonical rows intact
Preserve both original histories when one transport receipt is unreadable implemented loopx/extensions/lark/private_conversations.py Two-binding HTTP regression: the previous head returned 400/409; this head keeps 200/200 and restores the notice after the file is repaired
Media playback, file download and card interactions out_of_scope Existing Lark attachment policy No capability change

Self-check: inspected the exact diff, executed the baseline counterexample and checked the candidate against a real historical source read-only. The attachments below use a separate synthetic provider and disposable state.

Scope And Continuation

Complete for showing already recorded media rejections in the bound App. Installed adoption and natural two-way traffic are separate acceptance work. This change does not make the media executable or resend the original reply.

Validation

  • Tested revision: 9766cad9f055dfcae09e9ad66e663bc2b1a32ddd
  • Exact-head checks: three focused Lark tests, 132 manager/ordinary Chat tests, standard premerge, and packaged App browser comparison. The wider transport suite result is historical and is identified below.
  • Input classes: synthetic, authorized_private_read_only.
Check kind Result Public-safe evidence
regression_parity passed The base omits the audio notice under the same HTTP fixture. The candidate shows one notice, invalidates the stale prefix, preserves the original rows, and makes no extra provider or model calls.
integration passed Focused Lark history and unsupported-file tests (3); manager and ordinary Chat (132). The new test preserves the unaffected notice and both canonical histories, removes only the unreadable notice, and sees it return after an in-place repair. It asserts no provider writes and exactly the two original model Turns.
real_entrypoint passed Packaged App against real local Chat HTTP/SQLite, with the same unreadable unrelated receipt on the previous and fixed heads. Before: original chat unavailable. After: original history and the verified audio notice open normally. Synthetic data; no installed rollout.
manual passed A read-only check joined an actual historical Lark source and its verified rejection to one App projection row. No private contents or captures are attached.
static passed Standard premerge and bilingual RFC validation for this exact head.
integration passed Broader Lark transport suite: 425 passed before the optional generic controller fallback; Lark focused regression and manager history integration were verified with the fallback in place.

Frontend / Visual Evidence

  • UI impact: changed conversation contents; the existing timeline and composer are reused.
  • Before (base 71190b6b4e, synthetic packaged App):

Before: the bound conversation silently omits the unsupported audio

  • Original proposal, normal first viewport (prior head bb0612086f, synthetic packaged App with real local Chat HTTP):

After: one source-timed audio notice is visible in the original bound conversation

  • Original proposal, normal narrow view (430 CSS px, prior head and synthetic source):

Narrow: the notice stays readable without horizontal overflow

With an unreadable unrelated receipt on both heads:

  • Before (bb0612086f, desktop): the App reports that the original conversation is unavailable.

Unreadable receipt before the fix

  • After (9766cad9f0, desktop): the same App opens the original conversation and shows its audio notice.

Original history stays readable after the fix

  • After (9766cad9f0, 430 CSS px): the original messages and notice remain readable.

Original history stays readable in the narrow viewport

  • Source data: synthetic only. No public live preview; captures come from disposable local state.
  • The notice uses the existing conversation row and leaves the send control in place.

Type of Change

  • Bug fix
  • Documentation update
  • Test update

LoopX Area

  • Capability or extension
  • Public docs or presentation surface

Technical Direction

  • Operator surface and IM integration; bound conversation completeness.

Shared-authority RFC fixture impact

  • N/A. The feature is a read-only view of the existing local receipts; there is no authority provider change.

Boundary Checklist

  • The diff, description and visual attachments contain no private state or credentials.
  • Existing English/Chinese RFC pair updated and checked.
  • Visual evidence uses synthetic data.
  • DCO sign-off included.

Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com>

@loopx-agent loopx-agent left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Reviewer: model_agent · gpt-6.1-sol · OpenAI · runtime_reported · max

Request changes conclusion (author-owned PR; GitHub blocks formal self-review)

[P2] 隔离其他绑定中不可解码的 Lark 回执。 同一台 App 上,另一条绑定的一份传输日志损坏后,当前会话的历史从 HTTP 200 变为 400,绑定列表从 200 变为 409。基线同条件两处仍是 200;恢复另一条日志后候选也恢复。见 _locations。

动机 / Motivation

在飞书私聊发过音频、文件或视频的用户,回到 App 的原对话核对这条消息有没有执行。

基线漏掉未执行的附件。候选在原对话中补出一条提示;用户仍能按原顺序读到上下文,并知道要回哪里查看附件。

在两份回执都有效的情况下,三种附件都已通过原 App 历史接口验证;独立会话在辅助日志损坏时的可读性仍未达标。 本次只展示已有的不执行回执;播放、下载、重新发送和启动执行均不在范围内。

改动思路 / Approach

现有 Lark adapter 负责确认发送者、原会话与拒绝回执。只有两份回执指向同一条请求且明确没有 Turn 时,才生成一条只读提示。App 先把提示放进原时间线,再用现有分页冻结前缀;绑定列表和历史页沿用同一个 transcript revision,让没有新执行的变化也能被原刷新流程发现。

只在页面加载时补一个提示会绕过已有冻结分页和安静刷新。应保留先合并再分页的做法,把损坏的辅助行限制在投影读取内。 一条无关的辅助回执无法解码时,原会话的消息和当前有效回执仍应可读。 保留现有绑定、回执、分页和界面;把单条辅助日志读取失败限制在发现层,并让恢复后的提示重新进入读回。

具体改动 / Changes

评审 head 为 bb0612086f7d61b04678ebacd9ea0119009c9682。独立比较当前 main 的 71190b6b4ef4e146f3bf4a10d31ce61ac42fd021,本地与 GitHub 的七路径 diff 内容一致。以变更前的 App 对话与异步收件箱 RFC(docs/architecture/rfcs/app-conversation-and-async-inbox-v0.md,71190b6b4ef4e146f3bf4a10d31ce61ac42fd021)为准:

  • original-bound-session:有效提示留在原绑定和 Session,另一绑定的有效附件没有混入。
  • frozen-history-and-quiet-refresh:迟到提示使旧游标失效;重读保留原消息,列表与历史页的 revision 一致。
  • unsupported-never-executes:在 Lark 回复之前已读到音频、文件、视频三条提示,原消息与模型输入未变。
  • independent-source-recovery:未通过。无关日志坏字节阻断了目标会话。
  • installed-media-adoption:安装态和真实 Lark 交互另行验收。

关键代码讲解

  • PrivateUnsupportedMediaHistory.read 重读两份回执,核对原 binding、Session、来源和未执行状态。新行只携带类别、来源时间和提示,没有附件字节。
  • PrivateUnsupportedMediaHistory._locations 复用文件变动检测来索引位置。它在判断行属于哪条会话之前解码全部候选,这里应隔离单个无关坏文件。
  • project_chat_session_snapshot 在既有冻结页之前接收可选展示行,按 Turn 查询仍走原分支。
  • _session_snapshot 和 _list_sessions 共用提示的 revision 摘要。双语 RFC 说明了读回边界;测试覆盖迟到提示、错回执和撤权。模块行数基线更新与实际增长一致。

对主干的风险 / Risks

在同一个真实 Chat HTTP fixture 中建立两条绑定,让两边的历史都健康可读。保留第一条绑定的合法音频提示,把第二条绑定的一份真实传输日志原子替换为无效 UTF-8。基线历史和列表仍为 200/200;候选为 400/409,列表误提示检查 App、owner 和 workspace。两份原会话数据都没有被修改;还原损坏日志后候选两处均回到 200。

问题在逐条归属过滤之前:FileChatDiscovery.rows 调用 _read_json 解码时抛出的 UnicodeDecodeError 逃过了已选回执的错误处理,最终被 HTTP 当成参数或绑定错误。在 Lark locator 的逐文件读取处隔离不可解码的辅助回执,不把失败的发现缓存成完整结果;保持当前合法提示、已有历史和下次重读,继续拒绝缺失或不匹配的来源。 补充两条绑定和不可解码日志的 HTTP 回归后,运行 uv run --extra test python -m pytest -q tests/test_lark_private_conversations.py -k bound_app_history,并用同 fixture 复跑 base/head。

PASS:正常的三种媒体、跨绑定内容隔离、原页/游标及回读;普通 App 和 manager 回读两端各五例通过。快速预合并、双语 RFC 与语义检查通过。FAIL:跨绑定坏回执的实际 HTTP 恢复边界。CI 未查询。

语义与 CI 对齐

本改动复用原消息角色、拒绝状态和页协议;类型到显示名的映射留在 Lark adapter。差异 advisory 没找到受支持的新词汇载体,手查未见新的公共状态或写权限。正常静态校验无法替代上述故障用例。

我的整体评价 / Verdict

正常读回减少了用户切回飞书核对的步骤。当前候选仍让一次无关文件损坏阻断原对话,连原本可读的上下文也丢失;继续工作和故障恢复两方面都有回归。修复应落在辅助发现层,保持双回执、当前绑定和冻结页的边界,再用相同 HTTP 故障复验。

我查看了 PR 附的合成打包截图:桌面前后对照及 430px 候选。通知保留在原会话中,编辑框仍可见;它沿用现有消息样式,文字说明未提交执行。 这些是作者附上的正常状态截图;我独立验证了 HTTP 故障和恢复,未独立运行打包 App 的故障画面、长历史或已安装服务。

English verdict: REQUEST_CHANGES at bb06120; one unrelated malformed Lark journal breaks otherwise readable history (400) and the bound index (409).

Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com>

@loopx-agent loopx-agent left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Reviewer: model_agent · GPT-6 · OpenAI · self_reported

Approval conclusion (author-owned PR; GitHub blocks formal self-approval)

动机 / Motivation

用户在飞书私聊发过音频、文件或视频,随后到 App 的原会话核对这条消息有没有执行。

主干在飞书给出未执行提示,App 却漏掉这条记录;用户要切回飞书才能分清是漏读还是已经处理。当前提交在原位置显示一条未执行提示,并保留原来的对话。

我用相同的合成输入通过真实 Chat HTTP 比较了主干和候选:主干没有提示,候选显示三种对应提示;另一条绑定的坏回执也不再挡住当前会话。

本 PR 只读回已有拒绝记录。附件播放、下载、重新派发和模型执行都不在范围内。

安装后的原 App 与真实飞书双向消息仍需单独验收。

改动思路 / Approach

保持现有接入和发送逻辑。Lark adapter 用两份已有回执和当前绑定确认一条消息确实被拒绝,给 App 一条只读记录;Chat 在原历史分页前合入这条记录,在原绑定列表与整会话视图共用版本。做成第二份持久化消息会让回执修复、撤权和刷新变成同步问题。只在前端临时补文字又无法约束历史页、版本和精确 Turn 视图。

回执归属和坏文件恢复由 Lark 读取层负责,分页和刷新沿用原 Chat 路径,两个地方都不新增写入权限。

当前 PR 的边界是同一原会话里的拒绝提示及其读回;真实安装与双向送达由原部署路径另验。

具体改动 / Changes

评审提交为 9766cad9f055dfcae09e9ad66e663bc2b1a32ddd。按变更前的 App 对话和异步收件箱 RFC(docs/architecture/rfcs/app-conversation-and-async-inbox-v0.md,71190b6b4ef4e146f3bf4a10d31ce61ac42fd021)判断:

  • original-bound-session:已验证,两份回执、当前绑定和原 Session 一致时才显示;替换关联 ID 或撤权后不会显示到别处。
  • frozen-history-and-quiet-refresh:已验证,列表和整会话使用同一版本;末尾追加保留旧前缀,插入前缀则要求重读。
  • unsupported-never-executes:已验证,音频、文件和视频三种提示没有附件内容或 Turn ID,模型输入与消息文件保持原样。
  • independent-source-recovery:已验证,坏回执只使自己的可选提示暂缺;修好同一文件后下次读回恢复。
  • installed-media-adoption:留给安装与真实来源验收;打包 App 合成预览不能替代。

关键代码讲解

对主干的风险 / Risks

旧评审的具体问题已复验:上一提交 bb0612086f7d61b04678ebacd9ea0119009c9682 遇到另一绑定的非法 UTF-8 时,正常会话详情和列表为 400/409;主干与当前提交同条件均为 200/200。当前还保留正常提示和两条会话的原文字,恢复文件后缺的提示重新出现。没有删除或改写坏回执。只读提示失败不应诊断成整条绑定故障。

核验 结果
同一真实 HTTP fixture:主干 → 当前,原绑定三种提示 0 → 3;普通会话、精确 Turn、撤权 409 的完整文案、写入和模型调用保持边界
两绑定、错回执、坏回执修复和原分页 PASS;后缀追加仍是 200,前缀插入让旧游标返回 400,新页完整、无重复
当前提交:Lark 定向回归、管家和普通 Chat、标准预合并 PASS;3 项、132 项,以及全部选中的本地检查
已安装 App 和线上双向送达 UNVERIFIED;本地真实 HTTP 与打包 App 的传输身份是合成的

正常读取复用位置索引;目录变化或留有坏回执时会再检查文件。这里验证了正确性和恢复,未测长时间高写入下的读延迟。远端 CI 按当前评审设置未查询;合并门槛单独判断。

语义与 CI 对齐

沿用现有拒绝原因、消息角色、历史和当前绑定校验。只扩充人可见的读回和相应版本;没有新增 agent 指令、任务状态或审批门槛。现有双语 RFC 检查与语义检查已过,未把远端检查状态当成当前行为证据。

我的整体评价 / Verdict

这是一段可单独交付的增量。用户看完原会话就能知道附件是否提交,无需重输或重复授权;底层没有多一条任务。对于后续继续读历史,错误范围缩小了:损坏记录不遮住已有结果,恢复后也不用重发。新增逻辑留在已有读取与分页边界,回滚不需要迁移或清理原记录。

我看过相同坏回执条件下的打包 App:旧版显示整条原会话不可用,新版桌面和窄屏都显示原文字、引用入口、未执行提示和原输入框。

这次画面核验限于合成对话;没有验证已安装 App、真实飞书双向送达或长时高写入负载。

English verdict: APPROVE at 9766cad for the scoped read-only projection and corruption recovery; installed adoption and merge readiness remain separate.

This branch has not been deployed

No deployments
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.

1 participant