@armadra/agent 0.7.0 → 0.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/CHANGELOG.zh-CN.md +41 -0
  3. package/dist/agent/session-cache.d.ts +4 -2
  4. package/dist/agent/session-cache.js +5 -3
  5. package/dist/agent/session-compaction.d.ts +13 -1
  6. package/dist/agent/session-compaction.js +36 -0
  7. package/dist/agent/session-core.d.ts +2 -0
  8. package/dist/agent/session-state.d.ts +9 -3
  9. package/dist/agent/session-state.js +8 -6
  10. package/dist/agent/session.js +3 -3
  11. package/dist/agent/types-w5.d.ts +3 -0
  12. package/dist/agent/types.d.ts +18 -0
  13. package/dist/agents/task-record.d.ts +4 -0
  14. package/dist/agents/task-record.js +18 -0
  15. package/dist/ai/context.d.ts +7 -2
  16. package/dist/ai/context.js +45 -7
  17. package/dist/ai/providers/catalog-data.js +1 -1
  18. package/dist/auth/oauth/token-store.d.ts +2 -0
  19. package/dist/auth/oauth/token-store.js +7 -2
  20. package/dist/bundle/ama.cjs +1684 -490
  21. package/dist/checkpoints/shadow-git.js +2 -1
  22. package/dist/compaction/breakdown.d.ts +51 -0
  23. package/dist/compaction/breakdown.js +159 -0
  24. package/dist/compaction/estimate.d.ts +6 -0
  25. package/dist/compaction/estimate.js +1 -1
  26. package/dist/compaction/prefix-estimate.d.ts +23 -0
  27. package/dist/compaction/prefix-estimate.js +34 -0
  28. package/dist/config/auth-file.js +10 -2
  29. package/dist/config/fs-retry.d.ts +18 -0
  30. package/dist/config/fs-retry.js +33 -0
  31. package/dist/config/json-schema.js +1 -0
  32. package/dist/config/key-docs.js +1 -0
  33. package/dist/config/schema.js +2 -0
  34. package/dist/config/settings-registry.js +1 -0
  35. package/dist/config/types.d.ts +2 -0
  36. package/dist/drivers/native/codex-app-server.js +6 -0
  37. package/dist/drivers/runner.js +17 -0
  38. package/dist/drivers/store.d.ts +3 -0
  39. package/dist/drivers/store.js +4 -0
  40. package/dist/i18n/catalog.d.ts +53 -0
  41. package/dist/i18n/messages/agents.d.ts +3 -0
  42. package/dist/i18n/messages/agents.js +3 -0
  43. package/dist/i18n/messages/config-keys.d.ts +2 -0
  44. package/dist/i18n/messages/config-keys.js +2 -0
  45. package/dist/i18n/messages/config.d.ts +2 -0
  46. package/dist/i18n/messages/interactive.d.ts +6 -0
  47. package/dist/i18n/messages/interactive.js +6 -0
  48. package/dist/i18n/messages/panels.d.ts +3 -0
  49. package/dist/i18n/messages/panels.js +5 -2
  50. package/dist/i18n/messages/report-context.d.ts +94 -0
  51. package/dist/i18n/messages/report-context.js +95 -0
  52. package/dist/i18n/messages/report.d.ts +96 -0
  53. package/dist/i18n/messages/report.js +13 -2
  54. package/dist/i18n/messages/settings.d.ts +2 -0
  55. package/dist/i18n/messages/settings.js +2 -0
  56. package/dist/i18n/messages/subcommands.js +2 -2
  57. package/dist/modes/acp/acp-events.d.ts +17 -4
  58. package/dist/modes/acp/acp-events.js +32 -6
  59. package/dist/modes/commands-core.js +4 -0
  60. package/dist/modes/context-report.d.ts +35 -0
  61. package/dist/modes/context-report.js +172 -0
  62. package/dist/modes/interactive/agent-bar.d.ts +2 -0
  63. package/dist/modes/interactive/agent-bar.js +6 -0
  64. package/dist/modes/interactive/commands.d.ts +2 -1
  65. package/dist/modes/interactive/commands.js +10 -1
  66. package/dist/modes/interactive/interactive-mode.d.ts +3 -0
  67. package/dist/modes/interactive/interactive-mode.js +6 -1
  68. package/dist/modes/interactive/panels.d.ts +2 -1
  69. package/dist/modes/interactive/panels.js +13 -5
  70. package/dist/modes/interactive/program-status-ui.d.ts +72 -0
  71. package/dist/modes/interactive/program-status-ui.js +229 -0
  72. package/dist/modes/interactive/session-events.d.ts +4 -0
  73. package/dist/modes/interactive/session-events.js +24 -2
  74. package/dist/modes/interactive/status-bar.d.ts +21 -3
  75. package/dist/modes/interactive/status-bar.js +64 -25
  76. package/dist/modes/interactive/status-ctx.d.ts +55 -0
  77. package/dist/modes/interactive/status-ctx.js +125 -0
  78. package/dist/modes/interactive/status-line.d.ts +2 -1
  79. package/dist/modes/interactive/status-line.js +3 -2
  80. package/dist/modes/interactive/tasks-report.js +4 -0
  81. package/dist/modes/print/print-mode.js +12 -0
  82. package/dist/modes/session-report.d.ts +10 -2
  83. package/dist/modes/session-report.js +26 -2
  84. package/dist/sdk.d.ts +1 -1
  85. package/dist/tools/types.d.ts +6 -0
  86. package/dist/tui/components/meter.d.ts +2 -0
  87. package/dist/tui/components/meter.js +1 -1
  88. package/dist/tui/program-status.d.ts +90 -0
  89. package/dist/tui/program-status.js +259 -0
  90. package/docs/acp.md +4 -4
  91. package/docs/agents.md +1 -1
  92. package/docs/en/acp.md +4 -4
  93. package/docs/en/providers.md +5 -3
  94. package/docs/en/rpc.md +3 -0
  95. package/docs/en/sessions.md +24 -1
  96. package/docs/en/tui.md +79 -22
  97. package/docs/providers.md +8 -5
  98. package/docs/rpc.md +1 -0
  99. package/docs/session-format.md +3 -3
  100. package/docs/sessions.md +23 -1
  101. package/docs/tui-design.md +2 -2
  102. package/docs/tui.md +79 -22
  103. package/package.json +1 -1
@@ -204,8 +204,9 @@ export class ShadowRepo {
204
204
  /** 新回合快照:返回提交 id 与是否该降级。 */
205
205
  snapshot(parent, message) {
206
206
  return exclusive(this.dir, async () => {
207
- const started = this.now();
208
207
  await this.init();
208
+ // 计时不含一次性的建仓与本地配置(十来个 git 子进程,Windows 上可达数秒),只量快照本身
209
+ const started = this.now();
209
210
  await this.writeExcludes();
210
211
  if (!this.counted) {
211
212
  const out = await this.run([
@@ -0,0 +1,51 @@
1
+ /**
2
+ * 上下文分类明细(`/context`):把投影消息按类别拆开估算。纯函数,只读,不改请求体。
3
+ *
4
+ * 口径与 `estimateMessagesTokens` 一致:每条消息先按 `estimateMessageTokens` 求总量,再把这个总量
5
+ * 分到消息内各部分(文本部分取 `wide + floor(narrow / 4)`,余下的 0–3 个 token 按最大余数补给
6
+ * 字符余数最大的部分;图片每张 `IMAGE_TOKENS`),所以各类别之和恰好等于整组消息的估算。
7
+ *
8
+ * 系统提示与工具声明来自活动分支上的 `system` 消息(首条全量、之后是补丁,补丁也计入——与估算口径
9
+ * 相同):按节名、按工具名汇总;工具表数组的括号与逗号计入工具声明但不归到具体工具。首次请求前
10
+ * 分支上还没有 `system` 消息,这两类为 0(`hasSystem: false`)。
11
+ *
12
+ * 只产出数字与名字(节名、工具名、custom 类型),不带任何正文。
13
+ */
14
+ import type { AgentMessage } from "../session/types.js";
15
+ export declare const BREAKDOWN_CATEGORIES: readonly ["system", "tools", "user", "assistant", "reasoning", "toolCalls", "toolResults", "images", "summaries", "custom"];
16
+ export type BreakdownCategory = (typeof BREAKDOWN_CATEGORIES)[number];
17
+ export interface BreakdownPart {
18
+ /** 节名 / 工具名 / `compaction`·`branch` / custom 类型 / 图片来源角色。 */
19
+ name: string;
20
+ tokens: number;
21
+ }
22
+ export interface BreakdownEntry {
23
+ category: BreakdownCategory;
24
+ tokens: number;
25
+ /** 计入本类的块数(消息、块、节、图片)。 */
26
+ count: number;
27
+ /** 按名字汇总,从大到小;没有名字的部分(如工具表的括号)只进 `tokens`。 */
28
+ parts: BreakdownPart[];
29
+ }
30
+ export interface ToolResultSize {
31
+ /** 在投影里第几个工具结果(从 1 开始)。 */
32
+ ordinal: number;
33
+ toolName: string;
34
+ tokens: number;
35
+ }
36
+ export interface ContextBreakdown {
37
+ /** 固定顺序(`BREAKDOWN_CATEGORIES`)。 */
38
+ entries: BreakdownEntry[];
39
+ /** = `estimateMessagesTokens(messages)`。 */
40
+ total: number;
41
+ /** 系统提示 + 工具声明。 */
42
+ prefixTokens: number;
43
+ /** 分支上已有 `system` 消息(首次请求后)。 */
44
+ hasSystem: boolean;
45
+ /** 每个工具结果的大小,按出现顺序。 */
46
+ toolResults: ToolResultSize[];
47
+ }
48
+ /** 消息 → 分类明细(各类别之和 = `estimateMessagesTokens(messages)`)。 */
49
+ export declare function breakdownMessages(messages: readonly AgentMessage[]): ContextBreakdown;
50
+ /** 最大的 n 个工具结果(大小相同按出现顺序)。 */
51
+ export declare function largestToolResults(breakdown: ContextBreakdown, n: number): ToolResultSize[];
@@ -0,0 +1,159 @@
1
+ /**
2
+ * 上下文分类明细(`/context`):把投影消息按类别拆开估算。纯函数,只读,不改请求体。
3
+ *
4
+ * 口径与 `estimateMessagesTokens` 一致:每条消息先按 `estimateMessageTokens` 求总量,再把这个总量
5
+ * 分到消息内各部分(文本部分取 `wide + floor(narrow / 4)`,余下的 0–3 个 token 按最大余数补给
6
+ * 字符余数最大的部分;图片每张 `IMAGE_TOKENS`),所以各类别之和恰好等于整组消息的估算。
7
+ *
8
+ * 系统提示与工具声明来自活动分支上的 `system` 消息(首条全量、之后是补丁,补丁也计入——与估算口径
9
+ * 相同):按节名、按工具名汇总;工具表数组的括号与逗号计入工具声明但不归到具体工具。首次请求前
10
+ * 分支上还没有 `system` 消息,这两类为 0(`hasSystem: false`)。
11
+ *
12
+ * 只产出数字与名字(节名、工具名、custom 类型),不带任何正文。
13
+ */
14
+ import { countText, estimateMessageTokens, IMAGE_TOKENS } from "./estimate.js";
15
+ /** 与 estimate.ts 的字符口径相同(非宽字符每 4 个 1 token)。 */
16
+ const CHARS_PER_TOKEN = 4;
17
+ export const BREAKDOWN_CATEGORIES = [
18
+ "system",
19
+ "tools",
20
+ "user",
21
+ "assistant",
22
+ "reasoning",
23
+ "toolCalls",
24
+ "toolResults",
25
+ "images",
26
+ "summaries",
27
+ "custom",
28
+ ];
29
+ function textPiece(category, name, text) {
30
+ const count = countText(text, { wide: 0, narrow: 0 });
31
+ return { category, name, wide: count.wide, narrow: count.narrow, fixed: 0 };
32
+ }
33
+ function imagePiece(name) {
34
+ return { category: "images", name, wide: 0, narrow: 0, fixed: IMAGE_TOKENS };
35
+ }
36
+ function contentPieces(content, category, name, imageSource) {
37
+ if (typeof content === "string")
38
+ return [textPiece(category, name, content)];
39
+ return content.map((block) => block.type === "text" ? textPiece(category, name, block.text) : imagePiece(imageSource));
40
+ }
41
+ /** 一条消息拆成的部分(字符合计与 `estimateMessageTokens` 的计数完全相同)。 */
42
+ function messagePieces(message) {
43
+ switch (message.role) {
44
+ case "system": {
45
+ const pieces = Object.entries(message.sections).map(([name, text]) => textPiece("system", name, text ?? ""));
46
+ const tools = message.toolsAdded;
47
+ if (tools !== undefined) {
48
+ for (const tool of tools)
49
+ pieces.push(textPiece("tools", tool.name, JSON.stringify(tool)));
50
+ // JSON.stringify(数组) 比各元素多出 `[`、`]` 与元素间的逗号
51
+ pieces.push({
52
+ category: "tools",
53
+ name: undefined,
54
+ wide: 0,
55
+ narrow: 2 + Math.max(0, tools.length - 1),
56
+ fixed: 0,
57
+ });
58
+ }
59
+ return pieces;
60
+ }
61
+ case "user":
62
+ return contentPieces(message.content, "user", undefined, "user");
63
+ case "toolResult":
64
+ return contentPieces(message.content, "toolResults", message.toolName, "toolResult");
65
+ case "custom":
66
+ return contentPieces(message.content, "custom", message.customType, "custom");
67
+ case "assistant":
68
+ return message.content.map((block) => block.type === "text"
69
+ ? textPiece("assistant", undefined, block.text)
70
+ : block.type === "thinking"
71
+ ? textPiece("reasoning", undefined, block.thinking)
72
+ : textPiece("toolCalls", block.name, block.name + JSON.stringify(block.arguments)));
73
+ case "compactionSummary":
74
+ return [textPiece("summaries", "compaction", message.summary)];
75
+ case "branchSummary":
76
+ return [textPiece("summaries", "branch", message.summary)];
77
+ }
78
+ }
79
+ /** 把消息总量分到各部分(最大余数法),返回与 pieces 对齐的 token 数。 */
80
+ function allocate(pieces, total) {
81
+ const tokens = pieces.map((p) => p.fixed + p.wide + Math.floor(p.narrow / CHARS_PER_TOKEN));
82
+ let left = total - tokens.reduce((sum, n) => sum + n, 0);
83
+ const order = pieces
84
+ .map((p, i) => ({ i, rest: p.narrow % CHARS_PER_TOKEN }))
85
+ .filter((x) => x.rest > 0)
86
+ .sort((a, b) => b.rest - a.rest || a.i - b.i);
87
+ for (const { i } of order) {
88
+ if (left <= 0)
89
+ break;
90
+ tokens[i] = (tokens[i] ?? 0) + 1;
91
+ left--;
92
+ }
93
+ // 理论上不会剩;保险起见补到最大的部分,保证合计与估算一致
94
+ if (left !== 0 && tokens.length > 0) {
95
+ let max = 0;
96
+ for (let i = 1; i < tokens.length; i++)
97
+ if ((tokens[i] ?? 0) > (tokens[max] ?? 0))
98
+ max = i;
99
+ tokens[max] = (tokens[max] ?? 0) + left;
100
+ }
101
+ return tokens;
102
+ }
103
+ /** 消息 → 分类明细(各类别之和 = `estimateMessagesTokens(messages)`)。 */
104
+ export function breakdownMessages(messages) {
105
+ const totals = new Map();
106
+ const parts = new Map();
107
+ for (const category of BREAKDOWN_CATEGORIES) {
108
+ totals.set(category, { tokens: 0, count: 0 });
109
+ parts.set(category, new Map());
110
+ }
111
+ const toolResults = [];
112
+ let total = 0;
113
+ let hasSystem = false;
114
+ for (const message of messages) {
115
+ const messageTokens = estimateMessageTokens(message);
116
+ total += messageTokens;
117
+ if (message.role === "system")
118
+ hasSystem = true;
119
+ if (message.role === "toolResult") {
120
+ toolResults.push({
121
+ ordinal: toolResults.length + 1,
122
+ toolName: message.toolName,
123
+ tokens: messageTokens,
124
+ });
125
+ }
126
+ const pieces = messagePieces(message);
127
+ const allocated = allocate(pieces, messageTokens);
128
+ pieces.forEach((piece, i) => {
129
+ const tokens = allocated[i] ?? 0;
130
+ const entry = totals.get(piece.category);
131
+ if (entry === undefined)
132
+ return;
133
+ entry.tokens += tokens;
134
+ if (piece.name !== undefined || piece.category !== "tools")
135
+ entry.count++;
136
+ if (piece.name !== undefined) {
137
+ const named = parts.get(piece.category);
138
+ named?.set(piece.name, (named.get(piece.name) ?? 0) + tokens);
139
+ }
140
+ });
141
+ }
142
+ const entries = BREAKDOWN_CATEGORIES.map((category) => {
143
+ const entry = totals.get(category) ?? { tokens: 0, count: 0 };
144
+ const named = [...(parts.get(category) ?? new Map())]
145
+ .map(([name, tokens]) => ({ name, tokens }))
146
+ .sort((a, b) => b.tokens - a.tokens || a.name.localeCompare(b.name));
147
+ return { category, tokens: entry.tokens, count: entry.count, parts: named };
148
+ });
149
+ const prefixTokens = entries
150
+ .filter((e) => e.category === "system" || e.category === "tools")
151
+ .reduce((sum, e) => sum + e.tokens, 0);
152
+ return { entries, total, prefixTokens, hasSystem, toolResults };
153
+ }
154
+ /** 最大的 n 个工具结果(大小相同按出现顺序)。 */
155
+ export function largestToolResults(breakdown, n) {
156
+ return [...breakdown.toolResults]
157
+ .sort((a, b) => b.tokens - a.tokens || a.ordinal - b.ordinal)
158
+ .slice(0, Math.max(0, n));
159
+ }
@@ -15,6 +15,11 @@ import type { AgentMessage, SessionEntry } from "../session/types.js";
15
15
  export declare const IMAGE_TOKENS = 1600;
16
16
  /** 文本的 token 估算:CJK / 假名 / 谚文每字 1,其余字符 / 4(向上取整)。 */
17
17
  export declare function estimateTextTokens(text: string): number;
18
+ interface CharCount {
19
+ wide: number;
20
+ narrow: number;
21
+ }
22
+ export declare function countText(text: string, into: CharCount): CharCount;
18
23
  export declare function calculateContextTokens(usage: Usage): number;
19
24
  /** 内容的估算(文本按脚本,图片每张 1 600)。 */
20
25
  export declare function estimateContentTokens(content: string | readonly ContentBlock[]): number;
@@ -36,3 +41,4 @@ export declare function estimateContextTokens(messages: readonly AgentMessage[])
36
41
  * `items` 是 buildProjection(branch).items,`branch` 是同一活动分支。
37
42
  */
38
43
  export declare function estimateProjectedTokens(items: readonly ContextItem[], branch: readonly SessionEntry[]): ContextEstimate;
44
+ export {};
@@ -29,7 +29,7 @@ function isWideCode(code) {
29
29
  export function estimateTextTokens(text) {
30
30
  return tokensOf(countText(text, { wide: 0, narrow: 0 }));
31
31
  }
32
- function countText(text, into) {
32
+ export function countText(text, into) {
33
33
  let wide = 0;
34
34
  let narrow = 0;
35
35
  for (let i = 0; i < text.length; i++) {
@@ -0,0 +1,23 @@
1
+ /**
2
+ * 启动基线:系统消息落盘之前(第一次请求前)的上下文估算。
3
+ *
4
+ * 系统提示与工具表在第一次请求前才作为首条 `system` 消息写进分支,在此之前投影里没有它们,
5
+ * 状态栏会一直显示 0。这里按「将要发送的那一份」估算:命名节文本 + 工具声明 JSON,与
6
+ * `estimateMessageTokens` 对 `system` 消息的口径一致(CJK 每字 1,其余字符 / 4)。
7
+ *
8
+ * 只用于统计:不写分支、不改请求体,前缀缓存不受影响;压缩阈值也不看它(摘要压不掉这部分)。
9
+ */
10
+ import type { ToolDecl } from "../ai/types.js";
11
+ import type { ContextEstimate } from "./estimate.js";
12
+ /** 系统提示(已装配的命名节)+ 工具声明的估算。 */
13
+ export declare function estimatePrefixTokens(sections: Readonly<Record<string, string>>, tools: readonly ToolDecl[]): number;
14
+ /** 上下文估算的来源(`SessionStats.context.source`)。 */
15
+ export type ContextSource = "usage" | "estimate" | "prefix";
16
+ export interface SourcedEstimate extends ContextEstimate {
17
+ source: ContextSource;
18
+ }
19
+ /**
20
+ * 给投影估算标来源;投影里还没有系统消息时把前缀基线加进去(`prefix` 懒算,只在需要时调用)。
21
+ * 有 usage 时照旧(usage 已经包含系统提示与工具表)。
22
+ */
23
+ export declare function withPrefixBaseline(estimate: ContextEstimate, hasSystemMessage: boolean, prefix: () => number): SourcedEstimate;
@@ -0,0 +1,34 @@
1
+ /**
2
+ * 启动基线:系统消息落盘之前(第一次请求前)的上下文估算。
3
+ *
4
+ * 系统提示与工具表在第一次请求前才作为首条 `system` 消息写进分支,在此之前投影里没有它们,
5
+ * 状态栏会一直显示 0。这里按「将要发送的那一份」估算:命名节文本 + 工具声明 JSON,与
6
+ * `estimateMessageTokens` 对 `system` 消息的口径一致(CJK 每字 1,其余字符 / 4)。
7
+ *
8
+ * 只用于统计:不写分支、不改请求体,前缀缓存不受影响;压缩阈值也不看它(摘要压不掉这部分)。
9
+ */
10
+ import { estimateMessageTokens } from "./estimate.js";
11
+ /** 系统提示(已装配的命名节)+ 工具声明的估算。 */
12
+ export function estimatePrefixTokens(sections, tools) {
13
+ const message = { role: "system", sections: { ...sections }, timestamp: 0 };
14
+ if (tools.length > 0)
15
+ message.toolsAdded = [...tools];
16
+ return estimateMessageTokens(message);
17
+ }
18
+ /**
19
+ * 给投影估算标来源;投影里还没有系统消息时把前缀基线加进去(`prefix` 懒算,只在需要时调用)。
20
+ * 有 usage 时照旧(usage 已经包含系统提示与工具表)。
21
+ */
22
+ export function withPrefixBaseline(estimate, hasSystemMessage, prefix) {
23
+ if (estimate.usageTokens > 0)
24
+ return { ...estimate, source: "usage" };
25
+ if (hasSystemMessage)
26
+ return { ...estimate, source: "estimate" };
27
+ const base = prefix();
28
+ return {
29
+ ...estimate,
30
+ tokens: estimate.tokens + base,
31
+ trailingTokens: estimate.trailingTokens + base,
32
+ source: "prefix",
33
+ };
34
+ }
@@ -6,7 +6,8 @@
6
6
  * - 本模块只搬运字符串,不解析 `!command` / `$ENV`(那是 ai/providers/auth.ts 的事),
7
7
  * 也不把 key 写进任何日志或返回给展示层(`describeAuthFile` 只给来源与形态)。
8
8
  */
9
- import { chmodSync, mkdirSync, renameSync, statSync, writeFileSync } from "node:fs";
9
+ import { chmodSync, mkdirSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
10
+ import { retryTransientFs } from "./fs-retry.js";
10
11
  import { dirname, join } from "node:path";
11
12
  import { loadConfigFile } from "./load.js";
12
13
  import { AUTH_FILE } from "./paths.js";
@@ -54,7 +55,14 @@ export function writeAuthFile(path, file) {
54
55
  mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
55
56
  const tmp = `${path}.${process.pid}.${Date.now()}.tmp`;
56
57
  writeFileSync(tmp, `${JSON.stringify(file, null, 2)}\n`, { mode: 0o600 });
57
- renameSync(tmp, path);
58
+ try {
59
+ // Windows:别的 ama 进程正读着 auth.json 时覆盖它会暂时失败
60
+ retryTransientFs(() => renameSync(tmp, path));
61
+ }
62
+ catch (error) {
63
+ rmSync(tmp, { force: true });
64
+ throw error;
65
+ }
58
66
  if (process.platform !== "win32")
59
67
  chmodSync(path, 0o600);
60
68
  }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Windows 上的暂时性文件错误(多个 ama 进程同时读写 auth.json 时)。
3
+ *
4
+ * NTFS 上文件被删除后、最后一个句柄关闭前处于「删除挂起」:此时 `open(wx)` 报 EPERM 而不是 EEXIST;
5
+ * 目标文件正被别的进程读时 `rename` 覆盖它会报 EPERM / EACCES / EBUSY。这些都是瞬时的,稍等重试即可。
6
+ * 其它平台上同样的错误码是真的权限问题,原样抛出。
7
+ */
8
+ /** 这个错误码在当前平台上是不是「别的进程正占着,稍后再试」。 */
9
+ export declare function isTransientFsError(code: unknown, platform?: string): boolean;
10
+ export interface FsRetryOptions {
11
+ platform?: string;
12
+ attempts?: number;
13
+ /** 第 n 次重试前等 `delayMs * n`。 */
14
+ delayMs?: number;
15
+ sleep?: (ms: number) => void;
16
+ }
17
+ /** 执行 `fn`;Windows 的暂时性错误按递增间隔重试(缺省 8 次,共约 0.7 s),其它错误立即抛出。 */
18
+ export declare function retryTransientFs<T>(fn: () => T, options?: FsRetryOptions): T;
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Windows 上的暂时性文件错误(多个 ama 进程同时读写 auth.json 时)。
3
+ *
4
+ * NTFS 上文件被删除后、最后一个句柄关闭前处于「删除挂起」:此时 `open(wx)` 报 EPERM 而不是 EEXIST;
5
+ * 目标文件正被别的进程读时 `rename` 覆盖它会报 EPERM / EACCES / EBUSY。这些都是瞬时的,稍等重试即可。
6
+ * 其它平台上同样的错误码是真的权限问题,原样抛出。
7
+ */
8
+ const TRANSIENT_WIN32 = new Set(["EPERM", "EACCES", "EBUSY"]);
9
+ /** 这个错误码在当前平台上是不是「别的进程正占着,稍后再试」。 */
10
+ export function isTransientFsError(code, platform = process.platform) {
11
+ return platform === "win32" && typeof code === "string" && TRANSIENT_WIN32.has(code);
12
+ }
13
+ /** 同步睡眠(只用在重试之间,最长几十毫秒)。 */
14
+ function sleepSync(ms) {
15
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
16
+ }
17
+ /** 执行 `fn`;Windows 的暂时性错误按递增间隔重试(缺省 8 次,共约 0.7 s),其它错误立即抛出。 */
18
+ export function retryTransientFs(fn, options = {}) {
19
+ const attempts = options.attempts ?? 8;
20
+ const delayMs = options.delayMs ?? 20;
21
+ const sleep = options.sleep ?? sleepSync;
22
+ for (let n = 1;; n++) {
23
+ try {
24
+ return fn();
25
+ }
26
+ catch (error) {
27
+ const code = error.code;
28
+ if (n >= attempts || !isTransientFsError(code, options.platform))
29
+ throw error;
30
+ sleep(delayMs * n);
31
+ }
32
+ }
33
+ }
@@ -281,6 +281,7 @@ function buildBaseSchema() {
281
281
  animation: bool(),
282
282
  restoreOnCancel: bool(),
283
283
  enterWhileRunning: oneOf(["queue", "interrupt"]),
284
+ programStatus: oneOf(["auto", "on", "off"]),
284
285
  statusLine: oneOf(STATUS_LINE_MODES),
285
286
  language: oneOf(LANGUAGE_SETTINGS),
286
287
  replyLanguage: str(),
@@ -29,6 +29,7 @@ const IMPLICIT_DEFAULTS = {
29
29
  animation: true,
30
30
  restoreOnCancel: true,
31
31
  enterWhileRunning: "queue",
32
+ programStatus: "auto",
32
33
  language: "auto",
33
34
  },
34
35
  // [W6-C0] 第六波键的缺省(行为由各批次实现)
@@ -261,6 +261,7 @@ export function validateConfig(value) {
261
261
  "animation",
262
262
  "restoreOnCancel",
263
263
  "enterWhileRunning",
264
+ "programStatus",
264
265
  ...W5_UI_KEYS,
265
266
  ...W6_UI_KEYS,
266
267
  ], (s, p) => {
@@ -275,6 +276,7 @@ export function validateConfig(value) {
275
276
  c.boolean(s, "animation", p);
276
277
  c.boolean(s, "restoreOnCancel", p);
277
278
  c.oneOf(s, "enterWhileRunning", p, ["queue", "interrupt"]);
279
+ c.oneOf(s, "programStatus", p, ["auto", "on", "off"]);
278
280
  checkUiW5(c, s, p);
279
281
  checkUiW6(c, s, p);
280
282
  });
@@ -35,6 +35,7 @@ const ROWS = [
35
35
  row("ui.statusLine", "ui", "now", "any"),
36
36
  row("ui.restoreOnCancel", "ui", "nextSession", "any"),
37
37
  row("ui.enterWhileRunning", "ui", "now", "any"),
38
+ row("ui.programStatus", "ui", "restart", "any"),
38
39
  row("ui.quietStartup", "ui", "restart", "any"),
39
40
  row("ui.ascii", "ui", "restart", "any", { envOverride: "AMA_ASCII" }),
40
41
  row("ui.language", "ui", "restart", "any", { envOverride: "AMA_LANG" }),
@@ -153,6 +153,8 @@ export interface UiConfig extends UiConfigW6 {
153
153
  restoreOnCancel?: boolean;
154
154
  /** 运行中按 Enter:queue(缺省)排队插话,interrupt 打断并立即发送(与 `app.message.interrupt` 互换)。 */
155
155
  enterWhileRunning?: "queue" | "interrupt";
156
+ /** 终端程序状态 OSC 7501:auto(缺省)检测通过才发,on 不检测直接发,off 不发。 */
157
+ programStatus?: "auto" | "on" | "off";
156
158
  /** [W5-C0] 底部信息行(W5-A):缺省独立终端 full、嵌入宿主(有 profile)compact。 */
157
159
  statusLine?: StatusLineMode;
158
160
  }
@@ -83,6 +83,8 @@ class CodexSession {
83
83
  tokensTotal;
84
84
  tokensAtStart;
85
85
  contextWindow;
86
+ /** 最近一次请求的总量(`tokenUsage.last.totalTokens`)= 线程当前的上下文占用。 */
87
+ contextTokens;
86
88
  closed = false;
87
89
  interruptRequested = false;
88
90
  constructor(agentId, transport, deps) {
@@ -234,6 +236,7 @@ class CodexSession {
234
236
  input: end.inputTokens - (start?.inputTokens ?? 0),
235
237
  output: end.outputTokens - (start?.outputTokens ?? 0),
236
238
  cacheRead: end.cachedInputTokens - (start?.cachedInputTokens ?? 0),
239
+ ...(this.contextTokens !== undefined ? { contextTokens: this.contextTokens } : {}),
237
240
  ...(this.contextWindow !== undefined ? { contextWindow: this.contextWindow } : {}),
238
241
  });
239
242
  }
@@ -257,6 +260,9 @@ class CodexSession {
257
260
  const total = readTokens(usage["total"]);
258
261
  if (total !== undefined)
259
262
  this.tokensTotal = total;
263
+ const last = readTokens(usage["last"]);
264
+ if (last !== undefined && last.totalTokens > 0)
265
+ this.contextTokens = last.totalTokens;
260
266
  if (typeof usage["modelContextWindow"] === "number")
261
267
  this.contextWindow = usage["modelContextWindow"];
262
268
  return;
@@ -161,6 +161,8 @@ class ProcessHandle {
161
161
  agent;
162
162
  /** [W6-C0] 本回合工具的骨架(id → 种类 / 状态 / 起止),回合结束写 `turn_trace`。 */
163
163
  turnTools = new Map();
164
+ /** 外部 Agent 最近报告的上下文占用与窗口(只记数字,跨回合保留)。 */
165
+ context = {};
164
166
  constructor(runner, chosen, request, deps) {
165
167
  this.runner = runner;
166
168
  this.chosen = chosen;
@@ -299,6 +301,7 @@ class ProcessHandle {
299
301
  this.emit({ type: "notice", level: event.level, text: event.text });
300
302
  return;
301
303
  case "usage":
304
+ this.noteContext(event.contextTokens, event.contextWindow);
302
305
  if (event.costUsd !== undefined) {
303
306
  turnCost.usd = event.costUsd;
304
307
  const budget = this.request.budgetUsd;
@@ -319,6 +322,19 @@ class ProcessHandle {
319
322
  return;
320
323
  }
321
324
  }
325
+ /** 上下文占用或窗口变了:记下并单独发一条只带它们的 usage(任务记录 / Agent 栏即时更新)。 */
326
+ noteContext(tokens, window) {
327
+ const next = { ...this.context };
328
+ if (tokens !== undefined)
329
+ next.contextTokens = tokens;
330
+ if (window !== undefined)
331
+ next.contextWindow = window;
332
+ if (next.contextTokens === this.context.contextTokens &&
333
+ next.contextWindow === this.context.contextWindow)
334
+ return;
335
+ this.context = next;
336
+ this.emit({ type: "usage", ...next });
337
+ }
322
338
  now() {
323
339
  return (this.deps.now ?? Date.now)();
324
340
  }
@@ -412,6 +428,7 @@ class ProcessHandle {
412
428
  unit,
413
429
  amount,
414
430
  ...(usage.totalTokens > 0 ? { tokens: usage.totalTokens } : {}),
431
+ ...this.context,
415
432
  });
416
433
  this.emit({ type: "usage", usage, unit, amount });
417
434
  }
@@ -27,6 +27,9 @@ export interface AgentUsageRecord {
27
27
  unit: ExternalUsageUnit;
28
28
  amount: number;
29
29
  tokens?: number;
30
+ /** 回合结束时该 Agent 报告的上下文占用与窗口(只记数字)。 */
31
+ contextTokens?: number;
32
+ contextWindow?: number;
30
33
  }
31
34
  /** 会话一侧的最小接口(`ToolContext.session` 或 SessionCore 都能提供)。 */
32
35
  export interface AgentStoreBackend {
@@ -75,6 +75,10 @@ export function aggregateExternal(entries) {
75
75
  row.amount += u.amount;
76
76
  if (typeof u.tokens === "number")
77
77
  row.tokens = (row.tokens ?? 0) + u.tokens;
78
+ if (typeof u.contextTokens === "number")
79
+ row.contextTokens = u.contextTokens;
80
+ if (typeof u.contextWindow === "number")
81
+ row.contextWindow = u.contextWindow;
78
82
  }
79
83
  return { byAgent };
80
84
  }
@@ -541,6 +541,8 @@ declare const en: {
541
541
  cacheSilent: string;
542
542
  sandbox: string;
543
543
  cycleHint: string;
544
+ autoCompact: string;
545
+ noWindow: (model: string) => string;
544
546
  quota: {
545
547
  session: string;
546
548
  reset: string;
@@ -998,6 +1000,7 @@ declare const en: {
998
1000
  interrupted: string;
999
1001
  };
1000
1002
  turns: (n: number) => string;
1003
+ context: (percent: string) => string;
1001
1004
  agentRunner: (agent: string, runner: string) => string;
1002
1005
  background: string;
1003
1006
  none: string;
@@ -1084,6 +1087,7 @@ declare const en: {
1084
1087
  tools: string;
1085
1088
  hooks: string;
1086
1089
  session: string;
1090
+ context: string;
1087
1091
  cache: string;
1088
1092
  statusline: string;
1089
1093
  planArgs: string;
@@ -1139,6 +1143,50 @@ declare const en: {
1139
1143
  cwdAnswer: string;
1140
1144
  notDirectory: (path: string) => string;
1141
1145
  };
1146
+ contextReport: {
1147
+ title: string;
1148
+ keyUsed: string;
1149
+ used: (tokens: string, window: string, percent: string | undefined) => string;
1150
+ keyLeft: string;
1151
+ left: (tokens: string) => string;
1152
+ keySource: string;
1153
+ sourceUsage: (usage: string, estimate: string) => string;
1154
+ sourceEstimate: string;
1155
+ keyAutoCompact: string;
1156
+ autoCompact: (at: string, distance: string) => string;
1157
+ autoCompactReached: (at: string) => string;
1158
+ autoCompactOff: string;
1159
+ windowUnknown: string;
1160
+ keyPrune: string;
1161
+ prune: (at: string) => string;
1162
+ breakdown: (total: string) => string;
1163
+ prefixCounted: string;
1164
+ prefixPending: string;
1165
+ categories: {
1166
+ system: string;
1167
+ tools: string;
1168
+ user: string;
1169
+ assistant: string;
1170
+ reasoning: string;
1171
+ toolCalls: string;
1172
+ toolResults: string;
1173
+ images: string;
1174
+ summaries: string;
1175
+ custom: string;
1176
+ };
1177
+ partNames: {
1178
+ compaction: string;
1179
+ branch: string;
1180
+ user: string;
1181
+ toolResult: string;
1182
+ custom: string;
1183
+ };
1184
+ images: (count: number) => string;
1185
+ more: (count: number) => string;
1186
+ largest: (count: number) => string;
1187
+ noToolResults: string;
1188
+ empty: string;
1189
+ };
1142
1190
  cache: {
1143
1191
  unreported: string;
1144
1192
  unknown: string;
@@ -1196,6 +1244,8 @@ declare const en: {
1196
1244
  usage: (input: number, output: number, cacheRead: number, cacheWrite: number, cost: string) => string;
1197
1245
  keyContext: string;
1198
1246
  context: (tokens: string, window: string, percent: string) => string;
1247
+ toAutoCompact: (tokens: string) => string;
1248
+ autoCompactOff: string;
1199
1249
  keyAgents: string;
1200
1250
  externalTitle: string;
1201
1251
  noFingerprint: string;
@@ -1333,6 +1383,7 @@ declare const en: {
1333
1383
  "ui.animation": string;
1334
1384
  "ui.restoreOnCancel": string;
1335
1385
  "ui.enterWhileRunning": string;
1386
+ "ui.programStatus": string;
1336
1387
  "ui.statusLine": string;
1337
1388
  "ui.language": string;
1338
1389
  "ui.replyLanguage": string;
@@ -1742,6 +1793,7 @@ declare const en: {
1742
1793
  interrupted: string;
1743
1794
  };
1744
1795
  turns: (n: number) => string;
1796
+ context: (percent: string) => string;
1745
1797
  bar: {
1746
1798
  more: (n: number) => string;
1747
1799
  keys: (up: string, down: string) => string;
@@ -2089,6 +2141,7 @@ declare const en: {
2089
2141
  "ui.statusLine": string;
2090
2142
  "ui.restoreOnCancel": string;
2091
2143
  "ui.enterWhileRunning": string;
2144
+ "ui.programStatus": string;
2092
2145
  "ui.quietStartup": string;
2093
2146
  "ui.ascii": string;
2094
2147
  "ui.language": string;