@armadra/agent 0.7.5 → 0.8.0

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 (84) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/CHANGELOG.zh-CN.md +34 -0
  3. package/dist/agent/session-classifier.d.ts +2 -0
  4. package/dist/agent/session-classifier.js +24 -3
  5. package/dist/agent/session-extensions.d.ts +1 -0
  6. package/dist/agent/session-extensions.js +3 -0
  7. package/dist/agent/session-subagent.js +9 -2
  8. package/dist/agent/subagent-background.d.ts +5 -1
  9. package/dist/agent/subagent-background.js +13 -1
  10. package/dist/agent/subagent-fork.d.ts +10 -5
  11. package/dist/agent/subagent-fork.js +22 -6
  12. package/dist/agent/subagent-registry.d.ts +1 -1
  13. package/dist/agent/subagent-registry.js +1 -1
  14. package/dist/agents/catalog.d.ts +1 -0
  15. package/dist/ai/apis/chatgpt-backend.d.ts +3 -1
  16. package/dist/ai/apis/chatgpt-backend.js +10 -1
  17. package/dist/ai/apis/max-tokens.d.ts +4 -0
  18. package/dist/ai/apis/max-tokens.js +12 -1
  19. package/dist/ai/providers/enrich.d.ts +6 -2
  20. package/dist/ai/providers/enrich.js +13 -2
  21. package/dist/ai/providers/max-tokens-cache.d.ts +22 -0
  22. package/dist/ai/providers/max-tokens-cache.js +95 -0
  23. package/dist/ai/providers/registry.js +3 -3
  24. package/dist/bundle/ama.cjs +1250 -375
  25. package/dist/checkpoints/shadow-git.d.ts +5 -1
  26. package/dist/checkpoints/shadow-git.js +11 -2
  27. package/dist/cli/compose-providers.d.ts +1 -0
  28. package/dist/cli/compose-providers.js +3 -0
  29. package/dist/cli/compose-store.d.ts +6 -2
  30. package/dist/cli/compose-store.js +8 -4
  31. package/dist/cli/compose.js +2 -2
  32. package/dist/config/json-schema.js +1 -0
  33. package/dist/config/key-docs.js +1 -0
  34. package/dist/config/schema-w5.js +2 -0
  35. package/dist/config/settings-registry.js +1 -0
  36. package/dist/config/types-w5.d.ts +5 -0
  37. package/dist/drivers/acp/driver.d.ts +22 -1
  38. package/dist/drivers/acp/driver.js +59 -5
  39. package/dist/drivers/agents.js +3 -0
  40. package/dist/drivers/catalog.d.ts +2 -0
  41. package/dist/drivers/catalog.js +94 -4
  42. package/dist/drivers/native/claude-normalize.d.ts +8 -0
  43. package/dist/drivers/native/claude-normalize.js +27 -0
  44. package/dist/drivers/native/claude-stream.js +16 -1
  45. package/dist/drivers/native/codex-app-server.js +4 -2
  46. package/dist/drivers/native/oneshot.js +12 -2
  47. package/dist/drivers/native/pi-gate.d.ts +19 -0
  48. package/dist/drivers/native/pi-gate.js +39 -0
  49. package/dist/drivers/native/pi-rpc.d.ts +29 -0
  50. package/dist/drivers/native/pi-rpc.js +483 -0
  51. package/dist/drivers/runner.js +1 -0
  52. package/dist/drivers/types.d.ts +1 -1
  53. package/dist/i18n/catalog.d.ts +5 -0
  54. package/dist/i18n/messages/auth.d.ts +2 -0
  55. package/dist/i18n/messages/auth.js +2 -0
  56. package/dist/i18n/messages/config-keys.d.ts +2 -0
  57. package/dist/i18n/messages/config-keys.js +2 -0
  58. package/dist/i18n/messages/config.d.ts +2 -0
  59. package/dist/i18n/messages/drivers.d.ts +4 -0
  60. package/dist/i18n/messages/drivers.js +4 -0
  61. package/dist/i18n/messages/interactive.d.ts +2 -0
  62. package/dist/i18n/messages/interactive.js +2 -0
  63. package/dist/i18n/messages/session.js +2 -2
  64. package/dist/modes/interactive/agent-ui.js +7 -0
  65. package/dist/session/manager.d.ts +3 -1
  66. package/dist/session/manager.js +5 -0
  67. package/dist/session/offload.d.ts +10 -2
  68. package/dist/session/offload.js +27 -4
  69. package/dist/tools/background-jobs.d.ts +4 -4
  70. package/dist/tools/background-jobs.js +4 -4
  71. package/dist/tools/bash.d.ts +2 -1
  72. package/dist/tools/bash.js +4 -3
  73. package/dist/tools/task-ctl.js +2 -1
  74. package/dist/tools/task.d.ts +9 -0
  75. package/dist/tools/task.js +27 -1
  76. package/docs/agents.md +70 -14
  77. package/docs/en/permissions.md +1 -1
  78. package/docs/en/providers.md +11 -10
  79. package/docs/en/sessions.md +2 -1
  80. package/docs/permissions.md +1 -1
  81. package/docs/providers.md +16 -11
  82. package/docs/session-format.md +2 -1
  83. package/docs/sessions.md +2 -1
  84. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -5,6 +5,40 @@ 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.8.0 (2026-10-10)
9
+
10
+ ### External agents
11
+
12
+ - **New `pi` driver**: `task(agent="pi")` drives `pi --mode rpc` (verified with pi 1.1.0). pi has no approval channel of its own, so ama loads a minimal extension for that run (`-e`, written to a per-session temp dir and deleted on close; pi's config is untouched) that routes every non-read-only tool call to ama: rejected in `plan` / `allowlist` (which also only enable `read,grep,find,ls`), `edit` / `write` allowed from `auto-edit` up, everything else asks you. Dialogs from your own pi extensions are cancelled, never answered. Two-turn resume, steer, interrupt, USD usage and context occupancy are reported. The community `pi-acp` adapter is not listed: it never sends permission requests (#198).
13
+ - **Cursor CLI listed as unverified**: `task(agent="cursor")` runs `cursor-agent acp` per Cursor's ACP docs (modes `agent` / `plan` / `ask`); not tested locally because it is not installed (#198).
14
+ - **Codex one-shot mode works outside git repos**: `codex exec` / `exec resume` now pass `--skip-git-repo-check` (ama only starts external agents in directories it already trusts); previously they failed immediately in a non-git directory (#198).
15
+ - **ACP adapters no longer stay in a looser mode**: explicit mode maps for claude-agent-acp (`auto-edit` → `acceptEdits`, `full-auto` → `auto`, so a user default of `bypassPermissions` is replaced), codex-acp (`plan` / `default` → `read-only`, `auto-edit` → `workspace-write`, `auto` / `full-auto` → `agent`; `plan` used to refuse to start) and Copilot (URL mode ids `…#plan` / `…#agent`). Modes that open every permission are never chosen (#198).
16
+ - **`model` reaches ACP agents**: `agents.<id>.model` and the `task` `model` argument are now applied through the agent's `model` session config option (matched by value, name, or the model part of `provider/model`); when nothing matches you get a notice and the agent's default model (#198).
17
+ - **Usage numbers corrected**: Codex (app-server and `exec`) and Copilot report input tokens including cache hits, and Copilot's ACP usage is a session total; ama now subtracts the cached part and takes per-turn differences, so totals no longer count cache reads twice. Claude stream-json now reports context occupancy and window like the other drivers (#198).
18
+ - **Verified ranges updated** after a real-CLI audit on 2026-10-10: claude-agent-acp 0.89, codex-acp 2.2, Copilot 1.0.95, OpenCode 1.18, pi 1.1; capability matrix in docs/agents.md, numbers in docs/benchmarks/external-agents-2026-10.md (#198).
19
+ - **`model_unavailable` for ChatGPT models the account cannot use**: a codex-backend 400 "model is not supported when using Codex with a ChatGPT account" now fails with `model_unavailable` and tells you to refresh the list with `ama models discover chatgpt` (the backend withdrew `gpt-6-astra`, which an older discovery cache still listed) (#198).
20
+
21
+ ## 0.7.6 (2026-10-10)
22
+
23
+ ### Model efficiency
24
+
25
+ - **The auto-mode classifier picks a small model on relays and custom providers too**: when the session model inherits an official catalog entry by id (e.g. `packy/deepseek-v4-pro`), the classifier uses the same vendor's catalog `small` model if the provider's model table lists it (`packy/deepseek-v4-flash`); only listed models are used, never synthesized ones. `permission.autoModel` still overrides (#153).
26
+ - **Fork sub-agents are steered back to their task when `task` / `task_ctl` is rejected**: the rejection now says the caller is a forked sub-agent that cannot start sub-agents, asks it to do the sub-task itself, and quotes the first line of its `<task>` instructions (fresh sub-agents keep the old text; the tool list and the first request are unchanged). DeepSeek retest: one run made no `task` call, the other made one and then read the file and reported (#191).
27
+ - **Learned `max_tokens` limits persist across processes**: when an endpoint rejects with a 400 stating its output limit, the limit is now also written to `<dataDir>/models/max-tokens-caps.json` and loaded back on startup, so later processes no longer get the first request rejected once per model (packy `kimi-k2.5`: second process 0 rejections, previously 1). Entries expire after 30 days; delete the file to re-probe.
28
+ - **Warning for a wrong `models[].catalog` reference**: `catalog: "provider/id"` that matches no catalog entry now produces a registry warning (startup header count, `ama models list`, `AMA_LOG`) instead of silently inheriting nothing.
29
+
30
+ ### Sessions
31
+
32
+ - **Unreadable session images are reported**: when a downgraded image cannot be read back because the session file was changed by another process, the warning now reaches the session log (`ama: [warn] cannot read session entry …` on stderr, filtered by `AMA_LOG`; a notice in the TUI) instead of being dropped. Warnings raised while the file is being opened are buffered and shown once the session is ready (at most 16, the rest counted); the source session of `--fork` reports through the same log.
33
+ - **`subagents.forkMaxContextRatio`**: the share of (window − `compaction.reserveTokens`) above which a fork sub-agent falls back to fresh is now configurable (0.05–0.95, default 0.5, user level only). Built-in types, `general` included, stay fresh by default; docs/agents.md explains the per-turn cost of re-reading the parent context.
34
+ - **Fork sub-agents are told `task` / `task_ctl` are unavailable**: the `<task>` message now lists them and asks not to call them (they stay in the tool list and are still rejected by depth). In two DeepSeek retests the sub-agent still made one rejected `task` call each time.
35
+ - **Background fork tasks report their mode right away**: the `running` result of a background `task` now carries `details.context` (`fork` / `fresh`) once the mode is known; a queued task or one waiting for its worktree still gets it later in `TaskInfo.context`.
36
+ - **Background `bash` job output fits the session result limit in one cut**: `{job, action: "wait" | "output" | "stop"}` now tail-truncates to the same byte limit as foreground `bash` (50 KB or `maxToolResultChars` − 512, whichever is smaller) instead of a fixed 2000 lines / 50 KB that the session then cut again in the middle; the note still gives the shown line count and `Full output: <path>`.
37
+
38
+ ### Checkpoints
39
+
40
+ - **A slow first shadow snapshot no longer downgrades the session**: the first snapshot (which builds the index) is no longer timed, and the session downgrades to `tools` only after two snapshots in a row exceed 3 seconds (one slow snapshot followed by a normal one keeps shadow-git). The one-time notice now says so (#194).
41
+
8
42
  ## 0.7.5 (2026-10-10)
9
43
 
10
44
  ### Memory footprint
@@ -4,6 +4,40 @@
4
4
 
5
5
  > 从 0.6.0 起 [CHANGELOG.md](CHANGELOG.md) 为英文,本文件保留中文记录(0.1–0.5.1 的完整历史在此)。新条目两份都要加。
6
6
 
7
+ ## 0.8.0(2026-10-10)
8
+
9
+ ### 外部 Agent
10
+
11
+ - **新增 `pi` 驱动**:`task(agent="pi")` 驱动 `pi --mode rpc`(pi 1.1.0 实测)。pi 自己没有审批通道,ama 以 `-e` 给这一次运行加载一个最小扩展(写在每个会话自己的临时目录、关会话即删,不碰 pi 的配置),把只读工具以外的调用交给 ama:`plan` / `allowlist` 直接拒绝(并只开 `read,grep,find,ls`),`auto-edit` 及更宽放行 `edit` / `write`,其余问你。你自己的 pi 扩展弹出的对话框一律取消、不代答。支持两轮续聊、steer、中断、美元用量与上下文占用。社区适配器 `pi-acp` 从不发权限请求,不收录(#198)。
12
+ - **收录 Cursor CLI(未验证)**:`task(agent="cursor")` 按官方 ACP 文档运行 `cursor-agent acp`(模式 `agent` / `plan` / `ask`);本机未安装,未实测(#198)。
13
+ - **Codex 一次性模式在非 git 目录可用**:`codex exec` / `exec resume` 带上 `--skip-git-repo-check`(ama 只在自己已信任的目录里起外部 Agent);此前在非 git 目录直接失败(#198)。
14
+ - **ACP 适配器不再停在更宽的模式**:claude-agent-acp(`auto-edit` → `acceptEdits`、`full-auto` → `auto`,用户缺省为 `bypassPermissions` 时也会改回)、codex-acp(`plan` / `default` → `read-only`、`auto-edit` → `workspace-write`、`auto` / `full-auto` → `agent`;此前 `plan` 拒绝启动)、Copilot(URL 形式的模式 id `…#plan` / `…#agent`)写入显式映射,从不选放开全部权限的模式(#198)。
15
+ - **`model` 交给 ACP Agent**:`agents.<id>.model` 与 `task` 的 `model` 参数经 Agent 的 `model` 会话配置项设置(按值、名称或 `provider/model` 的模型部分匹配);没有匹配时提示并用它的缺省模型(#198)。
16
+ - **用量口径修正**:Codex(app-server 与 `exec`)和 Copilot 报的 input 含缓存命中,Copilot 的 ACP usage 还是会话累计;ama 现在扣掉缓存部分、按回合取差值,总量不再把缓存读算两次。Claude stream-json 也像其它驱动一样上报上下文占用与窗口(#198)。
17
+ - **已验证区间更新**:2026-10-10 用真实 CLI 实测,claude-agent-acp 0.89、codex-acp 2.2、Copilot 1.0.95、OpenCode 1.18、pi 1.1;能力矩阵见 docs/agents.md,数字见 docs/benchmarks/external-agents-2026-10.md(#198)。
18
+ - **ChatGPT 账户不可用的模型报 `model_unavailable`**:codex 后端以 400「model is not supported when using Codex with a ChatGPT account」拒绝时,报 `model_unavailable` 并提示用 `ama models discover chatgpt` 刷新模型表(后端已撤下 `gpt-6-astra`,旧的发现缓存里还有它)(#198)。
19
+
20
+ ## 0.7.6(2026-10-10)
21
+
22
+ ### 模型调用效率
23
+
24
+ - **中转 / 自定义供应商的 auto 分类器也用小模型**:会话模型按 id 继承了官方目录条目时(如 `packy/deepseek-v4-pro`),若该供应商模型表里列有同厂商目录的 `small` 模型(`packy/deepseek-v4-flash`)就用它分类;只认模型表里列出的条目,不用合成模型。`permission.autoModel` 仍优先(#153)。
25
+ - **fork 子 Agent 调 `task` / `task_ctl` 被拒后回到子任务**:拒绝文案说明它是 fork 出的子 Agent、不能再派生,要求自己完成子任务并附 `<task>` 指令首行(fresh 子 Agent 文案不变;工具表与首请求不变)。DeepSeek 复测:一次没调 `task`,另一次调了一次被拒后读文件并给出报告(#191)。
26
+ - **学到的 `max_tokens` 上限跨进程保留**:端点以 400 说出输出上限时,上限同时写入 `<dataDir>/models/max-tokens-caps.json`,启动时载回,之后的进程不再每个模型首个请求被拒一次(packy `kimi-k2.5`:第二个进程 0 次 400,原为 1 次)。条目 30 天过期,删掉该文件即重测。
27
+ - **`models[].catalog` 写错时给 warning**:`catalog: "provider/id"` 不命中任何目录条目时,注册表记一条 warning(启动头部计数、`ama models list`、`AMA_LOG`),不再静默不继承。
28
+
29
+ ### 会话
30
+
31
+ - **会话图片读不回时有提示**:降级图片因会话文件被其它进程改写而读不回时,告警进会话日志(stderr 一行 `ama: [warn] cannot read session entry …`,受 `AMA_LOG` 过滤;TUI 显示在通知区),不再被丢弃。打开文件期间的告警先缓冲、会话建好时补出(最多 16 条,其余计数);`--fork` 的源会话走同一日志。
32
+ - **`subagents.forkMaxContextRatio`**:fork 子 Agent 回落为 fresh 的比例(父上一次请求占(窗口 − `compaction.reserveTokens`)的份额)可配置,0.05–0.95,缺省 0.5,只认用户级。内置类型(包括 `general`)仍缺省 fresh;docs/agents.md 说明了每回合重读父上下文的成本。
33
+ - **fork 子 Agent 被告知 `task` / `task_ctl` 不可用**:`<task>` 消息列出这两个工具并要求不要调用(它们仍在工具表里、按深度拒绝)。两次 DeepSeek 复测里子 Agent 仍各调了一次被拒的 `task`。
34
+ - **后台 fork 任务立即给出模式**:后台 `task` 的 `running` 结果在模式已知时带 `details.context`(`fork` / `fresh`);排队或等待 worktree 的任务仍在之后的 `TaskInfo.context` 里给出。
35
+ - **后台 `bash` 任务输出按会话结果上限一次截到位**:`{job, action: "wait" | "output" | "stop"}` 改为按与前台 `bash` 相同的字节上限(50 KB 与 `maxToolResultChars` − 512 取小)尾截断,不再固定 2000 行 / 50 KB 再被会话层截掉中段;说明行照旧写显示行数与 `Full output: <路径>`。
36
+
37
+ ### 检查点
38
+
39
+ - **首次影子快照偏慢不再让会话降级**:首次快照(建索引)不计时,之后连续两次快照超过 3 秒才降级为 `tools`(单次偏慢、下一次正常则继续用影子 git);降级仍只提示一次,文案改为说明是连续两次(#194)。
40
+
7
41
  ## 0.7.5(2026-10-10)
8
42
 
9
43
  ### 内存占用
@@ -4,6 +4,8 @@
4
4
  * - 模型:`options.permissionClassifier.model`(config `permission.autoModel`)解析成功就用它;
5
5
  * 否则(未配置、找不到;找不到时记一次 warning)[ME-D] 用会话供应商目录里的小模型(目录文件级
6
6
  * `small`,D12;要能找到且有 key),再否则用当前会话模型;选择结果记一次 debug。
7
+ * - [#153] 中转 / 自定义供应商(没有目录文件):会话模型按 id 继承的目录条目 → 原厂 `small` →
8
+ * 本供应商模型表(config `models[]`、发现缓存)里**已列出**的同一条目;不走合成路径。
7
9
  * - 请求是**独立**的:只有分类系统提示与一条用户消息,不带会话转录与工具表;`purpose: "classify"`
8
10
  * 让会话层缓存包装直接透传(不观测、不暂停 / 触发保温、不成为下一次请求的前缀依据);
9
11
  * `cacheRetention: "none"`,关闭思考,`maxTokens` 256。
@@ -4,13 +4,15 @@
4
4
  * - 模型:`options.permissionClassifier.model`(config `permission.autoModel`)解析成功就用它;
5
5
  * 否则(未配置、找不到;找不到时记一次 warning)[ME-D] 用会话供应商目录里的小模型(目录文件级
6
6
  * `small`,D12;要能找到且有 key),再否则用当前会话模型;选择结果记一次 debug。
7
+ * - [#153] 中转 / 自定义供应商(没有目录文件):会话模型按 id 继承的目录条目 → 原厂 `small` →
8
+ * 本供应商模型表(config `models[]`、发现缓存)里**已列出**的同一条目;不走合成路径。
7
9
  * - 请求是**独立**的:只有分类系统提示与一条用户消息,不带会话转录与工具表;`purpose: "classify"`
8
10
  * 让会话层缓存包装直接透传(不观测、不暂停 / 触发保温、不成为下一次请求的前缀依据);
9
11
  * `cacheRetention: "none"`,关闭思考,`maxTokens` 256。
10
12
  * - 用量记一条 `usage{kind:"permission_classify"}` 条目:计入 `/session` 费用,不进上下文。
11
13
  */
12
14
  import { CLASSIFIER_MAX_TOKENS, PermissionClassifier, } from "../permissions/classifier.js";
13
- import { catalogSmall } from "../ai/providers/catalog.js";
15
+ import { catalogByAlias, catalogSmall } from "../ai/providers/catalog.js";
14
16
  export const CLASSIFY_USAGE_KIND = "permission_classify";
15
17
  function textOf(message) {
16
18
  return message.content
@@ -19,9 +21,28 @@ function textOf(message) {
19
21
  .join("")
20
22
  .trim();
21
23
  }
22
- /** 会话供应商的目录小模型:能找到且 key 可用(或无需 key)才用。 */
24
+ /** [#153] 会话模型继承的目录条目所属厂商的 `small`,在本供应商模型表里列出的条目 id。 */
25
+ function inferredSmall(core, session) {
26
+ const hit = catalogByAlias(session.id);
27
+ if (hit === undefined)
28
+ return undefined;
29
+ const origin = hit.ref.slice(0, hit.ref.indexOf("/"));
30
+ const small = catalogSmall(origin);
31
+ const target = `${origin}/${small}`;
32
+ if (small === undefined || hit.ref === target)
33
+ return undefined;
34
+ const listed = core.options.providers.get(session.provider)?.models ?? [];
35
+ return listed.find((m) => {
36
+ if (m.id === small)
37
+ return true;
38
+ const entry = catalogByAlias(m.id);
39
+ return entry?.ref === target && entry.tier === undefined;
40
+ })?.id;
41
+ }
42
+ /** 分类用的小模型:会话供应商的目录 small,或 [#153] 推断出的同厂商小模型;能找到且 key 可用(或无需 key)才用。 */
23
43
  async function smallModel(core, session) {
24
- const small = catalogSmall(session.provider);
44
+ const own = catalogSmall(session.provider);
45
+ const small = own === undefined ? inferredSmall(core, session) : own;
25
46
  if (small === undefined || small === session.id)
26
47
  return undefined;
27
48
  const lookup = core.options.providers.findModel(`${session.provider}/${small}`);
@@ -20,6 +20,7 @@
20
20
  * 工厂在会话构造中途调用:只可读 `core.options / depth / cwd / manager`,不要 emit 或请求模型。
21
21
  * - 钩子抛错不影响会话:同步钩子的异常记 warn 日志后忽略;`beforePrompts` / `onAgentSettled`
22
22
  * 的异常同样记日志、按「无追加」处理。
23
+ * - 会话级诊断(#183):构造时顺带把 `core.manager` 的告警接到 `core.log`(每个会话实例一次)。
23
24
  * - `onAgentSettled` 不要等待人工输入(计划审批等):需要等待的流程自行异步进行,之后用
24
25
  * `prompt / followUp` 开新回合。
25
26
  */
@@ -20,6 +20,7 @@
20
20
  * 工厂在会话构造中途调用:只可读 `core.options / depth / cwd / manager`,不要 emit 或请求模型。
21
21
  * - 钩子抛错不影响会话:同步钩子的异常记 warn 日志后忽略;`beforePrompts` / `onAgentSettled`
22
22
  * 的异常同样记日志、按「无追加」处理。
23
+ * - 会话级诊断(#183):构造时顺带把 `core.manager` 的告警接到 `core.log`(每个会话实例一次)。
23
24
  * - `onAgentSettled` 不要等待人工输入(计划审批等):需要等待的流程自行异步进行,之后用
24
25
  * `prompt / followUp` 开新回合。
25
26
  */
@@ -36,6 +37,8 @@ export class SessionExtensions {
36
37
  list.push(extension);
37
38
  }
38
39
  this.list = list;
40
+ // 会话级诊断(#183):manager 的告警(图片读不回)走会话日志;动态读 options.log,TUI 换掉也生效
41
+ core.manager.setWarn((message) => core.log("warn", message));
39
42
  }
40
43
  get size() {
41
44
  return this.list.length;
@@ -137,7 +137,9 @@ async function startAmaChild(parent, spec, createChild, run) {
137
137
  }
138
138
  const thinkingLevel = spec.request.thinkingLevel ?? spec.agent.thinking ?? base.thinkingLevel ?? "off";
139
139
  const wanted = spec.resumeFile === undefined && requestedContext(spec.request, spec.agent) === "fork";
140
- const plan = wanted ? forkPlan(parent, spec, model, thinkingLevel) : undefined;
140
+ // #149:比例来自 `subagents.forkMaxContextRatio`(注册表环境里的整段配置)
141
+ const ratio = subagentRegistryFor(parent).env.modelConfig?.subagents?.forkMaxContextRatio;
142
+ const plan = wanted ? forkPlan(parent, spec, model, thinkingLevel, ratio) : undefined;
141
143
  if (plan !== undefined && "fallback" in plan)
142
144
  parent.log("info", `task ${spec.taskId}: fork falls back to fresh (${plan.fallback})`);
143
145
  const forked = plan !== undefined && "manager" in plan ? plan.manager : undefined;
@@ -149,6 +151,11 @@ async function startAmaChild(parent, spec, createChild, run) {
149
151
  // fork:工具表 = 父活动集(前缀不变),类型限制的工具在执行层拒绝
150
152
  const inherited = base.activeTools ?? (parent.options.tools ?? []).map((tool) => tool.name);
151
153
  const unavailable = inherited.filter((name) => !allowed.includes(name) && !PARENT_ONLY_TOOLS.includes(name));
154
+ // #150:<task> 里也列出按深度拒绝的 task / task_ctl(执行层的拒绝文案不变)
155
+ const briefUnavailable = [
156
+ ...unavailable,
157
+ ...PARENT_ONLY_TOOLS.filter((name) => inherited.includes(name)),
158
+ ];
152
159
  const options = {
153
160
  ...parent.options,
154
161
  ...base,
@@ -252,7 +259,7 @@ async function startAmaChild(parent, spec, createChild, run) {
252
259
  : forkBrief({
253
260
  prompt: run.prompt,
254
261
  role: spec.agent.prompt,
255
- unavailable,
262
+ unavailable: briefUnavailable,
256
263
  ...(spec.isolated ? { worktree: { cwd: spec.cwd, parentCwd: parent.cwd } } : {}),
257
264
  });
258
265
  let current = runOnce(first, spec.origin);
@@ -18,7 +18,11 @@ export type BackgroundReason = "user" | "timeout" | "host";
18
18
  export type BackgroundSetting = "auto" | "always" | "never";
19
19
  /** `auto`:交互 / RPC / ACP 缺省后台,`-p`(无人值守)缺省前台。 */
20
20
  export declare function resolveTaskBackground(setting: BackgroundSetting | undefined, unattended: boolean): boolean;
21
- export declare function startedResult(record: TaskRecord, outputFile?: string): SubagentResult;
21
+ /**
22
+ * 直接后台的工具结果。`settle`(任务的运行 promise)给出时先等一个宏任务:ama 子会话在第一个
23
+ * await 之前就发出 `context` 事件,池有空位时此刻模式已知;排队或 worktree 隔离未就绪时不带。
24
+ */
25
+ export declare function startedResult(record: TaskRecord, outputFile?: string, settle?: Promise<unknown>): Promise<SubagentResult>;
22
26
  export declare function backgroundedResult(record: TaskRecord, reason: BackgroundReason, afterMs: number, outputFile?: string): SubagentResult;
23
27
  /** 前台任务的等待者:`resolve` 让工具调用先返回,`detachParent` 解绑父 signal 与进度回调。 */
24
28
  export declare function foregroundWaiter(detachParent: () => void): {
@@ -32,9 +32,21 @@ function runningResult(record, text, outputFile) {
32
32
  };
33
33
  if (outputFile !== undefined)
34
34
  result.outputFile = outputFile;
35
+ // #156:fork / fresh 已定时带上实际模式(与完成结果的 details.context 同源)
36
+ if (record.info.context !== undefined)
37
+ result.context = record.info.context;
35
38
  return result;
36
39
  }
37
- export function startedResult(record, outputFile) {
40
+ /**
41
+ * 直接后台的工具结果。`settle`(任务的运行 promise)给出时先等一个宏任务:ama 子会话在第一个
42
+ * await 之前就发出 `context` 事件,池有空位时此刻模式已知;排队或 worktree 隔离未就绪时不带。
43
+ */
44
+ export async function startedResult(record, outputFile, settle) {
45
+ if (settle !== undefined)
46
+ await Promise.race([
47
+ settle.catch(() => undefined),
48
+ new Promise((resolve) => setImmediate(resolve)),
49
+ ]);
38
50
  return runningResult(record, startedText(record.info.taskId, record.agent.name), outputFile);
39
51
  }
40
52
  export function backgroundedResult(record, reason, afterMs, outputFile) {
@@ -5,7 +5,7 @@
5
5
  * 同一条 assistant 里并行的多个 fork 任务共享同一个 fork 点。
6
6
  * - `forkPlan`:满足条件时用 `SessionManager.fork(entryId, { head })` 复制分支,`head` 是
7
7
  * `custom{ama.task, context:"fork", forkedFrom}`(子会话首条仍是 `ama.task`);否则给出回落原因:
8
- * 与父不同的模型 / 思考级别、父还没有真实请求、父上一次请求超过 `FORK_MAX_CONTEXT_RATIO` ×
8
+ * 与父不同的模型 / 思考级别、父还没有真实请求、父上一次请求超过 `subagents.forkMaxContextRatio`(缺省 0.5)×
9
9
  * (窗口 − reserveTokens)、找不到发起调用的 assistant。续聊与 resume 不经过这里(照原文件重开)。
10
10
  * - `forkBrief`:角色说明 + 任务合成一条 user 消息(固定英文),追加在继承的历史之后;子会话不写
11
11
  * `role` 节,工具表与父逐字节相同,类型限制的工具在执行层拒绝(`unavailableTools`)。
@@ -16,8 +16,13 @@ import type { SessionManager } from "../session/manager.js";
16
16
  import type { SessionEntry } from "../session/types.js";
17
17
  import type { SubagentRequest } from "../tools/types.js";
18
18
  import type { AgentSessionOptions } from "./session-core.js";
19
- /** 父上一次请求的输入 token 超过(子模型窗口 − reserveTokens)的这个比例时回落 fresh。 */
19
+ /**
20
+ * 父上一次请求的输入 token 超过(子模型窗口 − reserveTokens)的这个比例时回落 fresh;
21
+ * 缺省值,可由 `subagents.forkMaxContextRatio` 覆盖(#149)。
22
+ */
20
23
  export declare const FORK_MAX_CONTEXT_RATIO = 0.5;
24
+ /** 配置的比例:非有限数或不在 (0, 1) 内时用缺省。 */
25
+ export declare function forkRatio(ratio: number | undefined): number;
21
26
  export type ContextMode = "fork" | "fresh";
22
27
  /** 请求的上下文模式:调用参数 > 类型 frontmatter > `fresh`。 */
23
28
  export declare function requestedContext(request: Pick<SubagentRequest, "context">, agent: Pick<AgentDefinition, "context">): ContextMode;
@@ -36,7 +41,7 @@ export interface ForkParent {
36
41
  export interface ForkSpec {
37
42
  taskId: string;
38
43
  agent: Pick<AgentDefinition, "name">;
39
- request: Pick<SubagentRequest, "parentToolCallId" | "description">;
44
+ request: Pick<SubagentRequest, "parentToolCallId" | "description" | "prompt">;
40
45
  /** 子会话 cwd(隔离时是 worktree)。 */
41
46
  cwd: string;
42
47
  }
@@ -48,9 +53,9 @@ export type ForkPlan = {
48
53
  };
49
54
  /**
50
55
  * D3 的回落条件依次检查,全部通过才复制分支(复制即落盘:父有文件时子会话文件立即写出)。
51
- * `model` / `thinking` 是子会话按类型与参数解析出的值。
56
+ * `model` / `thinking` 是子会话按类型与参数解析出的值;`ratio` 是 `subagents.forkMaxContextRatio`。
52
57
  */
53
- export declare function forkPlan(parent: ForkParent, spec: ForkSpec, model: Model, thinking: ModelThinkingLevel): ForkPlan;
58
+ export declare function forkPlan(parent: ForkParent, spec: ForkSpec, model: Model, thinking: ModelThinkingLevel, ratio?: number): ForkPlan;
54
59
  export interface ForkBriefInput {
55
60
  prompt: string;
56
61
  /** 类型正文(不含通用前言:这里的首句已说明子 Agent 身份)。 */
@@ -5,14 +5,24 @@
5
5
  * 同一条 assistant 里并行的多个 fork 任务共享同一个 fork 点。
6
6
  * - `forkPlan`:满足条件时用 `SessionManager.fork(entryId, { head })` 复制分支,`head` 是
7
7
  * `custom{ama.task, context:"fork", forkedFrom}`(子会话首条仍是 `ama.task`);否则给出回落原因:
8
- * 与父不同的模型 / 思考级别、父还没有真实请求、父上一次请求超过 `FORK_MAX_CONTEXT_RATIO` ×
8
+ * 与父不同的模型 / 思考级别、父还没有真实请求、父上一次请求超过 `subagents.forkMaxContextRatio`(缺省 0.5)×
9
9
  * (窗口 − reserveTokens)、找不到发起调用的 assistant。续聊与 resume 不经过这里(照原文件重开)。
10
10
  * - `forkBrief`:角色说明 + 任务合成一条 user 消息(固定英文),追加在继承的历史之后;子会话不写
11
11
  * `role` 节,工具表与父逐字节相同,类型限制的工具在执行层拒绝(`unavailableTools`)。
12
12
  */
13
+ import { FORK_CUSTOM_TYPE, taskLine } from "../tools/task.js";
13
14
  import { DEFAULT_COMPACTION_SETTINGS } from "./session-compaction.js";
14
- /** 父上一次请求的输入 token 超过(子模型窗口 − reserveTokens)的这个比例时回落 fresh。 */
15
+ /**
16
+ * 父上一次请求的输入 token 超过(子模型窗口 − reserveTokens)的这个比例时回落 fresh;
17
+ * 缺省值,可由 `subagents.forkMaxContextRatio` 覆盖(#149)。
18
+ */
15
19
  export const FORK_MAX_CONTEXT_RATIO = 0.5;
20
+ /** 配置的比例:非有限数或不在 (0, 1) 内时用缺省。 */
21
+ export function forkRatio(ratio) {
22
+ return ratio !== undefined && Number.isFinite(ratio) && ratio > 0 && ratio < 1
23
+ ? ratio
24
+ : FORK_MAX_CONTEXT_RATIO;
25
+ }
16
26
  const TASK_CUSTOM_TYPE = "ama.task";
17
27
  /** 请求的上下文模式:调用参数 > 类型 frontmatter > `fresh`。 */
18
28
  export function requestedContext(request, agent) {
@@ -27,9 +37,9 @@ export function forkPoint(branch, parentToolCallId) {
27
37
  }
28
38
  /**
29
39
  * D3 的回落条件依次检查,全部通过才复制分支(复制即落盘:父有文件时子会话文件立即写出)。
30
- * `model` / `thinking` 是子会话按类型与参数解析出的值。
40
+ * `model` / `thinking` 是子会话按类型与参数解析出的值;`ratio` 是 `subagents.forkMaxContextRatio`。
31
41
  */
32
- export function forkPlan(parent, spec, model, thinking) {
42
+ export function forkPlan(parent, spec, model, thinking, ratio) {
33
43
  const base = parent.childBase();
34
44
  if (model.provider !== base.model.provider || model.id !== base.model.id)
35
45
  return { fallback: `model ${model.provider}/${model.id} differs from the parent` };
@@ -40,7 +50,7 @@ export function forkPlan(parent, spec, model, thinking) {
40
50
  return { fallback: "the parent has not sent a request yet" };
41
51
  const reserve = parent.options.compaction?.reserveTokens ?? DEFAULT_COMPACTION_SETTINGS.reserveTokens;
42
52
  const window = model.contextWindow;
43
- const limit = window === undefined ? Infinity : FORK_MAX_CONTEXT_RATIO * Math.max(0, window - reserve);
53
+ const limit = window === undefined ? Infinity : forkRatio(ratio) * Math.max(0, window - reserve);
44
54
  if (last.promptTokens > limit)
45
55
  return { fallback: `parent context ${last.promptTokens} tokens exceeds ${Math.floor(limit)}` };
46
56
  const forkedFrom = forkPoint(parent.manager.branch(), spec.request.parentToolCallId);
@@ -63,6 +73,12 @@ export function forkPlan(parent, spec, model, thinking) {
63
73
  },
64
74
  },
65
75
  });
76
+ // #191:fork 标记(不进上下文);复制来的父 `ama.task` 快照可能更晚,故不复用那个类型
77
+ manager.append({
78
+ type: "custom",
79
+ customType: FORK_CUSTOM_TYPE,
80
+ data: { taskLine: taskLine(spec.request.prompt) },
81
+ });
66
82
  return { manager, forkedFrom };
67
83
  }
68
84
  /** fork 子会话的首条 user 消息(固定英文,发给模型)。 */
@@ -72,7 +88,7 @@ export function forkBrief(input) {
72
88
  "You are a sub-agent forked from the conversation above at this point. The main agent cannot " +
73
89
  "see your work, only your final reply; do not delegate further. Requests above were for the " +
74
90
  "main agent: do only this task. Instructions in this block take precedence over earlier " +
75
- "plans or reminders above.",
91
+ "plans or reminders above. Do not call task or task_ctl.",
76
92
  ];
77
93
  if (input.worktree !== undefined)
78
94
  lines.push(`Working directory for this task: ${input.worktree.cwd}. Relative paths in the ` +
@@ -62,7 +62,7 @@ export declare function registryOf(sessionId: string): SubagentRegistry | undefi
62
62
  export declare function subagentRegistryFor(host: RegistryHost, env?: SubagentEnvironment): SubagentRegistry;
63
63
  export declare class SubagentRegistry implements TaskControl {
64
64
  private readonly host;
65
- private readonly env;
65
+ readonly env: SubagentEnvironment;
66
66
  readonly catalog: AgentCatalog;
67
67
  private readonly pool;
68
68
  private readonly tasks;
@@ -245,7 +245,7 @@ export class SubagentRegistry {
245
245
  const file = this.outputFileFor(record.info.taskId);
246
246
  if (background) {
247
247
  void running.then((result) => this.notifier.notify(record, result));
248
- return Promise.resolve(startedResult(record, file));
248
+ return startedResult(record, file, running);
249
249
  }
250
250
  // [W7-B1] 前台:转后台(background())时等待者先行 resolve,工具调用立即返回
251
251
  const { waiter, promise } = foregroundWaiter(() => {
@@ -14,6 +14,7 @@ export interface AgentModelConfig {
14
14
  agents?: AgentsConfig;
15
15
  subagents?: {
16
16
  defaultModel?: string;
17
+ forkMaxContextRatio?: number;
17
18
  };
18
19
  models?: ModelsConfig;
19
20
  }
@@ -10,7 +10,9 @@
10
10
  * - 出错恢复(每种至多一次):401 → 强制刷新重试;SIWC 400 `subscription_sharing_unsupported_capability` → 删去
11
11
  * `error.param` 重试;codex 400 `Instructions are not valid` → 本会话切 developer-message 重试;
12
12
  * - 错误映射(文案带码前缀,宿主按码判断):429 配额 → `quota_exceeded`(不重试);失效 → `auth_expired`;
13
- * SIWC 403 `subscription_sharing_user_not_eligible` → `not_eligible`(i18n 文案列出原因);503 留给会话层现有重试;
13
+ * SIWC 403 `subscription_sharing_user_not_eligible` → `not_eligible`(i18n 文案列出原因);codex 400「model is not
14
+ * supported when using Codex with a ChatGPT account」→ `model_unavailable`(发现缓存过期时常见,提示刷新);
15
+ * 503 留给会话层现有重试;
14
16
  * - 渠道跟随登录方式由会话层做(auth/chatgpt/follow.ts);到这里仍不符说明用户显式写了 `@渠道`,报
15
17
  * `chatgpt_flavor_mismatch`(i18n);
16
18
  * - 用量:`cost = 0`、`billing: "subscription"`。
@@ -10,7 +10,9 @@
10
10
  * - 出错恢复(每种至多一次):401 → 强制刷新重试;SIWC 400 `subscription_sharing_unsupported_capability` → 删去
11
11
  * `error.param` 重试;codex 400 `Instructions are not valid` → 本会话切 developer-message 重试;
12
12
  * - 错误映射(文案带码前缀,宿主按码判断):429 配额 → `quota_exceeded`(不重试);失效 → `auth_expired`;
13
- * SIWC 403 `subscription_sharing_user_not_eligible` → `not_eligible`(i18n 文案列出原因);503 留给会话层现有重试;
13
+ * SIWC 403 `subscription_sharing_user_not_eligible` → `not_eligible`(i18n 文案列出原因);codex 400「model is not
14
+ * supported when using Codex with a ChatGPT account」→ `model_unavailable`(发现缓存过期时常见,提示刷新);
15
+ * 503 留给会话层现有重试;
14
16
  * - 渠道跟随登录方式由会话层做(auth/chatgpt/follow.ts);到这里仍不符说明用户显式写了 `@渠道`,报
15
17
  * `chatgpt_flavor_mismatch`(i18n);
16
18
  * - 用量:`cost = 0`、`billing: "subscription"`。
@@ -220,6 +222,13 @@ export function recoverChatGptError(error, model, compat, options, apiKey, tried
220
222
  return { kind: "retry" };
221
223
  }
222
224
  }
225
+ if (backend === "codex" &&
226
+ error.status === 400 &&
227
+ /not supported when using codex with a chatgpt account/i.test(error.body))
228
+ return {
229
+ kind: "fail",
230
+ error: new Error(`model_unavailable: ${msg().auth.request.modelUnavailable(`${provider}/${model.id}`)}`),
231
+ };
223
232
  if (backend === "codex" &&
224
233
  error.status === 400 &&
225
234
  /instructions are not valid/i.test(error.body) &&
@@ -8,6 +8,8 @@
8
8
  * 以上限重发一次;之后同一模型的请求直接用上限。Anthropic「输入 + max_tokens 超出上下文」只按这次
9
9
  * 的输入算出可用值重发、不记上限;可用值不足 `MIN_OUTPUT_TOKENS` 判为溢出(overflow.ts 认得该文案)。
10
10
  * 重发只发生在 `start` 之前,流契约不变(与 `postWithCacheFallback` 同模式,包在它外层)。
11
+ * - 跨进程:学到的上限经 `onMaxTokensCap` 写进 `<dataDir>/models/max-tokens-caps.json`(30 天),
12
+ * 组装注册表时载回(providers/max-tokens-cache.ts,#152)。
11
13
  */
12
14
  import { type PostOptions } from "../http.js";
13
15
  import type { Model } from "../types.js";
@@ -19,6 +21,8 @@ export declare const OUTPUT_HEADROOM_TOKENS = 2048;
19
21
  export type MaxTokensField = "max_tokens" | "max_completion_tokens" | "max_output_tokens";
20
22
  /** `${provider}/${model}` → 已知上限(被动修正时记入,之后的请求直接用)。 */
21
23
  export declare const maxTokensCaps: Map<string, number>;
24
+ /** 被动修正学到新上限时通知(数据目录持久化用,max-tokens-cache.ts);返回取消函数。 */
25
+ export declare function onMaxTokensCap(listener: (key: string, cap: number) => void): () => void;
22
26
  type Json = Record<string, unknown>;
23
27
  /** 主动收紧后的 max_tokens;`window` 未知或 `fixed`(预算型思考)时返回 requested。 */
24
28
  export declare function clampMaxTokens(requested: number, window: number | undefined, estimatedInput: number, fixed: boolean): number;
@@ -8,6 +8,8 @@
8
8
  * 以上限重发一次;之后同一模型的请求直接用上限。Anthropic「输入 + max_tokens 超出上下文」只按这次
9
9
  * 的输入算出可用值重发、不记上限;可用值不足 `MIN_OUTPUT_TOKENS` 判为溢出(overflow.ts 认得该文案)。
10
10
  * 重发只发生在 `start` 之前,流契约不变(与 `postWithCacheFallback` 同模式,包在它外层)。
11
+ * - 跨进程:学到的上限经 `onMaxTokensCap` 写进 `<dataDir>/models/max-tokens-caps.json`(30 天),
12
+ * 组装注册表时载回(providers/max-tokens-cache.ts,#152)。
11
13
  */
12
14
  import { HttpError, errorText } from "../http.js";
13
15
  import { postWithCacheFallback } from "./cache-params.js";
@@ -17,6 +19,12 @@ export const MIN_OUTPUT_TOKENS = 1024;
17
19
  export const OUTPUT_HEADROOM_TOKENS = 2048;
18
20
  /** `${provider}/${model}` → 已知上限(被动修正时记入,之后的请求直接用)。 */
19
21
  export const maxTokensCaps = new Map();
22
+ const capListeners = new Set();
23
+ /** 被动修正学到新上限时通知(数据目录持久化用,max-tokens-cache.ts);返回取消函数。 */
24
+ export function onMaxTokensCap(listener) {
25
+ capListeners.add(listener);
26
+ return () => capListeners.delete(listener);
27
+ }
20
28
  /** 主动收紧后的 max_tokens;`window` 未知或 `fixed`(预算型思考)时返回 requested。 */
21
29
  export function clampMaxTokens(requested, window, estimatedInput, fixed) {
22
30
  if (fixed || window === undefined || !Number.isFinite(window) || window <= 0)
@@ -111,8 +119,11 @@ export async function postWithMaxTokensFallback(model, url, options, field) {
111
119
  throw error;
112
120
  if (rejection.cap >= sent || !budgetFits(body, rejection.cap))
113
121
  throw error;
114
- if (!rejection.transient)
122
+ if (!rejection.transient) {
115
123
  maxTokensCaps.set(key, rejection.cap);
124
+ for (const listener of capListeners)
125
+ listener(key, rejection.cap);
126
+ }
116
127
  return postWithCacheFallback(model, url, {
117
128
  ...options,
118
129
  body: withCap(body, field, rejection.cap),
@@ -25,6 +25,8 @@ export interface ModelMetadata {
25
25
  toolCall?: boolean;
26
26
  /** [ME-D] 按 id 继承的内置目录条目 `provider/id`。 */
27
27
  catalog?: string;
28
+ /** 显式 `catalog` / `modelsDev` 引用不命中时的告警(注册表转成 warnings,#154)。 */
29
+ warnings?: string[];
28
30
  }
29
31
  export type ModelsDevSource = ModelsDevIndex | (() => ModelsDevIndex | undefined) | undefined;
30
32
  /** 惰性取索引:只有真的要补字段时才读缓存(启动时没有自定义模型就不读约 2 MB 的缓存)。 */
@@ -32,9 +34,11 @@ export declare function lazyIndex(source: ModelsDevSource): () => ModelsDevIndex
32
34
  type EnrichedEntry = Omit<ModelConfig, "modelsDev" | "channels" | "catalog">;
33
35
  /**
34
36
  * 补全一个自定义模型条目(config 的 `models[]` 或合成的模型)。返回补过字段的条目(不含
35
- * `modelsDev` / `channels` / `catalog` 这些配置专用键)与每个字段的来源。
37
+ * `modelsDev` / `channels` / `catalog` 这些配置专用键)与每个字段的来源。显式 `catalog: "provider/id"`
38
+ * 或 `modelsDev` 引用不命中时记入 `metadata.warnings`(`providerId` 用于告警文案);写错的 `catalog`
39
+ * 不回落按 id 继承——用户显式指定,按指定失败处理。
36
40
  */
37
- export declare function enrichEntry(entry: ModelConfig, index: () => ModelsDevIndex | undefined): {
41
+ export declare function enrichEntry(entry: ModelConfig, index: () => ModelsDevIndex | undefined, providerId?: string): {
38
42
  entry: EnrichedEntry;
39
43
  metadata: ModelMetadata;
40
44
  };
@@ -79,9 +79,11 @@ function fillLimits(out, hit, sources) {
79
79
  }
80
80
  /**
81
81
  * 补全一个自定义模型条目(config 的 `models[]` 或合成的模型)。返回补过字段的条目(不含
82
- * `modelsDev` / `channels` / `catalog` 这些配置专用键)与每个字段的来源。
82
+ * `modelsDev` / `channels` / `catalog` 这些配置专用键)与每个字段的来源。显式 `catalog: "provider/id"`
83
+ * 或 `modelsDev` 引用不命中时记入 `metadata.warnings`(`providerId` 用于告警文案);写错的 `catalog`
84
+ * 不回落按 id 继承——用户显式指定,按指定失败处理。
83
85
  */
84
- export function enrichEntry(entry, index) {
86
+ export function enrichEntry(entry, index, providerId) {
85
87
  const { modelsDev, channels: _channels, catalog: _catalog, ...rest } = entry;
86
88
  const sources = {};
87
89
  for (const field of ENRICHABLE_FIELDS) {
@@ -89,11 +91,16 @@ export function enrichEntry(entry, index) {
89
91
  }
90
92
  const metadata = { sources, looked: false };
91
93
  const out = { ...rest };
94
+ const warnings = [];
95
+ const ref = `${providerId ?? "?"}/${entry.id}`;
92
96
  const hit = catalogHit(entry);
93
97
  if (hit !== undefined) {
94
98
  metadata.catalog = hit.ref;
95
99
  inheritIntrinsic(out, hit, sources);
96
100
  }
101
+ else if (typeof entry.catalog === "string") {
102
+ warnings.push(`catalog "${entry.catalog}" for "${ref}" not found; ignored`);
103
+ }
97
104
  const explicit = typeof modelsDev === "string" ? modelsDev : hit?.snapshotRef;
98
105
  const lookup = modelsDev !== false && (needsLookup(out) || modelsDev !== undefined);
99
106
  const idx = lookup ? index() : undefined;
@@ -127,8 +134,12 @@ export function enrichEntry(entry, index) {
127
134
  }
128
135
  }
129
136
  }
137
+ if (typeof modelsDev === "string" && metadata.looked && metadata.match === undefined)
138
+ warnings.push(`modelsDev "${modelsDev}" for "${ref}" not found`);
130
139
  if (hit !== undefined && modelsDev !== false)
131
140
  fillLimits(out, hit, sources);
141
+ if (warnings.length > 0)
142
+ metadata.warnings = warnings;
132
143
  return { entry: out, metadata };
133
144
  }
134
145
  /**
@@ -0,0 +1,22 @@
1
+ /**
2
+ * `max_tokens` 上限的跨进程缓存:`<dataDir>/models/max-tokens-caps.json`(#152)。
3
+ *
4
+ * 被动修正(apis/max-tokens.ts)学到的 `provider/model` 上限原本只在进程内,重启后每个模型的首个请求仍被
5
+ * 400 拒一次。这里把它写进数据目录:组装注册表时载回进程内的表(进程内已有的键优先),学到新上限就把文件里
6
+ * 未过期的条目加上这一条原子写回(临时文件 + 改名;并发进程后写者赢,丢的一条下次再学)。条目 30 天过期,
7
+ * 过期后重新探测(中转放宽上限时不会一直压低输出);删掉文件即重测。只存模型引用、上限与时间,没有凭据。
8
+ */
9
+ export declare const MAX_TOKENS_CACHE_VERSION = 1;
10
+ /** 条目有效期:30 天。 */
11
+ export declare const MAX_TOKENS_CACHE_TTL_MS: number;
12
+ /** 缓存文件位置。 */
13
+ export declare function maxTokensCachePath(dataDir: string): string;
14
+ /** 载入未过期的上限到进程内的表(已有的键不覆盖);返回载入条数。 */
15
+ export declare function loadMaxTokensCaps(dataDir: string, now?: () => number): number;
16
+ /** 把一条新上限并入文件(顺带清掉过期条目);写失败不抛(缓存只是省一次 400)。 */
17
+ export declare function recordMaxTokensCap(dataDir: string, key: string, cap: number, now?: () => number): void;
18
+ /**
19
+ * 载入并订阅:之后学到的上限写回该数据目录。按 `dataDir` 幂等(同一进程重复组装注册表不重复订阅);
20
+ * 返回取消函数(取消后可重新 attach)。
21
+ */
22
+ export declare function attachMaxTokensCache(dataDir: string, now?: () => number): () => void;