@armadra/agent 0.7.3 → 0.7.5
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 +48 -0
- package/CHANGELOG.zh-CN.md +48 -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 +15 -4
- package/dist/ai/apis/cache-params.js +2 -1
- package/dist/ai/apis/google-generative-ai.js +4 -2
- package/dist/ai/apis/google-request.js +2 -1
- 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 +4 -2
- package/dist/ai/apis/openai-responses-request.js +3 -2
- 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 +63 -0
- package/dist/ai/json-body.js +286 -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 +3637 -1973
- package/dist/cli/bootstrap.js +13 -5
- 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/deps.d.ts +1 -1
- 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 +30 -18
- package/dist/modes/acp/acp-sessions.d.ts +22 -0
- package/dist/modes/acp/acp-sessions.js +68 -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 +23 -3
- package/dist/session/manager.js +91 -62
- package/dist/session/migrate.d.ts +2 -1
- package/dist/session/migrate.js +2 -1
- package/dist/session/offload.d.ts +51 -0
- package/dist/session/offload.js +195 -0
- 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 +15 -4
- package/dist/session/store.js +54 -38
- 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 +71 -0
- package/dist/tools/read-lines.js +224 -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 +8 -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 +13 -2
- package/docs/sessions.md +3 -1
- package/docs/tui.md +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 会话图片卸载(#170):被 `context_edit` 改写的旧消息不再进上下文,内存里只留它在会话文件中的行位置,
|
|
3
|
+
* 图片 base64 需要时再从文件回读。JSONL 格式不变。
|
|
4
|
+
*
|
|
5
|
+
* - 对象:活动分支上、被活动分支上的 `context_edit`(任何 reason)改写的 `message` 条目里的 `image` 块;
|
|
6
|
+
* 不在活动分支上的条目一律保持原文(rewind 回到编辑点之前、`/tree` 换分支时回读);
|
|
7
|
+
* - 打开文件时先扫一遍 `context_edit` 行(只解码行首判断类型)收集目标,读条目时目标里的图片解析完
|
|
8
|
+
* 随即剥掉,峰值不再含全部旧图;随后按活动分支对齐(不在分支上的编辑目标回读);
|
|
9
|
+
* - 卸载**不改共享的 ImageBlock**(驻留 block 被其它条目 / 会话共享):`entry.message` 换成浅拷贝,
|
|
10
|
+
* `content` 换新数组,image 块换成 `data: ""` 的占位;
|
|
11
|
+
* - 回读:按行位置 `readSync` 一行、`JSON.parse`,校验 id 与内容块数,图片经 `internImage` 驻留后
|
|
12
|
+
* **就地**放回占位所在的数组(之前拿到同一消息对象的调用方随之看到原图);读不回(文件被外部改写)时
|
|
13
|
+
* 保留 `""` 并告警,不再重试;
|
|
14
|
+
* - `original(entry)`:给 `getEntries` / `fork` 的原文副本,不改内存状态。
|
|
15
|
+
*/
|
|
16
|
+
import { closeSync, openSync, readSync } from "node:fs";
|
|
17
|
+
import { internImage } from "../ai/image-intern.js";
|
|
18
|
+
import { forEachLineSync, lineTypeOf } from "./line-reader.js";
|
|
19
|
+
import { collectContextEdits } from "./projection.js";
|
|
20
|
+
function isImage(block) {
|
|
21
|
+
return (typeof block === "object" &&
|
|
22
|
+
block !== null &&
|
|
23
|
+
block.type === "image" &&
|
|
24
|
+
typeof block.data === "string");
|
|
25
|
+
}
|
|
26
|
+
function contentOf(entry) {
|
|
27
|
+
if (entry.type !== "message")
|
|
28
|
+
return undefined;
|
|
29
|
+
const content = entry.message.content;
|
|
30
|
+
return Array.isArray(content) ? content : undefined;
|
|
31
|
+
}
|
|
32
|
+
/** `message` 条目的内容里有图片块。 */
|
|
33
|
+
export function hasImages(entry) {
|
|
34
|
+
return contentOf(entry)?.some(isImage) === true;
|
|
35
|
+
}
|
|
36
|
+
/** 读文件里 `locator` 处的一行并解析。 */
|
|
37
|
+
export function readLineAt(file, locator) {
|
|
38
|
+
const fd = openSync(file, "r");
|
|
39
|
+
try {
|
|
40
|
+
const buf = Buffer.allocUnsafe(locator.length);
|
|
41
|
+
let read = 0;
|
|
42
|
+
while (read < locator.length) {
|
|
43
|
+
const n = readSync(fd, buf, read, locator.length - read, locator.offset + read);
|
|
44
|
+
if (n === 0)
|
|
45
|
+
throw new Error("unexpected end of file");
|
|
46
|
+
read += n;
|
|
47
|
+
}
|
|
48
|
+
return JSON.parse(buf.toString("utf8"));
|
|
49
|
+
}
|
|
50
|
+
finally {
|
|
51
|
+
closeSync(fd);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/** 文件里全部 `context_edit` 的目标 id(只解析 context_edit 行;读不了时为空集)。 */
|
|
55
|
+
export function editTargets(file) {
|
|
56
|
+
const targets = new Set();
|
|
57
|
+
try {
|
|
58
|
+
forEachLineSync(file, (line) => {
|
|
59
|
+
if (lineTypeOf(line)?.type !== "context_edit")
|
|
60
|
+
return;
|
|
61
|
+
try {
|
|
62
|
+
const id = JSON.parse(line.toString("utf8")).targetId;
|
|
63
|
+
if (typeof id === "string")
|
|
64
|
+
targets.add(id);
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
// 末尾半行:随后的 readSessionLines 修复
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
// 读不了:由随后的 readSessionLines 报错
|
|
73
|
+
}
|
|
74
|
+
return targets;
|
|
75
|
+
}
|
|
76
|
+
const placeholder = (block) => ({ ...block, data: "" });
|
|
77
|
+
export class ImageOffload {
|
|
78
|
+
warn;
|
|
79
|
+
/** 含图 message 条目的行位置(只有已落盘的条目才有)。 */
|
|
80
|
+
locators = new Map();
|
|
81
|
+
offloaded = new Set();
|
|
82
|
+
constructor(warn = () => { }) {
|
|
83
|
+
this.warn = warn;
|
|
84
|
+
}
|
|
85
|
+
/** 记下条目的行位置;不含图片的条目忽略。 */
|
|
86
|
+
track(entry, locator) {
|
|
87
|
+
if (hasImages(entry))
|
|
88
|
+
this.locators.set(entry.id, locator);
|
|
89
|
+
}
|
|
90
|
+
/** 读文件时逐行调用:记下行位置;`strip` 时就地换成占位(刚解析的对象,别处没有引用)。 */
|
|
91
|
+
load(line, locator, strip) {
|
|
92
|
+
if (typeof line.id !== "string" || !hasImages(line))
|
|
93
|
+
return;
|
|
94
|
+
this.locators.set(line.id, locator);
|
|
95
|
+
if (!strip)
|
|
96
|
+
return;
|
|
97
|
+
const content = contentOf(line);
|
|
98
|
+
content.forEach((block, i) => {
|
|
99
|
+
if (isImage(block))
|
|
100
|
+
content[i] = placeholder(block);
|
|
101
|
+
});
|
|
102
|
+
this.offloaded.add(line.id);
|
|
103
|
+
}
|
|
104
|
+
isOffloaded(id) {
|
|
105
|
+
return this.offloaded.has(id);
|
|
106
|
+
}
|
|
107
|
+
/** 已卸载的条目数(测试与诊断用)。 */
|
|
108
|
+
get size() {
|
|
109
|
+
return this.offloaded.size;
|
|
110
|
+
}
|
|
111
|
+
/** 把条目里的图片换成占位;没有行位置(未落盘、不含图)或已卸载时返回 false。 */
|
|
112
|
+
offload(entry) {
|
|
113
|
+
if (entry.type !== "message" || this.offloaded.has(entry.id))
|
|
114
|
+
return false;
|
|
115
|
+
const content = contentOf(entry);
|
|
116
|
+
if (content === undefined || !this.locators.has(entry.id))
|
|
117
|
+
return false;
|
|
118
|
+
entry.message = {
|
|
119
|
+
...entry.message,
|
|
120
|
+
content: content.map((block) => (isImage(block) ? placeholder(block) : block)),
|
|
121
|
+
};
|
|
122
|
+
this.offloaded.add(entry.id);
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
/** 就地放回图片;读不回时返回 false(占位保留,之后不再卸载 / 回读这一条)。 */
|
|
126
|
+
hydrate(file, entry) {
|
|
127
|
+
if (!this.offloaded.has(entry.id))
|
|
128
|
+
return true;
|
|
129
|
+
this.offloaded.delete(entry.id);
|
|
130
|
+
const source = this.read(file, entry);
|
|
131
|
+
const content = contentOf(entry);
|
|
132
|
+
if (source === undefined || content === undefined)
|
|
133
|
+
return false;
|
|
134
|
+
const originals = contentOf(source) ?? [];
|
|
135
|
+
content.forEach((block, i) => {
|
|
136
|
+
const original = originals[i];
|
|
137
|
+
if (isImage(block) && block.data === "" && isImage(original)) {
|
|
138
|
+
content[i] = internImage(original);
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
143
|
+
/** 原文:已卸载的条目从文件读一份新副本(图片驻留),否则原样返回。读不回时返回占位版本。 */
|
|
144
|
+
original(file, entry) {
|
|
145
|
+
if (!this.offloaded.has(entry.id))
|
|
146
|
+
return entry;
|
|
147
|
+
const copy = this.read(file, entry);
|
|
148
|
+
if (copy === undefined)
|
|
149
|
+
return entry;
|
|
150
|
+
const content = contentOf(copy) ?? [];
|
|
151
|
+
content.forEach((block, i) => {
|
|
152
|
+
if (isImage(block))
|
|
153
|
+
content[i] = internImage(block);
|
|
154
|
+
});
|
|
155
|
+
return copy;
|
|
156
|
+
}
|
|
157
|
+
/** 按活动分支对齐:分支上被生效编辑改写的含图条目卸载,其余已卸载的回读。 */
|
|
158
|
+
sync(file, branch, index) {
|
|
159
|
+
const edits = collectContextEdits(branch);
|
|
160
|
+
const onBranch = new Set();
|
|
161
|
+
for (const entry of branch)
|
|
162
|
+
if (edits.has(entry.id))
|
|
163
|
+
onBranch.add(entry.id);
|
|
164
|
+
for (const id of [...this.offloaded]) {
|
|
165
|
+
const entry = index.get(id);
|
|
166
|
+
if (entry !== undefined && !onBranch.has(id))
|
|
167
|
+
this.hydrate(file, entry);
|
|
168
|
+
}
|
|
169
|
+
for (const id of onBranch) {
|
|
170
|
+
const entry = index.get(id);
|
|
171
|
+
if (entry !== undefined)
|
|
172
|
+
this.offload(entry);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
/** 从文件读回条目(新对象);不成功时告警、忘掉行位置并返回 undefined。 */
|
|
176
|
+
read(file, entry) {
|
|
177
|
+
const locator = this.locators.get(entry.id);
|
|
178
|
+
try {
|
|
179
|
+
if (locator === undefined)
|
|
180
|
+
throw new Error("no line locator");
|
|
181
|
+
const line = readLineAt(file, locator);
|
|
182
|
+
const content = line.id === entry.id ? contentOf(line) : undefined;
|
|
183
|
+
if (content === undefined || content.length !== contentOf(entry)?.length) {
|
|
184
|
+
throw new Error("line does not match the entry");
|
|
185
|
+
}
|
|
186
|
+
return line;
|
|
187
|
+
}
|
|
188
|
+
catch (error) {
|
|
189
|
+
this.locators.delete(entry.id);
|
|
190
|
+
this.offloaded.delete(entry.id);
|
|
191
|
+
this.warn(`cannot read session entry ${entry.id} back from ${file}: ${String(error)}`);
|
|
192
|
+
return undefined;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
@@ -5,8 +5,11 @@
|
|
|
5
5
|
* `firstKeptEntryId` 到 compaction 之间的非 system 条目,再放 compaction 之后的条目。
|
|
6
6
|
* - `buildProjection(branch)`:对上一步的条目套用 `context_edit`(同一目标在活动分支上最新一条赢;
|
|
7
7
|
* `null` 剔除,字符串只换内容、保留角色与元数据),并把每个条目映射为 `AgentMessage`。
|
|
8
|
-
* 有 compaction
|
|
9
|
-
*
|
|
8
|
+
* 有 compaction 时([ME-B] D4「开头只写一次」):检查点只重放**对话开始前**(首条非 system
|
|
9
|
+
* 条目之前)的 system 消息,与压缩前请求的开头逐字节相同;此后到 compaction 之间的补丁合并成一条
|
|
10
|
+
* 合成补丁放在摘要之后,由 normalizeContext 渲染为尾部提醒。合成补丁的 `toolsAdded` 按首次出现
|
|
11
|
+
* 的顺序、用首次发送的声明,工具表与压缩前一致;多次压缩递归成立(都从原始条目重放)。
|
|
12
|
+
* 会话条目里不另存检查点与合成补丁,重放即可得到。
|
|
10
13
|
* - `buildContext(branch)`:投影消息 + 路径上最近的模型 / 思考级别选择。
|
|
11
14
|
*/
|
|
12
15
|
import type { Message, ModelRef, ModelThinkingLevel, SystemMessage, ToolDecl } from "../ai/types.js";
|
|
@@ -42,9 +45,19 @@ export interface SystemState {
|
|
|
42
45
|
sections: Record<string, string>;
|
|
43
46
|
tools: ToolDecl[];
|
|
44
47
|
}
|
|
45
|
-
/**
|
|
48
|
+
/**
|
|
49
|
+
* 依次应用转录里的 system 消息(首条全量,之后是补丁);没有 system 消息 → undefined。
|
|
50
|
+
* 工具「保留原位、只换内容」(与 normalizeContext 一致);同一条里先加后删(`diffSystem` 的补丁两者
|
|
51
|
+
* 不相交,只有压缩的合成补丁会对同名既加又删:声明过、现已移除)。
|
|
52
|
+
*/
|
|
46
53
|
export declare function replaySystem(messages: Iterable<AgentMessage | Message>): SystemState | undefined;
|
|
47
54
|
/** 把重放结果写成一条完整的 system 消息。 */
|
|
48
55
|
export declare function systemCheckpoint(state: SystemState, timestamp: number): SystemMessage;
|
|
56
|
+
/**
|
|
57
|
+
* 压缩的合成补丁:`head`(对话开始前的状态)→ 之后全部补丁重放的结果。节按差异给;工具先列对话中
|
|
58
|
+
* 首次出现的新名字(首次发送的声明,normalizeContext 冻结的就是它),再列内容有更新的(只更新重放
|
|
59
|
+
* 状态,不改请求工具表);移除 = 声明过而现在不在。无差异 → undefined。
|
|
60
|
+
*/
|
|
61
|
+
export declare function synthesizeSystemPatch(head: SystemState, later: readonly SystemMessage[], timestamp: number): SystemMessage | undefined;
|
|
49
62
|
export declare function buildProjection(branch: readonly SessionEntry[]): Projection;
|
|
50
63
|
export declare function buildContext(branch: readonly SessionEntry[]): ProjectedContext;
|
|
@@ -5,8 +5,11 @@
|
|
|
5
5
|
* `firstKeptEntryId` 到 compaction 之间的非 system 条目,再放 compaction 之后的条目。
|
|
6
6
|
* - `buildProjection(branch)`:对上一步的条目套用 `context_edit`(同一目标在活动分支上最新一条赢;
|
|
7
7
|
* `null` 剔除,字符串只换内容、保留角色与元数据),并把每个条目映射为 `AgentMessage`。
|
|
8
|
-
* 有 compaction
|
|
9
|
-
*
|
|
8
|
+
* 有 compaction 时([ME-B] D4「开头只写一次」):检查点只重放**对话开始前**(首条非 system
|
|
9
|
+
* 条目之前)的 system 消息,与压缩前请求的开头逐字节相同;此后到 compaction 之间的补丁合并成一条
|
|
10
|
+
* 合成补丁放在摘要之后,由 normalizeContext 渲染为尾部提醒。合成补丁的 `toolsAdded` 按首次出现
|
|
11
|
+
* 的顺序、用首次发送的声明,工具表与压缩前一致;多次压缩递归成立(都从原始条目重放)。
|
|
12
|
+
* 会话条目里不另存检查点与合成补丁,重放即可得到。
|
|
10
13
|
* - `buildContext(branch)`:投影消息 + 路径上最近的模型 / 思考级别选择。
|
|
11
14
|
*/
|
|
12
15
|
function parseTime(iso) {
|
|
@@ -98,7 +101,11 @@ export function applyReplacement(message, text) {
|
|
|
98
101
|
return message;
|
|
99
102
|
}
|
|
100
103
|
}
|
|
101
|
-
/**
|
|
104
|
+
/**
|
|
105
|
+
* 依次应用转录里的 system 消息(首条全量,之后是补丁);没有 system 消息 → undefined。
|
|
106
|
+
* 工具「保留原位、只换内容」(与 normalizeContext 一致);同一条里先加后删(`diffSystem` 的补丁两者
|
|
107
|
+
* 不相交,只有压缩的合成补丁会对同名既加又删:声明过、现已移除)。
|
|
108
|
+
*/
|
|
102
109
|
export function replaySystem(messages) {
|
|
103
110
|
let found = false;
|
|
104
111
|
const sections = new Map();
|
|
@@ -113,12 +120,10 @@ export function replaySystem(messages) {
|
|
|
113
120
|
else
|
|
114
121
|
sections.set(name, text);
|
|
115
122
|
}
|
|
123
|
+
for (const tool of message.toolsAdded ?? [])
|
|
124
|
+
tools.set(tool.name, tool);
|
|
116
125
|
for (const name of message.toolsRemoved ?? [])
|
|
117
126
|
tools.delete(name);
|
|
118
|
-
for (const tool of message.toolsAdded ?? []) {
|
|
119
|
-
tools.delete(tool.name);
|
|
120
|
-
tools.set(tool.name, tool);
|
|
121
|
-
}
|
|
122
127
|
}
|
|
123
128
|
if (!found)
|
|
124
129
|
return undefined;
|
|
@@ -131,39 +136,101 @@ export function systemCheckpoint(state, timestamp) {
|
|
|
131
136
|
message.toolsAdded = state.tools.map((tool) => ({ ...tool }));
|
|
132
137
|
return message;
|
|
133
138
|
}
|
|
139
|
+
function sameDecl(a, b) {
|
|
140
|
+
return (a.description === b.description && JSON.stringify(a.parameters) === JSON.stringify(b.parameters));
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* 压缩的合成补丁:`head`(对话开始前的状态)→ 之后全部补丁重放的结果。节按差异给;工具先列对话中
|
|
144
|
+
* 首次出现的新名字(首次发送的声明,normalizeContext 冻结的就是它),再列内容有更新的(只更新重放
|
|
145
|
+
* 状态,不改请求工具表);移除 = 声明过而现在不在。无差异 → undefined。
|
|
146
|
+
*/
|
|
147
|
+
export function synthesizeSystemPatch(head, later, timestamp) {
|
|
148
|
+
const full = replaySystem([systemCheckpoint(head, timestamp), ...later]) ?? head;
|
|
149
|
+
const sections = {};
|
|
150
|
+
for (const [name, text] of Object.entries(full.sections))
|
|
151
|
+
if (head.sections[name] !== text)
|
|
152
|
+
sections[name] = text;
|
|
153
|
+
for (const name of Object.keys(head.sections))
|
|
154
|
+
if (!(name in full.sections))
|
|
155
|
+
sections[name] = null;
|
|
156
|
+
const sent = new Map(head.tools.map((tool) => [tool.name, tool]));
|
|
157
|
+
const added = [];
|
|
158
|
+
for (const message of later)
|
|
159
|
+
for (const tool of message.toolsAdded ?? []) {
|
|
160
|
+
if (sent.has(tool.name))
|
|
161
|
+
continue;
|
|
162
|
+
sent.set(tool.name, tool);
|
|
163
|
+
added.push(tool);
|
|
164
|
+
}
|
|
165
|
+
const now = new Map(full.tools.map((tool) => [tool.name, tool]));
|
|
166
|
+
for (const [name, tool] of sent) {
|
|
167
|
+
const current = now.get(name);
|
|
168
|
+
if (current !== undefined && !sameDecl(current, tool))
|
|
169
|
+
added.push(current);
|
|
170
|
+
}
|
|
171
|
+
const removed = [...sent.keys()].filter((name) => !now.has(name));
|
|
172
|
+
if (Object.keys(sections).length === 0 && added.length === 0 && removed.length === 0)
|
|
173
|
+
return undefined;
|
|
174
|
+
const patch = { role: "system", sections, timestamp };
|
|
175
|
+
if (added.length > 0)
|
|
176
|
+
patch.toolsAdded = added.map((tool) => ({ ...tool }));
|
|
177
|
+
if (removed.length > 0)
|
|
178
|
+
patch.toolsRemoved = removed;
|
|
179
|
+
return patch;
|
|
180
|
+
}
|
|
134
181
|
// ---------------------------------------------------------------------------
|
|
135
182
|
// 投影
|
|
136
183
|
// ---------------------------------------------------------------------------
|
|
184
|
+
/** 对话开始:首条进上下文且不是 system 的条目(custom_message 算;custom / usage 等不算)。 */
|
|
185
|
+
function startsConversation(entry) {
|
|
186
|
+
const message = entryToMessage(entry);
|
|
187
|
+
return message !== undefined && message.role !== "system";
|
|
188
|
+
}
|
|
189
|
+
function systemMessagesOf(entries) {
|
|
190
|
+
const out = [];
|
|
191
|
+
for (const entry of entries)
|
|
192
|
+
if (entry.type === "message" && entry.message.role === "system")
|
|
193
|
+
out.push(entry.message);
|
|
194
|
+
return out;
|
|
195
|
+
}
|
|
196
|
+
/** 压缩之前的 system 状态:开头检查点(对话开始前)+ 合成补丁(之后的全部补丁)。 */
|
|
197
|
+
function compactedSystem(before, timestamp) {
|
|
198
|
+
let headEnd = before.findIndex(startsConversation);
|
|
199
|
+
if (headEnd < 0)
|
|
200
|
+
headEnd = before.length;
|
|
201
|
+
const head = replaySystem(systemMessagesOf(before.slice(0, headEnd)));
|
|
202
|
+
const later = systemMessagesOf(before.slice(headEnd));
|
|
203
|
+
const out = {};
|
|
204
|
+
if (head !== undefined)
|
|
205
|
+
out.checkpoint = systemCheckpoint(head, timestamp);
|
|
206
|
+
if (later.length === 0)
|
|
207
|
+
return out;
|
|
208
|
+
const patch = synthesizeSystemPatch(head ?? { sections: {}, tools: [] }, later, timestamp);
|
|
209
|
+
if (patch !== undefined)
|
|
210
|
+
out.patch = patch;
|
|
211
|
+
return out;
|
|
212
|
+
}
|
|
137
213
|
export function buildProjection(branch) {
|
|
138
214
|
const edits = collectContextEdits(branch);
|
|
139
215
|
const at = latestCompactionIndex(branch);
|
|
140
216
|
const compaction = at >= 0 ? branch[at] : undefined;
|
|
141
217
|
const items = [];
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
}
|
|
148
|
-
const state = replaySystem(before);
|
|
149
|
-
if (state !== undefined) {
|
|
150
|
-
items.push({
|
|
151
|
-
entry: compaction,
|
|
152
|
-
message: systemCheckpoint(state, parseTime(compaction.timestamp)),
|
|
153
|
-
});
|
|
154
|
-
}
|
|
155
|
-
}
|
|
218
|
+
const system = compaction === undefined
|
|
219
|
+
? {}
|
|
220
|
+
: compactedSystem(branch.slice(0, at), parseTime(compaction.timestamp));
|
|
221
|
+
if (compaction !== undefined && system.checkpoint !== undefined)
|
|
222
|
+
items.push({ entry: compaction, message: system.checkpoint });
|
|
156
223
|
for (const entry of buildContextEntries(branch)) {
|
|
157
224
|
let message = entryToMessage(entry);
|
|
158
|
-
if (message
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
if (edit.replacement
|
|
163
|
-
|
|
164
|
-
message = applyReplacement(message, edit.replacement);
|
|
225
|
+
if (message !== undefined) {
|
|
226
|
+
const edit = edits.get(entry.id);
|
|
227
|
+
if (edit === undefined)
|
|
228
|
+
items.push({ entry, message });
|
|
229
|
+
else if (edit.replacement !== null)
|
|
230
|
+
items.push({ entry, message: applyReplacement(message, edit.replacement) });
|
|
165
231
|
}
|
|
166
|
-
|
|
232
|
+
if (entry === compaction && system.patch !== undefined)
|
|
233
|
+
items.push({ entry: compaction, message: system.patch });
|
|
167
234
|
}
|
|
168
235
|
return { items, messages: items.map((item) => item.message), compaction };
|
|
169
236
|
}
|
package/dist/session/scan.d.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* 与 store.ts / manager.ts 的区别:这里**从不加锁、不修复、不写文件**——正在运行的会话也能读。
|
|
5
5
|
* - `sessionFilesInScope`:按 cwd(只看它的编码子目录)或全部子目录(跳过 `.trash`)列文件,最新在前;
|
|
6
|
-
* - `forEachLine
|
|
6
|
+
* - `forEachLine`:按块读盘、按行回调(不读全文、不 split);提前返回即停止读盘;
|
|
7
7
|
* - `lineType`:ama 写入的行以 `{"type":"…"` 开头(manager 的 append 把 type 放在第一个键),
|
|
8
8
|
* 据此不解析就能跳过不需要的行;其它程序写的行退回 JSON.parse;
|
|
9
9
|
* - `readSessionReadOnly`:头 + 条目 + 叶子(同 migrate.ts 的规则),末尾半行忽略、中间坏行抛错。
|
|
@@ -18,9 +18,9 @@ export declare function sessionFilesInScope(root: string, cwd?: string): string[
|
|
|
18
18
|
export declare function findSessionFileReadOnly(root: string, id: string, cwd?: string): string;
|
|
19
19
|
/**
|
|
20
20
|
* 逐行回调(不含行尾 `\n` / `\r`,跳过空行);`last` 表示文件最后一行且没有换行结尾
|
|
21
|
-
* (可能是写入中途的半行)。回调返回 false
|
|
21
|
+
* (可能是写入中途的半行)。回调返回 false 提前结束——按块读盘(line-reader.ts),停下即不再读。
|
|
22
22
|
*/
|
|
23
|
-
export declare function forEachLine(file: string, visit: (line: string, index: number, last: boolean) => boolean | void): void;
|
|
23
|
+
export declare function forEachLine(file: string, visit: (line: string, index: number, last: boolean) => boolean | void, chunkBytes?: number): void;
|
|
24
24
|
/** ama 写的行:不解析就取出 type(与消息的 role,若紧随其后);不是这种形状时 undefined。 */
|
|
25
25
|
export declare function lineType(line: string): {
|
|
26
26
|
type: string;
|
package/dist/session/scan.js
CHANGED
|
@@ -3,14 +3,15 @@
|
|
|
3
3
|
*
|
|
4
4
|
* 与 store.ts / manager.ts 的区别:这里**从不加锁、不修复、不写文件**——正在运行的会话也能读。
|
|
5
5
|
* - `sessionFilesInScope`:按 cwd(只看它的编码子目录)或全部子目录(跳过 `.trash`)列文件,最新在前;
|
|
6
|
-
* - `forEachLine
|
|
6
|
+
* - `forEachLine`:按块读盘、按行回调(不读全文、不 split);提前返回即停止读盘;
|
|
7
7
|
* - `lineType`:ama 写入的行以 `{"type":"…"` 开头(manager 的 append 把 type 放在第一个键),
|
|
8
8
|
* 据此不解析就能跳过不需要的行;其它程序写的行退回 JSON.parse;
|
|
9
9
|
* - `readSessionReadOnly`:头 + 条目 + 叶子(同 migrate.ts 的规则),末尾半行忽略、中间坏行抛错。
|
|
10
10
|
*/
|
|
11
|
-
import { existsSync,
|
|
11
|
+
import { existsSync, readdirSync, statSync } from "node:fs";
|
|
12
12
|
import { basename, join } from "node:path";
|
|
13
13
|
import { AmaError } from "../errors.js";
|
|
14
|
+
import { forEachLineSync } from "./line-reader.js";
|
|
14
15
|
import { migrateSessionLines } from "./migrate.js";
|
|
15
16
|
import { SESSION_FILE_SUFFIX, TRASH_DIR_NAME, listSessionFiles, sessionDirForCwd, sessionIdFromFileName, } from "./store.js";
|
|
16
17
|
import { msg } from "../i18n/index.js";
|
|
@@ -68,26 +69,16 @@ export function findSessionFileReadOnly(root, id, cwd) {
|
|
|
68
69
|
}
|
|
69
70
|
/**
|
|
70
71
|
* 逐行回调(不含行尾 `\n` / `\r`,跳过空行);`last` 表示文件最后一行且没有换行结尾
|
|
71
|
-
* (可能是写入中途的半行)。回调返回 false
|
|
72
|
+
* (可能是写入中途的半行)。回调返回 false 提前结束——按块读盘(line-reader.ts),停下即不再读。
|
|
72
73
|
*/
|
|
73
|
-
export function forEachLine(file, visit) {
|
|
74
|
-
const text = readFileSync(file, "utf8");
|
|
75
|
-
let start = 0;
|
|
74
|
+
export function forEachLine(file, visit, chunkBytes) {
|
|
76
75
|
let index = 0;
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
const last = end === -1;
|
|
80
|
-
if (last)
|
|
81
|
-
end = text.length;
|
|
82
|
-
let line = text.slice(start, end);
|
|
83
|
-
if (line.endsWith("\r"))
|
|
84
|
-
line = line.slice(0, -1);
|
|
85
|
-
start = end + 1;
|
|
76
|
+
forEachLineSync(file, (buf, _physical, last) => {
|
|
77
|
+
const line = buf.toString("utf8");
|
|
86
78
|
if (line.trim() === "")
|
|
87
|
-
continue;
|
|
88
|
-
if (visit(line, index++, last) === false)
|
|
89
79
|
return;
|
|
90
|
-
|
|
80
|
+
return visit(line, index++, last);
|
|
81
|
+
}, chunkBytes);
|
|
91
82
|
}
|
|
92
83
|
const TYPE_PREFIX = '{"type":"';
|
|
93
84
|
/** ama 写的行:不解析就取出 type(与消息的 role,若紧随其后);不是这种形状时 undefined。 */
|
package/dist/session/store.d.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* - 锁:`<file>.lock`(O_EXCL 创建,内容为 pid);持锁进程已不在 → 视为陈旧锁并接管。
|
|
8
8
|
* - prune:移到 `<sessionsRoot>/.trash/`,文件名前缀删除时间;`purgeTrash` 清理超过 7 天的。
|
|
9
9
|
*/
|
|
10
|
+
import type { LineLocator } from "./offload.js";
|
|
10
11
|
import type { SessionLine } from "./types.js";
|
|
11
12
|
export declare const SESSION_FILE_SUFFIX = ".jsonl";
|
|
12
13
|
export declare const TRASH_DIR_NAME = ".trash";
|
|
@@ -22,17 +23,27 @@ export interface ReadResult {
|
|
|
22
23
|
lines: SessionLine[];
|
|
23
24
|
/** 末尾半行被截掉时为 true。 */
|
|
24
25
|
repairedTail: boolean;
|
|
26
|
+
/** 给了 `onLine` 时:读完(含修复)后的文件字节数。 */
|
|
27
|
+
bytes?: number;
|
|
25
28
|
}
|
|
29
|
+
/** 空白行(同旧口径 `trim() === ""`):ASCII 空白直接判定,遇到非 ASCII 字节才解码再 trim。 */
|
|
30
|
+
export declare function isBlankLine(line: Buffer): boolean;
|
|
26
31
|
/**
|
|
27
32
|
* 读并解析 JSONL。`repair: true` 时把末尾半行从文件里截掉(只截最后一行,且只在它无法解析时)。
|
|
33
|
+
* 按块逐行读(line-reader.ts,docs/memory-plan.md §2.4):不生成全文字符串与 split 数组。
|
|
28
34
|
*/
|
|
29
35
|
export declare function readSessionLines(file: string, options?: {
|
|
30
36
|
repair?: boolean;
|
|
37
|
+
/** 每解析出一行就回调(行位置不含行尾;#170 图片卸载在这里就地剥掉图片)。 */
|
|
38
|
+
onLine?: (line: SessionLine, locator: LineLocator) => void;
|
|
31
39
|
}): ReadResult;
|
|
32
|
-
/** 追加若干行(一次 write,O_APPEND
|
|
33
|
-
export declare function appendLines(file: string, lines: readonly SessionLine[]):
|
|
34
|
-
/**
|
|
35
|
-
|
|
40
|
+
/** 追加若干行(一次 write,O_APPEND),返回写入的字节数。 */
|
|
41
|
+
export declare function appendLines(file: string, lines: readonly SessionLine[]): number;
|
|
42
|
+
/**
|
|
43
|
+
* 新建文件:先写临时文件再 rename,避免出现只有半个头的文件。返回每行的字节数(不含行尾,与
|
|
44
|
+
* `lines` 同序)。
|
|
45
|
+
*/
|
|
46
|
+
export declare function writeNewSessionFile(file: string, lines: readonly SessionLine[]): number[];
|
|
36
47
|
export interface SessionLock {
|
|
37
48
|
readonly path: string;
|
|
38
49
|
release(): void;
|
package/dist/session/store.js
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import { closeSync, existsSync, mkdirSync, openSync, readFileSync, readdirSync, renameSync, readSync, rmSync, statSync, truncateSync, writeFileSync, writeSync, } from "node:fs";
|
|
11
11
|
import { basename, dirname, join } from "node:path";
|
|
12
12
|
import { AmaError } from "../errors.js";
|
|
13
|
+
import { forEachLineSync } from "./line-reader.js";
|
|
13
14
|
export const SESSION_FILE_SUFFIX = ".jsonl";
|
|
14
15
|
export const TRASH_DIR_NAME = ".trash";
|
|
15
16
|
export const TRASH_RETENTION_MS = 7 * 24 * 60 * 60 * 1000;
|
|
@@ -33,75 +34,90 @@ export function sessionIdFromFileName(file) {
|
|
|
33
34
|
const at = stem.indexOf("_");
|
|
34
35
|
return at < 0 ? undefined : stem.slice(at + 1);
|
|
35
36
|
}
|
|
37
|
+
/** 空白行(同旧口径 `trim() === ""`):ASCII 空白直接判定,遇到非 ASCII 字节才解码再 trim。 */
|
|
38
|
+
export function isBlankLine(line) {
|
|
39
|
+
for (const byte of line) {
|
|
40
|
+
if (byte === 0x20 || (byte >= 0x09 && byte <= 0x0d))
|
|
41
|
+
continue;
|
|
42
|
+
return byte >= 0x80 && line.toString("utf8").trim() === "";
|
|
43
|
+
}
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
36
46
|
/**
|
|
37
47
|
* 读并解析 JSONL。`repair: true` 时把末尾半行从文件里截掉(只截最后一行,且只在它无法解析时)。
|
|
48
|
+
* 按块逐行读(line-reader.ts,docs/memory-plan.md §2.4):不生成全文字符串与 split 数组。
|
|
38
49
|
*/
|
|
39
50
|
export function readSessionLines(file, options = {}) {
|
|
40
|
-
let text;
|
|
41
|
-
try {
|
|
42
|
-
text = readFileSync(file, "utf8");
|
|
43
|
-
}
|
|
44
|
-
catch (error) {
|
|
45
|
-
throw new AmaError("session_not_found", `cannot read session file ${file}`, { cause: error });
|
|
46
|
-
}
|
|
47
|
-
const rawLines = text.split("\n");
|
|
48
|
-
const endsWithNewline = text.endsWith("\n");
|
|
49
|
-
if (endsWithNewline)
|
|
50
|
-
rawLines.pop();
|
|
51
51
|
const lines = [];
|
|
52
52
|
let repairedTail = false;
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
53
|
+
let unterminated = false;
|
|
54
|
+
try {
|
|
55
|
+
forEachLineSync(file, (buf, index, last, offset) => {
|
|
56
|
+
if (last)
|
|
57
|
+
unterminated = true;
|
|
58
|
+
if (isBlankLine(buf))
|
|
59
|
+
return;
|
|
60
|
+
let line;
|
|
61
|
+
try {
|
|
62
|
+
line = JSON.parse(buf.toString("utf8"));
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
if (last) {
|
|
66
|
+
repairedTail = true;
|
|
67
|
+
if (options.repair === true)
|
|
68
|
+
truncateSync(file, offset);
|
|
69
|
+
return false;
|
|
67
70
|
}
|
|
68
|
-
|
|
71
|
+
throw new AmaError("session_corrupt", `${file}:${index + 1}: invalid JSON line`, {
|
|
72
|
+
cause: error,
|
|
73
|
+
});
|
|
69
74
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
}
|
|
75
|
+
options.onLine?.(line, { offset, length: buf.length });
|
|
76
|
+
lines.push(line);
|
|
77
|
+
});
|
|
74
78
|
}
|
|
75
|
-
|
|
79
|
+
catch (error) {
|
|
80
|
+
if (error instanceof AmaError)
|
|
81
|
+
throw error;
|
|
82
|
+
throw new AmaError("session_not_found", `cannot read session file ${file}`, { cause: error });
|
|
83
|
+
}
|
|
84
|
+
if (!repairedTail && unterminated && options.repair === true) {
|
|
76
85
|
// 最后一行完整但缺 LF:补上,保证后续追加另起一行。
|
|
77
86
|
appendRaw(file, "\n");
|
|
78
87
|
}
|
|
79
|
-
|
|
88
|
+
if (options.onLine === undefined)
|
|
89
|
+
return { lines, repairedTail };
|
|
90
|
+
return { lines, repairedTail, bytes: statSync(file).size };
|
|
80
91
|
}
|
|
81
92
|
function appendRaw(file, text) {
|
|
82
93
|
const fd = openSync(file, "a");
|
|
83
94
|
try {
|
|
84
|
-
writeSync(fd, text);
|
|
95
|
+
return writeSync(fd, text);
|
|
85
96
|
}
|
|
86
97
|
finally {
|
|
87
98
|
closeSync(fd);
|
|
88
99
|
}
|
|
89
100
|
}
|
|
90
|
-
/** 追加若干行(一次 write,O_APPEND
|
|
101
|
+
/** 追加若干行(一次 write,O_APPEND),返回写入的字节数。 */
|
|
91
102
|
export function appendLines(file, lines) {
|
|
92
103
|
if (lines.length === 0)
|
|
93
|
-
return;
|
|
94
|
-
appendRaw(file, lines.map((line) => JSON.stringify(line)).join("\n") + "\n");
|
|
104
|
+
return 0;
|
|
105
|
+
return appendRaw(file, lines.map((line) => JSON.stringify(line)).join("\n") + "\n");
|
|
95
106
|
}
|
|
96
|
-
/**
|
|
107
|
+
/**
|
|
108
|
+
* 新建文件:先写临时文件再 rename,避免出现只有半个头的文件。返回每行的字节数(不含行尾,与
|
|
109
|
+
* `lines` 同序)。
|
|
110
|
+
*/
|
|
97
111
|
export function writeNewSessionFile(file, lines) {
|
|
98
112
|
mkdirSync(dirname(file), { recursive: true, mode: 0o700 });
|
|
99
113
|
if (existsSync(file)) {
|
|
100
114
|
throw new AmaError("session_exists", `session file already exists: ${file}`);
|
|
101
115
|
}
|
|
102
116
|
const tmp = `${file}.${process.pid}.tmp`;
|
|
103
|
-
|
|
117
|
+
const texts = lines.map((line) => JSON.stringify(line));
|
|
118
|
+
writeFileSync(tmp, texts.join("\n") + "\n", { mode: 0o600 });
|
|
104
119
|
renameSync(tmp, file);
|
|
120
|
+
return texts.map((text) => Buffer.byteLength(text));
|
|
105
121
|
}
|
|
106
122
|
// ---------------------------------------------------------------------------
|
|
107
123
|
// 锁
|
package/dist/session/types.d.ts
CHANGED
|
@@ -199,7 +199,12 @@ export interface SessionManagerApi {
|
|
|
199
199
|
getTree(): SessionTreeNode[];
|
|
200
200
|
name(): string | undefined;
|
|
201
201
|
setName(name: string): void;
|
|
202
|
-
/**
|
|
203
|
-
|
|
202
|
+
/**
|
|
203
|
+
* 复制 root → entryId 的分支到新文件(parentSession 指回);`head` 作为新根条目,复制的首条重挂到
|
|
204
|
+
* 它下面([ME-C0] fork 子会话的 `custom{ama.task}` 标记)。
|
|
205
|
+
*/
|
|
206
|
+
fork(entryId: string, options?: {
|
|
207
|
+
head?: SessionEntryInput;
|
|
208
|
+
}): SessionManagerApi;
|
|
204
209
|
}
|
|
205
210
|
export {};
|