chatccc 0.2.243 → 0.2.244
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/README.md +5 -5
- package/bin/cccagent.mjs +17 -17
- package/deepccc-agent/bin/deepccc.mjs +26 -26
- package/deepccc-agent/os-prompts/darwin.md +8 -0
- package/deepccc-agent/os-prompts/linux.md +8 -0
- package/deepccc-agent/os-prompts/win32.md +9 -0
- package/deepccc-agent/package.json +2 -1
- package/deepccc-agent/src/__tests__/chat-session.test.ts +39 -0
- package/deepccc-agent/src/__tests__/cli-json.test.ts +49 -49
- package/deepccc-agent/src/__tests__/config.test.ts +26 -26
- package/deepccc-agent/src/__tests__/context.test.ts +319 -319
- package/deepccc-agent/src/__tests__/file-tools.test.ts +240 -240
- package/deepccc-agent/src/__tests__/permissions.test.ts +195 -195
- package/deepccc-agent/src/__tests__/progress-reducer.test.ts +121 -121
- package/deepccc-agent/src/__tests__/session-search.test.ts +262 -262
- package/deepccc-agent/src/__tests__/session-select.test.ts +116 -116
- package/deepccc-agent/src/__tests__/sigint.test.ts +56 -56
- package/deepccc-agent/src/__tests__/skills.test.ts +284 -284
- package/deepccc-agent/src/__tests__/terminal-renderer.test.ts +247 -247
- package/deepccc-agent/src/__tests__/web-tools.test.ts +220 -220
- package/deepccc-agent/src/config.ts +84 -84
- package/deepccc-agent/src/context.ts +465 -465
- package/deepccc-agent/src/file-log.ts +38 -38
- package/deepccc-agent/src/index.ts +46 -16
- package/deepccc-agent/src/proc-tree-kill.ts +61 -61
- package/deepccc-agent/src/progress/cards-helpers.ts +76 -76
- package/deepccc-agent/src/progress/reducer.ts +113 -113
- package/deepccc-agent/src/progress/terminal-renderer.ts +294 -294
- package/deepccc-agent/src/progress/view.ts +77 -77
- package/deepccc-agent/src/raw-stream-log.ts +124 -124
- package/deepccc-agent/src/session-search.ts +370 -370
- package/deepccc-agent/src/session-select.ts +48 -48
- package/deepccc-agent/src/sigint.ts +50 -50
- package/deepccc-agent/src/skills.ts +205 -205
- package/deepccc-agent/src/web-tools.ts +313 -313
- package/deepccc-agent/tsconfig.build.json +13 -13
- package/deepccc-agent/tsconfig.json +13 -13
- package/deepccc-agent/vitest.config.ts +7 -7
- package/package.json +73 -73
- package/src/__tests__/builtin-chat-session.test.ts +522 -522
- package/src/__tests__/builtin-config.test.ts +26 -26
- package/src/__tests__/builtin-context.test.ts +319 -319
- package/src/__tests__/builtin-file-tools.test.ts +240 -240
- package/src/__tests__/builtin-permissions.test.ts +211 -211
- package/src/__tests__/builtin-session-search.test.ts +262 -262
- package/src/__tests__/builtin-session-select.test.ts +116 -116
- package/src/__tests__/builtin-sigint.test.ts +56 -56
- package/src/__tests__/builtin-skills.test.ts +284 -284
- package/src/__tests__/builtin-web-tools.test.ts +220 -220
- package/src/__tests__/config.test.ts +17 -17
- package/src/__tests__/progress-reducer.test.ts +121 -121
- package/src/__tests__/session-ccc-config.test.ts +45 -45
- package/src/__tests__/session.test.ts +298 -298
- package/src/adapters/ccc-adapter.ts +145 -145
- package/src/config-utils.ts +13 -13
- package/src/config.ts +13 -13
- package/src/progress/reducer.ts +113 -113
- package/src/session-chat-binding.ts +83 -83
- package/src/session.ts +311 -311
|
@@ -1,77 +1,77 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* progress/view.ts — 平台无关的"过程进度"视图模型
|
|
3
|
-
*
|
|
4
|
-
* 这是飞书过程卡片与终端过程区块共享的唯一数据模型:
|
|
5
|
-
* - 飞书:buildProgressCard(view) 把 ProgressView 渲染为卡片 JSON
|
|
6
|
-
* - 终端:TerminalProgressRenderer 把 ProgressView 渲染为 ANSI 区块
|
|
7
|
-
* - 事件流:reduceProgress(prev, event) 把 ChatEvent 增量合并进 ProgressView
|
|
8
|
-
*
|
|
9
|
-
* 语义与飞书卡片对齐:
|
|
10
|
-
* - headerTitle 是"生成中"阶段展示的标题(如"正在启动 Agent · 0秒")
|
|
11
|
-
* - status 决定终态外观(完成 / 已停止 / 异常结束)
|
|
12
|
-
* - text 是正文累积(对应卡片 main_content / 终端区块正文)
|
|
13
|
-
* - tools 是本轮工具调用列表(飞书卡片暂不展示,终端折叠为单行)
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
export type ProgressStatus = "generating" | "done" | "stopped" | "error";
|
|
17
|
-
|
|
18
|
-
export type ProgressToolStatus = "running" | "ok" | "error";
|
|
19
|
-
|
|
20
|
-
export interface ProgressToolCall {
|
|
21
|
-
/** 工具调用 ID(ChatEvent.tool_use.id,可能缺失) */
|
|
22
|
-
id: string;
|
|
23
|
-
/** 工具名,如 edit_file */
|
|
24
|
-
name: string;
|
|
25
|
-
status: ProgressToolStatus;
|
|
26
|
-
/** 工具输入摘要(终端折叠行展示,截断保存) */
|
|
27
|
-
detail?: string;
|
|
28
|
-
/** 工具结果摘要(成功/失败,截断保存) */
|
|
29
|
-
summary?: string;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export interface ProgressView {
|
|
33
|
-
/** 当前阶段状态,决定终态外观 */
|
|
34
|
-
status: ProgressStatus;
|
|
35
|
-
/** 生成中阶段的头部标题(对应飞书卡片 header title) */
|
|
36
|
-
headerTitle: string;
|
|
37
|
-
/** 卡片头部颜色模板(飞书用),终端忽略 */
|
|
38
|
-
headerTemplate: string;
|
|
39
|
-
/** 正文累积内容 */
|
|
40
|
-
text: string;
|
|
41
|
-
/** 本轮工具调用列表 */
|
|
42
|
-
tools: ProgressToolCall[];
|
|
43
|
-
/** 是否展示停止按钮 / 停止提示 */
|
|
44
|
-
showStop: boolean;
|
|
45
|
-
/** 最近一次更新时间戳 */
|
|
46
|
-
updatedAt: number;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export interface ProgressViewInit {
|
|
50
|
-
status?: ProgressStatus;
|
|
51
|
-
headerTitle?: string;
|
|
52
|
-
headerTemplate?: string;
|
|
53
|
-
text?: string;
|
|
54
|
-
tools?: ProgressToolCall[];
|
|
55
|
-
showStop?: boolean;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/** 创建 ProgressView,未提供的字段使用与飞书 buildProgressCard 一致的默认值 */
|
|
59
|
-
export function progressView(init: ProgressViewInit = {}): ProgressView {
|
|
60
|
-
return {
|
|
61
|
-
status: init.status ?? "generating",
|
|
62
|
-
headerTitle: init.headerTitle ?? "生成中...",
|
|
63
|
-
headerTemplate: init.headerTemplate ?? "blue",
|
|
64
|
-
text: init.text ?? "",
|
|
65
|
-
tools: init.tools ?? [],
|
|
66
|
-
showStop: init.showStop ?? true,
|
|
67
|
-
updatedAt: Date.now(),
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/** 只读浅拷贝并覆盖部分字段,返回新视图(reducer 的不可变更新用) */
|
|
72
|
-
export function withProgressView(
|
|
73
|
-
prev: ProgressView,
|
|
74
|
-
patch: Partial<ProgressView>,
|
|
75
|
-
): ProgressView {
|
|
76
|
-
return { ...prev, ...patch, updatedAt: Date.now() };
|
|
77
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* progress/view.ts — 平台无关的"过程进度"视图模型
|
|
3
|
+
*
|
|
4
|
+
* 这是飞书过程卡片与终端过程区块共享的唯一数据模型:
|
|
5
|
+
* - 飞书:buildProgressCard(view) 把 ProgressView 渲染为卡片 JSON
|
|
6
|
+
* - 终端:TerminalProgressRenderer 把 ProgressView 渲染为 ANSI 区块
|
|
7
|
+
* - 事件流:reduceProgress(prev, event) 把 ChatEvent 增量合并进 ProgressView
|
|
8
|
+
*
|
|
9
|
+
* 语义与飞书卡片对齐:
|
|
10
|
+
* - headerTitle 是"生成中"阶段展示的标题(如"正在启动 Agent · 0秒")
|
|
11
|
+
* - status 决定终态外观(完成 / 已停止 / 异常结束)
|
|
12
|
+
* - text 是正文累积(对应卡片 main_content / 终端区块正文)
|
|
13
|
+
* - tools 是本轮工具调用列表(飞书卡片暂不展示,终端折叠为单行)
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export type ProgressStatus = "generating" | "done" | "stopped" | "error";
|
|
17
|
+
|
|
18
|
+
export type ProgressToolStatus = "running" | "ok" | "error";
|
|
19
|
+
|
|
20
|
+
export interface ProgressToolCall {
|
|
21
|
+
/** 工具调用 ID(ChatEvent.tool_use.id,可能缺失) */
|
|
22
|
+
id: string;
|
|
23
|
+
/** 工具名,如 edit_file */
|
|
24
|
+
name: string;
|
|
25
|
+
status: ProgressToolStatus;
|
|
26
|
+
/** 工具输入摘要(终端折叠行展示,截断保存) */
|
|
27
|
+
detail?: string;
|
|
28
|
+
/** 工具结果摘要(成功/失败,截断保存) */
|
|
29
|
+
summary?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface ProgressView {
|
|
33
|
+
/** 当前阶段状态,决定终态外观 */
|
|
34
|
+
status: ProgressStatus;
|
|
35
|
+
/** 生成中阶段的头部标题(对应飞书卡片 header title) */
|
|
36
|
+
headerTitle: string;
|
|
37
|
+
/** 卡片头部颜色模板(飞书用),终端忽略 */
|
|
38
|
+
headerTemplate: string;
|
|
39
|
+
/** 正文累积内容 */
|
|
40
|
+
text: string;
|
|
41
|
+
/** 本轮工具调用列表 */
|
|
42
|
+
tools: ProgressToolCall[];
|
|
43
|
+
/** 是否展示停止按钮 / 停止提示 */
|
|
44
|
+
showStop: boolean;
|
|
45
|
+
/** 最近一次更新时间戳 */
|
|
46
|
+
updatedAt: number;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface ProgressViewInit {
|
|
50
|
+
status?: ProgressStatus;
|
|
51
|
+
headerTitle?: string;
|
|
52
|
+
headerTemplate?: string;
|
|
53
|
+
text?: string;
|
|
54
|
+
tools?: ProgressToolCall[];
|
|
55
|
+
showStop?: boolean;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** 创建 ProgressView,未提供的字段使用与飞书 buildProgressCard 一致的默认值 */
|
|
59
|
+
export function progressView(init: ProgressViewInit = {}): ProgressView {
|
|
60
|
+
return {
|
|
61
|
+
status: init.status ?? "generating",
|
|
62
|
+
headerTitle: init.headerTitle ?? "生成中...",
|
|
63
|
+
headerTemplate: init.headerTemplate ?? "blue",
|
|
64
|
+
text: init.text ?? "",
|
|
65
|
+
tools: init.tools ?? [],
|
|
66
|
+
showStop: init.showStop ?? true,
|
|
67
|
+
updatedAt: Date.now(),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** 只读浅拷贝并覆盖部分字段,返回新视图(reducer 的不可变更新用) */
|
|
72
|
+
export function withProgressView(
|
|
73
|
+
prev: ProgressView,
|
|
74
|
+
patch: Partial<ProgressView>,
|
|
75
|
+
): ProgressView {
|
|
76
|
+
return { ...prev, ...patch, updatedAt: Date.now() };
|
|
77
|
+
}
|
|
@@ -1,124 +1,124 @@
|
|
|
1
|
-
import { createWriteStream, type Dirent } from "node:fs";
|
|
2
|
-
import { mkdir, readdir, rm, stat, unlink } from "node:fs/promises";
|
|
3
|
-
import { once } from "node:events";
|
|
4
|
-
import { join } from "node:path";
|
|
5
|
-
import { createGzip } from "node:zlib";
|
|
6
|
-
|
|
7
|
-
export interface RawStreamLogOptions {
|
|
8
|
-
enabled: boolean;
|
|
9
|
-
rootDir: string;
|
|
10
|
-
tool: string;
|
|
11
|
-
sessionId: string;
|
|
12
|
-
label: string;
|
|
13
|
-
maxBytesPerTurn: number;
|
|
14
|
-
retentionDays: number;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export interface RawStreamLogHandle {
|
|
18
|
-
filePath: string;
|
|
19
|
-
writeLine(line: string): void;
|
|
20
|
-
close(options: { keep: boolean }): Promise<void>;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export function sanitizeLogPathSegment(value: string): string {
|
|
24
|
-
const safe = value.replace(/[^a-zA-Z0-9._-]+/g, "_").replace(/^[._]+|_+$/g, "");
|
|
25
|
-
return safe || "unknown";
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
async function cleanupOldRawStreamLogs(rootDir: string, retentionDays: number): Promise<void> {
|
|
29
|
-
if (!Number.isFinite(retentionDays) || retentionDays <= 0) return;
|
|
30
|
-
const cutoff = Date.now() - retentionDays * 24 * 60 * 60 * 1000;
|
|
31
|
-
|
|
32
|
-
const visit = async (dir: string): Promise<void> => {
|
|
33
|
-
let entries: Dirent[];
|
|
34
|
-
try {
|
|
35
|
-
entries = await readdir(dir, { withFileTypes: true });
|
|
36
|
-
} catch {
|
|
37
|
-
return;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
await Promise.all(entries.map(async (entry) => {
|
|
41
|
-
const path = join(dir, entry.name);
|
|
42
|
-
if (entry.isDirectory()) {
|
|
43
|
-
await visit(path);
|
|
44
|
-
try {
|
|
45
|
-
await rm(path, { recursive: false });
|
|
46
|
-
} catch {
|
|
47
|
-
// Directory is not empty or already gone.
|
|
48
|
-
}
|
|
49
|
-
return;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
if (!entry.isFile()) return;
|
|
53
|
-
try {
|
|
54
|
-
const info = await stat(path);
|
|
55
|
-
if (info.mtimeMs < cutoff) await rm(path, { force: true });
|
|
56
|
-
} catch {
|
|
57
|
-
// Best-effort cleanup only.
|
|
58
|
-
}
|
|
59
|
-
}));
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
await visit(rootDir);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export async function createRawStreamLog(options: RawStreamLogOptions): Promise<RawStreamLogHandle | null> {
|
|
66
|
-
if (!options.enabled) return null;
|
|
67
|
-
|
|
68
|
-
const maxBytes = Math.max(0, Math.floor(options.maxBytesPerTurn));
|
|
69
|
-
const tool = sanitizeLogPathSegment(options.tool);
|
|
70
|
-
const session = sanitizeLogPathSegment(options.sessionId);
|
|
71
|
-
const label = sanitizeLogPathSegment(options.label);
|
|
72
|
-
const dir = join(options.rootDir, tool, session);
|
|
73
|
-
await mkdir(dir, { recursive: true });
|
|
74
|
-
void cleanupOldRawStreamLogs(join(options.rootDir, tool), options.retentionDays);
|
|
75
|
-
|
|
76
|
-
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
77
|
-
const filePath = join(dir, `${timestamp}-${label}.jsonl.gz`);
|
|
78
|
-
const output = createWriteStream(filePath);
|
|
79
|
-
const gzip = createGzip();
|
|
80
|
-
gzip.pipe(output);
|
|
81
|
-
|
|
82
|
-
let bytes = 0;
|
|
83
|
-
let truncated = false;
|
|
84
|
-
let ended = false;
|
|
85
|
-
|
|
86
|
-
const writeLine = (line: string): void => {
|
|
87
|
-
if (ended || truncated) return;
|
|
88
|
-
const payload = `${line}\n`;
|
|
89
|
-
const payloadBytes = Buffer.byteLength(payload, "utf-8");
|
|
90
|
-
if (maxBytes > 0 && bytes + payloadBytes > maxBytes) {
|
|
91
|
-
const marker = JSON.stringify({
|
|
92
|
-
type: "deepccc_raw_stream_log_truncated",
|
|
93
|
-
reason: "max_bytes_per_turn_exceeded",
|
|
94
|
-
maxBytesPerTurn: maxBytes,
|
|
95
|
-
writtenBytes: bytes,
|
|
96
|
-
});
|
|
97
|
-
gzip.write(`${marker}\n`);
|
|
98
|
-
truncated = true;
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
|
-
bytes += payloadBytes;
|
|
102
|
-
gzip.write(payload);
|
|
103
|
-
};
|
|
104
|
-
|
|
105
|
-
const close = async ({ keep }: { keep: boolean }): Promise<void> => {
|
|
106
|
-
if (ended) return;
|
|
107
|
-
ended = true;
|
|
108
|
-
gzip.end();
|
|
109
|
-
try {
|
|
110
|
-
await once(output, "finish");
|
|
111
|
-
} catch {
|
|
112
|
-
// Ignore close errors; caller cannot recover from debug log failures.
|
|
113
|
-
}
|
|
114
|
-
if (!keep) {
|
|
115
|
-
try {
|
|
116
|
-
await unlink(filePath);
|
|
117
|
-
} catch {
|
|
118
|
-
// Best-effort cleanup only.
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
};
|
|
122
|
-
|
|
123
|
-
return { filePath, writeLine, close };
|
|
124
|
-
}
|
|
1
|
+
import { createWriteStream, type Dirent } from "node:fs";
|
|
2
|
+
import { mkdir, readdir, rm, stat, unlink } from "node:fs/promises";
|
|
3
|
+
import { once } from "node:events";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { createGzip } from "node:zlib";
|
|
6
|
+
|
|
7
|
+
export interface RawStreamLogOptions {
|
|
8
|
+
enabled: boolean;
|
|
9
|
+
rootDir: string;
|
|
10
|
+
tool: string;
|
|
11
|
+
sessionId: string;
|
|
12
|
+
label: string;
|
|
13
|
+
maxBytesPerTurn: number;
|
|
14
|
+
retentionDays: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface RawStreamLogHandle {
|
|
18
|
+
filePath: string;
|
|
19
|
+
writeLine(line: string): void;
|
|
20
|
+
close(options: { keep: boolean }): Promise<void>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function sanitizeLogPathSegment(value: string): string {
|
|
24
|
+
const safe = value.replace(/[^a-zA-Z0-9._-]+/g, "_").replace(/^[._]+|_+$/g, "");
|
|
25
|
+
return safe || "unknown";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function cleanupOldRawStreamLogs(rootDir: string, retentionDays: number): Promise<void> {
|
|
29
|
+
if (!Number.isFinite(retentionDays) || retentionDays <= 0) return;
|
|
30
|
+
const cutoff = Date.now() - retentionDays * 24 * 60 * 60 * 1000;
|
|
31
|
+
|
|
32
|
+
const visit = async (dir: string): Promise<void> => {
|
|
33
|
+
let entries: Dirent[];
|
|
34
|
+
try {
|
|
35
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
36
|
+
} catch {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
await Promise.all(entries.map(async (entry) => {
|
|
41
|
+
const path = join(dir, entry.name);
|
|
42
|
+
if (entry.isDirectory()) {
|
|
43
|
+
await visit(path);
|
|
44
|
+
try {
|
|
45
|
+
await rm(path, { recursive: false });
|
|
46
|
+
} catch {
|
|
47
|
+
// Directory is not empty or already gone.
|
|
48
|
+
}
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (!entry.isFile()) return;
|
|
53
|
+
try {
|
|
54
|
+
const info = await stat(path);
|
|
55
|
+
if (info.mtimeMs < cutoff) await rm(path, { force: true });
|
|
56
|
+
} catch {
|
|
57
|
+
// Best-effort cleanup only.
|
|
58
|
+
}
|
|
59
|
+
}));
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
await visit(rootDir);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export async function createRawStreamLog(options: RawStreamLogOptions): Promise<RawStreamLogHandle | null> {
|
|
66
|
+
if (!options.enabled) return null;
|
|
67
|
+
|
|
68
|
+
const maxBytes = Math.max(0, Math.floor(options.maxBytesPerTurn));
|
|
69
|
+
const tool = sanitizeLogPathSegment(options.tool);
|
|
70
|
+
const session = sanitizeLogPathSegment(options.sessionId);
|
|
71
|
+
const label = sanitizeLogPathSegment(options.label);
|
|
72
|
+
const dir = join(options.rootDir, tool, session);
|
|
73
|
+
await mkdir(dir, { recursive: true });
|
|
74
|
+
void cleanupOldRawStreamLogs(join(options.rootDir, tool), options.retentionDays);
|
|
75
|
+
|
|
76
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
77
|
+
const filePath = join(dir, `${timestamp}-${label}.jsonl.gz`);
|
|
78
|
+
const output = createWriteStream(filePath);
|
|
79
|
+
const gzip = createGzip();
|
|
80
|
+
gzip.pipe(output);
|
|
81
|
+
|
|
82
|
+
let bytes = 0;
|
|
83
|
+
let truncated = false;
|
|
84
|
+
let ended = false;
|
|
85
|
+
|
|
86
|
+
const writeLine = (line: string): void => {
|
|
87
|
+
if (ended || truncated) return;
|
|
88
|
+
const payload = `${line}\n`;
|
|
89
|
+
const payloadBytes = Buffer.byteLength(payload, "utf-8");
|
|
90
|
+
if (maxBytes > 0 && bytes + payloadBytes > maxBytes) {
|
|
91
|
+
const marker = JSON.stringify({
|
|
92
|
+
type: "deepccc_raw_stream_log_truncated",
|
|
93
|
+
reason: "max_bytes_per_turn_exceeded",
|
|
94
|
+
maxBytesPerTurn: maxBytes,
|
|
95
|
+
writtenBytes: bytes,
|
|
96
|
+
});
|
|
97
|
+
gzip.write(`${marker}\n`);
|
|
98
|
+
truncated = true;
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
bytes += payloadBytes;
|
|
102
|
+
gzip.write(payload);
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const close = async ({ keep }: { keep: boolean }): Promise<void> => {
|
|
106
|
+
if (ended) return;
|
|
107
|
+
ended = true;
|
|
108
|
+
gzip.end();
|
|
109
|
+
try {
|
|
110
|
+
await once(output, "finish");
|
|
111
|
+
} catch {
|
|
112
|
+
// Ignore close errors; caller cannot recover from debug log failures.
|
|
113
|
+
}
|
|
114
|
+
if (!keep) {
|
|
115
|
+
try {
|
|
116
|
+
await unlink(filePath);
|
|
117
|
+
} catch {
|
|
118
|
+
// Best-effort cleanup only.
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
return { filePath, writeLine, close };
|
|
124
|
+
}
|