@qilin-agent/workspace-changes 3.1.7 → 3.1.8
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.i18n.yaml +2 -2
- package/README.md +1 -1
- package/README.zh.md +1 -1
- package/lib/index.js +38 -17
- package/lib/types/capture.d.ts +6 -5
- package/lib/types/capture.js +36 -13
- package/lib/types/recorder.d.ts +1 -1
- package/lib/types/recorder.js +12 -10
- package/package.json +18 -17
package/README.i18n.yaml
CHANGED
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
en: d152484eb41ac6b4
|
|
16
16
|
zh: 09388d293f9be9cb
|
|
17
17
|
/qilin-agent-workspace-changes/use-this-package:
|
|
18
|
-
en:
|
|
19
|
-
zh:
|
|
18
|
+
en: 9dcc7d2769c44ebe
|
|
19
|
+
zh: dce16dd03d8009df
|
|
20
20
|
/qilin-agent-workspace-changes/understand-the-implementation:
|
|
21
21
|
en: efb5cc9657ce290d
|
|
22
22
|
zh: b659836fb1fba36a
|
package/README.md
CHANGED
|
@@ -43,7 +43,7 @@ The shipped Web bundle mounts this plugin. Mount it in any composition with the
|
|
|
43
43
|
|
|
44
44
|
Every Session with a working directory and no subagent origin is recorded; subagent Sessions are not. Snapshots are written through a private index into a temporary object directory owned by the Session, with the repository's own object store attached as a read-only alternate; the repository's index, objects, work tree, and refs stay untouched, and the user's earlier uncommitted changes never enter a summary. Session disposal removes the directory. Nested repositories and submodules inside the working directory are recorded as gitlinks, so their internal changes do not appear. A working directory outside any git repository takes no snapshots. Without git — or, on macOS, with only the developer-tools stub at `/usr/bin/git` — no repository is located either, and the plugin logs that once. Either way the summary lists the file-tool edits alone, as described next, with the working directory as the workspace; shell edits are absent.
|
|
45
45
|
|
|
46
|
-
Before a `write`, `edit`,
|
|
46
|
+
Before a `write`, `edit`, mutating `str_replace_editor`, or `apply_patch` call runs, the recorder copies every file the call will address into the Session's temporary directory, once per path per turn, and copies it again at turn end; an `apply_patch` capture covers every operation path, including both ends of a move, and a syntactically invalid patch captures nothing; the copies are named by the SHA-1 of their bytes, so identical content is stored once. This needs no git. Paths the snapshots cover keep their git counts; the copies serve the other paths — files matching an ignore pattern, files outside the repository, and every file-tool edit when there is no snapshot — with counts from a line comparison of the two copies, so repeated edits to one file count once and a shell edit after a file-tool edit is included. A path whose content is unchanged at turn end is not listed. A copy larger than `maxFileBytes` is not stored: the file is listed with `oversized` and no counts, and a path whose both sides are that large is listed too, since unread content is never known to be unchanged. A path whose only difference is a missing final newline compares as unchanged, while git still counts that line. Files under `/tmp` or the platform temporary directory are excluded unless they lie inside the repository. Changes made only through shell commands outside the snapshot coverage are not recorded.
|
|
47
47
|
|
|
48
48
|
Each file carries a durable `path` — relative to the working directory inside it, absolute elsewhere — and a `display` path used for ordering and labels: the relative path, a `../` path for repository files above the working directory, a `~` path under the home directory, otherwise the absolute path. Files sort by `display` in code-unit order, which lists parent and absolute paths before the working directory's own files. The `workspace/changes` event carries only the turn number; `ctx.workspaceChanges.summary(sessionId, seq)` returns the summary the event with that sequence announced, or undefined once the Session is disposed or when this Host process never recorded it. `ctx.workspaceChanges.diff(sessionId, seq, index, signal)` compares the listed file at that index: hunks with three context lines from the two snapshot trees or the two copies, `binary` for a side git reported as binary or that holds a NUL byte, `oversized` for a side larger than `maxFileBytes`. A line comparison that runs longer than `diffTimeoutMs` degrades to one hunk replacing every line, marked `coarse`. A conversation reopened after a Host restart therefore has no card, and no comparison, for its earlier turns.
|
|
49
49
|
|
package/README.zh.md
CHANGED
|
@@ -43,7 +43,7 @@ kind: "package-reference"
|
|
|
43
43
|
|
|
44
44
|
有工作目录且不是子代理来源的 Session 都会被记录;子代理 Session 不记录。快照通过私有 index 写入 Session 自己拥有的临时对象目录,仓库自己的对象库以只读 alternate 的方式挂接;仓库的 index、对象、工作树和 ref 保持不变,用户此前未提交的改动也不会进入摘要。Session 释放时删除该目录。工作目录内的嵌套仓库和 submodule 记录为 gitlink,其内部改动不会出现。不在任何 git 仓库内的工作目录不做快照。没有 git 时——或者 macOS 上只有 `/usr/bin/git` 的开发者工具桩程序时——同样定位不到仓库,插件记录一次日志。两种情况下摘要都只列下文所述的文件工具编辑,并以工作目录作为工作区;shell 的改动不会出现。
|
|
45
45
|
|
|
46
|
-
在 `write`、`edit`
|
|
46
|
+
在 `write`、`edit`、有修改作用的 `str_replace_editor` 或 `apply_patch` 调用运行之前,记录器把该调用将要触及的每个文件复制到 Session 的临时目录,每轮每个路径只复制第一次,轮次结束时再复制一次;`apply_patch` 的捕获覆盖每个操作路径(含移动的两端),语法无效的补丁不捕获任何路径;副本按其字节的 SHA-1 命名,相同内容只存一份。这一步不需要 git。快照覆盖到的路径保留 git 的行数;其余路径由副本提供,也就是匹配忽略模式的文件、仓库之外的文件,以及没有快照时的每一次文件工具编辑,行数来自两份副本的逐行对比,因此同一文件的重复编辑只计一次,文件工具编辑之后的 shell 改动也包含在内。轮次结束时内容没有变化的路径不会列出。超过 `maxFileBytes` 的副本不会保存:该文件列出时带 `oversized`,没有行数;两侧都这么大的路径同样列出,因为没读过的内容永远不能认定为没有改动。只差一个末尾换行的路径对比为两侧相同,而 git 仍会把那一行计入行数。`/tmp` 与平台临时目录下的文件被排除,除非它们位于仓库内。快照覆盖范围之外只通过 shell 命令做出的改动不会被记录。
|
|
47
47
|
|
|
48
48
|
每个文件携带持久的 `path`——位于工作目录内时为相对路径,否则为绝对路径——以及用于排序和标签的 `display` 路径:相对路径,仓库内位于工作目录之上的文件为 `../` 路径,家目录下的文件为 `~` 路径,其余为绝对路径。文件按 `display` 的码元顺序排序,因此上级路径和绝对路径排在工作目录自身文件之前。`workspace/changes` 事件只携带轮号;`ctx.workspaceChanges.summary(sessionId, seq)` 返回该序号的事件宣告的摘要,Session 已释放或本 Host 进程从未记录时返回 undefined。`ctx.workspaceChanges.diff(sessionId, seq, index, signal)` 对比该下标所列的文件:从两棵快照树或两份副本得出带三行上下文的 hunk;git 报告为二进制或某一侧含 NUL 字节时返回 `binary`;某一侧超过 `maxFileBytes` 时返回 `oversized`。逐行对比运行超过 `diffTimeoutMs` 时退化为一个替换全部行的 hunk,并标记 `coarse`。因此 Host 重启后重新打开的对话,先前轮次既没有卡片也没有对比。
|
|
49
49
|
|
package/lib/index.js
CHANGED
|
@@ -3,6 +3,7 @@ import z from "@qilin-agent/schemastery";
|
|
|
3
3
|
import { copyFile, mkdir, mkdtemp, open, readFile, realpath, rm, writeFile } from "node:fs/promises";
|
|
4
4
|
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
5
5
|
import { createHash } from "node:crypto";
|
|
6
|
+
import { PatchParseError, parsePatch } from "@qilin-agent/fs-patch";
|
|
6
7
|
import { structuredPatch } from "diff";
|
|
7
8
|
//#region lib/types/numstat.js
|
|
8
9
|
/** Parsing of `git diff-tree --numstat -z` output. */
|
|
@@ -519,21 +520,39 @@ function text(value) {
|
|
|
519
520
|
return typeof value === "string" && value.trim() !== "" ? value : void 0;
|
|
520
521
|
}
|
|
521
522
|
/**
|
|
522
|
-
* The
|
|
523
|
-
*
|
|
524
|
-
*
|
|
523
|
+
* The paths a first-party file-tool call is about to mutate: `write`, `edit`,
|
|
524
|
+
* the mutating `str_replace_editor` commands, and every path a
|
|
525
|
+
* syntax-valid `apply_patch` addresses (both ends of a move). Other tools,
|
|
526
|
+
* reads, and incomplete arguments yield nothing.
|
|
525
527
|
* @param name - wire tool name.
|
|
526
528
|
* @param args - parsed call arguments.
|
|
527
|
-
* @returns the model-facing
|
|
529
|
+
* @returns the model-facing paths, empty when the call mutates no known path.
|
|
528
530
|
*/
|
|
529
|
-
function
|
|
530
|
-
if (typeof args !== "object" || args === null || Array.isArray(args)) return
|
|
531
|
+
function mutationPaths(name, args) {
|
|
532
|
+
if (typeof args !== "object" || args === null || Array.isArray(args)) return [];
|
|
531
533
|
const record = args;
|
|
532
534
|
switch (name) {
|
|
533
|
-
case "write":
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
535
|
+
case "write": {
|
|
536
|
+
const path = typeof record.content === "string" ? text(record.file_path) : void 0;
|
|
537
|
+
return path === void 0 ? [] : [path];
|
|
538
|
+
}
|
|
539
|
+
case "edit": {
|
|
540
|
+
const path = typeof record.old_string === "string" && typeof record.new_string === "string" ? text(record.file_path) : void 0;
|
|
541
|
+
return path === void 0 ? [] : [path];
|
|
542
|
+
}
|
|
543
|
+
case "str_replace_editor": {
|
|
544
|
+
const path = record.command === "create" || record.command === "str_replace" || record.command === "insert" ? text(record.path) : void 0;
|
|
545
|
+
return path === void 0 ? [] : [path];
|
|
546
|
+
}
|
|
547
|
+
case "apply_patch":
|
|
548
|
+
if (typeof record.input !== "string" || record.input.trim() === "") return [];
|
|
549
|
+
try {
|
|
550
|
+
return parsePatch(record.input).flatMap((operation) => operation.kind === "update" && operation.moveTo !== void 0 ? [operation.path, operation.moveTo] : [operation.path]);
|
|
551
|
+
} catch (error) {
|
|
552
|
+
if (error instanceof PatchParseError) return [];
|
|
553
|
+
throw error;
|
|
554
|
+
}
|
|
555
|
+
default: return [];
|
|
537
556
|
}
|
|
538
557
|
}
|
|
539
558
|
//#endregion
|
|
@@ -674,23 +693,25 @@ var TurnRecorder = class {
|
|
|
674
693
|
});
|
|
675
694
|
}
|
|
676
695
|
/**
|
|
677
|
-
* Queue the capture of
|
|
696
|
+
* Queue the capture of every path a file tool is about to mutate, before the
|
|
678
697
|
* tool runs; only the turn's first mutation of a path captures it. Await
|
|
679
698
|
* {@link settled} afterwards so the tool cannot overtake the capture.
|
|
680
699
|
* @param name - wire tool name.
|
|
681
700
|
* @param args - parsed call arguments.
|
|
682
701
|
*/
|
|
683
702
|
capture(name, args) {
|
|
684
|
-
const
|
|
685
|
-
if (
|
|
703
|
+
const targets = mutationPaths(name, args);
|
|
704
|
+
if (targets.length === 0) return;
|
|
686
705
|
const state = this.state;
|
|
687
706
|
this.enqueue(async () => {
|
|
688
707
|
const paths = this.paths;
|
|
689
708
|
if (paths === void 0) return;
|
|
690
|
-
const
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
709
|
+
for (const target of targets) {
|
|
710
|
+
const absolute = await canonicalPath(resolve(paths.cwd, target));
|
|
711
|
+
if (state.captures.has(absolute)) continue;
|
|
712
|
+
const capture = await captureFile(absolute, join(await this.scratchDir(), "captures"), this.env.maxFileBytes);
|
|
713
|
+
if (capture !== void 0) state.captures.set(absolute, capture);
|
|
714
|
+
}
|
|
694
715
|
});
|
|
695
716
|
}
|
|
696
717
|
/**
|
package/lib/types/capture.d.ts
CHANGED
|
@@ -36,12 +36,13 @@ export declare function captureFile(absolute: string, directory: string, maxByte
|
|
|
36
36
|
*/
|
|
37
37
|
export declare function sameCapture(a: Capture, b: Capture): boolean;
|
|
38
38
|
/**
|
|
39
|
-
* The
|
|
40
|
-
*
|
|
41
|
-
*
|
|
39
|
+
* The paths a first-party file-tool call is about to mutate: `write`, `edit`,
|
|
40
|
+
* the mutating `str_replace_editor` commands, and every path a
|
|
41
|
+
* syntax-valid `apply_patch` addresses (both ends of a move). Other tools,
|
|
42
|
+
* reads, and incomplete arguments yield nothing.
|
|
42
43
|
* @param name - wire tool name.
|
|
43
44
|
* @param args - parsed call arguments.
|
|
44
|
-
* @returns the model-facing
|
|
45
|
+
* @returns the model-facing paths, empty when the call mutates no known path.
|
|
45
46
|
*/
|
|
46
|
-
export declare function
|
|
47
|
+
export declare function mutationPaths(name: string, args: unknown): string[];
|
|
47
48
|
//# sourceMappingURL=capture.d.ts.map
|
package/lib/types/capture.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import { createHash } from 'node:crypto';
|
|
8
8
|
import { mkdir, open, writeFile } from 'node:fs/promises';
|
|
9
9
|
import { join } from 'node:path';
|
|
10
|
+
import { parsePatch, PatchParseError } from '@qilin-agent/fs-patch';
|
|
10
11
|
/** Bytes git inspects for a NUL byte before treating content as binary. */
|
|
11
12
|
const BINARY_PROBE_BYTES = 8000;
|
|
12
13
|
/** Whether a filesystem error names a missing path. */
|
|
@@ -82,26 +83,48 @@ function text(value) {
|
|
|
82
83
|
return typeof value === 'string' && value.trim() !== '' ? value : undefined;
|
|
83
84
|
}
|
|
84
85
|
/**
|
|
85
|
-
* The
|
|
86
|
-
*
|
|
87
|
-
*
|
|
86
|
+
* The paths a first-party file-tool call is about to mutate: `write`, `edit`,
|
|
87
|
+
* the mutating `str_replace_editor` commands, and every path a
|
|
88
|
+
* syntax-valid `apply_patch` addresses (both ends of a move). Other tools,
|
|
89
|
+
* reads, and incomplete arguments yield nothing.
|
|
88
90
|
* @param name - wire tool name.
|
|
89
91
|
* @param args - parsed call arguments.
|
|
90
|
-
* @returns the model-facing
|
|
92
|
+
* @returns the model-facing paths, empty when the call mutates no known path.
|
|
91
93
|
*/
|
|
92
|
-
export function
|
|
94
|
+
export function mutationPaths(name, args) {
|
|
93
95
|
if (typeof args !== 'object' || args === null || Array.isArray(args))
|
|
94
|
-
return
|
|
96
|
+
return [];
|
|
95
97
|
const record = args;
|
|
96
98
|
switch (name) {
|
|
97
|
-
case 'write':
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
case '
|
|
102
|
-
|
|
99
|
+
case 'write': {
|
|
100
|
+
const path = typeof record.content === 'string' ? text(record.file_path) : undefined;
|
|
101
|
+
return path === undefined ? [] : [path];
|
|
102
|
+
}
|
|
103
|
+
case 'edit': {
|
|
104
|
+
const path = typeof record.old_string === 'string' && typeof record.new_string === 'string' ? text(record.file_path) : undefined;
|
|
105
|
+
return path === undefined ? [] : [path];
|
|
106
|
+
}
|
|
107
|
+
case 'str_replace_editor': {
|
|
108
|
+
const path = record.command === 'create' || record.command === 'str_replace' || record.command === 'insert' ? text(record.path) : undefined;
|
|
109
|
+
return path === undefined ? [] : [path];
|
|
110
|
+
}
|
|
111
|
+
case 'apply_patch': {
|
|
112
|
+
if (typeof record.input !== 'string' || record.input.trim() === '')
|
|
113
|
+
return [];
|
|
114
|
+
try {
|
|
115
|
+
return parsePatch(record.input).flatMap(operation => operation.kind === 'update' && operation.moveTo !== undefined
|
|
116
|
+
? [operation.path, operation.moveTo]
|
|
117
|
+
: [operation.path]);
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
// A malformed patch changes nothing; the tool reports the syntax error itself.
|
|
121
|
+
if (error instanceof PatchParseError)
|
|
122
|
+
return [];
|
|
123
|
+
throw error;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
103
126
|
default:
|
|
104
|
-
return
|
|
127
|
+
return [];
|
|
105
128
|
}
|
|
106
129
|
}
|
|
107
130
|
//# sourceMappingURL=capture.js.map
|
package/lib/types/recorder.d.ts
CHANGED
|
@@ -50,7 +50,7 @@ export declare class TurnRecorder {
|
|
|
50
50
|
*/
|
|
51
51
|
start(turn: number): void;
|
|
52
52
|
/**
|
|
53
|
-
* Queue the capture of
|
|
53
|
+
* Queue the capture of every path a file tool is about to mutate, before the
|
|
54
54
|
* tool runs; only the turn's first mutation of a path captures it. Await
|
|
55
55
|
* {@link settled} afterwards so the tool cannot overtake the capture.
|
|
56
56
|
* @param name - wire tool name.
|
package/lib/types/recorder.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { mkdtemp, readFile, realpath, rm } from 'node:fs/promises';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
import { join, relative, resolve } from 'node:path';
|
|
5
|
-
import { captureFile,
|
|
5
|
+
import { captureFile, mutationPaths, sameCapture } from "./capture.js";
|
|
6
6
|
import { compareText } from "./compare.js";
|
|
7
7
|
import { blobText, diffTrees, gitlinkPaths, ignoredPaths, locateGitWorkspace, snapshotTree, treeBlob, } from "./git.js";
|
|
8
8
|
import { canonicalPath, compareDisplay, displayPathOf, durablePathOf, isInside, isTemporaryPath, temporaryRoots, toPosix } from "./paths.js";
|
|
@@ -67,27 +67,29 @@ export class TurnRecorder {
|
|
|
67
67
|
});
|
|
68
68
|
}
|
|
69
69
|
/**
|
|
70
|
-
* Queue the capture of
|
|
70
|
+
* Queue the capture of every path a file tool is about to mutate, before the
|
|
71
71
|
* tool runs; only the turn's first mutation of a path captures it. Await
|
|
72
72
|
* {@link settled} afterwards so the tool cannot overtake the capture.
|
|
73
73
|
* @param name - wire tool name.
|
|
74
74
|
* @param args - parsed call arguments.
|
|
75
75
|
*/
|
|
76
76
|
capture(name, args) {
|
|
77
|
-
const
|
|
78
|
-
if (
|
|
77
|
+
const targets = mutationPaths(name, args);
|
|
78
|
+
if (targets.length === 0)
|
|
79
79
|
return;
|
|
80
80
|
const state = this.state;
|
|
81
81
|
void this.enqueue(async () => {
|
|
82
82
|
const paths = this.paths;
|
|
83
83
|
if (paths === undefined)
|
|
84
84
|
return;
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
85
|
+
for (const target of targets) {
|
|
86
|
+
const absolute = await canonicalPath(resolve(paths.cwd, target));
|
|
87
|
+
if (state.captures.has(absolute))
|
|
88
|
+
continue;
|
|
89
|
+
const capture = await captureFile(absolute, join(await this.scratchDir(), 'captures'), this.env.maxFileBytes);
|
|
90
|
+
if (capture !== undefined)
|
|
91
|
+
state.captures.set(absolute, capture);
|
|
92
|
+
}
|
|
91
93
|
});
|
|
92
94
|
}
|
|
93
95
|
/**
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@qilin-agent/workspace-changes",
|
|
3
3
|
"description": "Per-turn workspace file changes recorded from git working-tree snapshots and whole-file captures, with per-file comparisons, for the QiLin",
|
|
4
|
-
"version": "3.1.
|
|
4
|
+
"version": "3.1.8",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -35,25 +35,26 @@
|
|
|
35
35
|
"lib/types/**/*.d.ts"
|
|
36
36
|
],
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"
|
|
39
|
-
"@qilin-agent/schemastery": "~3.18.
|
|
38
|
+
"@qilin-agent/fs-patch": "3.1.8",
|
|
39
|
+
"@qilin-agent/schemastery": "~3.18.6-alpha.2",
|
|
40
|
+
"diff": "^9.0.0"
|
|
40
41
|
},
|
|
41
42
|
"peerDependencies": {
|
|
42
|
-
"@qilin-agent/kylin": "~4.0.
|
|
43
|
-
"@qilin-agent/
|
|
44
|
-
"@qilin-agent/
|
|
45
|
-
"@qilin-agent/
|
|
46
|
-
"@qilin-agent/
|
|
43
|
+
"@qilin-agent/kylin": "~4.0.6-alpha.2",
|
|
44
|
+
"@qilin-agent/agent": "3.1.8",
|
|
45
|
+
"@qilin-agent/session": "3.1.8",
|
|
46
|
+
"@qilin-agent/subprocess": "3.1.8",
|
|
47
|
+
"@qilin-agent/tools": "3.1.8"
|
|
47
48
|
},
|
|
48
49
|
"devDependencies": {
|
|
49
|
-
"@qilin-agent/kylin": "~4.0.
|
|
50
|
-
"@qilin-agent/
|
|
51
|
-
"@qilin-agent/
|
|
52
|
-
"@qilin-agent/
|
|
53
|
-
"@qilin-agent/
|
|
54
|
-
"@qilin-agent/
|
|
55
|
-
"@qilin-agent/
|
|
56
|
-
"@qilin-agent/
|
|
57
|
-
"@qilin-agent/
|
|
50
|
+
"@qilin-agent/kylin": "~4.0.6-alpha.2",
|
|
51
|
+
"@qilin-agent/kylin-plugin-include": "~1.0.11-alpha.2",
|
|
52
|
+
"@qilin-agent/kylin-plugin-loader": "~1.0.7-alpha.2",
|
|
53
|
+
"@qilin-agent/agent": "3.1.8",
|
|
54
|
+
"@qilin-agent/session": "3.1.8",
|
|
55
|
+
"@qilin-agent/subprocess": "3.1.8",
|
|
56
|
+
"@qilin-agent/subprocess-local": "3.1.8",
|
|
57
|
+
"@qilin-agent/tools": "3.1.8",
|
|
58
|
+
"@qilin-agent/llm": "3.1.8"
|
|
58
59
|
}
|
|
59
60
|
}
|