@armadra/agent 0.7.0 → 0.7.1
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 +8 -0
- package/CHANGELOG.zh-CN.md +7 -0
- package/dist/auth/oauth/token-store.d.ts +2 -0
- package/dist/auth/oauth/token-store.js +7 -2
- package/dist/bundle/ama.cjs +42 -5
- package/dist/checkpoints/shadow-git.js +2 -1
- package/dist/config/auth-file.js +10 -2
- package/dist/config/fs-retry.d.ts +18 -0
- package/dist/config/fs-retry.js +33 -0
- package/docs/en/sessions.md +1 -1
- package/docs/sessions.md +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,14 @@ English · [简体中文](CHANGELOG.zh-CN.md)
|
|
|
5
5
|
> This file is in English starting with 0.6.0. Release notes for 0.1 through 0.5.1 are in Chinese in
|
|
6
6
|
> [CHANGELOG.zh-CN.md](CHANGELOG.zh-CN.md). New entries go into both files.
|
|
7
7
|
|
|
8
|
+
## 0.7.1 (2026-10-05)
|
|
9
|
+
|
|
10
|
+
- **Windows: concurrent OAuth refresh**: while one ama process releases the `auth.json.lock`, another one opening it
|
|
11
|
+
got EPERM (the file is "delete pending" on NTFS) and failed the refresh; it now keeps waiting for the lock. Replacing
|
|
12
|
+
or reading `auth.json` while another process has it open retries briefly on EPERM / EACCES / EBUSY (Windows only).
|
|
13
|
+
- **Checkpoints (shadow-git)**: the 3-second snapshot budget no longer includes creating the shadow repository the first
|
|
14
|
+
time (a dozen `git` processes, several seconds on Windows), which could downgrade a session to `tools` on its first turn.
|
|
15
|
+
|
|
8
16
|
## 0.7.0 (2026-10-04)
|
|
9
17
|
|
|
10
18
|
ACP completion: `ama --mode acp` as an agent for editors (Zed and other ACP clients) and `AcpClient` / `AcpDriver` as a
|
package/CHANGELOG.zh-CN.md
CHANGED
|
@@ -4,6 +4,13 @@
|
|
|
4
4
|
|
|
5
5
|
> 从 0.6.0 起 [CHANGELOG.md](CHANGELOG.md) 为英文,本文件保留中文记录(0.1–0.5.1 的完整历史在此)。新条目两份都要加。
|
|
6
6
|
|
|
7
|
+
## 0.7.1(2026-10-05)
|
|
8
|
+
|
|
9
|
+
- **Windows:并发刷新 OAuth**:一个 ama 进程刚释放 `auth.json.lock` 时,另一个进程打开它会报 EPERM(NTFS 上文件处于删除挂起),刷新
|
|
10
|
+
直接失败;现在当作锁被占用继续等。别的进程正打开 `auth.json` 时的覆盖 / 读取遇到 EPERM / EACCES / EBUSY 短暂重试(只在 Windows)。
|
|
11
|
+
- **检查点(影子 git)**:3 秒的快照耗时上限不再算上第一次建影子仓库(十来个 `git` 子进程,Windows 上要好几秒),以前可能第一回合就把
|
|
12
|
+
会话降级为 `tools`。
|
|
13
|
+
|
|
7
14
|
## 0.7.0(2026-10-04)
|
|
8
15
|
|
|
9
16
|
ACP 补全:`ama --mode acp` 作为编辑器(Zed 等 ACP 客户端)的 Agent,`AcpClient` / `AcpDriver` 作为客户端,仓库内对照官方 ACP v1
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
* - 写:读-改-写 `auth.json` 整个文件,经 `writeAuthFile`(同目录临时文件 0600 → rename → chmod),其它条目原样保留;
|
|
7
7
|
* - 锁:`<auth.json>.lock`,`open(O_CREAT|O_EXCL)`(flag `wx`)写 `{ pid, at }`;等待 ≤ 15 s;锁文件超过 60 s 且
|
|
8
8
|
* pid 已死视为陈旧,删除后重试。取锁后由调用方**重读文件**再决定要不要刷新。
|
|
9
|
+
* - Windows:锁文件删除挂起时 `open(wx)` 报 EPERM / EACCES,按「被占用」继续等;读 / rename 的同类瞬时错误
|
|
10
|
+
* 短暂重试(`config/fs-retry.ts`)。
|
|
9
11
|
*/
|
|
10
12
|
import { type OAuthAuthEntry } from "../../config/types-w6.js";
|
|
11
13
|
export declare const LOCK_WAIT_MS = 15000;
|
|
@@ -6,10 +6,13 @@
|
|
|
6
6
|
* - 写:读-改-写 `auth.json` 整个文件,经 `writeAuthFile`(同目录临时文件 0600 → rename → chmod),其它条目原样保留;
|
|
7
7
|
* - 锁:`<auth.json>.lock`,`open(O_CREAT|O_EXCL)`(flag `wx`)写 `{ pid, at }`;等待 ≤ 15 s;锁文件超过 60 s 且
|
|
8
8
|
* pid 已死视为陈旧,删除后重试。取锁后由调用方**重读文件**再决定要不要刷新。
|
|
9
|
+
* - Windows:锁文件删除挂起时 `open(wx)` 报 EPERM / EACCES,按「被占用」继续等;读 / rename 的同类瞬时错误
|
|
10
|
+
* 短暂重试(`config/fs-retry.ts`)。
|
|
9
11
|
*/
|
|
10
12
|
import { closeSync, mkdirSync, openSync, readFileSync, statSync, unlinkSync, writeSync, } from "node:fs";
|
|
11
13
|
import { dirname } from "node:path";
|
|
12
14
|
import { writeAuthFile } from "../../config/auth-file.js";
|
|
15
|
+
import { isTransientFsError, retryTransientFs } from "../../config/fs-retry.js";
|
|
13
16
|
import { CONFIG_FILE_VERSION } from "../../config/types.js";
|
|
14
17
|
import { isOAuthEntry } from "../../config/types-w6.js";
|
|
15
18
|
import { AmaError } from "../../errors.js";
|
|
@@ -18,7 +21,7 @@ export const LOCK_STALE_MS = 60_000;
|
|
|
18
21
|
const LOCK_POLL_MS = 50;
|
|
19
22
|
function readRaw(path) {
|
|
20
23
|
try {
|
|
21
|
-
const value = JSON.parse(readFileSync(path, "utf8"));
|
|
24
|
+
const value = JSON.parse(retryTransientFs(() => readFileSync(path, "utf8")));
|
|
22
25
|
return typeof value === "object" && value !== null && typeof value.providers === "object"
|
|
23
26
|
? value
|
|
24
27
|
: undefined;
|
|
@@ -85,7 +88,9 @@ function tryAcquire(path) {
|
|
|
85
88
|
return true;
|
|
86
89
|
}
|
|
87
90
|
catch (error) {
|
|
88
|
-
|
|
91
|
+
const code = error.code;
|
|
92
|
+
// Windows:上一个持锁进程刚删掉锁文件(删除挂起)时 open(wx) 报 EPERM / EACCES——同样是「被占用」
|
|
93
|
+
if (code === "EEXIST" || isTransientFsError(code))
|
|
89
94
|
return false;
|
|
90
95
|
throw error;
|
|
91
96
|
}
|
package/dist/bundle/ama.cjs
CHANGED
|
@@ -41,7 +41,7 @@ var AMA_VERSION;
|
|
|
41
41
|
var init_version = __esm({
|
|
42
42
|
"src/version.ts"() {
|
|
43
43
|
"use strict";
|
|
44
|
-
AMA_VERSION = true ? "0.7.
|
|
44
|
+
AMA_VERSION = true ? "0.7.1" : readPackageVersion();
|
|
45
45
|
}
|
|
46
46
|
});
|
|
47
47
|
|
|
@@ -6432,6 +6432,35 @@ var init_args = __esm({
|
|
|
6432
6432
|
}
|
|
6433
6433
|
});
|
|
6434
6434
|
|
|
6435
|
+
// src/config/fs-retry.ts
|
|
6436
|
+
function isTransientFsError(code, platform = process.platform) {
|
|
6437
|
+
return platform === "win32" && typeof code === "string" && TRANSIENT_WIN32.has(code);
|
|
6438
|
+
}
|
|
6439
|
+
function sleepSync(ms) {
|
|
6440
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
6441
|
+
}
|
|
6442
|
+
function retryTransientFs(fn, options = {}) {
|
|
6443
|
+
const attempts2 = options.attempts ?? 8;
|
|
6444
|
+
const delayMs = options.delayMs ?? 20;
|
|
6445
|
+
const sleep6 = options.sleep ?? sleepSync;
|
|
6446
|
+
for (let n = 1; ; n++) {
|
|
6447
|
+
try {
|
|
6448
|
+
return fn();
|
|
6449
|
+
} catch (error2) {
|
|
6450
|
+
const code = error2.code;
|
|
6451
|
+
if (n >= attempts2 || !isTransientFsError(code, options.platform)) throw error2;
|
|
6452
|
+
sleep6(delayMs * n);
|
|
6453
|
+
}
|
|
6454
|
+
}
|
|
6455
|
+
}
|
|
6456
|
+
var TRANSIENT_WIN32;
|
|
6457
|
+
var init_fs_retry = __esm({
|
|
6458
|
+
"src/config/fs-retry.ts"() {
|
|
6459
|
+
"use strict";
|
|
6460
|
+
TRANSIENT_WIN32 = /* @__PURE__ */ new Set(["EPERM", "EACCES", "EBUSY"]);
|
|
6461
|
+
}
|
|
6462
|
+
});
|
|
6463
|
+
|
|
6435
6464
|
// src/config/checker.ts
|
|
6436
6465
|
function join(path, key) {
|
|
6437
6466
|
if (typeof key === "number") return `${path}[${key}]`;
|
|
@@ -7745,7 +7774,12 @@ function writeAuthFile(path, file) {
|
|
|
7745
7774
|
const tmp = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
7746
7775
|
(0, import_node_fs3.writeFileSync)(tmp, `${JSON.stringify(file, null, 2)}
|
|
7747
7776
|
`, { mode: 384 });
|
|
7748
|
-
|
|
7777
|
+
try {
|
|
7778
|
+
retryTransientFs(() => (0, import_node_fs3.renameSync)(tmp, path));
|
|
7779
|
+
} catch (error2) {
|
|
7780
|
+
(0, import_node_fs3.rmSync)(tmp, { force: true });
|
|
7781
|
+
throw error2;
|
|
7782
|
+
}
|
|
7749
7783
|
if (process.platform !== "win32") (0, import_node_fs3.chmodSync)(path, 384);
|
|
7750
7784
|
}
|
|
7751
7785
|
function setAuthKey(path, provider, apiKey) {
|
|
@@ -7796,6 +7830,7 @@ var init_auth_file = __esm({
|
|
|
7796
7830
|
"src/config/auth-file.ts"() {
|
|
7797
7831
|
"use strict";
|
|
7798
7832
|
import_node_fs3 = require("node:fs");
|
|
7833
|
+
init_fs_retry();
|
|
7799
7834
|
import_node_path4 = require("node:path");
|
|
7800
7835
|
init_load();
|
|
7801
7836
|
init_paths();
|
|
@@ -28826,8 +28861,8 @@ var init_shadow_git = __esm({
|
|
|
28826
28861
|
/** 新回合快照:返回提交 id 与是否该降级。 */
|
|
28827
28862
|
snapshot(parent, message) {
|
|
28828
28863
|
return exclusive(this.dir, async () => {
|
|
28829
|
-
const started = this.now();
|
|
28830
28864
|
await this.init();
|
|
28865
|
+
const started = this.now();
|
|
28831
28866
|
await this.writeExcludes();
|
|
28832
28867
|
if (!this.counted) {
|
|
28833
28868
|
const out = await this.run([
|
|
@@ -37672,7 +37707,7 @@ var init_bootstrap = __esm({
|
|
|
37672
37707
|
// src/auth/oauth/token-store.ts
|
|
37673
37708
|
function readRaw(path) {
|
|
37674
37709
|
try {
|
|
37675
|
-
const value = JSON.parse((0, import_node_fs43.readFileSync)(path, "utf8"));
|
|
37710
|
+
const value = JSON.parse(retryTransientFs(() => (0, import_node_fs43.readFileSync)(path, "utf8")));
|
|
37676
37711
|
return typeof value === "object" && value !== null && typeof value.providers === "object" ? value : void 0;
|
|
37677
37712
|
} catch {
|
|
37678
37713
|
return void 0;
|
|
@@ -37726,7 +37761,8 @@ function tryAcquire(path) {
|
|
|
37726
37761
|
}
|
|
37727
37762
|
return true;
|
|
37728
37763
|
} catch (error2) {
|
|
37729
|
-
|
|
37764
|
+
const code = error2.code;
|
|
37765
|
+
if (code === "EEXIST" || isTransientFsError(code)) return false;
|
|
37730
37766
|
throw error2;
|
|
37731
37767
|
}
|
|
37732
37768
|
}
|
|
@@ -37765,6 +37801,7 @@ var init_token_store = __esm({
|
|
|
37765
37801
|
import_node_fs43 = require("node:fs");
|
|
37766
37802
|
import_node_path66 = require("node:path");
|
|
37767
37803
|
init_auth_file();
|
|
37804
|
+
init_fs_retry();
|
|
37768
37805
|
init_types2();
|
|
37769
37806
|
init_types_w6();
|
|
37770
37807
|
init_errors();
|
|
@@ -204,8 +204,9 @@ export class ShadowRepo {
|
|
|
204
204
|
/** 新回合快照:返回提交 id 与是否该降级。 */
|
|
205
205
|
snapshot(parent, message) {
|
|
206
206
|
return exclusive(this.dir, async () => {
|
|
207
|
-
const started = this.now();
|
|
208
207
|
await this.init();
|
|
208
|
+
// 计时不含一次性的建仓与本地配置(十来个 git 子进程,Windows 上可达数秒),只量快照本身
|
|
209
|
+
const started = this.now();
|
|
209
210
|
await this.writeExcludes();
|
|
210
211
|
if (!this.counted) {
|
|
211
212
|
const out = await this.run([
|
package/dist/config/auth-file.js
CHANGED
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
* - 本模块只搬运字符串,不解析 `!command` / `$ENV`(那是 ai/providers/auth.ts 的事),
|
|
7
7
|
* 也不把 key 写进任何日志或返回给展示层(`describeAuthFile` 只给来源与形态)。
|
|
8
8
|
*/
|
|
9
|
-
import { chmodSync, mkdirSync, renameSync, statSync, writeFileSync } from "node:fs";
|
|
9
|
+
import { chmodSync, mkdirSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
10
|
+
import { retryTransientFs } from "./fs-retry.js";
|
|
10
11
|
import { dirname, join } from "node:path";
|
|
11
12
|
import { loadConfigFile } from "./load.js";
|
|
12
13
|
import { AUTH_FILE } from "./paths.js";
|
|
@@ -54,7 +55,14 @@ export function writeAuthFile(path, file) {
|
|
|
54
55
|
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
55
56
|
const tmp = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
56
57
|
writeFileSync(tmp, `${JSON.stringify(file, null, 2)}\n`, { mode: 0o600 });
|
|
57
|
-
|
|
58
|
+
try {
|
|
59
|
+
// Windows:别的 ama 进程正读着 auth.json 时覆盖它会暂时失败
|
|
60
|
+
retryTransientFs(() => renameSync(tmp, path));
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
rmSync(tmp, { force: true });
|
|
64
|
+
throw error;
|
|
65
|
+
}
|
|
58
66
|
if (process.platform !== "win32")
|
|
59
67
|
chmodSync(path, 0o600);
|
|
60
68
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Windows 上的暂时性文件错误(多个 ama 进程同时读写 auth.json 时)。
|
|
3
|
+
*
|
|
4
|
+
* NTFS 上文件被删除后、最后一个句柄关闭前处于「删除挂起」:此时 `open(wx)` 报 EPERM 而不是 EEXIST;
|
|
5
|
+
* 目标文件正被别的进程读时 `rename` 覆盖它会报 EPERM / EACCES / EBUSY。这些都是瞬时的,稍等重试即可。
|
|
6
|
+
* 其它平台上同样的错误码是真的权限问题,原样抛出。
|
|
7
|
+
*/
|
|
8
|
+
/** 这个错误码在当前平台上是不是「别的进程正占着,稍后再试」。 */
|
|
9
|
+
export declare function isTransientFsError(code: unknown, platform?: string): boolean;
|
|
10
|
+
export interface FsRetryOptions {
|
|
11
|
+
platform?: string;
|
|
12
|
+
attempts?: number;
|
|
13
|
+
/** 第 n 次重试前等 `delayMs * n`。 */
|
|
14
|
+
delayMs?: number;
|
|
15
|
+
sleep?: (ms: number) => void;
|
|
16
|
+
}
|
|
17
|
+
/** 执行 `fn`;Windows 的暂时性错误按递增间隔重试(缺省 8 次,共约 0.7 s),其它错误立即抛出。 */
|
|
18
|
+
export declare function retryTransientFs<T>(fn: () => T, options?: FsRetryOptions): T;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Windows 上的暂时性文件错误(多个 ama 进程同时读写 auth.json 时)。
|
|
3
|
+
*
|
|
4
|
+
* NTFS 上文件被删除后、最后一个句柄关闭前处于「删除挂起」:此时 `open(wx)` 报 EPERM 而不是 EEXIST;
|
|
5
|
+
* 目标文件正被别的进程读时 `rename` 覆盖它会报 EPERM / EACCES / EBUSY。这些都是瞬时的,稍等重试即可。
|
|
6
|
+
* 其它平台上同样的错误码是真的权限问题,原样抛出。
|
|
7
|
+
*/
|
|
8
|
+
const TRANSIENT_WIN32 = new Set(["EPERM", "EACCES", "EBUSY"]);
|
|
9
|
+
/** 这个错误码在当前平台上是不是「别的进程正占着,稍后再试」。 */
|
|
10
|
+
export function isTransientFsError(code, platform = process.platform) {
|
|
11
|
+
return platform === "win32" && typeof code === "string" && TRANSIENT_WIN32.has(code);
|
|
12
|
+
}
|
|
13
|
+
/** 同步睡眠(只用在重试之间,最长几十毫秒)。 */
|
|
14
|
+
function sleepSync(ms) {
|
|
15
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
16
|
+
}
|
|
17
|
+
/** 执行 `fn`;Windows 的暂时性错误按递增间隔重试(缺省 8 次,共约 0.7 s),其它错误立即抛出。 */
|
|
18
|
+
export function retryTransientFs(fn, options = {}) {
|
|
19
|
+
const attempts = options.attempts ?? 8;
|
|
20
|
+
const delayMs = options.delayMs ?? 20;
|
|
21
|
+
const sleep = options.sleep ?? sleepSync;
|
|
22
|
+
for (let n = 1;; n++) {
|
|
23
|
+
try {
|
|
24
|
+
return fn();
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
const code = error.code;
|
|
28
|
+
if (n >= attempts || !isTransientFsError(code, options.platform))
|
|
29
|
+
throw error;
|
|
30
|
+
sleep(delayMs * n);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
package/docs/en/sessions.md
CHANGED
|
@@ -178,7 +178,7 @@ Rolling back code (`/rewind`, design in [rewind-plan.md](../rewind-plan.md), Chi
|
|
|
178
178
|
- **Snapshots**: at the start of a new turn, `git add -A` + `write-tree` + `commit-tree` (parent: the previous shadow commit), with the commit id recorded as the checkpoint's `shadowCommit`. The shadow repository uses a fixed identity and empty global / system config (your signing, hooks, filters and templates are not read), `core.autocrlf=false` with line-ending conversion off, so raw disk bytes are stored; `gc.auto=0`.
|
|
179
179
|
- **Ignores**: `.gitignore` in the working directory applies; when the working directory is inside a git repository, paths your repository ignores (parent `.gitignore` files, `info/exclude`, the global ignore file) stay out of the shadow repository too; `.git` is always excluded.
|
|
180
180
|
- **Restore**: the current working directory is written as a tree and compared with the target commit, so only differing files are processed; conflict and safety checks are the same as in `tools` (symbolic links, hard links, non-regular files and directories on the path replaced by links are skipped). The "known" current content = what is in the latest shadow snapshot, what ama last wrote or what the latest checkpoint recorded; anything else counts as a change outside the turn and is skipped by default. Ignored files are left alone; files changed by edit / write but not in the shadow repository (outside the working directory, ignored) restore from the `tools` records. When the target checkpoint has no shadow commit (after a downgrade, or the shadow repository was deleted) the whole restore follows `tools`.
|
|
181
|
-
- **Guards**: in these cases the session downgrades to `tools` with a one-time notice: `git` is not on PATH; the working directory (ignored files excluded) has more than 20 000 files (checked before the first snapshot); a single snapshot takes longer than 3 seconds (that commit is kept). It is not enabled when the working directory is the home directory or the file-system root.
|
|
181
|
+
- **Guards**: in these cases the session downgrades to `tools` with a one-time notice: `git` is not on PATH; the working directory (ignored files excluded) has more than 20 000 files (checked before the first snapshot); a single snapshot takes longer than 3 seconds (that commit is kept; creating the shadow repository the first time does not count). It is not enabled when the working directory is the home directory or the file-system root.
|
|
182
182
|
- **Limitations**:
|
|
183
183
|
- bash changes in the latest turn enter a snapshot only when the next turn starts; rolling back before that, they cannot be told apart from manual changes and count as conflicts (overwriting is an option).
|
|
184
184
|
- git only records the executable bit: restore only adjusts the executable bit and keeps other permission bits; symbolic links and submodules are not restored.
|
package/docs/sessions.md
CHANGED
|
@@ -160,7 +160,7 @@ ama sessions trace <id|文件> [--html [文件]] [--json] [--output <文件>] [-
|
|
|
160
160
|
- **快照**:新回合开始时 `git add -A` + `write-tree` + `commit-tree`(父为上一个影子提交),提交 id 记进检查点的 `shadowCommit`。影子仓库用固定身份、空的全局 / 系统配置(不读你的签名、钩子、过滤器与模板),`core.autocrlf=false` 且关掉换行转换,存的是磁盘原字节;`gc.auto=0`。
|
|
161
161
|
- **忽略**:工作目录里的 `.gitignore` 生效;工作目录在 git 仓库里时,你的仓库判定为忽略的路径(上级目录的 `.gitignore`、`info/exclude`、全局忽略文件)同样不进影子仓库;`.git` 一律排除。
|
|
162
162
|
- **恢复**:把当前工作目录写成树,与目标提交比较,只处理有差异的文件;冲突与安全检查与 `tools` 相同(符号链接、硬链接、非普通文件、路径上的目录被换成链接都跳过)。「已知」的当前内容 = 最近一个影子快照里的、ama 最后写入的或最近检查点记录的,其余视为回合外的改动,缺省跳过。被忽略的文件不碰;edit / write 改过、但不在影子仓库里的文件(工作目录外、被忽略)按 `tools` 记录恢复。目标检查点没有影子提交(降级之后、或影子仓库已删)时整次按 `tools` 恢复。
|
|
163
|
-
- **护栏**:以下情况本会话降级为 `tools` 并提示一次——PATH 里找不到 `git`;工作目录(不含忽略的)超过 20 000 个文件(第一次快照前检查);单次快照超过 3
|
|
163
|
+
- **护栏**:以下情况本会话降级为 `tools` 并提示一次——PATH 里找不到 `git`;工作目录(不含忽略的)超过 20 000 个文件(第一次快照前检查);单次快照超过 3 秒(这次的提交保留;不含第一次建影子仓库的时间)。工作目录是家目录或文件系统根目录时不启用。
|
|
164
164
|
- **限制**:
|
|
165
165
|
- 最近一个回合里 bash 的改动要到下一个回合开始才进快照;在那之前回滚,这些改动与手动修改分不开,算冲突(可选择覆盖)。
|
|
166
166
|
- git 只记可执行位:恢复时只调整可执行位,其它权限位保留;符号链接与子模块不恢复。
|