feat: 账号健康管理、流终态可观测性与用量刷新菜单 - #84
Merged
Merged
Conversation
- 恢复尝试在调用 open() 前记录 attempted account,避免建流失败时 activeAccount 未更新 - 以请求级 failedAccountIds 累计失败账号,后续尝试不再重选已失败账号 - 新增 ServiceQuotaExceededException/402/429 统一分类器,不永久标记账号失效 - 每次恢复记录 attemptIndex/phase/cause/failureClass/selectionReason - 建流失败日志保留 conversationId/model/processId 并标记 identitySource
- computeCodeRanges 支持 3 个及以上反引号或波浪号开栏 - 闭栏须为同一字符且长度不小于开栏,未闭合围栏延伸至文本末尾 - 围栏内 marker 一律解析为 none 而非 incomplete,避免恢复模式丢弃整轮输出 - 孤立未闭合单反引号不视为代码区,防止过度抑制 - 补充 9 条回归测试
- 导出共享 helper textToolCallOpeningMarkerStarts(),DialectGate.firstMarkerIndex 改为复用它 - 围栏内 marker 不再阻塞后续正文发布,也不再激活 dialect 状态 - 围栏外 marker 的 anti-leak 行为保持不变 - 保留 partialMarkerTail 防跨 chunk 半 marker 泄漏 - 未闭合行内单反引号仍暂扣,直至闭合证明其为 inline code - stream-observer-closure.test.ts 中 fenced code 的 dialectActive 期望由 true 改为 false, 即本次目标行为变更;该测试 sawToolIntent / hasOpenToolIntent 断言未变
- failedAccountIds 提升到 handleKiroRequest 作用域,外层首输出前重试与 live recovery 共用同一集合 - 恢复期 quota_or_rate_limit 触发有界 30 秒 markRateLimited,不再 markUnhealthy - 账号选择前按 PRAGMA data_version 或 1 秒 TTL 局部刷新健康字段, 新增 getDataVersion / getAccountHealthSnapshots,不做全量 loadFromDisk - 以 pendingHealthWriteCounts 计数守卫本地未持久化的健康改动,避免刷新回滚刚写入的 cooldown - 写入 reject 时保留本地值并解除守卫
- 新增稳定事件常量 STREAM_TERMINAL_LOG(Kiro stream request terminal), 统一输出成功/失败/取消三类终态摘要;原有五个稳定事件名保持不变,仅追加字段 - 新增字段 streamStartedAt、eventTypeCounts(仅记录 SDK 事件 discriminator 名与计数,不保留任何 payload)、hasOpenToolIntent、reasoningPhase、 dialectActive、dialectMarkerIndex、dialectMarkerInCodeRegion、 dialectResolution、terminalSource - terminalSource 取值为 clean_eof_without_completion_metadata、 completion_metadata_received、iterator_failure、semantic_truncation、 caller_abort、stream_attempt_budget_exhausted;语义截断优先级高于 iterator 包装,不被其覆盖 - observeTextToolCallOpeningMarker() 单次代码区扫描同时给出原始 marker 位置 与可执行 marker 位置,DialectGate 复用该结果并新增 markerIndex / markerInCodeRegion 只读 getter - StreamObserver 新增的 noteRawEvent / noteDialectMarker / noteTerminalSource 均为只写摄入方法,transformer 不回读,观测不改变任何 已发出的 chunk - OpenCode session/message 真实身份在 provider fetch 边界不可获得 (conversationId 由 crypto.randomUUID() 每次请求生成,并非 session ID), 故不输出永久为空的 sessionId/messageId,改以 conversationId + processId + streamStartedAt + model 作为最强可用关联键,并以负向测试断言不存在伪身份字段
- 健康写入保护:HTTP 500 的 failCount 增量改由新增 AccountManager.recordFailure() 统一持久化并注册 pending write,避免 1 秒健康刷新用数据库旧值覆盖内存计数; 此前持续 500 时计数可能永远到不了 5,账号既不标记异常也不轮换。 handleSuccessfulRequest 改用新增 markHealthy(),不再绕过 pending guard; getCurrentOrNext 三条本地恢复路径同样接入。 - 终态摘要 exactly-once:终态日志所有权提升到 handleKiroRequest 请求作用域, 单一 once guard 覆盖 HTTP 错误、网络错误、流、live recovery、abort 与兜底异常; Kiro stream request started 恢复为严格分母,每个已开始的请求恰好一条终态记录。 - 恢复建流失败降级记录:recovery attempt 在 open() 阶段失败且无 logDetails 时, 改用 latestRequestLogIdentity 输出终态记录,保留 phase: 'pre_stream_open' 与 identitySource: 'previous_attempt'。 - exact replay early_end 优先级:协调器显式传出终止原因,attempt_budget_exhausted 覆盖该 attempt 已记录的 clean EOF,修正此前日志把回放失败误报成 clean EOF 与 completed;semantic_truncation 不被 iterator_failure 覆盖的规则保持不变。 - 终态来源双层类型:STREAM_TERMINAL_SOURCES 为 observer 可达的 6 种, REQUEST_TERMINAL_SOURCES 追加 http_error / network_error / request_error; logTerminalSummary 强制 RequestTerminalSource,调用点写入未声明值现在会 tsc 报错。 - repository cache 失效:persistLocalHealthMutation 在写入成功后经注入回调调用 repository.invalidateAccount(),修复 markHealthy 改造中丢失的缓存失效; AccountCache TTL 60 秒,5 处读取面此前最长 60 秒看不到健康恢复。
- 持久化移出同步调用栈:getCurrentOrNext() 是同步函数,而 upsertAccount 在无锁 竞争时会在首个真实 await 之前同步走完 BEGIN IMMEDIATE + 全量 read-merge-write + COMMIT,等于在选号热路径上同步持有 SQLite 写锁(同一线程负责转发 SSE)。 persistLocalHealthMutation 改为经 macrotask(setTimeout(…, 0))延迟启动写入, 同步 API 立即返回。microtask 不够:它仍在事件循环让出 I/O 之前执行。 - 同账号写入串行化:新增 healthWriteChains,同一账号的健康写入按调用顺序排队。 此前两个并发快照各自随机退避可能乱序提交,而 mergeAccounts 会把 healthy incoming 视为 recovered 并采用其 failCount,导致陈旧快照复活已清零的失败计数 或抹掉刚写入的 unhealthyReason。pending-count 守卫保留,两者解决的是不同问题 (回灌 vs 顺序)。 - 单账号永久错误:error-handler.ts 原本直接 account.failCount = 10,不持久化、 不注册守卫,且 isHealthy 仍为 true;单账号时后续分支不会补救,1 秒健康刷新即 回滚。改为经 markUnhealthy(account, toDeadReason(errorReason)) 统一处理;多账号 分支不再重复调用。 - 终态记录不再混入陈旧 attempt 字段:恢复尝试在 open() 前失败时,仅继承稳定请求 身份(conversationId / model / processId),不再叠加上一个成功 attempt 的 streamAttempt / region / streamStartedAt / eventTypeCounts / emitted*。 - coordinator_failure 优先级:clean EOF 之后 onComplete / 终止 chunk 发布抛错时, 协调器以 coordinator_failure 结束并 controller.error(),此前终态仍报告 clean EOF 与 phase: 'completed',把已失败的客户端流记成成功。现在与 attempt_budget_exhausted 一样覆盖 clean EOF。 - 新增 stream_processing_failure:区分插件内部 transformer / SSE 序列化 / 协调器 故障与上游 SDK iterator 失败。此前 null → iterator_failure 的无条件回退会把自身 故障误报为上游断流。该值已加入 REQUEST_TERMINAL_SOURCES,由 tsc 强制。
- 有界合并:healthWriteChains 的 FIFO 改为每账号 HealthWriteState(current 不可替换 + 至多一个可替换的 pending)。原实现每次 mutation 追加一个完整快照 与 Promise,既无合并也无上限;当 SQLite 被其他进程持有、同步 mutator 持续 触发时,队列可无界增长,每项还各自可等待 WRITE_LOCK_DEADLINE_MS(30 秒)。 健康字段是 last-writer-wins 状态而非事件流,被取代的中间快照没有持久化价值, 因此每账号最多保留两个快照即足够。 - macrotask 时机:yield 从入队时移到 drainHealthWrites() 中每次 upsertAccount() 之前。原实现所有 timer 在入队时创建,积压解除时早已 resolve,队列会在同一 microtask 链里连续执行事务,重新引入本应消除的事件循环停顿。 - 读回守卫窗口:pendingHealthWriteCounts 在 current + pending 时为 2、pending 晋升后为 1、全部完成才删除,因此 applyHealthSnapshot 在整个在途与待写窗口都 不会覆盖本地状态。 - 测试可信性:两个 pending-write 测试原先只 resolve 并 drain microtask,从不等待 新的 timer,因此在「持久化根本没启动」时也会通过;spy 恢复后迟到的 timer 还会 解析到真实 kiroDb.upsertAccount,而 afterEach 可能已删除测试 id,导致向进程 共享的 kiro.db 回插测试账号。现改为先等 macrotask 并断言 spy 已调用,再 resolve,待整条链结算后才恢复 spy。 - 补齐 item-6 覆盖:新增 live-recovery transformer 故障经 observer 记录 stream_processing_failure 的测试。此前只有 mode-off 端到端断言,单独回退 response-handler.ts 的分类仍会因 request-handler.ts 的兜底而通过。
- 顶层菜单:`opencode auth login` 选择 `kiro-auth` 后新增 `Refresh all accounts · tokens + usage`,输出每个账号刷新前后的用量、 token 状态与错误;保持 `type: 'oauth'`,因为 `type: 'api'` 会被 OpenCode 强制弹出 API key 输入,破坏无凭据流程 - 单账号动作:`Manage / remove accounts` 更名为 `Manage accounts`,选中账号后 新增二级菜单 `Refresh token & usage` / `Delete account` / `Cancel`,刷新后用 `formatAccountOption` 重新渲染该行直接显示新额度;原菜单只能删除,用户看到的 一直是缓存里的旧用量 - 切换前刷新:`AccountSelector.selectAlternativeAccount` 与 `ErrorHandler` 的 429、402/403 多账号切换分支,在选号前执行 cooldown 门控的全量刷新,使切换 基于真实额度而非陈旧值;刷新失败只记日志,绝不抛入请求路径 - 三重并发守卫:进程内 single-flight 按 `force` 兼容性分槽(强制刷新可满足自动 调用,自动刷新不可满足强制调用,故手动刷新不会被自动刷新的 `cooldown` / `lock_unavailable` 结果顶替);`refresh_all_cooldown_ms` 冷却,手动 `force: true` 绕过;跨进程复用 `tryAcquireKeepAliveLock` 尽力协调,自动路径 拿不到锁即跳过以免给实时请求加延迟,手动路径无锁继续(每账号 token 刷新仍由 `TokenRefresher` 的 refresh lock 保证安全),锁一律在 `finally` 释放 - deadline:自动路径 `refresh_all_deadline_ms`(默认 5000ms),手动路径为其 6 倍(默认 30000ms),确保无响应的 usage 端点不会冻结 TTY;超时如实报告 `timedOut` - token 语义:仅在 `accessTokenExpired` 时经 `refreshIfNeeded` 续期,不使用 `forceRefresh` —— 轮换后的 refresh token 是一次性的,强制刷新仍有效的 token 等于白消耗;用量则每次都重新拉取,因为陈旧用量才是本次要解决的问题;摘要如实 区分 `token renewed` 与 `not_needed` - 写路径:仅经 `updateAccountQuota(account, usage, accountManager)` → `updateUsage` → `persistLocalHealthMutation`,复用已按账号合并、macrotask 延迟、带 pending 守卫的写入路径,未新增 `batchSave`,未在同步选号栈内写库; 用量拉取并发上限 4 - 配置:新增 `refresh_all_cooldown_ms`(默认 60000)、 `refresh_all_deadline_ms`(默认 5000)、`refresh_before_switch_enabled` (默认 true),已同步 zod schema 与默认配置对象 - `fetchUsageLimits` 新增可选 `AbortSignal` 参数以支持 deadline 中断
Merged
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.
Summary
本 PR 集中三条主线,共 9 个提交,全部只改动
src/。1. 恢复路径账号轮换与账号健康管理修复
kiro.db,选号前刷新健康字段,避免拿着本进程内存里的陈旧健康状态做决策。2. 流请求终态可观测性
Kiro stream request terminal摘要日志,与既有的Kiro stream request started构成完整的分子/分母,可以直接算失败率。terminalSource九态终态归因、eventTypeCounts上游事件类型分布、以及 dialect marker 诊断字段。3. 工具标记正确性修复与账号刷新菜单
<invoke>不再被当作真实工具调用解析。What was tested
bun test:1240 pass / 0 fail,3728 次expect(),93 个测试文件。bun run typecheck:exit 0。bun run build:exit 0(tsc -p tsconfig.build.json+fix-esm-importspatched 50/156)。Notable behavior change
stream_recovery_mode: 'off'下的恢复机制行为不变 —— 仍然不额外发起 SDK 请求、不抑制 dialect remainder 或工具调用、不做截断判定。但未闭合围栏内的完整
<invoke>不再被解析为工具调用这一条对所有 mode 生效,包括off。这是有意的正确性修复,不是恢复特性:模型在代码块里演示工具调用语法时不应该真的触发调用。Blocked / not included
运行环境实际加载的插件版本仍需发版后更新
opencode.json才会生效,本 PR 不包含该变更。