@armadra/agent 0.7.5 → 0.7.6
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.
- package/CHANGELOG.md +21 -0
- package/CHANGELOG.zh-CN.md +21 -0
- package/dist/agent/session-classifier.d.ts +2 -0
- package/dist/agent/session-classifier.js +24 -3
- package/dist/agent/session-extensions.d.ts +1 -0
- package/dist/agent/session-extensions.js +3 -0
- package/dist/agent/session-subagent.js +9 -2
- package/dist/agent/subagent-background.d.ts +5 -1
- package/dist/agent/subagent-background.js +13 -1
- package/dist/agent/subagent-fork.d.ts +10 -5
- package/dist/agent/subagent-fork.js +22 -6
- package/dist/agent/subagent-registry.d.ts +1 -1
- package/dist/agent/subagent-registry.js +1 -1
- package/dist/agents/catalog.d.ts +1 -0
- package/dist/ai/apis/max-tokens.d.ts +4 -0
- package/dist/ai/apis/max-tokens.js +12 -1
- package/dist/ai/providers/enrich.d.ts +6 -2
- package/dist/ai/providers/enrich.js +13 -2
- package/dist/ai/providers/max-tokens-cache.d.ts +22 -0
- package/dist/ai/providers/max-tokens-cache.js +95 -0
- package/dist/ai/providers/registry.js +3 -3
- package/dist/bundle/ama.cjs +296 -103
- package/dist/checkpoints/shadow-git.d.ts +5 -1
- package/dist/checkpoints/shadow-git.js +11 -2
- package/dist/cli/compose-providers.d.ts +1 -0
- package/dist/cli/compose-providers.js +3 -0
- package/dist/cli/compose-store.d.ts +6 -2
- package/dist/cli/compose-store.js +8 -4
- package/dist/cli/compose.js +2 -2
- package/dist/config/json-schema.js +1 -0
- package/dist/config/key-docs.js +1 -0
- package/dist/config/schema-w5.js +2 -0
- package/dist/config/settings-registry.js +1 -0
- package/dist/config/types-w5.d.ts +5 -0
- package/dist/i18n/catalog.d.ts +2 -0
- package/dist/i18n/messages/config-keys.d.ts +2 -0
- package/dist/i18n/messages/config-keys.js +2 -0
- package/dist/i18n/messages/config.d.ts +2 -0
- package/dist/i18n/messages/interactive.d.ts +2 -0
- package/dist/i18n/messages/interactive.js +2 -0
- package/dist/i18n/messages/session.js +2 -2
- package/dist/modes/interactive/agent-ui.js +7 -0
- package/dist/session/manager.d.ts +3 -1
- package/dist/session/manager.js +5 -0
- package/dist/session/offload.d.ts +10 -2
- package/dist/session/offload.js +27 -4
- package/dist/tools/background-jobs.d.ts +4 -4
- package/dist/tools/background-jobs.js +4 -4
- package/dist/tools/bash.d.ts +2 -1
- package/dist/tools/bash.js +4 -3
- package/dist/tools/task-ctl.js +2 -1
- package/dist/tools/task.d.ts +9 -0
- package/dist/tools/task.js +27 -1
- package/docs/agents.md +14 -3
- package/docs/en/permissions.md +1 -1
- package/docs/en/providers.md +4 -4
- package/docs/en/sessions.md +2 -1
- package/docs/permissions.md +1 -1
- package/docs/providers.md +9 -5
- package/docs/session-format.md +2 -1
- package/docs/sessions.md +2 -1
- package/package.json +1 -1
|
@@ -16,8 +16,10 @@
|
|
|
16
16
|
export declare const SHADOW_DIR = "shadow";
|
|
17
17
|
/** cwd 下(不含忽略的)文件数上限,超出本会话降级为 tools。 */
|
|
18
18
|
export declare const SHADOW_MAX_FILES = 20000;
|
|
19
|
-
/**
|
|
19
|
+
/** 单次快照耗时上限(毫秒),连续 `SHADOW_SLOW_STREAK` 次超出本会话降级为 tools。 */
|
|
20
20
|
export declare const SHADOW_MAX_SNAPSHOT_MS = 3000;
|
|
21
|
+
/** 连续这么多次快照超时才降级(首次快照不计)。 */
|
|
22
|
+
export declare const SHADOW_SLOW_STREAK = 2;
|
|
21
23
|
export declare function shadowRoot(dataDir: string): string;
|
|
22
24
|
/** cwd 对应的影子仓库目录。 */
|
|
23
25
|
export declare function shadowRepoDir(dataDir: string, cwd: string): string;
|
|
@@ -73,6 +75,8 @@ export declare class ShadowRepo {
|
|
|
73
75
|
private readonly indexFile;
|
|
74
76
|
private initialized;
|
|
75
77
|
private counted;
|
|
78
|
+
/** 连续超时的快照数(首次快照不计),到 `SHADOW_SLOW_STREAK` 才降级。 */
|
|
79
|
+
private slowStreak;
|
|
76
80
|
constructor(dataDir: string, cwd: string, options?: ShadowGitOptions);
|
|
77
81
|
private now;
|
|
78
82
|
private env;
|
|
@@ -23,8 +23,10 @@ import { msg } from "../i18n/index.js";
|
|
|
23
23
|
export const SHADOW_DIR = "shadow";
|
|
24
24
|
/** cwd 下(不含忽略的)文件数上限,超出本会话降级为 tools。 */
|
|
25
25
|
export const SHADOW_MAX_FILES = 20_000;
|
|
26
|
-
/**
|
|
26
|
+
/** 单次快照耗时上限(毫秒),连续 `SHADOW_SLOW_STREAK` 次超出本会话降级为 tools。 */
|
|
27
27
|
export const SHADOW_MAX_SNAPSHOT_MS = 3_000;
|
|
28
|
+
/** 连续这么多次快照超时才降级(首次快照不计)。 */
|
|
29
|
+
export const SHADOW_SLOW_STREAK = 2;
|
|
28
30
|
/** 单个 git 进程的硬上限,防止卡死。 */
|
|
29
31
|
const GIT_TIMEOUT_MS = 60_000;
|
|
30
32
|
/** 本地配置的版本;改了配置项就加一,旧仓库会补写。 */
|
|
@@ -85,6 +87,8 @@ export class ShadowRepo {
|
|
|
85
87
|
indexFile;
|
|
86
88
|
initialized;
|
|
87
89
|
counted = false;
|
|
90
|
+
/** 连续超时的快照数(首次快照不计),到 `SHADOW_SLOW_STREAK` 才降级。 */
|
|
91
|
+
slowStreak = 0;
|
|
88
92
|
constructor(dataDir, cwd, options = {}) {
|
|
89
93
|
this.cwd = resolve(cwd);
|
|
90
94
|
this.dir = shadowRepoDir(dataDir, this.cwd);
|
|
@@ -207,6 +211,8 @@ export class ShadowRepo {
|
|
|
207
211
|
await this.init();
|
|
208
212
|
// 计时不含一次性的建仓与本地配置(十来个 git 子进程,Windows 上可达数秒),只量快照本身
|
|
209
213
|
const started = this.now();
|
|
214
|
+
// 首次快照要把整个工作区读进索引,天然偏慢,不计入超时
|
|
215
|
+
const first = !this.counted;
|
|
210
216
|
await this.writeExcludes();
|
|
211
217
|
if (!this.counted) {
|
|
212
218
|
const out = await this.run([
|
|
@@ -233,7 +239,10 @@ export class ShadowRepo {
|
|
|
233
239
|
args.push("-p", parent);
|
|
234
240
|
const commit = await this.text(args);
|
|
235
241
|
const elapsed = this.now() - started;
|
|
236
|
-
if (
|
|
242
|
+
if (first)
|
|
243
|
+
return { commit };
|
|
244
|
+
this.slowStreak = elapsed > this.maxSnapshotMs ? this.slowStreak + 1 : 0;
|
|
245
|
+
if (this.slowStreak >= SHADOW_SLOW_STREAK) {
|
|
237
246
|
return {
|
|
238
247
|
commit,
|
|
239
248
|
degrade: {
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* `originator` 头;OAuth 刷新拿到 `auth.chatgpt` 配置与同一份 env。
|
|
15
15
|
* - 发现缓存(`<dataDir>/models/discovered/<provider>.json`)并入模型表为空的供应商(discovered-cache.ts);
|
|
16
16
|
* chatgpt 的缓存 flavor 与当前登录不符时视为过期、不并入。
|
|
17
|
+
* - max_tokens 上限缓存(`<dataDir>/models/max-tokens-caps.json`,30 天)载回进程内的表并订阅写回(#152)。
|
|
17
18
|
*/
|
|
18
19
|
import type { ApiRegistry } from "../ai/apis/api.js";
|
|
19
20
|
import { ProviderRegistry } from "../ai/providers/registry.js";
|
|
@@ -14,10 +14,12 @@
|
|
|
14
14
|
* `originator` 头;OAuth 刷新拿到 `auth.chatgpt` 配置与同一份 env。
|
|
15
15
|
* - 发现缓存(`<dataDir>/models/discovered/<provider>.json`)并入模型表为空的供应商(discovered-cache.ts);
|
|
16
16
|
* chatgpt 的缓存 flavor 与当前登录不符时视为过期、不并入。
|
|
17
|
+
* - max_tokens 上限缓存(`<dataDir>/models/max-tokens-caps.json`,30 天)载回进程内的表并订阅写回(#152)。
|
|
17
18
|
*/
|
|
18
19
|
import { loadModelsDevIndex } from "../ai/providers/models-dev-cache.js";
|
|
19
20
|
import { ProviderRegistry, discoverLocalModels } from "../ai/providers/registry.js";
|
|
20
21
|
import { mergeDiscoveredModels } from "../ai/providers/discovered-cache.js";
|
|
22
|
+
import { attachMaxTokensCache } from "../ai/providers/max-tokens-cache.js";
|
|
21
23
|
import { lazyIndex } from "../ai/providers/enrich.js";
|
|
22
24
|
import { readOAuthEntry } from "../auth/oauth/token-store.js";
|
|
23
25
|
import { CHATGPT_BASE_URLS, CHATGPT_PROVIDER_ID } from "../auth/chatgpt/presets.js";
|
|
@@ -110,6 +112,7 @@ export async function buildProviderRegistry(input, options = {}) {
|
|
|
110
112
|
}
|
|
111
113
|
// 发现缓存(`ama auth login chatgpt` / `ama models discover chatgpt`)并入模型表为空的供应商
|
|
112
114
|
if (dataDir !== undefined) {
|
|
115
|
+
attachMaxTokensCache(dataDir);
|
|
113
116
|
const flavor = readOAuthEntry(input.authFile, CHATGPT_PROVIDER_ID)?.flavor;
|
|
114
117
|
mergeDiscoveredModels(registry, dataDir, lazyIndex(() => loadModelsDevIndex(dataDir)), (id) => (id === CHATGPT_PROVIDER_ID ? flavor : undefined));
|
|
115
118
|
}
|
|
@@ -6,16 +6,19 @@
|
|
|
6
6
|
* - `resume{id}` / `fork{id}`:先在本目录、再在全部子目录里按 id 或唯一前缀找文件;找不到 →
|
|
7
7
|
* `session_not_found`,前缀不唯一 → `invalid_arguments` 并列出候选;
|
|
8
8
|
* - `session-id{id}`:找到则打开,否则以该 id 新建;
|
|
9
|
-
* - `fork{id}
|
|
9
|
+
* - `fork{id}`:打开后从叶子复制出新文件,原会话立即关闭(释放锁);源会话不属于任何 AgentSession,
|
|
10
|
+
* 它的告警(图片读不回,#183)显式交给 `context.log`。
|
|
10
11
|
*/
|
|
11
12
|
import { SessionManager } from "../session/manager.js";
|
|
12
13
|
import type { SessionListItem } from "../session/types.js";
|
|
14
|
+
import type { LogFn } from "./compose-session.js";
|
|
13
15
|
import type { RuntimeDeps, SessionRequest } from "./deps.js";
|
|
14
16
|
/** 按 id / 唯一前缀找会话文件;本 cwd 目录优先。 */
|
|
15
17
|
export declare function findSessionFile(root: string, id: string, cwd?: string): string | undefined;
|
|
16
18
|
export declare function openSession(request: SessionRequest, context: {
|
|
17
19
|
sessionDir: string;
|
|
18
20
|
cwd: string;
|
|
21
|
+
log?: LogFn | undefined;
|
|
19
22
|
}): SessionManager;
|
|
20
23
|
/** 列会话(最新在前);子 Agent 会话缺省跳过,`includeSubagents` 时保留(带 `subagent` 标记)。 */
|
|
21
24
|
export declare function listSessions(context: {
|
|
@@ -23,4 +26,5 @@ export declare function listSessions(context: {
|
|
|
23
26
|
cwd?: string;
|
|
24
27
|
includeSubagents?: boolean;
|
|
25
28
|
}): SessionListItem[];
|
|
26
|
-
|
|
29
|
+
/** `log`:`--fork` 源会话的告警出口(通常是会话的 `options.log`)。 */
|
|
30
|
+
export declare function createSessionStore(log?: LogFn): RuntimeDeps["sessions"];
|
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
* - `resume{id}` / `fork{id}`:先在本目录、再在全部子目录里按 id 或唯一前缀找文件;找不到 →
|
|
7
7
|
* `session_not_found`,前缀不唯一 → `invalid_arguments` 并列出候选;
|
|
8
8
|
* - `session-id{id}`:找到则打开,否则以该 id 新建;
|
|
9
|
-
* - `fork{id}
|
|
9
|
+
* - `fork{id}`:打开后从叶子复制出新文件,原会话立即关闭(释放锁);源会话不属于任何 AgentSession,
|
|
10
|
+
* 它的告警(图片读不回,#183)显式交给 `context.log`。
|
|
10
11
|
*/
|
|
11
12
|
import { existsSync, readdirSync, statSync } from "node:fs";
|
|
12
13
|
import { join } from "node:path";
|
|
@@ -81,7 +82,9 @@ export function openSession(request, context) {
|
|
|
81
82
|
: SessionManager.open(file);
|
|
82
83
|
}
|
|
83
84
|
case "fork": {
|
|
84
|
-
const source = SessionManager.open(requireFile(root, request.id, cwd)
|
|
85
|
+
const source = SessionManager.open(requireFile(root, request.id, cwd), {
|
|
86
|
+
warn: (message) => context.log?.("warn", message),
|
|
87
|
+
});
|
|
85
88
|
try {
|
|
86
89
|
const leaf = source.leafId();
|
|
87
90
|
if (leaf === null)
|
|
@@ -104,9 +107,10 @@ export function listSessions(context) {
|
|
|
104
107
|
.filter((item) => context.includeSubagents === true || item.subagent !== true)
|
|
105
108
|
.sort((a, b) => b.modifiedAt.localeCompare(a.modifiedAt));
|
|
106
109
|
}
|
|
107
|
-
|
|
110
|
+
/** `log`:`--fork` 源会话的告警出口(通常是会话的 `options.log`)。 */
|
|
111
|
+
export function createSessionStore(log) {
|
|
108
112
|
return {
|
|
109
|
-
open: (request, context) => openSession(request, context),
|
|
113
|
+
open: (request, context) => openSession(request, { ...context, log }),
|
|
110
114
|
list: async (context) => listSessions(context),
|
|
111
115
|
show: async (id, context) => {
|
|
112
116
|
const file = requireFile(context.sessionDir, id);
|
package/dist/cli/compose.js
CHANGED
|
@@ -40,7 +40,7 @@ import { builtinTools } from "../tools/registry.js";
|
|
|
40
40
|
import { MEMORY_TOOL, createMemoryTool } from "../memory/tool.js";
|
|
41
41
|
import { takeCodemodeNotice } from "./codemode-notice.js";
|
|
42
42
|
import { buildProviderRegistry } from "./compose-providers.js";
|
|
43
|
-
import { composeSession, emptyComposeState, } from "./compose-session.js";
|
|
43
|
+
import { composeSession, emptyComposeState, stderrLog, } from "./compose-session.js";
|
|
44
44
|
import { createSessionStore } from "./compose-store.js";
|
|
45
45
|
import { defaultSendUser } from "./startup-steps.js";
|
|
46
46
|
import { imageFitOptionsFor } from "../modes/image-input.js";
|
|
@@ -211,7 +211,7 @@ export function createRuntimeDeps(options = {}) {
|
|
|
211
211
|
return registry;
|
|
212
212
|
},
|
|
213
213
|
},
|
|
214
|
-
sessions: createSessionStore(),
|
|
214
|
+
sessions: createSessionStore(options.log ?? stderrLog()),
|
|
215
215
|
tools: { create: (input) => createTools(input, options, state) },
|
|
216
216
|
permissions: {
|
|
217
217
|
create: (input) => new PermissionPipeline({
|
|
@@ -161,6 +161,7 @@ function w5Sections() {
|
|
|
161
161
|
background: oneOf(SUBAGENT_BACKGROUND_MODES),
|
|
162
162
|
autoBackgroundAfterMs: num(0),
|
|
163
163
|
retainSessions: num(0),
|
|
164
|
+
forkMaxContextRatio: num(0.05, 0.95),
|
|
164
165
|
}),
|
|
165
166
|
models: object({ aliases: object({ fast: str(), strong: str() }), enabled: strings }),
|
|
166
167
|
fallbackModel: str(),
|
package/dist/config/key-docs.js
CHANGED
package/dist/config/schema-w5.js
CHANGED
|
@@ -90,6 +90,7 @@ export function validateConfigW5(c, config) {
|
|
|
90
90
|
"background",
|
|
91
91
|
"autoBackgroundAfterMs",
|
|
92
92
|
"retainSessions",
|
|
93
|
+
"forkMaxContextRatio",
|
|
93
94
|
], (s, p) => {
|
|
94
95
|
c.number(s, "maxConcurrent", p, 1, 64);
|
|
95
96
|
c.number(s, "maxPending", p, 0, 1024);
|
|
@@ -97,6 +98,7 @@ export function validateConfigW5(c, config) {
|
|
|
97
98
|
c.oneOf(s, "background", p, SUBAGENT_BACKGROUND_MODES);
|
|
98
99
|
c.number(s, "autoBackgroundAfterMs", p, 0);
|
|
99
100
|
c.number(s, "retainSessions", p, 0);
|
|
101
|
+
c.number(s, "forkMaxContextRatio", p, 0.05, 0.95);
|
|
100
102
|
});
|
|
101
103
|
checkSection(c, config, "models", ["aliases", "enabled"], (s, p) => {
|
|
102
104
|
c.stringArray(s, "enabled", p);
|
|
@@ -101,6 +101,7 @@ const ROWS = [
|
|
|
101
101
|
row("subagents.maxConcurrent", "agents", "nextSession", "deny"),
|
|
102
102
|
row("subagents.maxPending", "agents", "nextSession", "deny"),
|
|
103
103
|
row("subagents.retainSessions", "agents", "nextSession", "deny"),
|
|
104
|
+
row("subagents.forkMaxContextRatio", "agents", "nextSession", "deny"),
|
|
104
105
|
// [W7-B2] 改 task 工具描述(缺省后台 / 前台两种文案),故 prefix
|
|
105
106
|
row("subagents.background", "agents", "restart", "any", { prefix: true }),
|
|
106
107
|
row("subagents.autoBackgroundAfterMs", "agents", "restart", "any"),
|
|
@@ -79,6 +79,11 @@ export interface SubagentsConfig {
|
|
|
79
79
|
autoBackgroundAfterMs?: number;
|
|
80
80
|
/** [M-F] 已结束子会话保留在内存的句柄数(LRU),缺省 4;超出的续聊时从会话文件重开。只认用户级。 */
|
|
81
81
|
retainSessions?: number;
|
|
82
|
+
/**
|
|
83
|
+
* [#149] fork 回落比例:父上一次请求的输入 token 超过(窗口 − compaction.reserveTokens)× 本值时
|
|
84
|
+
* fork 回落为 fresh;0.05–0.95,缺省 0.5。只认用户级。
|
|
85
|
+
*/
|
|
86
|
+
forkMaxContextRatio?: number;
|
|
82
87
|
}
|
|
83
88
|
/** `models.enabled` 的一项:`provider/model[@channel]` 或 `provider/*`。 */
|
|
84
89
|
export declare const MODELS_ENABLED_REF: RegExp;
|
package/dist/i18n/catalog.d.ts
CHANGED
|
@@ -534,6 +534,7 @@ declare const en: {
|
|
|
534
534
|
backgroundStopped: (jobId: string, command: string) => string;
|
|
535
535
|
compactionFailed: (error: string) => string;
|
|
536
536
|
compactionCancelled: string;
|
|
537
|
+
imageReadBackFailed: (id: string) => string;
|
|
537
538
|
};
|
|
538
539
|
statusLine: {
|
|
539
540
|
usage: string;
|
|
@@ -1446,6 +1447,7 @@ declare const en: {
|
|
|
1446
1447
|
"subagents.maxPending": string;
|
|
1447
1448
|
"subagents.retainSessions": string;
|
|
1448
1449
|
"subagents.defaultModel": string;
|
|
1450
|
+
"subagents.forkMaxContextRatio": string;
|
|
1449
1451
|
"subagents.background": string;
|
|
1450
1452
|
"subagents.autoBackgroundAfterMs": string;
|
|
1451
1453
|
models: string;
|
|
@@ -115,6 +115,7 @@ export declare const en: {
|
|
|
115
115
|
"subagents.maxPending": string;
|
|
116
116
|
"subagents.retainSessions": string;
|
|
117
117
|
"subagents.defaultModel": string;
|
|
118
|
+
"subagents.forkMaxContextRatio": string;
|
|
118
119
|
"subagents.background": string;
|
|
119
120
|
"subagents.autoBackgroundAfterMs": string;
|
|
120
121
|
models: string;
|
|
@@ -269,6 +270,7 @@ export declare const zh: {
|
|
|
269
270
|
"subagents.maxPending": string;
|
|
270
271
|
"subagents.retainSessions": string;
|
|
271
272
|
"subagents.defaultModel": string;
|
|
273
|
+
"subagents.forkMaxContextRatio": string;
|
|
272
274
|
"subagents.background": string;
|
|
273
275
|
"subagents.autoBackgroundAfterMs": string;
|
|
274
276
|
models: string;
|
|
@@ -164,6 +164,7 @@ const keysEn = {
|
|
|
164
164
|
"subagents.maxPending": "Queue limit; task fails beyond it",
|
|
165
165
|
"subagents.retainSessions": "Finished sub-sessions kept in memory (least recently used first out); others reopen from their session file when continued",
|
|
166
166
|
"subagents.defaultModel": "Default sub-session model provider/model[@channel]",
|
|
167
|
+
"subagents.forkMaxContextRatio": "Fork sub-agents fall back to fresh when the parent's last request used more than this share of (window − compaction.reserveTokens); 0.05–0.95, default 0.5",
|
|
167
168
|
"subagents.background": "Whether task runs in the background by default: auto = background in the TUI / RPC / ACP and foreground with -p; always / never are fixed. The call argument and the agent type's background: override it; user, project and host level",
|
|
168
169
|
"subagents.autoBackgroundAfterMs": "Move a foreground task to the background after it has run this many milliseconds; 0 disables; user and project level",
|
|
169
170
|
models: "Model aliases and the model picker list; user level only (effective from wave 5 W5-G)",
|
|
@@ -292,6 +293,7 @@ const keysZh = {
|
|
|
292
293
|
"subagents.maxPending": "排队上限,超出时 task 报错",
|
|
293
294
|
"subagents.retainSessions": "内存中保留的已结束子会话数(最久未用的先释放),其余续聊时从会话文件重开",
|
|
294
295
|
"subagents.defaultModel": "子会话缺省模型 provider/model[@channel]",
|
|
296
|
+
"subagents.forkMaxContextRatio": "fork 子 Agent 的回落比例:父上一次请求超过(窗口 − compaction.reserveTokens)的这一比例时改为 fresh;0.05–0.95,缺省 0.5",
|
|
295
297
|
"subagents.background": "task 缺省是否后台:auto = TUI / RPC / ACP 下后台、-p 下前台;always / never 固定。调用参数与类型定义的 background: 优先;用户、项目、宿主级都认",
|
|
296
298
|
"subagents.autoBackgroundAfterMs": "前台任务运行超过该毫秒数自动转后台;0 关闭;用户、项目级都认",
|
|
297
299
|
models: "模型别名与模型选择器清单;只认用户级(第五波 W5-G 起生效)",
|
|
@@ -115,6 +115,7 @@ export declare const en: {
|
|
|
115
115
|
"subagents.maxPending": string;
|
|
116
116
|
"subagents.retainSessions": string;
|
|
117
117
|
"subagents.defaultModel": string;
|
|
118
|
+
"subagents.forkMaxContextRatio": string;
|
|
118
119
|
"subagents.background": string;
|
|
119
120
|
"subagents.autoBackgroundAfterMs": string;
|
|
120
121
|
models: string;
|
|
@@ -383,6 +384,7 @@ export declare const zh: {
|
|
|
383
384
|
"subagents.maxPending": string;
|
|
384
385
|
"subagents.retainSessions": string;
|
|
385
386
|
"subagents.defaultModel": string;
|
|
387
|
+
"subagents.forkMaxContextRatio": string;
|
|
386
388
|
"subagents.background": string;
|
|
387
389
|
"subagents.autoBackgroundAfterMs": string;
|
|
388
390
|
models: string;
|
|
@@ -85,6 +85,7 @@ export declare const en: {
|
|
|
85
85
|
backgroundStopped: (jobId: string, command: string) => string;
|
|
86
86
|
compactionFailed: (error: string) => string;
|
|
87
87
|
compactionCancelled: string;
|
|
88
|
+
imageReadBackFailed: (id: string) => string;
|
|
88
89
|
};
|
|
89
90
|
statusLine: {
|
|
90
91
|
usage: string;
|
|
@@ -335,6 +336,7 @@ export declare const zh: {
|
|
|
335
336
|
backgroundStopped: (jobId: string, command: string) => string;
|
|
336
337
|
compactionFailed: (error: string) => string;
|
|
337
338
|
compactionCancelled: string;
|
|
339
|
+
imageReadBackFailed: (id: string) => string;
|
|
338
340
|
};
|
|
339
341
|
statusLine: {
|
|
340
342
|
usage: string;
|
|
@@ -99,6 +99,7 @@ export const en = {
|
|
|
99
99
|
backgroundStopped: (jobId, command) => `Background command ${jobId} stopped: ${command}`,
|
|
100
100
|
compactionFailed: (error) => `Compaction failed: ${error}`,
|
|
101
101
|
compactionCancelled: "Compaction cancelled",
|
|
102
|
+
imageReadBackFailed: (id) => `Session image for entry ${id} could not be read back (file changed); it stays empty`,
|
|
102
103
|
},
|
|
103
104
|
statusLine: {
|
|
104
105
|
usage: "Usage: /statusline [full|compact]",
|
|
@@ -234,6 +235,7 @@ export const zh = {
|
|
|
234
235
|
backgroundStopped: (jobId, command) => `后台命令 ${jobId} 已停止:${command}`,
|
|
235
236
|
compactionFailed: (error) => `压缩失败:${error}`,
|
|
236
237
|
compactionCancelled: "压缩已取消",
|
|
238
|
+
imageReadBackFailed: (id) => `会话文件已改变,条目 ${id} 的图片读不回,保留为空`,
|
|
237
239
|
},
|
|
238
240
|
statusLine: {
|
|
239
241
|
usage: "用法:/statusline [full|compact]",
|
|
@@ -88,7 +88,7 @@ export const en = {
|
|
|
88
88
|
cwdIsHome: "the working directory is the home directory",
|
|
89
89
|
gitNotFound: "git not found",
|
|
90
90
|
tooManyFiles: (files, max) => `the working directory has ${plural(files, "file")} (limit ${max})`,
|
|
91
|
-
tooSlow: (seconds, max) => `
|
|
91
|
+
tooSlow: (seconds, max) => `snapshots took longer than ${max} s twice in a row (latest ${seconds} s)`,
|
|
92
92
|
gitFailed: (command, code, detail) => `git ${command} failed (${code})${detail === "" ? "" : `: ${detail}`}`,
|
|
93
93
|
noShadowCommit: "the checkpoint has no shadow commit",
|
|
94
94
|
shadowCommitMissing: (commit) => `shadow commit ${commit} does not exist`,
|
|
@@ -211,7 +211,7 @@ export const zh = {
|
|
|
211
211
|
cwdIsHome: "工作目录是家目录",
|
|
212
212
|
gitNotFound: "找不到 git",
|
|
213
213
|
tooManyFiles: (files, max) => `工作目录有 ${files} 个文件(上限 ${max})`,
|
|
214
|
-
tooSlow: (seconds, max) =>
|
|
214
|
+
tooSlow: (seconds, max) => `连续两次快照超过 ${max} 秒(最近一次 ${seconds} 秒)`,
|
|
215
215
|
gitFailed: (command, code, detail) => `git ${command} 失败(${code})${detail === "" ? "" : `:${detail}`}`,
|
|
216
216
|
noShadowCommit: "检查点没有影子提交",
|
|
217
217
|
shadowCommitMissing: (commit) => `影子提交 ${commit} 不存在`,
|
|
@@ -45,6 +45,8 @@ export function agentCommandHooks(get) {
|
|
|
45
45
|
};
|
|
46
46
|
}
|
|
47
47
|
const TASK_NOTICE = /^\[task (t\d+)\] ([\s\S]*)$/;
|
|
48
|
+
/** #183:会话图片读不回(`ImageOffload` 的告警)转通知区。 */
|
|
49
|
+
const SESSION_NOTICE = /^cannot read session entry (\S+) back from /;
|
|
48
50
|
export class AgentUi {
|
|
49
51
|
deps;
|
|
50
52
|
plan;
|
|
@@ -164,6 +166,11 @@ export class AgentUi {
|
|
|
164
166
|
this.deps.notice(level === "warn" ? "warn" : "info", `[${agent} · ${taskId}] ${match[2]}`);
|
|
165
167
|
return;
|
|
166
168
|
}
|
|
169
|
+
const image = level === "warn" ? SESSION_NOTICE.exec(message) : null;
|
|
170
|
+
if (image !== null) {
|
|
171
|
+
this.deps.notice("warn", msg().interactive.events.imageReadBackFailed(image[1]));
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
167
174
|
original?.call(holder, level, message);
|
|
168
175
|
};
|
|
169
176
|
holder.log = routed;
|
|
@@ -37,7 +37,7 @@ export declare class SessionManager implements SessionManagerApi {
|
|
|
37
37
|
private closed;
|
|
38
38
|
private readonly now;
|
|
39
39
|
private readonly images;
|
|
40
|
-
private
|
|
40
|
+
private warn;
|
|
41
41
|
/** 已知的会话文件字节数(新行的行首偏移)。 */
|
|
42
42
|
private fileBytes;
|
|
43
43
|
private constructor();
|
|
@@ -65,6 +65,8 @@ export declare class SessionManager implements SessionManagerApi {
|
|
|
65
65
|
private syncImages;
|
|
66
66
|
/** 原文条目(已卸载的从文件回读一份副本)。 */
|
|
67
67
|
private original;
|
|
68
|
+
/** 接上(或换掉)告警出口(图片读不回等);`open()` 期间缓冲的告警随即按序冲出(#183)。 */
|
|
69
|
+
setWarn(warn: (message: string) => void): void;
|
|
68
70
|
/** 已卸载图片的条目数(测试与诊断用)。 */
|
|
69
71
|
offloadedCount(): number;
|
|
70
72
|
private leafLine;
|
package/dist/session/manager.js
CHANGED
|
@@ -182,6 +182,11 @@ export class SessionManager {
|
|
|
182
182
|
return entry;
|
|
183
183
|
return this.images.original(this.storage.file, entry);
|
|
184
184
|
}
|
|
185
|
+
/** 接上(或换掉)告警出口(图片读不回等);`open()` 期间缓冲的告警随即按序冲出(#183)。 */
|
|
186
|
+
setWarn(warn) {
|
|
187
|
+
this.warn = warn;
|
|
188
|
+
this.images.setWarn(warn);
|
|
189
|
+
}
|
|
185
190
|
/** 已卸载图片的条目数(测试与诊断用)。 */
|
|
186
191
|
offloadedCount() {
|
|
187
192
|
return this.images.size;
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
* `content` 换新数组,image 块换成 `data: ""` 的占位;
|
|
11
11
|
* - 回读:按行位置 `readSync` 一行、`JSON.parse`,校验 id 与内容块数,图片经 `internImage` 驻留后
|
|
12
12
|
* **就地**放回占位所在的数组(之前拿到同一消息对象的调用方随之看到原图);读不回(文件被外部改写)时
|
|
13
|
-
* 保留 `""`
|
|
13
|
+
* 保留 `""` 并告警,不再重试;告警通道可后接(`setWarn`):接上之前的告警先缓冲(最多
|
|
14
|
+
* `PENDING_WARNINGS` 条,其余折叠成一条计数),接上时按序冲出(#183);
|
|
14
15
|
* - `original(entry)`:给 `getEntries` / `fork` 的原文副本,不改内存状态。
|
|
15
16
|
*/
|
|
16
17
|
import type { SessionEntry } from "./types.js";
|
|
@@ -25,12 +26,18 @@ export declare function hasImages(entry: SessionEntry): boolean;
|
|
|
25
26
|
export declare function readLineAt(file: string, locator: LineLocator): unknown;
|
|
26
27
|
/** 文件里全部 `context_edit` 的目标 id(只解析 context_edit 行;读不了时为空集)。 */
|
|
27
28
|
export declare function editTargets(file: string): Set<string>;
|
|
29
|
+
/** 告警通道接上之前最多缓冲的条数;超出的只计数,冲出时折叠成一条。 */
|
|
30
|
+
export declare const PENDING_WARNINGS = 16;
|
|
28
31
|
export declare class ImageOffload {
|
|
29
|
-
private readonly warn;
|
|
30
32
|
/** 含图 message 条目的行位置(只有已落盘的条目才有)。 */
|
|
31
33
|
private readonly locators;
|
|
32
34
|
private readonly offloaded;
|
|
35
|
+
private warn;
|
|
36
|
+
private readonly pending;
|
|
37
|
+
private dropped;
|
|
33
38
|
constructor(warn?: (message: string) => void);
|
|
39
|
+
/** 接上(或换掉)告警通道;之前缓冲的告警按序冲出。 */
|
|
40
|
+
setWarn(warn: (message: string) => void): void;
|
|
34
41
|
/** 记下条目的行位置;不含图片的条目忽略。 */
|
|
35
42
|
track(entry: SessionEntry, locator: LineLocator): void;
|
|
36
43
|
/** 读文件时逐行调用:记下行位置;`strip` 时就地换成占位(刚解析的对象,别处没有引用)。 */
|
|
@@ -48,4 +55,5 @@ export declare class ImageOffload {
|
|
|
48
55
|
sync(file: string, branch: readonly SessionEntry[], index: ReadonlyMap<string, SessionEntry>): void;
|
|
49
56
|
/** 从文件读回条目(新对象);不成功时告警、忘掉行位置并返回 undefined。 */
|
|
50
57
|
private read;
|
|
58
|
+
private report;
|
|
51
59
|
}
|
package/dist/session/offload.js
CHANGED
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
* `content` 换新数组,image 块换成 `data: ""` 的占位;
|
|
11
11
|
* - 回读:按行位置 `readSync` 一行、`JSON.parse`,校验 id 与内容块数,图片经 `internImage` 驻留后
|
|
12
12
|
* **就地**放回占位所在的数组(之前拿到同一消息对象的调用方随之看到原图);读不回(文件被外部改写)时
|
|
13
|
-
* 保留 `""`
|
|
13
|
+
* 保留 `""` 并告警,不再重试;告警通道可后接(`setWarn`):接上之前的告警先缓冲(最多
|
|
14
|
+
* `PENDING_WARNINGS` 条,其余折叠成一条计数),接上时按序冲出(#183);
|
|
14
15
|
* - `original(entry)`:给 `getEntries` / `fork` 的原文副本,不改内存状态。
|
|
15
16
|
*/
|
|
16
17
|
import { closeSync, openSync, readSync } from "node:fs";
|
|
@@ -74,14 +75,28 @@ export function editTargets(file) {
|
|
|
74
75
|
return targets;
|
|
75
76
|
}
|
|
76
77
|
const placeholder = (block) => ({ ...block, data: "" });
|
|
78
|
+
/** 告警通道接上之前最多缓冲的条数;超出的只计数,冲出时折叠成一条。 */
|
|
79
|
+
export const PENDING_WARNINGS = 16;
|
|
77
80
|
export class ImageOffload {
|
|
78
|
-
warn;
|
|
79
81
|
/** 含图 message 条目的行位置(只有已落盘的条目才有)。 */
|
|
80
82
|
locators = new Map();
|
|
81
83
|
offloaded = new Set();
|
|
82
|
-
|
|
84
|
+
warn;
|
|
85
|
+
pending = [];
|
|
86
|
+
dropped = 0;
|
|
87
|
+
constructor(warn) {
|
|
83
88
|
this.warn = warn;
|
|
84
89
|
}
|
|
90
|
+
/** 接上(或换掉)告警通道;之前缓冲的告警按序冲出。 */
|
|
91
|
+
setWarn(warn) {
|
|
92
|
+
this.warn = warn;
|
|
93
|
+
const pending = this.pending.splice(0);
|
|
94
|
+
if (this.dropped > 0)
|
|
95
|
+
pending.push(`and ${this.dropped} more session entries could not be read back`);
|
|
96
|
+
this.dropped = 0;
|
|
97
|
+
for (const message of pending)
|
|
98
|
+
warn(message);
|
|
99
|
+
}
|
|
85
100
|
/** 记下条目的行位置;不含图片的条目忽略。 */
|
|
86
101
|
track(entry, locator) {
|
|
87
102
|
if (hasImages(entry))
|
|
@@ -188,8 +203,16 @@ export class ImageOffload {
|
|
|
188
203
|
catch (error) {
|
|
189
204
|
this.locators.delete(entry.id);
|
|
190
205
|
this.offloaded.delete(entry.id);
|
|
191
|
-
this.
|
|
206
|
+
this.report(`cannot read session entry ${entry.id} back from ${file}: ${String(error)}`);
|
|
192
207
|
return undefined;
|
|
193
208
|
}
|
|
194
209
|
}
|
|
210
|
+
report(message) {
|
|
211
|
+
if (this.warn !== undefined)
|
|
212
|
+
this.warn(message);
|
|
213
|
+
else if (this.pending.length < PENDING_WARNINGS)
|
|
214
|
+
this.pending.push(message);
|
|
215
|
+
else
|
|
216
|
+
this.dropped++;
|
|
217
|
+
}
|
|
195
218
|
}
|
|
@@ -5,14 +5,14 @@
|
|
|
5
5
|
* 这里只拿到 ChildProcess;stdout / stderr 直接写进 `outputPath`(文件描述符交给子进程,ama 退出也不
|
|
6
6
|
* 会因管道断开而 SIGPIPE)。POSIX 自成进程组并登记(进程退出时同步 SIGKILL 残留组)。
|
|
7
7
|
* - `wait(id, timeoutMs)`:等到退出或超时;`stop(id)`:杀整棵树(SIGTERM → 宽限 → SIGKILL);
|
|
8
|
-
* `output(id)
|
|
8
|
+
* `output(id, options?)`:读输出文件尾部(缺省 2000 行 / 50 KB,调用方可传字节上限)。
|
|
9
9
|
* - 生命周期事件经 `onChange` 发出(started / exited / stopped);`takeExited()` 取走尚未通知的已退出
|
|
10
10
|
* 任务(提醒通道 reminders.ts 据此告诉模型「后台命令 jobId 已退出(码 N)」)。
|
|
11
11
|
* - 按会话 id 登记(`jobsForSession`);`disposeSessionJobs` 在会话 dispose 时回收进程树。
|
|
12
12
|
*/
|
|
13
13
|
import type { ChildProcess } from "node:child_process";
|
|
14
14
|
import { type ProcessDeps } from "./process-tree.js";
|
|
15
|
-
import { type TruncateResult } from "./truncate.js";
|
|
15
|
+
import { type TruncateOptions, type TruncateResult } from "./truncate.js";
|
|
16
16
|
export type JobStatus = "running" | "exited" | "stopped";
|
|
17
17
|
export interface Job {
|
|
18
18
|
id: string;
|
|
@@ -49,8 +49,8 @@ export declare class BackgroundJobs {
|
|
|
49
49
|
/** 等到退出或超时(`signal` 可提前结束等待,不杀进程)。 */
|
|
50
50
|
wait(id: string, timeoutMs: number, signal?: AbortSignal): Promise<Job | undefined>;
|
|
51
51
|
stop(id: string): Promise<Job | undefined>;
|
|
52
|
-
/**
|
|
53
|
-
output(id: string): (TruncateResult & {
|
|
52
|
+
/** 输出文件尾部(缺省 2000 行 / 50 KB;调用方可传字节上限,#155)。 */
|
|
53
|
+
output(id: string, options?: TruncateOptions): (TruncateResult & {
|
|
54
54
|
job: Job;
|
|
55
55
|
}) | undefined;
|
|
56
56
|
/** 取走已退出、尚未通知的任务(提醒通道用;wait 已看到退出的也算已知)。 */
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* 这里只拿到 ChildProcess;stdout / stderr 直接写进 `outputPath`(文件描述符交给子进程,ama 退出也不
|
|
6
6
|
* 会因管道断开而 SIGPIPE)。POSIX 自成进程组并登记(进程退出时同步 SIGKILL 残留组)。
|
|
7
7
|
* - `wait(id, timeoutMs)`:等到退出或超时;`stop(id)`:杀整棵树(SIGTERM → 宽限 → SIGKILL);
|
|
8
|
-
* `output(id)
|
|
8
|
+
* `output(id, options?)`:读输出文件尾部(缺省 2000 行 / 50 KB,调用方可传字节上限)。
|
|
9
9
|
* - 生命周期事件经 `onChange` 发出(started / exited / stopped);`takeExited()` 取走尚未通知的已退出
|
|
10
10
|
* 任务(提醒通道 reminders.ts 据此告诉模型「后台命令 jobId 已退出(码 N)」)。
|
|
11
11
|
* - 按会话 id 登记(`jobsForSession`);`disposeSessionJobs` 在会话 dispose 时回收进程树。
|
|
@@ -119,8 +119,8 @@ export class BackgroundJobs {
|
|
|
119
119
|
await entry.done;
|
|
120
120
|
return entry.job;
|
|
121
121
|
}
|
|
122
|
-
/**
|
|
123
|
-
output(id) {
|
|
122
|
+
/** 输出文件尾部(缺省 2000 行 / 50 KB;调用方可传字节上限,#155)。 */
|
|
123
|
+
output(id, options = {}) {
|
|
124
124
|
const entry = this.entries.get(id);
|
|
125
125
|
if (entry === undefined)
|
|
126
126
|
return undefined;
|
|
@@ -132,7 +132,7 @@ export class BackgroundJobs {
|
|
|
132
132
|
catch {
|
|
133
133
|
// 输出文件被删:按空输出
|
|
134
134
|
}
|
|
135
|
-
return { ...truncateTail(text), job: entry.job };
|
|
135
|
+
return { ...truncateTail(text, options), job: entry.job };
|
|
136
136
|
}
|
|
137
137
|
/** 取走已退出、尚未通知的任务(提醒通道用;wait 已看到退出的也算已知)。 */
|
|
138
138
|
takeExited() {
|
package/dist/tools/bash.d.ts
CHANGED
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
* - 注入 `AMA_*` 环境变量(shell.ts)。shell 退出后不等仍占着管道的后台进程。
|
|
10
10
|
* - [W5-H2] 后台命令(§8.3 H6,不加新工具):`{ command, background: true }` 立即返回
|
|
11
11
|
* `{ jobId, outputPath, pid }`;`{ job, action: "wait" | "output" | "stop" }` 查询 / 结束
|
|
12
|
-
* (background-jobs.ts,按会话登记,会话 dispose
|
|
12
|
+
* (background-jobs.ts,按会话登记,会话 dispose 时回收进程树;退出经提醒通道通知)。查询输出同样按
|
|
13
|
+
* 会话结果上限尾截断(#155)。
|
|
13
14
|
* - **进程只在 `spawnShell` 里创建**(前台与后台共用)。[S2] `sandbox.bash: auto` 且有能限制写入的
|
|
14
15
|
* OS 沙箱时在这一处包一层(src/sandbox/bash.ts);`sandbox: false` 不包装(权限管线照常审批)。
|
|
15
16
|
* 失败输出像是被沙箱拒绝时末尾追加一行说明。
|
package/dist/tools/bash.js
CHANGED
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
* - 注入 `AMA_*` 环境变量(shell.ts)。shell 退出后不等仍占着管道的后台进程。
|
|
10
10
|
* - [W5-H2] 后台命令(§8.3 H6,不加新工具):`{ command, background: true }` 立即返回
|
|
11
11
|
* `{ jobId, outputPath, pid }`;`{ job, action: "wait" | "output" | "stop" }` 查询 / 结束
|
|
12
|
-
* (background-jobs.ts,按会话登记,会话 dispose
|
|
12
|
+
* (background-jobs.ts,按会话登记,会话 dispose 时回收进程树;退出经提醒通道通知)。查询输出同样按
|
|
13
|
+
* 会话结果上限尾截断(#155)。
|
|
13
14
|
* - **进程只在 `spawnShell` 里创建**(前台与后台共用)。[S2] `sandbox.bash: auto` 且有能限制写入的
|
|
14
15
|
* OS 沙箱时在这一处包一层(src/sandbox/bash.ts);`sandbox: false` 不包装(权限管线照常审批)。
|
|
15
16
|
* 失败输出像是被沙箱拒绝时末尾追加一行说明。
|
|
@@ -304,7 +305,7 @@ async function jobAction(input, ctx, options) {
|
|
|
304
305
|
}
|
|
305
306
|
if (action === "stop") {
|
|
306
307
|
const job = (await jobs.stop(id));
|
|
307
|
-
const out = jobs.output(id);
|
|
308
|
+
const out = jobs.output(id, { maxBytes: toolOutputBytes(ctx.maxResultChars) });
|
|
308
309
|
const text = out?.content ?? "";
|
|
309
310
|
return {
|
|
310
311
|
content: `${text === "" ? "(no output)" : text}\n\n${jobStatusLine(job)}`,
|
|
@@ -317,7 +318,7 @@ async function jobAction(input, ctx, options) {
|
|
|
317
318
|
return fail(`timeoutMs must be between 1 and ${MAX_BASH_TIMEOUT_MS}`);
|
|
318
319
|
await jobs.wait(id, waitMs, ctx.signal);
|
|
319
320
|
}
|
|
320
|
-
const out = jobs.output(id);
|
|
321
|
+
const out = jobs.output(id, { maxBytes: toolOutputBytes(ctx.maxResultChars) });
|
|
321
322
|
if (out === undefined)
|
|
322
323
|
return fail(`Unknown background job ${id}`);
|
|
323
324
|
const job = out.job;
|
package/dist/tools/task-ctl.js
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import { capTaskText, formatTokens, waitDetachedText } from "../agents/result.js";
|
|
14
14
|
import { taskControl } from "../agents/task-control.js";
|
|
15
|
+
import { nestedTaskRejection } from "./task.js";
|
|
15
16
|
export const TASK_CTL_TOOL = "task_ctl";
|
|
16
17
|
export const DEFAULT_WAIT_MS = 30_000;
|
|
17
18
|
export const MAX_WAIT_MS = 600_000;
|
|
@@ -66,7 +67,7 @@ export function createTaskCtlTool() {
|
|
|
66
67
|
annotations: { pollable: true },
|
|
67
68
|
async execute(input, ctx) {
|
|
68
69
|
if (ctx.depth >= 1)
|
|
69
|
-
return fail("task_ctl is not available inside a sub-agent.");
|
|
70
|
+
return fail(nestedTaskRejection(ctx, "task_ctl", "task_ctl is not available inside a sub-agent."));
|
|
70
71
|
const control = taskControl(ctx.sessionId);
|
|
71
72
|
if (input.action === "list") {
|
|
72
73
|
const tasks = control?.list() ?? ctx.tasks?.list() ?? [];
|