@armadra/agent 0.7.0 → 0.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/CHANGELOG.zh-CN.md +41 -0
  3. package/dist/agent/session-cache.d.ts +4 -2
  4. package/dist/agent/session-cache.js +5 -3
  5. package/dist/agent/session-compaction.d.ts +13 -1
  6. package/dist/agent/session-compaction.js +36 -0
  7. package/dist/agent/session-core.d.ts +2 -0
  8. package/dist/agent/session-state.d.ts +9 -3
  9. package/dist/agent/session-state.js +8 -6
  10. package/dist/agent/session.js +3 -3
  11. package/dist/agent/types-w5.d.ts +3 -0
  12. package/dist/agent/types.d.ts +18 -0
  13. package/dist/agents/task-record.d.ts +4 -0
  14. package/dist/agents/task-record.js +18 -0
  15. package/dist/ai/context.d.ts +7 -2
  16. package/dist/ai/context.js +45 -7
  17. package/dist/ai/providers/catalog-data.js +1 -1
  18. package/dist/auth/oauth/token-store.d.ts +2 -0
  19. package/dist/auth/oauth/token-store.js +7 -2
  20. package/dist/bundle/ama.cjs +1684 -490
  21. package/dist/checkpoints/shadow-git.js +2 -1
  22. package/dist/compaction/breakdown.d.ts +51 -0
  23. package/dist/compaction/breakdown.js +159 -0
  24. package/dist/compaction/estimate.d.ts +6 -0
  25. package/dist/compaction/estimate.js +1 -1
  26. package/dist/compaction/prefix-estimate.d.ts +23 -0
  27. package/dist/compaction/prefix-estimate.js +34 -0
  28. package/dist/config/auth-file.js +10 -2
  29. package/dist/config/fs-retry.d.ts +18 -0
  30. package/dist/config/fs-retry.js +33 -0
  31. package/dist/config/json-schema.js +1 -0
  32. package/dist/config/key-docs.js +1 -0
  33. package/dist/config/schema.js +2 -0
  34. package/dist/config/settings-registry.js +1 -0
  35. package/dist/config/types.d.ts +2 -0
  36. package/dist/drivers/native/codex-app-server.js +6 -0
  37. package/dist/drivers/runner.js +17 -0
  38. package/dist/drivers/store.d.ts +3 -0
  39. package/dist/drivers/store.js +4 -0
  40. package/dist/i18n/catalog.d.ts +53 -0
  41. package/dist/i18n/messages/agents.d.ts +3 -0
  42. package/dist/i18n/messages/agents.js +3 -0
  43. package/dist/i18n/messages/config-keys.d.ts +2 -0
  44. package/dist/i18n/messages/config-keys.js +2 -0
  45. package/dist/i18n/messages/config.d.ts +2 -0
  46. package/dist/i18n/messages/interactive.d.ts +6 -0
  47. package/dist/i18n/messages/interactive.js +6 -0
  48. package/dist/i18n/messages/panels.d.ts +3 -0
  49. package/dist/i18n/messages/panels.js +5 -2
  50. package/dist/i18n/messages/report-context.d.ts +94 -0
  51. package/dist/i18n/messages/report-context.js +95 -0
  52. package/dist/i18n/messages/report.d.ts +96 -0
  53. package/dist/i18n/messages/report.js +13 -2
  54. package/dist/i18n/messages/settings.d.ts +2 -0
  55. package/dist/i18n/messages/settings.js +2 -0
  56. package/dist/i18n/messages/subcommands.js +2 -2
  57. package/dist/modes/acp/acp-events.d.ts +17 -4
  58. package/dist/modes/acp/acp-events.js +32 -6
  59. package/dist/modes/commands-core.js +4 -0
  60. package/dist/modes/context-report.d.ts +35 -0
  61. package/dist/modes/context-report.js +172 -0
  62. package/dist/modes/interactive/agent-bar.d.ts +2 -0
  63. package/dist/modes/interactive/agent-bar.js +6 -0
  64. package/dist/modes/interactive/commands.d.ts +2 -1
  65. package/dist/modes/interactive/commands.js +10 -1
  66. package/dist/modes/interactive/interactive-mode.d.ts +3 -0
  67. package/dist/modes/interactive/interactive-mode.js +6 -1
  68. package/dist/modes/interactive/panels.d.ts +2 -1
  69. package/dist/modes/interactive/panels.js +13 -5
  70. package/dist/modes/interactive/program-status-ui.d.ts +72 -0
  71. package/dist/modes/interactive/program-status-ui.js +229 -0
  72. package/dist/modes/interactive/session-events.d.ts +4 -0
  73. package/dist/modes/interactive/session-events.js +24 -2
  74. package/dist/modes/interactive/status-bar.d.ts +21 -3
  75. package/dist/modes/interactive/status-bar.js +64 -25
  76. package/dist/modes/interactive/status-ctx.d.ts +55 -0
  77. package/dist/modes/interactive/status-ctx.js +125 -0
  78. package/dist/modes/interactive/status-line.d.ts +2 -1
  79. package/dist/modes/interactive/status-line.js +3 -2
  80. package/dist/modes/interactive/tasks-report.js +4 -0
  81. package/dist/modes/print/print-mode.js +12 -0
  82. package/dist/modes/session-report.d.ts +10 -2
  83. package/dist/modes/session-report.js +26 -2
  84. package/dist/sdk.d.ts +1 -1
  85. package/dist/tools/types.d.ts +6 -0
  86. package/dist/tui/components/meter.d.ts +2 -0
  87. package/dist/tui/components/meter.js +1 -1
  88. package/dist/tui/program-status.d.ts +90 -0
  89. package/dist/tui/program-status.js +259 -0
  90. package/docs/acp.md +4 -4
  91. package/docs/agents.md +1 -1
  92. package/docs/en/acp.md +4 -4
  93. package/docs/en/providers.md +5 -3
  94. package/docs/en/rpc.md +3 -0
  95. package/docs/en/sessions.md +24 -1
  96. package/docs/en/tui.md +79 -22
  97. package/docs/providers.md +8 -5
  98. package/docs/rpc.md +1 -0
  99. package/docs/session-format.md +3 -3
  100. package/docs/sessions.md +23 -1
  101. package/docs/tui-design.md +2 -2
  102. package/docs/tui.md +79 -22
  103. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -5,6 +5,71 @@ English · [简体中文](CHANGELOG.zh-CN.md)
5
5
  > This file is in English starting with 0.6.0. Release notes for 0.1 through 0.5.1 are in Chinese in
6
6
  > [CHANGELOG.zh-CN.md](CHANGELOG.zh-CN.md). New entries go into both files.
7
7
 
8
+ ## 0.7.2 (2026-10-09)
9
+
10
+ - **Prompt cache survives mid-session context changes**: when AGENTS.md, Skills or SessionStart hook output changed on
11
+ resume, host instructions were refreshed or the memory section was re-rendered after compaction, the changed section
12
+ used to be folded back into the leading system prompt on endpoints without mid-conversation system messages, so the
13
+ whole context was billed at full price from token 0. It is now delivered as a `<system-reminder>` user message at the
14
+ end, and the previous request stays a byte-for-byte prefix; only a patch that removes tools still rewrites the head.
15
+ DeepSeek accepts a mid-conversation system message but measurably keeps answering from the leading one, so it stays off
16
+ there (docs/benchmarks/cache-midconvo-2026-10-09.md).
17
+ - **No early pruning on a guessed cache lifetime**: tier-one pruning no longer treats the cache as cold after an implicit
18
+ 10 minutes when the catalog promises no TTL (implicit caches may live for hours); the 10 minutes remain for miss
19
+ attribution only.
20
+ - **DeepSeek catalog cache data**: `promptCache: { short: 3600, minTokens: 2048 }` (the vendor says unused cache is
21
+ cleared after a few hours to a few days; reads were measured in 2048-token blocks).
22
+ - **`ama models cache-probe` advice**: without a catalog lifetime it no longer suggests filling in `short: 300`; it
23
+ explains that ama then neither warms nor prunes early and to fill in only a documented lifetime.
24
+ - **Terminal program status (OSC 7501)**: the interactive interface reports idle / working / blocked (permission,
25
+ question, auth) / done / error to the terminal with the
26
+ [Program Status Protocol](https://www.superlogical.com/rex/docs/build/program-status), sub-agent tasks as child records
27
+ `task/<id>`, and clears its records on exit. `ui.programStatus`: `auto` (default, only after the terminal answers the
28
+ detection query; off inside tmux), `on` (no detection; tmux passthrough), `off`. Detection replies never reach the input
29
+ box. Print / RPC / ACP modes send nothing.
30
+ - **ACP context usage right away**: `ama --mode acp` now sends `usage_update` after the `session/new`, `session/load`
31
+ (after the replay) and `session/resume` responses, after a model change and at the end of every assistant message (so
32
+ multi-tool turns update too), instead of only at the end of a turn; unchanged values are not resent. With an unknown
33
+ context window there is still no `usage_update` (`size` is required by the schema).
34
+ - **`-p --output-format json` context**: the result object carries `context: { tokens, window, percent }`; unknown items
35
+ are omitted.
36
+ - **External agents' context**: when an external agent reports its context usage and window (ACP `usage_update`, Codex
37
+ app-server `tokenUsage`), the numbers go into the task record and `ama.agent-usage`, `getStats().external.byAgent`
38
+ keeps the latest, and `/tasks` and the Agent bar show `ctx 34%`.
39
+ - **`/context`**: a new command that breaks the context down by category — system prompt (per section), tool
40
+ declarations (per tool), user messages, assistant text, reasoning, tool-call arguments, tool results (per tool name),
41
+ images, summaries and custom messages — with estimated tokens, share and a bar each. The top shows used / window, tokens
42
+ left, the source ("reported usage X + estimated Y" or "estimated in full") and the auto-compact trigger; the bottom
43
+ lists the largest tool results by ordinal, tool name and size only, never their content. Panel in the TUI, plain text
44
+ in line mode. Read-only; the request body and cache prefix are unchanged.
45
+ - **Context usage from the start**: before the first request, `contextTokens` is now a baseline estimated from the system
46
+ prompt and tool declarations that are about to be sent (stats only; nothing is written to the session and the request
47
+ body is unchanged), so a new session no longer shows `Ctx 0.0%`. `SessionStats.context` (`getStats()`, RPC
48
+ `get_session_stats`) tells where the number comes from (`usage` / `estimate` / `prefix`) and carries the
49
+ auto-compaction thresholds `autoCompactAt` / `pruneAt`; absent fields mean "unknown" (docs/sessions.md).
50
+ - **Status bar**: `full` shows `Ctx 3.0% 8.2k/272k auto` (used / window, `auto` while auto-compaction is on; narrow
51
+ screens drop `auto`, then `/window`, then the used tokens); estimates get a leading `≈`; the warning color starts at
52
+ the tier-one pruning threshold; `compact` keeps one decimal below 1%. Ctx refreshes right after a message is submitted
53
+ and, while streaming, follows usage reported by the reply (at most twice a second). The rate line marks billed tokens
54
+ as the session total `Σ↑ ↓` with cache reads / writes as `R` / `W`. A model without a context window gets a one-time
55
+ hint on how to set one.
56
+ - **`/session`**: the usage row is labeled as the session total; the context row adds the distance to auto-compaction
57
+ (or "auto-compaction off") and `≈` for estimates.
58
+ - **Contributing**: the repository now has issue forms (bug report, feature request, provider / model compatibility,
59
+ documentation), a pull request template, `CONTRIBUTING.md`, `SECURITY.md` and an `AGENTS.md` with the hard constraints
60
+ and review rules. None of these ship in the npm package.
61
+ - **Docs**: design notes, plans and research reports no longer name the third-party projects they were compared with;
62
+ those are described generically or by code names. Agents, providers and protocols that ama actually drives or supports
63
+ keep their names.
64
+
65
+ ## 0.7.1 (2026-10-05)
66
+
67
+ - **Windows: concurrent OAuth refresh**: while one ama process releases the `auth.json.lock`, another one opening it
68
+ got EPERM (the file is "delete pending" on NTFS) and failed the refresh; it now keeps waiting for the lock. Replacing
69
+ or reading `auth.json` while another process has it open retries briefly on EPERM / EACCES / EBUSY (Windows only).
70
+ - **Checkpoints (shadow-git)**: the 3-second snapshot budget no longer includes creating the shadow repository the first
71
+ time (a dozen `git` processes, several seconds on Windows), which could downgrade a session to `tools` on its first turn.
72
+
8
73
  ## 0.7.0 (2026-10-04)
9
74
 
10
75
  ACP completion: `ama --mode acp` as an agent for editors (Zed and other ACP clients) and `AcpClient` / `AcpDriver` as a
@@ -4,6 +4,47 @@
4
4
 
5
5
  > 从 0.6.0 起 [CHANGELOG.md](CHANGELOG.md) 为英文,本文件保留中文记录(0.1–0.5.1 的完整历史在此)。新条目两份都要加。
6
6
 
7
+ ## 0.7.2(2026-10-09)
8
+
9
+ - **会话中途上下文变化不再打断提示缓存**:resume 时 AGENTS.md、Skills 或 SessionStart Hook 输出变了、宿主 instructions 刷新、压缩后记忆节
10
+ 重新渲染,在不支持对话中途 system 消息的端点上原先会把变化的节并回开头的 system,整段上下文从第 0 个 token 起按全价重读。现在改为在
11
+ 尾部追加一条 `<system-reminder>` 包裹的 user 消息,上一次请求仍是逐字节前缀;只有移除工具的补丁仍改写开头。DeepSeek 接受对话中途的
12
+ system 消息,但实测模型仍按开头那条回答,所以不为它打开(docs/benchmarks/cache-midconvo-2026-10-09.md)。
13
+ - **不按猜测的缓存寿命提前裁剪**:目录没有承诺 TTL 时,档一裁剪不再按隐式 10 分钟判冷(隐式缓存可能存活数小时);10 分钟只用于未命中归因。
14
+ - **DeepSeek 目录补缓存信息**:`promptCache: { short: 3600, minTokens: 2048 }`(官方说未使用的缓存「几小时到几天」后清除;缓存读实测按
15
+ 2048 token 一块计)。
16
+ - **`ama models cache-probe` 建议**:目录没有寿命时不再建议自填 `short: 300`,改为说明此时 ama 不保温、不提前裁剪,只有上游文档写明寿命时才填。
17
+ - **终端程序状态(OSC 7501)**:交互界面用 [程序状态协议](https://www.superlogical.com/rex/docs/build/program-status) 把
18
+ idle / working / blocked(permission、question、auth)/ done / error 报告给终端,子 Agent 任务为子记录 `task/<id>`,退出时清掉记录。
19
+ `ui.programStatus`:`auto`(缺省,终端回应检测查询后才发;tmux 里不发)、`on`(不检测;tmux 里经 passthrough)、`off`。检测回复不会
20
+ 进输入框。print / RPC / ACP 模式不发。
21
+ - **ACP 立即有上下文用量**:`ama --mode acp` 在 `session/new`、`session/load`(回放之后)、`session/resume` 答复之后、换模型时与每条助手
22
+ 消息结束时都发 `usage_update`(多工具回合中途也更新),不再只在回合结束时发;与上次同值不重发。窗口未知时仍不发(schema 里 `size` 必填)。
23
+ - **`-p --output-format json` 带上下文**:结果对象加 `context: { tokens, window, percent }`,未知项省略。
24
+ - **外部 Agent 的上下文**:外部 Agent 报告了上下文占用与窗口(ACP `usage_update`、Codex app-server `tokenUsage`)时,数字记进任务记录与
25
+ `ama.agent-usage`,`getStats().external.byAgent` 取最近一次,`/tasks` 与 Agent 栏显示 `ctx 34%`。
26
+ - **`/context`**:新命令,把上下文按类别拆开——系统提示(按节)、工具声明(按工具)、用户消息、助手文本、推理、工具调用参数、
27
+ 工具结果(按工具名汇总)、附件图片、摘要、自定义消息——每类给 token 估算、占比与条形;顶部是已用 / 窗口、剩余、来源(「usage
28
+ 实测 X + 估算 Y」或「全量估算」)与自动压缩触发点,底部只按序号、工具名与大小列出最大的工具结果,不显示正文。交互模式是面板,
29
+ line 模式是纯文本。统计只读,不改请求体与缓存前缀。
30
+ - **上下文一开始就有数**:第一次请求前,`contextTokens` 改为按即将发送的系统提示 + 工具声明估算的基线(只统计,不写会话、不改请求体),
31
+ 新会话不再显示 `Ctx 0.0%`。`SessionStats.context`(`getStats()`、RPC `get_session_stats`)标明数字来源(`usage` / `estimate` /
32
+ `prefix`)并给出自动压缩阈值 `autoCompactAt` / `pruneAt`;字段缺省表示「不知道」(docs/sessions.md)。
33
+ - **状态栏**:`full` 显示 `Ctx 3.0% 8.2k/272k auto`(已用量 / 窗口,自动压缩开着时有 `auto`;窄屏先丢 `auto`,再丢 `/窗口`,最后丢已用量);
34
+ 估算值前加 `≈`;黄色从档一裁剪阈值开始;`compact` 占用 < 1% 时保留一位小数。提交消息后立刻刷新 Ctx,流式中按回复带回的 usage 更新
35
+ (每秒至多 2 次)。速率行用 `Σ↑ ↓` 标明是会话累计计费量,缓存读写另列 `R` / `W`。模型没有上下文窗口时提示一次怎么补。
36
+ - **`/session`**:用量行注明「累计」;上下文行加上距自动压缩的余量(或「自动压缩关闭」),估算值带 `≈`。
37
+ - **参与贡献**:仓库新增 issue 表单(缺陷报告、功能建议、供应商 / 模型兼容问题、文档问题)、PR 模板、`CONTRIBUTING.md`、
38
+ `SECURITY.md`,以及写明硬约束与评审规则的 `AGENTS.md`;这些文件都不进 npm 包。
39
+ - **文档**:设计、计划与调研文档不再点名作为参照的第三方项目,改为不具名表述或代号;ama 实际驱动或兼容的外部 Agent、供应商与协议照常写名字。
40
+
41
+ ## 0.7.1(2026-10-05)
42
+
43
+ - **Windows:并发刷新 OAuth**:一个 ama 进程刚释放 `auth.json.lock` 时,另一个进程打开它会报 EPERM(NTFS 上文件处于删除挂起),刷新
44
+ 直接失败;现在当作锁被占用继续等。别的进程正打开 `auth.json` 时的覆盖 / 读取遇到 EPERM / EACCES / EBUSY 短暂重试(只在 Windows)。
45
+ - **检查点(影子 git)**:3 秒的快照耗时上限不再算上第一次建影子仓库(十来个 `git` 子进程,Windows 上要好几秒),以前可能第一回合就把
46
+ 会话降级为 `tools`。
47
+
7
48
  ## 0.7.0(2026-10-04)
8
49
 
9
50
  ACP 补全:`ama --mode acp` 作为编辑器(Zed 等 ACP 客户端)的 Agent,`AcpClient` / `AcpDriver` 作为客户端,仓库内对照官方 ACP v1
@@ -82,8 +82,10 @@ export declare class SessionCacheController {
82
82
  window?: number | undefined;
83
83
  }): SessionCacheStats;
84
84
  /**
85
- * [W5-H1] 缓存是否已冷(wave5 §8.2 C3):上一次请求(含保温)距今超过 TTL(目录无承诺时按隐式
86
- * 10 分钟)。只对 `reported` 端点判断;`silent` / `unknown` 返回 false,档一退化为只按阈值。
85
+ * [W5-H1] 缓存是否已冷(wave5 §8.2 C3):上一次请求(含保温)距今超过目录承诺的 TTL。
86
+ * 目录没有承诺 TTL 时返回 false:隐式缓存的寿命可能远长于 10 分钟,按猜测的寿命提前裁剪会改写
87
+ * 仍然有效的缓存(隐式 600 s 只用于未命中归因)。只对 `reported` 端点判断;`silent` / `unknown`
88
+ * 返回 false,档一退化为只按阈值。
87
89
  */
88
90
  isCold(): boolean;
89
91
  /**
@@ -299,8 +299,10 @@ export class SessionCacheController {
299
299
  return stats;
300
300
  }
301
301
  /**
302
- * [W5-H1] 缓存是否已冷(wave5 §8.2 C3):上一次请求(含保温)距今超过 TTL(目录无承诺时按隐式
303
- * 10 分钟)。只对 `reported` 端点判断;`silent` / `unknown` 返回 false,档一退化为只按阈值。
302
+ * [W5-H1] 缓存是否已冷(wave5 §8.2 C3):上一次请求(含保温)距今超过目录承诺的 TTL。
303
+ * 目录没有承诺 TTL 时返回 false:隐式缓存的寿命可能远长于 10 分钟,按猜测的寿命提前裁剪会改写
304
+ * 仍然有效的缓存(隐式 600 s 只用于未命中归因)。只对 `reported` 端点判断;`silent` / `unknown`
305
+ * 返回 false,档一退化为只按阈值。
304
306
  */
305
307
  isCold() {
306
308
  const prev = this.prev;
@@ -311,7 +313,7 @@ export class SessionCacheController {
311
313
  if (this.tracker.get(endpoint, model.compat?.cacheReporting) !== "reported")
312
314
  return false;
313
315
  const ttl = cacheTtlMs(this.modelOf(prev), prev.options.cacheRetention);
314
- return this.now() - prev.at > (ttl ?? IMPLICIT_CACHE_TTL_MS);
316
+ return ttl !== undefined && this.now() - prev.at > ttl;
315
317
  }
316
318
  /**
317
319
  * 摘要续写的前缀(第三波 §1.8):当前转录按回合同样的方式转换,且上一次 turn 请求的消息
@@ -14,6 +14,8 @@
14
14
  * `overflow`,受跳闸限制)→ 压缩后估算 ≤ 0.8 × 窗口才重试。
15
15
  * - 手动 `/compact`:PreCompact(trigger `manual`)→ 摘要;成功清零熔断。
16
16
  * PreCompact 的 `decision: "block"` 取消本次压缩;`customInstructions` 追加到摘要提示。
17
+ * - 统计(`contextStats`,供 getStats):投影估算标来源;系统消息落盘前加系统提示 + 工具声明的前缀基线
18
+ * (只统计,不写分支、不改请求);附档一 / 档二阈值。压缩调度本身仍只看 `estimate()`。
17
19
  * [W3-C1b] 阈值 / 手动压缩与分支摘要优先走会话前缀续写(缓存控制器给前缀),溢出恢复不走
18
20
  * (前缀本身已超窗口);续写失败回落独立请求并记 warning。
19
21
  */
@@ -22,7 +24,7 @@ import { type ContextEstimate } from "../compaction/estimate.js";
22
24
  import { type PrunePolicy } from "../compaction/prune-tier.js";
23
25
  import type { BranchSummaryEntry } from "../session/types.js";
24
26
  import type { SessionCore } from "./session-core.js";
25
- import type { CompactionResult, CompactionSettings } from "./types.js";
27
+ import type { CompactionResult, CompactionSettings, SessionContextStats } from "./types.js";
26
28
  export declare const DEFAULT_COMPACTION_SETTINGS: CompactionSettings;
27
29
  /** 保留区不超过 (窗口 − 预留) 的 40%,否则小窗口模型上摘要后仍然溢出。 */
28
30
  export declare const KEEP_RECENT_MAX_RATIO = 0.4;
@@ -40,6 +42,16 @@ export declare class CompactionController {
40
42
  refresh(): void;
41
43
  setAuto(enabled: boolean): void;
42
44
  estimate(): ContextEstimate;
45
+ /**
46
+ * 统计用的上下文量(`getStats`):投影估算标上来源;系统消息还没落盘时加上前缀基线(系统提示 +
47
+ * 工具声明,只统计不写分支)。附自动压缩的两个阈值(自动压缩可用时)。压缩调度仍用 `estimate()`。
48
+ */
49
+ contextStats(): {
50
+ tokens: number;
51
+ detail: SessionContextStats;
52
+ };
53
+ /** 将要发送的系统提示 + 工具声明的估算(与首条 system 消息同样装配,不落盘)。 */
54
+ private prefixTokens;
43
55
  private budget;
44
56
  private keepRecent;
45
57
  /** 摘要请求的公共选项;`continuation` 时附上会话前缀续写的材料。 */
@@ -14,18 +14,22 @@
14
14
  * `overflow`,受跳闸限制)→ 压缩后估算 ≤ 0.8 × 窗口才重试。
15
15
  * - 手动 `/compact`:PreCompact(trigger `manual`)→ 摘要;成功清零熔断。
16
16
  * PreCompact 的 `decision: "block"` 取消本次压缩;`customInstructions` 追加到摘要提示。
17
+ * - 统计(`contextStats`,供 getStats):投影估算标来源;系统消息落盘前加系统提示 + 工具声明的前缀基线
18
+ * (只统计,不写分支、不改请求);附档一 / 档二阈值。压缩调度本身仍只看 `estimate()`。
17
19
  * [W3-C1b] 阈值 / 手动压缩与分支摘要优先走会话前缀续写(缓存控制器给前缀),溢出恢复不走
18
20
  * (前缀本身已超窗口);续写失败回落独立请求并记 warning。
19
21
  */
20
22
  import { AmaError } from "../errors.js";
21
23
  import { CompactionBreaker } from "../compaction/breaker.js";
22
24
  import { estimateMessageTokens, estimateProjectedTokens, } from "../compaction/estimate.js";
25
+ import { estimatePrefixTokens, withPrefixBaseline } from "../compaction/prefix-estimate.js";
23
26
  import { planPrune, prunePolicy } from "../compaction/prune-tier.js";
24
27
  import { createProtection, skillLocations } from "../compaction/protect.js";
25
28
  import { buildPostCompactBlock } from "../compaction/post-compact.js";
26
29
  import { prepareCompaction, prepareCompactionAt, runCompaction, } from "../compaction/summarize-tier.js";
27
30
  import { prepareBranchSummary, runBranchSummary } from "../compaction/branch-summary.js";
28
31
  import { buildProjection } from "../session/projection.js";
32
+ import { assembleSections, definedSections, toolDecls } from "./system-prompt.js";
29
33
  export const DEFAULT_COMPACTION_SETTINGS = {
30
34
  enabled: true,
31
35
  reserveTokens: 16_384,
@@ -69,6 +73,38 @@ export class CompactionController {
69
73
  const branch = this.core.manager.branch();
70
74
  return estimateProjectedTokens(buildProjection(branch).items, branch);
71
75
  }
76
+ /**
77
+ * 统计用的上下文量(`getStats`):投影估算标上来源;系统消息还没落盘时加上前缀基线(系统提示 +
78
+ * 工具声明,只统计不写分支)。附自动压缩的两个阈值(自动压缩可用时)。压缩调度仍用 `estimate()`。
79
+ */
80
+ contextStats() {
81
+ const branch = this.core.manager.branch();
82
+ const items = buildProjection(branch).items;
83
+ const sourced = withPrefixBaseline(estimateProjectedTokens(items, branch), items.some((item) => item.message.role === "system"), () => this.prefixTokens());
84
+ const detail = {
85
+ source: sourced.source,
86
+ usageTokens: sourced.usageTokens,
87
+ trailingTokens: sourced.trailingTokens,
88
+ };
89
+ const budget = this.budget();
90
+ const policy = this.prunePolicy();
91
+ if (this.breaker.autoEnabled && budget !== undefined && policy !== undefined) {
92
+ detail.autoCompactAt = budget;
93
+ detail.pruneAt = Math.floor(policy.triggerTokens);
94
+ }
95
+ return { tokens: sourced.tokens, detail };
96
+ }
97
+ /** 将要发送的系统提示 + 工具声明的估算(与首条 system 消息同样装配,不落盘)。 */
98
+ prefixTokens() {
99
+ const core = this.core;
100
+ const tools = core
101
+ .activeToolNames()
102
+ .map((name) => core.activeTool(name))
103
+ .filter((tool) => tool !== undefined);
104
+ const system = core.childBase?.().system ?? core.options.system ?? {};
105
+ const sections = definedSections(assembleSections({ ...system, tools, cwd: core.cwd }));
106
+ return estimatePrefixTokens(sections, toolDecls(tools));
107
+ }
72
108
  budget() {
73
109
  const window = this.core.model().contextWindow;
74
110
  return window === undefined ? undefined : Math.max(0, window - this.settings.reserveTokens);
@@ -130,6 +130,8 @@ export interface SessionCore {
130
130
  /** 任一已登记工具(不限于活动集;嵌套调用用)。 */
131
131
  tool(name: string): ToolDefinition | undefined;
132
132
  activeToolNames(): string[];
133
+ /** 当前的系统提示静态部分(`updateSystem` 之后的;统计启动基线用)。缺省时用 `options.system`。 */
134
+ childBase?(): Pick<AgentSessionOptions, "system">;
133
135
  runHook(event: HookEvent, payload: HookEventPayload, signal?: AbortSignal): Promise<HookOutcome | undefined>;
134
136
  /** Hook 返回 continue:false 时记下,finishTurn 据此结束 run。 */
135
137
  requestStop(reason: string | undefined): void;
@@ -3,14 +3,15 @@
3
3
  *
4
4
  * 统计口径:活动分支上的全部条目(不是投影)——被压缩 / 剔除的消息仍计入用量与成本;
5
5
  * 压缩与分支摘要请求的 usage 也计入;`usage` 条目(缓存保温等不进上下文的请求,第三波 §1.7)
6
- * 计入 token 与费用但不算消息。上下文 % 用投影感知估算(§9)。
6
+ * 计入 token 与费用但不算消息。上下文 % 用投影感知估算(§9);`context` 标明估算来源(usage / 全量估算 /
7
+ * 启动前缀基线)与自动压缩阈值。
7
8
  */
8
9
  import type { Model, ModelThinkingLevel } from "../ai/types.js";
9
10
  import type { PermissionMode } from "../permissions/types.js";
10
11
  import type { SessionManager } from "../session/manager.js";
11
12
  import type { SessionEntry } from "../session/types.js";
12
13
  import type { Agent } from "./agent.js";
13
- import type { SessionCacheStats, SessionState, SessionStats } from "./types.js";
14
+ import type { SessionCacheStats, SessionContextStats, SessionState, SessionStats } from "./types.js";
14
15
  import type { QuotaUpdateEvent } from "./types-w6.js";
15
16
  export interface StateInput {
16
17
  agent: Agent;
@@ -28,7 +29,12 @@ export interface StatsInput {
28
29
  sessionId: string;
29
30
  sessionFile: string | undefined;
30
31
  branch: readonly SessionEntry[];
31
- contextTokens: number | undefined;
32
+ contextTokens?: number | undefined;
33
+ /** 会话的上下文估算(CompactionController.contextStats,含来源与阈值);给了以它的 tokens 为准。 */
34
+ context?: {
35
+ tokens: number;
36
+ detail: SessionContextStats;
37
+ };
32
38
  contextWindow: number | undefined;
33
39
  /** [W3-C1b] 会话层缓存控制器的统计(未接线时缺省)。 */
34
40
  cache?: SessionCacheStats;
@@ -3,7 +3,8 @@
3
3
  *
4
4
  * 统计口径:活动分支上的全部条目(不是投影)——被压缩 / 剔除的消息仍计入用量与成本;
5
5
  * 压缩与分支摘要请求的 usage 也计入;`usage` 条目(缓存保温等不进上下文的请求,第三波 §1.7)
6
- * 计入 token 与费用但不算消息。上下文 % 用投影感知估算(§9)。
6
+ * 计入 token 与费用但不算消息。上下文 % 用投影感知估算(§9);`context` 标明估算来源(usage / 全量估算 /
7
+ * 启动前缀基线)与自动压缩阈值。
7
8
  */
8
9
  import { modelRefOf } from "../ai/providers/channels.js";
9
10
  export function buildSessionState(input) {
@@ -87,11 +88,10 @@ export function computeStats(input) {
87
88
  addCost(entry.usage);
88
89
  }
89
90
  }
90
- const contextPercent = input.contextWindow === undefined ||
91
- input.contextTokens === undefined ||
92
- input.contextWindow <= 0
91
+ const contextTokens = input.context?.tokens ?? input.contextTokens;
92
+ const contextPercent = input.contextWindow === undefined || contextTokens === undefined || input.contextWindow <= 0
93
93
  ? undefined
94
- : Math.min(100, Math.round((input.contextTokens / input.contextWindow) * 1000) / 10);
94
+ : Math.min(100, Math.round((contextTokens / input.contextWindow) * 1000) / 10);
95
95
  const stats = {
96
96
  sessionId: input.sessionId,
97
97
  sessionFile: input.sessionFile,
@@ -101,10 +101,12 @@ export function computeStats(input) {
101
101
  toolResults,
102
102
  tokens,
103
103
  cost,
104
- contextTokens: input.contextTokens,
104
+ contextTokens,
105
105
  contextWindow: input.contextWindow,
106
106
  contextPercent,
107
107
  };
108
+ if (input.context !== undefined)
109
+ stats.context = { ...input.context.detail };
108
110
  const rate = cacheHitRate(tokens);
109
111
  if (rate !== undefined)
110
112
  stats.cacheHitRate = rate;
@@ -445,15 +445,15 @@ export class AgentSessionImpl {
445
445
  return lastAssistantText(this.manager.branch());
446
446
  }
447
447
  getStats() {
448
- const contextTokens = this.compaction.estimate().tokens;
448
+ const context = this.compaction.contextStats();
449
449
  const contextWindow = this.settings.model.contextWindow;
450
450
  const stats = computeStats({
451
451
  sessionId: this.manager.id,
452
452
  sessionFile: this.manager.file(),
453
453
  branch: this.manager.branch(),
454
- contextTokens,
454
+ context,
455
455
  contextWindow,
456
- cache: this.cache.stats({ tokens: contextTokens, window: contextWindow }),
456
+ cache: this.cache.stats({ tokens: context.tokens, window: contextWindow }),
457
457
  quota: this.lastQuota,
458
458
  });
459
459
  return this.extensions.contributeStats(stats);
@@ -179,6 +179,9 @@ export interface ExternalAgentStats {
179
179
  unit: ExternalUsageUnit;
180
180
  amount: number;
181
181
  tokens?: number;
182
+ /** 最近一次报告的上下文占用与窗口(token);驱动不报时缺省。 */
183
+ contextTokens?: number;
184
+ contextWindow?: number;
182
185
  }>;
183
186
  }
184
187
  /** 子 Agent 任务注册表的汇总(`/session` 与 `get_session_stats`)。 */
@@ -356,6 +356,8 @@ export interface SessionStats extends SessionStatsW5, SessionStatsW6 {
356
356
  contextWindow: number | undefined;
357
357
  /** 0–100;无窗口时 undefined(显示 `ctx ?`)。 */
358
358
  contextPercent: number | undefined;
359
+ /** 上下文用量的来源与压缩阈值(`contextTokens` 怎么来的);旧实现与测试替身可缺省。 */
360
+ context?: SessionContextStats;
359
361
  /**
360
362
  * 缓存命中率 0–1 = cacheRead /(input + cacheRead + cacheWrite)(设计 §9.1);
361
363
  * 还没有任何输入用量时不给。
@@ -364,6 +366,22 @@ export interface SessionStats extends SessionStatsW5, SessionStatsW6 {
364
366
  /** [W3-C0] 缓存可观测性(第三波 §1.10);会话层缓存控制器(C1b)未接线时缺省。 */
365
367
  cache?: SessionCacheStats;
366
368
  }
369
+ /**
370
+ * `SessionStats.context`:`contextTokens` 的来源与自动压缩阈值(只统计,不改请求)。
371
+ * - `usage`:最后一条有效助手消息的 usage(`usageTokens`)+ 其后消息的估算(`trailingTokens`);
372
+ * - `estimate`:没有可信的 usage(压缩 / 上下文编辑之后、新 usage 到来之前,或从未有 usage),按投影全量估算;
373
+ * - `prefix`:系统消息还没落盘(第一次请求前),用当前系统提示 + 工具声明估算的基线加上已有消息的估算。
374
+ * 后两种 `usageTokens` 为 0、`trailingTokens` 等于 `contextTokens`。
375
+ */
376
+ export interface SessionContextStats {
377
+ source: "usage" | "estimate" | "prefix";
378
+ usageTokens: number;
379
+ trailingTokens: number;
380
+ /** 档二自动摘要的阈值(窗口 − reserveTokens);自动压缩关闭、熔断或窗口未知时缺省。 */
381
+ autoCompactAt?: number;
382
+ /** 档一裁剪的阈值(0.7 ×(窗口 − reserveTokens));缺省条件同上。 */
383
+ pruneAt?: number;
384
+ }
367
385
  /** [W3-C0] `SessionStats.cache`(`get_session_stats` / `/session` / `-p --output-format json`)。 */
368
386
  export interface SessionCacheStats {
369
387
  /** 当前端点的三态。 */
@@ -150,6 +150,10 @@ export interface ProgressSink {
150
150
  export declare function flushText(record: TaskRecord, sink: ProgressSink): void;
151
151
  /** runner 进度 → 记录与 `subagent_update`(文本 ≥ 250 ms 合并,轮次与结束时冲刷)。 */
152
152
  export declare function applyRunnerEvent(record: TaskRecord, event: SubagentEvent, sink: ProgressSink): void;
153
+ /** 任务的上下文占用(0–100,一位小数,封顶 100);占用或窗口未知时 undefined。 */
154
+ export declare function taskContextPercent(info: Pick<TaskInfo, "contextTokens" | "contextWindow">): number | undefined;
155
+ /** 上下文占用的显示:≥ 10% 取整,以下保留一位小数(`34%`、`0.4%`);未知时 undefined。 */
156
+ export declare function formatTaskContext(info: Pick<TaskInfo, "contextTokens" | "contextWindow">): string | undefined;
153
157
  /** `subagent_start` 事件(新开或续聊都发)。 */
154
158
  export declare function startEvent(record: TaskRecord, handle: TaskHandle): SubagentStartEvent;
155
159
  /** `getStats().tasks`:没有任务时 undefined。 */
@@ -163,6 +163,10 @@ export function applyRunnerEvent(record, event, sink) {
163
163
  record.info.usage = event.usage;
164
164
  if (event.unit === "usd" && event.amount !== undefined)
165
165
  record.info.costUsd = event.amount;
166
+ if (event.contextTokens !== undefined)
167
+ record.info.contextTokens = event.contextTokens;
168
+ if (event.contextWindow !== undefined)
169
+ record.info.contextWindow = event.contextWindow;
166
170
  return;
167
171
  case "notice":
168
172
  sink.log(event.level, `[task ${taskId}] ${event.text}`);
@@ -209,6 +213,20 @@ function recordDisplay(record, event, now) {
209
213
  return;
210
214
  }
211
215
  }
216
+ /** 任务的上下文占用(0–100,一位小数,封顶 100);占用或窗口未知时 undefined。 */
217
+ export function taskContextPercent(info) {
218
+ const { contextTokens: used, contextWindow: size } = info;
219
+ if (used === undefined || size === undefined || size <= 0)
220
+ return undefined;
221
+ return Math.min(100, Math.round((used / size) * 1000) / 10);
222
+ }
223
+ /** 上下文占用的显示:≥ 10% 取整,以下保留一位小数(`34%`、`0.4%`);未知时 undefined。 */
224
+ export function formatTaskContext(info) {
225
+ const percent = taskContextPercent(info);
226
+ if (percent === undefined)
227
+ return undefined;
228
+ return `${percent >= 10 ? Math.round(percent) : percent.toFixed(1)}%`;
229
+ }
212
230
  /** `subagent_start` 事件(新开或续聊都发)。 */
213
231
  export function startEvent(record, handle) {
214
232
  const event = {
@@ -2,8 +2,11 @@
2
2
  * normalizeContext(设计 §1.2 ai/context.ts):把循环交来的转录(含 `system` 补丁消息)变成
3
3
  * 协议可直接拼请求体的形状。
4
4
  *
5
- * - system 折叠:按出现顺序重放全部 system 消息的 `sections`(节名级替换,null 删除),得到
6
- * `systemSections`(保持节首次出现的顺序)与拼好的 `systemPrompt`(节间空一行);
5
+ * - system 折叠:首条非 system 消息之前的 system 消息按出现顺序重放(节名级替换,null 删除),
6
+ * 得到 `systemSections`(保持节首次出现的顺序)与拼好的 `systemPrompt`(节间空一行);
7
+ * - 中途节补丁(设计 §9.1「会话中途变化的上下文只追加」):对话开始之后的 system 补丁不改写开头,
8
+ * 渲染成 `<system-reminder>` 包裹的 user 消息按位置插回,开头与之前的消息逐字节不变;
9
+ * 例外是补丁移除了工具——工具表本身已变、前缀必然失效,这时全部补丁照旧折回开头;
7
10
  * - 工具表:按顺序重放 `toolsRemoved`(先)与 `toolsAdded`(后),同名后者覆盖;
8
11
  * - 模态过滤:模型不收图片时,用户消息与工具结果里的图片块换成文字占位。
9
12
  *
@@ -18,6 +21,8 @@ export interface NormalizeOptions {
18
21
  }
19
22
  export declare const IMAGE_OMITTED_TEXT = "[image omitted: the model does not accept image input]";
20
23
  export declare function normalizeContext(context: TranscriptContext, options?: NormalizeOptions): NormalizedContext;
24
+ /** 中途节补丁作为尾部上下文消息送达时的文本(固定英文,与界面语言无关)。 */
25
+ export declare function systemReminderText(updates: readonly string[]): string;
21
26
  /** 渲染一条中途 system 补丁的文本(节变更;工具表变化由请求的工具列表体现)。 */
22
27
  export declare function renderSystemUpdate(message: SystemMessage): string;
23
28
  export interface InlineSystemUpdate {
@@ -2,8 +2,11 @@
2
2
  * normalizeContext(设计 §1.2 ai/context.ts):把循环交来的转录(含 `system` 补丁消息)变成
3
3
  * 协议可直接拼请求体的形状。
4
4
  *
5
- * - system 折叠:按出现顺序重放全部 system 消息的 `sections`(节名级替换,null 删除),得到
6
- * `systemSections`(保持节首次出现的顺序)与拼好的 `systemPrompt`(节间空一行);
5
+ * - system 折叠:首条非 system 消息之前的 system 消息按出现顺序重放(节名级替换,null 删除),
6
+ * 得到 `systemSections`(保持节首次出现的顺序)与拼好的 `systemPrompt`(节间空一行);
7
+ * - 中途节补丁(设计 §9.1「会话中途变化的上下文只追加」):对话开始之后的 system 补丁不改写开头,
8
+ * 渲染成 `<system-reminder>` 包裹的 user 消息按位置插回,开头与之前的消息逐字节不变;
9
+ * 例外是补丁移除了工具——工具表本身已变、前缀必然失效,这时全部补丁照旧折回开头;
7
10
  * - 工具表:按顺序重放 `toolsRemoved`(先)与 `toolsAdded`(后),同名后者覆盖;
8
11
  * - 模态过滤:模型不收图片时,用户消息与工具结果里的图片块换成文字占位。
9
12
  *
@@ -55,16 +58,51 @@ function allowsImages(options) {
55
58
  return options?.model === undefined || options.model.input.includes("image");
56
59
  }
57
60
  export function normalizeContext(context, options) {
58
- const state = new SystemState();
61
+ const head = new SystemState();
62
+ const tools = new SystemState();
59
63
  const messages = [];
60
64
  const allowImages = allowsImages(options);
65
+ const fold = removesToolsMidway(context.messages);
66
+ let pending = [];
67
+ const flush = () => {
68
+ if (pending.length === 0)
69
+ return;
70
+ messages.push({ role: "user", content: systemReminderText(pending), timestamp: 0 });
71
+ pending = [];
72
+ };
61
73
  for (const message of context.messages) {
62
- if (message.role === "system")
63
- state.apply(message);
64
- else
74
+ if (message.role !== "system") {
75
+ flush();
65
76
  messages.push(filterMessage(message, allowImages));
77
+ continue;
78
+ }
79
+ tools.apply(message);
80
+ if (fold || messages.length === 0) {
81
+ head.apply(message);
82
+ continue;
83
+ }
84
+ const text = renderSystemUpdate(message);
85
+ if (text.length > 0)
86
+ pending.push(text);
66
87
  }
67
- return { ...state.snapshot(), messages };
88
+ flush();
89
+ return { ...head.snapshot(), tools: tools.snapshot().tools, messages };
90
+ }
91
+ /** 对话开始之后有补丁移除了工具(这时整段前缀必然失效,补丁全部折回开头)。 */
92
+ function removesToolsMidway(transcript) {
93
+ let started = false;
94
+ for (const message of transcript) {
95
+ if (message.role !== "system")
96
+ started = true;
97
+ else if (started && (message.toolsRemoved?.length ?? 0) > 0)
98
+ return true;
99
+ }
100
+ return false;
101
+ }
102
+ /** 中途节补丁作为尾部上下文消息送达时的文本(固定英文,与界面语言无关)。 */
103
+ export function systemReminderText(updates) {
104
+ return (`<system-reminder>\n${updates.join("\n\n")}\n\n` +
105
+ "These updates replace the earlier versions of those system prompt sections.\n</system-reminder>");
68
106
  }
69
107
  /** 渲染一条中途 system 补丁的文本(节变更;工具表变化由请求的工具列表体现)。 */
70
108
  export function renderSystemUpdate(message) {
@@ -6,7 +6,7 @@ export const CATALOG_SOURCES = {
6
6
  anthropic: '{"version":1,"provider":"anthropic","modelsDev":"anthropic","models":[{"id":"claude-fable-5-1","thinkingLevelMap":{"off":null,"xhigh":"xhigh"},"promptCache":{"short":300,"long":3600,"minTokens":512},"compat":{"adaptiveThinking":true}},{"id":"claude-fable-5","thinkingLevelMap":{"off":null,"xhigh":"xhigh"},"promptCache":{"short":300,"long":3600,"minTokens":512},"compat":{"adaptiveThinking":true}},{"id":"claude-opus-5-5","thinkingLevelMap":{"off":null,"minimal":null,"low":"low","medium":"medium","high":"high","xhigh":"xhigh"},"promptCache":{"short":300,"long":3600,"minTokens":512},"compat":{"adaptiveThinking":true}},{"id":"claude-opus-5","thinkingLevelMap":{"off":null,"xhigh":"xhigh"},"promptCache":{"short":300,"long":3600,"minTokens":512},"compat":{"adaptiveThinking":true}},{"id":"claude-opus-4-8","thinkingLevelMap":{"xhigh":"xhigh"},"promptCache":{"short":300,"long":3600,"minTokens":1024},"compat":{"adaptiveThinking":true}},{"id":"claude-opus-4-7","thinkingLevelMap":{"xhigh":"xhigh"},"promptCache":{"short":300,"long":3600,"minTokens":2048},"compat":{"adaptiveThinking":true}},{"id":"claude-opus-4-6","promptCache":{"short":300,"long":3600,"minTokens":4096},"compat":{"adaptiveThinking":true}},{"id":"claude-sonnet-5-5","thinkingLevelMap":{"off":null,"minimal":null,"low":"low","medium":"medium","high":"high","xhigh":"xhigh"},"promptCache":{"short":300,"long":3600,"minTokens":512},"compat":{"adaptiveThinking":true}},{"id":"claude-sonnet-5","thinkingLevelMap":{"xhigh":"xhigh"},"promptCache":{"short":300,"long":3600,"minTokens":1024},"compat":{"adaptiveThinking":true}},{"id":"claude-sonnet-4-6","promptCache":{"short":300,"long":3600,"minTokens":1024},"compat":{"adaptiveThinking":true}},{"id":"claude-sonnet-4-5","promptCache":{"short":300,"long":3600,"minTokens":1024}},{"id":"claude-haiku-4-5","promptCache":{"short":300,"long":3600,"minTokens":4096}},{"id":"claude-opus-4-5","promptCache":{"short":300,"long":3600,"minTokens":4096}}]}',
7
7
  chatgpt: '{"version":1,"provider":"chatgpt","modelsDev":false,"models":[]}',
8
8
  dashscope: '{"version":1,"provider":"dashscope","modelsDev":"alibaba-cn","models":[{"id":"qwen3.8-max","thinkingLevelMap":{"off":null,"minimal":null,"low":"low","medium":"medium","high":null,"xhigh":"xhigh"},"compat":{"supportsReasoningEffort":true}},{"id":"qwen3.8-flash","thinkingLevelMap":{"off":null,"minimal":null,"low":"low","medium":"medium","high":null,"xhigh":"xhigh"},"compat":{"supportsReasoningEffort":true}},{"id":"qwen3.7-max","maxTokens":131072,"compat":{"supportsReasoningEffort":false}},{"id":"qwen3.7-plus","maxTokens":65536,"compat":{"supportsReasoningEffort":false}},{"id":"qwen3.6-plus","compat":{"supportsReasoningEffort":false}},{"id":"qwen3.6-flash","compat":{"supportsReasoningEffort":false}}]}',
9
- deepseek: '{"version":1,"provider":"deepseek","modelsDev":"deepseek","models":[{"id":"deepseek-v4-pro","maxTokens":384000,"cost":{"input":1.32,"output":3.96,"cacheRead":0.044},"thinkingLevelMap":{"minimal":null,"low":null,"medium":null,"high":"high"},"_reason":"按官方价格页的高峰价(UTC 工作日 01–04、06–10 时)计,作上界;平峰为半价。快照价是平峰价或旧价"},{"id":"deepseek-flash","maxTokens":384000,"cost":{"input":0.3,"output":1.2,"cacheRead":0.006},"thinkingLevelMap":{"minimal":null,"low":"low","medium":null,"high":"high"},"_reason":"按官方价格页的高峰价(UTC 工作日 01–04、06–10 时)计,作上界;平峰为半价。快照价是平峰价或旧价"}]}',
9
+ deepseek: '{"version":1,"provider":"deepseek","modelsDev":"deepseek","models":[{"id":"deepseek-v4-pro","maxTokens":384000,"cost":{"input":1.32,"output":3.96,"cacheRead":0.044},"thinkingLevelMap":{"minimal":null,"low":null,"medium":null,"high":"high"},"_reason":"按官方价格页的高峰价(UTC 工作日 01–04、06–10 时)计,作上界;平峰为半价。快照价是平峰价或旧价","promptCache":{"short":3600,"minTokens":2048}},{"id":"deepseek-flash","maxTokens":384000,"cost":{"input":0.3,"output":1.2,"cacheRead":0.006},"thinkingLevelMap":{"minimal":null,"low":"low","medium":null,"high":"high"},"_reason":"按官方价格页的高峰价(UTC 工作日 01–04、06–10 时)计,作上界;平峰为半价。快照价是平峰价或旧价","promptCache":{"short":3600,"minTokens":2048}}]}',
10
10
  google: '{"version":1,"provider":"google","modelsDev":"google","models":[{"id":"gemini-3.8-flash","thinkingLevelMap":{"off":null,"minimal":null,"low":"low","medium":"medium","high":"high","xhigh":null}},{"id":"gemini-3.7-flash","thinkingLevelMap":{"off":null,"minimal":null,"low":"low","medium":"medium","high":"high","xhigh":null}},{"id":"gemini-3.5-flash","thinkingLevelMap":{"off":null,"minimal":"minimal","low":"low","medium":"medium","high":"high","xhigh":null}},{"id":"gemini-3.5-flash-lite","thinkingLevelMap":{"off":null,"minimal":"minimal","low":"low","medium":"medium","high":"high","xhigh":null}},{"id":"gemini-3.1-pro-preview","thinkingLevelMap":{"off":null,"minimal":null,"low":"low","medium":"medium","high":"high","xhigh":null}},{"id":"gemini-3-flash-preview","thinkingLevelMap":{"off":null,"minimal":"minimal","low":"low","medium":"medium","high":"high","xhigh":null}},{"id":"gemini-2.5-pro","thinkingLevelMap":{"off":null,"minimal":128,"low":2048,"medium":8192,"high":32768}},{"id":"gemini-2.5-flash","thinkingLevelMap":{"minimal":128,"low":2048,"medium":8192,"high":24576}},{"id":"gemini-2.5-flash-lite","thinkingLevelMap":{"minimal":512,"low":2048,"medium":8192,"high":24576}}]}',
11
11
  groq: '{"version":1,"provider":"groq","modelsDev":"groq","models":[{"id":"openai/gpt-oss-120b","thinkingLevelMap":{"off":null,"minimal":null,"low":"low","medium":"medium","high":"high","xhigh":null}},{"id":"openai/gpt-oss-20b","thinkingLevelMap":{"off":null,"minimal":null,"low":"low","medium":"medium","high":"high","xhigh":null}},{"id":"openai/gpt-oss-safeguard-20b","thinkingLevelMap":{"off":null,"minimal":null,"low":"low","medium":"medium","high":"high","xhigh":null}},{"id":"qwen/qwen3.8-27b","thinkingLevelMap":{"off":"none","minimal":null,"low":"low","medium":"medium","high":"high","xhigh":null}},{"id":"qwen/qwen3.6-27b","thinkingLevelMap":{"off":"none","minimal":null,"low":null,"medium":null,"high":"default","xhigh":null}},{"id":"llama-3.3-70b-versatile"},{"id":"llama-3.1-8b-instant"}]}',
12
12
  lmstudio: '{"version":1,"provider":"lmstudio","modelsDev":false,"models":[]}',
@@ -6,6 +6,8 @@
6
6
  * - 写:读-改-写 `auth.json` 整个文件,经 `writeAuthFile`(同目录临时文件 0600 → rename → chmod),其它条目原样保留;
7
7
  * - 锁:`<auth.json>.lock`,`open(O_CREAT|O_EXCL)`(flag `wx`)写 `{ pid, at }`;等待 ≤ 15 s;锁文件超过 60 s 且
8
8
  * pid 已死视为陈旧,删除后重试。取锁后由调用方**重读文件**再决定要不要刷新。
9
+ * - Windows:锁文件删除挂起时 `open(wx)` 报 EPERM / EACCES,按「被占用」继续等;读 / rename 的同类瞬时错误
10
+ * 短暂重试(`config/fs-retry.ts`)。
9
11
  */
10
12
  import { type OAuthAuthEntry } from "../../config/types-w6.js";
11
13
  export declare const LOCK_WAIT_MS = 15000;
@@ -6,10 +6,13 @@
6
6
  * - 写:读-改-写 `auth.json` 整个文件,经 `writeAuthFile`(同目录临时文件 0600 → rename → chmod),其它条目原样保留;
7
7
  * - 锁:`<auth.json>.lock`,`open(O_CREAT|O_EXCL)`(flag `wx`)写 `{ pid, at }`;等待 ≤ 15 s;锁文件超过 60 s 且
8
8
  * pid 已死视为陈旧,删除后重试。取锁后由调用方**重读文件**再决定要不要刷新。
9
+ * - Windows:锁文件删除挂起时 `open(wx)` 报 EPERM / EACCES,按「被占用」继续等;读 / rename 的同类瞬时错误
10
+ * 短暂重试(`config/fs-retry.ts`)。
9
11
  */
10
12
  import { closeSync, mkdirSync, openSync, readFileSync, statSync, unlinkSync, writeSync, } from "node:fs";
11
13
  import { dirname } from "node:path";
12
14
  import { writeAuthFile } from "../../config/auth-file.js";
15
+ import { isTransientFsError, retryTransientFs } from "../../config/fs-retry.js";
13
16
  import { CONFIG_FILE_VERSION } from "../../config/types.js";
14
17
  import { isOAuthEntry } from "../../config/types-w6.js";
15
18
  import { AmaError } from "../../errors.js";
@@ -18,7 +21,7 @@ export const LOCK_STALE_MS = 60_000;
18
21
  const LOCK_POLL_MS = 50;
19
22
  function readRaw(path) {
20
23
  try {
21
- const value = JSON.parse(readFileSync(path, "utf8"));
24
+ const value = JSON.parse(retryTransientFs(() => readFileSync(path, "utf8")));
22
25
  return typeof value === "object" && value !== null && typeof value.providers === "object"
23
26
  ? value
24
27
  : undefined;
@@ -85,7 +88,9 @@ function tryAcquire(path) {
85
88
  return true;
86
89
  }
87
90
  catch (error) {
88
- if (error.code === "EEXIST")
91
+ const code = error.code;
92
+ // Windows:上一个持锁进程刚删掉锁文件(删除挂起)时 open(wx) 报 EPERM / EACCES——同样是「被占用」
93
+ if (code === "EEXIST" || isTransientFsError(code))
89
94
  return false;
90
95
  throw error;
91
96
  }