Conversation
deep_link.rs/windows.rs 两处既存漂移(P3-01 工单已知);重排 test 断言换行 与 vec! 字面量换行,无字符串/逻辑变化。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- 55 处 assert_eq!(x, true/false) -> assert!(x)/assert!(!x)(纯断言写法,零语义) - events.rs: event_channel_name 挪到 #[cfg(test)] mod tests 之前(items_after_test_module,纯移位无内容改动) - main.rs: workhub_env_flag_value 的 match 改写为 matches! 宏(match_like_matches_macro) - main.rs: 三处 &app.handle() 去掉多余取址(needless_borrow,app.handle() 本身返回引用) 均为 clippy --fix 自动应用 + 1 处手工同构改写(match_like_matches_macro 无自动修复), cargo test 121/121 全绿,无需任何 #[allow] 压制。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…i18n job 复用既有 job 的 Linux Tauri 系统依赖 + Swatinem/rust-cache,新增两步: fmt --check(rustfmt 纯语法排版,不评估 cfg,覆盖全部源码包括 mac/windows 专属分支)、 clippy --all-targets -D warnings(真编译再 lint,Linux target 编译期会剔除 `#[cfg(target_os = "macos"/"windows")]` 门下的分支,故只覆盖 Linux 生效路径—— 与该 job 里 cargo test 的覆盖范围一致,mac/windows 专属分支仍需本机验)。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…e (P2-02)
账号停用(POST /api/auth/users/:id/deactivate)的善后清理(撤会话/设备/凭据、
工作交接、清在线态)此前是 best-effort:中途失败被静默吞掉、仍回 200 ok:true,
留下半清理态(残留会话/设备可能仍可访问),且重发请求因 softDelete 落空一律 404,
无可重入的重试路径。
- 把善后步骤收进 runOffboardCleanup 幂等例程:逐步捕获、失败写结构化日志并记入
cleanup.steps,返回 complete 标志(不吞错伪装成功)。
- 清理未完成 → 回 500 + { code: offboard_cleanup_incomplete, deactivated, cleanup },
调用方可感知失败步并重试。
- softDelete 落空时用既有 findRefsByIds 分辨墓碑 vs 不存在:已停用则重跑幂等清理
收敛到全清理(重试入口),从不存在才 404。首次停用才写 auth.user_deactivated 审计。
- 无迁移:复用软删墓碑 + 既有 findRefsByIds 作为可重入的任务状态。
- 边界:区别于 SEC-1(工作区移出的同事务撤 token);本单是账号级停用,且含非库内的
presence.forgetUser,无法整体入库事务,故取幂等重入而非单事务。
Make account deactivation cleanup re-entrant: idempotent steps, structured
failure logging, and a retry entrypoint via the soft-delete tombstone. No
migration. Root-cause tests reproduce the half-cleaned state and prove
convergence on retry.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
重命名协同会话此前只有发起端本地就地改名,不发领域事件——其他端/其他人
要等下一次全量轮询才看到新名字,web 会话镜像页则完全不知道。
- contracts: 新增 conversation.title.updated 事件(enums + events schema),
极简 payload {conversation_id, title},topic 与 conversation_id 绑定;不删改既有枚举(R17 G2)。
- api: renameConversation 落库后 best-effort 广播到会话私有流 conversation:<id>
(仅参与者可订,不广播全工作区),收口历史遗留的「不发事件」取舍。
- desktop: 会话流分发表新增 parseIncomingConversationTitleUpdated 分支,
经 onConversationTitleUpdated 回调复用 renameCollabConversationInVm 就地改左栏树叶,不整页刷。
- web: conversation.title.updated 纳入 CONVERSATION_MIRROR_LIVE_EVENT_TYPES
(EventSource 按事件名订阅,漏登记即静默丢弃),到达触发既有全量重渲对齐标题。
- tests: api 发布断言(修复前红)、桌面 parser、契约 schema、web 订阅面各一。
Rename collab conversations now emit a domain event on the conversation-private
SSE topic so other ends update their rail leaf / mirror-page title in place.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…20 P2-01) 修「会话消息 DB commit 与 publish 之间无 outbox/replay」丢投裂缝:消息先落库提交、再 best-effort bus.publish,两步之间进程崩溃或 publish 抛错则 conversation.message.created 永久丢失(SSE resume_mode='fresh' 不重放,只有重连才全量补拉)。 - 0069 event_outbox 表(additive):事件与消息写同事务落 outbox,drain 提交后 publish、 成功才置 published;event_id 幂等键,消费端按全量重拉对账容忍重复。 - createUserMessage 增可选 enqueueOutbox 钩子,事务内原子写 outbox 行。 - createConversationService 接 outboxDrain:提交后即席 drain;崩溃残留由 event-outbox-drain 调度器启动补扫 + 周期重放(drain 失败结构化日志,禁空 catch)。 - 范围仅会话消息(人到人 DM/协同/主区人类消息);其它 publish 点留后续。 - 根因测试含 crash 窗口复现(纯内存 + 真 PG,opt-in),修复前稳定红。 Closes the commit→publish gap where a crash between the message DB commit and the SSE/event-bus publish permanently dropped conversation.message.created. Events now land in an outbox table within the same transaction as the message row; a drain loop publishes committed rows and replays anything left pending after a crash. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Onboarding used to fire-and-forget `updatePreferences({ locale })` with
`.catch(() => undefined)` right after a successful sign-in — a failed sync
was invisible, so the user believed their language preference was saved
server-side when it was not (it would silently revert on a fresh device or
cleared cache). Extract the retry orchestration into a small, DOM-free,
unit-testable module (onboarding-locale-sync.ts, mirroring the existing
confirm-button.ts/avatar-crop-modal.ts pattern, since browser.ts's top-level
`document` access makes it untestable directly): a failed sync now shows a
persistent notice with a working retry action, and a successful retry shows
a confirmation — it no longer proceeds without ever telling the user.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The avatar crop modal had no keyboard focus lifecycle at all: opening it never moved focus in, Tab could leak out to the page behind it (the modal is appended to body as a sibling, not a native <dialog>), Escape did not close it, and closing never restored focus to the button that opened it. Adds, following the R10 accessibility-batch precedent for this codebase: - open -> focus moves to the first operable control (the zoom slider) - Tab / Shift+Tab traps focus in a loop across the three controls (slider -> cancel -> confirm -> slider), never leaking to the background - Escape closes like Cancel (no confirm/upload) - any close path (cancel/confirm/Escape) restores focus to whatever had focus before the modal opened AvatarCropDeps gains an optional `getActiveElement` (defaults to document.activeElement) and AvatarCropElement now requires `focus()`, so the existing fake-DOM test harness can observe and drive this deterministically without a real browser. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
launchChrome() only ever threw "Timed out waiting for Chrome CDP target: <fetch error>" on failure — the fetch error is almost always a generic ECONNREFUSED, which says nothing about whether Chrome actually spawned, died immediately, or why (missing shared library, sandbox/permission issue, corrupt profile...). It also never attached an 'error' listener to the spawned child, so a bad chromePath (ENOENT) crashed the whole process uncaught instead of rejecting. Now captures: the spawn error (code/message), an early process exit (code/signal) detected without waiting out the full timeout, and a captured stderr tail — all folded into one structured error message alongside chromePath/debugPort/userDataDir. This is qa-infra-adjacent launcher code, not a smoke suite; verified with two new unit tests that inject a fake "chrome" executable (a controlled shell script), not by running any smoke suite. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…eline The backend has long had a cross-run audit timeline endpoint (GET /api/workitems/:id/audit — snapshots + audit-log facts + manifest checks, covered by packages/db's audit-repository tests), but no typed client method ever called it and the web work item detail page never rendered it. - packages/api-client: add getWorkItemAuditTimeline(workItemId), following the existing listWorkItemProposals/listWorkItemConflicts pattern. - packages/ui/gold-path/route-components.ts: render a new "cross-run audit timeline" card on the work item detail page (after the evidence section) with a hydration placeholder; renderWorkItemAuditTimelineRows is an exported pure function (localizes time + action + actor, marks undone entries, truncates to 8 with an honest "N more not shown" note, matching the evidenceRows precedent). - apps/web/src/browser.ts: bindWorkItemAuditTimelinePanel fetches the timeline client-side after the workitem route renders and hydrates the placeholder; a 403 is shown as "no permission" (distinct from empty), any other failure shows a visible error with a working retry — never disguised as "no history yet" (matches the P1-07 project-home-plans precedent). This only touches route-components.ts's workitem renderer and browser.ts (web-only); the desktop work item panel renders through a separate function (packages/ui/gold-path/render.ts's renderWorkItem) and is unaffected either way. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
refreshClaim swallowed both DB errors and 0-row renewals from
reservationRepo.refreshLease() with a bare `.catch(() => {})`, so a lost
budget reservation lease (already expired/released, or never reserved for
this workspace+run) went completely unnoticed. Outstanding-budget
accounting for concurrent runs would silently under-count the still-running
run, risking budget overrun with nothing in the logs to explain it.
Log agent_run_budget_lease_renew_no_rows / ..._failed via the structured
logger on the 0-row and error paths respectively; run status/execution is
unchanged (best-effort, same as before). Root-cause test in
agent-runs.test.ts drives a real heartbeat cycle with a fake reservation
repo that returns 0 and asserts the log line now appears (red before this
fix, green after).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
R19 review flagged three read-only endpoints with zero front-end/SDK
consumer beyond their own test stubs, each superseded by data already
delivered through an existing page VM:
- GET /api/agent-runs/{id}/handoff — structured handoff already embedded
in the /replay page VM (handoff/handoff_md fields, apps/api/src/pages/
replay.ts). SDK's getAgentRunHandoff had no caller in web/desktop.
- GET /api/ai-worklog/today — same "today's AI worklog" metrics already
embedded via AiWorklogMetricsService in the attention/agent-army
dashboard page VMs (apps/api/src/routes/pages.ts, agent-army-dashboard).
Never had an SDK wrapper at all.
- GET /api/workitems/{id}/proposals (list-work-item-proposals) — proposal
list already embedded in the work item detail page VM. SDK's
listWorkItemProposals had no caller; the POST on the same path (create
proposal from manifest) stays untouched and is still consumed.
Verified via repo-wide grep that no production web/desktop code calls
getAgentRunHandoff / listWorkItemProposals, and no code references
/api/ai-worklog anywhere outside its own route+openapi+service. Deleted
the route handlers, the ai-worklog route module (its metrics service is
still used elsewhere and is untouched), the SDK methods/types, and the
matching OpenAPI path/response-schema entries; updated the tests that
exercised the now-removed paths.
Added a regression test (app.test.ts) asserting all three are gone from
both the OpenAPI document and, for ai-worklog, a live HTTP request (404
instead of the old 401) — red before this change (paths still resolve),
green after. Per R17 G2, no SSE event enum values were touched — this is
HTTP-route-only cleanup.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…lling (R19-30)
GET /api/agent-runs/{id}/trace has supported an ?after= step_no cursor for
incremental fetching since it was built, but no front end ever passed it —
web's replay page re-fetches trace bundled into one full page-VM request
per SSE event, and desktop's Spotlight "replay" capability (list running
AI runs -> open a trace timeline) only ever did a single one-shot
getAgentRun() fetch with no live refresh at all while the detail view
stayed open.
Wire real incremental polling into the Spotlight replay view: once the
initial full run VM confirms the run is still active (queued/running),
poll GET /api/agent-runs/{id}/trace every 4s with after set to the highest
step_no seen so far, append only the new steps, and re-render just the
timeline. A full getAgentRun() resync (to catch status/usage and decide
whether to stop polling) runs when a "final" step appears or every 5th
poll as a safety net, since the incremental endpoint doesn't carry run
status. First load stays a full fetch; only the live-refresh loop is
incremental. Polling stops on dispose, on navigating back to the run
list, or once the run reaches a terminal status.
Investigated wiring the same pattern into web's /agent-runs/{id}/replay
page and decided against it for this batch: unlike desktop, web's replay
page has no existing client-side incremental-render capability — trace
currently arrives as part of one combined page-VM SSE-triggered reload
(not a separate call), and the shared SSE refresh path
(refreshCurrentRouteFromLiveEvent in browser.ts) is a heavily-hardened,
widely-reused mechanism across every route. Building a parallel
incremental-DOM-patch capability from scratch there is a materially
larger, riskier change for a low-priority finding; flagging as a
follow-up rather than bolting it on here.
Root-cause test in replay.test.ts drives a mocked setTimeout to trigger
one poll tick and asserts getAgentRunTrace is called with after=1 (the
initial trace's max step_no) and that getAgentRun is NOT re-called that
tick — red before this change (the view never calls getAgentRunTrace at
all), green after.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…pace roster (P1-08 tail)
The read-only web conversation-mirror route (/conversations/:id) still
resolved sender nicknames via client.listUsers() — the global, cross-tenant
/api/users directory that the rest of P1-08 already replaced everywhere
else with the workspace-scoped roster endpoint, precisely because it leaks
across tenants and hard-truncates at 200 users.
Checked the alternative (GET /conversations/:id/participants) before
switching: for "main" conversations — the common case this route mirrors —
listParticipants always returns scope:"workspace" with an empty
participants array by design (main has no conversation_participants rows;
it's workspace-visible, not participant-tracked), so it can't resolve any
nickname at all for the most common case. It only carries real rows for
collab/DM conversations. The workspace roster, by contrast, always has
data and is the same source already used for the approval-delegate picker
in browser.ts, so it was the right fit — full paginate-to-total is already
implemented in workspace-roster.ts.
Swapped the route to fetchWorkspaceRosterMembers(client), mapping
{user_id, nickname} to the {id, nickname} shape the renderer expects.
Confirmed apps/web now has zero non-avatar consumers of the global
/api/users endpoint (grep shows only /api/users/:id/avatar image URLs
remain). Updated routes.test.ts's fake client to serve GET
/api/workspace/roster instead of the removed listUsers stub; all prior
conversation-mirror assertions (sender nicknames, fail-soft on directory
error, not_identified bubbling) still pass unchanged.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
内容(SSOT: r19-iteration-review/02-ssot-2026-07-17.md)
六条代码线 + 一条调查线,各自隔离 worktree 施工,负责人逐单审查 + 独立红绿复验(含真 PG)后合流:
孤儿表处置(R19-7/34-37)为调查报告交付,drop/复活待产品拍板,本 PR 不含迁移之外的表变更。
验证
🤖 Generated with Claude Code