@chatcode/chatcode-cli 3.0.4-rc.2 → 3.0.4-rc.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/README.i18n.yaml +2 -2
- package/README.md +2 -0
- package/README.zh.md +2 -0
- package/lib/{command-errors-CScdshCl.js → command-errors-BJ_ZWzUU.js} +12 -12
- package/lib/{command-trees-Dc17MYfV.js → command-trees-C9b7-bci.js} +2 -2
- package/lib/{component-identity-DFJD_d-P.js → component-identity-Jb1DYjke.js} +3 -3
- package/lib/{extension-events-qXmhzmHo.js → extension-events-DKX4LXOd.js} +20 -20
- package/lib/{host-access-B0CRAwsX.js → host-access-CMg6rsWt.js} +1 -1
- package/lib/{keymap-CIoVICBI.js → keymap-CrC4pSNH.js} +2 -2
- package/lib/{plugin-BKQsL2xn.js → plugin-CmyMCGfb.js} +350 -339
- package/lib/{scenes-BRf-zMpF.js → scenes-Cil9RnwU.js} +3 -3
- package/lib/{settings-sections-4gMhveN_.js → settings-sections-jVCqXwdH.js} +3 -3
- package/lib/{statusBarSettings-CnmdSHkO.js → statusBarSettings-DNoBH3Cq.js} +3 -3
- package/lib/{themes-Ce97Zsri.js → themes-BdgeY3It.js} +12 -12
- package/lib/tui-compat-command-trees.js +1 -1
- package/lib/tui-compat-extensions.js +3 -3
- package/lib/tui-compat-plugin-host.js +5 -5
- package/lib/tui-compat-scenes.js +1 -1
- package/lib/tui-compat-settings-sections.js +1 -1
- package/lib/tui-compat-working-activity.js +1 -1
- package/lib/tui-compat-workspaces.js +1 -1
- package/lib/tui-compat.js +5 -5
- package/lib/{workspaces-1xrm-e3V.js → workspaces-DOatJRYr.js} +2 -2
- package/package.json +28 -28
- package/runtime/manifest.json +2 -2
- package/runtime/node_modules/@deepseek-ai/dsh-app-boot/lib/index.js +2 -1
- package/runtime/node_modules/@deepseek-ai/dsh-atomic-write/lib/index.js +271 -51
- package/runtime/node_modules/@deepseek-ai/dsh-atomic-write/lib/types/index.d.ts +19 -7
- package/runtime/node_modules/@deepseek-ai/dsh-atomic-write/lib/types/linux-lock.d.ts +19 -0
- package/runtime/node_modules/@deepseek-ai/dsh-atomic-write/lib/types/linux-native.d.ts +12 -0
- package/runtime/node_modules/@deepseek-ai/dsh-atomic-write/lib/types/windows-lock.d.ts +7 -0
- package/runtime/node_modules/@deepseek-ai/dsh-atomic-write/package.json +4 -1
- package/runtime/node_modules/@deepseek-ai/dsh-client-ui-settings-account/lib/client.js +2 -2
- package/runtime/node_modules/@deepseek-ai/dsh-client-ui-settings-general/lib/client.js +1 -1
- package/runtime/node_modules/@deepseek-ai/dsh-client-ui-sidebar/lib/client.js +1 -3
- package/runtime/node_modules/@deepseek-ai/dsh-llm-deepseek/lib/types/adapter.d.ts +1 -1
- package/runtime/node_modules/@deepseek-ai/dsh-llm-deepseek/lib/types/config.d.ts +3 -3
- package/runtime/node_modules/@deepseek-ai/dsh-llm-deepseek-account/lib/types/config.d.ts +2 -2
- package/runtime/node_modules/@deepseek-ai/dsh-llm-deepseek-api-key/lib/types/config.d.ts +2 -2
- package/runtime/node_modules/@deepseek-ai/dsh-tool-cordis/lib/types/api-catalog.js +1 -1
- package/scripts/launch.mjs +8 -0
|
@@ -1,45 +1,252 @@
|
|
|
1
1
|
import { createHash, randomBytes } from "node:crypto";
|
|
2
|
-
import { lstat, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
3
|
-
import { dirname } from "node:path";
|
|
2
|
+
import { link, lstat, mkdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
3
|
+
import { dirname, win32 } from "node:path";
|
|
4
|
+
import { execFile } from "node:child_process";
|
|
5
|
+
import { promisify } from "node:util";
|
|
6
|
+
import { closeSync, constants, fstatSync, lstatSync, openSync, readFileSync } from "node:fs";
|
|
7
|
+
import { constants as constants$1 } from "node:os";
|
|
8
|
+
//#region lib/types/windows-lock.js
|
|
9
|
+
/** Exclusive Windows inspection for an empty or zero-filled lock left by a failed creator. */
|
|
10
|
+
const execute = promisify(execFile);
|
|
11
|
+
/**
|
|
12
|
+
* Read a small lock only when no other handle has the file open.
|
|
13
|
+
* @param path - lock path, protected by the caller's takeover claim.
|
|
14
|
+
* @returns its record, or undefined if exclusive access or inspection fails.
|
|
15
|
+
*/
|
|
16
|
+
async function readUnopenedWindowsLock(path) {
|
|
17
|
+
const powershell = win32.join(process.env.SystemRoot ?? "C:\\Windows", "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
|
|
18
|
+
const script = [
|
|
19
|
+
"$ErrorActionPreference = 'Stop'",
|
|
20
|
+
`$file = [IO.File]::Open('${path.replaceAll("'", "''")}', [IO.FileMode]::Open, [IO.FileAccess]::Read, [IO.FileShare]::None)`,
|
|
21
|
+
"try {",
|
|
22
|
+
" if ($file.Length -gt 64) { exit 1 }",
|
|
23
|
+
" $reader = [IO.StreamReader]::new($file, [Text.Encoding]::UTF8, $false)",
|
|
24
|
+
" try { [Console]::Out.Write([Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($reader.ReadToEnd()))) }",
|
|
25
|
+
" finally { $reader.Dispose() }",
|
|
26
|
+
"} finally { $file.Dispose() }"
|
|
27
|
+
].join("\n");
|
|
28
|
+
try {
|
|
29
|
+
const { stdout } = await execute(powershell, [
|
|
30
|
+
"-NoLogo",
|
|
31
|
+
"-NoProfile",
|
|
32
|
+
"-NonInteractive",
|
|
33
|
+
"-EncodedCommand",
|
|
34
|
+
Buffer.from(script, "utf16le").toString("base64")
|
|
35
|
+
], {
|
|
36
|
+
encoding: "utf8",
|
|
37
|
+
windowsHide: true,
|
|
38
|
+
timeout: 2e3,
|
|
39
|
+
maxBuffer: 4096,
|
|
40
|
+
env: Object.fromEntries(Object.entries(process.env).filter(([name]) => !/KEY|SECRET|TOKEN|PASSWORD/i.test(name)))
|
|
41
|
+
});
|
|
42
|
+
const value = stdout.trim();
|
|
43
|
+
const bytes = Buffer.from(value, "base64");
|
|
44
|
+
if (bytes.length > 64 || bytes.toString("base64") !== value) return void 0;
|
|
45
|
+
return bytes.toString("utf8");
|
|
46
|
+
} catch (error) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
//#endregion
|
|
51
|
+
//#region lib/types/linux-native.js
|
|
52
|
+
/** Lazy Linux file-lease inspection for incomplete legacy writer records. */
|
|
53
|
+
let bindings;
|
|
54
|
+
const F_SETOWN = 8;
|
|
55
|
+
const F_SETSIG = 10;
|
|
56
|
+
const F_SETLEASE = 1024;
|
|
57
|
+
const F_GETLEASE = 1025;
|
|
58
|
+
const F_RDLCK = 0;
|
|
59
|
+
const SC_CLK_TCK = 2;
|
|
60
|
+
async function linux() {
|
|
61
|
+
return bindings ??= (async () => {
|
|
62
|
+
const libc = (await import("koffi")).default.load(null);
|
|
63
|
+
const fcntl = libc.func("fcntl", "int", [
|
|
64
|
+
"int",
|
|
65
|
+
"int",
|
|
66
|
+
"int"
|
|
67
|
+
]);
|
|
68
|
+
const sysconf = libc.func("sysconf", "long", ["int"]);
|
|
69
|
+
return {
|
|
70
|
+
fcntl,
|
|
71
|
+
clockTicks: () => sysconf(SC_CLK_TCK)
|
|
72
|
+
};
|
|
73
|
+
})();
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Read the Linux userspace clock-tick rate used by /proc process start times.
|
|
77
|
+
* @returns ticks per second, or undefined when libc cannot supply the rate.
|
|
78
|
+
*/
|
|
79
|
+
async function linuxClockTicks() {
|
|
80
|
+
try {
|
|
81
|
+
const value = (await linux()).clockTicks();
|
|
82
|
+
return Number.isSafeInteger(value) && value > 0 ? value : void 0;
|
|
83
|
+
} catch (error) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Read a small regular lock only after the kernel excludes open writers.
|
|
89
|
+
* @param path - existing lock path protected by the caller's takeover claim.
|
|
90
|
+
* @returns the unchanged inode's contents, or undefined when inspection is inconclusive.
|
|
91
|
+
*/
|
|
92
|
+
async function readClosedLinuxLock(path) {
|
|
93
|
+
let fd;
|
|
94
|
+
try {
|
|
95
|
+
const api = await linux();
|
|
96
|
+
fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
97
|
+
const held = fstatSync(fd);
|
|
98
|
+
if (!held.isFile() || held.size > 128) return void 0;
|
|
99
|
+
if (api.fcntl(fd, F_SETSIG, constants$1.signals.SIGURG) !== 0) return void 0;
|
|
100
|
+
if (api.fcntl(fd, F_SETLEASE, F_RDLCK) !== 0) return void 0;
|
|
101
|
+
if (api.fcntl(fd, F_SETOWN, 0) !== 0) return void 0;
|
|
102
|
+
const record = readFileSync(fd, "utf8");
|
|
103
|
+
const current = lstatSync(path);
|
|
104
|
+
if (current.dev !== held.dev || current.ino !== held.ino || api.fcntl(fd, F_GETLEASE, 0) !== F_RDLCK) return void 0;
|
|
105
|
+
return record;
|
|
106
|
+
} catch (error) {
|
|
107
|
+
return;
|
|
108
|
+
} finally {
|
|
109
|
+
if (fd !== void 0) closeSync(fd);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
//#endregion
|
|
113
|
+
//#region lib/types/linux-lock.js
|
|
114
|
+
/** Linux writer identity and complete-record publication; other platforms retain PID records. */
|
|
115
|
+
const RECORD = /^(\d+)\r?\n(?:linux ([a-f0-9-]{36}) (\d+) ([a-f0-9]{24})\n)?$/u;
|
|
116
|
+
let ownIdentity;
|
|
117
|
+
/** Read kernel identity without treating inaccessible procfs as evidence of an exited writer. */
|
|
118
|
+
async function identity(pid) {
|
|
119
|
+
try {
|
|
120
|
+
const [boot, stat] = await Promise.all([readFile("/proc/sys/kernel/random/boot_id", "utf8"), readFile(`/proc/${pid}/stat`, "utf8")]);
|
|
121
|
+
const end = stat.lastIndexOf(")");
|
|
122
|
+
const fields = end < 0 ? [] : stat.slice(end + 2).trim().split(/\s+/u);
|
|
123
|
+
const start = fields[19];
|
|
124
|
+
if (!/^[a-f0-9-]{36}$/u.test(boot.trim()) || start === void 0 || !/^\d+$/u.test(start)) return void 0;
|
|
125
|
+
return {
|
|
126
|
+
id: `${boot.trim()} ${start}`,
|
|
127
|
+
start: Number(start),
|
|
128
|
+
state: fields[0] ?? ""
|
|
129
|
+
};
|
|
130
|
+
} catch (error) {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Render a new acquisition's record; Linux adds boot identity, start ticks, and a unique claim token.
|
|
136
|
+
* @returns the PID record, with Linux identity when the kernel exposes it.
|
|
137
|
+
*/
|
|
138
|
+
async function writerLockRecord() {
|
|
139
|
+
const pid = `${process.pid}\n`;
|
|
140
|
+
if (process.platform !== "linux") return pid;
|
|
141
|
+
const current = await (ownIdentity ??= identity(process.pid).then((value) => value?.id));
|
|
142
|
+
return current === void 0 ? pid : `${pid}linux ${current} ${randomBytes(12).toString("hex")}\n`;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Exclusively publish a complete Linux identity record, or create a legacy PID lock.
|
|
146
|
+
* @param path - lock sibling in an existing parent directory.
|
|
147
|
+
* @param record - one acquisition's record from writerLockRecord.
|
|
148
|
+
*/
|
|
149
|
+
async function createWriterLock(path, record) {
|
|
150
|
+
const nonce = RECORD.exec(record)?.[4];
|
|
151
|
+
if (process.platform !== "linux" || nonce === void 0) {
|
|
152
|
+
await writeFile(path, record, {
|
|
153
|
+
mode: 384,
|
|
154
|
+
flag: "wx"
|
|
155
|
+
});
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
const temp = `${path}.${randomBytes(12).toString("hex")}.tmp`;
|
|
159
|
+
try {
|
|
160
|
+
await writeFile(temp, record, {
|
|
161
|
+
mode: 384,
|
|
162
|
+
flag: "wx"
|
|
163
|
+
});
|
|
164
|
+
await link(temp, path);
|
|
165
|
+
} finally {
|
|
166
|
+
try {
|
|
167
|
+
await rm(temp, { force: true });
|
|
168
|
+
} catch (error) {}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Determine whether a recorded writer exited, including Linux PID reuse and a previous boot.
|
|
173
|
+
* @param record - untrusted contents of an existing lock file.
|
|
174
|
+
* @param path - optional lock path for checking legacy PID records against process creation.
|
|
175
|
+
* @returns true for an exited process, changed Linux identity, or a legacy record predating its current process.
|
|
176
|
+
*/
|
|
177
|
+
async function writerExited(record, path) {
|
|
178
|
+
const match = RECORD.exec(record);
|
|
179
|
+
if (match === null) return false;
|
|
180
|
+
const pid = Number(match[1]);
|
|
181
|
+
if (pid === 0 || pid > 2147483647) return false;
|
|
182
|
+
if (pid !== process.pid) try {
|
|
183
|
+
process.kill(pid, 0);
|
|
184
|
+
} catch (error) {
|
|
185
|
+
return error.code === "ESRCH";
|
|
186
|
+
}
|
|
187
|
+
if (process.platform !== "linux") return false;
|
|
188
|
+
const current = await identity(pid);
|
|
189
|
+
if (current === void 0) return false;
|
|
190
|
+
if (current.state === "Z" || current.state === "X") return true;
|
|
191
|
+
if (match[2] !== void 0) return current.id !== `${match[2]} ${match[3]}`;
|
|
192
|
+
if (path === void 0) return false;
|
|
193
|
+
try {
|
|
194
|
+
const [metadata, kernelStat, ticks] = await Promise.all([
|
|
195
|
+
stat(path),
|
|
196
|
+
readFile("/proc/stat", "utf8"),
|
|
197
|
+
linuxClockTicks()
|
|
198
|
+
]);
|
|
199
|
+
const bootSeconds = /^btime (\d+)$/mu.exec(kernelStat)?.[1];
|
|
200
|
+
if (bootSeconds === void 0 || ticks === void 0) return false;
|
|
201
|
+
return Number(bootSeconds) * 1e3 + current.start / ticks * 1e3 > Math.max(metadata.mtimeMs, metadata.ctimeMs, metadata.birthtimeMs) + 2e3;
|
|
202
|
+
} catch (error) {
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
//#endregion
|
|
4
207
|
//#region lib/types/index.js
|
|
5
208
|
/**
|
|
6
|
-
*
|
|
209
|
+
* Atomic file replacement and writer coordination.
|
|
7
210
|
* `writeFileAtomic` writes a random-suffix sibling with exclusive create and
|
|
8
211
|
* the caller's permission bits, then renames it over the target, so readers
|
|
9
212
|
* observe either the old or the new complete content and a replaced file ends
|
|
10
213
|
* up with exactly the stated mode. `withFileLock` serializes cross-process
|
|
11
|
-
* writers of one file through
|
|
214
|
+
* writers of one file through an exclusively published `<file>.lock` sibling, so a
|
|
12
215
|
* read-modify-write cycle can never resurrect a state another writer just
|
|
13
216
|
* replaced; readers stay lock-free because the rename commit is atomic. A lock
|
|
14
|
-
* whose recorded holder process no longer exists is taken over.
|
|
217
|
+
* whose recorded holder process no longer exists is taken over. Windows also
|
|
218
|
+
* recovers empty or zero-filled records after an exclusive read proves no
|
|
219
|
+
* creator still has the lock file open. Linux uses a read lease to exclude
|
|
220
|
+
* unfinished writers, and kernel process identity to distinguish a previous
|
|
221
|
+
* boot, reused PID, or zombie process.
|
|
15
222
|
* @module @deepseek-ai/dsh-atomic-write
|
|
16
223
|
*/
|
|
17
|
-
const
|
|
224
|
+
const WINDOWS_TRANSIENT_FILE_ERRORS = new Set([
|
|
18
225
|
"EACCES",
|
|
19
226
|
"EBUSY",
|
|
20
227
|
"EPERM"
|
|
21
228
|
]);
|
|
22
|
-
const
|
|
23
|
-
const
|
|
24
|
-
const
|
|
229
|
+
const WINDOWS_FILE_RETRY_INITIAL_MS = 20;
|
|
230
|
+
const WINDOWS_FILE_RETRY_MAX_MS = 200;
|
|
231
|
+
const WINDOWS_FILE_RETRY_LIMIT = 8;
|
|
25
232
|
/** Whether Windows reported temporary interference with an atomic replacement. */
|
|
26
|
-
function
|
|
233
|
+
function isTransientWindowsFileError(error) {
|
|
27
234
|
if (process.platform !== "win32") return false;
|
|
28
|
-
return
|
|
235
|
+
return WINDOWS_TRANSIENT_FILE_ERRORS.has(error?.code ?? "");
|
|
29
236
|
}
|
|
30
|
-
/**
|
|
31
|
-
async function
|
|
32
|
-
let delay =
|
|
237
|
+
/** Retry a replacement or lock release while Windows briefly holds the path open. */
|
|
238
|
+
async function retryWindowsFileOperation(operation) {
|
|
239
|
+
let delay = WINDOWS_FILE_RETRY_INITIAL_MS;
|
|
33
240
|
for (let retries = 0;; retries += 1) {
|
|
34
241
|
try {
|
|
35
|
-
await
|
|
242
|
+
await operation();
|
|
36
243
|
return;
|
|
37
244
|
} catch (error) {
|
|
38
|
-
if (!
|
|
39
|
-
if (retries >=
|
|
245
|
+
if (!isTransientWindowsFileError(error)) throw error;
|
|
246
|
+
if (retries >= WINDOWS_FILE_RETRY_LIMIT) throw error;
|
|
40
247
|
}
|
|
41
248
|
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
42
|
-
delay = Math.min(delay * 2,
|
|
249
|
+
delay = Math.min(delay * 2, WINDOWS_FILE_RETRY_MAX_MS);
|
|
43
250
|
}
|
|
44
251
|
}
|
|
45
252
|
/**
|
|
@@ -69,7 +276,7 @@ async function writeFileAtomic(filename, content, options) {
|
|
|
69
276
|
mode: options.mode,
|
|
70
277
|
flag: "wx"
|
|
71
278
|
});
|
|
72
|
-
await
|
|
279
|
+
await retryWindowsFileOperation(() => rename(temp, filename));
|
|
73
280
|
} catch (error) {
|
|
74
281
|
await rm(temp, { force: true });
|
|
75
282
|
throw error;
|
|
@@ -87,18 +294,13 @@ async function isLockContention(error, lockPath) {
|
|
|
87
294
|
return false;
|
|
88
295
|
}
|
|
89
296
|
}
|
|
90
|
-
/** Whether the
|
|
91
|
-
function holderExited(record) {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
process.kill(pid, 0);
|
|
98
|
-
return false;
|
|
99
|
-
} catch (error) {
|
|
100
|
-
return error.code === "ESRCH";
|
|
101
|
-
}
|
|
297
|
+
/** Whether the recorded writer is absent or has a different observable Linux identity. */
|
|
298
|
+
async function holderExited(record, path) {
|
|
299
|
+
return writerExited(record, path);
|
|
300
|
+
}
|
|
301
|
+
/** A crash can leave an empty or zero-filled record before the holder's PID reaches disk. */
|
|
302
|
+
function damagedRecord(record, limit = 64) {
|
|
303
|
+
return record.length <= limit && /^\0*$/.test(record);
|
|
102
304
|
}
|
|
103
305
|
/** The lock file's content, or undefined when it cannot be read. */
|
|
104
306
|
async function readLockRecord(lockPath) {
|
|
@@ -109,31 +311,38 @@ async function readLockRecord(lockPath) {
|
|
|
109
311
|
}
|
|
110
312
|
}
|
|
111
313
|
/**
|
|
112
|
-
* Remove
|
|
314
|
+
* Remove an exited holder's lock or a closed, damaged Windows or Linux lock. Contenders that read the
|
|
113
315
|
* same record serialize on a claim file named after it. Under the claim, the
|
|
114
316
|
* claimant re-reads the lock and probes its PID again, and removes it only
|
|
115
|
-
* when it still holds that record and that PID is still gone
|
|
317
|
+
* when it still holds that record and that PID is still gone. A damaged
|
|
318
|
+
* record instead requires kernel-backed inspection after claiming it. The record's
|
|
116
319
|
* holder can no longer release it, and no other contender can remove it
|
|
117
320
|
* without the claim, so a removal never deletes a lock another contender
|
|
118
321
|
* acquired after the dead holder's, including one whose holder reused the PID.
|
|
119
|
-
* @returns Whether this call removed the
|
|
322
|
+
* @returns Whether this call removed the abandoned lock.
|
|
120
323
|
*/
|
|
121
324
|
async function takeOverExitedLock(lockPath) {
|
|
122
325
|
const record = await readLockRecord(lockPath);
|
|
123
|
-
if (record === void 0
|
|
326
|
+
if (record === void 0) return false;
|
|
327
|
+
const damaged = (process.platform === "win32" || process.platform === "linux") && damagedRecord(record, process.platform === "linux" ? 128 : 64);
|
|
328
|
+
const partialLinuxPid = process.platform === "linux" && /^\d{1,10}$/u.test(record);
|
|
329
|
+
if (!damaged && !partialLinuxPid && !await holderExited(record, lockPath)) return false;
|
|
124
330
|
const claim = `${lockPath}.takeover-${createHash("sha256").update(record).digest("hex").slice(0, 16)}`;
|
|
125
331
|
try {
|
|
126
|
-
await
|
|
127
|
-
mode: 384,
|
|
128
|
-
flag: "wx"
|
|
129
|
-
});
|
|
332
|
+
await createWriterLock(claim, await writerLockRecord());
|
|
130
333
|
} catch (error) {
|
|
131
334
|
const code = error.code;
|
|
132
|
-
if (code === "EEXIST" || code === "EPERM")
|
|
335
|
+
if (code === "EEXIST" || code === "EPERM") {
|
|
336
|
+
if (await takeOverExitedLock(claim)) return takeOverExitedLock(lockPath);
|
|
337
|
+
return false;
|
|
338
|
+
}
|
|
133
339
|
throw error;
|
|
134
340
|
}
|
|
135
341
|
try {
|
|
136
|
-
if (await readLockRecord(lockPath) !== record
|
|
342
|
+
if (await readLockRecord(lockPath) !== record) return false;
|
|
343
|
+
if (damaged || partialLinuxPid) {
|
|
344
|
+
if ((process.platform === "win32" ? await readUnopenedWindowsLock(lockPath) : await readClosedLinuxLock(lockPath)) !== record) return false;
|
|
345
|
+
} else if (!await holderExited(record, lockPath)) return false;
|
|
137
346
|
try {
|
|
138
347
|
await rm(lockPath, { force: true });
|
|
139
348
|
} catch (error) {
|
|
@@ -163,7 +372,7 @@ const LOCK_RETRY_MAX_MS = 200;
|
|
|
163
372
|
const DEFAULT_LOCK_WAIT_MS = 2e3;
|
|
164
373
|
/**
|
|
165
374
|
* Hold the cross-process writer lock for `filename` around one operation. The
|
|
166
|
-
* lock is
|
|
375
|
+
* lock is an exclusively published sibling (`<filename>.lock`); paired with the
|
|
167
376
|
* rename-based commit of {@link writeFileAtomic}, readers stay lock-free and
|
|
168
377
|
* only writers contend. `EEXIST` is contention directly; an `EPERM` is
|
|
169
378
|
* contention only when a fresh `lstat` confirms the lock path exists, covering
|
|
@@ -172,9 +381,17 @@ const DEFAULT_LOCK_WAIT_MS = 2e3;
|
|
|
172
381
|
* permission error is rethrown. The lock records its holder's PID. A contender
|
|
173
382
|
* removes the lock and retries at once when no process with that PID exists
|
|
174
383
|
* (`ESRCH`); any other lock, including one whose holder exists under another
|
|
175
|
-
* user (`EPERM`) or whose record is incomplete, is waited for.
|
|
176
|
-
*
|
|
177
|
-
*
|
|
384
|
+
* user (`EPERM`) or whose record is incomplete, is waited for. On Windows,
|
|
385
|
+
* empty or zero-filled records of at most 64 bytes can also be recovered after
|
|
386
|
+
* exclusive inspection proves no creator still has the file open. A failed
|
|
387
|
+
* inspection preserves the lock. Linux also recovers empty or zero-filled
|
|
388
|
+
* records of at most 128 bytes and unfinished PID records after a read lease
|
|
389
|
+
* excludes open writers. Dead takeover claimants are recovered automatically.
|
|
390
|
+
* Windows lock release retries transient file interference for a bounded interval. Contention backs
|
|
391
|
+
* off exponentially and times out after the deadline. Linux identity records
|
|
392
|
+
* distinguish PID reuse, a previous boot, and zombies. Legacy PID-only records
|
|
393
|
+
* are recovered when the current process began after the lock's file timestamps.
|
|
394
|
+
* Missing or inaccessible identity metadata never authorizes takeover. Takeover
|
|
178
395
|
* proves only that the recorded process exited: an operation that starts other
|
|
179
396
|
* writers must stop them with it or leave its successor a way to find them.
|
|
180
397
|
* PIDs are compared on the contender's host, so writers on other hosts or in
|
|
@@ -187,15 +404,13 @@ const DEFAULT_LOCK_WAIT_MS = 2e3;
|
|
|
187
404
|
*/
|
|
188
405
|
async function withFileLock(filename, operation, options) {
|
|
189
406
|
const lockPath = `${filename}.lock`;
|
|
407
|
+
const record = await writerLockRecord();
|
|
190
408
|
const deadline = Date.now() + (options?.waitMs ?? DEFAULT_LOCK_WAIT_MS);
|
|
191
409
|
let delay = LOCK_RETRY_INITIAL_MS;
|
|
192
410
|
let retriedUnconfirmedPermissionError = false;
|
|
193
411
|
for (;;) {
|
|
194
412
|
try {
|
|
195
|
-
await
|
|
196
|
-
mode: 384,
|
|
197
|
-
flag: "wx"
|
|
198
|
-
});
|
|
413
|
+
await createWriterLock(lockPath, record);
|
|
199
414
|
break;
|
|
200
415
|
} catch (error) {
|
|
201
416
|
if (!await isLockContention(error, lockPath)) {
|
|
@@ -203,14 +418,19 @@ async function withFileLock(filename, operation, options) {
|
|
|
203
418
|
retriedUnconfirmedPermissionError = true;
|
|
204
419
|
} else if (await takeOverExitedLock(lockPath)) continue;
|
|
205
420
|
}
|
|
206
|
-
if (Date.now() >= deadline)
|
|
421
|
+
if (Date.now() >= deadline) {
|
|
422
|
+
const held = await readLockRecord(lockPath);
|
|
423
|
+
const pid = held === void 0 ? void 0 : /^(\d+)\r?\n/u.exec(held)?.[1];
|
|
424
|
+
const state = held === void 0 ? "unreadable" : held === "" ? "empty" : /^\0+$/u.test(held) ? `zero-filled, ${String(held.length)} bytes` : pid === void 0 ? `incomplete or unrecognized, ${String(held.length)} bytes` : `PID ${pid}`;
|
|
425
|
+
throw new Error(`atomic-write: timed out waiting for the writer lock at ${lockPath} (record: ${state})`);
|
|
426
|
+
}
|
|
207
427
|
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
208
428
|
delay = Math.min(delay * 2, LOCK_RETRY_MAX_MS);
|
|
209
429
|
}
|
|
210
430
|
try {
|
|
211
431
|
return await operation();
|
|
212
432
|
} finally {
|
|
213
|
-
await rm(lockPath, { force: true });
|
|
433
|
+
await retryWindowsFileOperation(() => rm(lockPath, { force: true }));
|
|
214
434
|
}
|
|
215
435
|
}
|
|
216
436
|
//#endregion
|
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Atomic file replacement and writer coordination.
|
|
3
3
|
* `writeFileAtomic` writes a random-suffix sibling with exclusive create and
|
|
4
4
|
* the caller's permission bits, then renames it over the target, so readers
|
|
5
5
|
* observe either the old or the new complete content and a replaced file ends
|
|
6
6
|
* up with exactly the stated mode. `withFileLock` serializes cross-process
|
|
7
|
-
* writers of one file through
|
|
7
|
+
* writers of one file through an exclusively published `<file>.lock` sibling, so a
|
|
8
8
|
* read-modify-write cycle can never resurrect a state another writer just
|
|
9
9
|
* replaced; readers stay lock-free because the rename commit is atomic. A lock
|
|
10
|
-
* whose recorded holder process no longer exists is taken over.
|
|
10
|
+
* whose recorded holder process no longer exists is taken over. Windows also
|
|
11
|
+
* recovers empty or zero-filled records after an exclusive read proves no
|
|
12
|
+
* creator still has the lock file open. Linux uses a read lease to exclude
|
|
13
|
+
* unfinished writers, and kernel process identity to distinguish a previous
|
|
14
|
+
* boot, reused PID, or zombie process.
|
|
11
15
|
* @module @deepseek-ai/dsh-atomic-write
|
|
12
16
|
*/
|
|
13
17
|
/**
|
|
@@ -58,7 +62,7 @@ export interface FileLockOptions {
|
|
|
58
62
|
}
|
|
59
63
|
/**
|
|
60
64
|
* Hold the cross-process writer lock for `filename` around one operation. The
|
|
61
|
-
* lock is
|
|
65
|
+
* lock is an exclusively published sibling (`<filename>.lock`); paired with the
|
|
62
66
|
* rename-based commit of {@link writeFileAtomic}, readers stay lock-free and
|
|
63
67
|
* only writers contend. `EEXIST` is contention directly; an `EPERM` is
|
|
64
68
|
* contention only when a fresh `lstat` confirms the lock path exists, covering
|
|
@@ -67,9 +71,17 @@ export interface FileLockOptions {
|
|
|
67
71
|
* permission error is rethrown. The lock records its holder's PID. A contender
|
|
68
72
|
* removes the lock and retries at once when no process with that PID exists
|
|
69
73
|
* (`ESRCH`); any other lock, including one whose holder exists under another
|
|
70
|
-
* user (`EPERM`) or whose record is incomplete, is waited for.
|
|
71
|
-
*
|
|
72
|
-
*
|
|
74
|
+
* user (`EPERM`) or whose record is incomplete, is waited for. On Windows,
|
|
75
|
+
* empty or zero-filled records of at most 64 bytes can also be recovered after
|
|
76
|
+
* exclusive inspection proves no creator still has the file open. A failed
|
|
77
|
+
* inspection preserves the lock. Linux also recovers empty or zero-filled
|
|
78
|
+
* records of at most 128 bytes and unfinished PID records after a read lease
|
|
79
|
+
* excludes open writers. Dead takeover claimants are recovered automatically.
|
|
80
|
+
* Windows lock release retries transient file interference for a bounded interval. Contention backs
|
|
81
|
+
* off exponentially and times out after the deadline. Linux identity records
|
|
82
|
+
* distinguish PID reuse, a previous boot, and zombies. Legacy PID-only records
|
|
83
|
+
* are recovered when the current process began after the lock's file timestamps.
|
|
84
|
+
* Missing or inaccessible identity metadata never authorizes takeover. Takeover
|
|
73
85
|
* proves only that the recorded process exited: an operation that starts other
|
|
74
86
|
* writers must stop them with it or leave its successor a way to find them.
|
|
75
87
|
* PIDs are compared on the contender's host, so writers on other hosts or in
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Render a new acquisition's record; Linux adds boot identity, start ticks, and a unique claim token.
|
|
3
|
+
* @returns the PID record, with Linux identity when the kernel exposes it.
|
|
4
|
+
*/
|
|
5
|
+
export declare function writerLockRecord(): Promise<string>;
|
|
6
|
+
/**
|
|
7
|
+
* Exclusively publish a complete Linux identity record, or create a legacy PID lock.
|
|
8
|
+
* @param path - lock sibling in an existing parent directory.
|
|
9
|
+
* @param record - one acquisition's record from writerLockRecord.
|
|
10
|
+
*/
|
|
11
|
+
export declare function createWriterLock(path: string, record: string): Promise<void>;
|
|
12
|
+
/**
|
|
13
|
+
* Determine whether a recorded writer exited, including Linux PID reuse and a previous boot.
|
|
14
|
+
* @param record - untrusted contents of an existing lock file.
|
|
15
|
+
* @param path - optional lock path for checking legacy PID records against process creation.
|
|
16
|
+
* @returns true for an exited process, changed Linux identity, or a legacy record predating its current process.
|
|
17
|
+
*/
|
|
18
|
+
export declare function writerExited(record: string, path?: string): Promise<boolean>;
|
|
19
|
+
//# sourceMappingURL=linux-lock.d.ts.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read the Linux userspace clock-tick rate used by /proc process start times.
|
|
3
|
+
* @returns ticks per second, or undefined when libc cannot supply the rate.
|
|
4
|
+
*/
|
|
5
|
+
export declare function linuxClockTicks(): Promise<number | undefined>;
|
|
6
|
+
/**
|
|
7
|
+
* Read a small regular lock only after the kernel excludes open writers.
|
|
8
|
+
* @param path - existing lock path protected by the caller's takeover claim.
|
|
9
|
+
* @returns the unchanged inode's contents, or undefined when inspection is inconclusive.
|
|
10
|
+
*/
|
|
11
|
+
export declare function readClosedLinuxLock(path: string): Promise<string | undefined>;
|
|
12
|
+
//# sourceMappingURL=linux-native.d.ts.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read a small lock only when no other handle has the file open.
|
|
3
|
+
* @param path - lock path, protected by the caller's takeover claim.
|
|
4
|
+
* @returns its record, or undefined if exclusive access or inspection fails.
|
|
5
|
+
*/
|
|
6
|
+
export declare function readUnopenedWindowsLock(path: string): Promise<string | undefined>;
|
|
7
|
+
//# sourceMappingURL=windows-lock.d.ts.map
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deepseek-ai/dsh-atomic-write",
|
|
3
|
-
"description": "
|
|
3
|
+
"description": "Atomic file replacement and cross-process writer coordination with automatic orphan recovery",
|
|
4
4
|
"version": "0.1.7-rc.2",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -26,6 +26,9 @@
|
|
|
26
26
|
"lib/types/**/*.d.ts"
|
|
27
27
|
],
|
|
28
28
|
"license": "MIT",
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"koffi": "^3.1.0"
|
|
31
|
+
},
|
|
29
32
|
"peerDependencies": {
|
|
30
33
|
"@deepseek-ai/cordis": "workspace:~"
|
|
31
34
|
},
|
|
@@ -4125,7 +4125,7 @@ window.__ModuleLoader__.load({
|
|
|
4125
4125
|
for (const listener of listeners) listener();
|
|
4126
4126
|
};
|
|
4127
4127
|
/** @returns the client identity for one account call, read at call time so it carries the language and zone in effect then. */
|
|
4128
|
-
const client = () => accountClientMetadata(ctx.locale.getSnapshot().active, "3.0.4-rc.
|
|
4128
|
+
const client = () => accountClientMetadata(ctx.locale.getSnapshot().active, "3.0.4-rc.4");
|
|
4129
4129
|
const notices = createBonusNoticeController({
|
|
4130
4130
|
ackRetryDelayMs: config.bonusAckRetryDelayMs,
|
|
4131
4131
|
ackRetryMaxDelayMs: config.bonusAckRetryMaxDelayMs,
|
|
@@ -4256,7 +4256,7 @@ window.__ModuleLoader__.load({
|
|
|
4256
4256
|
refreshAccount,
|
|
4257
4257
|
contactUs() {
|
|
4258
4258
|
const url = contactUrl(config, {
|
|
4259
|
-
version: "3.0.4-rc.
|
|
4259
|
+
version: "3.0.4-rc.4",
|
|
4260
4260
|
locale: ctx.locale.getSnapshot().active === "zh" ? "zh-CN" : "en",
|
|
4261
4261
|
width: window.screen.width,
|
|
4262
4262
|
height: window.screen.height,
|
|
@@ -657,7 +657,7 @@ window.__ModuleLoader__.load({
|
|
|
657
657
|
function CurrentVersionRow({ t }) {
|
|
658
658
|
return (0, react_jsx_runtime.jsx)("div", {
|
|
659
659
|
className: CurrentVersionRow_module_css_default.row,
|
|
660
|
-
children: t("general.currentVersion", { version: "3.0.4-rc.
|
|
660
|
+
children: t("general.currentVersion", { version: "3.0.4-rc.4" })
|
|
661
661
|
});
|
|
662
662
|
}
|
|
663
663
|
//#endregion
|
|
@@ -167,9 +167,7 @@ window.__ModuleLoader__.load({
|
|
|
167
167
|
const SCROLLBAR_LINGER_MS = 2e3;
|
|
168
168
|
/** Format complete-build metadata for the local brand badge. */
|
|
169
169
|
function localBuildVersion() {
|
|
170
|
-
|
|
171
|
-
const dirty = {}.CHATCODE_CLI_CLIENT_GIT_DIRTY ?? {}["DSH_CLIENT_GIT_DIRTY"];
|
|
172
|
-
return `3.0.4-rc.2-${commit}` + (dirty === "true" ? "-dirty" : "");
|
|
170
|
+
return `3.0.4-rc.4-b93a39c-dirty`;
|
|
173
171
|
}
|
|
174
172
|
/** Each panel row subscribes only to its own selection state. */
|
|
175
173
|
function PanelRow({ id, label, wide, usePanelInfo, selectPanel, renderSlot }) {
|
|
@@ -13,7 +13,7 @@ export declare class DeepSeekAdapter<C extends Connection = Connection> extends
|
|
|
13
13
|
name: string;
|
|
14
14
|
};
|
|
15
15
|
providerRetryPolicy(_provider: string): import("@deepseek-ai/dsh-llm").ResolvedRetryPolicy;
|
|
16
|
-
listModels(provider: string): Promise<import("@deepseek-ai/dsh-llm").LlmModelInfo[]
|
|
16
|
+
listModels(provider: string): Promise<never[] | import("@deepseek-ai/dsh-llm").LlmModelInfo[]>;
|
|
17
17
|
resolveModel(provider: string, model: string, _signal?: AbortSignal): Promise<import("@deepseek-ai/dsh-llm").LlmResolvedModelInfo>;
|
|
18
18
|
imageRequestPricing(_provider: string, model: string): import("@deepseek-ai/dsh-llm").LlmImageRequestPricing;
|
|
19
19
|
prepareCall(provider: string, model: string, _signal?: AbortSignal): Promise<PreparedAdapterCall>;
|
|
@@ -56,7 +56,7 @@ export declare function plainOptions(config: Config): Options;
|
|
|
56
56
|
export declare const deepSeekConfigFields: {
|
|
57
57
|
baseURL: z<string, string, "volatile">;
|
|
58
58
|
thinking: z<"enabled" | "disabled", "enabled" | "disabled", "volatile">;
|
|
59
|
-
reasoningEffort: z<"
|
|
59
|
+
reasoningEffort: z<"low" | "off" | "high" | "max", "low" | "off" | "high" | "max", "volatile">;
|
|
60
60
|
maxTokens: z<number, number, "volatile-defined">;
|
|
61
61
|
defaultContextWindow: z<number, number, "volatile-defined">;
|
|
62
62
|
models: z<NoInfer<DeepSeekCatalogModel[]>, NoInfer<DeepSeekCatalogModel[]>, "volatile-defined">;
|
|
@@ -76,7 +76,7 @@ export declare const deepSeekConfigFields: {
|
|
|
76
76
|
export declare const Config: z<Schemastery.ObjectS<NoInfer<{
|
|
77
77
|
baseURL: z<string, string, "volatile">;
|
|
78
78
|
thinking: z<"enabled" | "disabled", "enabled" | "disabled", "volatile">;
|
|
79
|
-
reasoningEffort: z<"
|
|
79
|
+
reasoningEffort: z<"low" | "off" | "high" | "max", "low" | "off" | "high" | "max", "volatile">;
|
|
80
80
|
maxTokens: z<number, number, "volatile-defined">;
|
|
81
81
|
defaultContextWindow: z<number, number, "volatile-defined">;
|
|
82
82
|
models: z<NoInfer<DeepSeekCatalogModel[]>, NoInfer<DeepSeekCatalogModel[]>, "volatile-defined">;
|
|
@@ -95,7 +95,7 @@ export declare const Config: z<Schemastery.ObjectS<NoInfer<{
|
|
|
95
95
|
}>>, Schemastery.ObjectT<NoInfer<{
|
|
96
96
|
baseURL: z<string, string, "volatile">;
|
|
97
97
|
thinking: z<"enabled" | "disabled", "enabled" | "disabled", "volatile">;
|
|
98
|
-
reasoningEffort: z<"
|
|
98
|
+
reasoningEffort: z<"low" | "off" | "high" | "max", "low" | "off" | "high" | "max", "volatile">;
|
|
99
99
|
maxTokens: z<number, number, "volatile-defined">;
|
|
100
100
|
defaultContextWindow: z<number, number, "volatile-defined">;
|
|
101
101
|
models: z<NoInfer<DeepSeekCatalogModel[]>, NoInfer<DeepSeekCatalogModel[]>, "volatile-defined">;
|
|
@@ -5,7 +5,7 @@ export type Config = ProtocolConfig;
|
|
|
5
5
|
export declare const Config: import("@deepseek-ai/schemastery").default<Schemastery.ObjectS<NoInfer<{
|
|
6
6
|
baseURL: import("@deepseek-ai/schemastery").default<string, string, "volatile">;
|
|
7
7
|
thinking: import("@deepseek-ai/schemastery").default<"enabled" | "disabled", "enabled" | "disabled", "volatile">;
|
|
8
|
-
reasoningEffort: import("@deepseek-ai/schemastery").default<"
|
|
8
|
+
reasoningEffort: import("@deepseek-ai/schemastery").default<"low" | "off" | "high" | "max", "low" | "off" | "high" | "max", "volatile">;
|
|
9
9
|
maxTokens: import("@deepseek-ai/schemastery").default<number, number, "volatile-defined">;
|
|
10
10
|
defaultContextWindow: import("@deepseek-ai/schemastery").default<number, number, "volatile-defined">;
|
|
11
11
|
models: import("@deepseek-ai/schemastery").default<NoInfer<import("@deepseek-ai/dsh-llm-deepseek").DeepSeekCatalogModel[]>, NoInfer<import("@deepseek-ai/dsh-llm-deepseek").DeepSeekCatalogModel[]>, "volatile-defined">;
|
|
@@ -24,7 +24,7 @@ export declare const Config: import("@deepseek-ai/schemastery").default<Schemast
|
|
|
24
24
|
}>>, Schemastery.ObjectT<NoInfer<{
|
|
25
25
|
baseURL: import("@deepseek-ai/schemastery").default<string, string, "volatile">;
|
|
26
26
|
thinking: import("@deepseek-ai/schemastery").default<"enabled" | "disabled", "enabled" | "disabled", "volatile">;
|
|
27
|
-
reasoningEffort: import("@deepseek-ai/schemastery").default<"
|
|
27
|
+
reasoningEffort: import("@deepseek-ai/schemastery").default<"low" | "off" | "high" | "max", "low" | "off" | "high" | "max", "volatile">;
|
|
28
28
|
maxTokens: import("@deepseek-ai/schemastery").default<number, number, "volatile-defined">;
|
|
29
29
|
defaultContextWindow: import("@deepseek-ai/schemastery").default<number, number, "volatile-defined">;
|
|
30
30
|
models: import("@deepseek-ai/schemastery").default<NoInfer<import("@deepseek-ai/dsh-llm-deepseek").DeepSeekCatalogModel[]>, NoInfer<import("@deepseek-ai/dsh-llm-deepseek").DeepSeekCatalogModel[]>, "volatile-defined">;
|