@armadra/agent 0.7.3 → 0.7.4
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 +37 -0
- package/CHANGELOG.zh-CN.md +37 -0
- package/dist/agent/retry.d.ts +14 -5
- package/dist/agent/retry.js +30 -8
- package/dist/agent/session-cache-key.d.ts +8 -0
- package/dist/agent/session-cache-key.js +46 -0
- package/dist/agent/session-cache.d.ts +2 -3
- package/dist/agent/session-cache.js +19 -37
- package/dist/agent/session-classifier.d.ts +3 -2
- package/dist/agent/session-classifier.js +36 -10
- package/dist/agent/session-compaction.d.ts +24 -5
- package/dist/agent/session-compaction.js +54 -20
- package/dist/agent/session-core.d.ts +2 -0
- package/dist/agent/session-run.d.ts +12 -0
- package/dist/agent/session-run.js +29 -10
- package/dist/agent/session-subagent.d.ts +8 -1
- package/dist/agent/session-subagent.js +37 -14
- package/dist/agent/session-sync.d.ts +8 -2
- package/dist/agent/session-sync.js +7 -10
- package/dist/agent/session-tools.js +3 -2
- package/dist/agent/subagent-fork.d.ts +67 -0
- package/dist/agent/subagent-fork.js +87 -0
- package/dist/agent/subagent-registry.d.ts +2 -2
- package/dist/agent/subagent-registry.js +2 -2
- package/dist/agent/tool-availability.d.ts +8 -0
- package/dist/agent/tool-availability.js +8 -0
- package/dist/agent/tool-runner.js +13 -4
- package/dist/agent/worktree.d.ts +8 -0
- package/dist/agent/worktree.js +51 -4
- package/dist/agents/parse.js +6 -1
- package/dist/agents/task-record.js +3 -0
- package/dist/agents/types.d.ts +2 -0
- package/dist/ai/apis/anthropic-messages.js +10 -5
- package/dist/ai/apis/anthropic-request.d.ts +2 -1
- package/dist/ai/apis/anthropic-request.js +9 -3
- package/dist/ai/apis/google-generative-ai.js +4 -2
- package/dist/ai/apis/max-tokens.d.ts +42 -0
- package/dist/ai/apis/max-tokens.js +121 -0
- package/dist/ai/apis/openai-completions.js +10 -5
- package/dist/ai/apis/openai-request.js +1 -1
- package/dist/ai/apis/openai-responses-request.js +1 -1
- package/dist/ai/apis/openai-responses.js +9 -5
- package/dist/ai/apis/shared.d.ts +3 -1
- package/dist/ai/apis/shared.js +21 -3
- package/dist/ai/cache/fingerprint.d.ts +4 -1
- package/dist/ai/cache/fingerprint.js +18 -1
- package/dist/ai/cache/miss.d.ts +2 -1
- package/dist/ai/cache/miss.js +9 -3
- package/dist/ai/cache/reporting.js +5 -1
- package/dist/ai/cache/types.d.ts +10 -2
- package/dist/ai/cache/warmer.d.ts +6 -1
- package/dist/ai/cache/warmer.js +7 -0
- package/dist/ai/context.d.ts +12 -4
- package/dist/ai/context.js +43 -24
- package/dist/ai/fake/fake-provider.d.ts +8 -1
- package/dist/ai/fake/fake-provider.js +19 -7
- package/dist/ai/fake/fake-script.d.ts +2 -0
- package/dist/ai/http.d.ts +17 -9
- package/dist/ai/http.js +33 -13
- package/dist/ai/image-intern.d.ts +29 -0
- package/dist/ai/image-intern.js +58 -0
- package/dist/ai/json-body.d.ts +31 -0
- package/dist/ai/json-body.js +208 -0
- package/dist/ai/overflow.js +5 -0
- package/dist/ai/providers/catalog-data.js +13 -13
- package/dist/ai/providers/catalog.d.ts +33 -0
- package/dist/ai/providers/catalog.js +105 -6
- package/dist/ai/providers/enrich.d.ts +13 -3
- package/dist/ai/providers/enrich.js +87 -31
- package/dist/ai/sse.d.ts +3 -2
- package/dist/ai/sse.js +3 -2
- package/dist/ai/types.d.ts +9 -2
- package/dist/bundle/ama.cjs +3110 -1793
- package/dist/cli/compose-agents.js +3 -0
- package/dist/cli/compose-extensions.js +2 -0
- package/dist/cli/compose-request.d.ts +20 -0
- package/dist/cli/compose-request.js +37 -0
- package/dist/cli/compose-session.js +2 -2
- package/dist/cli/subcommands/model-meta.d.ts +2 -1
- package/dist/cli/subcommands/model-meta.js +3 -2
- package/dist/codemode/host-side.d.ts +11 -4
- package/dist/codemode/host-side.js +25 -6
- package/dist/codemode/tool.d.ts +2 -0
- package/dist/codemode/tool.js +4 -0
- package/dist/config/json-schema.js +9 -2
- package/dist/config/key-docs.js +13 -4
- package/dist/config/schema-w5.js +9 -1
- package/dist/config/schema.d.ts +1 -1
- package/dist/config/schema.js +19 -3
- package/dist/config/settings-registry.js +6 -0
- package/dist/config/types-w5.d.ts +2 -0
- package/dist/config/types.d.ts +19 -1
- package/dist/config/types.js +4 -0
- package/dist/i18n/catalog.d.ts +13 -2
- package/dist/i18n/messages/cli.d.ts +2 -0
- package/dist/i18n/messages/cli.js +2 -0
- package/dist/i18n/messages/config-keys.d.ts +10 -0
- package/dist/i18n/messages/config-keys.js +12 -2
- package/dist/i18n/messages/config.d.ts +14 -0
- package/dist/i18n/messages/config.js +4 -0
- package/dist/i18n/messages/errors.d.ts +3 -2
- package/dist/i18n/messages/errors.js +15 -2
- package/dist/i18n/messages/report.d.ts +2 -0
- package/dist/i18n/messages/report.js +2 -0
- package/dist/i18n/messages/settings.d.ts +4 -0
- package/dist/i18n/messages/settings.js +6 -2
- package/dist/i18n/messages/subcommands.d.ts +2 -2
- package/dist/i18n/messages/subcommands.js +4 -2
- package/dist/modes/acp/acp-server.js +15 -5
- package/dist/modes/acp/acp-sessions.d.ts +9 -0
- package/dist/modes/acp/acp-sessions.js +25 -0
- package/dist/modes/context-report.js +3 -2
- package/dist/modes/print/json-event.d.ts +12 -1
- package/dist/modes/print/json-event.js +48 -1
- package/dist/modes/rpc/rpc-mode.d.ts +1 -0
- package/dist/modes/rpc/rpc-mode.js +9 -2
- package/dist/modes/session-report.d.ts +1 -1
- package/dist/modes/session-report.js +12 -6
- package/dist/rpc.d.ts +5 -2
- package/dist/session/line-reader.d.ts +28 -0
- package/dist/session/line-reader.js +82 -0
- package/dist/session/list.d.ts +14 -0
- package/dist/session/list.js +150 -0
- package/dist/session/manager.d.ts +7 -3
- package/dist/session/manager.js +21 -48
- package/dist/session/migrate.d.ts +2 -1
- package/dist/session/migrate.js +2 -1
- package/dist/session/projection.d.ts +16 -3
- package/dist/session/projection.js +96 -29
- package/dist/session/scan.d.ts +3 -3
- package/dist/session/scan.js +9 -18
- package/dist/session/store.d.ts +3 -0
- package/dist/session/store.js +37 -31
- package/dist/session/types.d.ts +7 -2
- package/dist/tools/bash.d.ts +1 -1
- package/dist/tools/bash.js +5 -4
- package/dist/tools/glob.d.ts +2 -2
- package/dist/tools/glob.js +2 -2
- package/dist/tools/grep.js +4 -6
- package/dist/tools/image-file.js +2 -1
- package/dist/tools/read-lines.d.ts +32 -0
- package/dist/tools/read-lines.js +147 -0
- package/dist/tools/read.d.ts +6 -2
- package/dist/tools/read.js +47 -27
- package/dist/tools/task.d.ts +9 -0
- package/dist/tools/task.js +21 -8
- package/dist/tools/truncate.d.ts +7 -0
- package/dist/tools/truncate.js +11 -0
- package/dist/tools/types.d.ts +13 -0
- package/dist/trace/build-index.js +13 -1
- package/dist/tui/ansi.d.ts +2 -0
- package/dist/tui/ansi.js +7 -3
- package/dist/tui/components/editor-buffer.js +2 -2
- package/dist/tui/components/editor.js +5 -6
- package/dist/tui/keys.js +2 -2
- package/dist/tui/stdin-buffer.js +3 -3
- package/docs/acp.md +5 -1
- package/docs/agents.md +42 -6
- package/docs/codemode.md +4 -2
- package/docs/en/acp.md +3 -2
- package/docs/en/host-api.md +1 -1
- package/docs/en/permissions.md +2 -2
- package/docs/en/providers.md +54 -20
- package/docs/en/rpc.md +23 -7
- package/docs/en/sessions.md +4 -2
- package/docs/en/tui.md +2 -2
- package/docs/permissions.md +4 -1
- package/docs/providers.md +85 -22
- package/docs/rpc.md +22 -6
- package/docs/session-format.md +3 -2
- package/docs/sessions.md +3 -1
- package/docs/tui.md +1 -1
- package/package.json +2 -2
|
@@ -31,7 +31,7 @@ import { AcpEventMapper, permissionModes, toolKind, toolLocations, toolTitle, }
|
|
|
31
31
|
import { msg } from "../../i18n/index.js";
|
|
32
32
|
import { CONFIG_IDS, applyConfigOption, availableCommands, buildConfigOptions, prepareConfigOptions, } from "./acp-config.js";
|
|
33
33
|
import { clientCapabilitiesOf } from "./acp-connection.js";
|
|
34
|
-
import { PromptQueue, cancelledResult, pageSessions, sessionTitle, } from "./acp-sessions.js";
|
|
34
|
+
import { PromptQueue, cancelledResult, pageSessions, promptWhenIdle, sessionTitle, } from "./acp-sessions.js";
|
|
35
35
|
export const ACP_AGENT_CAPABILITIES = {
|
|
36
36
|
loadSession: true,
|
|
37
37
|
promptCapabilities: { image: true, audio: false, embeddedContext: true },
|
|
@@ -336,6 +336,7 @@ export class AcpServer {
|
|
|
336
336
|
async prompt(params, signal) {
|
|
337
337
|
const entry = this.entryOf(params);
|
|
338
338
|
const { text, images } = promptOf(params["prompt"]);
|
|
339
|
+
let onAbort;
|
|
339
340
|
const result = new Promise((resolve, reject) => {
|
|
340
341
|
const job = {
|
|
341
342
|
sessionId: entry.id,
|
|
@@ -347,13 +348,22 @@ export class AcpServer {
|
|
|
347
348
|
};
|
|
348
349
|
// 客户端以 $/cancel_request 撤回:等价 session/cancel,答复由对等端改成 -32800。
|
|
349
350
|
// 连接关闭(stdin 结束)也会 abort 这个 signal——那时不算撤回,已开始的运行照常跑完。
|
|
350
|
-
|
|
351
|
+
onAbort = () => {
|
|
351
352
|
if (this.peer.isOpen)
|
|
352
353
|
this.cancelJob(job);
|
|
353
|
-
}
|
|
354
|
+
};
|
|
355
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
354
356
|
this.queue.push(job);
|
|
355
357
|
});
|
|
356
|
-
|
|
358
|
+
let answer;
|
|
359
|
+
try {
|
|
360
|
+
answer = await result;
|
|
361
|
+
}
|
|
362
|
+
finally {
|
|
363
|
+
// 有监听的组合信号(AbortSignal.any)被运行时强持有:不摘掉,job → 会话随之常驻(D2 L1)
|
|
364
|
+
if (onAbort !== undefined)
|
|
365
|
+
signal?.removeEventListener("abort", onAbort);
|
|
366
|
+
}
|
|
357
367
|
if (signal?.aborted === true && this.peer.isOpen)
|
|
358
368
|
throw new RpcError(RPC_ERRORS.requestCancelled, "session/prompt: request cancelled");
|
|
359
369
|
return answer;
|
|
@@ -370,7 +380,7 @@ export class AcpServer {
|
|
|
370
380
|
const before = session.getStats().tokens;
|
|
371
381
|
let failure;
|
|
372
382
|
try {
|
|
373
|
-
await session
|
|
383
|
+
await promptWhenIdle(session, job);
|
|
374
384
|
}
|
|
375
385
|
catch (error) {
|
|
376
386
|
failure = error;
|
|
@@ -78,3 +78,12 @@ export declare function decodeCursor(cursor: unknown): {
|
|
|
78
78
|
};
|
|
79
79
|
/** 一页会话(`cursor` 缺省从头)。 */
|
|
80
80
|
export declare function pageSessions(items: readonly SessionListItem[], cursor: unknown, pageSize?: number): AcpListSessionsResult;
|
|
81
|
+
/**
|
|
82
|
+
* 跑一个出队的提示(docs/memory-plan.md D4、§2.2,Issue #139)。[M-A]
|
|
83
|
+
*
|
|
84
|
+
* 后台子 Agent 的完成通知以 `followUp` 在会话空闲时开回合,不经 {@link PromptQueue};那一回合在跑时
|
|
85
|
+
* 直接 `prompt` 会报 busy。这里先等它结束;与通知器竞速输了(等到空闲后它抢先开了回合)再等一轮。
|
|
86
|
+
* 不用 `streamingBehavior: "followUp"` 入队:`abort()` 不清队列,等待期间的 `session/cancel`
|
|
87
|
+
* 会让这条提示在之后的周期里冒出来。`job.cancelRequested`(cancel / close / 撤回)→ 不再发,直接返回。
|
|
88
|
+
*/
|
|
89
|
+
export declare function promptWhenIdle(session: AgentSessionImpl, job: PromptJob): Promise<void>;
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { RPC_ERRORS, } from "../../drivers/acp/types.js";
|
|
10
10
|
import { RpcError } from "../../drivers/jsonrpc.js";
|
|
11
|
+
import { AmaError } from "../../errors.js";
|
|
11
12
|
import { msg } from "../../i18n/index.js";
|
|
12
13
|
/** `session/list` 每页条数。 */
|
|
13
14
|
export const LIST_PAGE_SIZE = 50;
|
|
@@ -155,3 +156,27 @@ export function pageSessions(items, cursor, pageSize = LIST_PAGE_SIZE) {
|
|
|
155
156
|
? { sessions, nextCursor: encodeCursor(last.updatedAt, last.sessionId) }
|
|
156
157
|
: { sessions };
|
|
157
158
|
}
|
|
159
|
+
/**
|
|
160
|
+
* 跑一个出队的提示(docs/memory-plan.md D4、§2.2,Issue #139)。[M-A]
|
|
161
|
+
*
|
|
162
|
+
* 后台子 Agent 的完成通知以 `followUp` 在会话空闲时开回合,不经 {@link PromptQueue};那一回合在跑时
|
|
163
|
+
* 直接 `prompt` 会报 busy。这里先等它结束;与通知器竞速输了(等到空闲后它抢先开了回合)再等一轮。
|
|
164
|
+
* 不用 `streamingBehavior: "followUp"` 入队:`abort()` 不清队列,等待期间的 `session/cancel`
|
|
165
|
+
* 会让这条提示在之后的周期里冒出来。`job.cancelRequested`(cancel / close / 撤回)→ 不再发,直接返回。
|
|
166
|
+
*/
|
|
167
|
+
export async function promptWhenIdle(session, job) {
|
|
168
|
+
for (;;) {
|
|
169
|
+
// 等周期而不是看 `state.isStreaming`:busy 以周期为准,周期的收尾阶段 isStreaming 已是 false
|
|
170
|
+
await session.waitForIdle();
|
|
171
|
+
if (job.cancelRequested)
|
|
172
|
+
return;
|
|
173
|
+
try {
|
|
174
|
+
await session.prompt(job.text, job.images.length > 0 ? { images: job.images } : {});
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
catch (error) {
|
|
178
|
+
if (!(error instanceof AmaError && error.code === "busy"))
|
|
179
|
+
throw error;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* 统计只读:取活动分支的投影(与 `getStats().contextTokens` 同一口径),不改请求体、不写条目。
|
|
11
11
|
*/
|
|
12
12
|
import { AgentSessionImpl } from "../agent/session.js";
|
|
13
|
-
import { DEFAULT_COMPACTION_SETTINGS, pendingPrefixMessage } from "../agent/session-compaction.js";
|
|
13
|
+
import { DEFAULT_COMPACTION_SETTINGS, effectiveWindow, pendingPrefixMessage, } from "../agent/session-compaction.js";
|
|
14
14
|
import { breakdownMessages, largestToolResults, } from "../compaction/breakdown.js";
|
|
15
15
|
import { estimateContextTokens, estimateProjectedTokens, } from "../compaction/estimate.js";
|
|
16
16
|
import { prunePolicy } from "../compaction/prune-tier.js";
|
|
@@ -48,7 +48,8 @@ export function contextSnapshot(session) {
|
|
|
48
48
|
estimate = { ...estimate, tokens: stats.contextTokens };
|
|
49
49
|
const window = stats.contextWindow;
|
|
50
50
|
const reserve = compaction.reserveTokens ?? DEFAULT_COMPACTION_SETTINGS.reserveTokens;
|
|
51
|
-
const
|
|
51
|
+
const soft = effectiveWindow(window, compaction);
|
|
52
|
+
const budget = soft === undefined ? undefined : Math.max(0, soft - reserve);
|
|
52
53
|
const percent = window === undefined || window <= 0
|
|
53
54
|
? undefined
|
|
54
55
|
: Math.min(100, Math.round((estimate.tokens / window) * 1000) / 10);
|
|
@@ -4,13 +4,24 @@
|
|
|
4
4
|
* - `message_update` 变成纯增量:去掉累计消息与 `partial`,附最新 `usage`;`done / error`
|
|
5
5
|
* 保留最终 `message`。`tool_execution_end` 去掉 `result.fileChange`([ACP-C0] 改前 / 改后全文只给
|
|
6
6
|
* ACP 映射用,不上 RPC / stream-json 线)。其余 `SessionEvent` 原样。
|
|
7
|
+
* - [M-G] `compact`(RPC 客户端声明 `compact_events`,docs/memory-plan.md D9):`compactEvent()` 去掉
|
|
8
|
+
* 重复的大正文——`turn_end.toolResults[]` 每项只留 `{ toolCallId, toolName, isError, timestamp,
|
|
9
|
+
* contentOmitted }`;`message_start` 与 `entry_appended`(`message` 条目)里的 toolResult / 带图片的 user
|
|
10
|
+
* 消息 `content` 换成 `""` 并加 `contentOmitted: true`。`message_end` 与 `tool_execution_end` 保持全量。
|
|
11
|
+
* `stream-json` 不传 compact,输出不变。
|
|
7
12
|
* - `toJsonLine()`:一行 JSON;Error 序列化为 `{ name, message }`,bigint 转字符串,
|
|
8
13
|
* U+2028 / U+2029 转义(部分 JSONL 客户端会把它们当换行);图片 base64 不截断。
|
|
9
14
|
*/
|
|
10
15
|
import type { SessionEvent } from "../../agent/types.js";
|
|
11
16
|
import type { AssistantEvent } from "../../ai/types.js";
|
|
12
17
|
import type { RpcEvent, WireAssistantEvent } from "../../rpc.js";
|
|
18
|
+
export interface WireOptions {
|
|
19
|
+
/** [M-G] 客户端声明了 `compact_events`。 */
|
|
20
|
+
compact?: boolean;
|
|
21
|
+
}
|
|
13
22
|
export declare function toWireAssistantEvent(event: AssistantEvent): WireAssistantEvent;
|
|
14
|
-
|
|
23
|
+
/** [M-G] 精简事件:只改 turn_end / message_start / entry_appended,其余(含 message_end)原样。 */
|
|
24
|
+
export declare function compactEvent(event: SessionEvent): SessionEvent;
|
|
25
|
+
export declare function toWireEvent(input: SessionEvent, options?: WireOptions): RpcEvent;
|
|
15
26
|
/** 一行 JSON(不含换行符)。 */
|
|
16
27
|
export declare function toJsonLine(value: unknown): string;
|
|
@@ -4,6 +4,11 @@
|
|
|
4
4
|
* - `message_update` 变成纯增量:去掉累计消息与 `partial`,附最新 `usage`;`done / error`
|
|
5
5
|
* 保留最终 `message`。`tool_execution_end` 去掉 `result.fileChange`([ACP-C0] 改前 / 改后全文只给
|
|
6
6
|
* ACP 映射用,不上 RPC / stream-json 线)。其余 `SessionEvent` 原样。
|
|
7
|
+
* - [M-G] `compact`(RPC 客户端声明 `compact_events`,docs/memory-plan.md D9):`compactEvent()` 去掉
|
|
8
|
+
* 重复的大正文——`turn_end.toolResults[]` 每项只留 `{ toolCallId, toolName, isError, timestamp,
|
|
9
|
+
* contentOmitted }`;`message_start` 与 `entry_appended`(`message` 条目)里的 toolResult / 带图片的 user
|
|
10
|
+
* 消息 `content` 换成 `""` 并加 `contentOmitted: true`。`message_end` 与 `tool_execution_end` 保持全量。
|
|
11
|
+
* `stream-json` 不传 compact,输出不变。
|
|
7
12
|
* - `toJsonLine()`:一行 JSON;Error 序列化为 `{ name, message }`,bigint 转字符串,
|
|
8
13
|
* U+2028 / U+2029 转义(部分 JSONL 客户端会把它们当换行);图片 base64 不截断。
|
|
9
14
|
*/
|
|
@@ -13,7 +18,49 @@ export function toWireAssistantEvent(event) {
|
|
|
13
18
|
const { partial: _partial, ...rest } = event;
|
|
14
19
|
return rest;
|
|
15
20
|
}
|
|
16
|
-
|
|
21
|
+
/** toolResult 与带图片的 user 消息:正文换成 `""` 并标 `contentOmitted`;其它消息原样。 */
|
|
22
|
+
function omitContent(message) {
|
|
23
|
+
const omit = message.role === "toolResult" ||
|
|
24
|
+
(message.role === "user" &&
|
|
25
|
+
Array.isArray(message.content) &&
|
|
26
|
+
message.content.some((block) => block.type === "image"));
|
|
27
|
+
return omit ? { ...message, content: "", contentOmitted: true } : message;
|
|
28
|
+
}
|
|
29
|
+
/** [M-G] 精简事件:只改 turn_end / message_start / entry_appended,其余(含 message_end)原样。 */
|
|
30
|
+
export function compactEvent(event) {
|
|
31
|
+
switch (event.type) {
|
|
32
|
+
case "turn_end":
|
|
33
|
+
if (event.toolResults.length === 0)
|
|
34
|
+
return event;
|
|
35
|
+
return {
|
|
36
|
+
...event,
|
|
37
|
+
// 线上形状(docs/rpc.md)不是完整的 ToolResultMessage:正文与 details 都不带
|
|
38
|
+
toolResults: event.toolResults.map(({ toolCallId, toolName, isError, timestamp }) => ({
|
|
39
|
+
toolCallId,
|
|
40
|
+
toolName,
|
|
41
|
+
isError,
|
|
42
|
+
timestamp,
|
|
43
|
+
contentOmitted: true,
|
|
44
|
+
})),
|
|
45
|
+
};
|
|
46
|
+
case "message_start": {
|
|
47
|
+
const message = omitContent(event.message);
|
|
48
|
+
return message === event.message ? event : { ...event, message };
|
|
49
|
+
}
|
|
50
|
+
case "entry_appended": {
|
|
51
|
+
if (event.entry.type !== "message")
|
|
52
|
+
return event;
|
|
53
|
+
const message = omitContent(event.entry.message);
|
|
54
|
+
return message === event.entry.message
|
|
55
|
+
? event
|
|
56
|
+
: { ...event, entry: { ...event.entry, message } };
|
|
57
|
+
}
|
|
58
|
+
default:
|
|
59
|
+
return event;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
export function toWireEvent(input, options = {}) {
|
|
63
|
+
const event = options.compact === true ? compactEvent(input) : input;
|
|
17
64
|
if (event.type === "tool_execution_end" && event.result.fileChange !== undefined) {
|
|
18
65
|
const { fileChange: _fileChange, ...result } = event.result;
|
|
19
66
|
return { ...event, result };
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* - 审批:客户端 `set_client_capabilities{capabilities:["approvals"]}` 之后才挂 UI broker;
|
|
7
7
|
* 之前的 ask 无人作答 → deny。`permission_request` 带 `timeoutMs`,超时由会话 deny 并发
|
|
8
8
|
* `permission_resolved`。
|
|
9
|
+
* - [M-G] 声明 `compact_events` 之后的事件经 `toWireEvent(event, { compact: true })` 精简(json-event.ts)。
|
|
9
10
|
* - 宿主 `ui.notify` → `{type:"notification", level, message}` 一行(stderr 同时一份)。
|
|
10
11
|
* - stdin 关闭:撤下审批(之后的 ask 无人作答 → deny)、等在途命令与已开始的运行结束、
|
|
11
12
|
* 应答写完 → 退出 0(会话由 runCli dispose)。`printf '{…prompt…}' | ama --mode rpc` 因此能拿到
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* - 审批:客户端 `set_client_capabilities{capabilities:["approvals"]}` 之后才挂 UI broker;
|
|
7
7
|
* 之前的 ask 无人作答 → deny。`permission_request` 带 `timeoutMs`,超时由会话 deny 并发
|
|
8
8
|
* `permission_resolved`。
|
|
9
|
+
* - [M-G] 声明 `compact_events` 之后的事件经 `toWireEvent(event, { compact: true })` 精简(json-event.ts)。
|
|
9
10
|
* - 宿主 `ui.notify` → `{type:"notification", level, message}` 一行(stderr 同时一份)。
|
|
10
11
|
* - stdin 关闭:撤下审批(之后的 ask 无人作答 → deny)、等在途命令与已开始的运行结束、
|
|
11
12
|
* 应答写完 → 退出 0(会话由 runCli dispose)。`printf '{…prompt…}' | ama --mode rpc` 因此能拿到
|
|
@@ -24,7 +25,13 @@ import { cachedAgentInfos } from "../../agents/external.js";
|
|
|
24
25
|
import { RpcApprovals, handlers } from "./commands.js";
|
|
25
26
|
import { createLineReader, writeChunked } from "./jsonl.js";
|
|
26
27
|
import { msg } from "../../i18n/index.js";
|
|
27
|
-
export const RPC_CAPABILITIES = [
|
|
28
|
+
export const RPC_CAPABILITIES = [
|
|
29
|
+
"approvals",
|
|
30
|
+
"images",
|
|
31
|
+
"hooks",
|
|
32
|
+
"plans",
|
|
33
|
+
"compact_events",
|
|
34
|
+
];
|
|
28
35
|
export async function runRpcMode(runtime, context, options = {}) {
|
|
29
36
|
const stdin = options.stdin ?? process.stdin;
|
|
30
37
|
const stdout = options.stdout ?? process.stdout;
|
|
@@ -34,7 +41,7 @@ export async function runRpcMode(runtime, context, options = {}) {
|
|
|
34
41
|
writing = writing.then(() => writeChunked(stdout, line)).catch(() => undefined);
|
|
35
42
|
return writing;
|
|
36
43
|
};
|
|
37
|
-
const subscribe = (s) => s.subscribe((event) => void write(toWireEvent(event)));
|
|
44
|
+
const subscribe = (s) => s.subscribe((event) => void write(toWireEvent(event, { compact: ctx.capabilities.has("compact_events") })));
|
|
38
45
|
let session = currentSession(runtime);
|
|
39
46
|
let unsubscribe = subscribe(session);
|
|
40
47
|
const approvals = new RpcApprovals();
|
|
@@ -72,5 +72,5 @@ export declare function subscriptionRows(session: AgentSession, now?: number): K
|
|
|
72
72
|
export declare function renderRows(rows: readonly KeyValueRow[], indent?: string): string[];
|
|
73
73
|
export declare function describeCache(session: AgentSession, now?: number): string;
|
|
74
74
|
export declare function describeSession(session: AgentSession, now?: number): string;
|
|
75
|
-
/** `/cache fingerprint`:最近一次真实请求的前缀指纹(system / tools
|
|
75
|
+
/** `/cache fingerprint`:最近一次真实请求的前缀指纹(system / tools 哈希与模型;[ME-B] 再逐节列哈希)。 */
|
|
76
76
|
export declare function describeFingerprint(session: AgentSession): string;
|
|
@@ -80,11 +80,13 @@ export function missReasonText(miss) {
|
|
|
80
80
|
case "model_changed":
|
|
81
81
|
return m.missModel;
|
|
82
82
|
case "prefix_changed":
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
83
|
+
if (miss.detail === "tools")
|
|
84
|
+
return m.missTools;
|
|
85
|
+
if (miss.detail === "system")
|
|
86
|
+
return m.missSystem;
|
|
87
|
+
if (miss.detail?.startsWith("system:") === true)
|
|
88
|
+
return m.missSystemSections(miss.detail.slice("system:".length).replaceAll(",", ", "));
|
|
89
|
+
return m.missPrefix;
|
|
88
90
|
case "evicted":
|
|
89
91
|
return m.missEvicted;
|
|
90
92
|
}
|
|
@@ -325,7 +327,7 @@ export function describeSession(session, now = Date.now()) {
|
|
|
325
327
|
...(external.length > 0 ? [m.externalTitle, ...renderRows(external, " ")] : []),
|
|
326
328
|
].join("\n");
|
|
327
329
|
}
|
|
328
|
-
/** `/cache fingerprint`:最近一次真实请求的前缀指纹(system / tools
|
|
330
|
+
/** `/cache fingerprint`:最近一次真实请求的前缀指纹(system / tools 哈希与模型;[ME-B] 再逐节列哈希)。 */
|
|
329
331
|
export function describeFingerprint(session) {
|
|
330
332
|
const m = msg().report.session;
|
|
331
333
|
if (!(session instanceof AgentSessionImpl))
|
|
@@ -338,6 +340,10 @@ export function describeFingerprint(session) {
|
|
|
338
340
|
m.fingerprintTitle,
|
|
339
341
|
...renderRows([
|
|
340
342
|
{ key: "system", value: f.system },
|
|
343
|
+
...Object.entries(f.sections ?? {}).map(([name, hash]) => ({
|
|
344
|
+
key: ` ${name}`,
|
|
345
|
+
value: hash,
|
|
346
|
+
})),
|
|
341
347
|
{ key: "tools", value: f.tools },
|
|
342
348
|
{ key: "model", value: f.model },
|
|
343
349
|
], " "),
|
package/dist/rpc.d.ts
CHANGED
|
@@ -33,8 +33,11 @@ import type { TracePreview } from "./trace/preview.js";
|
|
|
33
33
|
import type { Trace, TraceSubagentNode } from "./trace/types.js";
|
|
34
34
|
export type { SessionEvent } from "./agent/types.js";
|
|
35
35
|
export declare const RPC_PROTOCOL_VERSION: 1;
|
|
36
|
-
/**
|
|
37
|
-
|
|
36
|
+
/**
|
|
37
|
+
* `plans`(W5-C0):客户端声明后计划审批交给客户端(`plan_proposed` → `plan_response`)。
|
|
38
|
+
* `compact_events`(M-G):客户端声明后 turn_end / message_start / entry_appended 不重复带工具结果与图片正文。
|
|
39
|
+
*/
|
|
40
|
+
export type RpcCapability = "approvals" | "images" | "hooks" | "plans" | "compact_events";
|
|
38
41
|
export interface RpcHello {
|
|
39
42
|
type: "hello";
|
|
40
43
|
protocolVersion: typeof RPC_PROTOCOL_VERSION;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 会话文件的字节级按行读取(docs/memory-plan.md D6、§2.4)。[M-C0] 实现与单测;调用方由 [M-C] 接入
|
|
3
|
+
* (`store.ts readSessionLines`、`scan.ts forEachLine`、`list.ts`)。
|
|
4
|
+
*
|
|
5
|
+
* - fd + 一块复用的缓冲(缺省 64 KiB)顺序 `readSync`,以 `indexOf(0x0a)` 切行;跨块的残片挪到缓冲
|
|
6
|
+
* 开头续读,一行比缓冲还长时缓冲翻倍——每行不再分配,不生成全文字符串,也不 split 出整个数组;
|
|
7
|
+
* - 回调拿到的 `line` 不含 `\n` 与行尾的一个 `\r`,**只在回调期间有效**(可能是复用块的视图),
|
|
8
|
+
* 需要保留时自行拷贝或解码;空行照样回调(是否跳过由调用方决定);
|
|
9
|
+
* - `index` 是物理行号(0 起,含空行);`byteOffset` 是该行首字节在文件中的偏移(修复半行时
|
|
10
|
+
* 直接 `truncateSync(file, byteOffset)`);`last` 为 true 表示这是**没有以 `\n` 结尾**的末行;
|
|
11
|
+
* 文件以 `\n` 结尾时不会多回调一个空的末行;
|
|
12
|
+
* - 回调返回 `false` 立即停止读盘并关闭 fd。
|
|
13
|
+
*/
|
|
14
|
+
/** 缺省块大小。 */
|
|
15
|
+
export declare const LINE_CHUNK_BYTES: number;
|
|
16
|
+
export interface LineVisit {
|
|
17
|
+
(line: Buffer, index: number, last: boolean, byteOffset: number): boolean | void;
|
|
18
|
+
}
|
|
19
|
+
/** 逐行访问 `file`(同步);打不开时抛 `fs` 的原始错误。 */
|
|
20
|
+
export declare function forEachLineSync(file: string, visit: LineVisit, chunkBytes?: number): void;
|
|
21
|
+
/**
|
|
22
|
+
* ama 写的行:不解析、只解码行首就取出 type(与消息的 role),口径同 `scan.ts lineType`;
|
|
23
|
+
* 不是这种形状(或行首不足以判断 role)时 undefined,调用方退回 `JSON.parse`。
|
|
24
|
+
*/
|
|
25
|
+
export declare function lineTypeOf(line: Buffer): {
|
|
26
|
+
type: string;
|
|
27
|
+
role?: string;
|
|
28
|
+
} | undefined;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 会话文件的字节级按行读取(docs/memory-plan.md D6、§2.4)。[M-C0] 实现与单测;调用方由 [M-C] 接入
|
|
3
|
+
* (`store.ts readSessionLines`、`scan.ts forEachLine`、`list.ts`)。
|
|
4
|
+
*
|
|
5
|
+
* - fd + 一块复用的缓冲(缺省 64 KiB)顺序 `readSync`,以 `indexOf(0x0a)` 切行;跨块的残片挪到缓冲
|
|
6
|
+
* 开头续读,一行比缓冲还长时缓冲翻倍——每行不再分配,不生成全文字符串,也不 split 出整个数组;
|
|
7
|
+
* - 回调拿到的 `line` 不含 `\n` 与行尾的一个 `\r`,**只在回调期间有效**(可能是复用块的视图),
|
|
8
|
+
* 需要保留时自行拷贝或解码;空行照样回调(是否跳过由调用方决定);
|
|
9
|
+
* - `index` 是物理行号(0 起,含空行);`byteOffset` 是该行首字节在文件中的偏移(修复半行时
|
|
10
|
+
* 直接 `truncateSync(file, byteOffset)`);`last` 为 true 表示这是**没有以 `\n` 结尾**的末行;
|
|
11
|
+
* 文件以 `\n` 结尾时不会多回调一个空的末行;
|
|
12
|
+
* - 回调返回 `false` 立即停止读盘并关闭 fd。
|
|
13
|
+
*/
|
|
14
|
+
import { closeSync, openSync, readSync } from "node:fs";
|
|
15
|
+
import { lineType } from "./scan.js";
|
|
16
|
+
/** 缺省块大小。 */
|
|
17
|
+
export const LINE_CHUNK_BYTES = 64 * 1024;
|
|
18
|
+
const LF = 0x0a;
|
|
19
|
+
const CR = 0x0d;
|
|
20
|
+
function stripCr(line) {
|
|
21
|
+
return line.length > 0 && line[line.length - 1] === CR ? line.subarray(0, -1) : line;
|
|
22
|
+
}
|
|
23
|
+
/** 逐行访问 `file`(同步);打不开时抛 `fs` 的原始错误。 */
|
|
24
|
+
export function forEachLineSync(file, visit, chunkBytes = LINE_CHUNK_BYTES) {
|
|
25
|
+
const fd = openSync(file, "r");
|
|
26
|
+
try {
|
|
27
|
+
// 一块复用的缓冲:未成行的残片挪到开头再续读;一行比缓冲还长时才翻倍(之后沿用)
|
|
28
|
+
let buf = Buffer.allocUnsafe(Math.max(1, Math.floor(chunkBytes)));
|
|
29
|
+
let start = 0; // 未成行数据在 buf 中的起点
|
|
30
|
+
let end = 0; // 有效数据的终点
|
|
31
|
+
let index = 0;
|
|
32
|
+
let lineStart = 0; // `start` 处对应的文件偏移
|
|
33
|
+
let position = 0;
|
|
34
|
+
for (;;) {
|
|
35
|
+
if (start > 0) {
|
|
36
|
+
buf.copyWithin(0, start, end);
|
|
37
|
+
end -= start;
|
|
38
|
+
start = 0;
|
|
39
|
+
}
|
|
40
|
+
if (end === buf.length) {
|
|
41
|
+
const grown = Buffer.allocUnsafe(buf.length * 2);
|
|
42
|
+
buf.copy(grown, 0, 0, end);
|
|
43
|
+
buf = grown;
|
|
44
|
+
}
|
|
45
|
+
const scanFrom = end; // 残片里已确认没有 \n
|
|
46
|
+
const read = readSync(fd, buf, end, buf.length - end, position);
|
|
47
|
+
if (read === 0)
|
|
48
|
+
break;
|
|
49
|
+
position += read;
|
|
50
|
+
end += read;
|
|
51
|
+
const view = buf.subarray(0, end);
|
|
52
|
+
for (let nl = view.indexOf(LF, scanFrom); nl >= 0; nl = view.indexOf(LF, start)) {
|
|
53
|
+
const line = view.subarray(start, nl);
|
|
54
|
+
const offset = lineStart;
|
|
55
|
+
lineStart += nl + 1 - start;
|
|
56
|
+
start = nl + 1;
|
|
57
|
+
if (visit(stripCr(line), index++, false, offset) === false)
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (end > start)
|
|
62
|
+
visit(stripCr(buf.subarray(start, end)), index, true, lineStart);
|
|
63
|
+
}
|
|
64
|
+
finally {
|
|
65
|
+
closeSync(fd);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/** 只解码这么多字节来判断行类型(`{"type":"message","message":{"role":"toolResult"` 约 48 字节)。 */
|
|
69
|
+
const TYPE_HEAD_BYTES = 96;
|
|
70
|
+
/**
|
|
71
|
+
* ama 写的行:不解析、只解码行首就取出 type(与消息的 role),口径同 `scan.ts lineType`;
|
|
72
|
+
* 不是这种形状(或行首不足以判断 role)时 undefined,调用方退回 `JSON.parse`。
|
|
73
|
+
*/
|
|
74
|
+
export function lineTypeOf(line) {
|
|
75
|
+
if (line.length <= TYPE_HEAD_BYTES)
|
|
76
|
+
return lineType(line.toString("utf8"));
|
|
77
|
+
const found = lineType(line.subarray(0, TYPE_HEAD_BYTES).toString("utf8"));
|
|
78
|
+
// role 可能被截断:宁可让调用方完整解析,也不给出与 `lineType` 不同的答案
|
|
79
|
+
if (found?.type === "message" && found.role === undefined)
|
|
80
|
+
return undefined;
|
|
81
|
+
return found;
|
|
82
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 会话列表(`SessionManager.list` 的实现)。[M-C] 按块逐行读(docs/memory-plan.md D6、§2.4):
|
|
3
|
+
* 只解析头、第一条条目、`leaf` / `session_info` 行、要取 `firstPrompt` 的那一条 user 消息与末行;
|
|
4
|
+
* 其余 ama 写的行只看行首的 type / role(`lineTypeOf`),不生成全文字符串、不留条目对象。
|
|
5
|
+
*
|
|
6
|
+
* 口径与「`migrateSessionLines(readSessionLines(file).lines)` 再统计」一致:空行跳过、末尾半行忽略、
|
|
7
|
+
* 头必须是 `type: "session"` / `version === SESSION_FORMAT_VERSION` / id 与 cwd 为字符串、`leaf` 行不算
|
|
8
|
+
* 条目、重复的头与解析失败的中间行让整个文件跳过。差别只有一处:不解析的 ama 行不再逐条检查
|
|
9
|
+
* `id`(ama 写入的条目总有 id);行尾不是 `}` 的(写到一半又被追加的坏行)仍会退回完整解析。
|
|
10
|
+
* 不经 `migrateSessionLines`——格式升到 v2 时要同时改这里(R6,见 migrate.ts)。
|
|
11
|
+
*/
|
|
12
|
+
import { type SessionListItem } from "./types.js";
|
|
13
|
+
/** 只读列出目录下的会话(最新在前);损坏的文件跳过。 */
|
|
14
|
+
export declare function listSessionItems(dir: string): SessionListItem[];
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 会话列表(`SessionManager.list` 的实现)。[M-C] 按块逐行读(docs/memory-plan.md D6、§2.4):
|
|
3
|
+
* 只解析头、第一条条目、`leaf` / `session_info` 行、要取 `firstPrompt` 的那一条 user 消息与末行;
|
|
4
|
+
* 其余 ama 写的行只看行首的 type / role(`lineTypeOf`),不生成全文字符串、不留条目对象。
|
|
5
|
+
*
|
|
6
|
+
* 口径与「`migrateSessionLines(readSessionLines(file).lines)` 再统计」一致:空行跳过、末尾半行忽略、
|
|
7
|
+
* 头必须是 `type: "session"` / `version === SESSION_FORMAT_VERSION` / id 与 cwd 为字符串、`leaf` 行不算
|
|
8
|
+
* 条目、重复的头与解析失败的中间行让整个文件跳过。差别只有一处:不解析的 ama 行不再逐条检查
|
|
9
|
+
* `id`(ama 写入的条目总有 id);行尾不是 `}` 的(写到一半又被追加的坏行)仍会退回完整解析。
|
|
10
|
+
* 不经 `migrateSessionLines`——格式升到 v2 时要同时改这里(R6,见 migrate.ts)。
|
|
11
|
+
*/
|
|
12
|
+
import { statSync } from "node:fs";
|
|
13
|
+
import { forEachLineSync, lineTypeOf } from "./line-reader.js";
|
|
14
|
+
import { isBlankLine, isSubagentSession, listSessionFiles } from "./store.js";
|
|
15
|
+
import { SESSION_FORMAT_VERSION } from "./types.js";
|
|
16
|
+
class SkipFile extends Error {
|
|
17
|
+
}
|
|
18
|
+
function isObject(value) {
|
|
19
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
20
|
+
}
|
|
21
|
+
function endsWithBrace(line) {
|
|
22
|
+
for (let i = line.length - 1; i >= 0; i--) {
|
|
23
|
+
const byte = line[i];
|
|
24
|
+
if (byte === 0x20 || (byte >= 0x09 && byte <= 0x0d))
|
|
25
|
+
continue;
|
|
26
|
+
return byte === 0x7d;
|
|
27
|
+
}
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
function promptText(content) {
|
|
31
|
+
if (typeof content === "string")
|
|
32
|
+
return content;
|
|
33
|
+
return content
|
|
34
|
+
.map((block) => (block.type === "text" ? block.text : ""))
|
|
35
|
+
.join("");
|
|
36
|
+
}
|
|
37
|
+
/** 统计一条已解析的行(头之后);不合 v1 规则时抛 SkipFile。 */
|
|
38
|
+
function tallyParsed(tally, line) {
|
|
39
|
+
if (isObject(line) && line["type"] === "leaf") {
|
|
40
|
+
const id = line["id"];
|
|
41
|
+
if (id !== null && typeof id !== "string")
|
|
42
|
+
throw new SkipFile();
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
if (!isObject(line) || typeof line["id"] !== "string" || typeof line["type"] !== "string") {
|
|
46
|
+
throw new SkipFile();
|
|
47
|
+
}
|
|
48
|
+
if (line["type"] === "session")
|
|
49
|
+
throw new SkipFile();
|
|
50
|
+
if (!tally.hasFirst) {
|
|
51
|
+
tally.hasFirst = true;
|
|
52
|
+
tally.first = line;
|
|
53
|
+
}
|
|
54
|
+
if (line["type"] === "session_info" && line["name"] !== undefined) {
|
|
55
|
+
tally.name = line["name"];
|
|
56
|
+
}
|
|
57
|
+
if (line["type"] !== "message")
|
|
58
|
+
return;
|
|
59
|
+
const message = line["message"];
|
|
60
|
+
if (message.role === "system")
|
|
61
|
+
return;
|
|
62
|
+
tally.messageCount++;
|
|
63
|
+
if (tally.firstPrompt === undefined && message.role === "user") {
|
|
64
|
+
tally.firstPrompt = promptText(message.content);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function parseHeader(line) {
|
|
68
|
+
if (!isObject(line) || line["type"] !== "session")
|
|
69
|
+
throw new SkipFile();
|
|
70
|
+
if (line["version"] !== SESSION_FORMAT_VERSION)
|
|
71
|
+
throw new SkipFile();
|
|
72
|
+
if (typeof line["id"] !== "string" || typeof line["cwd"] !== "string")
|
|
73
|
+
throw new SkipFile();
|
|
74
|
+
return line;
|
|
75
|
+
}
|
|
76
|
+
/** 不解析就能统计的 ama 行:已过第一条条目、类型确定、行尾完整,且不需要读出内容。 */
|
|
77
|
+
function tallyByType(tally, line, firstPromptTaken) {
|
|
78
|
+
if (!tally.hasFirst || !endsWithBrace(line))
|
|
79
|
+
return false;
|
|
80
|
+
const kind = lineTypeOf(line);
|
|
81
|
+
if (kind === undefined)
|
|
82
|
+
return false;
|
|
83
|
+
if (kind.type === "leaf" || kind.type === "session_info" || kind.type === "session")
|
|
84
|
+
return false;
|
|
85
|
+
if (kind.type !== "message")
|
|
86
|
+
return true;
|
|
87
|
+
if (kind.role === undefined)
|
|
88
|
+
return false;
|
|
89
|
+
if (kind.role === "system")
|
|
90
|
+
return true;
|
|
91
|
+
if (kind.role === "user" && !firstPromptTaken)
|
|
92
|
+
return false;
|
|
93
|
+
tally.messageCount++;
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
function tallyFile(file) {
|
|
97
|
+
const tally = { hasFirst: false, messageCount: 0 };
|
|
98
|
+
forEachLineSync(file, (buf, _index, last) => {
|
|
99
|
+
if (isBlankLine(buf))
|
|
100
|
+
return;
|
|
101
|
+
if (tally.header !== undefined && !last) {
|
|
102
|
+
if (tallyByType(tally, buf, tally.firstPrompt !== undefined))
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
let parsed;
|
|
106
|
+
try {
|
|
107
|
+
parsed = JSON.parse(buf.toString("utf8"));
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
if (last)
|
|
111
|
+
return false; // 末尾半行:与 readSessionLines 一样忽略
|
|
112
|
+
throw new SkipFile();
|
|
113
|
+
}
|
|
114
|
+
if (tally.header === undefined)
|
|
115
|
+
tally.header = parseHeader(parsed);
|
|
116
|
+
else
|
|
117
|
+
tallyParsed(tally, parsed);
|
|
118
|
+
});
|
|
119
|
+
return tally;
|
|
120
|
+
}
|
|
121
|
+
/** 只读列出目录下的会话(最新在前);损坏的文件跳过。 */
|
|
122
|
+
export function listSessionItems(dir) {
|
|
123
|
+
const items = [];
|
|
124
|
+
for (const file of listSessionFiles(dir)) {
|
|
125
|
+
try {
|
|
126
|
+
const { header, first, name, firstPrompt, messageCount } = tallyFile(file);
|
|
127
|
+
if (header === undefined)
|
|
128
|
+
continue;
|
|
129
|
+
const item = {
|
|
130
|
+
id: header.id,
|
|
131
|
+
file,
|
|
132
|
+
cwd: header.cwd,
|
|
133
|
+
createdAt: header.timestamp,
|
|
134
|
+
modifiedAt: statSync(file).mtime.toISOString(),
|
|
135
|
+
messageCount,
|
|
136
|
+
};
|
|
137
|
+
if (name !== undefined)
|
|
138
|
+
item.name = name;
|
|
139
|
+
if (isSubagentSession(header, first))
|
|
140
|
+
item.subagent = true;
|
|
141
|
+
if (firstPrompt !== undefined)
|
|
142
|
+
item.firstPrompt = firstPrompt.slice(0, 200);
|
|
143
|
+
items.push(item);
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
// 损坏 / 不可读:列表里跳过(sessions show 会给出具体错误)
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return items;
|
|
150
|
+
}
|
|
@@ -7,8 +7,9 @@
|
|
|
7
7
|
* - `open(file)`:读文件(修复末尾半行)、校验版本、加锁;叶子 = 文件中最后一条条目,
|
|
8
8
|
* 若其后还有 `leaf` 行(`/tree` 换叶子落盘,第三波 A7)则取最后一条 leaf 行。
|
|
9
9
|
* - `setLeaf(id)`:已落盘时追加一行 `leaf{id, timestamp}`;延迟会话在 `flush()` 时补写。
|
|
10
|
-
* - `fork(entryId)`:复制 root → entryId 的分支到新文件(头的 parentSession 指回本文件),
|
|
11
|
-
* 不复制 leaf
|
|
10
|
+
* - `fork(entryId, { head })`:复制 root → entryId 的分支到新文件(头的 parentSession 指回本文件),
|
|
11
|
+
* 不复制 leaf 行;给了 `head` 时它成为新根条目,复制的首条重挂到它下面(fork 子会话的 ama.task);
|
|
12
|
+
* `cwd` 缺省同本会话([ME-A] 隔离的 fork 子会话传 worktree)。
|
|
12
13
|
* - `close()`:释放锁;之后的 append 抛错。
|
|
13
14
|
*/
|
|
14
15
|
import type { SessionEntry, SessionEntryInput, SessionHeader, SessionListItem, SessionManagerApi, SessionTreeNode } from "./types.js";
|
|
@@ -59,7 +60,10 @@ export declare class SessionManager implements SessionManagerApi {
|
|
|
59
60
|
getTree(): SessionTreeNode[];
|
|
60
61
|
name(): string | undefined;
|
|
61
62
|
setName(name: string): void;
|
|
62
|
-
fork(entryId: string
|
|
63
|
+
fork(entryId: string, forkOptions?: {
|
|
64
|
+
head?: SessionEntryInput;
|
|
65
|
+
cwd?: string;
|
|
66
|
+
}): SessionManager;
|
|
63
67
|
/** 延迟会话:建文件、写头与缓存条目、加锁。返回文件路径(内存会话为 undefined)。幂等。 */
|
|
64
68
|
flush(): string | undefined;
|
|
65
69
|
isPersisted(): boolean;
|