@epoch-agent/core 0.8.1 → 0.10.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 (4) hide show
  1. package/README.md +28 -0
  2. package/dist/index.d.ts +1825 -177
  3. package/dist/index.js +3287 -2562
  4. package/package.json +3 -3
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { ToolProvider, EpochTool, EpochPlugin, ToolExposure, EpochSkill, HookType, EpochHook, PluginContext, QuestionRequest, QuestionAnswer, AgentRole, PermissionLevel, PermissionManager as PermissionManager$1, PlanProposal, ApprovalRequest, ApprovalReply, ScheduleDefinition, DiagnosticSink, AgentRoleSource, CustomCommandDef, ModelRef, ExpandedCommand, CustomCommandSource, BackgroundTaskInfo, ContextBreakdown, EpochMessage, AgentRoleScope, ComplianceCategory, ComplianceAction, ToolArtifactInput, ToolArtifact, TrustLevel, TrustManager as TrustManager$1, TokenUsage, EpochConfig, BudgetBreach, ProviderType, JsonSchema, ModelSelection, EpochToolCall, Telemetry, HookManager as HookManager$1, EpochUserContent, EpochContentPart, AgentEvent, SelectionContext, SetSelectionResult, PermissionRuleLists, KeybindingTable, Diagnostic, KeybindingReport, FileOmitReason, SessionOmitReason, EpochToolResult, EpochTurnFacts, LocalizedDetail, OperationType, RuleValue, Operation, ApprovalOutcome, PermissionDecision, ScheduleTrigger, ScheduleAllowlist, ScheduleBackendKind, ScheduleRunStatus, ScheduleRun, SchedulePendingApproval, ScheduleDraft, ScheduleWideningKind, WireAgentEvent, WireAgentEnvelope, ScheduleRunnerSpec, HookEvent, LogFn, HookRegistration, HookHandler, HookContext, VerifyResult, Lang, EpochFilePart, EpochSessionPart, MentionSet, EpochImagePart, DecidedTrustLevel, TrustScope, TrustRecord } from '@epoch-agent/protocol';
2
- export { API_KEY_ENV_VARS, AgentEvent, AgentEventOf, AgentEventType, ApprovalAnswer, ApprovalOutcome, ApprovalReply, ApprovalRequest, BudgetBreach, COMPLIANCE_ACTIONS, COMPLIANCE_CATEGORIES, ComplianceAction, ComplianceCategory, DecidedTrustLevel, EpochConfig, EpochHook, EpochPlugin, EpochSkill, EpochTool, FinishReason, HookContext, HookEvent, HookHandler, HookRegistration, HookType, HookManager as IHookManager, PermissionManager as IPermissionManager, TrustManager as ITrustManager, JsonSchema, MAX_ATTACH_BYTES, MAX_ATTACH_FILES, MAX_ATTACH_TOTAL_BYTES, MAX_SESSION_ATTACH_BYTES, MAX_SESSION_REFS, OPERATION_TYPES, Operation, OperationType, PERMISSION_LEVELS, PLAN_OUTCOMES, PROVIDER_INFOS, PROVIDER_TYPES, PermissionDecision, PermissionLevel, PlanApprovalOutcome, PlanProposal, PluginContext, ProviderInfo, ProviderType, TokenUsage, ToolContext, ToolDefinition, ToolExposure, ToolResult, TrustLevel, TrustRecord, TrustScope, VerifyResult, apiKeyEnvVar, extractAllMentions, extractMentions, extractSessionMentions, getProviderInfo, isOperationType, isPermissionLevel, isPlanOutcome, isProviderType, normalizeApproval } from '@epoch-agent/protocol';
1
+ import { ToolProvider, EpochTool, EpochPlugin, ToolExposure, EpochSkill, HookType, EpochHook, PluginContext, QuestionRequest, QuestionAnswer, AgentRole, PermissionLevel, PermissionManager as PermissionManager$1, PlanProposal, ApprovalRequest, ApprovalReply, ScheduleDefinition, DiagnosticSink, AgentRoleSource, CustomCommandDef, ModelRef, ExpandedCommand, CustomCommandSource, BackgroundTaskInfo, ContextBreakdown, EpochMessage, AgentRoleScope, Lang, WireMcpConnector, ComplianceCategory, ComplianceAction, ToolArtifactInput, ToolArtifact, TrustLevel, TrustManager as TrustManager$1, TokenUsage, EpochConfig, BudgetBreach, ProviderType, JsonSchema, ModelSelection, EpochToolCall, Telemetry, HookManager as HookManager$1, EpochUserContent, EpochContentPart, AgentEvent, SelectionContext, SetSelectionResult, MoneyDigits, CurrencyDisplay, PermissionRuleLists, KeybindingTable, Diagnostic, KeybindingReport, FileOmitReason, SessionOmitReason, EpochToolResult, EpochTurnFacts, LocalizedDetail, OperationType, RuleValue, Operation, ApprovalOutcome, PermissionDecision, ScheduleTrigger, ScheduleAllowlist, ScheduleBackendKind, ScheduleRunStatus, ScheduleRun, SchedulePendingApproval, ScheduleDraft, ScheduleWideningKind, WireAgentEvent, WireAgentEnvelope, ScheduleRunnerSpec, HookEvent, LogFn, HookRegistration, HookHandler, HookContext, VerifyResult, EpochFilePart, EpochSessionPart, MentionSet, EpochImagePart, TrustExecGrant, DecidedTrustLevel, TrustScope, TrustGrant, TrustRecord } from '@epoch-agent/protocol';
2
+ export { API_KEY_ENV_VARS, AgentEvent, AgentEventOf, AgentEventType, ApprovalAnswer, ApprovalOutcome, ApprovalReply, ApprovalRequest, BudgetBreach, COMPLIANCE_ACTIONS, COMPLIANCE_CATEGORIES, ComplianceAction, ComplianceCategory, DecidedTrustLevel, EpochConfig, EpochHook, EpochPlugin, EpochSkill, EpochTool, FinishReason, HookContext, HookEvent, HookHandler, HookRegistration, HookType, HookManager as IHookManager, PermissionManager as IPermissionManager, TrustManager as ITrustManager, JsonSchema, MAX_ATTACH_BYTES, MAX_ATTACH_FILES, MAX_ATTACH_TOTAL_BYTES, MAX_SESSION_ATTACH_BYTES, MAX_SESSION_REFS, OPERATION_TYPES, Operation, OperationType, PERMISSION_LEVELS, PLAN_OUTCOMES, PROVIDER_INFOS, PROVIDER_TYPES, PermissionDecision, PermissionLevel, PlanApprovalOutcome, PlanProposal, PluginContext, ProviderInfo, ProviderType, TokenUsage, ToolContext, ToolDefinition, ToolExposure, ToolResult, TrustExecGrant, TrustGrant, TrustLevel, TrustRecord, TrustScope, TrustTier, VerifyResult, apiKeyEnvVar, extractAllMentions, extractMentions, extractSessionMentions, getProviderInfo, isOperationType, isPermissionLevel, isPlanOutcome, isProviderType, normalizeApproval } from '@epoch-agent/protocol';
3
3
  import { IsolationBackend, SqliteDatabase, ParseIssue, ShellFlavor } from '@epoch-agent/infra';
4
4
  export { BubblewrapOptions, DangerMatch, IS_WINDOWS, IsolationBackend, IsolationOptions, LogLevel, SeatbeltOptions, WINDOWS_HIDE_FLAGS, buildBwrapArgs, buildProfile, checkDangerousCommand, checkObfuscation, createLogger, detectBackend, getDefaultShell, getPythonCommand, isInWorkspace, isReadOnlyCommand, isolate, maskApiKey, probeBubblewrap, probeSeatbelt, complianceDir as resolveComplianceDir, policiesDir as resolvePoliciesDir, setLogLevel } from '@epoch-agent/infra';
5
5
  import { z } from 'zod';
@@ -1369,6 +1369,16 @@ interface MemorySearchResult {
1369
1369
  score: number;
1370
1370
  similarity?: number;
1371
1371
  }
1372
+ /** {@link MemoryManager.read} 的结果。**认不出的 id 单独一栏**,不是静默丢掉 */
1373
+ interface MemoryReadResult {
1374
+ found: Array<{
1375
+ id: string;
1376
+ target: MemoryTarget;
1377
+ content: string;
1378
+ }>;
1379
+ /** 索引里没有这个 id。多半是模型自己编了一个,或者引用了上个会话的索引 */
1380
+ missing: string[];
1381
+ }
1372
1382
  /** 数据库中的记忆条目 */
1373
1383
  interface MemoryEntry {
1374
1384
  id: string;
@@ -1382,8 +1392,15 @@ interface MemoryEntry {
1382
1392
  lastAccessedAt: Date;
1383
1393
  }
1384
1394
  interface MemoryConfig {
1395
+ /** `MEMORY.md` 能存多少字符。**存储**配额,不进 prompt,抬它几乎不花钱 */
1385
1396
  memoryCharLimit: number;
1397
+ /** `USER.md` 能存多少字符。同上 */
1386
1398
  userCharLimit: number;
1399
+ /**
1400
+ * 索引块进 system prompt 的字符上限 —— 三个数里**唯一按轮计费**的那个。
1401
+ * 对照表在 protocol 的 `EpochConfig.memory` 上。
1402
+ */
1403
+ indexCharLimit: number;
1387
1404
  }
1388
1405
  interface MemoryManager$1 {
1389
1406
  /** 会话启动:加载文件 → 去重 → 注入扫描 → 冻结快照 → 返回 system prompt 块 */
@@ -1397,9 +1414,15 @@ interface MemoryManager$1 {
1397
1414
  search(query: string, limit?: number): Promise<MemorySearchResult[]>;
1398
1415
  keywordSearch(query: string, limit?: number): Promise<MemorySearchResult[]>;
1399
1416
  semanticSearch(query: string, limit?: number): Promise<MemorySearchResult[]>;
1417
+ /** 按索引里的短 id 取全文。索引块印的 id 就是这里认的那一套 */
1418
+ read(ids: readonly string[]): Promise<MemoryReadResult>;
1400
1419
  add(target: MemoryTarget, content: string): Promise<MemoryWriteResult>;
1401
1420
  replace(target: MemoryTarget, oldText: string, content: string): Promise<MemoryWriteResult>;
1402
- remove(target: MemoryTarget, oldText: string): Promise<void>;
1421
+ /**
1422
+ * 删一条。`ref` 是**全文**或索引里那个**短 id**。
1423
+ * 返回删没删掉 —— 报「已删除」而其实一条没动,会让模型以为腾出了地方。
1424
+ */
1425
+ remove(target: MemoryTarget, ref: string): Promise<boolean>;
1403
1426
  getBudget(): {
1404
1427
  memory: BudgetInfo;
1405
1428
  user: BudgetInfo;
@@ -1412,10 +1435,29 @@ interface MemoryManager$1 {
1412
1435
  /**
1413
1436
  * memory 工具 — 让 LLM 显式读写长期记忆。
1414
1437
  *
1415
- * MemoryManager.sync() 不自动写入(见 manager.ts:97),记忆的产生完全依赖
1438
+ * MemoryManager.sync() 不自动写入(见 manager.ts),记忆的产生完全依赖
1416
1439
  * 本工具被调用。没有它,MemoryManager 只能读不能写。
1417
1440
  *
1418
- * 对标 Hermes tools/memory_tool.py 的 action 分发形式。
1441
+ * ---
1442
+ *
1443
+ * # ⚠️ 改这个文件里的 `description` / `parameters` = 34 段 eval 录像作废
1444
+ *
1445
+ * 工具定义(名字 / 描述 / 参数)进 eval 的新鲜度指纹
1446
+ * (`evals/harness/fingerprint.ts`,那个文件头说得很清楚为什么必须进)。
1447
+ * 于是这里改一个字,`pnpm eval` 全红,而 pre-push 会跑它。
1448
+ *
1449
+ * **2026-09-10 那一轮是明知代价改的**:`read` 这个 action 是记忆结构改造
1450
+ * (正文不再全量进 prompt,见 `memory/manager.ts` 文件头)**必需**的另一半 ——
1451
+ * 没有它,索引块里那些 id 一个也取不回全文,那不是「少一个便利」,
1452
+ * 是把记忆变成只有摘要。
1453
+ *
1454
+ * 收账走 `evals/README.md` 里那条**「只重贴指纹」的窄例外**(它成立的前提是
1455
+ * 「改的是工具表增量、system prompt 没动」,而这一轮 system prompt 那一侧
1456
+ * 恰好没动 —— 空库的索引块是空串,判据在 `manager.ts` 文件头最后一节)。
1457
+ * 例外带**举证义务**:拿真 key 对最可能被干扰的那几条 eval 录活体,
1458
+ * 数 `memory` 在 `toolCalls` 里出现几次,必须是 0。
1459
+ *
1460
+ * ⚠️ 下次再想在这儿加一句话之前先问:**非说在工具描述里不可吗**。
1419
1461
  */
1420
1462
 
1421
1463
  declare function createMemoryTool(memory: MemoryManager$1): EpochTool;
@@ -1845,91 +1887,6 @@ interface PlanModeDeps {
1845
1887
  */
1846
1888
  declare function createPlanModeTools(deps: PlanModeDeps): EpochTool[];
1847
1889
 
1848
- /**
1849
- * `plugin_validate` / `plugin_pack` —— 模型**做一个插件包**的那两条路(2026-09-09)。
1850
- *
1851
- * ## 一、⚠️ 刻意**没有** `plugin_install`,而那是这个文件的正题
1852
- *
1853
- * 装一个插件会往用户机器上落下 `hooks.json`(每一条都是会 spawn 的 shell 命令)
1854
- * 和 `mcp.json`(stdio 那档同样起子进程)。整套安装链上给人的那道闸门就是
1855
- * 「装之前看一屏预览再确认」(`core/src/plugin/install.ts` 的 `confirm`),
1856
- * 而把安装交给模型等于**让被审批的一方自己按下确认**。
1857
- *
1858
- * 所以这两个工具在**打出那个包**那一步停住,回执里明写交接点:
1859
- * `epoch plugin install <zip>`(本机压缩包那一档是现成的路,见 `source.ts`)。
1860
- * ⚠️ **想加安装的时候要答的问题是「模型给用户装了一条 PreToolUse hook,
1861
- * 他从哪儿知道」**,而不是「能力层已经有这个函数了」——
1862
- * 判据同 `skill-write.ts` 第三节对 `delete` 的处置。
1863
- *
1864
- * ## 二、为什么值得有这两个工具(模型手上明明有 `file_write`)
1865
- *
1866
- * 因为它们恰好是 `file_write` 做不对的那两件:
1867
- *
1868
- * | 模型要答的问题 | 只有 file_write 时 | 有了这两个 |
1869
- * | -------------------------- | ------------------------ | ------------------------------ |
1870
- * | 「我这个目录算不算插件」 | 猜,或者读一遍文档再猜 | {@link PLUGIN_VALIDATE_TOOL} |
1871
- * | 「装进去会带来什么」 | 答不上来 | 同上(六类扩展物盘点) |
1872
- * | 「打成包」 | 做不到(要起 tar) | {@link PLUGIN_PACK_TOOL} |
1873
- *
1874
- * 校验那一条**复用安装时那一份**(`readPluginManifest` + `scanPluginDir`),
1875
- * 和 `epoch plugin validate` 逐字节同一条判据 —— 于是「这里绿了装上去就不会因为
1876
- * 格式被拒」这句话对模型和对人是同一句。**不为模型另写一份校验**:两份的那天,
1877
- * 模型会照着一份不作数的规则改文件。
1878
- *
1879
- * ## 三、⚠️ `plugin_pack` 归 `file_write` 不归 `command`,虽然它 spawn `tar`
1880
- *
1881
- * 反过来那条判据(`runtime/src/mcp-tools.ts` 第一节:`mcp_add` 归 `command`,
1882
- * 因为「写一台 stdio server 就是配一条以后会被 spawn 的命令行」)在这里
1883
- * **结论相反**,而理由是同一条:
1884
- *
1885
- * - 那 argv 是定死的(`tar -c -f <zip> --format zip -C <dir> -T <list>`),
1886
- * **不过 shell**,模型能控的只有两条路径 —— 它配不出一条命令行;
1887
- * - `PermissionManager.ruleMatches` 按**类别**授权(规则到不了工具名这一级),
1888
- * 于是一条 `allow: plugin_pack` 如果归 `command`,顺带放行的是**任意进程启动**。
1889
- * 而用户在审批框里真正要判的是「把这个目录打成一个包写到那儿」,那是 `file_write`。
1890
- *
1891
- * ## 四、⚠️ 两个工具都吃工作区闸门(`isInWorkspace`),这是唯一一处边界
1892
- *
1893
- * `planPack` 会**递归走完整个目录**,`packPlugin` 会把它写成一个 zip ——
1894
- * 不卡边界的话这一对就是一条「读任意目录 + 写任意位置」的原语。
1895
- * 判定走 infra 那一份(全仓唯一,验收 #11),相对路径按 `ctx.workDir` 解析。
1896
- *
1897
- * ⚠️ 顺带还有一道**天然的**闸,比上面那道更早生效:`planPack` 第一句就是
1898
- * `readPluginManifest(dir)`,没有 `epoch-plugin.json` 直接拒。也就是说这条路
1899
- * 打不出「随便一个目录的包」—— 目标必须先是一个插件。
1900
- *
1901
- * ## 五、错误话术为什么是英文
1902
- *
1903
- * 判据逐字同 [skill-view.ts](./skill-view.js) 第七节。⚠️ 但**下一层的 `t()`
1904
- * 措辞原样转发**(`plugin_manifest.*` / `plugin_pack.*` 那几句)——
1905
- * 那些话里带着字段路径和文件路径,是模型下一步唯一的依据,翻一遍只会译丢。
1906
- * 口径同 `mcp-tools.ts` 第四节。
1907
- */
1908
-
1909
- /**
1910
- * 两个工具名。**这是同构位置的真源** —— 另外两处认它们:
1911
- * `permission/operation-type.ts`(`plugin_validate` 进 `FILE_READ_TOOLS`,
1912
- * `plugin_pack` 进 `FILE_WRITE_TOOLS`)、`docs/TOOLS.md` 顶上那张表
1913
- * (两处由根 `__tests__/tool-registration.test.ts` 守着)。
1914
- *
1915
- * ⚠️ **下面 `name:` 那几格写的是字面量,不是这些常量,别「顺手」统一** ——
1916
- * 判据全文在 `SKILL_VIEW_TOOL` 的 JSDoc 上。
1917
- */
1918
- declare const PLUGIN_VALIDATE_TOOL = "plugin_validate";
1919
- declare const PLUGIN_PACK_TOOL = "plugin_pack";
1920
- /**
1921
- * 建那两个插件写作工具。
1922
- *
1923
- * **一个服务都不要,所以无条件注册** —— 口径同 `createMcpTools` 那句「控制面在
1924
- * 就全部注册」。它们的边界不是「有没有依赖」,是权限管线 + 工作区闸门,
1925
- * 两者都在每一次调用时才生效。
1926
- *
1927
- * 交出一个数组而不是两个 `createXxxTool`:装配层那一侧是一行
1928
- * `registry.registerTools(...)`,分成两个等于让「再加一个」变成一次要改两个文件的
1929
- * 改动,而漏接的表现是那个工具静默不存在(判据同 `createSkillWriteTools`)。
1930
- */
1931
- declare function createPluginAuthorTools(): EpochTool[];
1932
-
1933
1890
  /**
1934
1891
  * `schedule_draft` —— 在对话里**起草**一条定时任务,交给人去审。
1935
1892
  *
@@ -4719,6 +4676,9 @@ declare function currentEpochVersion(): string;
4719
4676
  * 不如解完之后看一眼:**清单不在顶层、而顶层恰好只有一个目录**,就把那一层
4720
4677
  * 提上来。判据是「清单在哪」而不是「有几个条目」,所以一个本来就把
4721
4678
  * `epoch-plugin.json` 放在顶层、旁边还有一个 `commands/` 的正常包不会被误摊。
4679
+ *
4680
+ * ⚠️ 数「有几个条目」时 `__MACOSX/` 和 `.DS_Store` **不算**,判据见
4681
+ * `ARCHIVE_NOISE`:不剔它们的话,Mac 上右键压缩出来的包一律摊不平。
4722
4682
  */
4723
4683
  type ExtractOutcome = {
4724
4684
  ok: true;
@@ -5240,6 +5200,20 @@ declare function resolveSource(raw: string): ResolvedSource | undefined;
5240
5200
  *
5241
5201
  * 不是「警告之后照装」。带 `#sha256=` 的 URL 说明用户在乎可重现性,
5242
5202
  * 而一个内容变了的压缩包和一个被替换掉的压缩包,在这一层是同一件事。
5203
+ *
5204
+ * ## ⚠️ 装这一下**没有对应的模型工具**,而那是判断不是缺口
5205
+ *
5206
+ * 装一个插件会往用户机器上落下 `hooks.json`(每一条都是会 spawn 的 shell 命令)
5207
+ * 和 `mcp.json`(stdio 那档同样起子进程),而整条安装链上给人的闸门就是
5208
+ * {@link InstallOptions.confirm} 那一屏 —— **把安装交给模型等于让被审批的一方
5209
+ * 自己按下确认**。判据同 `tools/builtin/skill-write.ts` 第三节对 `delete` 的处置。
5210
+ *
5211
+ * 这条判断的家原来在 `tools/builtin/plugin-author.ts` 的文件头上,而那个文件
5212
+ * 2026-09-10 整个删了(`plugin_validate` / `plugin_pack` 移除,模型侧现在一个
5213
+ * 插件工具都没有)—— 搬到这儿是因为**它和那两个工具在不在没关系**:只要这条
5214
+ * 安装链还在,「要不要给模型一个 `plugin_install`」就还会被问一次。
5215
+ * ⚠️ 想加的时候要答的问题是**「模型给用户装了一条 hook,他从哪儿知道」**,
5216
+ * 而不是「能力层已经有这个函数了」。
5243
5217
  */
5244
5218
 
5245
5219
  interface InstallOptions {
@@ -5307,7 +5281,16 @@ interface InstallPreview {
5307
5281
  manifest: PluginManifest;
5308
5282
  source: ResolvedSource;
5309
5283
  inventory: PluginInventory;
5310
- /** 已经装过的同名插件;有值时这次安装会被拒(验收 #8) */
5284
+ /**
5285
+ * 已经装过的同名插件;有值时这次安装会被拒(验收 #8)。
5286
+ *
5287
+ * ⚠️ **`replace` 命中的那一条不算**(2026-09-10):`epoch plugin update` 走的
5288
+ * 就是「同名重装一遍」,那条记录是**被替换的那一个**,不是挡路的。判据在
5289
+ * {@link InstallOptions.replace} 上(「所以同名不是冲突」那句从一开始就写着)。
5290
+ *
5291
+ * 这一格是**结论**不是事实 —— 它答的是「这次会不会被拒」,不是「锁里有没有
5292
+ * 同名的」。写成后者的代价见 `runInstall` 里算它那两行上的注释。
5293
+ */
5311
5294
  conflict?: PluginRecord;
5312
5295
  /**
5313
5296
  * 清单里的 `epochVersion` 和当前引擎不匹配:**装得上,但加载器会跳过它**。
@@ -5378,10 +5361,10 @@ declare function installPlugin(source: string, opts: InstallOptions): Promise<In
5378
5361
  *
5379
5362
  * 环境变量单独用不了,两条:
5380
5363
  *
5381
- * 1. **MCP 那边给不了。** SDK 的 `StdioClientTransport` 在 `env` 给了值的时候
5382
- * **拿它当整份环境**(不与 `process.env` 合并)—— 我们为了塞一个变量而
5383
- * 构造一份 env,等于把没写 `env` 的 server 的 `PATH` 一起拿掉,
5384
- * 那是一个「配置没动、昨天还好好的、今天起不来」的失败形态;
5364
+ * 1. **配置文件里读不到它。** `mcp.json` 的 `args` 和 `hooks.json` 的 `command`
5365
+ * 是我们自己解析的字符串,没有任何一层会去展开 `$EPOCH_PLUGIN_ROOT` ——
5366
+ * 环境变量只对**已经跑起来的子进程**有意义,而这两处要解决的正是
5367
+ * 「怎么把子进程叫起来」;
5385
5368
  * 2. **Windows 上写法不通用。** hook 走 `shell: true`,那个 shell 在 win32 上是
5386
5369
  * `cmd.exe`:`$EPOCH_PLUGIN_ROOT` 在那儿不展开(要写 `%EPOCH_PLUGIN_ROOT%`)。
5387
5370
  * 一份想跨平台的 `hooks.json` 于是写不出一行两边都对的命令。
@@ -5392,12 +5375,21 @@ declare function installPlugin(source: string, opts: InstallOptions): Promise<In
5392
5375
  * 而 `${EPOCH_PLUGIN_ROOT}/scripts/check.py` 变成 `/scripts/check.py` 之后
5393
5376
  * 报的是「文件不存在」,一个字都不会提到插件。
5394
5377
  *
5395
- * ⚠️ hook 那一侧**额外**还注入一个同名环境变量(`command-runner.ts` 的
5396
- * `injectEpochEnv`),因为那一侧的 env 是**并到 `process.env` 上**的,没有第 1 条
5397
- * 那个风险;而脚本跑起来之后要找自己包里的**兄弟文件**时,拿环境变量比让
5398
- * 调用方把路径传进来省事。MCP 那一侧没有这一份,判据同上第 1 条 ——
5399
- * 真需要的作者自己在 `mcp.json` 里写 `"env": { "EPOCH_PLUGIN_ROOT": "${EPOCH_PLUGIN_ROOT}" }`,
5400
- * 那时整份 env 是他自己写的,`PATH` 该带什么由他负责。
5378
+ * ⚠️ 替换之外,**两侧都额外注入一个同名环境变量**:hook 那边是
5379
+ * `command-runner.ts` 的 `injectEpochEnv`,MCP 那边是
5380
+ * `runtime/src/plugin-mcp-servers.ts` 的 `withPluginRoot`(2026-09-11 补上的,
5381
+ * 那之前 MCP 这一侧是空的)。理由是替换只管得到「怎么把进程叫起来」,
5382
+ * 而脚本**跑起来之后**要找自己包里的兄弟文件时,拿一个环境变量比让调用方
5383
+ * 把路径当参数传进来省事。
5384
+ *
5385
+ * ⚠️ MCP 那一侧原来空着的理由(「SDK 在 `env` 有值时拿它当整份环境,注入会把
5386
+ * `PATH` 弄丢」)**是错的**:实读 `@modelcontextprotocol/sdk@1.30.0` 的
5387
+ * `client/stdio.js`,它是 `{ ...getDefaultEnvironment(), ...serverParams.env }`,
5388
+ * 并上去的。全文判据在 `withPluginRoot` 的 JSDoc 上。
5389
+ *
5390
+ * ⚠️ 但**别把它当成 server 能拿到完整环境**:SDK 只往下传一张白名单
5391
+ * (`DEFAULT_INHERITED_ENV_VARS`,posix 六个 / win32 十二个)再并上这一份 ——
5392
+ * 用户 export 的变量到不了 server,那条路要走得让作者在 `mcp.json` 里写 `env`。
5401
5393
  *
5402
5394
  * ## ⚠️ 只在**插件带来的**那几份文件上替换
5403
5395
  *
@@ -5495,6 +5487,7 @@ declare function updatePlugin(name: string, opts: InstallOptions): Promise<Insta
5495
5487
  * {
5496
5488
  * "name": "acme",
5497
5489
  * "owner": { "name": "Acme Inc." },
5490
+ * "display": { "fields": ["status", "downloads", "author"] },
5498
5491
  * "plugins": [
5499
5492
  * {
5500
5493
  * "name": "toolkit",
@@ -5502,6 +5495,7 @@ declare function updatePlugin(name: string, opts: InstallOptions): Promise<Insta
5502
5495
  * "source": "./plugins/toolkit",
5503
5496
  * "category": "dev",
5504
5497
  * "author": { "name": "张三" },
5498
+ * "status": "listed",
5505
5499
  * "downloads": 1240,
5506
5500
  * "likes": 87
5507
5501
  * }
@@ -5512,6 +5506,16 @@ declare function updatePlugin(name: string, opts: InstallOptions): Promise<Insta
5512
5506
  * ⚠️ `downloads` / `likes` 是**市场那一侧数的**,而且**冻在加市场那一刻**
5513
5507
  * (目录是快照,见下面第二节)—— 判据在那两格自己的注释上。
5514
5508
  *
5509
+ * ## 顶上那格 `display`:**市场自己说印哪几格**(2026-09-10)
5510
+ *
5511
+ * 在它之前,「印不印作者」这件事只有一个开关 —— **填没填**。于是一个既想在自己
5512
+ * 后台记着作者、又不想在这套界面上印它的市场,没有任何写法表达得出来。
5513
+ *
5514
+ * `display.fields` 是一张**白名单**,只管徽标 / 元信息那一档(名单和「为什么承重
5515
+ * 的那几格一格都不在里面」在 {@link MARKETPLACE_DISPLAY_FIELDS} 上)。
5516
+ * ⚠️ **整格不写 = 照旧「填了就画」** —— 存量目录一个字都不用改,
5517
+ * 判据在 `DisplaySchema.fields` 上那张三行的表。
5518
+ *
5515
5519
  * ## ⚠️ 「没有上传接口」这句话 2026-09-08 改了半句
5516
5520
  *
5517
5521
  * 原话是「没有服务端、没有账号、**没有上传接口**、没有审核」。改的是第三句 ——
@@ -5565,10 +5569,42 @@ declare const EntrySchema: z.ZodObject<{
5565
5569
  changelog: z.ZodOptional<z.ZodString>;
5566
5570
  size: z.ZodOptional<z.ZodNumber>;
5567
5571
  homepage: z.ZodOptional<z.ZodString>;
5568
- visibility: z.ZodOptional<z.ZodEnum<{
5572
+ visibility: z.ZodCatch<z.ZodOptional<z.ZodEnum<{
5569
5573
  private: "private";
5570
5574
  public: "public";
5571
- }>>;
5575
+ }>>>;
5576
+ status: z.ZodCatch<z.ZodOptional<z.ZodEnum<{
5577
+ rejected: "rejected";
5578
+ listed: "listed";
5579
+ reviewing: "reviewing";
5580
+ offline: "offline";
5581
+ }>>>;
5582
+ }, z.core.$strip>;
5583
+ /**
5584
+ * 市场说了算的那几格「要不要印在屏幕上」(2026-09-10)。
5585
+ *
5586
+ * ## ⚠️ 这张名单里**一格承重的都没有**,那是它的定义不是巧合
5587
+ *
5588
+ * 缺席的那几格各有各的理由,而它们都不是「忘了列」:
5589
+ *
5590
+ * | 没进名单 | 为什么它不许被藏起来 |
5591
+ * | ------------ | -------------------------------------------------------------- |
5592
+ * | `name` | 它是标识 —— 藏了之后这一条在屏幕上没有名字 |
5593
+ * | `source` | 装它要靠它,而且 `search()` 拿它判「本地还是要 clone」 |
5594
+ * | `version` | `updateAvailable` 是拿它比出来的 —— 藏它 = **藏掉一次真的更新** |
5595
+ * | `description`| 卡片的正题;藏了之后一屏卡片全是空的 |
5596
+ * | `keywords` | 它压根不上屏,它是检索面 |
5597
+ * | `displayName`| 「给人看的名字」是身份不是徽标 —— 藏它只会退回那串机器标识 |
5598
+ *
5599
+ * 也就是说这张名单管的**只有徽标 / 元信息那一档**:一格藏没了,屏幕上少一枚芯片,
5600
+ * 别的一个字都不动。这条性质是这个功能能成立的全部前提 —— 一张能藏掉 `source`
5601
+ * 的名单会让一份合法的目录变得装不了东西,而报错会指着一个和它无关的地方。
5602
+ */
5603
+ declare const MARKETPLACE_DISPLAY_FIELDS: readonly ["status", "downloads", "likes", "author", "category", "visibility", "icon", "homepage", "size"];
5604
+ /** {@link MARKETPLACE_DISPLAY_FIELDS} 里的一格 */
5605
+ type MarketplaceDisplayField = (typeof MARKETPLACE_DISPLAY_FIELDS)[number];
5606
+ declare const DisplaySchema: z.ZodObject<{
5607
+ fields: z.ZodOptional<z.ZodArray<z.ZodString>>;
5572
5608
  }, z.core.$strip>;
5573
5609
  declare const CatalogSchema: z.ZodObject<{
5574
5610
  name: z.ZodString;
@@ -5577,6 +5613,9 @@ declare const CatalogSchema: z.ZodObject<{
5577
5613
  name: z.ZodString;
5578
5614
  url: z.ZodOptional<z.ZodString>;
5579
5615
  }, z.core.$strip>>;
5616
+ display: z.ZodOptional<z.ZodObject<{
5617
+ fields: z.ZodOptional<z.ZodArray<z.ZodString>>;
5618
+ }, z.core.$strip>>;
5580
5619
  plugins: z.ZodArray<z.ZodObject<{
5581
5620
  name: z.ZodString;
5582
5621
  description: z.ZodOptional<z.ZodString>;
@@ -5596,10 +5635,16 @@ declare const CatalogSchema: z.ZodObject<{
5596
5635
  changelog: z.ZodOptional<z.ZodString>;
5597
5636
  size: z.ZodOptional<z.ZodNumber>;
5598
5637
  homepage: z.ZodOptional<z.ZodString>;
5599
- visibility: z.ZodOptional<z.ZodEnum<{
5638
+ visibility: z.ZodCatch<z.ZodOptional<z.ZodEnum<{
5600
5639
  private: "private";
5601
5640
  public: "public";
5602
- }>>;
5641
+ }>>>;
5642
+ status: z.ZodCatch<z.ZodOptional<z.ZodEnum<{
5643
+ rejected: "rejected";
5644
+ listed: "listed";
5645
+ reviewing: "reviewing";
5646
+ offline: "offline";
5647
+ }>>>;
5603
5648
  }, z.core.$strip>>;
5604
5649
  }, z.core.$strip>;
5605
5650
  declare const RecordSchema: z.ZodObject<{
@@ -5613,6 +5658,9 @@ declare const RecordSchema: z.ZodObject<{
5613
5658
  name: z.ZodString;
5614
5659
  url: z.ZodOptional<z.ZodString>;
5615
5660
  }, z.core.$strip>>;
5661
+ display: z.ZodOptional<z.ZodObject<{
5662
+ fields: z.ZodOptional<z.ZodArray<z.ZodString>>;
5663
+ }, z.core.$strip>>;
5616
5664
  plugins: z.ZodArray<z.ZodObject<{
5617
5665
  name: z.ZodString;
5618
5666
  description: z.ZodOptional<z.ZodString>;
@@ -5632,16 +5680,45 @@ declare const RecordSchema: z.ZodObject<{
5632
5680
  changelog: z.ZodOptional<z.ZodString>;
5633
5681
  size: z.ZodOptional<z.ZodNumber>;
5634
5682
  homepage: z.ZodOptional<z.ZodString>;
5635
- visibility: z.ZodOptional<z.ZodEnum<{
5683
+ visibility: z.ZodCatch<z.ZodOptional<z.ZodEnum<{
5636
5684
  private: "private";
5637
5685
  public: "public";
5638
- }>>;
5686
+ }>>>;
5687
+ status: z.ZodCatch<z.ZodOptional<z.ZodEnum<{
5688
+ rejected: "rejected";
5689
+ listed: "listed";
5690
+ reviewing: "reviewing";
5691
+ offline: "offline";
5692
+ }>>>;
5639
5693
  }, z.core.$strip>>;
5640
5694
  }, z.core.$strip>;
5641
5695
  }, z.core.$strip>;
5642
5696
  type MarketplaceEntry = z.infer<typeof EntrySchema>;
5643
5697
  type MarketplaceCatalog = z.infer<typeof CatalogSchema>;
5644
5698
  type MarketplaceRecord = z.infer<typeof RecordSchema>;
5699
+ /** 这一条在市场那一侧的档口,见 `EntrySchema.status` 上那张四行的表 */
5700
+ type MarketplaceStatus = NonNullable<MarketplaceEntry['status']>;
5701
+ type MarketplaceDisplay = z.infer<typeof DisplaySchema>;
5702
+ /**
5703
+ * 按目录自己声明的白名单,把**没被选中的展示格抹掉**(2026-09-10)。
5704
+ *
5705
+ * ## ⚠️ 它是「抹掉」不是「打个标记」,而那是这个功能唯一说得通的做法
5706
+ *
5707
+ * 另一条路是把 `display` 原样发到每一层去,让 CLI / 网线 / 浏览器各自判一次
5708
+ * 「这一格该不该画」。那条路上「这一格印不印」会有**三个**能推出答案的地方,
5709
+ * 而它们分叉的那天表现是:同一份目录在 `epoch plugin search` 里印着作者、
5710
+ * 在浏览器上不印。判据同 `PluginSearchHit.installable` 为什么不把 `allowRemote`
5711
+ * 透出去 —— **一个问题只该有一个答案**。
5712
+ *
5713
+ * 抹掉之后,下游那一整条链路上现成的规矩(「有就画、没有整格不画」)一个字都不用改,
5714
+ * 而**新增的分支数是零**。这也正是它被放在 {@link searchMarketplaces} 里的理由:
5715
+ * 那是「市场里有什么」这个问题在全仓唯一的出口,CLI / TUI / 网线三条路都从它出来。
5716
+ *
5717
+ * ⚠️ **不碰装那条路**(`resolveMarketplaceRef` 读的是原始记录,不经过这儿):
5718
+ * 一份目录该印什么和它装不装得上是两件事,判据在 {@link MARKETPLACE_DISPLAY_FIELDS}
5719
+ * 那张表上。
5720
+ */
5721
+ declare function projectEntryForDisplay(entry: MarketplaceEntry, display: MarketplaceDisplay | undefined): MarketplaceEntry;
5645
5722
  interface MarketplaceOptions {
5646
5723
  /** `marketplacesPath(homeDir)` */
5647
5724
  statePath: string;
@@ -6423,6 +6500,38 @@ interface SystemPromptParams {
6423
6500
  /** 认不认图。来自 provider 自己,不是配置项 */
6424
6501
  supportsImages: boolean;
6425
6502
  };
6503
+ /**
6504
+ * 用哪种语言回复用户(2026-09-11)。
6505
+ *
6506
+ * ## 它补的是一句**无条件的假话**
6507
+ *
6508
+ * `## 规则` 第一条在这一笔之前逐字是 `- 用中文回复用户`,**没有任何分叉** ——
6509
+ * 而 `display.language` 这个配置项存在、`locales/en.yaml` 有六千多行、
6510
+ * `resolveLang()` 还会按系统 locale 探测(一台法语机器解析出 `en`)。
6511
+ * 于是一个把界面切成英文的用户,拿到的回复仍然是中文。
6512
+ *
6513
+ * 2026-09-11 那一轮加 `<settings>` 之后这件事变得更难看:同一段 prompt 里
6514
+ * 上面印着「界面语言 en」、最后一段写着「用中文回复用户」,两句话直接打架。
6515
+ *
6516
+ * ## 为什么是「跟界面语言走」,不是「跟用户这条消息的语言走」
6517
+ *
6518
+ * 后者是 Codex 的做法 —— 它的 prompt 里**一个字都不提回复语言**,靠模型
6519
+ * 自己跟随用户(实读 `gpt_5_1_prompt.md`,零命中)。那条路对我们不成立:
6520
+ * 这个仓库的 `## 规则` 整段是中文、工具描述也是中文,什么都不说的话模型的
6521
+ * 回复语言会跟着上下文漂(一个英文代码库里做事的中文用户会收到英文回复)。
6522
+ *
6523
+ * 界面语言是**用户已经表过一次态**的那个信号,而且它和用户屏幕上其余的字
6524
+ * 同源 —— 拿它当判据,回复语言和界面语言从此不会说两套话。
6525
+ *
6526
+ * ## ⚠️ 不给 = 按 `zh` 走,**而不是**「与这一轮之前逐字节相同」
6527
+ *
6528
+ * 这一格和 {@link SystemPromptParams.model} 那一格不一样:那一格不给就整段
6529
+ * 不出现,这一格**总要出一行**(`## 规则` 里那条语言规则无条件存在)。
6530
+ * 而那一行的措辞这一天改了(旧的那句实测在弱模型上不够,判据在
6531
+ * {@link REPLY_LANG_RULE} 上),所以嵌入宿主不接这个参数**也会**看到
6532
+ * 一行新的字节。这是认下的代价,理由写在 `systemPromptSegments` 里那一行上。
6533
+ */
6534
+ replyLang?: Lang;
6426
6535
  }
6427
6536
  /**
6428
6537
  * system prompt 的**分段**形态(方案 25 §2.5 的 `/context` 要它)。
@@ -6539,6 +6648,220 @@ declare function systemPromptSegments(params: SystemPromptParams): SystemPromptS
6539
6648
  */
6540
6649
  declare function buildSystemPrompt(params: SystemPromptParams): string;
6541
6650
 
6651
+ /**
6652
+ * `<self>` / `<settings>` —— 模型对**自己手上有什么**的那份自述,唯一产地。
6653
+ *
6654
+ * ## 它补的是什么
6655
+ *
6656
+ * 用户装了插件之后问「这个插件能干嘛」「我都有什么插件 / 连接器 / 自动化配置」,
6657
+ * 而模型在这一笔之前**一个字都答不上来**:一个插件对它只剩三样投影(技能索引里带
6658
+ * `<前缀>:` 的那几行、`delegate` 描述里的角色、工具表里的 `mcp__<前缀>__*`),
6659
+ * 清单里那句 `description` 只走 CLI 打印和 web 卡片,README 压根不在 `PLUGIN_LAYOUT`
6660
+ * 里。于是它唯一剩下的路是 `terminal` + `find`/`ls`/`cat` **盲摸用户的家目录**。
6661
+ *
6662
+ * ⚠️ 这**不是**「缺一个插件工具」。[docs/TOOLS.md](../../../../docs/TOOLS.md) 那节
6663
+ * 「模型做插件:这张表上一个插件工具都没有」守的是**做插件和装插件**
6664
+ * (`plugin_pack` / `plugin_validate` / `plugin_install`),判据分别是「那是知识不是
6665
+ * 能力面」和「不能让被审批的一方自己按确认」。**读自己手上有什么**两条都不沾 ——
6666
+ * 那一节压根没覆盖它,所以这一轮加的是数据不是工具。
6667
+ *
6668
+ * ## ⚠️ 这是同一类病的第四次复发,前三次都修在同一个地方
6669
+ *
6670
+ * `## 规则` 里那条「被问到你是谁……**再说清楚你能做什么**」指着空气 —— 和
6671
+ * 2026-09-02 那条「照实说(界面上本来就印着模型名)」一模一样:规则约束的是模型的
6672
+ * 嘴,而它的上下文里没有对应的事实。那次的处置是往 `## 环境` 补两行真值
6673
+ * (判据在 `SystemPromptParams.model` 上),这次是这个文件。
6674
+ *
6675
+ * 所以**下面每一句话都必须是现读的真值**。一句写死的「你可以管理插件」比没有更坏:
6676
+ * 模型会照着它回答,而用户那台机器上可能一个插件都没装。
6677
+ *
6678
+ * ## 一、为什么只给**计数 + 去哪儿找**,不列名字
6679
+ *
6680
+ * 这是一次预算决定,判据是「**知道去哪儿找 ≠ 到处找**」:插件名靠一次
6681
+ * `file_list ~/.epoch/plugins/` 一把拿全,那是一次定点读;而列进这一段的字
6682
+ * **每一轮都付**。技能名更不该在这儿重复 —— 它们已经逐条列在
6683
+ * `<available_skills>` 里了(`skill/prompt-index.ts`),抄第二遍是纯重复,
6684
+ * 下场写在 `SystemPromptSegments` 上那段「工具表在一次请求里出现两遍」里。
6685
+ *
6686
+ * 📌 **想升级成「插件名也列出来」是改一行**(每个插件 ≈5 tokens)。Hermes 的
6687
+ * `_skills_prompt()` 走的就是那一档(「名字永远可见,描述按需降级」)。
6688
+ * 但那是一次独立的决定,别顺手做 —— 它会让这一段随着用户装插件线性变长。
6689
+ *
6690
+ * ## 二、⚠️ 连接器和 hook 为什么破例**投影**,而不是给一条「去哪儿找」
6691
+ *
6692
+ * 因为它们的文件**带凭据**,永远不会进读窗口:
6693
+ *
6694
+ * - `~/.epoch/mcp.json` —— `RawServerSchema` 上就有 `headers` 和 `env`
6695
+ * (`plugin-mcp/src/schema.ts`),那是 bearer token 的常驻地;
6696
+ * - `~/.epoch/hooks.json` —— 整行 shell 命令,凭据经常写在参数里。
6697
+ *
6698
+ * 「去哪儿找」这条路对一个读不到的文件不成立,所以这两类只能把**结论**投影进来。
6699
+ * 好在它们恰好最便宜:一台连接器 ≈10 tokens,hook 只给一个总数
6700
+ * (连事件分组都不给,判据在 {@link SelfIndexInput.hooks} 上)。
6701
+ * 完整的拒绝名单和判据在 `plugin-file/src/utils.ts` 的 `resolveReadablePath` 上。
6702
+ *
6703
+ * ## 三、那几条「约定」是整段里最便宜的准确度
6704
+ *
6705
+ * 形状抄 Codex 的 `context/available_plugins_instructions.rs`(那份 `## Plugins`
6706
+ * 片段同样**不列插件清单**,只讲命名约定和触发规则)。它值钱在于零数据成本:
6707
+ * `<前缀>:` 这个约定今天对模型只是一串字符 —— 没有任何一处告诉过它那是出处。
6708
+ * 讲了之后,「这个插件带来了什么」就变成一次在已有上下文里按前缀筛,一次工具调用都不用。
6709
+ *
6710
+ * ## 四、条件注入:没有的东西一个字都不说
6711
+ *
6712
+ * 一个插件都没装时,插件那一行**和**它那几条约定一起不出现 —— 口径同
6713
+ * `formatSkillIndex` 回空串、同 `projectBlock()` 里那五个按工具存在与否开的策略块。
6714
+ * 讲一个这台机器上不存在的概念是纯噪音,而且它会让模型去猜一个不存在的路径。
6715
+ * 全空时整段回空串,那时 `buildSystem()` 的 `filter` 会把它丢掉。
6716
+ *
6717
+ * ## 五、措辞是中文、写在代码里、不进 locales
6718
+ *
6719
+ * 整份是**给模型读的字**,判据逐字同 `tools/builtin/schedule-draft.ts` 文件头最后
6720
+ * 一节和 `skill/prompt-index.ts`:换界面语言不该换模型看到的东西。
6721
+ * ⚠️ 这个文件因此在 `scripts/i18n-scan.mjs` 的 `SKIP_FILES` 里。
6722
+ */
6723
+
6724
+ /**
6725
+ * 一台连接器在这一段里的样子。
6726
+ *
6727
+ * ⚠️ **不收 `lastError` 的原文**。三条:那句话来自第三方 server(内容不可控、
6728
+ * 可能很长、也可能带 URL 里的 token);它每次重连都可能变,而这一段一变就作废
6729
+ * 后面整段 prompt cache;而模型对它没有下一步动作 —— 「连不上」和「要登录」
6730
+ * 这两档已经把用户该做什么说完了(重连 / `epoch mcp login`)。
6731
+ * 要看原文的地方是能力页和 `/mcp`,那两处给的是人。
6732
+ */
6733
+ interface SelfConnector {
6734
+ name: string;
6735
+ /**
6736
+ * 三档不是三种颜色,是**三种下一步**(重连 / `epoch mcp login` / 没有下一步)。
6737
+ *
6738
+ * ⚠️ **类型直接引 `WireMcpConnector['state']`,不在这儿抄一份联合。** 能力页
6739
+ * 那一栏用的就是这一组,而两处各写一份字面量联合的话,哪天加一档(比如
6740
+ * 「正在连」)这边不会红 —— 表现是界面上多一档而模型这侧静默把它归进
6741
+ * `failed`。判序同样不在这儿:那是 protocol 的 `mcpConnectorState()`。
6742
+ */
6743
+ state: WireMcpConnector['state'];
6744
+ toolCount: number;
6745
+ }
6746
+ /**
6747
+ * 渲染 `<self>` 要的全部数字。**全部现读**,判据见文件头。
6748
+ *
6749
+ * ⚠️ **这几个数一个都不许在这儿重算**。真源各有出处,调用方从那儿取:
6750
+ * 技能走 `skillIndexStats()`(`skill/prompt-index.ts`,`/context` 用的同一个),
6751
+ * 插件走 `LoadedPlugin.counts` / 加载结果(`plugin/loader.ts`),
6752
+ * hook 走 `hook/config-loader.ts` 那份解析。在这儿数第二遍的下场写在
6753
+ * `inventoryTotal()` 的 JSDoc 上:两处口径走散之后**两边都答得出「我算了」**。
6754
+ */
6755
+ interface SelfIndexInput {
6756
+ /**
6757
+ * 这一程真正在用的家目录。**必须由调用方给**,不许在这儿 `resolveHomeDir()` ——
6758
+ * 判据逐字同 `resolveReadablePath` 的「`homeDir` 不能省」:嵌入宿主的数据目录
6759
+ * 和全局那份不是同一个,印错了等于告诉模型一条它读不到的路径。
6760
+ */
6761
+ homeDir: string;
6762
+ /**
6763
+ * 当前 profile 名(`resolveProfile()`,`default` = 没启用)。
6764
+ *
6765
+ * ⚠️ **这一行不是装饰**。`infra/src/paths.ts` 文件头记着一次真实故障:
6766
+ * 设了 `EPOCH_PROFILE=work` 之后写和读落在两个目录上,设置静默失效。
6767
+ * Hermes 的 `_active_profile_line()` 为同一个坑专门在 prompt 里留了一行
6768
+ * (原话是「免得 agent 把 `~/.hermes/skills` 和
6769
+ * `~/.hermes/profiles/<active>/skills` 搞混」)—— 这一格是同一条。
6770
+ */
6771
+ profile: string;
6772
+ /** 装了几个、其中几个被用户停用了(停用的不加载,但它确实装着) */
6773
+ plugins: {
6774
+ active: number;
6775
+ disabled: number;
6776
+ };
6777
+ /** 真进了 `<available_skills>` 的条数 —— 取 `skillIndexStats().indexed` */
6778
+ skills: number;
6779
+ connectors: readonly SelfConnector[];
6780
+ /** 斜杠命令条数(用户级 + 项目级 + 插件带的,同名覆盖之后的总数) */
6781
+ commands: number;
6782
+ /**
6783
+ * **生效**的 hook 条数(跨全部来源和事件)。
6784
+ *
6785
+ * ## 为什么只给一个总数,连事件分组都不给
6786
+ *
6787
+ * 三条,第三条是硬的:
6788
+ *
6789
+ * 1. 用户问「我有什么自动化配置」时要的是「有没有、在哪改」,
6790
+ * 而不是 `PreToolUse 2 / Stop 1` 这种分布 —— 后者对他没有下一步动作;
6791
+ * 2. 分组要么走 `HookManager.count(type)`(它把**宿主用 `register()` 注册的
6792
+ * JS hook** 和用户配的 command hook 加在一起数,于是一台没配过 hook 的机器
6793
+ * 可能印出「hook 3 条」—— 那是假话),要么在装配层按
6794
+ * `HookSourceKind` 映一遍档名,而那等于把这一段的措辞劈成两个文件;
6795
+ * 3. **模型对 hook 没有任何下一步动作** —— 它既看不到 hook 的正文
6796
+ * (`hooks.json` 在读窗口的拒绝名单里,里面是整行 shell 命令),
6797
+ * 也不该去改。它只需要答得出「有」和「在哪」。
6798
+ *
6799
+ * 真源是 `HookStatus.sources` 的 `count` 之和(「这个来源实际装进去的条数,
6800
+ * 坏项已经跳掉」),装配层加总。⚠️ **别换成 `HookManager.count()`** ——
6801
+ * 上面第 2 条说的就是它。
6802
+ */
6803
+ hooks: number;
6804
+ /** `<settings>` 那一段。不给就整段不出现 */
6805
+ settings?: SelfSettings;
6806
+ }
6807
+ /**
6808
+ * `<settings>` 里允许出现的**全部**字段 —— 这是一份白名单,不是 `EpochConfig` 的投影。
6809
+ *
6810
+ * ## ⚠️ 凭据族永远不进这一格,而且要在类型上说不出口
6811
+ *
6812
+ * 收一个窄接口而**不是** `EpochConfig`(或者它的 `Partial`),理由不是洁癖:
6813
+ * 收整份配置之后,「这一段里会不会出现 `provider.apiKey`」就变成一个要靠人每次
6814
+ * review 的问题,而它今天的正确答案必须由**类型**给出。`credentials[]`、
6815
+ * MCP 的 `env` / `headers`、`.env` 里的一切,同理。
6816
+ *
6817
+ * 形状上抄的是 Codex 的 `context/environment_context.rs`:它也不投影整份 config,
6818
+ * 只发 `<filesystem>`(可写根 + 权限档)和 `<network>`(域名白名单)——
6819
+ * **只投影边界**。加一格之前先问一句:用户真的会问它吗,而模型答错会怎样。
6820
+ *
6821
+ * ## 这里**没有**权限级别和模型型号,那不是漏了
6822
+ *
6823
+ * `<permission_level>` 由 `projectBlock()` 印(`agent/prompt.ts`),
6824
+ * 「当前模型 / 图片输入」两行由 `## 环境` 印(`agent/message-utils.ts`)。
6825
+ * 在这儿再印一遍就是同一个事实的第二处产地,而两处一旦走散,模型会照着
6826
+ * **排在后面**的那一份回答。
6827
+ */
6828
+ interface SelfSettings {
6829
+ /** 界面语言(`display.language`)。没配 = 探测出来的,说不出确定值就别给 */
6830
+ language?: string;
6831
+ /** `maxTurns`。`0` 是「不限」那一档(`UNLIMITED_TURNS`),别读成「一轮都不许跑」 */
6832
+ maxTurns: number;
6833
+ contextLength: number;
6834
+ /**
6835
+ * `compression.enabled` / `compression.threshold`,**缺省已经解开**。
6836
+ *
6837
+ * ⚠️ 这两格在 protocol 的 `EpochConfig` 上是可选的,但**缺席不是「不知道」** ——
6838
+ * 那份契约明写着缺省语义(`enabled` 缺省 = `true`、`threshold` 缺省 =
6839
+ * `DEFAULT_COMPRESSION_THRESHOLD`)。所以解缺省的活归**取数那一层**
6840
+ * (`runtime/src/self-index.ts`),这儿收的是已经解完的值:
6841
+ * 渲染器认一个 `undefined` 档就等于在这里长出第二处默认值,
6842
+ * 而「默认值只有那一处」是那份契约自己写的。
6843
+ */
6844
+ compression: {
6845
+ enabled: boolean;
6846
+ threshold: number;
6847
+ };
6848
+ /** `sandbox.terminal`(缺省 = `true`)—— 关掉等于命令能写整个文件系统 */
6849
+ sandboxTerminal: boolean;
6850
+ /** `budget.maxCostUsd` / `budget.maxTokens`,两格都可能没设 */
6851
+ budget?: {
6852
+ maxCostUsd?: number;
6853
+ maxTokens?: number;
6854
+ };
6855
+ }
6856
+ /**
6857
+ * `<self>` + `<settings>` 的成品串。**一样都没有时回空串。**
6858
+ *
6859
+ * 回空串这件事是接口的一部分:空段被 `buildSystem()` 的 `filter` 丢掉,
6860
+ * 于是一台什么都没装的机器拿到的 prompt 与这个功能存在之前**逐字节相同**。
6861
+ * 判据逐字同 `formatSkillIndex`。
6862
+ */
6863
+ declare function renderSelfIndex(input: SelfIndexInput): string;
6864
+
6542
6865
  /**
6543
6866
  * Nudge 检测器。
6544
6867
  *
@@ -7004,6 +7327,11 @@ declare function admitArtifacts(raws: readonly ToolArtifactInput[], policy: Mult
7004
7327
  /**
7005
7328
  * 信任闸门 —— 把「这个目录能不能加载项目级配置」折成一次判定。
7006
7329
  *
7330
+ * ⚠️ **2026-09-11 起这一次判定有两档**(`trusted` / `exec`,见
7331
+ * {@link TrustDecision})。档位本身的判据全文在 protocol 的 `TrustTier` 上,
7332
+ * 这儿只说降级规则怎么作用在它们身上:**两条规则同时作用于两档**,
7333
+ * 没有「读类走一套、执行类走另一套」那种不对称。
7334
+ *
7007
7335
  * 判定 = 存储里的 `TrustLevel` + 两条降级规则:
7008
7336
  * 1. `config.trust.enabled === false` → 闸门整体不生效(回到最初行为)
7009
7337
  * 2. `unknown` + 非交互(没有 TTY)→ 按 `untrusted` 处理。
@@ -7036,8 +7364,25 @@ declare function admitArtifacts(raws: readonly ToolArtifactInput[], policy: Mult
7036
7364
 
7037
7365
  /** 一次已决的工作区信任判定 */
7038
7366
  interface TrustDecision {
7039
- /** 是否允许加载项目级配置(项目指令文件等) */
7367
+ /**
7368
+ * **读类**:允许把这个目录里的文件当指令读(指令文件 / 技能 / 角色 / 命令)。
7369
+ *
7370
+ * ⚠️ 名字没改成 `read`,那是刻意的:这一格的语义**一个字节都没变**,
7371
+ * 改名只会让 70 多个调用点和用例产生一片与本次判断无关的 diff,
7372
+ * 真正的新东西藏在里头看不见。新增的那一档是下面的 {@link exec}。
7373
+ */
7040
7374
  trusted: boolean;
7375
+ /**
7376
+ * **执行类**:允许这个目录里的 `hooks.json` / `.claude/settings*.json` /
7377
+ * `policies/` 生效,以及 `.epoch/settings.json` 里 allow 那一半
7378
+ * (2026-09-11,档位判据见 [protocol 的 `TrustTier`](../../../protocol/src/trust.ts))。
7379
+ *
7380
+ * ⚠️ **`exec` 为真必然 `trusted` 为真,反过来不成立**,而「反过来不成立」正是
7381
+ * 这一格存在的全部意义。想合并成一个三态枚举的人先读那一份 `TrustTier`:
7382
+ * 两档是相加的,而调用点各自只关心其中一档 —— 合成枚举之后每个调用点都要
7383
+ * 自己写一次 `>= exec` 的比较,那才是真会写错的地方。
7384
+ */
7385
+ exec: boolean;
7041
7386
  /** 存储里的原始判定,未经上面两条降级规则处理 */
7042
7387
  level: TrustLevel;
7043
7388
  /** 人类可读的判定依据,进启动 diagnostics */
@@ -7050,6 +7395,14 @@ interface TrustGateOptions {
7050
7395
  manager: TrustManager$1;
7051
7396
  /** 有 TTY = 有人能回答「要不要信任」 */
7052
7397
  interactive: boolean;
7398
+ /**
7399
+ * 执行类内容指纹的算法,缺省 `computeExecHash`。**只给用例注入用。**
7400
+ *
7401
+ * ⚠️ 缺省值就是真算法,所以忘了传的人拿到的是**正确行为**而不是「不校验」——
7402
+ * 与 `LoadCommandsOptions.trusted` 那条「不给默认参数」刚好相反,判据也相反:
7403
+ * 那一个的默认值会**放宽**(漏传 = 自动获得信任),这一个的默认值只会**收紧**。
7404
+ */
7405
+ execHash?: (root: string) => string;
7053
7406
  }
7054
7407
  declare function createTrustGate(opts: TrustGateOptions): TrustGate;
7055
7408
  /** 信任记录本身读不出来时的闸门:fail closed */
@@ -7674,6 +8027,22 @@ interface BudgetStatus {
7674
8027
  justWarned: boolean;
7675
8028
  /** 本次调用是否**首次**遇到「模型无定价数据」 */
7676
8029
  justWarnedUnknownPricing: boolean;
8030
+ /**
8031
+ * **刚加进去的这一笔**折成多少钱(美元)。算不出来时 `undefined`,不是 0
8032
+ * (会话库 v9,2026-09-13)。
8033
+ *
8034
+ * ## 为什么这个数必须由闸门给出来,而不是调用方自己算一遍
8035
+ *
8036
+ * 落盘那条路要记「这一轮花了多少」(`SessionDB.recordUsage`),而**只有闸门
8037
+ * 知道这一笔是按哪份单价算的** —— `add()` 里那句
8038
+ * `rate ? rate.pricing : this.defaultPricing` 有两个分支,provider 不报 model
8039
+ * 的时候走的是后者。调用方拿 `rateOf(model)?.pricing` 自己乘一遍,在那一档上
8040
+ * **会得到 undefined,而闸门记的是一个真数** —— 于是会话列表说「查不到单价」,
8041
+ * 预算闸门却在照常扣钱。两个数出自同一次请求,却讲两个故事。
8042
+ *
8043
+ * 所以它和 {@link cumulative} 是同一条原则的两半:**一次请求的账只有一个说法。**
8044
+ */
8045
+ turnCostUsd?: number;
7677
8046
  }
7678
8047
 
7679
8048
  /**
@@ -7830,6 +8199,30 @@ interface GenerateOutput {
7830
8199
  */
7831
8200
  model: string;
7832
8201
  provider: ProviderType;
8202
+ /**
8203
+ * 这次请求**从哪个模型起步** —— 和上面的 {@link model}(落到哪)是一次请求的两头。
8204
+ *
8205
+ * ## 为什么光有 `model` 不够
8206
+ *
8207
+ * 上面那段注释治的是「记账按错误的单价算」,而它**没有**回答
8208
+ * 「`model` 和别处那个不一样,是谁改的」。落点和任何一个「当前模型」不等,
8209
+ * 至少有四种成因(`agent/loop.ts` 那段列得全):用户 `/model` 换了、
8210
+ * 命令 frontmatter 的 `model:` 覆盖了、provider 失败降级、路由 404 换模型。
8211
+ * **前两种不是故障,后两种是**,而一个落点答不出是哪一种 ——
8212
+ * web 脚注那枚「故障转移」徽标就是拿落点去和 `GET /api/config` 那一发的快照比,
8213
+ * 于是用户在顶栏换一次模型,整段会话的轮次集体被标成「故障转移」。
8214
+ *
8215
+ * 起点接出来之后那个判断落在**同一次请求的两个值**上:不等就是真降级,
8216
+ * 用户换模型时两者相等(起点读的就是换过之后那个选择)。
8217
+ *
8218
+ * ⚠️ **它是「进 while 之前」那个值**,不是循环里的当前值。`escalate()` 就地改
8219
+ * `state.ct` / `state.cm`,在出口处读 `state.cm` 拿到的是落点 —— 那就又成了
8220
+ * 一个恒等于 `model` 的字段,比不加更糟(它会让上面那枚徽标永远不亮)。
8221
+ *
8222
+ * 缺席规则同 {@link usage}:拿不到就整个不给。**不拿 `model` 顶上** ——
8223
+ * 「不知道起点」和「起点就是落点」在读侧是两件事,后者会被读成「这一轮没降级」。
8224
+ */
8225
+ requestedModel?: string;
7833
8226
  /**
7834
8227
  * 这一次的 `responseFormat` **真的到了 provider 那一侧吗**(方案 59 §2.3.1)。
7835
8228
  *
@@ -7889,6 +8282,18 @@ interface ProviderStreamChunk {
7889
8282
  */
7890
8283
  model?: string;
7891
8284
  provider?: ProviderType;
8285
+ /**
8286
+ * 这次请求从哪个模型起步,**只在 `finish` chunk 上给**。
8287
+ *
8288
+ * 语义与缺席规则逐条同 `GenerateOutput.requestedModel`(连「别拿落点顶上」
8289
+ * 那条一起),别在这儿另立一套。挂在 finish 而不是每个 delta 上的判据同上面的
8290
+ * {@link model} / {@link provider}。
8291
+ *
8292
+ * ⚠️ **这一格在流式这条路上格外要紧**:web 走的是 `stream()`,
8293
+ * 而两条路的 finish 出口是两段独立代码 —— 只在 `generate()` 那边填,
8294
+ * 症状是「命令行下对、浏览器里那枚徽标永远不亮」,两边都不报错。
8295
+ */
8296
+ requestedModel?: string;
7892
8297
  /**
7893
8298
  * 强制 JSON 生效了没有 / provider 抱怨了什么,**只在 `finish` chunk 上给**
7894
8299
  * (方案 59 §2.3)。
@@ -7990,6 +8395,19 @@ interface ProviderGenerateOutput {
7990
8395
  * (即改造前的行为)。
7991
8396
  */
7992
8397
  model?: string;
8398
+ /**
8399
+ * 这一轮**要求**跑的那个模型 —— 和上面的 {@link model}(实际跑的)是一对。
8400
+ *
8401
+ * 两个都在、且不等 = 这一轮真的降级了。**光看 `model` 分不出成因**:
8402
+ * 用户换模型、命令 frontmatter 覆盖、provider 降级、路由 404 换模型,
8403
+ * 四种都会让 `model` 和别处那个模型名不一样,而前两种不是故障。
8404
+ * 判据全文在 `provider/types.ts` 的 `GenerateOutput.requestedModel` 上。
8405
+ *
8406
+ * **可选**,理由逐字同上面那条:脚本化的假 provider 没有「模型」这个概念。
8407
+ * 缺席时读侧一律按「不知道这一轮要的是谁」处理 —— 那时不许拿 `model`
8408
+ * 自比(恒相等,会被读成「确认没降级」)。
8409
+ */
8410
+ requestedModel?: string;
7993
8411
  /**
7994
8412
  * provider 对这一次请求的抱怨(2026-09-01)。
7995
8413
  *
@@ -8039,7 +8457,32 @@ interface AgentProvider {
8039
8457
  */
8040
8458
  capabilities?: {
8041
8459
  supportsImages: boolean;
8460
+ /**
8461
+ * `supportsImages: false` 到底是「查到了,不支持」还是「查不到」
8462
+ * (2026-09-11)。判据全文在 core 的 `ModelCapability.imagesUnknown` 上。
8463
+ *
8464
+ * 为真时闸门不再当场拒绝,而是调 {@link AgentProvider.probeImageSupport}
8465
+ * 实测一次。**不给 = 当成「查到了」** —— 脚本化的假 provider(用例里那些)
8466
+ * 不该被迫长出一个探测器。
8467
+ */
8468
+ imagesUnknown?: boolean;
8042
8469
  };
8470
+ /**
8471
+ * 「这个模型到底认不认图」——**发一张图问它本人**(2026-09-11)。
8472
+ *
8473
+ * ## 为什么倒置接口上要有这一格
8474
+ *
8475
+ * 上面 `capabilities` 答的是「我们**查到**它认不认图」,而这一轮的病就出在
8476
+ * 「查」这个动作上:上游改一次模型名字,静态表和 OpenRouter 两层同时 miss,
8477
+ * 默认值 `false` 让一个认图的模型静默瞎掉。实测是唯一不会过期的那一层,
8478
+ * 而它需要一个真的 `LanguageModel` 实例 —— 那是 router 的东西,循环够不着。
8479
+ *
8480
+ * ⚠️ **只在 `imagesUnknown` 且这一轮真有图时才调**。它是一发真的网络请求
8481
+ * (约 225 token),无条件调等于给每一轮凭空加一次往返和一笔钱。
8482
+ *
8483
+ * 不给(脚本化的假 provider)= 退回这一轮之前的行为:查不到就拒发。
8484
+ */
8485
+ probeImageSupport?: () => Promise<boolean>;
8043
8486
  /**
8044
8487
  * **此刻这一轮会用哪个模型**(2026-09-02)。
8045
8488
  *
@@ -8282,6 +8725,28 @@ interface AgentConfig {
8282
8725
  * `AgentRole.skills` 上)。
8283
8726
  */
8284
8727
  roleSkills?: () => readonly string[] | undefined;
8728
+ /**
8729
+ * `<self>` / `<settings>` 那两段的现读出口(2026-09-11)——「我手上有什么」。
8730
+ *
8731
+ * 纯透传给 `PromptBuilder`,循环体一处都不读它。立项理由、每一格的判据、
8732
+ * 以及为什么是**串**而不是那堆数字,全文在
8733
+ * [agent/self-index.ts](./self-index.ts) 的文件头和 `PromptBuilderDeps.selfIndex` 上。
8734
+ *
8735
+ * 不接 = 那两段整个不出现 = 这一轮之前逐字节相同。**代价要说清**:不接它的
8736
+ * 宿主,它的模型答不出用户装了什么插件、连了哪几台 server —— 而 `## 规则` 里
8737
+ * 那条「说清楚你能做什么」还在,于是它只能猜或者拒答。
8738
+ */
8739
+ selfIndex?: () => string;
8740
+ /**
8741
+ * 用哪种语言回复用户(2026-09-11)。纯透传给 `PromptBuilder`,循环体不读它。
8742
+ *
8743
+ * 判据全文在 `agent/message-utils.ts` 的 `SystemPromptParams.replyLang` 上。
8744
+ * 一句话:`## 规则` 第一条原来无条件是「用中文回复用户」,而
8745
+ * `display.language` 一直存在 —— 切英文界面的用户拿到的仍然是中文回复。
8746
+ *
8747
+ * 不接 = 那条规则逐字保持原样 = 这一轮之前逐字节相同。
8748
+ */
8749
+ replyLang?: () => Lang | undefined;
8285
8750
  /**
8286
8751
  * 技能学习器(可选——Nudge 触发时自动学习)。
8287
8752
  *
@@ -8846,6 +9311,29 @@ interface ModelCapability {
8846
9311
  maxOutputTokens: number;
8847
9312
  supportsTools: boolean;
8848
9313
  supportsImages: boolean;
9314
+ /**
9315
+ * **`supportsImages: false` 说的是「不知道」而不是「不支持」**(2026-09-11)。
9316
+ *
9317
+ * ## 为什么需要第二格,而不是把 `supportsImages` 改成三态
9318
+ *
9319
+ * 三态(`true | false | 'unknown'`)在这儿是个陷阱:`'unknown'` 在 JS 里是
9320
+ * **真值**,任何一处漏改的 `if (cap.supportsImages)` 会静默变成「支持」——
9321
+ * 也就是把图发给一个看不见它的模型,然后收一份对着空气的胡说。多一格布尔的
9322
+ * 话,漏改的那一处拿到的是 `false`,行为和这一轮之前逐字节相同(拒发)。
9323
+ * **失败要落在安全的那一侧**,判据同 `AgentProvider.capabilities` 那条
9324
+ * 「不给就按不支持处理」。
9325
+ *
9326
+ * ## 它是干什么用的:触发一次实测
9327
+ *
9328
+ * 这一格为 `true` 时,闸门(`loop.ts` 闸门 1 的输入侧)不再当场拒绝,而是
9329
+ * 先发一次语义探针问模型本人(见 [capability-probe.ts](./capability-probe.ts)),
9330
+ * 把答案记进缓存。少了这一格,一个上游刚改过名字的模型在我们这儿是**静默**
9331
+ * 瞎掉的 —— 2026-09-11 的现场是 DeepSeek 把 `deepseek-v4-flash-vision-exp`
9332
+ * 改名成 `deepseek-flash`,静态表和 OpenRouter 两层同时 miss。
9333
+ *
9334
+ * 缺席 = `false` = 「这个答案是查到的,不用再探」。
9335
+ */
9336
+ imagesUnknown?: boolean;
8849
9337
  supportsStreaming: boolean;
8850
9338
  supportsPromptCaching: boolean;
8851
9339
  supportsReasoning: boolean;
@@ -8866,6 +9354,15 @@ interface ModelMetaEntry {
8866
9354
  prompt: string;
8867
9355
  completion: string;
8868
9356
  input_cache_read?: string;
9357
+ /**
9358
+ * 分时价(2026-09-13)。DeepSeek 那一族有,多数模型没有。
9359
+ *
9360
+ * ⚠️ **基础那三格不是「平均价」,是某一档的价**(实测 `deepseek-v4.1-flash`:
9361
+ * 基础 = 闲时 0.15/0.6,峰时 override 到 0.3/1.2)。所以**忽略 overrides
9362
+ * 不是「差一点」,是峰时整整少记一半** —— 而「成本看起来很便宜」正是这个仓
9363
+ * 最不想要的那一类静默错误。判据在 {@link resolvePricingAt}。
9364
+ */
9365
+ overrides?: readonly PricingOverride[];
8869
9366
  };
8870
9367
  /** OpenRouter 的 `supported_parameters` / `architecture` 能推出的几项能力 */
8871
9368
  supportsTools?: boolean;
@@ -8877,6 +9374,19 @@ interface ModelMetaEntry {
8877
9374
  * 失败静默,不阻挡启动。
8878
9375
  */
8879
9376
  declare function refreshOpenRouterMetadata(homeDir: string): Promise<void>;
9377
+ /**
9378
+ * 查单价,**并告诉调用方这份价管到什么时候**(2026-09-13)。
9379
+ *
9380
+ * `getCapability().pricing` 给的是「此刻」的价,够绝大多数调用点用;只有
9381
+ * **逐轮调用且会把结果缓存住**的那一个(`runtime` 的 `makePricingLookup`)
9382
+ * 需要多知道一格 —— 一份会过期的价被永久缓存住,症状是跨过峰谷分界之后
9383
+ * 整段会话继续按旧价记账,而**一次都不会红**。
9384
+ *
9385
+ * 优先级和 {@link getCapability} 逐字相同:`STATIC_PRICING` → 磁盘缓存
9386
+ * (含 {@link PRICING_ALIASES})。静态价不随时间变,所以那一支 `validUntil`
9387
+ * 是 `Infinity`。
9388
+ */
9389
+ declare function pricingFor(modelId: string, homeDir: string, at?: number): PricingAt;
8880
9390
  /**
8881
9391
  * 获取模型的 Capability。
8882
9392
  * 优先级:静态配置 > 磁盘缓存 > 默认值 [Hermes]
@@ -8884,7 +9394,72 @@ declare function refreshOpenRouterMetadata(homeDir: string): Promise<void>;
8884
9394
  * **「优先」是逐格的,不是整条记录的**(2026-09-01 改):静态表里没写单价的
8885
9395
  * 条目,单价回落到磁盘缓存那一份。见下面静态分支里的判据。
8886
9396
  */
8887
- declare function getCapability(modelId: string, homeDir: string): ModelCapability;
9397
+ declare function getCapability(modelId: string, homeDir: string,
9398
+ /**
9399
+ * 哪一家的这个型号。**只有 `supportsImages` 那一格用得到它** —— 实测缓存
9400
+ * 按 `provider:model` 记(同一个裸名在两家下面可以是两个模型)。
9401
+ *
9402
+ * 可选是因为它的调用方分两种:router 知道自己此刻路由到哪一家(一定传),
9403
+ * 而装配期那次只为了拿 `contextLength` / 单价(不传,也不需要)。不传 =
9404
+ * 跳过实测那一层,剩下三层原样。
9405
+ */
9406
+ provider?: string): ModelCapability;
9407
+ /** 一档分时价。字段全可选 —— 缺了 `utc_days` / 时段就是「一直适用」 */
9408
+ interface PricingOverride {
9409
+ /** UTC 星期几,小写全名(`'monday'`…)。缺席 = 每天 */
9410
+ utc_days?: readonly string[];
9411
+ /** UTC 时刻,**HHMM 形式的整数**(`100` = 01:00,`1000` = 10:00)。缺席 = 0 */
9412
+ utc_start?: number;
9413
+ utc_end?: number;
9414
+ prompt?: string;
9415
+ completion?: string;
9416
+ input_cache_read?: string;
9417
+ }
9418
+ /** 一份**在某个时刻有效**的单价,外加它什么时候失效 */
9419
+ interface PricingAt {
9420
+ pricing: ModelPricing | undefined;
9421
+ /**
9422
+ * 这份单价管到哪一刻(epoch ms,闭区间外)。没有分时价时是 `Infinity`。
9423
+ *
9424
+ * **存在的理由是记忆化**:`runtime` 那个逐轮调用的查价函数把结果 Map 住了,
9425
+ * 而一份会过期的单价被永久 Map 住,症状是**跨过峰谷分界之后整段会话继续按
9426
+ * 旧价记账** —— 一次都不会红。判据在 `build.ts` 的 `makePricingLookup`。
9427
+ */
9428
+ validUntil: number;
9429
+ }
9430
+ /**
9431
+ * 按**某个时刻**解出这个模型此刻的单价。
9432
+ *
9433
+ * ## 为什么非要解 overrides
9434
+ *
9435
+ * DeepSeek 是分时价,而 OpenRouter 那条记录里**基础价不是平均价,是某一档的价**。
9436
+ * 实测 `deepseek-v4.1-flash`(2026-09-13 快照):基础 `0.15 / 0.6`,而工作日
9437
+ * UTC 01:00–04:00 与 06:00–10:00 两段 override 到 `0.3 / 1.2`。
9438
+ * 只读基础那三格 = **峰时每一笔都只记一半**,而屏幕上那个数看起来完全正常。
9439
+ *
9440
+ * 这也正是 `STATIC_PRICING` 上那条「单价故意不进静态表」当初担心的东西的另一面:
9441
+ * 静态表会把价钉死在**峰值**,而只读基础字段会把它钉死在**谷值**。两个都是错的,
9442
+ * 对的那个答案是「现在几点」。
9443
+ *
9444
+ * ## 匹配规则(逐条照着真实响应核过)
9445
+ *
9446
+ * - `utc_days` 缺席 = 每天;给了就按**UTC 星期几**匹配(不是本地星期几 ——
9447
+ * 字段名里那个 `utc` 是字面意思,而东八区和 UTC 跨日差 8 小时);
9448
+ * - 时段是 HHMM 的整数,**左闭右开** `[utc_start, utc_end)`;
9449
+ * - `utc_end` 为 `0` 表示**到当日 24:00**(真实数据里最后一档就是
9450
+ * `{utc_start: 1000, utc_end: 0}`),不是一个零长度窗口;
9451
+ * - `utc_start > utc_end` 表示**跨午夜**;
9452
+ * - **第一条命中的赢**,同真实数据里那张表互不重叠的排法;
9453
+ * - 一条都没命中就用基础价 —— 那是「这一刻没有折扣」,不是「不知道」。
9454
+ *
9455
+ * ## `validUntil` 怎么算
9456
+ *
9457
+ * 取**所有档的所有边界**(每档的 start 和 end)折成今天的 UTC 时刻,加上次日
9458
+ * 零点,然后取其中第一个晚于 `at` 的。这样算而不是「命中那一档的 end」,
9459
+ * 是因为**没命中**的那一支也需要一个正确的过期点(下一档随时会开始)。
9460
+ * 没有 overrides 时返回 `Infinity`:那份价不随时间变,记忆化可以一直留着。
9461
+ */
9462
+ declare function resolvePricingAt(raw: ModelMetaEntry['pricing'], at: number): PricingAt;
8888
9463
  /**
8889
9464
  * 检查模型是否支持 Prompt Caching [Hermes]
8890
9465
  */
@@ -8920,6 +9495,15 @@ declare class ProviderRouter {
8920
9495
  *
8921
9496
  * 单独存一份而不是每次去读 `config` 现算:`/model --reset` 要回到的是
8922
9497
  * 「启动时那个」,而 config 上的字段将来会被别的方案改(方案 22 的分层来源)。
9498
+ *
9499
+ * ⚠️ **「将来」是 2026-09-13,而这一格刻意不跟着它走。** 设置页改 `model` 从那天起
9500
+ * 会推进进程那份 `config.model`(`SettingApply` 的 `new-session` 一档),于是
9501
+ * `this.config.model` 和这一格会分家 —— **那是对的**:这个 router 服务的那段会话
9502
+ * 是用旧值开出来的,把它 `reset()` 到一个它从没待过的模型上,就是拿「以后默认用它」
9503
+ * 去回答「回到我开始时那个」。判据全文在 core 的 `config/write.ts` 的 `SettingApply` 上。
9504
+ *
9505
+ * 分家之后「`reset` 会把你送回哪儿」只有这一格答得上,所以它有了一个读口:
9506
+ * {@link getConfiguredModel}。
8923
9507
  */
8924
9508
  private readonly configSelection;
8925
9509
  /** 模型连续失败的节流状态(方案 26 验收 #8)。只在本会话内有效,不落盘 */
@@ -8939,6 +9523,16 @@ declare class ProviderRouter {
8939
9523
  constructor(config: EpochConfig);
8940
9524
  /** 当前生效的选择。回一份拷贝,免得调用方改到内部状态 */
8941
9525
  getSelection(): ModelSelection;
9526
+ /**
9527
+ * `resetSelection()` 会把这段会话送回哪个模型 —— 也就是**开这段会话那一刻**
9528
+ * 配置里写的那个。
9529
+ *
9530
+ * ⚠️ **宿主别拿 `config.model` 顶替它**(2026-09-13):那一格从设置页那条写路
9531
+ * 起会往前走,而这一格刻意不跟(判据在 {@link configSelection} 上)。顶替的表现是
9532
+ * 换模型菜单里「回到配置里那个」显示一个名字、按下去回到另一个 —— 而两个都是
9533
+ * 真实存在的模型,屏幕上没有任何东西会红。
9534
+ */
9535
+ getConfiguredModel(): string;
8942
9536
  /**
8943
9537
  * 换模型。`next.provider` 不给就是「同一家换个模型」。
8944
9538
  *
@@ -9092,6 +9686,26 @@ declare class ProviderRouter {
9092
9686
  */
9093
9687
  getApiKeyHint(): string;
9094
9688
  getCapability(): ModelCapability;
9689
+ /**
9690
+ * 「此刻这个模型到底认不认图」—— **实测一次**(2026-09-11)。
9691
+ *
9692
+ * 只在 `getCapability().imagesUnknown` 为真、而且用户这一轮真的发了图的时候
9693
+ * 才会被调到(判据在 `loop.ts` 闸门 1 的输入侧)。也就是说:**没人发图就
9694
+ * 永远不花这笔钱**,而发了图的那一次,用户本来就打算为这一轮付费。
9695
+ *
9696
+ * ## 为什么它在 router 上而不是在 loop 里
9697
+ *
9698
+ * 探针要一个**具体的 `LanguageModel` 实例**,而「此刻路由到哪一家、用哪把
9699
+ * key、走不走自建 baseUrl」整个只有 router 知道。放 loop 里等于让循环第二次
9700
+ * 学会造模型实例 —— 而它连 model id 都不该知道(`AgentProvider` 的判据)。
9701
+ *
9702
+ * ## ⚠️ 不走 `generate()`,也就是**不走降级**
9703
+ *
9704
+ * 这一发的宾语是「**这个**型号认不认图」。让它掉进 fallback 的话,回答会变成
9705
+ * 「降级之后那个型号认不认图」,然后被写进**原来那个型号**的缓存里 ——
9706
+ * 一个张冠李戴的实测结论比没有结论更难查。
9707
+ */
9708
+ probeImageSupport(): Promise<boolean>;
9095
9709
  hasPromptCaching(): boolean;
9096
9710
  /**
9097
9711
  * 估算一段文本占多少 token(真分词,见 `tokenizer.ts`)。
@@ -9105,16 +9719,99 @@ declare class ProviderRouter {
9105
9719
  }
9106
9720
 
9107
9721
  /**
9108
- * Provider 错误的分类:一个异常对象 → 「该怎么恢复」。
9722
+ * 「这个模型认不认图」—— **现问模型本人,不查表**(2026-09-11)。
9109
9723
  *
9110
- * 从 `router.ts` 拆出来(方案 26 PR-2)。理由不是行数,是**它是纯函数**:
9111
- * 输入一个错误、输出一档恢复动作,不碰状态、不碰网络。留在 router 里的时候,
9112
- * 想验「'maximum context length is 128500 tokens' 会不会被当成 500 服务器错误」
9113
- * 得先造一个有凭据的 router 出来 —— 而那跟这个判断毫无关系。
9114
- */
9115
- /** 恢复动作。降级链的每一档由它决定走到哪 */
9116
- type RecoveryAction = 'retry' | 'rotate_credential' | 'switch_model' | 'switch_provider' | 'abort';
9117
- declare class ProviderError extends Error {
9724
+ * ## 为什么需要它:查表这条路有一个它永远答不出的问题
9725
+ *
9726
+ * `model-metadata.ts` 的三层(静态表 → OpenRouter 缓存 → 默认值)全都是
9727
+ * **别人对这个模型的描述**。上游改一次名字,两层同时失效,第三层的默认值是
9728
+ * `supportsImages: false` —— 于是一个认图的模型在我们眼里瞎掉,而且是**静默**
9729
+ * 瞎掉:用户得到的是「当前模型不支持图片输入」,一句可核对的理由都没有。
9730
+ *
9731
+ * 2026-09-11 实测的现场:DeepSeek 把 `deepseek-v4-flash-vision-exp` 改名成
9732
+ * `deepseek-flash`,静态表 miss、OpenRouter 那份 872 条里**根本没有这个裸名**,
9733
+ * 于是 `epoch -i 图.png` 被我们自己拦死。而同一张图直接发给它,它答得一字不差。
9734
+ *
9735
+ * ## 判据是 **input token 涨没涨**,而这一格换过两次,两次都是被实测逼的
9736
+ *
9737
+ * ### 第一版:靠 warning —— 当天就否掉了
9738
+ *
9739
+ * 最省事的做法是不确定就照发,SDK 报了 warning(`droppedUserContentParts`)
9740
+ * 就说明它不认。**走不通**,同一天的 A/B:
9741
+ *
9742
+ * | 模型 | 出站 body | warnings | 回答 | input token |
9743
+ * | ----------------- | --------- | -------- | ----------- | ----------- |
9744
+ * | `deepseek-flash` | 带图 | `[]` | `"Red"` | 225 |
9745
+ * | `deepseek-v4-pro` | 带图 | `[]` | `"Unknown"` | 99 |
9746
+ *
9747
+ * 第二行就是那个坑:**它把图无声吞了,一条 warning 都不报**,SDK 和 provider
9748
+ * 两侧都认为这次请求很正常。同理「不报错就算认图」「答了话就算认图」也都被这一行
9749
+ * 否掉 —— 它不报错,它也答了话。
9750
+ *
9751
+ * ### 第二版:靠**回答**(发一张纯红图问颜色,答得出红就算)—— 2026-09-12 否掉
9752
+ *
9753
+ * 它把断言落在了模型的**识图准确率**上,而那个东西既不稳定、也不是我们的责任。
9754
+ * 同一张 132 字节的红方块、同一个型号(`deepseek-v4-flash-vision-exp`)、
9755
+ * **逐字节相同的出站 body**(钩 `fetch` 抓的,四次都是 `bodyLen=50948`),
9756
+ * 连着跑四次的回答是:
9757
+ *
9758
+ * | run | 回答 |
9759
+ * | --- | --------- |
9760
+ * | 1 | `Red` |
9761
+ * | 2 | `White` |
9762
+ * | 3 | `Magenta` |
9763
+ * | 4 | `Blue` |
9764
+ *
9765
+ * 也就是说这一版探针在这个型号上有约 3/4 的概率把一个**确实收到了图**的模型
9766
+ * 判成瞎子,然后把 `false` 写进缓存。
9767
+ *
9768
+ * ### 这一版:靠 input token 的**增量**
9769
+ *
9770
+ * 同一批运行里,回答四选一,而 `inputTokens` 纹丝不动 ——
9771
+ * 带图 21088、不带图 20904(各跑两次,两两分毫不差)。差的那 184 个 token
9772
+ * 就是那张图:**模型为它计了费,也就意味着它真的收下并处理了。**
9773
+ *
9774
+ * 于是探针发两发(同一句话,一发带图一发不带),比 `inputTokens`:
9775
+ *
9776
+ * - 涨了 → 图被接收(上表第一行,225 vs 99 也正是这个形状);
9777
+ * - 没涨 → 被无声吞了(上表第二行);
9778
+ * - 拿不到 usage → `inconclusive`,**不写缓存**。
9779
+ *
9780
+ * ⚠️ **这一版探不出「收了图但看不懂」**,这是刻意放弃的:那是模型质量,
9781
+ * 不在我们的责任边界内。上面那四次 `White` / `Magenta` / `Blue` 在新判据下
9782
+ * 全部算「认图」—— 而它们确实都认图,这正是想要的。
9783
+ *
9784
+ * ⚠️ 探针的两发**不共享长前缀、也够不着任何 prompt 缓存**(裸 `generateText`,
9785
+ * 没有 system prompt,整条消息一百多 token)。这一条是承重的:provider 一旦
9786
+ * 把 `inputTokens` 按「未命中缓存的部分」报,两发的数就不可比了。
9787
+ *
9788
+ * ## ⚠️ 第三档 `inconclusive` 的含义跟着换了
9789
+ *
9790
+ * 第二版里它指「正文是空的」(推理型号把输出预算花在思考上)。这一版**不看正文**,
9791
+ * 所以那一档现在指「**拿不到 usage**」(provider 不报、或基线那一发自己失败了)。
9792
+ * 处置没变:这一次按「不认图」走,但**一个字都不写缓存** —— 写进去会把一个
9793
+ * 认图的模型钉死(`true` 的缓存是 30 天)。
9794
+ */
9795
+
9796
+ /**
9797
+ * ⚠️ 2026-09-12 从 1 提到 2:判据从「答不答得出红」换成「input token 涨没涨」
9798
+ * (判据全文在文件头)。**旧结论是用另一把尺子量的**,其中的 `false` 有相当一部分
9799
+ * 是第二版那个 3/4 误判率产出的 —— 继续信它等于把一次已经作废的实验的结果
9800
+ * 当成这次的。整份丢掉,重新探。
9801
+ */
9802
+ declare const PROBE_VERSION = 2;
9803
+
9804
+ /**
9805
+ * Provider 错误的分类:一个异常对象 → 「该怎么恢复」。
9806
+ *
9807
+ * 从 `router.ts` 拆出来(方案 26 PR-2)。理由不是行数,是**它是纯函数**:
9808
+ * 输入一个错误、输出一档恢复动作,不碰状态、不碰网络。留在 router 里的时候,
9809
+ * 想验「'maximum context length is 128500 tokens' 会不会被当成 500 服务器错误」
9810
+ * 得先造一个有凭据的 router 出来 —— 而那跟这个判断毫无关系。
9811
+ */
9812
+ /** 恢复动作。降级链的每一档由它决定走到哪 */
9813
+ type RecoveryAction = 'retry' | 'rotate_credential' | 'switch_model' | 'switch_provider' | 'abort';
9814
+ declare class ProviderError extends Error {
9118
9815
  readonly code: number;
9119
9816
  readonly recoverable: boolean;
9120
9817
  readonly recovery: RecoveryAction;
@@ -9641,15 +10338,53 @@ declare function usdToMicros(usd: number): number;
9641
10338
  /**
9642
10339
  * 金额的人类可读形式。
9643
10340
  *
9644
- * 不到 1 分钱的时候 `$0.00` 等于没说,所以小额多给两位有效数字。
10341
+ * 不到 1 分钱的时候 `$0.00` 等于没说,所以小额多给两位有效数字 ——
10342
+ * 那条规则现在是 protocol 里 `formatMoney()` 的 `'adaptive'` 档,行为一字未改。
10343
+ *
10344
+ * ## ⚠️ 名字里的 `Usd` 说的是**入参**,不是输出
10345
+ *
10346
+ * 入参恒为美元(全仓的算术都是),而印出来的那一串跟着
10347
+ * `display.currency` 走 —— 配了人民币就是 `¥6.10`。这两件事的分界全文在
10348
+ * [protocol/src/money.ts](../../../protocol/src/money.ts) 文件头那张表。
10349
+ *
10350
+ * 名字**刻意没改成 `formatMoney`**:这个仓里 `Usd` 后缀的一贯含义就是
10351
+ * 「这一格装的是美元」(`costUsd`、`maxCostUsd`、`microUsd`),而它的入参正是。
10352
+ * 改名会让调用点看起来像是可以传别的币种进来 —— 那是这里最不想要的误解。
10353
+ *
10354
+ * @param digits 定死几位。缺省 `'adaptive'` = 这个函数的历史行为。
10355
+ * 闸门那几句文案给的是 `4`(**不是**口味:那几句印的是单轮 / 累计成本,
10356
+ * 常在 $0.003 这一档,两位会印成一个「免费」的假象)
9645
10357
  */
9646
- declare function formatUsd(usd: number): string;
10358
+ declare function formatUsd(usd: number, digits?: MoneyDigits): string;
9647
10359
  /**
9648
10360
  * 累加两份用量(含缓存 / 思考 / 总数四个可选维度),返回新对象。
9649
10361
  *
9650
10362
  * 可选维度加完仍是 0 就不写进结果 —— 保住「0 是真的零,undefined 是不知道」:
9651
10363
  * 两轮都没缓存命中,累计里就不该冒出一个 `cacheHitTokens: 0` 让人以为
9652
10364
  * 这个 provider 支持缓存只是没命中。
10365
+ *
10366
+ * ## ⚠️ `totalTokens` **不是**可加的维度,别照着上面三项写
10367
+ *
10368
+ * 上面三项缺席当 0 加是对的(0 是加法的单位元);`totalTokens` 不是,它是一个
10369
+ * **总量**。`(a.totalTokens ?? 0) + (b.totalTokens ?? 0)` 在「一边报了、另一边
10370
+ * 没报」时收到的只有报了的那一份,而 `inputTokens` / `outputTokens` 两侧是加全
10371
+ * 了的 —— 于是这份累计**自相矛盾**:`totalTokens < promptTokens + outputTokens`。
10372
+ *
10373
+ * 这一档不是假想:`toTokenUsage` 明确放行了「两个总量都在、`totalTokens` 缺席」
10374
+ * 的 usage(OpenAI 兼容端点不带 `stream_options` 的那一家就这样),而同一段会话
10375
+ * 里换 provider(降级、或压缩那一发走别的模型)就会把两种 usage 加到一起。
10376
+ *
10377
+ * 症状有两层,**第二层不是显示问题**:
10378
+ *
10379
+ * 1. 屏幕上「共 X token」比旁边 `↑ + ↓` 还小(两处都读 `totalUsageTokens()`,
10380
+ * 而它优先信 `totalTokens`);
10381
+ * 2. `budget.maxTokens` 那道闸门读的是同一个数(`budget-guard.ts` 的
10382
+ * `sessionBreach()`),偏小 = **上限拦不住**,多跑的那几轮是真花钱的。
10383
+ *
10384
+ * 所以没报的那一侧按我们自己的口径折算一份(`totalUsageTokens()` 的兜底那一支,
10385
+ * 对那一侧来说是精确值),再相加。两边都没报时仍然整个不写 —— 那时
10386
+ * `totalUsageTokens()` 自己算出来的和这里能算出来的是同一个数,凭空多一个字段
10387
+ * 只会让「provider 报过」和「我们替它算的」变得分不出来。
9653
10388
  */
9654
10389
  declare function addUsage(a: TokenUsage, b: TokenUsage): TokenUsage;
9655
10390
  /**
@@ -9675,6 +10410,58 @@ declare function estimateImageTokens(width?: number, height?: number): number |
9675
10410
  */
9676
10411
  declare function totalTokens(usage: TokenUsage): number;
9677
10412
 
10413
+ /**
10414
+ * 进程级的展示币种(2026-09-13)。
10415
+ *
10416
+ * ## 为什么是模块级插槽,而不是逐处穿参
10417
+ *
10418
+ * 判据逐条借 infra 的 `setLang()`,在这儿是原样成立的:金额的格式化点散在
10419
+ * core / runtime / cli / tui / web **五个包十几处**,而币种对它们中的每一个都是
10420
+ * **同一个进程级的一次性决定** —— 一个进程不会一半印美元一半印人民币。
10421
+ * 逐处穿参等于把一个显示偏好变成一次全仓签名改造,而 infra 里 `setLogLevel` /
10422
+ * `setShell` / `setSecretStore` 早就是这个形态。
10423
+ *
10424
+ * ## ⚠️ 它住在 core,不是 infra —— 因为 `CurrencyDisplay` 住在 protocol
10425
+ *
10426
+ * infra 是**零内部依赖包**(`check-layers.mjs` 的 `ALLOWED` 里它的内部依赖是空的),
10427
+ * import 不了 protocol,而这个插槽装的正是一个 protocol 类型。放 infra 就得在
10428
+ * infra 里再声明一遍那个形状 —— 那是 `LANGS` 在两边各写一份的老问题,而那一份
10429
+ * 有一条「两边行为逐项相等」的门禁兜着,这一份不值得再开一条。
10430
+ *
10431
+ * 代价是 **tui 够不着**(它不许 import core)。那一侧走宿主注入,形状和它那份
10432
+ * `t()` 逐字一样 —— 判据在 [tui/src/money.ts](../../../tui/src/money.ts)。
10433
+ *
10434
+ * ## 没设过的时候:`undefined` = 美元
10435
+ *
10436
+ * 这不是兜底的敷衍,它是**默认路径本身**:绝大多数用户没配 `display.currency`,
10437
+ * 而 `formatMoney()` 在 `currency` 缺席时的输出与 2026-09-13 之前逐字节相同
10438
+ * (判据在 protocol 的 `money.ts` 文件头最后一节)。
10439
+ *
10440
+ * ⚠️ 所以这里**不给一个 `{code:'USD', rate:1}` 的默认值**。给了的话「没配过」和
10441
+ * 「配了美元」在下游就分不出来了,而 `WireConfigResponse.currency` 那一格的语义
10442
+ * 恰恰是「没配就没有这个键」。
10443
+ */
10444
+
10445
+ /**
10446
+ * 钉住这个进程的展示币种。装配层读完配置后调一次。
10447
+ *
10448
+ * **生产路径上只有 `cli/src/language.ts` 的 `applyConfiguredCurrency()` 调它**,
10449
+ * 和 `setLang()` 在同两个入口、同一个位置 —— 理由也一样:它必须跑在**任何一句
10450
+ * 带金额的文案之前**,而最早那几句是启动诊断里的预算行(`boot.budget_cost`)。
10451
+ *
10452
+ * 传 `undefined` 可以把它清回美元 —— 用例需要这条(进程是复用的,
10453
+ * 一个用例设过币种之后不还原,下一个用例会莫名其妙拿到人民币)。
10454
+ */
10455
+ declare function setDisplayCurrency(next: CurrencyDisplay | undefined): void;
10456
+ /**
10457
+ * 当前展示币种,没设过就是 `undefined`(= 美元)。
10458
+ *
10459
+ * ⚠️ **现读,别存成模块级常量。** 常量在 `setDisplayCurrency()` 之前就求值完了,
10460
+ * 币种会被永久钉在「没设过」那一档 —— 同 tui 那份 `t()` 文件头「别在模块级调
10461
+ * `t()`」那条,失败形态一模一样:不报错,只是永远印美元。
10462
+ */
10463
+ declare function displayCurrency(): CurrencyDisplay | undefined;
10464
+
9678
10465
  /**
9679
10466
  * 设置文件的形状与读取 —— 项目级 / 项目本地 / `--settings` / 企业托管**共用这一份**。
9680
10467
  *
@@ -10012,9 +10799,21 @@ declare function countRules(lists: PermissionRuleLists | undefined): number;
10012
10799
  * @param base `loadConfig()` 的产物(第 ①② 层)。**不会被修改**
10013
10800
  * @param projectRoot 项目根,取自 `resolveProjectRoot()` —— 必须和信任判定用的是
10014
10801
  * 同一个答案,否则会出现「诊断说未信任、实际却加载了」
10015
- * @param trusted 工作区是否已信任,取自 `decideTrust()`。本文件不自己判
10802
+ * @param execTrusted 工作区授过**执行类**吗(`TrustDecision.exec`),取自
10803
+ * `decideTrust()`。本文件不自己判
10804
+ *
10805
+ * ## ⚠️ ③④ 两层**整份**归执行类,不按字段拆(2026-09-11)
10806
+ *
10807
+ * 拆档之后有一个看着更细的选法:`permissions.allow` 归执行类,`model` /
10808
+ * `maxTurns` 那两个标量跟着读类回来。**不这么做**,判据是这一层的 `gated`
10809
+ * 语义已经是「读到了、也解析了,但只有 deny 被采纳」——再按字段切一刀,
10810
+ * 这一层就有了三种半生不熟的状态,而设置页那条六层来源链要为每一格分别解释
10811
+ * 「这一格为什么在、隔壁那格为什么不在」。
10812
+ *
10813
+ * 代价如实写着:只授了读类的工作区,它 `.epoch/settings.json` 里的 `model`
10814
+ * 不生效 —— 和拆档之前「未信任」那一档的行为**逐字节相同**,不是新增的限制。
10016
10815
  */
10017
- declare function resolveSettings(base: EpochConfig, projectRoot: string, trusted: boolean, opts?: ResolveSettingsOptions): SettingsResolution;
10816
+ declare function resolveSettings(base: EpochConfig, projectRoot: string, execTrusted: boolean, opts?: ResolveSettingsOptions): SettingsResolution;
10018
10817
 
10019
10818
  /**
10020
10819
  * 「为什么是这个值」—— 一个配置键赢在哪一层,以及被压住的层各自写了什么。
@@ -10266,6 +11065,52 @@ declare function hasProviderCredentials(homeDir?: string): boolean;
10266
11065
  * 所以丢弃也不会动到用户文件)。
10267
11066
  */
10268
11067
 
11068
+ /**
11069
+ * 自动压缩的内置阈值 —— 提示词占到上下文窗口的几成时开始压。
11070
+ *
11071
+ * **默认值的单一真源就是这一行**(本文件头那句话对它同样成立)。
11072
+ * `ContextCompressor` 直接 `new` 出来、没给 `threshold` 时也回落到它,
11073
+ * 走的是 import 而不是在那边再写一个数 —— 两处各写一份的话,
11074
+ * 调了这个数之后「配置里读到的默认值」和「引擎真的在用的默认值」会分叉,
11075
+ * 而设置页印的恰好是前者。
11076
+ *
11077
+ * ⚠️ 改这个数对**所有没配过这个键的人**生效,是一次用户能感知的行为变更
11078
+ * (压缩来得更早或更晚),要写 changeset。
11079
+ *
11080
+ * ## ⚠️ 2026-09-11:`0.5` → `0.8`
11081
+ *
11082
+ * `0.5` 从来不是推出来的 —— 这一格的 JSDoc 自己写着它编码的是「改造前的行为」。
11083
+ * 它的实际含义是:**一个 200k 窗口的模型,提示词到 100k 就开始丢真实上下文,
11084
+ * 而那一半窗口永远用不到。**
11085
+ *
11086
+ * 丢的是什么不抽象:压缩留下 `protectFirstN: 1` + 一段摘要 + `protectLastN: 5`,
11087
+ * 而那个 5 是**消息条数不是轮数** —— agent 循环里一次工具往返就占两条
11088
+ * (assistant 的 tool-call + tool 的结果),所以压完之后模型手上真实的工具记录
11089
+ * 只剩最近两次往返。读过的文件正文、diff、报错原文,一律降级成摘要里的一句
11090
+ * 「曾经读过某个文件」。
11091
+ *
11092
+ * ### 为什么抬它是安全的:安全那一半根本不归它管
11093
+ *
11094
+ * 这条是**软线**,判据在 protocol 的 `isContextNearlyFull` / `OVERSIZE_RATIO`
11095
+ * 那张表上:软线问的是「该压了吗(省钱)」,越线之后**冷却期和熔断说不压就不压**;
11096
+ * 而「这一发还发得出去吗」是另一条**硬线**(`OVERSIZE_RATIO = 0.9`,冷却期让路)。
11097
+ * 也就是说撞窗口那一档 2026-09-10 起已经由硬线兜住,软线纯粹是一个成本旋钮。
11098
+ *
11099
+ * ### 为什么是 0.8 而不是照抄 0.9
11100
+ *
11101
+ * 同类产品的数:**Codex 的自动压缩线是窗口的 90%**,而且配置只能把它调更小
11102
+ * (`ModelInfo::auto_compact_token_limit()`:`(context_window * 9) / 10`,
11103
+ * 再 `min(config_limit, …)`;用例钉着 400k → 360k)。
11104
+ *
11105
+ * 我们**不取 0.9**,因为那会和硬线重合,而两条线在这个仓库里是**不同的机制**:
11106
+ * 重合之后软线永远不会先开火,每一次压缩都从硬线那条路走 —— 而硬线**不认冷却期**。
11107
+ * 结果是摘要调用变多而不是变少,等于把 2026-09-10 那一笔的冷却期删掉。
11108
+ * 0.8 留出 10% 的带宽给软线先试一次(它带冷却和熔断),硬线照旧在 0.9 兜底。
11109
+ *
11110
+ * ⚠️ 顺带一个正向效果:`prune.ts` 那一步(裁完重新量、够了就不调模型)的判定
11111
+ * 也吃这个阈值,抬高之后「裁一裁就够了、不用打摘要请求」的场景变多。
11112
+ */
11113
+ declare const DEFAULT_COMPRESSION_THRESHOLD = 0.8;
10269
11114
  declare const EpochConfigSchema: z.ZodObject<{
10270
11115
  provider: z.ZodPrefault<z.ZodObject<{
10271
11116
  type: z.ZodDefault<z.ZodEnum<{
@@ -10371,6 +11216,11 @@ declare const EpochConfigSchema: z.ZodObject<{
10371
11216
  enabled: z.ZodDefault<z.ZodBoolean>;
10372
11217
  threshold: z.ZodDefault<z.ZodNumber>;
10373
11218
  }, z.core.$strip>>;
11219
+ memory: z.ZodPrefault<z.ZodObject<{
11220
+ memoryCharLimit: z.ZodDefault<z.ZodInt>;
11221
+ userCharLimit: z.ZodDefault<z.ZodInt>;
11222
+ indexCharLimit: z.ZodDefault<z.ZodInt>;
11223
+ }, z.core.$strip>>;
10374
11224
  artifacts: z.ZodPrefault<z.ZodObject<{
10375
11225
  maxAgeDays: z.ZodDefault<z.ZodInt>;
10376
11226
  maxTotalBytes: z.ZodDefault<z.ZodInt>;
@@ -10449,6 +11299,13 @@ declare const EpochConfigSchema: z.ZodObject<{
10449
11299
  zh: "zh";
10450
11300
  en: "en";
10451
11301
  }>>;
11302
+ currency: z.ZodOptional<z.ZodObject<{
11303
+ code: z.ZodEnum<{
11304
+ USD: "USD";
11305
+ CNY: "CNY";
11306
+ }>;
11307
+ rate: z.ZodNumber;
11308
+ }, z.core.$strip>>;
10452
11309
  }, z.core.$strip>>;
10453
11310
  brand: z.ZodOptional<z.ZodString>;
10454
11311
  sidebarMenu: z.ZodOptional<z.ZodArray<z.ZodObject<{
@@ -10565,7 +11422,7 @@ interface ConfigMigration {
10565
11422
  * 见下面 {@link SETTING_WRITE_LAYERS} 和 {@link settingWriteTargets}。这是三件里
10566
11423
  * 唯一有硬约束的一件。
10567
11424
  *
10568
- * ### 3. 写完要重启,而且那是一句关于**进程**的话
11425
+ * ### 3. 写完要重启 —— 除了 `model`,它 2026-09-13 分叉成了「新开的会话就生效」
10569
11426
  *
10570
11427
  * 见 {@link SettingApply}。
10571
11428
  *
@@ -10717,7 +11574,7 @@ declare function isSettingWriteLayer(value: string): value is SettingWriteLayer;
10717
11574
  * 安全中心指两件事(「新会话从哪一档起」vs「这个会话此刻哪一档」),
10718
11575
  * 而那正是决定 20 ④ 拆开的那个歧义 —— 记住上一次那一档没有把这个歧义请回来。
10719
11576
  */
10720
- declare const SETTING_WRITE_KEYS: readonly ["model", "maxTurns", "models.utility", "fallback.model", "contextLength", "compression.enabled", "compression.threshold", "budget.maxCostUsd", "budget.maxTokens", "budget.warnAtPercent", "budget.onUnknownPricing", "permission"];
11577
+ declare const SETTING_WRITE_KEYS: readonly ["model", "maxTurns", "models.utility", "fallback.model", "contextLength", "compression.enabled", "compression.threshold", "memory.memoryCharLimit", "memory.userCharLimit", "memory.indexCharLimit", "budget.maxCostUsd", "budget.maxTokens", "budget.warnAtPercent", "budget.onUnknownPricing", "permission"];
10721
11578
  type SettingWriteKey = (typeof SETTING_WRITE_KEYS)[number];
10722
11579
  /** 这个键写得进去吗。名单就是 {@link SETTING_WRITE_KEYS} 本身,别在别处抄一份字面量 */
10723
11580
  declare function isWritableSettingKey(value: string): value is SettingWriteKey;
@@ -10738,33 +11595,87 @@ declare function settingWriteLayers(key: string): readonly SettingWriteLayer[];
10738
11595
  /**
10739
11596
  * 写完要做什么它才生效。
10740
11597
  *
10741
- * ## 今天只有 `restart` 一档,而这是一句关于**进程**的话
11598
+ * ## 两档,而它们都是关于**进程**的话
10742
11599
  *
10743
11600
  * `loadConfig()` 在装配时跑一次,之后这个进程手上那份 `config` 再也不重读。
10744
- * 所以落盘的那一刻:
11601
+ * 所以对绝大多数键,落盘的那一刻:
10745
11602
  *
10746
11603
  * - 正在跑的这一轮当然不变(`maxTurns` 在 `AgentLoop` 构造时就定死了)
10747
11604
  * - **新开一个会话也不变** —— 新会话用的还是同一份进程级 `config`
10748
11605
  * - 连 `GET /api/sessions/:id/settings` 回的值都还是旧的(它读
10749
11606
  * `services.baseConfig` / `services.settingsLayers`,那两样都是装配期的快照)
10750
11607
  *
10751
- * 于是「重启」在这条路上的准确含义是**重启 `epoch web` 那个进程**,不是
10752
- * 「新开一个会话」。这条必须说破:不说的话,用户改完 `model`、新建一个会话、
10753
- * 发现还是旧模型,然后对着一个没变的行为发呆 —— 而这正是设计稿第三问要防的事。
11608
+ * 于是 `restart` 的准确含义是**重启 `epoch web` 那个进程**,不是「新开一个会话」。
11609
+ * 这条必须说破:不说的话,用户改完、新建一个会话、发现行为没变,然后对着一个
11610
+ * 没动静的界面发呆 —— 而这正是设计稿第三问要防的事。
11611
+ *
11612
+ * ## ✅ `model` 2026-09-13 分叉出去了,它是 `new-session`
11613
+ *
11614
+ * 这一节上一版写着「今天只有 `restart` 一档」,并且逐字预告了这一天:
10754
11615
  *
10755
- * ## 为什么不摆一档 `newSession` / `live` 占着位
11616
+ * > 「什么时候生效」将来真会按键分叉:`model` 这个进程里已经有一条运行期换模型的
11617
+ * > 路(`ModelControl`),哪天设置页接上它,`model` 就先变成另一档,而 `maxTurns`
11618
+ * > 还是 `restart`。那一天加的是这个联合的第二个成员。
11619
+ *
11620
+ * 接上的那根线只有一处(runtime 的 `advanceModelDefault`):写赢了就推进进程那份
11621
+ * `config.model`,而 `SessionModels.open()` 建新会话的 router 时**现读**它。
11622
+ *
11623
+ * ⚠️ **是 `new-session` 不是 `live`,这个名字是判据本身。** 已经在跑的那些会话
11624
+ * 一个都不动,连它们 `reset()` 的目标(`ProviderRouter.configSelection`)都不动 ——
11625
+ * 每段会话带着它出生那一刻的配置跑。两条理由:
11626
+ *
11627
+ * 1. `wire-model.ts` 那张对照表把两条路分成「现在换个脑子试试」(顶栏,会话级、
11628
+ * 立刻、不写盘)和「以后默认用它」(这一条,写盘)。`new-session` 正好是第二句,
11629
+ * 而 `live` 会把用户在顶栏明确挑过的那个选择顶掉 —— 那恰恰是那张表在防的事;
11630
+ * 2. 判据逐字照 `SessionFactoryOptions.mainRole` 的「它是**这次启动**的参数」,
11631
+ * 这里只是把「这次启动」收窄成「开这段会话那一刻」。
11632
+ *
11633
+ * 于是这条路上**没有半失败这回事**:不过 `applySelection` 那两道前置检查
11634
+ * (会话里有图片 / 窗口装不装得下),一次赋值就完了。MCP 那条 `保存 → 应用`
11635
+ * 拆成两发的判据(「写盘是毫秒、握手不是,半失败是常态」)在这儿不成立。
11636
+ *
11637
+ * ## 仍然不摆 `live` 那一档占位
10756
11638
  *
10757
11639
  * 同 `WireSettingLayer` 里 `plugin` 那一档刻意缺席的判据:摆一档永远不会出现的
10758
- * 值,等于告诉读代码的人「有些键改完新开个会话就行」,而那是假的。
11640
+ * 值,等于告诉读代码的人「有些键改完当场就生效」,而那是假的。哪天真做了再加。
11641
+ */
11642
+ type SettingApply =
11643
+ /** 要重启 `epoch web` 那个进程。除 `model` 外所有可写键都是这一档 */
11644
+ 'restart'
11645
+ /**
11646
+ * **此后新开的会话**用新值,这个进程正在跑的那些不变、也不用重启。
10759
11647
  *
10760
- * 它挂在**每一个(键, 层)**上而不是整份响应上,是因为「什么时候生效」将来真会
10761
- * 按键分叉:`model` 这个进程里已经有一条运行期换模型的路(`ModelControl`),
10762
- * 哪天设置页接上它,`model` 就先变成另一档,而 `maxTurns` 还是 `restart`。
10763
- * 那一天加的是这个联合的第二个成员,不是把一句话从响应上拆到行上。
11648
+ * 今天只有 `model` 走得到,而且还要 {@link SettingApplyFacts.canOpenSessions}
11649
+ * 为真 —— 见那个字段。
10764
11650
  */
10765
- type SettingApply = 'restart';
10766
- /** 见 {@link SettingApply}。落在这一层是因为「写完怎么办」的判据在这个文件里 */
10767
- declare const SETTING_WRITE_APPLY: SettingApply;
11651
+ | 'new-session';
11652
+ /**
11653
+ * 算 {@link SettingApply} 要的那一件**事实**(规则在 core,事实在装配层)。
11654
+ *
11655
+ * 分法逐字照 `SelectionContext` 那句「判据在 core,事实在会话,runtime 不替宿主猜」:
11656
+ * 「`model` 改完新会话就生效」这条规则属于这个文件,而「这个进程开不开得出新会话」
11657
+ * 只有 runtime 知道。
11658
+ */
11659
+ interface SettingApplyFacts {
11660
+ /**
11661
+ * 这个进程建得出新会话吗(`services.provider !== null`)。
11662
+ *
11663
+ * ⚠️ **为假时 `model` 退回 `restart`,这一条不是保守起见。** provider 层起不来时
11664
+ * `SessionRuntimeFactory.assemble()` 当场回 null,这个进程**一个会话都建不出来** ——
11665
+ * 那时说「新开的会话会用它」是一句假话,而用户的下一步确实是重启。
11666
+ *
11667
+ * 这也正是「还没配 key,先存一个模型名」那一屏(`web/src/settings/config.tsx` 的
11668
+ * `no_provider_restart`)照旧要说「存完要重启」的原因:那一屏的前提就是这里为假。
11669
+ */
11670
+ canOpenSessions: boolean;
11671
+ }
11672
+ /**
11673
+ * 这个键写完要做什么才生效。**全仓唯一一处按键分叉的地方。**
11674
+ *
11675
+ * 不做成一张 `Record<string, SettingApply>`:可写键有十几个而分叉的只有一个,
11676
+ * 一张表会让「除了 model 全是 restart」这句话散在十几行里,加键的人得挨个填。
11677
+ */
11678
+ declare function settingWriteApply(key: string, facts: SettingApplyFacts): SettingApply;
10768
11679
  /**
10769
11680
  * 这一层排第几。**`unknown` 回 `null`,而那不是兜底**。
10770
11681
  *
@@ -10822,6 +11733,14 @@ interface SettingWriteTargetsOptions {
10822
11733
  * ⚠️ 它必须和 `paths.projectLocal` 来自**同一个工作区**,判据见文件头第四节。
10823
11734
  */
10824
11735
  trusted: boolean;
11736
+ /**
11737
+ * 算 {@link SettingWriteTarget.apply} 要的那件事实。见 {@link SettingApplyFacts}。
11738
+ *
11739
+ * ⚠️ **`rows()` 和 `write()` 两条路必须递同一份。** 递不同的话,用户按下去之前
11740
+ * 看到的是「新开的会话就生效」、按完回执说「要重启」—— 而这一屏存在的全部理由
11741
+ * 就是「写之前那句话是真的」。
11742
+ */
11743
+ apply: SettingApplyFacts;
10825
11744
  }
10826
11745
  /**
10827
11746
  * 一行能写进哪几层,以及**写之前**就说得出来的那句话。
@@ -11041,24 +11960,107 @@ declare function loadKeybindings({ path, defaults }: LoadKeybindingsOptions): Lo
11041
11960
  /**
11042
11961
  * 记忆管理器 — 文件驱动 + SQLite 索引双存储。
11043
11962
  *
11044
- * 参考 Hermes tools/memory_tool.py(1240 行)。
11045
11963
  * 核心设计:
11046
11964
  * 1. MEMORY.md / USER.md 文件是主存储
11047
11965
  * 2. SQLite 是搜索索引(FTS5 + sqlite-vec)
11048
11966
  * 3. 会话级冻结快照,保护 prompt cache
11049
11967
  * 4. 字符配额 + 超限压缩提示(而非直接拒绝)
11050
11968
  * 5. 注入检测
11969
+ *
11970
+ * ---
11971
+ *
11972
+ * # 2026-09-10:进 prompt 的从「全文」换成「索引」
11973
+ *
11974
+ * ## 改之前是什么样,以及为什么它把两件事焊死了
11975
+ *
11976
+ * 原来 `initialize()` 把**整个记忆库的正文**渲染进 system prompt(`renderBlock()`
11977
+ * 把每一条 `§` 拼起来)。参考的是 Hermes(`tools/memory_tool.py`,它今天仍是
11978
+ * 这个形状)。后果是「能存多少」和「每轮烧多少 token」**是同一个数**:
11979
+ * 想多记一点,就得每一轮都为它付钱。
11980
+ *
11981
+ * 于是那两个配额被钉在 `2200 / 1375`(连数字都是从 Hermes 抄的)好几个月,
11982
+ * 而用户的原话是「memory 好像只有 2000 字,太少了」。在旧结构下这个抱怨
11983
+ * **没有便宜的解法** —— 调大就是每轮变贵。
11984
+ *
11985
+ * ## 改之后:目录进 prompt,正文按需取
11986
+ *
11987
+ * 形状对齐 codex(`codex-rs/ext/memories/`):那边 `~/.codex/memories/` 里的
11988
+ * 文件数量不设上限,进 developer instructions 的只有一份摘要(它那个常量叫
11989
+ * `MEMORY_TOOL_DEVELOPER_INSTRUCTIONS_SUMMARY_TOKEN_LIMIT`,2500 tokens),
11990
+ * 正文靠 `memories.list` / `read` / `search` 三个工具按需取。
11991
+ *
11992
+ * epoch 这边**检索那一半本来就修好了**,所以这一轮做的比看上去小:
11993
+ *
11994
+ * - `search()` 是真的 BM25(FTS5,见 `db.ts`)
11995
+ * - `prefetch()` 每轮拿用户输入检索一次,塞进 `<memory-recall>`
11996
+ * (`agent/prompt.ts`,走 messages 那一侧)
11997
+ *
11998
+ * 缺的只有两样,这一轮补上:**system prompt 里换成索引**({@link renderIndexBlock}),
11999
+ * 以及**一个能按 id 取全文的动作**({@link read})。
12000
+ *
12001
+ * ## ⚠️ 空库时索引块必须是**空串**,这不是优化
12002
+ *
12003
+ * `agent/prompt.ts` 把这个块 push 进 system prompt,而 system prompt 全文进
12004
+ * eval 的新鲜度指纹(`evals/harness/fingerprint.ts`)。空库渲染成空串,
12005
+ * 是 34 段录像在这一轮里**不因为 system prompt 而**作废的唯一原因
12006
+ * (工具描述那一侧另算,见 `tools/builtin/memory.ts`)。
12007
+ * 改这个函数时先想一遍「库是空的时候它返回什么」。
11051
12008
  */
11052
12009
 
11053
12010
  declare class MemoryManager {
11054
12011
  private db;
11055
12012
  private config;
11056
12013
  private memDir;
11057
- /** 冻结快照——会话初始化时构建,整个会话不变 [Hermes] */
12014
+ /**
12015
+ * 冻结快照 —— 会话初始化时构建,整个会话不变 [Hermes]。
12016
+ *
12017
+ * ⚠️ **2026-09-10 起它装的是索引不是正文**(文件头那一节)。冻结这件事没变,
12018
+ * 变的是冻的是什么:冻正文是为了保 prefix cache,冻索引除了保 cache 还多一层
12019
+ * 作用 —— 模型手上那份 id 表在一个会话里不会动,于是它引用 `m:a3f1c2` 时
12020
+ * 指的一定是它当初看到的那一条。
12021
+ */
11058
12022
  private snapshot;
12023
+ /**
12024
+ * 短 id → 条目。**这一份是活的,不跟着快照冻**。
12025
+ *
12026
+ * 两者不同步是刻意的:快照冻住「模型看得到的目录」,这一份答「这个 id 现在
12027
+ * 还指着什么」。会话中途新写的条目立刻 `read` 得到(它不在冻结的索引里,
12028
+ * 但模型刚写完自己知道 id);被改掉的条目老 id 会落进
12029
+ * {@link MemoryReadResult.missing} —— 那是实话,那条内容确实不在了。
12030
+ *
12031
+ * id 从**内容哈希**来而不是从位置来,正是为了这里:删掉第 3 条不会让第 4 条
12032
+ * 之后所有 id 集体平移,而那种平移会让模型手上那份索引静默指错条目。
12033
+ */
12034
+ private ids;
12035
+ /** 上面那张表的反向索引(`target\0content` → id),渲染索引时按内容反查用 */
12036
+ private idByContent;
11059
12037
  /** 当前会话是否已初始化 */
11060
12038
  private initialized;
11061
- /** 本轮 consolidation 失败计数 [Hermes] */
12039
+ /**
12040
+ * 本轮 consolidation **白撞**次数。撞满 {@link MAX_CONSOLIDATION_FAILURES_PER_TURN}
12041
+ * 就不再给指引,改成一句「别再试了」。
12042
+ *
12043
+ * ## ⚠️ 它数的是「白撞了几次」,不是「试了几次」
12044
+ *
12045
+ * 这个区别就是用户 2026-09-10 那张截图:
12046
+ *
12047
+ * > 记忆库满了(3 次合并机会已用完),但关键信息已经落在 RUNBOOK.md 里
12048
+ *
12049
+ * 一个**谨慎**的模型会一条条腾地方 —— 删一条、试一下够不够、不够再删一条。
12050
+ * 而原来只有 `add` / `replace` **成功**时才重置,`remove` 成功不重置,于是:
12051
+ *
12052
+ * ```
12053
+ * add(失败1) → remove → add(失败2) → remove → add(失败3) → remove → add(失败4) → 叫停
12054
+ * ```
12055
+ *
12056
+ * 它每一步都在真的腾空间,却被记成四次失败,最后被自己的安全阀打死。
12057
+ * **越谨慎越容易撞**:一次性删五条的反而没事。上限本身是对的
12058
+ * (防模型无限重试同一个 `add`),错的是口径。
12059
+ *
12060
+ * 所以**任何一次让库变小 / 变整齐的操作都重置它**:`add` 成功、`replace`
12061
+ * 成功、`remove` 成功。剩下那一档 —— 反复 `add` 同一个装不下的东西而中间
12062
+ * 什么都没干 —— 才是它要拦的。
12063
+ */
11062
12064
  private consolidationFailures;
11063
12065
  /** 上次审查发现的问题(由 MemoryReviewer 写入) */
11064
12066
  reviewIssues: Array<{
@@ -11067,12 +12069,33 @@ declare class MemoryManager {
11067
12069
  message: string;
11068
12070
  suggestion?: string;
11069
12071
  }>;
11070
- constructor(dbPath?: string, memDir?: string);
12072
+ /**
12073
+ * @param config 三个配额,来自 `EpochConfig.memory`(runtime 的 `services.ts` 递进来)。
12074
+ * **按字段合并到默认值上**,不是整份替换 —— 用户只在 yaml 里写了
12075
+ * `memoryCharLimit` 时,另外两个该回落到内置默认,而不是变成 `undefined`
12076
+ * 然后在 `getBudget()` 里算出 `NaN%`。
12077
+ *
12078
+ * 不给 = 三个都走默认(真源在 `config/schema.ts`)。2026-09-10 之前这个参数
12079
+ * 压根不存在,于是 `MemoryConfig` 这个类型有、却没有任何人给得出非默认值 ——
12080
+ * 用户抱怨的「设置里改不了」就是这一格。
12081
+ */
12082
+ constructor(dbPath?: string, memDir?: string, config?: Partial<MemoryConfig>);
11071
12083
  initialize(_sessionId: string): Promise<string>;
11072
12084
  prefetch(userQuery: string): Promise<string>;
11073
12085
  sync(_userMessage: string, _assistantResponse: string): Promise<void>;
11074
12086
  finalize(): Promise<void>;
11075
12087
  reloadFromDisk(): Promise<void>;
12088
+ /**
12089
+ * 重建 短 id → 条目 的映射。
12090
+ *
12091
+ * id = `<前缀>:<内容 sha256 的前 N 位十六进制>`。前缀 `m` / `u` 分开两个库,
12092
+ * 于是**同一份内容同时存在两个库里也不会撞**(那是合法状态:「用户用 pnpm」
12093
+ * 既可以是画像也可以是项目知识)。
12094
+ *
12095
+ * 冲突时按 4 位一档加长,而**只加长撞上的那一个** —— 全表统一加长的话,
12096
+ * 加一条新记忆可能让**所有**老 id 变样,而模型手上那份索引是冻结的。
12097
+ */
12098
+ private assignIds;
11076
12099
  /**
11077
12100
  * 把某个 target 的内容同步成 `desired`,**保留仍存在条目的元数据**。
11078
12101
  *
@@ -11080,6 +12103,14 @@ declare class MemoryManager {
11080
12103
  * accessCount / createdAt 全部归零,schema 里那几个字段等于白设。
11081
12104
  */
11082
12105
  private syncTarget;
12106
+ /**
12107
+ * 落盘。**顺带重建 id 表** —— 这个方法是 add / replace / remove 三条写路径
12108
+ * 唯一的汇合点,挂在这儿就不会出现「新加了一条记忆,同一轮里 `read` 不到」。
12109
+ *
12110
+ * ⚠️ 重建的是 {@link ids}(活的),**不动 {@link snapshot}**(冻的)。
12111
+ * 顺手把快照也刷了的话,一次写记忆就会改掉 system prompt ——
12112
+ * prefix cache 当场作废,而那正是快照冻结要挡的事。
12113
+ */
11083
12114
  flushToDisk(): Promise<void>;
11084
12115
  /** 渲染成可被 readFile 原样解析回来的格式(多行条目也不会散) */
11085
12116
  private renderFile;
@@ -11089,9 +12120,28 @@ declare class MemoryManager {
11089
12120
  * score 是真实的 BM25 相关度(越大越相关),原来恒为 1——
11090
12121
  * 上层拿它排序等于没排。命中的条目会记一次访问,
11091
12122
  * 让 accessCount 真的反映使用情况。
12123
+ *
12124
+ * ⚠️ **返回的 `id` 是索引里那个短 id,不是 DB 的 UUID**(2026-09-10 改)。
12125
+ * 这一条上层看得见:模型拿搜索结果去 `read` / 对照索引时,两处的 id 必须是
12126
+ * 同一套词。发 UUID 出去等于给了它一个在别处一次也用不上的标识符 ——
12127
+ * 而它会照着用,然后 `read` 一律 `missing`。
12128
+ *
12129
+ * 短 id 反查不到时(`assignIds()` 还没跑过)**回落到 UUID 而不是丢掉这一条**:
12130
+ * 少一条搜索结果是静默的错误答案,多一个认不出的 id 只是一次 `missing`。
11092
12131
  */
11093
12132
  search(query: string, limit?: number): Promise<MemorySearchResult[]>;
11094
12133
  keywordSearch(query: string, limit?: number): Promise<MemorySearchResult[]>;
12134
+ /**
12135
+ * 按索引里的短 id 取全文(2026-09-10 新增,见文件头)。
12136
+ *
12137
+ * ⚠️ **认不出的 id 走 `missing` 而不是被跳过。** 模型会编 id(或者引用上个会话
12138
+ * 那份索引里的),静默少返回一条的话它拿到的是一份看起来完整、实际缺一块的
12139
+ * 答案,而它没有任何办法发现这件事。
12140
+ *
12141
+ * 命中的条目记一次访问 —— 于是「被 read 过」也算热度,下一个会话的索引里
12142
+ * 它更靠前(判据在 {@link renderIndexBlock} 的排序那一段)。
12143
+ */
12144
+ read(ids: readonly string[]): Promise<MemoryReadResult>;
11095
12145
  /** 获取所有条目(供后台审查使用) */
11096
12146
  getAllEntries(): Array<{
11097
12147
  content: string;
@@ -11106,7 +12156,43 @@ declare class MemoryManager {
11106
12156
  semanticSearch(query: string, limit?: number): Promise<MemorySearchResult[]>;
11107
12157
  add(target: MemoryTarget, content: string): Promise<MemoryWriteResult>;
11108
12158
  replace(target: MemoryTarget, oldText: string, content: string): Promise<MemoryWriteResult>;
11109
- remove(target: MemoryTarget, oldText: string): Promise<void>;
12159
+ /**
12160
+ * 删一条。`oldText` 可以是**全文**,也可以是索引里那个**短 id**(见 {@link locate})。
12161
+ *
12162
+ * 返回删没删掉 —— 原来固定返回 `void`,于是「id 拼错了」和「真的删了」
12163
+ * 在调用方那里长得一模一样,模型会以为腾出了地方然后继续撞墙。
12164
+ */
12165
+ remove(target: MemoryTarget, oldText: string): Promise<boolean>;
12166
+ /**
12167
+ * 按「全文」或「索引里的短 id」定位一条记忆。
12168
+ *
12169
+ * ## 为什么要认 id(2026-09-10)
12170
+ *
12171
+ * 索引化之后,模型在 prompt 里看到的只有**首行摘要**(截断到 80 字符),
12172
+ * 而 `replace` / `remove` 原来走的是 `WHERE content = ?` —— **全文精确相等**。
12173
+ * 两者对不上:**我们只发布了摘要,却要求全文**。于是「照着屏幕上看得见的
12174
+ * 东西去合并」这条最自然的路径必然 `Entry not found`。
12175
+ *
12176
+ * ⚠️ **它不是一个「不修就必死」的回归,别照那个力度去理解。** 2026-09-10
12177
+ * 拿真 key 打过 A/B(3 个场景 × 改动前后 × 各 3 发,DeepSeek):**不认 id 的
12178
+ * 那一侧也 9/9 成功** —— 模型会自己先 `read` 一遍拿全文,之后全文匹配就成立了。
12179
+ * `read` 和索引是同一轮加的,所以那条死路从来没有以「不可恢复」的形态存在过。
12180
+ *
12181
+ * 认 id 的价值是把它从「先 read 再匹配」缩成「一步」,并且让**只看索引**
12182
+ * 这条路径也成立。这是补齐操作面,不是抢救。
12183
+ *
12184
+ * ⚠️ 先试 id 再试全文,顺序不能反:一条内容恰好长得像 `m:abc123` 的记忆
12185
+ * 理论上存在,但那时按 id 解释才是**模型的本意**(它手上的 id 就长这样)。
12186
+ */
12187
+ private locate;
12188
+ /**
12189
+ * 「没找到」时**告诉模型下一步干什么**,而不是只说没找到。
12190
+ *
12191
+ * 一句光秃秃的 `Entry not found` 会让模型原地重试同一个错的 `old_text`,
12192
+ * 而它重试几次就撞上 consolidation 上限。绝大多数情况下它错在
12193
+ * **拿索引里的摘要当全文**,所以这句话要直接点破那一点。
12194
+ */
12195
+ private notFoundHint;
11110
12196
  getBudget(): {
11111
12197
  memory: BudgetInfo;
11112
12198
  user: BudgetInfo;
@@ -11127,11 +12213,53 @@ declare class MemoryManager {
11127
12213
  * 项目约定来说并不宽裕,原来的做法会把合法长条目直接吞掉。
11128
12214
  */
11129
12215
  private scanInjection;
11130
- /** 渲染 system prompt 注入块 [Hermes] */
11131
- private renderBlock;
11132
- /** 构建快照 system prompt 注入块 */
11133
- private buildSnapshotBlock;
12216
+ /**
12217
+ * 一条条目在索引里长什么样。摘要取**首行**并截断到
12218
+ * {@link INDEX_SUMMARY_CHARS}。
12219
+ *
12220
+ * 取首行而不是前 N 字符:条目是「一条一个事实」,第一行基本就是那句结论,
12221
+ * 而横跨换行去截会把下一行的开头黏上来,读起来像一句破句。
12222
+ */
12223
+ private indexEntries;
12224
+ /**
12225
+ * 渲染进 system prompt 的**索引块**(2026-09-10 起替代原来的全文块)。
12226
+ *
12227
+ * ## 空库返回空串 —— 这一条是硬约束
12228
+ *
12229
+ * 判据在文件头最后一节:这个块的返回值进 system prompt,而 system prompt
12230
+ * 全文进 eval 新鲜度指纹。改这个函数时先答「库是空的时候它返回什么」。
12231
+ *
12232
+ * ## 装不下的时候丢谁
12233
+ *
12234
+ * 按「用得多的留下」排(`accessCount` 降序,同票看 `lastAccessedAt`),
12235
+ * 取到装不下为止 —— 那两个字段 `db.ts` 一直在维护,而 `search()` 命中时会
12236
+ * `markAccessed()`,所以「用得多」是真的使用情况,不是猜的。
12237
+ *
12238
+ * ⚠️ **挑完之后按 position 重排再印**。挑选顺序是「谁重要」,展示顺序是
12239
+ * 「文件里的顺序」—— 两者混成一个的话,用户在 `MEMORY.md` 里看到的顺序和
12240
+ * 模型看到的顺序不一样,而排查「模型为什么没用那条记忆」时人是对着文件看的。
12241
+ *
12242
+ * ⚠️ 被挤掉的部分**在块尾如实说有几条**,并指向 `search`。静默截断是这里
12243
+ * 最不该干的事:模型会把「我看到的」当成「全部」,然后理直气壮地说
12244
+ * 「记忆里没有这件事」。
12245
+ */
12246
+ private renderIndexBlock;
12247
+ /** 索引里的一行:`m:a1b2c3 摘要 (412 chars)` */
12248
+ private indexLine;
11134
12249
  /** 构建超限压缩提示 [Hermes] */
12250
+ /**
12251
+ * 配额满时交给模型的指引。
12252
+ *
12253
+ * ## ⚠️ 这段话必须给出一条**在索引架构下真的走得通**的操作序列
12254
+ *
12255
+ * 上一版写的是「use replace() to combine or shorten existing entries」外加一个
12256
+ * 抄全文的例子。索引化之后那是一条**死路**:模型手上只有摘要,`old_text` 拼不出来
12257
+ * (判据全文在 {@link locate})。它照着做 → `Entry not found` → 重试 → 撞上
12258
+ * {@link MAX_CONSOLIDATION_FAILURES_PER_TURN} → 只好说「记忆库满了,合并机会用完」。
12259
+ *
12260
+ * 所以现在给的是**按 id 走**的三步,并且把最近几条 id 直接列出来 ——
12261
+ * 让模型不必先去猜、也不必多跑一趟 `search`。
12262
+ */
11135
12263
  private buildConsolidateHint;
11136
12264
  }
11137
12265
 
@@ -11709,7 +12837,54 @@ interface SessionMeta {
11709
12837
  cacheReadTokens: number;
11710
12838
  cacheWriteTokens: number;
11711
12839
  reasoningTokens: number;
11712
- estimatedCostUsd: number;
12840
+ /**
12841
+ * 这段会话累计**估算**花费(美元)。`recordUsage()` 逐轮累加(会话库 v9,2026-09-13)。
12842
+ *
12843
+ * ## ⚠️ `undefined` ≠ `0`,而这一格在 v9 之前**永远是 0**
12844
+ *
12845
+ * 它和上面五个 token 列是同一批从 Hermes 抄来的坑位,但那五列 2026-08 接上了
12846
+ * 真实来源,**这一格没有**:`manager.ts` 建行时写个 `0`,此后全仓没有第二个
12847
+ * 写入点。于是 `SessionSummary.costUsd`(`epoch sessions list` 和网线上那一格
12848
+ * 的来源)一直是 `0` —— 而按这个仓一以贯之的口径,**0 的意思是「免费」**。
12849
+ * 今天还没人把它画出来(`web/src/state/store.tsx` 那句「全仓没有一处读」),
12850
+ * 所以它是一个**等着被渲染的谎**,不是一个正在撒的谎。
12851
+ *
12852
+ * v9 之后两件事都对上了:
12853
+ *
12854
+ * | 这一格 | 意思 |
12855
+ * | --- | --- |
12856
+ * | `undefined` | **这段会话一轮都没算出过钱** —— 查不到单价(见 `cost/pricing.ts`),或者压根没跑过模型请求 |
12857
+ * | `0` | 算出来了,是零(免费模型) |
12858
+ * | `> 0` | 至少花了这么多 |
12859
+ *
12860
+ * 两者靠 `priced_call_count` 那一列分开(`estimated_cost_usd` 是
12861
+ * `REAL NOT NULL DEFAULT 0`,它自己装不下「不知道」)。
12862
+ *
12863
+ * ## 口径是「**至少**这么多」,同上面那五列
12864
+ *
12865
+ * 只累加 `TokenUsage.costUsd` 报得出来的那些轮;查不到单价的轮次一笔不记,
12866
+ * 不补 0 冒充。所以一段「有几轮算得出、有几轮算不出」的会话,这个数偏小 ——
12867
+ * 和花费页那一屏「总额自报是其中几轮的」是同一条规矩。
12868
+ */
12869
+ estimatedCostUsd?: number;
12870
+ /**
12871
+ * provider 账单上的**实际**花费。**今天永远是 `undefined`,这是有意的。**
12872
+ *
12873
+ * ⚠️ **别拿估算值填它。** 这一格的全部价值在于它和 {@link estimatedCostUsd}
12874
+ * 是两个不同的东西:一个是我们乘出来的,一个是对方收的。填成同一个数之后,
12875
+ * 「这个账对过没有」就再也问不出来了。
12876
+ *
12877
+ * 我们今天**拿不到**这个数:epoch-agent 的 provider 全是 OpenAI 兼容端点
12878
+ * (见 `config/schema.ts`),它们的响应里只有 token 用量,没有金额。
12879
+ * 隔壁两家能填是因为它们是自家后端的第一方客户端 —— codex 的
12880
+ * `turn_cost_worker` 每 150s 向自家 API 轮询已结算金额,hermes 走
12881
+ * `x-nous-credits-*` 响应头和 portal 账户。**这条路我们没有,所以留空是诚实的。**
12882
+ *
12883
+ * 将来真要接(比如某个 provider 开始在响应头里报账单),它要连着
12884
+ * `cost_source` / `cost_status` 一起来 —— 「这个数是谁说的、结算没有」
12885
+ * 和数字本身一样重要(那是 hermes 那套的形状)。只加一个数字列会让
12886
+ * 「估算」和「账单」在读侧分不出来。
12887
+ */
11713
12888
  actualCostUsd?: number;
11714
12889
  compressionIneffectiveCount: number;
11715
12890
  expiryFinalized: number;
@@ -12756,10 +13931,10 @@ declare const MAX_AUDIT_ENTRIES = 500;
12756
13931
  *
12757
13932
  * | 级别 | 意图 |
12758
13933
  * | ------------- | -------------------------------------------------- |
12759
- * | `bypass` | 全放行 —— 但**挡不住第 1 层安全底线**,那层在前面 |
12760
- * | `auto` | 信任 agent:读写和网络放行,非只读命令仍确认 |
13934
+ * | `bypass` | 全放行 —— 但**挡不住第 1 层那两条 deny**,那层在前面 |
13935
+ * | `auto` | 信任 agent:这一档自己不拦,边界全在第 1 层 |
12761
13936
  * | `plan` | 只读:写入 / 命令 / 网络 / 执行代码一律拒 |
12762
- * | `acceptEdits` | 工作区内读写免确认,工作区外和写命令确认 |
13937
+ * | `acceptEdits` | 同 `auto`,但**工作区外的文件**要确认 |
12763
13938
  * | `default` | 只读操作免确认,其余一律确认 |
12764
13939
  *
12765
13940
  * ⚠️ 这一层**只负责「没有规则命中时怎么办」**。它给出的 allow 不是最终答案 ——
@@ -12948,7 +14123,9 @@ declare function matchRuleLists(rules: CompiledRules, operation: Operation, flav
12948
14123
  *
12949
14124
  * check() 分三层,顺序即安全语义:
12950
14125
  * 1. 安全底线(危险命令 / 敏感路径 / 混淆构造)—— 任何级别、任何模式都生效,
12951
- * **不可**被 headless 预授权覆盖
14126
+ * **不可**被 headless 预授权覆盖。⚠️ 前两条直接 deny;第三条是「要人确认」,
14127
+ * 而「问」在 `bypass` 和非交互下问不出来 —— 那两种现场怎么塌成一个结论,
14128
+ * 判据全文在 {@link PermissionManager.settleUnaskable} 上
12952
14129
  * 2. 策略规则 + 审批缓存 + 权限级别 —— 得出 allow / deny / ask_user
12953
14130
  * 3. headless 预授权 —— 只能把 ask_user 变成放行,
12954
14131
  * 永远不能把第 1、2 层的 deny 变成放行
@@ -13145,6 +14322,111 @@ declare class PermissionManager implements PermissionManager$1 {
13145
14322
  check(operation: Operation): PermissionDecision;
13146
14323
  /** 三层判定的本体。**每个出口都必须带原因码**,见文件头 */
13147
14324
  private decide;
14325
+ /**
14326
+ * 安全底线里那条「要人确认」的判定 —— **问不出来的时候它塌成什么**(2026-09-10)。
14327
+ *
14328
+ * 返回 null = 问得出来,照原样弹给用户。
14329
+ *
14330
+ * ## 这一层要守的不变量:**「不问」这一档下没有任何东西可以塌成「问」**
14331
+ *
14332
+ * 两家参照实现都把它写成纪律,而 epoch 此前只守住了一半:
14333
+ *
14334
+ * - Hermes 靠顺序 —— `approval_floors.py` 的 docstring 第一句是
14335
+ * 「decisions that never reach a prompt」,floor 里只有 hardline 灾难命令、
14336
+ * `sudo -S` 灌密码、用户自己写的 deny 规则,**一个 ask 形状的出口都没有**;
14337
+ * 所有会弹的判定都排在 `_yolo_active()` 检查之后(`tools/approval.py`)。
14338
+ * - Codex 靠显式塌缩 —— `exec_policy.rs` 的原话是「We prefer to prompt the user
14339
+ * rather than outright forbid the command, but if the user has explicitly
14340
+ * disabled prompts, we must forbid the command」,落地成
14341
+ * `AskForApproval::Never => Decision::Forbidden`,另有 `prompt_is_rejected_by_policy`
14342
+ * 把**任何**要弹的东西在那一档下变成一句拒绝。
14343
+ *
14344
+ * 这个方法此前只有第 3 条(非交互),判据挂在「**有没有人可问**」上,漏了
14345
+ * 「**用户说了别问我**」—— 而这两件事在语义上是同一件。落地的后果是屏幕上
14346
+ * 那句「跳过全部检查 · 什么都不问」是假话:实测 `echo "$(date)"`、`node -e`、
14347
+ * `python3 -c` 在 `bypass` 档下照旧弹框。
14348
+ *
14349
+ * ## 四条的顺序不能换,每一条各有各的理由
14350
+ *
14351
+ * 1. **`plan` → 拒绝。** 那一档是只读的,「问一句就能干」这回事在它里面不存在。
14352
+ * ⚠️ 这一条修的是一个**旧洞**:底线那一层跑在档位判定前面,所以在这之前
14353
+ * plan 模式下一条判不了的命令会弹给用户,用户点「允许」它就真跑了 ——
14354
+ * 而 `checkPlan()` 那句「plan 模式禁止写入和命令执行」压根没机会说话。
14355
+ *
14356
+ * 2. **`bypass` + `obfuscated` → 放行。** 排在缓存前面,于是「`bypass` 整段不查
14357
+ * 审批缓存」({@link checkRulesAndCache} 那一节)对**规则那条路**逐字仍然成立。
14358
+ *
14359
+ * ⚠️ **认码,不是认「所有 ask 形状的底线」**(2026-09-10 用户拍的):
14360
+ * `dangerous-command`(tier-2,危险但可恢复)在这一档下**照旧问**。
14361
+ * 用户的原话是「除了那种真的非常危险的操作,和无法判断的才会……停下来找
14362
+ * 用户」,而他随后选定的两档形状是:**危险的问一句(点了就能干活)、
14363
+ * 判不了的不问**。所以这一支只塌 `obfuscated`。
14364
+ *
14365
+ * ⚠️ 塌向「放行」而不是 Codex 的「拒绝」:这条判定的原话是「静态检查无法
14366
+ * 判定其真实行为,需人工确认」—— 它是一句「我判不了」,不是一句「这危险」,
14367
+ * 而 `bypass` 的字面意思正是「别拿你判不了的事来问我」。这一档下**逐字就是
14368
+ * Hermes 的 yolo**(它的 floor 只有 hardline,判不了那一族住在 Tier-2、
14369
+ * 被 yolo 整段跳过)。
14370
+ *
14371
+ * ⚠️ **代价说清楚**:`bypass` + 提示注入 + `$(…)` 拼命令名这条路由此打通。
14372
+ * Hermes 在 `tools/approval.py` 那句 `# this and bypass every approval check
14373
+ * (prompt-injection escalation path)` 上承认的是同一个敞口。不接受的人该做的是
14374
+ * **别用这一档**,而不是让这一档偷偷弹框 —— 一个说了「不问」却还在问的开关,
14375
+ * 会让用户学会闭眼点「允许」,那比敞口更坏。
14376
+ *
14377
+ * 3. **用户以前对这一条明确答过 → 照他的答复。** 这是另一个缺陷:审批缓存躺在
14378
+ * 这一层**后面**,所以一条判不了 / tier-2 危险的命令上点一百次「永久允许」,
14379
+ * 第一百零一次照样弹 —— 那个出口在这一族上是**画着但不通**的。
14380
+ *
14381
+ * ⚠️ 这里查缓存**不受 `approvalCacheActive()` 约束**(那条只管规则那条路):
14382
+ * 走到这儿说明这一刻真的要弹窗,而缓存的定义就是「弹窗答案的记忆」。
14383
+ * 2026-08-17 那条判据(`bypass` 下缓存的 deny 不该拦住任何东西)没有被翻 ——
14384
+ * 它说的是**档位本来就放行**的操作,而这一支下面的操作档位从来不放行。
14385
+ *
14386
+ * 这不是「让缓存绕过黑名单」:绕不过的是 deny 形状的那两条(灾难级危险命令、
14387
+ * 敏感路径),它们在 `hard.requireApproval` 为假时压根走不到这儿。
14388
+ *
14389
+ * 4. **非交互 → 拒绝**(原有行为,一字未动)。它排在最后:前面几条各自都是
14390
+ * 「这道题已经有答案了」,而这一条说的是「没人可答」。
14391
+ */
14392
+ private settleUnaskable;
14393
+ /**
14394
+ * **规则那条路**上会不会查审批缓存(2026-08-17 的判据:`bypass` 整段不查,
14395
+ * 全文在 {@link checkRulesAndCache} 上)。
14396
+ *
14397
+ * ⚠️ **它不是「这个档位记不记得住」**:安全底线那条路({@link settleUnaskable})
14398
+ * 在任何档位下都查缓存。宿主要知道的是**这一次问的这道题**记不记得住,
14399
+ * 那个答案在 `PermissionDecision.rememberable` 上,由 {@link rememberFlag} 打。
14400
+ */
14401
+ approvalCacheActive(): boolean;
14402
+ /**
14403
+ * 给规则表那条路上的 ask 打「记不记得住」这一格。
14404
+ *
14405
+ * 只在**记不住**的时候产出这个键:`{ rememberable: undefined }` 和「没有这个键」
14406
+ * 在 JSON / wire 上是两回事(后者会被编成一个 null),而协议那边写的是
14407
+ * **不给 = 记得住**。
14408
+ *
14409
+ * 走到这儿的 ask 是规则表判的,所以它和 {@link approvalCacheActive} 同一个答案;
14410
+ * 底线那条路不经过这里(它的答复一律记得住)。
14411
+ */
14412
+ private rememberFlag;
14413
+ /**
14414
+ * 审批缓存那一格 —— **全类唯一一处查它的地方**。
14415
+ *
14416
+ * 两个调用方({@link checkRulesAndCache} 的第 2 层、{@link settleUnaskable} 的
14417
+ * 第 2 条)必须用同一套 key,否则「批过的那一条」在其中一条路上永远命不中;
14418
+ * 而那种分叉两边各自都自洽,没有任何检查抓得到。
14419
+ *
14420
+ * key 用「工具名 + 目标」:按类别做 key 会让拒绝一次 `code_exec` 连坐到
14421
+ * terminal 和所有 MCP 工具。
14422
+ *
14423
+ * ⚠️ **它自己不认档位。** 「`bypass` 不查缓存」那条判据只管**规则那条路**,
14424
+ * 所以那道闸在 {@link checkRulesAndCache} 的调用点上(`approvalCacheActive()`),
14425
+ * 不在这里 —— {@link settleUnaskable} 那条路必须在任何档位下都查得到,
14426
+ * 判据在它的第 3 条上。搬进来的后果是「问过一次的 tier-2 命令在 `bypass` 下
14427
+ * 永远问不完」。
14428
+ */
14429
+ private cachedDecision;
13148
14430
  /**
13149
14431
  * 策略规则 + 紧凑权限规则 + 审批缓存。返回 null 表示三者都没定论,
13150
14432
  * 交给权限级别判。
@@ -13168,8 +14450,9 @@ declare class PermissionManager implements PermissionManager$1 {
13168
14450
  * 档下对一条命令点了「拒绝」,`tool-executor` 把那一下写成
13169
14451
  * `recordApproval(…, 'deny')`;随后切到 `bypass` 再敲同一条,屏幕上回的是
13170
14452
  * 「操作被拦截 [权限]: 已缓存拒绝」。而三处真源都说它该过:
13171
- * [docs/PERMISSIONS.md](../../../../docs/PERMISSIONS.md) 的档位表写「允许一切」、
13172
- * [by-level.ts](./by-level.ts) 那张五档表写「全放行 —— 但挡不住第 1 层安全底线」、
14453
+ * [docs/PERMISSIONS.md](../../../../docs/PERMISSIONS.md) 的档位表(当时写的是
14454
+ * 「允许一切」,2026-09-10 改成「不问你任何事」)、
14455
+ * [by-level.ts](./by-level.ts) 那张五档表写「全放行 —— 但挡不住第 1 层那两条 deny」、
13173
14456
  * 方案 46 §七 写「`bypass` 之下危险命令表是**唯一还在岗的东西**」。
13174
14457
  *
13175
14458
  * 这一档下**没有被放松的东西**:第 1 层安全底线(危险命令表 / 敏感路径 /
@@ -14863,8 +16146,8 @@ declare const PolicyRuleSchema: z.ZodObject<{
14863
16146
  command_prefix: z.ZodOptional<z.ZodString>;
14864
16147
  target_pattern: z.ZodOptional<z.ZodString>;
14865
16148
  decision: z.ZodEnum<{
14866
- deny: "deny";
14867
16149
  allow: "allow";
16150
+ deny: "deny";
14868
16151
  ask_user: "ask_user";
14869
16152
  }>;
14870
16153
  deny_message: z.ZodOptional<z.ZodString>;
@@ -14906,11 +16189,15 @@ interface ResolvePolicyDirsOptions {
14906
16189
  /** 项目根目录。不给就不加载项目级策略 */
14907
16190
  projectRoot?: string;
14908
16191
  /**
14909
- * 工作区是否受信任。**必传**,没有默认值 ——
14910
- * 给个 `= true` 的默认参数就等于「忘了传的人自动获得信任」,
16192
+ * 工作区授过**执行类**吗(`TrustDecision.exec`,不是 `trusted`)。**必传**,
16193
+ * 没有默认值 —— 给个 `= true` 的默认参数就等于「忘了传的人自动获得信任」,
14911
16194
  * 与 trust/gate.ts 的 fail closed 立场相反。
16195
+ *
16196
+ * ⚠️ 2026-09-11 从 `trusted` 改名到这一格,判据同 `hook/sources.ts` 上那一条:
16197
+ * 两个都是 `boolean`,只有改名才能让漏改的调用点编译不过。
16198
+ * 策略规则和 hook 归同一档 —— 它不进上下文,但它直接改「哪些操作不用问就放行」。
14912
16199
  */
14913
- trusted: boolean;
16200
+ execTrusted: boolean;
14914
16201
  /** 给了就把取舍写进启动诊断 */
14915
16202
  diags?: DiagnosticSink;
14916
16203
  }
@@ -15011,10 +16298,15 @@ interface ResolveComplianceDirsOptions {
15011
16298
  /** 项目根目录。不给就不加载项目级词库 */
15012
16299
  projectRoot?: string;
15013
16300
  /**
15014
- * 工作区是否受信任。**必传**,没有默认值 ——
15015
- * 给个 `= true` 的默认参数就等于「忘了传的人自动获得信任」。
16301
+ * 工作区授过**执行类**吗(`TrustDecision.exec`,不是 `trusted`)。**必传**,
16302
+ * 没有默认值 —— 给个 `= true` 的默认参数就等于「忘了传的人自动获得信任」。
16303
+ *
16304
+ * ⚠️ **2026-09-11 归到执行类那一档,而这一条比 hook / policy 更容易判错。**
16305
+ * 词库看着像「只读的一张表」,但项目级的 `exempt.txt` 能把内置规则一条条
16306
+ * 豁免掉 —— 它**能放松**合规(文件头第二节写着这件事)。档位的判据从来不是
16307
+ * 「它执行不执行命令」,是「它能不能放宽一个安全判定」,而这一样能。
15016
16308
  */
15017
- trusted: boolean;
16309
+ execTrusted: boolean;
15018
16310
  /** 给了就把取舍写进启动诊断 */
15019
16311
  diags?: DiagnosticSink;
15020
16312
  }
@@ -15314,11 +16606,19 @@ declare class HookManager implements HookManager$1 {
15314
16606
  *
15315
16607
  * ## 项目级 hook 的闸门比别的来源严一档
15316
16608
  *
15317
- * | 信任状态 | 项目级 hook |
15318
- * | -------- | ----------- |
15319
- * | 已信任 | 生效 |
15320
- * | 未决定 | **不生效** |
15321
- * | 已拒绝 | **不生效** |
16609
+ * | 信任状态 | 项目级 hook |
16610
+ * | ----------------- | ----------- |
16611
+ * | 已信任 + 执行类 | 生效 |
16612
+ * | 已信任,仅读类 | **不生效** |
16613
+ * | 未决定 | **不生效** |
16614
+ * | 已拒绝 | **不生效** |
16615
+ *
16616
+ * ⚠️ **第二行是 2026-09-11 新长出来的一档**,也是这个文件本轮唯一的改动:
16617
+ * 它读的不再是 `TrustDecision.trusted` 而是 `.exec`({@link LoadHookSourcesOptions.execTrusted})。
16618
+ * 那之前,用户在一句只说了「本目录的项目指令会加载」的问话下点的「信任」,
16619
+ * 同时把这张表上的第一行也给了 —— 而这张表上的每一项都是一条要 spawn 子进程的
16620
+ * 命令。换句话说下面那段「clone 完还没看过的仓库里一行 `command: curl … | sh`
16621
+ * 就是任意代码执行」**在闸门自己这一侧也曾经成立**:问话没提过它。
15322
16622
  *
15323
16623
  * 方案 22 对项目级设置留了个不对称 —— 未信任时 `deny` 照样生效(一份提交进仓库的
15324
16624
  * `settings.json` 能替你收紧、不能替你放开)。**这里没有对应的例外**:hook 的每一项
@@ -15379,11 +16679,16 @@ interface HookSourcesOptions {
15379
16679
  */
15380
16680
  projectRoot?: string;
15381
16681
  /**
15382
- * 工作区是否受信任。**必传**,没有默认值 ——
15383
- * 给个 `= true` 的默认参数就等于「忘了传的人自动获得信任」,
16682
+ * 工作区授过**执行类**吗(`TrustDecision.exec`,不是 `trusted`)。**必传**,
16683
+ * 没有默认值 —— 给个 `= true` 的默认参数就等于「忘了传的人自动获得信任」,
15384
16684
  * 与 trust/gate.ts 的 fail closed 立场相反。
16685
+ *
16686
+ * ⚠️ **2026-09-11 从 `trusted` 改名到这一格,而改名本身就是这次改动的一半。**
16687
+ * 两个都是 `boolean`,传错了类型检查一个字都不报 —— 改名之后漏改的调用点
16688
+ * 当场编译不过,那是这次唯一一道编译期能给的保护。
16689
+ * 档位判据在 [protocol 的 `TrustTier`](../../../protocol/src/trust.ts)。
15385
16690
  */
15386
- trusted: boolean;
16691
+ execTrusted: boolean;
15387
16692
  /** `config.yaml` 里遗留的 `hooks` 字段(旧格式)。给了就照常生效,并报一条迁移提示 */
15388
16693
  legacyHooks?: Record<string, unknown>;
15389
16694
  /** 给了就把取舍和问题写进启动诊断 */
@@ -15855,7 +17160,7 @@ declare function scanSkillDirs(dirs: readonly SkillDirSpec[]): SkillScanOutcome;
15855
17160
  * | -------------- | -------------------------------------------- | ------------------------------- |
15856
17161
  * | 单个 `.md` | 路径是文件且以 `.md` 结尾 | `general/<名字>/SKILL.md` |
15857
17162
  * | 技能文件夹 | 目录里**顶层**就有 `SKILL.md` | `general/<名字>/…` |
15858
- * | 技能树 | 目录里是 `<分类>/<名字>/SKILL.md` | `<分类>/<名字>/…`(分类保留) |
17163
+ * | 技能树 | 顶层没有 `SKILL.md` —— **递归找**,见下 | `<分类>/<名字>/…` |
15859
17164
  * | `.zip`/`.tgz` | 路径是文件且不是 `.md` | 解开之后按上面两种目录形态再判 |
15860
17165
  *
15861
17166
  * 「技能树」这一档不在那句承诺里,是顺带的:`scanPluginDir` 数插件技能用的就是
@@ -15863,6 +17168,59 @@ declare function scanSkillDirs(dirs: readonly SkillDirSpec[]): SkillScanOutcome;
15863
17168
  * 判据是**顶层有没有 `SKILL.md`**,不是「有几个条目」—— 同 `extractArchive`
15864
17169
  * 摊平单根目录时那条「判据是清单在哪」。
15865
17170
  *
17171
+ * ## ⚠️ 「技能树」那一档是**递归**的,不是固定两层(2026-09-13 改)
17172
+ *
17173
+ * 原来 `stageTree` 只下潜**恰好两层**(`<分类>/<名字>/SKILL.md`)。那条规矩对
17174
+ * `~/.epoch/skills/` 自己是对的(`scanSkillDirs` 就是两层),但它让这条路在
17175
+ * **别人的技能目录**上整个失效:`~/.claude/skills/` 是**一层**
17176
+ * (`<名字>/SKILL.md`),指它进来时顶层没有 `SKILL.md` ⇒ 判成 `tree` ⇒ 去找
17177
+ * `<名字>/<子目录>/SKILL.md` ⇒ 一个都找不到 ⇒ 回 `empty`,而 issues 里**没有一句话
17178
+ * 解释为什么**。而 web 那一屏的文件头恰恰把 `~/.claude/skills` 列为「最常被导入的」。
17179
+ *
17180
+ * 所以改成 {@link findSkillDirs}:从根往下走,**任何深度**的 `SKILL.md` 都算。
17181
+ * 判据抄的是隔壁两家的交集(两边都这么干,形状高度一致):
17182
+ *
17183
+ * - codex `codex-rs/ext/skills/src/loader/discovery.rs` —— 递归走,深度上限 6,
17184
+ * 跳过 `.` 开头的目录,名字取 frontmatter 的 `name` 否则**父目录名**;
17185
+ * - hermes-agent `agent/skill_utils.py:736` 的 `iter_skill_index_files` ——
17186
+ * `os.walk` 不限深度,带一张噪声黑名单,名字同样是 frontmatter 否则父目录名。
17187
+ *
17188
+ * 四条规矩,每条都有它自己的理由:
17189
+ *
17190
+ * 1. **找到 `X/SKILL.md` 就不再往 X 里面钻。** X 里除了那四个白名单子目录本来
17191
+ * 也一个字节都不收(见下一节),继续钻只会把一份技能自带的**示例**
17192
+ * (`references/examples/xxx/SKILL.md` 这种)当成另一份技能收进来。
17193
+ * hermes 那边是只剪掉支持目录,我们比它更硬一档 —— 因为我们的技能目录布局
17194
+ * 本来就封死了;
17195
+ * 2. **`.` 开头的目录不进**(同 `scanSkillDirs`,也同 codex)。顺带盖住
17196
+ * `.git` / `.venv` / `.tox` 这一整族,staging 目录自己也靠这一条不被扫到;
17197
+ * 3. **{@link SCAN_SKIP_DIRS} 那几个不进。** 只有非点开头的那几个,逐个抄
17198
+ * hermes 的 `EXCLUDED_SKILL_DIRS`。⚠️ **`dist` / `build` / `target` 不在里面** ——
17199
+ * 两家都没剪它们,而剪掉的代价是一个真叫 `build` 的分类底下的技能静默消失;
17200
+ * 4. **三道上限**(深度 / 目录数 / 技能数)。防的是有人把一整个 monorepo 拖进来,
17201
+ * 而它们撞上时**要说一句话**(`import_scan_truncated`)—— 静默截断在这条路上
17202
+ * 读起来就是「你那儿只有这些」。
17203
+ *
17204
+ * ## 分类和名字怎么从一条深路径上取出来
17205
+ *
17206
+ * 引擎自己的布局只有两层(`<分类>/<名字>`),而递归找到的路径可以有任意多层,
17207
+ * 所以要把它压回两层。规矩在 {@link placeOf}:**倒数第一段是名字,倒数第二段是
17208
+ * 分类**,没有倒数第二段就落 `general`。于是:
17209
+ *
17210
+ * | 找到的位置 | 分类 | 名字 |
17211
+ * | ----------------------- | --------- | -------- |
17212
+ * | `<根>/SKILL.md` | `general` | 根目录名 |
17213
+ * | `<根>/foo/SKILL.md` | `general` | `foo` |
17214
+ * | `<根>/cat/foo/SKILL.md` | `cat` | `foo` |
17215
+ * | `<根>/a/b/c/SKILL.md` | `b` | `c` |
17216
+ *
17217
+ * 第三行就是**原来那条两层规矩**,逐字不变;第二行是这次修的那格。
17218
+ *
17219
+ * ⚠️ 分类那一段**不合法时退回 `general`,不是把整份技能丢掉**:名字非法是「这一份
17220
+ * 没法要」(它要当目录名,而且是技能的身份),分类非法只是「没法照着分」——
17221
+ * 为后者丢掉一份好技能,用户拿到的是一条他改不动的路(那个目录名不是他起的)。
17222
+ * 名字那一道仍然在 {@link stageSkillDir} 里,一个字没松。
17223
+ *
15866
17224
  * ## ⚠️ 抄过去的东西有白名单,而且它和 `skill_write_file` 是同一份
15867
17225
  *
15868
17226
  * {@link SKILL_ASSET_DIRS} 就是 `SkillSystem.writeFile` 那个 `allowedPrefixes`。
@@ -16404,7 +17762,85 @@ declare function readImageSize(bytes: Uint8Array): ImageSize | undefined;
16404
17762
  declare function sniffMediaType(bytes: Uint8Array): string | undefined;
16405
17763
 
16406
17764
  /**
16407
- * `~/.epoch/` 的磁盘总账 —— 两条各管各的上限,从此写在同一张表里(方案 47 PR-4)。
17765
+ * 附件目录的清理策略(2026-09-11)。
17766
+ *
17767
+ * 用户从输入框附上来的文件落在 `~/.epoch/attachments/<会话 id>/`
17768
+ * (判据在 infra 的 `attachmentsDir` 上)。**单个附件不设上限**是拍过的决定,
17769
+ * 于是「攒起来的总量」这一格就成了这条路上唯一的闸 —— 不设它的话,
17770
+ * 一个天天拖日志进来的用户几周就能攒出几个 GB,而这些文件在会话结束后
17771
+ * 基本没人再看。
17772
+ *
17773
+ * ## 算法一个字都不重写:直接复用 `pruneArtifacts`
17774
+ *
17775
+ * 那个函数**第一个参数就是根目录**,artifact 相关的东西只剩名字和默认配额。
17776
+ * 照着抄一份「按天 + 按容量 + 当前会话永不删」的实现出来,代价是同一条规矩
17777
+ * 有了两个真源:哪天有人给 artifact 那份加了「软链不跟随」,attachment 这份
17778
+ * 静悄悄地还是老样子,而两处的差异要等到一次事故才有人发现。
17779
+ *
17780
+ * 所以这一份只提供**两样 attachment 自己的东西**:根目录,和配额。
17781
+ *
17782
+ * ## ⚠️ 这条线兑现的不是「盘写不满」
17783
+ *
17784
+ * 驱逐按会话目录整个来、从最旧的删起,而**当前会话目录永不参与**
17785
+ * (那是 `pruneArtifacts` 的语义,判据在它自己那一格上:正在跑的会话把自己的
17786
+ * 附件删掉,是比磁盘涨得慢更糟的事)。于是一次 2 GB 的上传照样落得进来。
17787
+ * 它兑现的是「历史会话的附件不会无限攒着」,仅此而已 ——
17788
+ * 全文在 [disk-ledger.ts](../disk-ledger.js) 的 `ATTACHMENT_MAX_TOTAL_BYTES` 上。
17789
+ */
17790
+ /**
17791
+ * 保留策略。**结构上与 `ArtifactRetention` 相同,名字刻意分开**。
17792
+ *
17793
+ * 共用一个类型名的代价是「调 artifact 的保留天数」和「调 attachment 的保留天数」
17794
+ * 在类型系统里长得一模一样 —— 而它们是两个可以各自调的数(判据同 disk-ledger
17795
+ * 那张表:两者丢了的代价不一样)。两个名字之后,配置层要分开暴露哪一个,
17796
+ * 是一次看得见的选择而不是一次手滑。
17797
+ */
17798
+ interface AttachmentRetention {
17799
+ /** 超过这个天数的会话目录整个删掉 */
17800
+ maxAgeDays: number;
17801
+ /** attachments 根目录的总容量上限(字节) */
17802
+ maxTotalBytes: number;
17803
+ }
17804
+ /**
17805
+ * 内置默认。天数跟 artifact 走(7 天),容量来自 [disk-ledger.ts](../disk-ledger.js)。
17806
+ *
17807
+ * ⚠️ **天数这一格是「跟着走」,不是「必须一样」**:它今天等于 artifact 那个数,
17808
+ * 是因为没有任何测量支撑把它们分开,不是因为有一条规矩要求它们相等。
17809
+ * 哪天要调,调一个不影响另一个 —— 这正是上面那两个类型名分开的用处。
17810
+ */
17811
+ declare const ATTACHMENT_RETENTION_DEFAULTS: AttachmentRetention;
17812
+ /**
17813
+ * 清理 attachments 根目录。返回删掉的会话目录数(给诊断用)。
17814
+ *
17815
+ * @param root attachments 根目录(`attachmentsDir()` **不带 sessionId** 的那个)
17816
+ * @param keepSession 当前会话目录名,永不删除
17817
+ * @param now 当前时间戳。**显式传入**是为了让测试可复现,
17818
+ * 而不是在实现里埋一个 `Date.now()`
17819
+ */
17820
+ declare function pruneAttachments(root: string, keepSession?: string, retention?: AttachmentRetention, now?: number): number;
17821
+ /**
17822
+ * 删一段会话时连带删掉它的附件目录。
17823
+ *
17824
+ * 三处刻意和 {@link pruneAttachments} 不一样,判据逐字同 `removeSessionArtifacts`:
17825
+ * **不吞异常**(这是用户按下去的动作,不是 housekeeping)、**异步**、
17826
+ * **`force: true` 所以「本来就没有」= 成功**(绝大多数会话一个附件都没有)。
17827
+ *
17828
+ * ⚠️ 这一条比 artifact 那一条更要紧:附件是**用户自己的文件**(一份合同、
17829
+ * 一份体检报告)。点了删除的人以为它跟着没了,而不设这一条的话它还会在盘上
17830
+ * 躺最多 7 天。
17831
+ *
17832
+ * @param homeDir `~/.epoch` 那一层。**不能省** —— 省了就落回全局家目录,
17833
+ * 于是宿主自己那份数据目录里的附件永远删不掉,而用户全局的那份被误删
17834
+ * (同 `removeSessionArtifacts`,方案 54 §二那条漏接)
17835
+ */
17836
+ declare function removeSessionAttachments(sessionId: string, homeDir?: string): Promise<void>;
17837
+
17838
+ /**
17839
+ * `~/.epoch/` 的磁盘总账 —— 几条各管各的上限,从此写在同一张表里(方案 47 PR-4)。
17840
+ *
17841
+ * > 2026-09-11 进来第三条(`attachments`,用户从输入框附上来的文件)。
17842
+ * > 下面这段讲的是当初为什么要有这张表,**它的理由没变**:新加的那一条同样
17843
+ * > 只在这里写一次数,执行者在别处取值。
16408
17844
  *
16409
17845
  * ## 这张表答的是一个一直没人答的问题
16410
17846
  *
@@ -16414,7 +17850,7 @@ declare function sniffMediaType(bytes: Uint8Array): string | undefined;
16414
17850
  * **差 2.5 倍而谁也没参照过谁**。于是 `~/.epoch/` 的真实天花板是
16415
17851
  * 「200 + 512 + 会话库随便涨」——这句话谁都没写下来过,也就没人能说它对不对。
16416
17852
  *
16417
- * 现在两处都从这里取值。**改一个数就得在这张表上改**,那句天花板随之算得出来。
17853
+ * 现在每一处都从这里取值。**改一个数就得在这张表上改**,那句天花板随之算得出来。
16418
17854
  *
16419
17855
  * ## 立的是「一本账」,不是一条会驱逐的联合上限 —— 三条判据
16420
17856
  *
@@ -16460,25 +17896,52 @@ declare function sniffMediaType(bytes: Uint8Array): string | undefined;
16460
17896
  declare const CHECKPOINT_MAX_TOTAL_BYTES: number;
16461
17897
  /** 全部会话的 artifact 合计。执行者:`pruneArtifacts` */
16462
17898
  declare const ARTIFACT_MAX_TOTAL_BYTES: number;
17899
+ /**
17900
+ * 全部会话的 attachment 合计(2026-09-11)。执行者:`pruneAttachments`。
17901
+ *
17902
+ * ## 这个数的来历,和上面两个一样诚实:**没有测量依据**
17903
+ *
17904
+ * 取和 artifact 同一个数,判据是上面那张对照表里的同两栏 —— attachment 也是
17905
+ * 「单件大、丢了还能再有」那一族(原件在用户自己盘上,重新拖一次就回来了),
17906
+ * 所以它该站在 512 这一边而不是 checkpoint 的 200 那一边。
17907
+ *
17908
+ * ## ⚠️ 它**不是**单个附件的上限,两件事别混
17909
+ *
17910
+ * 单个附件**不设上限**(2026-09-11 拍的:上传走流式落盘,不整份读进内存)。
17911
+ * 这一格管的是「攒起来的总量」,执行方式同 artifact —— 按会话目录整个删、
17912
+ * 从最旧的删起、**当前会话永不删**。
17913
+ *
17914
+ * 于是有一个必须说清楚的后果:**一次 2 GB 的上传不会被这条线拦住**,
17915
+ * 它落在当前会话目录里,而当前会话目录不参与驱逐。这条线兑现的是
17916
+ * 「历史会话的附件不会无限攒着」,不是「这台机器的盘写不满」。
17917
+ * 真要后者,得给单文件加上限 —— 而那是被明确拒过的那条路。
17918
+ */
17919
+ declare const ATTACHMENT_MAX_TOTAL_BYTES: number;
16463
17920
  /**
16464
17921
  * 一本账,`null` = 这一格不受管。
16465
17922
  *
16466
- * `rest` 那一格是这张表里最要紧的一行 —— 少了它,两个有限的数字摆在一起会读成
16467
- * 「`~/.epoch/` 最多 712MB」,而那是假的。
17923
+ * `rest` 那一格是这张表里最要紧的一行 —— 少了它,三个有限的数字摆在一起会读成
17924
+ * 「`~/.epoch/` 最多 1224MB」,而那是假的。
16468
17925
  */
16469
17926
  declare const EPOCH_DISK_LEDGER: {
16470
17927
  /** `~/.epoch/checkpoints/` */
16471
17928
  readonly checkpoints: number;
16472
17929
  /** `~/.epoch/artifacts/` */
16473
17930
  readonly artifacts: number;
17931
+ /** `~/.epoch/attachments/` */
17932
+ readonly attachments: number;
16474
17933
  /** `sessions.db` + `telemetry.jsonl` + `workspaces.json` + 日志:**没有上限** */
16475
17934
  readonly rest: null;
16476
17935
  };
16477
17936
  /**
16478
- * 两条受管上限之和 —— **名义**天花板,不是实际上限。
17937
+ * 三条受管上限之和 —— **名义**天花板,不是实际上限。
16479
17938
  *
16480
17939
  * 拿它去跟 `du -sh ~/.epoch` 比会对不上,那不是 bug:差出来的部分就是
16481
17940
  * `EPOCH_DISK_LEDGER.rest` 那一格。
17941
+ *
17942
+ * ⚠️ 从 2026-09-11 起它**更不像**一个天花板了:attachment 那一格的单件不设上限,
17943
+ * 而当前会话目录不参与驱逐(判据在 {@link ATTACHMENT_MAX_TOTAL_BYTES} 上)。
17944
+ * 也就是说这个和现在同时被两件事架空 —— `rest` 那一格,和「正在跑的那一段会话」。
16482
17945
  */
16483
17946
  declare const NOMINAL_TOTAL_BYTES: number;
16484
17947
 
@@ -16911,8 +18374,33 @@ declare class TrustManager implements TrustManager$1 {
16911
18374
  */
16912
18375
  readonly loadErrors: string[];
16913
18376
  constructor(filePath?: string);
18377
+ /**
18378
+ * 管到这个目录的那一条记录(最长匹配),没有就 undefined。
18379
+ *
18380
+ * ⚠️ **`check()` 和 `checkExec()` 必须共用它,不许各走一遍循环。** 两处各写一份
18381
+ * 的话,「哪条记录说了算」就有了两个答案,而它们分叉的表现是:某个目录判成
18382
+ * 「已信任」却拿着**另一条**记录的 `exec` —— 一次没人授过的执行类放行。
18383
+ */
18384
+ private bestRecord;
16914
18385
  check(dir: string): TrustLevel;
16915
- record(dir: string, level: DecidedTrustLevel, scope: TrustScope): void;
18386
+ /**
18387
+ * 执行类授信。**`=== true` 而不是真值判断**:存量记录压根没有这一格
18388
+ * (`undefined` → false,那就是迁移本身),而一条手改坏的 `exec: "no"`
18389
+ * 在 JS 里是真值 —— 那会让一个谁也没授过的目录拿到执行权。
18390
+ *
18391
+ * 只回记录里写的,**不做新鲜度校验**(那要读仓库里的文件,不归这张表管)。
18392
+ */
18393
+ checkExec(dir: string): TrustExecGrant;
18394
+ /**
18395
+ * 记一次决策。
18396
+ *
18397
+ * ⚠️ **授 `exec` 时指纹是这里自己算的**(`grant.execHash` 只是给用例的覆盖口)。
18398
+ * 让调用方去算的话,三个写入口(CLI 的 `trust add --exec`、交互式询问、宿主的
18399
+ * `runtime.trust.record()`)里**任何一个漏传**,那条记录就永远校验不了 ——
18400
+ * 而漏传的表现是「一切正常」:授信成功、hook 生效,只是「变了就重新问」
18401
+ * 这件事从此对这个目录不成立。谁也不会发现。
18402
+ */
18403
+ record(dir: string, level: DecidedTrustLevel, scope: TrustScope, grant?: TrustGrant): void;
16916
18404
  list(): readonly TrustRecord[];
16917
18405
  revoke(dir: string): void;
16918
18406
  private load;
@@ -16928,6 +18416,166 @@ declare class TrustManager implements TrustManager$1 {
16928
18416
  private save;
16929
18417
  }
16930
18418
 
18419
+ /**
18420
+ * 执行类输入的内容指纹 —— 「授信之后这些文件被改过吗」(2026-09-11)。
18421
+ *
18422
+ * 授 `exec` 那一刻把这个值记进 `TrustRecord.execHash`;之后每次判定重算一遍,
18423
+ * 对不上就把 `exec` 降回未授、重新问一次。
18424
+ *
18425
+ * ## 它挡的是哪一种事
18426
+ *
18427
+ * 信任按路径存、而且是**一次性**的(`manager.ts` 文件头「两条明确不做」第 2 条)。
18428
+ * 于是 `git pull` 之后,一个当初被授了执行类的仓库可以凭空多出一条
18429
+ * `command: curl … | sh`,而没有任何地方会再问一次。hermes 为这件事在项目技能上
18430
+ * 挂了一整个扫描器(`agent/skill_utils.py` 的 `_PROJECT_QUARANTINE_CACHE`,
18431
+ * 注释里写着「trust 是一次性的 repo 级决定,而 repo 的内容每次 pull 都在变」);
18432
+ * codex 的做法便宜得多也窄得多 —— 只给 hook 记一个 `trusted_hash`
18433
+ * (`codex-rs/config/src/hook_config.rs`)。这一份跟 codex 走。
18434
+ *
18435
+ * ## ⚠️ 为什么这不是把「两条明确不做」整条作废
18436
+ *
18437
+ * 那一条否掉的是**给指令文件记 hash**,理由是「改一行 AGENTS.md 就被打断一次」。
18438
+ * 那个结论在读类上照旧成立,这一份一个字都没碰它 —— {@link EXEC_INPUTS} 里
18439
+ * 没有任何一个指令文件,也不该有。完整的两侧对照在 protocol 的
18440
+ * `TrustRecord.execHash` 上。
18441
+ *
18442
+ * ## 三条实现上的规矩
18443
+ *
18444
+ * 1. **「不存在」和「空文件」必须是两个指纹。** 前者不进摘要、后者进,
18445
+ * 所以删掉 `hooks.json` 和把它清空会得到不同的值 —— 两件事本来就不同;
18446
+ * 2. **读不出来的文件按「有内容但读不到」算,不按不存在算**(见 {@link digestOf})。
18447
+ * fail closed:一个读不出来的 `hooks.json` 不该和没有 `hooks.json` 长得一样;
18448
+ * 3. **相对路径进摘要,绝对路径不进。** 否则把仓库换个目录放就换了指纹,
18449
+ * 而那时文件一个字节都没变。
18450
+ */
18451
+ /**
18452
+ * 算一个项目根的执行类指纹。**纯函数式的读,不写任何东西。**
18453
+ *
18454
+ * ⚠️ 调用方要先短路:只有在「这条记录真的授过 exec 且真的记了指纹」时才值得算。
18455
+ * 无条件调用的代价是每次判定都去 stat 六个路径 + 列一个目录,而
18456
+ * `TrustGate.decide()` 被安全中心那一屏按行调用(`server/src/security.ts`
18457
+ * 的 `toWireTrustList` 逐行问一次,判据写在那儿)。
18458
+ */
18459
+ declare function computeExecHash(root: string): string;
18460
+
18461
+ /**
18462
+ * 未信任工作区里**有什么被挡着** —— 数得出来的那一份清单(2026-09-11)。
18463
+ *
18464
+ * ## 它推翻了一句写在 wire 上的话,推翻得有条件
18465
+ *
18466
+ * `protocol/src/wire-capability.ts` 的 `WireCapabilityProject.trusted` 上写着:
18467
+ *
18468
+ * > 界面上那句话必须照实说,不能写成「已屏蔽 N 个」—— 那个 N 根本不存在,
18469
+ * > 谁也没数过。
18470
+ *
18471
+ * 那句话在它写下的那天是对的:闸门那一侧**连 `readdir` 都不做**,所以 N 确实
18472
+ * 不存在。这一份把闸门的下限从「不 readdir」放宽到「readdir 但不读任何文件
18473
+ * 内容」,于是 N 真的被数出来了 —— 而上面那句话的约束(不许编一个没人数过的数)
18474
+ * 一个字都没松。
18475
+ *
18476
+ * 放宽的理由是宿主那条路:用户看到的是一句「这个工作区还没被信任,它的技能一个
18477
+ * 都没读」加一条他跑不了的命令。而「这个仓库里有 7 个技能、1 个 `hooks.json`」
18478
+ * 是把「要不要信任」从一道信仰题变成一道判断题的**唯一**材料。
18479
+ * hermes 的启动横幅是同一个形态(`◆ N 个项目技能…但没加载`,
18480
+ * `hermes_cli/cli_info_mixin.py`)——它同样是「不信任也数得出来」。
18481
+ *
18482
+ * ## ⚠️ 唯一的硬约束:**这个文件不读任何文件内容**
18483
+ *
18484
+ * 全文只用 `existsSync` / `readdirSync` / `statSync`,**一次 `readFileSync` 都没有**,
18485
+ * 而这不是「暂时没用上」。两条理由,第二条才是硬的:
18486
+ *
18487
+ * 1. 读了内容就等于读了 `SKILL.md` 的 frontmatter,那和「连读都不读」的承诺
18488
+ * 直接冲突 —— 闸门那一侧的措辞会当场变成假话;
18489
+ * 2. **这份清单会被画进一个用户正要点「确认」的对话框里。** frontmatter 的
18490
+ * `description`、hook 的 `command` 全是仓库作者写的散文,把它们渲染在授权按钮
18491
+ * 旁边,等于给人看一段攻击者可控的劝说词,而且是在最坏的时机。
18492
+ * **路径和个数是结构,散文不是** —— 只发结构。
18493
+ *
18494
+ * 所以这里数不出「有几条 hook」(那要解析 JSON)。`hooks.json` 存不存在就是
18495
+ * 这一格能给的全部,而它已经足够把两种仓库分开了。
18496
+ */
18497
+ /**
18498
+ * 一个项目根里、被两道闸门各自挡着的东西。**全是个数和路径,没有一句散文。**
18499
+ *
18500
+ * 路径一律是**相对项目根**的:这份清单会原样上网线进浏览器,而绝对路径在那儿
18501
+ * 只会让一行装不下,也没给用户多一个字的信息(他知道自己在哪个仓库)。
18502
+ */
18503
+ interface ProjectExtensionInventory {
18504
+ /** 读类:项目指令文件(`EPOCH.md` / `AGENTS.md` / `CLAUDE.md`),相对路径 */
18505
+ instructionFiles: string[];
18506
+ /** 读类:`.epoch/skills/<分类>/<名字>/SKILL.md` 的个数 */
18507
+ skills: number;
18508
+ /** 读类:`.epoch/agents/*.md` 的个数 */
18509
+ roles: number;
18510
+ /** 读类:`.epoch/commands/**\/*.md` 的个数 */
18511
+ commands: number;
18512
+ /** 执行类:存在的那几份 hook 配置,相对路径 */
18513
+ hookFiles: string[];
18514
+ /** 执行类:`.epoch/policies/*.toml` 的个数 */
18515
+ policies: number;
18516
+ /** 执行类:存在的那几份项目设置,相对路径 */
18517
+ settingsFiles: string[];
18518
+ }
18519
+ /**
18520
+ * 数一遍这个项目根里有什么。**不受信任的目录也能调**,那正是它存在的理由。
18521
+ *
18522
+ * 目录不存在 / 读不动 / `root` 是空串,一律回全零的清单而不是抛 ——
18523
+ * 这个函数的每一个调用点都在「顺便告诉用户一句」的路径上,
18524
+ * 为它中断一次装配或者让一屏画不出来,代价和收益完全不成比例。
18525
+ */
18526
+ declare function inventoryProjectExtensions(root: string | null | undefined): ProjectExtensionInventory;
18527
+ /** 读类那一档有东西被挡着吗 —— 「授了读类会多出什么」 */
18528
+ declare function hasReadTierContent(inv: ProjectExtensionInventory): boolean;
18529
+ /**
18530
+ * 授了执行类**会多出什么** —— 给对话框和安全中心那份清单用。
18531
+ *
18532
+ * ⚠️ **别拿它当「要不要报警」的判据**,那是下面 {@link hasDedicatedExecContent}
18533
+ * 的活。两者答的是两个问题,而混用的代价当场就付了,见那一个的文件注释。
18534
+ */
18535
+ declare function hasExecTierContent(inv: ProjectExtensionInventory): boolean;
18536
+ /**
18537
+ * 这个仓库里有**存在的唯一目的就是执行类**的文件吗 —— 「该不该主动说一句」。
18538
+ *
18539
+ * ## ⚠️ 这个函数是 2026-09-11 当天补的,因为上面那个用在这儿是错的
18540
+ *
18541
+ * 一开始「要不要报警」读的是 {@link hasExecTierContent},而它判的是**文件在不在**。
18542
+ * 现场(epoch-agent 自己这个仓库)当场打出一条假警告:
18543
+ *
18544
+ * > Trust: … is trusted (read tier only): … but its hooks and policies do not take effect
18545
+ *
18546
+ * 而那个仓库**一个 hook 都没有**。触发它的是 `.claude/settings.json` ——
18547
+ * 那份文件里只有一张 `permissions.deny` 表,而**那张表 epoch 压根不读**
18548
+ * (这个路径在 core 里只有一个消费方:hook 加载器的 `parseClaudeSettingsHooks`,
18549
+ * 它只从里面取 hooks)。也就是说那句话字面上是假的:什么都没少。
18550
+ *
18551
+ * 而 [hook/sources.ts](../hook/sources.ts) 的文件头自己就写着「仓库里带
18552
+ * `.claude/settings.json` 是今天的常态」—— 也就是说那条假警告会打在**几乎每一个**
18553
+ * 仓库上。它正是 `hasExecTierContent` 原来那段注释要防的「别无谓地吓人」,
18554
+ * 只是判据放错了一层。
18555
+ *
18556
+ * ## 判据:这份文件存在,本身就说明有东西被挡住了吗
18557
+ *
18558
+ * | 文件 | 算不算 | 为什么 |
18559
+ * | --------------------------- | ------- | ---------------------------------------------------------- |
18560
+ * | `.epoch/hooks.json` | **算** | 它存在的唯一目的就是装 hook |
18561
+ * | `.epoch/policies/*.toml` | **算** | 同上,一个 `.toml` 放在那儿就是为了带规则 |
18562
+ * | `.claude/settings*.json` | 不算 | 别的工具的文件,常见内容是 epoch 根本不读的 permissions |
18563
+ * | `.epoch/settings*.json` | 不算 | 它的 deny 那一半**未信任时照旧生效**,被挡的只有 allow 和标量 |
18564
+ *
18565
+ * ## 代价,如实写着
18566
+ *
18567
+ * 只把 hook 写在 `.claude/settings.json` 里、而且之前授过信任的仓库,
18568
+ * 启动时**不会**收到这句提醒(hook 照旧不跑 —— 闸门一个字没松,只是没人主动说)。
18569
+ * 那一档不是看不见:安全中心那一屏照旧把它列出来(那儿用的是
18570
+ * {@link hasExecTierContent},列全)。**这是「不主动打扰」和「不漏说」之间的取舍**,
18571
+ * 而站在假警告会打给所有人这一边,取舍是清楚的。
18572
+ *
18573
+ * ⚠️ 想把 `.claude/settings.json` 也变成「算」的人:正确的做法不是把它加回这张表,
18574
+ * 是让 hook 加载器回答「这份被挡下的文件里**真的有** hook 吗」—— 它手里就有那个
18575
+ * 解析器。那要动「不受信任时连读都不读」那条纪律的措辞,是一次单独的改动。
18576
+ */
18577
+ declare function hasDedicatedExecContent(inv: ProjectExtensionInventory): boolean;
18578
+
16931
18579
  /**
16932
18580
  * `code` 档下**哪些工具从模型的工具表里切走**(方案 51 PR-2)。
16933
18581
  *
@@ -17219,4 +18867,4 @@ declare function resolveCodeModeWorker(baseDir: string, exists?: typeof existsSy
17219
18867
  */
17220
18868
  declare function codeModeWorkerPath(): string | undefined;
17221
18869
 
17222
- export { AGENT_ROLE_DIAG_MODULE, ARTIFACT_MAX_TOTAL_BYTES, ARTIFACT_RETENTION_DEFAULTS, type AcquireResult, type AgentConfig, AgentLoop, type AgentProvider, type AgentSite, type AgentSiteRef, type AppendMessageInput, ApprovalCache, type ApprovalDecision, type ApprovalScope, type ArtifactOpenRefusal, type ArtifactRetention, type AskQuestionDeps, BLOCKED_TOOLS, type BackendOptions, type BackendOutcome, type BackgroundTaskSource, type BaseOrigin, type BaseSlot, type BeginTurnInput, type BreakdownInput, BudgetGuard, type BudgetGuardOptions, type BudgetInfo, type BudgetLevel, type BudgetOptions, type BudgetStatus, BudgetStore, type BudgetStoreOptions, type BuildFireArgvInput, type BuildIndexOptions, CHECKPOINT_MAX_TOTAL_BYTES, CLAUDE_EVENT_NAMES, CLAUDE_TOOL_NAMES, CODE_MODE_LIMITS, CODE_MODE_SDK_TAG, CONFIG_KEYS, CURRENT_CONFIG_VERSION, type CachedApproval, type CachedDecision, type CheckpointCapture, type CheckpointFile, CheckpointManager, type CheckpointManagerOptions, type CheckpointManifest, CheckpointStore, type CheckpointStoreOptions, type CheckpointSummary, type ClaudeHooksParseOptions, type CodeModeLimits, type CodeRequest, type CodeResult, CodeSandbox, type CommandHookConfig, type CommandHookResult, type CommandLoadOutcome, type CommandLoadSummary, type CommandResult, type CommandRunner, type CompactionResult, type CompiledRules, type ComplianceDirSpec, type ComplianceDirsSummary, ComplianceEngine, type ComplianceEngineInput, type ComplianceGate, type ComplianceHit, type ComplianceRule, type ComplianceRuleSource, type ComplianceSettings, type ComplianceVerdict, type CompressConfig, type CompressResult, type ConfigMigration, type ConfigOrigins, ContextCompressor, type ContextEngine, type CreateScheduleInput, type CreateSessionInput, DEFAULT_COMPLIANCE_ACTIONS, DEFAULT_COMPLIANCE_SETTINGS, DEFAULT_GOAL_MAX_ROUNDS, DEFAULT_MAX_HOLD_CHARS, type DelegateArgs, type DelegateConfig, DelegateManager, type DelegateTaskResult, type DiagnoseOptions, type DiscoverOutcome, type DiscoveredFile, type DoctorReport, type DraftParse, EMPTY_RULES, EPOCH_DISK_LEDGER, EpochConfigSchema, type ExpandOptions, type ExtractOutcome, FTS_TABLES, FTS_TRIGGERS, type FallbackEvent, type FileSlots, type FileSourceKind, type ForkInput, type FrontmatterParse, type FrontmatterSplit, type FtsHealth, type FtsReclaim, type FtsSpace, type FtsTableHealth, GOAL_BLOCK_CODES, type GateAxis, type GateVerdict, type GenerateInput, type GenerateOutput, type Goal, type GoalBlockReason, type GoalClear, type GoalPhase, type GoalPromptView, type GoalRefusal, type GoalRoundResult, GoalService, GoalStore, type GoalToolDeps, type GoalWrite, HEADLESS_AUDIT_CODES, type HeadlessPolicy, type HookConfigEntry, HookManager, type HookManagerOptions, type HookMatcher, type HookSourceKind, type HookSourceStatus, type HookSourcesOptions, type HookSourcesSummary, type HooksParseOutcome, type MemoryManager$1 as IMemoryManager, INSTRUCTION_BUDGET_CHARS, INSTRUCTION_FILES, type SessionManager$1 as ISessionManager, type SkillSystem$1 as ISkillSystem, ImageLoadError, type ImageSize, type ImportDecision, type ImportGate, type ImportNode, type ImportSkipReason, ImportTrustStore, type InstallOptions, type InstallOutcome, type InstallPreview, type InstructionScan, type InstructionSource, type IsolationReason, type IsolationReport, type JsonSchemaDocument, LAUNCHD_LABEL_PREFIX, LOCK_HEARTBEAT_MS, LOCK_STALE_MS, type LayerOutcome, type LayerScalars, type LayerStatus, type LearningContext, type LearningResult, type LearningSkipReason, type LexiconLoadOutcome, type LifecycleOutcome, type ListCandidatesInput, type ListSessionsInput, type ListSessionsOutput, type LoadCommandsOptions, type LoadConfigOptions, type LoadKeybindingsOptions, type LoadKeybindingsResult, type LoadRolesOptions, type LoadedPlugin, type LockHolder, MARKETPLACE_FILE, MAX_AUDIT_ENTRIES, MAX_COMMAND_DEPTH, MAX_GOAL_MAX_ROUNDS, MAX_IMPORT_DEPTH, MAX_OBJECTIVE_CHARS, MAX_REASON_CHARS, MAX_RECENT_WORKSPACES, MODEL_GIVE_UP_THRESHOLD, MULTIMODAL_DEFAULTS, type ManagedPolicy, ManagedPolicyError, type ManagedSettings, ManagedSettingsSchema, type MarketplaceCatalog, type MarketplaceEntry, type MarketplaceOutcome, type MarketplaceRecord, type MemoryConfig, type MemoryEntry, MemoryManager, MemoryReviewer, type MemorySearchResult, type MemoryTarget, type MemoryWriteResult, type MentionPart, type MentionResolution, type Message, type MessageHit, type MessageRecord, type MessageSurface, type ModelCapability, ModelFailureTracker, type ModelMetaEntry, type ModelPricing, type MultimodalPolicy, NOMINAL_TOTAL_BYTES, NO_REVEALED_TOOLS, type NonInteractiveProbe, type OsTaskSnapshot, type OutputGuard, PACK_EXCLUDES, PACK_MAX_BYTES, PACK_MAX_FILES, PERMISSION_AUDIT_CODES, PLUGIN_LAYOUT, PLUGIN_MANIFEST_FILE, PLUGIN_NAME_PATTERN, PLUGIN_PACK_TOOL, PLUGIN_ROOT_ENV, PLUGIN_ROOT_TOKEN, PLUGIN_VALIDATE_TOOL, POLICY_DECISIONS, PROJECT_AGENTS_SUBDIR, PROVIDER_NO_CREDENTIAL, type PackFile, type PackLimits, type PackOutcome, type PackPlan, type PackPlanOutcome, type PermissionAuditCode, type PermissionAuditEntry, type PermissionAuditOutcome, type PermissionAuditScope, type PermissionAuditSnapshot, PermissionManager, type PersistedBudget, type PersistedPart, type PlanApprovalFn, type PlanEnterResult, type PlanModeDeps, PlanModeState, type PlanSettlement, type PluginArtifacts, type PluginAuthor, PluginAuthorSchema, type PluginCommandDir, type PluginCounts, type PluginHookFile, type PluginInventory, type PluginLoadOptions, type PluginManifest, type PluginPathRef, type PluginRecord, type PluginSettingsFile, type PluginSourceType, type PluginStateReadOutcome, PluginStore, type PolicyDirSpec, type PolicyDirsSummary, type PolicyLoadOutcome, type PolicyRule, PolicyRuleSchema, type ProjectContext, ProviderError, ProviderRouter, type ProviderStreamChunk, type ProviderToolCall$1 as ProviderToolCall, type ProviderWarning, type PruneResult, type QuestionAskFn, RECORDING_KEEP_RUNS, RECORDING_MAX_BYTES, ROLE_FRONTMATTER_SCHEMA, RUN_CODE_TOOL, type ReadMessagesInput, type ReadMessagesOutput, type RecentWorkspace, RecentWorkspaces, type RecordRunInput, type RecorderOptions, type RecoveryAction, type RegisterInput, type RegisterOutcome, type RegistrarOptions, type RegistrationPatch, type RepairOutcome, type ResolveMentionsOptions, type ResolveReferenceInput, type ResolveSettingsOptions, type ResolvedCompression, type ResolvedReference, type ResolvedSource, type ResponseFormat, type RestorePartsOptions, type ReviewIssue, type RewindAction, type RewindDrift, type RewindFilePlan, type RewindOptions, type RewindOutcome, type RewindPreview, type RoleCreateFailure, type RoleCreateInput, type RoleCreateOptions, type RoleCreateOutcome, type RoleLoadOutcome, type RoleLoadSummary, type RoleMergeNotice, type RoleMergeResult, type RoleScope, type RuleMatch, type RuleSuggestion, type RuleVerdict, type RunCodeFailure, type RunCodeInput, type RunCodeOutcome, type RunCodeToolDeps, type RunEstimateInput, type RunInput, type RunResult, SANDBOX_COVERS, SANDBOX_EXCLUDES, SCALAR_KEYS, SCHEDULE_DEFAULTS, SCHEDULE_DEFAULT_ALLOWLIST, SCHEDULE_ISSUE_CODES, SCHTASKS_FOLDER, SESSION_MESSAGES_READ_TOOL, SESSION_MESSAGES_SEARCH_TOOL, SESSION_QUERY_TOOLS, SESSION_SEARCH_TOOL, SESSION_TRACE_TOOL, SETTINGS_LAYER_ORDER, SETTING_WRITE_APPLY, SETTING_WRITE_KEYS, SETTING_WRITE_LAYERS, SKILL_CREATE_TOOL, SKILL_EDIT_TOOL, SKILL_PATCH_TOOL, SKILL_VIEW_TOOL, SKILL_WRITE_FILE_TOOL, SLASH_COMMANDS_DIAG_MODULE, type ScalarKey, type ScanInstructionsOptions, type ScheduleArgvRefusal, type ScheduleArgvResult, type ScheduleArgvWarning, type ScheduleBackend, type ScheduleCliEntry, type ScheduleDraftDeps, type ScheduleDrift, type ScheduleDriftKind, type ScheduleInstallForm, type ScheduleIssue, type ScheduleIssueCode, type ScheduleLock, ScheduleRecorder, ScheduleRegistrar, ScheduleStore, type ScheduleValidation, type SchemaExport, type SearchHit, type SearchInput, type SearchMessagesInput, type SearchMessagesOutput, type SearchOutput, type SearchSessionsInput, type SelectionDeps, type Session, type SessionCandidate, type SessionHit, SessionManager, type SessionMentionDeps, type SessionMeta, SessionQueries, SessionReferences, type SessionSearchDeps, type SessionStats, type SessionSurface, type SessionTrace, type SessionVisibility, type SessionVisibilityInput, type SettingApply, type SettingChain, type SettingFutile, type SettingLayer, type SettingStep, type SettingValue, type SettingValueKind, type SettingWriteFailure, type SettingWriteKey, type SettingWriteLayer, type SettingWriteOutcome, type SettingWriteRequest, type SettingWriteTarget, type SettingWriteTargetsOptions, type SettingsFile, SettingsFileError, SettingsFileSchema, type SettingsLayer, type SettingsResolution, type ShadowFinding, type ShadowKind, type Skill, type SkillDirSource, type SkillDirSpec, type SkillDirsSummary, type SkillImportConflict, type SkillImportDone, type SkillImportEntry, type SkillImportFailure, type SkillImportForm, type SkillImportOptions, type SkillImportPreview, type SkillImportResult, type SkillIndexEntry, type SkillIndexOptions, type SkillIndexResidency, type SkillIndexStats, SkillLearner, type SkillLearningProvider, type SkillMeta, type SkillStats, SkillSystem, type SkillSystemOptions, type SkillViewDeps, type SkillWriteDeps, type SubAgentRunner, type SubTask, type SubToolCaller, type SummarizeOutput, type SurfaceMessage, type SystemPromptParams, type SystemPromptSegments, TOOL_SEARCH_TOOL, TaskNoticeTracker, type TodoItem, type ToolCall, type ToolConflict, type ToolGate, type ToolNameFidelity, type ToolNameLookup, ToolRegistry, type ToolRevealState, type ToolScope, type ToolSearchDeps, type ToolTableCost, type TraceInput, type TraceNode, TrackerDB, type TrustDecision, type TrustGate, type TrustGateOptions, TrustManager, type TurnRecord, type TurnTiming, type UpdateScheduleInput, type UtilityModelInfo, type ValidateScheduleInput, type VersionCheck, Workspace, type WorkspaceFiles, type WorkspaceIssue, type WorkspaceIssueKind, acquireScheduleLock, addMarketplace, addUsage, admitArtifact, admitArtifacts, affectedPaths, artifactLaunchArgv, artifactOpenRefusal, assertLevelAllowed, blockedRoleTools, buildContextBreakdown, buildFireArgv, buildFtsMatchQuery, buildSchemaExports, buildSystemPrompt, buildTrigramMatchQuery, builtinRoles, checkEpochVersion, checkFtsHealth, checkShadowing, classifyProviderError, clearModelCache, coalesceRecording, codeModeWorkerPath, commandNameFromRelPath, compileRuleLists, compressionOptions, computeCost, computeCostMicros, countRules, createAllowAllGate, createAllowAllImportGate, createAskQuestionTool, createCodeExecTool, createDelegateTool, createDenyAllGate, createDenyAllImportGate, createGoalTool, createImportGate, createMemoryTool, createPlanModeTools, createPluginAuthorTools, createRoleFile, createRoleScope, createRunCodeTool, createScheduleDraftTool, createSessionMessagesReadTool, createSessionMessagesSearchTool, createSessionSearchTool, createSessionSearchTools, createSessionTraceTool, createSkillViewTool, createSkillWriteTools, createTodoTool, createToolRevealState, createToolScope, createToolSearchTool, createTrustGate, currentEpochVersion, decideImport, decideTrust, defaultConfig, defaultModelOf, describeArtifactRefusal, describeBackgroundTask, describeHeadlessPolicy, describeIsolation, describeJsonError, diagnoseSchedules, discoverMarkdown, discoverModels, discoverProject, draftCommandLine, draftWidening, escapeRuleContent, estimateImageTokens, estimateRunTokens, estimateTokens, evaluateSelection, expandCommand, expandImports, explainSetting, extractArchive, findInstructionFiles, findProjectInstructions, findRole, formatBytes, formatSkillIndex, formatUsd, gateForTool, getCapability, getConfigIssues, getConfigOrigins, getImportGate, getTrustGate, hasPromptCaching, hasProviderCredentials, imageFromBase64, imageFromPath, imagePartTokens, importSkills, installPlugin, instructionDirs, instructionNotices, intervalSlots, inventoryTotal, isBlockCode, isCodeModeDeferred, isEmptyRules, isGoalPhase, isNonInteractive, isReadOnlySubCall, isSettingWriteLayer, isValidPluginName, isWithinWindow, isWritableSettingKey, labelSlug, layerRank, listSessionCandidates, listWorkspaceFiles, loadCommandDefinitions, loadCommandFile, loadCommands, loadConfig, loadHookSources, loadKeybindings, loadLexiconDir, loadLexiconFiles, loadPlugins, loadPolicyFiles, loadPolicyRules, loadRoleDefinitions, loadRoleDir, loadedImports, localDateString, managedSwitchNotes, mapClaudeToolName, mapEpochToolName, mapOperationType, matchPreauthorization, matchRuleLists, matchesHook, mentionsPluginRoot, mergeHeadlessPolicy, mergeRoles, mergeSchedulePatch, microsToUsd, modelListCandidates, needsAllowlist, nextFallbackModel, nextRunAt, noConfigOrigins, noManagedPolicy, noPlugins, openArtifact, originOf, packFileName, packPlugin, parseClaudeSettingsHooks, parseClock, parseDate, parseFrontmatter, parseHooksConfig, parseHooksConfigVerbose, parseImportLine, parseModelList, parseRoleDefinition, parseScheduleDraft, parseSkillFrontmatter, parseTrigger, pendingImageTokens, persistParts, persistToolResults, persistedUsage, planPack, previewSkillImport, pruneArtifacts, pruneRecordings, pruneSchedules, readImageSize, readMarketplaces, readPluginManifest, readPluginRecords, readProviderKeyEnv, readRecording, readSessionSurface, reclaimFtsSpace, redactLine, refreshOpenRouterMetadata, removeMarketplace, removeSessionArtifacts, renderImportTree, renderProjectContext, renderSdkDeclaration, repairSchedules, resolveArtifactRetention, resolveCodeModeWorker, resolveComplianceDirs, resolveEntrySource, resolveInWorkspace, resolveMarketplaceRef, resolveMentions, resolveMultimodalPolicy, resolvePolicyDirs, resolveProjectRoot, resolveScheduleBackend, resolveSessionReference, resolveSessionVisibility, resolveSettings, resolveSkillDirs, resolveSource, resolveTar, resolveWorkspace, restoreParts, revealScopedProvider, roleScopedProvider, roleSourceLabel, ruleCovers, ruleFromString, ruleToString, runCode, runCommand, sanitizeArgNames, sanitizeCommand, sanitizeFtsQuery, sanitizePath, sanitizeToolOutput, scanInstructions, scanPluginDir, scanSkillDirs, searchMarketplaces, setImportGate, setPluginEnabled, setTrustGate, settingValueChoices, settingValueKind, settingWriteLayers, settingWriteTargets, skillIndexDescription, skillIndexResidency, skillIndexStats, sniffMediaType, splitFrontmatter, staticToolProvider, stripArtifactData, substitutePluginRoot, suggestRuleFromApproval, systemOpenArgv, systemPromptSegments, toPosix, toolDefinitionTokens, toolMatches, toolTableTokens, totalTokens, traceSession, triggerPeriodMs, unescapeRuleContent, uninstallPlugin, unknownRoleSkills, unknownRoleTools, unloadedInstructionFiles, updateMarketplace, updatePlugin, usdToMicros, validateSchedule, validateTrigger, validateWindow, withGoalCompression, withGoalPrompt, workspaceDisplayPath, writeSettingValue };
18870
+ export { AGENT_ROLE_DIAG_MODULE, ARTIFACT_MAX_TOTAL_BYTES, ARTIFACT_RETENTION_DEFAULTS, ATTACHMENT_MAX_TOTAL_BYTES, ATTACHMENT_RETENTION_DEFAULTS, type AcquireResult, type AgentConfig, AgentLoop, type AgentProvider, type AgentSite, type AgentSiteRef, type AppendMessageInput, ApprovalCache, type ApprovalDecision, type ApprovalScope, type ArtifactOpenRefusal, type ArtifactRetention, type AskQuestionDeps, type AttachmentRetention, BLOCKED_TOOLS, type BackendOptions, type BackendOutcome, type BackgroundTaskSource, type BaseOrigin, type BaseSlot, type BeginTurnInput, type BreakdownInput, BudgetGuard, type BudgetGuardOptions, type BudgetInfo, type BudgetLevel, type BudgetOptions, type BudgetStatus, BudgetStore, type BudgetStoreOptions, type BuildFireArgvInput, type BuildIndexOptions, CHECKPOINT_MAX_TOTAL_BYTES, CLAUDE_EVENT_NAMES, CLAUDE_TOOL_NAMES, CODE_MODE_LIMITS, CODE_MODE_SDK_TAG, CONFIG_KEYS, CURRENT_CONFIG_VERSION, type CachedApproval, type CachedDecision, type CheckpointCapture, type CheckpointFile, CheckpointManager, type CheckpointManagerOptions, type CheckpointManifest, CheckpointStore, type CheckpointStoreOptions, type CheckpointSummary, type ClaudeHooksParseOptions, type CodeModeLimits, type CodeRequest, type CodeResult, CodeSandbox, type CommandHookConfig, type CommandHookResult, type CommandLoadOutcome, type CommandLoadSummary, type CommandResult, type CommandRunner, type CompactionResult, type CompiledRules, type ComplianceDirSpec, type ComplianceDirsSummary, ComplianceEngine, type ComplianceEngineInput, type ComplianceGate, type ComplianceHit, type ComplianceRule, type ComplianceRuleSource, type ComplianceSettings, type ComplianceVerdict, type CompressConfig, type CompressResult, type ConfigMigration, type ConfigOrigins, ContextCompressor, type ContextEngine, type CreateScheduleInput, type CreateSessionInput, DEFAULT_COMPLIANCE_ACTIONS, DEFAULT_COMPLIANCE_SETTINGS, DEFAULT_COMPRESSION_THRESHOLD, DEFAULT_GOAL_MAX_ROUNDS, DEFAULT_MAX_HOLD_CHARS, type DelegateArgs, type DelegateConfig, DelegateManager, type DelegateTaskResult, type DiagnoseOptions, type DiscoverOutcome, type DiscoveredFile, type DoctorReport, type DraftParse, EMPTY_RULES, EPOCH_DISK_LEDGER, EpochConfigSchema, type ExpandOptions, type ExtractOutcome, FTS_TABLES, FTS_TRIGGERS, type FallbackEvent, type FileSlots, type FileSourceKind, type ForkInput, type FrontmatterParse, type FrontmatterSplit, type FtsHealth, type FtsReclaim, type FtsSpace, type FtsTableHealth, GOAL_BLOCK_CODES, type GateAxis, type GateVerdict, type GenerateInput, type GenerateOutput, type Goal, type GoalBlockReason, type GoalClear, type GoalPhase, type GoalPromptView, type GoalRefusal, type GoalRoundResult, GoalService, GoalStore, type GoalToolDeps, type GoalWrite, HEADLESS_AUDIT_CODES, type HeadlessPolicy, type HookConfigEntry, HookManager, type HookManagerOptions, type HookMatcher, type HookSourceKind, type HookSourceStatus, type HookSourcesOptions, type HookSourcesSummary, type HooksParseOutcome, type MemoryManager$1 as IMemoryManager, INSTRUCTION_BUDGET_CHARS, INSTRUCTION_FILES, type SessionManager$1 as ISessionManager, type SkillSystem$1 as ISkillSystem, ImageLoadError, type ImageSize, type ImportDecision, type ImportGate, type ImportNode, type ImportSkipReason, ImportTrustStore, type InstallOptions, type InstallOutcome, type InstallPreview, type InstructionScan, type InstructionSource, type IsolationReason, type IsolationReport, type JsonSchemaDocument, LAUNCHD_LABEL_PREFIX, LOCK_HEARTBEAT_MS, LOCK_STALE_MS, type LayerOutcome, type LayerScalars, type LayerStatus, type LearningContext, type LearningResult, type LearningSkipReason, type LexiconLoadOutcome, type LifecycleOutcome, type ListCandidatesInput, type ListSessionsInput, type ListSessionsOutput, type LoadCommandsOptions, type LoadConfigOptions, type LoadKeybindingsOptions, type LoadKeybindingsResult, type LoadRolesOptions, type LoadedPlugin, type LockHolder, MARKETPLACE_DISPLAY_FIELDS, MARKETPLACE_FILE, MAX_AUDIT_ENTRIES, MAX_COMMAND_DEPTH, MAX_GOAL_MAX_ROUNDS, MAX_IMPORT_DEPTH, MAX_OBJECTIVE_CHARS, MAX_REASON_CHARS, MAX_RECENT_WORKSPACES, MODEL_GIVE_UP_THRESHOLD, MULTIMODAL_DEFAULTS, type ManagedPolicy, ManagedPolicyError, type ManagedSettings, ManagedSettingsSchema, type MarketplaceCatalog, type MarketplaceDisplay, type MarketplaceDisplayField, type MarketplaceEntry, type MarketplaceOutcome, type MarketplaceRecord, type MarketplaceStatus, type MemoryConfig, type MemoryEntry, MemoryManager, MemoryReviewer, type MemorySearchResult, type MemoryTarget, type MemoryWriteResult, type MentionPart, type MentionResolution, type Message, type MessageHit, type MessageRecord, type MessageSurface, type ModelCapability, ModelFailureTracker, type ModelMetaEntry, type ModelPricing, type MultimodalPolicy, NOMINAL_TOTAL_BYTES, NO_REVEALED_TOOLS, type NonInteractiveProbe, type OsTaskSnapshot, type OutputGuard, PACK_EXCLUDES, PACK_MAX_BYTES, PACK_MAX_FILES, PERMISSION_AUDIT_CODES, PLUGIN_LAYOUT, PLUGIN_MANIFEST_FILE, PLUGIN_NAME_PATTERN, PLUGIN_ROOT_ENV, PLUGIN_ROOT_TOKEN, POLICY_DECISIONS, PROBE_VERSION, PROJECT_AGENTS_SUBDIR, PROVIDER_NO_CREDENTIAL, type PackFile, type PackLimits, type PackOutcome, type PackPlan, type PackPlanOutcome, type PermissionAuditCode, type PermissionAuditEntry, type PermissionAuditOutcome, type PermissionAuditScope, type PermissionAuditSnapshot, PermissionManager, type PersistedBudget, type PersistedPart, type PlanApprovalFn, type PlanEnterResult, type PlanModeDeps, PlanModeState, type PlanSettlement, type PluginArtifacts, type PluginAuthor, PluginAuthorSchema, type PluginCommandDir, type PluginCounts, type PluginHookFile, type PluginInventory, type PluginLoadOptions, type PluginManifest, type PluginPathRef, type PluginRecord, type PluginSettingsFile, type PluginSourceType, type PluginStateReadOutcome, PluginStore, type PolicyDirSpec, type PolicyDirsSummary, type PolicyLoadOutcome, type PolicyRule, PolicyRuleSchema, type PricingAt, type PricingOverride, type ProjectContext, type ProjectExtensionInventory, ProviderError, ProviderRouter, type ProviderStreamChunk, type ProviderToolCall$1 as ProviderToolCall, type ProviderWarning, type PruneResult, type QuestionAskFn, RECORDING_KEEP_RUNS, RECORDING_MAX_BYTES, ROLE_FRONTMATTER_SCHEMA, RUN_CODE_TOOL, type ReadMessagesInput, type ReadMessagesOutput, type RecentWorkspace, RecentWorkspaces, type RecordRunInput, type RecorderOptions, type RecoveryAction, type RegisterInput, type RegisterOutcome, type RegistrarOptions, type RegistrationPatch, type RepairOutcome, type ResolveMentionsOptions, type ResolveReferenceInput, type ResolveSettingsOptions, type ResolvedCompression, type ResolvedReference, type ResolvedSource, type ResponseFormat, type RestorePartsOptions, type ReviewIssue, type RewindAction, type RewindDrift, type RewindFilePlan, type RewindOptions, type RewindOutcome, type RewindPreview, type RoleCreateFailure, type RoleCreateInput, type RoleCreateOptions, type RoleCreateOutcome, type RoleLoadOutcome, type RoleLoadSummary, type RoleMergeNotice, type RoleMergeResult, type RoleScope, type RuleMatch, type RuleSuggestion, type RuleVerdict, type RunCodeFailure, type RunCodeInput, type RunCodeOutcome, type RunCodeToolDeps, type RunEstimateInput, type RunInput, type RunResult, SANDBOX_COVERS, SANDBOX_EXCLUDES, SCALAR_KEYS, SCHEDULE_DEFAULTS, SCHEDULE_DEFAULT_ALLOWLIST, SCHEDULE_ISSUE_CODES, SCHTASKS_FOLDER, SESSION_MESSAGES_READ_TOOL, SESSION_MESSAGES_SEARCH_TOOL, SESSION_QUERY_TOOLS, SESSION_SEARCH_TOOL, SESSION_TRACE_TOOL, SETTINGS_LAYER_ORDER, SETTING_WRITE_KEYS, SETTING_WRITE_LAYERS, SKILL_CREATE_TOOL, SKILL_EDIT_TOOL, SKILL_PATCH_TOOL, SKILL_VIEW_TOOL, SKILL_WRITE_FILE_TOOL, SLASH_COMMANDS_DIAG_MODULE, type ScalarKey, type ScanInstructionsOptions, type ScheduleArgvRefusal, type ScheduleArgvResult, type ScheduleArgvWarning, type ScheduleBackend, type ScheduleCliEntry, type ScheduleDraftDeps, type ScheduleDrift, type ScheduleDriftKind, type ScheduleInstallForm, type ScheduleIssue, type ScheduleIssueCode, type ScheduleLock, ScheduleRecorder, ScheduleRegistrar, ScheduleStore, type ScheduleValidation, type SchemaExport, type SearchHit, type SearchInput, type SearchMessagesInput, type SearchMessagesOutput, type SearchOutput, type SearchSessionsInput, type SelectionDeps, type SelfConnector, type SelfIndexInput, type SelfSettings, type Session, type SessionCandidate, type SessionHit, SessionManager, type SessionMentionDeps, type SessionMeta, SessionQueries, SessionReferences, type SessionSearchDeps, type SessionStats, type SessionSurface, type SessionTrace, type SessionVisibility, type SessionVisibilityInput, type SettingApply, type SettingApplyFacts, type SettingChain, type SettingFutile, type SettingLayer, type SettingStep, type SettingValue, type SettingValueKind, type SettingWriteFailure, type SettingWriteKey, type SettingWriteLayer, type SettingWriteOutcome, type SettingWriteRequest, type SettingWriteTarget, type SettingWriteTargetsOptions, type SettingsFile, SettingsFileError, SettingsFileSchema, type SettingsLayer, type SettingsResolution, type ShadowFinding, type ShadowKind, type Skill, type SkillDirSource, type SkillDirSpec, type SkillDirsSummary, type SkillImportConflict, type SkillImportDone, type SkillImportEntry, type SkillImportFailure, type SkillImportForm, type SkillImportOptions, type SkillImportPreview, type SkillImportResult, type SkillIndexEntry, type SkillIndexOptions, type SkillIndexResidency, type SkillIndexStats, SkillLearner, type SkillLearningProvider, type SkillMeta, type SkillStats, SkillSystem, type SkillSystemOptions, type SkillViewDeps, type SkillWriteDeps, type SubAgentRunner, type SubTask, type SubToolCaller, type SummarizeOutput, type SurfaceMessage, type SystemPromptParams, type SystemPromptSegments, TOOL_SEARCH_TOOL, TaskNoticeTracker, type TodoItem, type ToolCall, type ToolConflict, type ToolGate, type ToolNameFidelity, type ToolNameLookup, ToolRegistry, type ToolRevealState, type ToolScope, type ToolSearchDeps, type ToolTableCost, type TraceInput, type TraceNode, TrackerDB, type TrustDecision, type TrustGate, type TrustGateOptions, TrustManager, type TurnRecord, type TurnTiming, type UpdateScheduleInput, type UtilityModelInfo, type ValidateScheduleInput, type VersionCheck, Workspace, type WorkspaceFiles, type WorkspaceIssue, type WorkspaceIssueKind, acquireScheduleLock, addMarketplace, addUsage, admitArtifact, admitArtifacts, affectedPaths, artifactLaunchArgv, artifactOpenRefusal, assertLevelAllowed, blockedRoleTools, buildContextBreakdown, buildFireArgv, buildFtsMatchQuery, buildSchemaExports, buildSystemPrompt, buildTrigramMatchQuery, builtinRoles, checkEpochVersion, checkFtsHealth, checkShadowing, classifyProviderError, clearModelCache, coalesceRecording, codeModeWorkerPath, commandNameFromRelPath, compileRuleLists, compressionOptions, computeCost, computeCostMicros, computeExecHash, countRules, createAllowAllGate, createAllowAllImportGate, createAskQuestionTool, createCodeExecTool, createDelegateTool, createDenyAllGate, createDenyAllImportGate, createGoalTool, createImportGate, createMemoryTool, createPlanModeTools, createRoleFile, createRoleScope, createRunCodeTool, createScheduleDraftTool, createSessionMessagesReadTool, createSessionMessagesSearchTool, createSessionSearchTool, createSessionSearchTools, createSessionTraceTool, createSkillViewTool, createSkillWriteTools, createTodoTool, createToolRevealState, createToolScope, createToolSearchTool, createTrustGate, currentEpochVersion, decideImport, decideTrust, defaultConfig, defaultModelOf, describeArtifactRefusal, describeBackgroundTask, describeHeadlessPolicy, describeIsolation, describeJsonError, diagnoseSchedules, discoverMarkdown, discoverModels, discoverProject, displayCurrency, draftCommandLine, draftWidening, escapeRuleContent, estimateImageTokens, estimateRunTokens, estimateTokens, evaluateSelection, expandCommand, expandImports, explainSetting, extractArchive, findInstructionFiles, findProjectInstructions, findRole, formatBytes, formatSkillIndex, formatUsd, gateForTool, getCapability, getConfigIssues, getConfigOrigins, getImportGate, getTrustGate, hasDedicatedExecContent, hasExecTierContent, hasPromptCaching, hasProviderCredentials, hasReadTierContent, imageFromBase64, imageFromPath, imagePartTokens, importSkills, installPlugin, instructionDirs, instructionNotices, intervalSlots, inventoryProjectExtensions, inventoryTotal, isBlockCode, isCodeModeDeferred, isEmptyRules, isGoalPhase, isNonInteractive, isReadOnlySubCall, isSettingWriteLayer, isValidPluginName, isWithinWindow, isWritableSettingKey, labelSlug, layerRank, listSessionCandidates, listWorkspaceFiles, loadCommandDefinitions, loadCommandFile, loadCommands, loadConfig, loadHookSources, loadKeybindings, loadLexiconDir, loadLexiconFiles, loadPlugins, loadPolicyFiles, loadPolicyRules, loadRoleDefinitions, loadRoleDir, loadedImports, localDateString, managedSwitchNotes, mapClaudeToolName, mapEpochToolName, mapOperationType, matchPreauthorization, matchRuleLists, matchesHook, mentionsPluginRoot, mergeHeadlessPolicy, mergeRoles, mergeSchedulePatch, microsToUsd, modelListCandidates, needsAllowlist, nextFallbackModel, nextRunAt, noConfigOrigins, noManagedPolicy, noPlugins, openArtifact, originOf, packFileName, packPlugin, parseClaudeSettingsHooks, parseClock, parseDate, parseFrontmatter, parseHooksConfig, parseHooksConfigVerbose, parseImportLine, parseModelList, parseRoleDefinition, parseScheduleDraft, parseSkillFrontmatter, parseTrigger, pendingImageTokens, persistParts, persistToolResults, persistedUsage, planPack, previewSkillImport, pricingFor, projectEntryForDisplay, pruneArtifacts, pruneAttachments, pruneRecordings, pruneSchedules, readImageSize, readMarketplaces, readPluginManifest, readPluginRecords, readProviderKeyEnv, readRecording, readSessionSurface, reclaimFtsSpace, redactLine, refreshOpenRouterMetadata, removeMarketplace, removeSessionArtifacts, removeSessionAttachments, renderImportTree, renderProjectContext, renderSdkDeclaration, renderSelfIndex, repairSchedules, resolveArtifactRetention, resolveCodeModeWorker, resolveComplianceDirs, resolveEntrySource, resolveInWorkspace, resolveMarketplaceRef, resolveMentions, resolveMultimodalPolicy, resolvePolicyDirs, resolvePricingAt, resolveProjectRoot, resolveScheduleBackend, resolveSessionReference, resolveSessionVisibility, resolveSettings, resolveSkillDirs, resolveSource, resolveTar, resolveWorkspace, restoreParts, revealScopedProvider, roleScopedProvider, roleSourceLabel, ruleCovers, ruleFromString, ruleToString, runCode, runCommand, sanitizeArgNames, sanitizeCommand, sanitizeFtsQuery, sanitizePath, sanitizeToolOutput, scanInstructions, scanPluginDir, scanSkillDirs, searchMarketplaces, setDisplayCurrency, setImportGate, setPluginEnabled, setTrustGate, settingValueChoices, settingValueKind, settingWriteApply, settingWriteLayers, settingWriteTargets, skillIndexDescription, skillIndexResidency, skillIndexStats, sniffMediaType, splitFrontmatter, staticToolProvider, stripArtifactData, substitutePluginRoot, suggestRuleFromApproval, systemOpenArgv, systemPromptSegments, toPosix, toolDefinitionTokens, toolMatches, toolTableTokens, totalTokens, traceSession, triggerPeriodMs, unescapeRuleContent, uninstallPlugin, unknownRoleSkills, unknownRoleTools, unloadedInstructionFiles, updateMarketplace, updatePlugin, usdToMicros, validateSchedule, validateTrigger, validateWindow, withGoalCompression, withGoalPrompt, workspaceDisplayPath, writeSettingValue };