Skip to content

release v0.9.9-rc.0: 知识库+Office预览+onboarding全面修复+可观测性/稳定性加固 - #304

Merged
jsyqrt merged 67 commits into
mainfrom
release/v0.9.9-rc.0
Aug 31, 2026
Merged

release v0.9.9-rc.0: 知识库+Office预览+onboarding全面修复+可观测性/稳定性加固#304
jsyqrt merged 67 commits into
mainfrom
release/v0.9.9-rc.0

Conversation

@jsyqrt

@jsyqrt jsyqrt commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Version bump v0.9.8 → v0.9.9-rc.0

jsyqrt added 30 commits August 25, 2026 14:10
…0% to lift prompt-cache hit rate

Compaction rewrites the middle of the request and is the #1 cause of prompt-cache
misses (full-input re-billing on Anthropic/OpenAI/DeepSeek/MiniMax/OpenRouter).
Raising CONTEXT_PROACTIVE_COMPACT_RATIO 0.55 -> 0.75 keeps the cached stable
prefix intact across most turns (append-only = prefix HIT), addressing the
cache-hit-rate drop / cost increase regression seen around the 128K-window fallback.

- CONTEXT_PROACTIVE_COMPACT_RATIO: 0.55 -> 0.75
- CONTEXT_WARN_RATIO: 0.85 -> 0.80 (sits just above auto-compact so the agent is
  nudged to run an anchor-preserving session_compact before the system bluntly
  auto-compacts; keep below it by invariant)
- CONTEXT_CRIT_RATIO unchanged (0.95 hard-trim backstop)

Tests: context-os/context-engine/context-engine-deep all green (79). Full core
suite 2310 passed; remaining failures are pre-existing external web-search/API
key-dependent cases, unrelated.
1. Team chat IME guard: while pinyin composition is active (isComposing/keyCode 229),
   Enter commits the text to the textarea instead of sending the uncommitted letters.

2. Store purchaseAndInstall: open checkout in the system browser via the Electron
   app:open-external bridge instead of a raw window.open that gets denied in Electron
   (returned null, breaking the poll-cancel path). Web path unchanged; Electron path
   now relies on a 5-min timeout + backend polling instead of popup.closed.

3. A2A storm guards:
   - a2a tool: derive a stable conversation_id from the DM channel key when the
     caller omits it (was generateId each send -> duplicate-conversation churn).
   - api-server DM trigger: add per-channel cooldown (mirrors group-chat layer) so
     rapid mutual proactive agent_send_message cannot loop unboundedly, and the DM
     auto-chain re-trigger is throttled the same way.
- shared: add LLMAssignment.defaultModel (stored in agents.llm_config JSON)
- core: getEffectiveModel() wired into all LLM call sites (chat/chatStream/task/RIS/browser); agent-manager restores defaultModel and injects agent settings ctx
- tools: agent_model_get / set_default / reset_default / test — view & update own + global model, persisted to DB
- web-ui: ChatModelMenu dual scope (agent default ON; global also resets current agent), per-agent model section grouped by team, AgentProfile shows effective model w/ source badge
- i18n: zh-CN + en for all new model-config UI
… agents first in per-agent model settings

- AgentProfile: badgeAgentModel/badgeSessionModel/followsGlobal looked up at overview.* but live under overview.labels.* — i18next fell back to English/key; now resolve to correct nested path
- PerAgentModelSection: agents without a team now render in the Ungrouped group at the TOP, before team groups, so loose agents stay visible
…ion rename

- agent_send_message response now includes recipient.status; a stopped
  recipient gets an explicit WARNING (no more silent dead letters)
- new wake_recipient: true wakes a stopped recipient before dispatch
  (reuses AgentManager.startAgent; clean error if wake fails)
- await_in_session renamed to reply_in_session (old name kept as
  deprecated alias, both still functional); docstring now makes clear
  it does NOT block - the reply re-wakes the thread
- system prompt + docs (PROMPT-ENGINEERING, MAILBOX-SYSTEM) updated
- +7 tests: stopped warning, wake success/skip-on-running/wake-fail,
  unknown recipient, schema, reply_in_session alias callback
Live testing exposed a bug in the stopped-recipient warning: the
roster status comes from the agent's internal execution state, which
still reads 'idle' right after a stop in the running app, so the
=== 'stopped' check (and wake_recipient gating) never fired.

- A2AContext gains isRunning(agentId): authoritative liveness from the
  manager (state.status !== 'offline' — stop() sets 'offline')
- agent_send_message prefers isRunning when provided; falls back to
  roster status (tests / contexts without runtime access)
- recipient.status now reports 'stopped' based on real liveness
- 2 regression tests pin the misleading-roster-status shape
- identity.colleagues[].status was a creation-time snapshot refreshed only on
  org changes — the system prompt showed stale 'all teammates stopped' while
  agents were actually running.
- Add liveColleagueStatuses (agentId -> runtime status) computed at prompt
  build time via Agent.setColleagueStatusProvider; identity snapshot remains
  as fallback.
- Move the '## Team Status' section to the very END of the dynamic tier
  (after timestamp/locale): cache as late as possible — a status flip only
  invalidates from Team Status to the prompt tail, never the stable prefix.
点击停止后 SSE 断开,若正在回复的气泡为空被清理,reattach 会取
'最后一条 agent 消息' 作为续写目标——那其实是上一轮的回复 B,
导致新回复 D 直接覆盖 B,用户问题 C 被挤到 D 下方(A→D→C)。

修复:
- tryReattachActiveStream 只允许续写 'in-flight' 气泡(isStreaming
  或空占位),绝不复用已完成回复;无目标则新建气泡
- reattach 消费端的 reverse().find(agent) 兜底要求 isStreaming,
  避免流式文本/收尾写入上一轮气泡
- stopSending 记录 userStoppedSessions,reattach 拒绝恢复用户
  主动停止的 turn(消除'点了停止还在继续输出'的假象);
  新一轮 send/session_start 时清除标记
- 新增 pickStreamReattachTarget 纯函数与回归测试
根因:单发 spawn_subagent 在 BASE_TOOL_NAMES/TOOL_DEF_CORE_KEEP,
永远默认暴露,是串行的(父循环 await,A→B→C);而并发版
spawn_subagents 只靠 recentToolNames 或 discover_tools 激活才进
schema,新会话模型拿不到,于是退化成多次串行 spawn_subagent。

修复:把 spawn_subagents 加入 TOOL_DEF_CORE_KEEP,常驻默认工具集,
模型扇出独立子任务时直接可用。执行本身已是 Promise.allSettled 真并发
(每子 agent 独立上下文+工具集),上限 SUBAGENT_MAX_PARALLEL=10,
聚合迭代预算 100 控成本。

隔离/安全不变:子 agent BLOCKED_TOOLS 仍禁止递归 spawn、
notify_user/request_user_input/request_user_approval、discover_tools、
schedule_wakeup,无法打扰用户或嵌套扇出;max_iterations/allowed_tools
仍可按子任务收敛。

测试:更新 converse 默认集断言;新增『3×200ms 并发,总耗时<500ms
而非串行 600ms』证明并发性。
分组用硬编码前缀表 categorizeTools/TOOL_CATEGORY_DEF。新增/改名的
平台工具没有前缀条目时会掉进「其他」,观感像分组 bug。

本次补全漏掉的工具到正确分组:
- agent_model_get/set_default/reset_default/test → 大模型(llm)
- describe_image, upload_reference → 多模态
- session → 记忆(与会话/上下文管理一致)
- list_teams → 团队管理
另补上缺的 terminal 本地化 key(zh/en)。

重构:把 categorizeTools + TOOL_CATEGORY_DEF 从 AgentProfile.tsx
内联提取为 lib/toolCategories.ts 纯函数模块,并加回归测试锁定
'这些平台工具不得进「其他」',防止将来新工具再次漂移漏分组。

验证:toolCategories 3 测试 + web-ui 172 测试全绿,tsc 通过。
- agent-manager: 新增 browserMode(embedded|system-chrome) 动态路由,
  内置浏览器优先于扩展与 npx 兜底;系统 Chrome 模式屏蔽内嵌分支并返回清晰报错
- runQuickBrowserTest 按 browserMode 分发,新增内置浏览器快速验证
- config: browser.mode 枚举落盘 markus.json,启动自动加载
- org-manager: /api/settings/browser GET/POST 读写 mode
- Settings 页重构:浏览器后端选择器 + 模式状态卡 + 通用/高级分层排版,
  内置模式不再暴露插件,验证连接按模式走对应后端
- i18n en/zh-CN 新增浏览器设置文案
…che hitrate)

- context-engine: deprecate in-system 'dynamic' segment; route all per-call
  dynamic content into volatile via buildSystemPrompt {text, segments, volatile}
- buildSystemPrompt returns stable prefix 'text' + volatile tail; stable prefix
  is byte-constant across turns so implicit prefix KV-cache (DeepSeek/OpenRouter
  via markus-provider) hits system + whole history; only tail message changes
- prepareMessages appends [SYSTEM][Live context] (role user) at history end,
  before current query; contextHint watermark + volatile both ride the tail
- memory/store: remove in-place rewrite of >100KB old tool results in
  checkAndCompact (was the last source-level mutation that broke prefix cache);
  transient truncation already handled per-request by shrinkOversizedMessages
- add byte-level stability regression test: two different dynamic contexts =>
  turn1.text === turn2.text (same cache key) while volatile differs
- fix ~15 tests to new contract (context-engine, cache-optimization, cognitive,
  manus-practices, prompt-profiles, agent-coverage, agent-deep...): dynamic
  asserts read result.volatile, stable reads result.text; fix 65->66 turns,
  last-user-message pick
- provider-helpers/tool-selector/agent-metrics/limits/api-server/web-ui:
  consistent volatile/live-context wiring and usage overview
- docs: consolidate prompt engineering into PROMPT-ENGINEERING.md (Scheme A
  volatile-tail), add ContextOS fixed/variable split + session anchors (§3.7),
  unify cross-references, drop docs/context-os.md (git history archived)
Data source (OpenRouter cached_tokens) lacked a reliable time-series feed,
and the Hub /user/cu/stats endpoint is not populated for direct(OpenRouter)
traffic. Per owner decision, drop the hit-rate feature entirely — remove the
Contributor cacheHitRate field, the ranking column, and related i18n keys.
…lt isolation (SS-1)

- anthropic chat(): add per-call hard timeout (was none -> eternal hang risk)
- anthropic chatStream(): idle per-chunk reset + 15min hard cap + graceful
  stream-termination (partial->max_tokens, none->descriptive timeout error)
- anthropic tool args: safe JSON parse (malformed/truncated args no longer throw)
- task-queue: per-task timeout frees concurrent slot so a hung provider call
  cannot block the whole scheduler queue
- tests: hung chat times out; stalled stream finalizes; hung task releases slot
- docs: mark feedback S-domain 16/17/18/24/29 status in 0.9.8 analysis
- 新增 lib/streamResilience: createStreamWatchdog(空转看门狗) + exponentialBackoffDelay(全抖动指数退避)
- api.agents.messageStream / api.sessions.reattachStream 接入看门狗, reader 不再卡死(pending)
  → 服务端死亡/半开连接时优雅降级, 杜绝永久思考中
- Team.tsx pollForReply 改用指数退避+抖动+上限(5次/base2s/max8s), 避免断连时紧密狂轮
- 全路径(done/error/catch/正常结束)清理计时器
- 测试: streamResilience.test.ts 10 例; 全 web-ui 182 例通过; tsc clean
- 更新 feedback-0.9.8-analysis.md (F域16/17/18) 与审计报告
- shared crash.ts: installCrashGuard(uncaughtException/unhandledRejection/OS 信号)
  写 ~/.markus/logs/crash.log + last-crash.json,记录退出码/信号/时序/内存峰值/上下文;
  detectUncleanShutdown 区分干净停止 vs 崩溃(run-state 心跳);memoryWatermarkWatchdog
  每 5s 采样内存水位 + 30s 心跳,OOM 留痕
- cli supervise.ts: markus supervise 父进程拉起 markus start,异常退出指数退避自动重启(上限 5 次),带出崩溃上下文
- start.ts: 启动时 detectUncleanShutdown 告警 + installCrashGuard + memoryWatermarkWatchdog,
  shutdown 时 markCleanShutdown;Electron backend.shutdown() 走同一 clean 标记
- 测试: crash.test.ts 9 项单元 + crash.e2e.test.ts 3 项真实子进程 E2E 全通过
- 文档: feedback-0.9.8-analysis.md 问题35 标记已闭环
根因:调度器 fireScheduledTask 无条件把 completed→in_progress 并投递到执行
agent 的 mailbox,不检查 agent 是否在线。agent 离线时消息堆积无人消费,
任务永久停在 in_progress(日志仍是上一轮'已提交审核',造成状态错位观感)。

修复(scheduled-task-runner + task-service):
- fireScheduledTask 触发前检查 assigned agent 运行态(getState().status !==
  'offline');离线则跳过本轮、不推进 nextRunAt(保留补跑语义),并节流告警
  日志避免每 60s 刷屏。
- 新增 reclaimStuckScheduledTask:对已卡在 in_progress 且 agent 离线的
  scheduled 任务,经合法 FSM 路径标 failed,agent 上线后 resetTaskForRerun
  自动补跑(failed 在 resettableStatuses 内)。
- 新增测试:离线 agent 不触发/不推进/不投递;in_progress+离线自动回收。

org-manager 42 文件 / 1043 测试全绿。
- agent-runtime.ts:纯函数派生 AgentRuntimeInfo(phase: idle/thinking/running/
  waiting-dependency/degraded/blocked/error/offline),blockedBy 回填依赖任务
  标题与状态,runningMinutes/lastHeartbeat/tokensUsedToday 一并暴露
- GET /api/agents 每个 agent 附加 runtime 字段,不改变 status 旧字段语义
- agent-manager.listAgents() 补充暴露 activeTaskIds/lastHeartbeat/tokensUsedToday
- 新增纯函数单测 + API 层断言(idle/running/waiting-dependency、无污染)
- AgentInfo.runtime 类型(phase/activityLabel/blockedBy/lastHeartbeat/…)
- Home.tsx Who's Working:按 runtime.phase 展示彩点 + 阶段文案
  (思考中/处理中/等待依赖/降级/出错),副标题「卡在依赖 XX · 已运行 N 分钟 ·
  最后活动 HH:mm · 最近错误」,旧后端回退到 currentActivity.label
- 概览「正在工作」数量按非 idle/offline 的 runtime.phase 统计,老后端回退 status
- en/zh-CN home.json 新增 agentFocus.phase*/blockedBy/runningFor/lastActivity
- 22:残留脏状态已可被 OB-1 派生为 thinking 可见;自动清理归 OB-3(待执行)
- 31/32/33:运行轨迹可见主体已闭环(runtime.phase + blockedBy 标题 + 概览展开),
  剩余节点级事件流/日志归属归 OB-2
- 34:可见性部分随 OB-1 改善,根因深挖归 S/F 域
…ocked 死分支消除:当前任务自身 status=blocked → phase=blocked,waiting-dependency 保留为「任务就绪但依赖未完成」
- agent-dirty.ts:纯函数 evaluateDirtyState 判定 processing-like 但无存活任务/心跳过期/标记超时的遗留脏态;剔除误杀(有存活任务/心跳新鲜/刚启动);恢复动作分级 reconcile-idle / trigger-heartbeat / human-review
- agent-dirty-reconciler.ts:周期扫描兜底,写 execution-stream 事件(谁/何时/为何可追溯),feature flag 可关、幂等去重
- api-server:GET /api/agents 派生暴露 runtime.dirty;start 守卫启动 reconciler(兜底=触发恢复心跳,非破坏性)
- 前端:Home Who's Working 对有 runtime.dirty 的 agent 显示「已自动兜底恢复 / 需人工介入」徽标
- docs O 域问题 22 标闭环并写清理机制
- 测试:agent-dirty 11 + reconciler 5 全绿(含不误杀/去重/disabled/human-review)
- agent-stall.ts:纯函数 evaluateStall 消费 OB-1 的 AgentRuntimeInfo,判定疑似卡死
  (stalled/stallKind/stuckOnTaskId/stuckOnTitle/currentTaskId/lastActivityAgoMin/
  stuckReason/suggestions):
  * stale-heartbeat —— running/thinking/waiting-dependency/blocked/degraded 但
    lastActivity 停滞 >=10min → 疑似卡死,定位当前任务
  * dead-dependency —— waiting/blocked 但被依赖任务 failed/cancelled/archived →
    依赖已死仍无限等待,明确卡在某依赖
  * 心跳新鲜 / 依赖仍在跑 / idle / error → 不判卡死,防误杀
- api-server GET /api/agents:runtime.stall 暴露,不改 phase/status 语义
- 前端 Home Who's Working:对 stall 显示琥珀色「疑似卡死/依赖已死」徽标 +
  明确定位文案(卡死依赖标题/停滞分钟),替代无限转圈;web-ui api.ts 镜像类型
- docs O 域 33/34 标卡死定位闭环
- 测试:agent-stall 单测 9 + API 层 4 全绿;org-manager 全量 1067 通过
# Conflicts:
#	docs/feedback-0.9.8-analysis.md
#	packages/org-manager/src/api-server.ts
#	packages/web-ui/src/api.ts
#	packages/web-ui/src/pages/Home.tsx
… google/ollama 流逐 chunk 重置超时

- blocked→failed 状态转换合法化(对称 cascadeCancel),blocked 任务在依赖 failed 时
  级联失败而非永久卡死(反馈#27/#28根因)
- createTask/updateTask 校验 blockedBy 存在性 + 环检测(修复 create 侧漏检)
- google/ollama 流改逐 chunk 重置 idle 超时 + 硬顶,不再单一定时器(SS-2遗留)
- 同步 STATE-MACHINES.md + 新增 5 个依赖治理测试用例
- 根因:remark-gfm 的裸 URL autolink 只以 ASCII 空白/标点为界,对 CJK
  和全角标点无边界识别,导致 https://x.com/)去接… 整段中文被渲染成链接
- 修复:新增 autolinkBareUrls——把裸 http(s) URL 预包裹为显式 [url](url)
  链接(在 URL 终止于首个 CJK/全角字符处),GFM 不再触发 autolink
- 接入 MarkdownMessage(聊天)与 ArtifactDetail(文件预览)两个 remark-gfm 渲染链
- 新增 7 个单测覆盖 CJK 尾随/ASCII 标点/已显式链接/多 URL 等场景
- LLMProviderConfig 新增 maxRetries / retryBaseDelayMs / maxRetryAfterMs
- markus-provider 构造与 reconfigure 时,config 优先 → env(MARKUS_*) → 默认
- clampInt 校验非法值回退默认,避免环境变量 NaN 把重试归零
- parseRetryAfterMs 接受 per-call 上限
- 新增 3 个测试:cap 生效 / config 读取 / env 非法值回退(67 全绿)
- #9 付费技能 UI:ef42eda7 Electron 外链打开修复
- #11 macOS 输入法回车:ef42eda7 IME guard
- #12/13 唤醒/恢复链:ef42eda7+a5349b42+f9c145ac
- #15 调度幂等:ef42eda7+2bae3ef0
- #26/27/28 依赖治理:d93720f1
- #29 重试可配:c0f5b5e8(FD-2)
jsyqrt added 27 commits August 29, 2026 14:52
- 新增 OfficePreviewer:按格式惰性分发渲染
  - pdf → pdf.js canvas 渲染(缩放 50%-300%、翻页、超大文件提示)
  - docx → docx-preview HTML 渲染
  - xlsx → SheetJS 自绘表格
  - pptx/doc/xls/ppt → 回退「用系统应用打开 / 下载」
  - 边界:加载中/失败重试/空文件/中文文件名/大文件提示
- RightPanel 接入 office 模式(flex 布局 + 工具栏 + 回退)
- Deliverables 详情页接入 office 预览(55vh 容器)
- 依赖:docx-preview@^0.4.0, xlsx@^0.18.5, pdfjs-dist@^6.2.108(均 Apache-2.0)
- i18n:zh-CN/en office 文案
- project-tools:新增三个知识库工具,薄封装 deliverable_*(无新基建)
  - knowledge_search:强制 source='knowledge' + project_id 过滤 + query 必填
  - knowledge_list:强制 source='knowledge' + project_id 过滤
  - knowledge_read:读取知识库文档文本(deliverableRead 桥 → extractTextFromFile)
  - ProjectToolsContext 增加 deliverableRead 桥
- agent-manager:buildDeliverableCallbacks 注入 deliverableRead(复用 file-converter extractTextFromFile,不可读返回 null)
- tool-selector:deliverables 组接入三个 knowledge 工具 + 关键字(zh/en 知识库/knowledge base)
  - 修复:deliverable_search 在 base 集导致关键词激活被短路(组未全选中时仍可补全)
- 测试:project-tools +13(source 强制/过滤/错误路径/缺桥未注册);tool-selector +2(中英文关键词激活,Agent 工具面可见)
- core 类型检查通过;相关测试 67+24+66 全绿
问题:多个任务同时分派 → 多条 triggerExecution 消息进队列 → 触发深度分拣
(deliberation)。但深度分拣的工具面是白名单(DELIBERATION_ALLOWED_TOOLS,
无 task_submit_review/shell/file),agent 在分拣中完成任务却无法提交评审,
且任务被延迟至少一轮。

修复:
- attention.ts needsLLMTriage:队列存在 triggerExecution 任务消息时跳过
  deliberation,正式任务按优先级队列正常出队 executeTask()(完整工具链)
- agent.ts deliberation 提示词补充作用域说明:分拣只处理消息,不处理正式任务
- attention.test.ts:新增测试锁定「任务在队 → 不分拣」行为
- AgentProfile.tsx:working 状态「处理中」区块显示 currentActivity(runtime)
  + mailbox 排队数,不再要求 activeN>0(修 working 但处理中为空的错位)
- api.ts AgentDetail 补充 runtime 类型(后端已下发,前端类型缺失)
- i18n:mailboxQueued 文案

测试:core attention/mailbox/agent 相关 119+79 全部通过;core/web-ui tsc 干净
- knowledge-tools-e2e.test.ts 新增 9 例宿主级 e2e:真实 Agent(AgentManager +
  createAgent 运行时)通过工具面自主调用 knowledge_search/list/read,核验
  source=knowledge 强制、updatedAt 对齐、中文文件名读取、知识库根内/外读取
- knowledge_read 桥接层前缀校验:project_id 提供时仅允许读项目绑定
  knowledge_base_paths 内文件(防任意文件读原语);文件不存在返回 null 报错
- knowledge_search 结果补 updatedAt 与 knowledge_list 对齐;错误信息统一包装
  (Knowledge search/list/read failed: ...)
- extractTextFromFile 文本类读取优雅降级:不可读/IO 错误返回空串而非抛错,
  避免单个文件权限问题中断整次知识库同步;>500KB 截断
- 边界用例:超大文件 500KB cap、中文文件名、权限/IO 错误、空目录、
  隐藏文件与 node_modules 忽略(knowledge-sync 8→13 例、file-converter 8→11 例)
- knowledge_read 支持 project_id 透传(含 camelCase 别名)

测试:core 关键套件 239 全绿(e2e 9 + project-tools 46 + file-converter 11 +
agent-manager 121 + tool-selector + office-generate 28);org-manager 171 全绿;
tsc --noEmit(core/web-ui)0 错误;vite build 通过(四 Office 库独立惰性 chunk)。
selectProject 在 rightDetailMode==='closed' 时保持关闭(仅更新筛选),
编辑按钮之前只调 selectProject → 详情面板永不打开。
现在 onEditProject 选中项目后强制 setRightDetailMode('project')。
- 将 recoverTextToolCalls 提为共享函数(provider-helpers),openai.ts 流式/非流式全部接入
- 混合输出(结构化 toolCalls + 正文明文)时始终清洗正文,不再跳过
- convertMessagesOpenAI 发送前对历史 user/assistant/tool 消息做明文剥离,阻断明文永久回灌
- 清理 MiniMax <]minimax[> 围栏噪声,finishReason 归一为 tool_use
- 新增 22 个相关测试(114 相关测试全通过;全量仅外部 API key 类失败,与本次无关)
- llm/provider-helpers: 新增 stripToolNoise 无条件噪声剥离与 createSafeTextEmitter
  流式安全发射器;parseOpenAICompatResponse 混合输出清理 + finishReason 归一化
- llm/markus-provider & openai: 流式 text_delta/thinking_delta 过安全发射器,
  泄露出 UI 的明文 <invoke> 工具标签不再可见;流结束前 flush 缓冲
- agent: 三处流式回调统一 stripToolNoise 兜底(含重复检测)
- tools/office-generate: docx 图片 type 由硬编码 png 改为按扩展名推断
- agent-manager: knowledge_read 无知识根目录时拒绝读取(防空转越权)
- storage: purgeLeakedToolMarkup 存量清洗由占位符改为剥离为空白,
  含未闭合标签与 DSML/minimax 围栏;补齐单测

测试:provider-helpers 30 项 + office-generate 30 项 + storage 15 项新增全部通过;
typecheck 通过;llm/agent 核心 242 项无回归
根因:任务执行指令(task_status_update + triggerExecution)在 mailbox
defer/drop 后,回调闭包(onLog/cancelToken/responsePromise)随 JSON 持久化
丢失,resurface 后 agent 无法真正执行 → 任务永久卡 in_progress 无人处理。

六层纵深防御 + 自愈:
1. shared: isStrictStateItem 判定 + AgentMindState.isStrictState 标记
2. attention: 严格状态项从 deliberation 视图隐藏,绝不触发深度分拣
3. mailbox-tools: defer/drop_mailbox_item 对严格项直接报错
4. agent: complete_deliberation 拒绝将严格项放入 defer/drop/inline/batch
5. 自愈: agent 发 agent:incomplete → TaskService.recoverLostTaskExecution
   带防重入重新调度(闭包完整)
6. prompt: deliberation 场景明确只可 prioritize、不可 defer/drop

新增 16 个回归测试;相关 267 测试全绿;typecheck 通过。
- ProjectKnowledgePanel: 绑定路径后自动调用同步(仅扫新增目录),
  避免需手动点'重新同步'才能在产出物页看到文件
- Deliverables: 绑定弹窗同样绑定即同步,成功后自动切到知识库筛选并刷新
- 同步中禁用按钮防止重复提交,失败保留弹窗并提示
- 更新中英文文案
- 新增进度心跳(lastProgressAt),工具/LLM 调用时刷新,解决长时间工作被标疑似卡死
- 修复 dirty reconciler 事件发错总线 + 去重永不重试 bug
- 前端 status=idle+残留活动不再派成 thinking,改为 idle + 徽标
- 文案弱化疑似卡死→长时间无活动,阈值 10min→30min
- AgentProfile 概览 tab 移除一直为 0 的 CU Used 指标(用户可用自有模型/CU 无意义)
Uint8Array.prototype.toHex 需要 Chrome 140+ (Electron 38+),
当前 Electron 35 (Chrome 134) 不支持。
pdfjs-dist 6.x 的 Worker 中调用 hashOriginal.toHex() 崩溃。

修复:在主线程 polyfill + Worker blob wrapper 中都注入 toHex polyfill,
与已有的 Map.getOrInsertComputed / Promise.try / Iterator.join 同策略。
升级 Electron 不现实(需 38+,且带来大规模 breakage)。
- 删除 docs/feedback-0.9.8-analysis.md(信息由交付物「Markus 0.9.8 用户反馈—修复状态对照」接管)
- 删除 docs/knowledge-base-office-plan.md、docs/knowledge-base-office-t7-summary.md
- 同步更新引用:project.ts / sqlite-storage.ts 注释、audit-frontend-stream-resilience 依据
问题:引导状态用全局 localStorage key(markus_onboarded / markus_checklist_dismissed),
导致 1) 用户在存量实例里新建测试账号看不到引导(组织已有数据 / key 已被旧用户置位);
2) 老用户清缓存后可能被再次引导。

修复:
- 新增 lib/onboarding.ts:per-user 引导状态(markus_onboarded_<uid>,
  markus_checklist_dismissed_<uid>),读取时回退兼容旧全局 key,老用户升级后不重新引导。
- App.tsx:全屏向导只由「首次登录(needsOnboarding)」「邀请流」显式触发;
  恢复会话(auth.me)与老用户登录均立即标记该用户已引导,杜绝打扰。
- Home.tsx:引导清单仅对未完成引导的用户渲染;project/requirement 步骤改为
  按当前用户归属(createdBy)判定,存量组织数据不再误标为新用户已完成;
  greet 步骤只统计当前用户会话。
- checklistReady 增加 8s 超时兜底:Hub claim 网络调用无超时,离线/慢网时
  不再永久隐藏整张引导卡。
- api.ts:ProjectInfo 补 createdBy 字段(后端已返回)。
- 新增 onboarding.test.ts 覆盖 per-user 隔离与旧 key 回退。

校验:tsc --noEmit 通过;vitest onboarding(4) + web-ui 其余(22) 全部通过。
问题梳理:登录方式(Google/GitHub/Email/本地/会话恢复)本身对引导判定无差异,
真正的三类缺陷:
1. 全屏向导完成后会写「已引导」→ 概览引导清单(配置模型/打招呼/建项目等
   真正上手步骤)永远不再显示(上轮引入的死代码回归);
2. 会话恢复(auth.me)无条件标记已引导 → 向导中途退出/清缓存的新用户
   永久丢失全部引导;
3. localStorage 状态可被清除 → 老用户换浏览器/清缓存可能被再次打扰。

修复(引导状态服务端化):
- 后端:PUT /auth/me/preferences 白名单新增 guideHidden 字段(合并语义,
  不丢 locale/timezone);login/hub-login/me 均回传 preferences。
- 前端 App.tsx:
  - 全屏向导完成【不再】标记已引导——让概览清单接棒呈现上手步骤;
  - 会话恢复/登录仅在服务端已有 guideHidden(或本地已有完成标记)时
    写本地并持久化,新用户/未完成引导用户绝不提前标记;
- 前端 Home.tsx:
  - 引导清单渲染条件由「未完成全屏向导」改为「!guideHidden && 尚有必做步骤」;
  - 清单全部完成或 dismiss(必做 setup 已完成) → 服务端持久化 guideHidden,
    换浏览器/清缓存永久隐藏;
  - dismiss 时必做 setup 未完成仅本地记录,防止过早丢失「配置模型」引导;
  - 步骤计算提升到组件主体,供渲染 + 自动完成检测复用。
- 测试:org-manager 新增 preferences 持久化/白名单过滤用例(含 mock 补
  updateHumanPreferences);web-ui 全量 193 用例 + tsc + eslint 通过。
- api-server: isProviderDisabled 添加可选链,mock router 无该方法时不再 TypeError→500
- office-generate.test: 中文嵌入用例检测系统 CJK 字体,无字体环境跳过(CI Ubuntu)
- publish.yml: NODE_OPTIONS=--max-old-space-size=8192 提升打包 heap,修复 macOS vite build OOM
@jsyqrt
jsyqrt merged commit f60644b into main Aug 31, 2026
1 check passed
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