@armadra/agent 0.7.2 → 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 +41 -0
- package/CHANGELOG.zh-CN.md +41 -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 +30 -5
- package/dist/agent/session-compaction.js +69 -29
- 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 +3142 -1805
- 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/compaction/prefix-estimate.d.ts +3 -1
- package/dist/compaction/prefix-estimate.js +7 -3
- 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 +15 -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-context.d.ts +4 -0
- package/dist/i18n/messages/report-context.js +4 -0
- package/dist/i18n/messages/report.d.ts +6 -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.d.ts +2 -0
- package/dist/modes/context-report.js +25 -8
- 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 +3 -3
- 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 +2 -2
- package/package.json +2 -2
|
@@ -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;
|
package/dist/session/manager.js
CHANGED
|
@@ -7,17 +7,19 @@
|
|
|
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 { randomBytes, randomUUID } from "node:crypto";
|
|
15
|
-
import { statSync } from "node:fs";
|
|
16
16
|
import { dirname, join } from "node:path";
|
|
17
|
+
import { internSessionImages } from "../ai/image-intern.js";
|
|
17
18
|
import { AmaError } from "../errors.js";
|
|
18
19
|
import { AMA_VERSION } from "../version.js";
|
|
20
|
+
import { listSessionItems } from "./list.js";
|
|
19
21
|
import { migrateSessionLines } from "./migrate.js";
|
|
20
|
-
import { acquireLock, appendLines,
|
|
22
|
+
import { acquireLock, appendLines, isSubagentSessionFile, listSessionFiles, readSessionLines, sessionDirForCwd, sessionFileName, writeNewSessionFile, } from "./store.js";
|
|
21
23
|
import { buildTree, indexEntries, pathToRoot } from "./tree.js";
|
|
22
24
|
import { SESSION_FORMAT_VERSION } from "./types.js";
|
|
23
25
|
function newEntryId(taken) {
|
|
@@ -79,6 +81,7 @@ export class SessionManager {
|
|
|
79
81
|
try {
|
|
80
82
|
const { lines } = readSessionLines(file, { repair: true });
|
|
81
83
|
const { header, entries, leafId } = migrateSessionLines(lines, file);
|
|
84
|
+
internSessionImages(entries); // D5:同一图片的 base64 只留一份;JSONL 不变
|
|
82
85
|
return new SessionManager(header, entries, { kind: "file", file, lock }, options.now ?? (() => new Date()), leafId);
|
|
83
86
|
}
|
|
84
87
|
catch (error) {
|
|
@@ -93,48 +96,7 @@ export class SessionManager {
|
|
|
93
96
|
}
|
|
94
97
|
/** 只读列出目录下的会话(最新在前);损坏的文件跳过。 */
|
|
95
98
|
static list(dir) {
|
|
96
|
-
|
|
97
|
-
for (const file of listSessionFiles(dir)) {
|
|
98
|
-
try {
|
|
99
|
-
const { header, entries } = migrateSessionLines(readSessionLines(file).lines, file);
|
|
100
|
-
let name;
|
|
101
|
-
let firstPrompt;
|
|
102
|
-
let messageCount = 0;
|
|
103
|
-
for (const entry of entries) {
|
|
104
|
-
if (entry.type === "session_info" && entry.name !== undefined)
|
|
105
|
-
name = entry.name;
|
|
106
|
-
if (entry.type !== "message" || entry.message.role === "system")
|
|
107
|
-
continue;
|
|
108
|
-
messageCount++;
|
|
109
|
-
if (firstPrompt === undefined && entry.message.role === "user") {
|
|
110
|
-
const { content } = entry.message;
|
|
111
|
-
firstPrompt =
|
|
112
|
-
typeof content === "string"
|
|
113
|
-
? content
|
|
114
|
-
: content.map((block) => (block.type === "text" ? block.text : "")).join("");
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
const item = {
|
|
118
|
-
id: header.id,
|
|
119
|
-
file,
|
|
120
|
-
cwd: header.cwd,
|
|
121
|
-
createdAt: header.timestamp,
|
|
122
|
-
modifiedAt: statSync(file).mtime.toISOString(),
|
|
123
|
-
messageCount,
|
|
124
|
-
};
|
|
125
|
-
if (name !== undefined)
|
|
126
|
-
item.name = name;
|
|
127
|
-
if (isSubagentSession(header, entries[0]))
|
|
128
|
-
item.subagent = true;
|
|
129
|
-
if (firstPrompt !== undefined)
|
|
130
|
-
item.firstPrompt = firstPrompt.slice(0, 200);
|
|
131
|
-
items.push(item);
|
|
132
|
-
}
|
|
133
|
-
catch {
|
|
134
|
-
// 损坏 / 不可读:列表里跳过(sessions show 会给出具体错误)
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
return items;
|
|
99
|
+
return listSessionItems(dir);
|
|
138
100
|
}
|
|
139
101
|
// -------------------------------------------------------------------------
|
|
140
102
|
// SessionManagerApi
|
|
@@ -206,16 +168,27 @@ export class SessionManager {
|
|
|
206
168
|
setName(name) {
|
|
207
169
|
this.append({ type: "session_info", name });
|
|
208
170
|
}
|
|
209
|
-
fork(entryId) {
|
|
171
|
+
fork(entryId, forkOptions = {}) {
|
|
210
172
|
if (!this.index.has(entryId)) {
|
|
211
173
|
throw new AmaError("invalid_arguments", `no such session entry: ${entryId}`);
|
|
212
174
|
}
|
|
213
175
|
const copied = this.branch(entryId).map((entry) => structuredClone(entry));
|
|
176
|
+
if (forkOptions.head !== undefined) {
|
|
177
|
+
const head = {
|
|
178
|
+
...forkOptions.head,
|
|
179
|
+
id: newEntryId(indexEntries(copied)),
|
|
180
|
+
parentId: null,
|
|
181
|
+
timestamp: this.now().toISOString(),
|
|
182
|
+
};
|
|
183
|
+
if (copied[0] !== undefined)
|
|
184
|
+
copied[0].parentId = head.id;
|
|
185
|
+
copied.unshift(head);
|
|
186
|
+
}
|
|
214
187
|
const options = { now: this.now };
|
|
215
188
|
const parentFile = this.file();
|
|
216
189
|
if (parentFile !== undefined)
|
|
217
190
|
options.parentSession = parentFile;
|
|
218
|
-
const header = SessionManager.newHeader(this.cwd, options, this.now());
|
|
191
|
+
const header = SessionManager.newHeader(forkOptions.cwd ?? this.cwd, options, this.now());
|
|
219
192
|
let storage;
|
|
220
193
|
if (this.storage.kind === "memory")
|
|
221
194
|
storage = { kind: "memory" };
|
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
* - 缺头 / 头损坏 / 条目缺 id → 拒绝,不猜测、不重写文件。
|
|
8
8
|
* - [W3-C1b] `leaf` 行(`/tree` 位置,第三波 A7)不是条目:不进 entries;最后一条 leaf 行若在
|
|
9
9
|
* 最后一条条目之后,作为 `leafId` 返回(之后又追加了条目则作废)。id 须为字符串或 null。
|
|
10
|
-
* 将来格式升级时在这里加「旧版本 →
|
|
10
|
+
* 将来格式升级时在这里加「旧版本 → 当前」的内存迁移(文件只追加,不回写);`list.ts` 不经本函数、
|
|
11
|
+
* 自己按 v1 规则流式统计,升级格式时要同时改它(docs/memory-plan.md R6)。
|
|
11
12
|
*/
|
|
12
13
|
import type { SessionEntry, SessionHeader, SessionLine } from "./types.js";
|
|
13
14
|
export interface MigratedSession {
|
package/dist/session/migrate.js
CHANGED
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
* - 缺头 / 头损坏 / 条目缺 id → 拒绝,不猜测、不重写文件。
|
|
8
8
|
* - [W3-C1b] `leaf` 行(`/tree` 位置,第三波 A7)不是条目:不进 entries;最后一条 leaf 行若在
|
|
9
9
|
* 最后一条条目之后,作为 `leafId` 返回(之后又追加了条目则作废)。id 须为字符串或 null。
|
|
10
|
-
* 将来格式升级时在这里加「旧版本 →
|
|
10
|
+
* 将来格式升级时在这里加「旧版本 → 当前」的内存迁移(文件只追加,不回写);`list.ts` 不经本函数、
|
|
11
|
+
* 自己按 v1 规则流式统计,升级格式时要同时改它(docs/memory-plan.md R6)。
|
|
11
12
|
*/
|
|
12
13
|
import { AmaError } from "../errors.js";
|
|
13
14
|
import { SESSION_FORMAT_VERSION } from "./types.js";
|
|
@@ -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
|
@@ -23,8 +23,11 @@ export interface ReadResult {
|
|
|
23
23
|
/** 末尾半行被截掉时为 true。 */
|
|
24
24
|
repairedTail: boolean;
|
|
25
25
|
}
|
|
26
|
+
/** 空白行(同旧口径 `trim() === ""`):ASCII 空白直接判定,遇到非 ASCII 字节才解码再 trim。 */
|
|
27
|
+
export declare function isBlankLine(line: Buffer): boolean;
|
|
26
28
|
/**
|
|
27
29
|
* 读并解析 JSONL。`repair: true` 时把末尾半行从文件里截掉(只截最后一行,且只在它无法解析时)。
|
|
30
|
+
* 按块逐行读(line-reader.ts,docs/memory-plan.md §2.4):不生成全文字符串与 split 数组。
|
|
28
31
|
*/
|
|
29
32
|
export declare function readSessionLines(file: string, options?: {
|
|
30
33
|
repair?: boolean;
|