@chatcode/chatcode-cli 3.0.4-rc.3 → 3.0.4-rc.5
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/package.json +24 -24
- package/runtime/manifest.json +2 -2
- package/runtime/node_modules/@deepseek-ai/dsh-atomic-write/lib/index.js +131 -34
- package/runtime/node_modules/@deepseek-ai/dsh-atomic-write/lib/types/index.d.ts +10 -6
- package/runtime/node_modules/@deepseek-ai/dsh-atomic-write/lib/types/linux-lock.d.ts +3 -2
- 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/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 -1
- package/runtime/node_modules/@deepseek-ai/dsh-subagent/lib/types/control.d.ts +1 -1
- package/runtime/node_modules/@deepseek-ai/dsh-workspace/lib/index.js +11 -3
- package/runtime/node_modules/@deepseek-ai/dsh-workspace/lib/types/index.d.ts +2 -0
- package/runtime/node_modules/@deepseek-ai/dsh-workspace/lib/types/index.js +18 -2
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chatcode/chatcode-cli",
|
|
3
3
|
"description": "ChatCode CLI: TUI-first coding agent, Web launcher, profiles, and legacy DSH-compatible plugins",
|
|
4
|
-
"version": "3.0.4-rc.
|
|
4
|
+
"version": "3.0.4-rc.5",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"dependencies": {
|
|
29
29
|
"@agentclientprotocol/sdk": "1.4.0",
|
|
30
30
|
"@babel/code-frame": "^7.29.0",
|
|
31
|
-
"@chatcode/cco-better-sidebar": "0.18.0-rc.
|
|
31
|
+
"@chatcode/cco-better-sidebar": "0.18.0-rc.8",
|
|
32
32
|
"@chatcode/cco-builtin-browser": "0.1.21",
|
|
33
33
|
"@chatcode/cco-chat-import": "0.17.0",
|
|
34
34
|
"@chatcode/cco-computer-use": "0.3.2",
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
"commander": "^15.0.0",
|
|
59
59
|
"compression": "^1.8.1",
|
|
60
60
|
"diff": "^9.0.0",
|
|
61
|
-
"dsh-better-sidebar": "npm:@chatcode/cco-better-sidebar@0.18.0-rc.
|
|
61
|
+
"dsh-better-sidebar": "npm:@chatcode/cco-better-sidebar@0.18.0-rc.8",
|
|
62
62
|
"dsh-builtin-browser": "npm:@chatcode/cco-builtin-browser@0.1.21",
|
|
63
63
|
"dsh-chat-import": "npm:@chatcode/cco-chat-import@0.17.0",
|
|
64
64
|
"dsh-computer-use": "npm:@chatcode/cco-computer-use@0.3.2",
|
|
@@ -98,45 +98,45 @@
|
|
|
98
98
|
"ws": "8.21.0",
|
|
99
99
|
"@deepseek-ai/dsh-acp": "0.1.7-rc.2",
|
|
100
100
|
"@deepseek-ai/dsh-agent": "0.1.7-rc.2",
|
|
101
|
-
"@deepseek-ai/dsh-agent-loop": "0.1.7-rc.2",
|
|
102
101
|
"@deepseek-ai/dsh-agent-loop-testkit": "0.1.7-rc.2",
|
|
103
|
-
"@deepseek-ai/dsh-credentials-local": "0.1.7-rc.2",
|
|
104
102
|
"@deepseek-ai/dsh-bash-local": "0.1.7-rc.2",
|
|
105
|
-
"@deepseek-ai/dsh-
|
|
103
|
+
"@deepseek-ai/dsh-credentials-local": "0.1.7-rc.2",
|
|
104
|
+
"@deepseek-ai/dsh-agent-loop": "0.1.7-rc.2",
|
|
105
|
+
"@deepseek-ai/dsh-attachment-local": "0.1.7-rc.2",
|
|
106
106
|
"@deepseek-ai/dsh-experimental-agent-team": "0.1.7-rc.2",
|
|
107
|
-
"@deepseek-ai/dsh-experimental-ptc-runtime-python": "0.1.7-rc.2",
|
|
108
107
|
"@deepseek-ai/dsh-fs-sandbox": "0.1.7-rc.2",
|
|
109
|
-
"@deepseek-ai/dsh-
|
|
110
|
-
"@deepseek-ai/dsh-llm": "0.1.7-rc.2",
|
|
111
|
-
"@deepseek-ai/dsh-experimental-tool-agent-team": "0.1.7-rc.2",
|
|
108
|
+
"@deepseek-ai/dsh-deepseek-llm-api-extensions": "0.1.7-rc.2",
|
|
112
109
|
"@deepseek-ai/dsh-fs-observation-policy": "0.1.7-rc.2",
|
|
113
|
-
"@deepseek-ai/dsh-
|
|
110
|
+
"@deepseek-ai/dsh-experimental-ptc-runtime-python": "0.1.7-rc.2",
|
|
114
111
|
"@deepseek-ai/dsh-host-frontend-static": "0.1.7-rc.2",
|
|
112
|
+
"@deepseek-ai/dsh-host-webserver": "0.1.7-rc.2",
|
|
113
|
+
"@deepseek-ai/dsh-llm": "0.1.7-rc.2",
|
|
115
114
|
"@deepseek-ai/dsh-llm-deepseek": "0.1.7-rc.2",
|
|
116
|
-
"@deepseek-ai/dsh-llm-pi-ai": "0.1.7-rc.2",
|
|
117
|
-
"@deepseek-ai/dsh-llm-deepseek-api-key": "0.1.7-rc.2",
|
|
118
115
|
"@deepseek-ai/dsh-llm-mock-server": "0.1.7-rc.2",
|
|
119
|
-
"@deepseek-ai/dsh-llm-
|
|
120
|
-
"@deepseek-ai/dsh-
|
|
121
|
-
"@deepseek-ai/dsh-llm-replay": "0.1.7-rc.2",
|
|
122
|
-
"@deepseek-ai/dsh-sandbox-policy": "0.1.7-rc.2",
|
|
116
|
+
"@deepseek-ai/dsh-llm-pi-ai": "0.1.7-rc.2",
|
|
117
|
+
"@deepseek-ai/dsh-experimental-tool-agent-team": "0.1.7-rc.2",
|
|
123
118
|
"@deepseek-ai/dsh-sandbox-local": "0.1.7-rc.2",
|
|
124
|
-
"@deepseek-ai/dsh-loader-smoke": "0.1.7-rc.2",
|
|
125
|
-
"@deepseek-ai/dsh-session": "0.1.7-rc.2",
|
|
126
119
|
"@deepseek-ai/dsh-sdk-client": "0.1.7-rc.2",
|
|
120
|
+
"@deepseek-ai/dsh-llm-deepseek-account": "0.1.7-rc.2",
|
|
127
121
|
"@deepseek-ai/dsh-session-checkpoint-policy": "0.1.7-rc.2",
|
|
122
|
+
"@deepseek-ai/dsh-plugin-package-inventory-deepseek": "0.1.7-rc.2",
|
|
128
123
|
"@deepseek-ai/dsh-session-log-deepseek": "0.1.7-rc.2",
|
|
129
|
-
"@deepseek-ai/dsh-
|
|
124
|
+
"@deepseek-ai/dsh-sandbox-policy": "0.1.7-rc.2",
|
|
125
|
+
"@deepseek-ai/dsh-session": "0.1.7-rc.2",
|
|
126
|
+
"@deepseek-ai/dsh-llm-deepseek-api-key": "0.1.7-rc.2",
|
|
127
|
+
"@deepseek-ai/dsh-loader-smoke": "0.1.7-rc.2",
|
|
130
128
|
"@deepseek-ai/dsh-session-persistence-jsonl": "0.1.7-rc.2",
|
|
131
129
|
"@deepseek-ai/dsh-session-query": "0.1.7-rc.2",
|
|
132
|
-
"@deepseek-ai/dsh-subagent-spawn-in-process": "0.1.7-rc.2",
|
|
133
130
|
"@deepseek-ai/dsh-shell-env": "0.1.7-rc.2",
|
|
134
|
-
"@deepseek-ai/dsh-
|
|
135
|
-
"@deepseek-ai/dsh-tools": "0.1.7-rc.2",
|
|
131
|
+
"@deepseek-ai/dsh-llm-replay": "0.1.7-rc.2",
|
|
136
132
|
"@deepseek-ai/dsh-subagent-fork-in-process": "0.1.7-rc.2",
|
|
137
133
|
"@deepseek-ai/dsh-settings": "0.1.7-rc.2",
|
|
134
|
+
"@deepseek-ai/dsh-subagent-spawn-in-process": "0.1.7-rc.2",
|
|
135
|
+
"@deepseek-ai/dsh-subagent": "0.1.7-rc.2",
|
|
136
|
+
"@deepseek-ai/dsh-system-prompt": "0.1.7-rc.2",
|
|
137
|
+
"@deepseek-ai/dsh-subprocess-local": "0.1.7-rc.2",
|
|
138
138
|
"@deepseek-ai/dsh-user-approval": "0.1.7-rc.2",
|
|
139
|
-
"@deepseek-ai/dsh-
|
|
139
|
+
"@deepseek-ai/dsh-tools": "0.1.7-rc.2"
|
|
140
140
|
},
|
|
141
141
|
"exports": {
|
|
142
142
|
"./headless-compat": {
|
package/runtime/manifest.json
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { createHash, randomBytes } from "node:crypto";
|
|
2
|
-
import { link, lstat, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import { link, lstat, mkdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
3
3
|
import { dirname, win32 } from "node:path";
|
|
4
4
|
import { execFile } from "node:child_process";
|
|
5
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";
|
|
6
8
|
//#region lib/types/windows-lock.js
|
|
7
9
|
/** Exclusive Windows inspection for an empty or zero-filled lock left by a failed creator. */
|
|
8
10
|
const execute = promisify(execFile);
|
|
@@ -46,6 +48,68 @@ async function readUnopenedWindowsLock(path) {
|
|
|
46
48
|
}
|
|
47
49
|
}
|
|
48
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
|
|
49
113
|
//#region lib/types/linux-lock.js
|
|
50
114
|
/** Linux writer identity and complete-record publication; other platforms retain PID records. */
|
|
51
115
|
const RECORD = /^(\d+)\r?\n(?:linux ([a-f0-9-]{36}) (\d+) ([a-f0-9]{24})\n)?$/u;
|
|
@@ -55,9 +119,14 @@ async function identity(pid) {
|
|
|
55
119
|
try {
|
|
56
120
|
const [boot, stat] = await Promise.all([readFile("/proc/sys/kernel/random/boot_id", "utf8"), readFile(`/proc/${pid}/stat`, "utf8")]);
|
|
57
121
|
const end = stat.lastIndexOf(")");
|
|
58
|
-
const
|
|
122
|
+
const fields = end < 0 ? [] : stat.slice(end + 2).trim().split(/\s+/u);
|
|
123
|
+
const start = fields[19];
|
|
59
124
|
if (!/^[a-f0-9-]{36}$/u.test(boot.trim()) || start === void 0 || !/^\d+$/u.test(start)) return void 0;
|
|
60
|
-
return
|
|
125
|
+
return {
|
|
126
|
+
id: `${boot.trim()} ${start}`,
|
|
127
|
+
start: Number(start),
|
|
128
|
+
state: fields[0] ?? ""
|
|
129
|
+
};
|
|
61
130
|
} catch (error) {
|
|
62
131
|
return;
|
|
63
132
|
}
|
|
@@ -69,7 +138,7 @@ async function identity(pid) {
|
|
|
69
138
|
async function writerLockRecord() {
|
|
70
139
|
const pid = `${process.pid}\n`;
|
|
71
140
|
if (process.platform !== "linux") return pid;
|
|
72
|
-
const current = await (ownIdentity ??= identity(process.pid));
|
|
141
|
+
const current = await (ownIdentity ??= identity(process.pid).then((value) => value?.id));
|
|
73
142
|
return current === void 0 ? pid : `${pid}linux ${current} ${randomBytes(12).toString("hex")}\n`;
|
|
74
143
|
}
|
|
75
144
|
/**
|
|
@@ -86,7 +155,7 @@ async function createWriterLock(path, record) {
|
|
|
86
155
|
});
|
|
87
156
|
return;
|
|
88
157
|
}
|
|
89
|
-
const temp = `${path}.${
|
|
158
|
+
const temp = `${path}.${randomBytes(12).toString("hex")}.tmp`;
|
|
90
159
|
try {
|
|
91
160
|
await writeFile(temp, record, {
|
|
92
161
|
mode: 384,
|
|
@@ -94,15 +163,18 @@ async function createWriterLock(path, record) {
|
|
|
94
163
|
});
|
|
95
164
|
await link(temp, path);
|
|
96
165
|
} finally {
|
|
97
|
-
|
|
166
|
+
try {
|
|
167
|
+
await rm(temp, { force: true });
|
|
168
|
+
} catch (error) {}
|
|
98
169
|
}
|
|
99
170
|
}
|
|
100
171
|
/**
|
|
101
172
|
* Determine whether a recorded writer exited, including Linux PID reuse and a previous boot.
|
|
102
173
|
* @param record - untrusted contents of an existing lock file.
|
|
103
|
-
* @
|
|
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.
|
|
104
176
|
*/
|
|
105
|
-
async function writerExited(record) {
|
|
177
|
+
async function writerExited(record, path) {
|
|
106
178
|
const match = RECORD.exec(record);
|
|
107
179
|
if (match === null) return false;
|
|
108
180
|
const pid = Number(match[1]);
|
|
@@ -112,14 +184,29 @@ async function writerExited(record) {
|
|
|
112
184
|
} catch (error) {
|
|
113
185
|
return error.code === "ESRCH";
|
|
114
186
|
}
|
|
115
|
-
if (process.platform !== "linux"
|
|
187
|
+
if (process.platform !== "linux") return false;
|
|
116
188
|
const current = await identity(pid);
|
|
117
|
-
|
|
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
|
+
}
|
|
118
205
|
}
|
|
119
206
|
//#endregion
|
|
120
207
|
//#region lib/types/index.js
|
|
121
208
|
/**
|
|
122
|
-
*
|
|
209
|
+
* Atomic file replacement and writer coordination.
|
|
123
210
|
* `writeFileAtomic` writes a random-suffix sibling with exclusive create and
|
|
124
211
|
* the caller's permission bits, then renames it over the target, so readers
|
|
125
212
|
* observe either the old or the new complete content and a replaced file ends
|
|
@@ -129,8 +216,9 @@ async function writerExited(record) {
|
|
|
129
216
|
* replaced; readers stay lock-free because the rename commit is atomic. A lock
|
|
130
217
|
* whose recorded holder process no longer exists is taken over. Windows also
|
|
131
218
|
* recovers empty or zero-filled records after an exclusive read proves no
|
|
132
|
-
* creator still has the lock file open. Linux
|
|
133
|
-
*
|
|
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.
|
|
134
222
|
* @module @deepseek-ai/dsh-atomic-write
|
|
135
223
|
*/
|
|
136
224
|
const WINDOWS_TRANSIENT_FILE_ERRORS = new Set([
|
|
@@ -207,12 +295,12 @@ async function isLockContention(error, lockPath) {
|
|
|
207
295
|
}
|
|
208
296
|
}
|
|
209
297
|
/** Whether the recorded writer is absent or has a different observable Linux identity. */
|
|
210
|
-
async function holderExited(record) {
|
|
211
|
-
return writerExited(record);
|
|
298
|
+
async function holderExited(record, path) {
|
|
299
|
+
return writerExited(record, path);
|
|
212
300
|
}
|
|
213
301
|
/** A crash can leave an empty or zero-filled record before the holder's PID reaches disk. */
|
|
214
|
-
function damagedRecord(record) {
|
|
215
|
-
return record.length <=
|
|
302
|
+
function damagedRecord(record, limit = 64) {
|
|
303
|
+
return record.length <= limit && /^\0*$/.test(record);
|
|
216
304
|
}
|
|
217
305
|
/** The lock file's content, or undefined when it cannot be read. */
|
|
218
306
|
async function readLockRecord(lockPath) {
|
|
@@ -223,11 +311,11 @@ async function readLockRecord(lockPath) {
|
|
|
223
311
|
}
|
|
224
312
|
}
|
|
225
313
|
/**
|
|
226
|
-
* Remove an exited holder's lock or a closed, damaged Windows lock. Contenders that read the
|
|
314
|
+
* Remove an exited holder's lock or a closed, damaged Windows or Linux lock. Contenders that read the
|
|
227
315
|
* same record serialize on a claim file named after it. Under the claim, the
|
|
228
316
|
* claimant re-reads the lock and probes its PID again, and removes it only
|
|
229
|
-
* when it still holds that record and that PID is still gone. A damaged
|
|
230
|
-
* record instead requires
|
|
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
|
|
231
319
|
* holder can no longer release it, and no other contender can remove it
|
|
232
320
|
* without the claim, so a removal never deletes a lock another contender
|
|
233
321
|
* acquired after the dead holder's, including one whose holder reused the PID.
|
|
@@ -236,24 +324,25 @@ async function readLockRecord(lockPath) {
|
|
|
236
324
|
async function takeOverExitedLock(lockPath) {
|
|
237
325
|
const record = await readLockRecord(lockPath);
|
|
238
326
|
if (record === void 0) return false;
|
|
239
|
-
const damaged = process.platform === "win32" && damagedRecord(record);
|
|
240
|
-
|
|
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;
|
|
241
330
|
const claim = `${lockPath}.takeover-${createHash("sha256").update(record).digest("hex").slice(0, 16)}`;
|
|
242
331
|
try {
|
|
243
|
-
await
|
|
244
|
-
mode: 384,
|
|
245
|
-
flag: "wx"
|
|
246
|
-
});
|
|
332
|
+
await createWriterLock(claim, await writerLockRecord());
|
|
247
333
|
} catch (error) {
|
|
248
334
|
const code = error.code;
|
|
249
|
-
if (code === "EEXIST" || code === "EPERM")
|
|
335
|
+
if (code === "EEXIST" || code === "EPERM") {
|
|
336
|
+
if (await takeOverExitedLock(claim)) return takeOverExitedLock(lockPath);
|
|
337
|
+
return false;
|
|
338
|
+
}
|
|
250
339
|
throw error;
|
|
251
340
|
}
|
|
252
341
|
try {
|
|
253
342
|
if (await readLockRecord(lockPath) !== record) return false;
|
|
254
|
-
if (damaged) {
|
|
255
|
-
if (await readUnopenedWindowsLock(lockPath) !== record) return false;
|
|
256
|
-
} else if (!await holderExited(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;
|
|
257
346
|
try {
|
|
258
347
|
await rm(lockPath, { force: true });
|
|
259
348
|
} catch (error) {
|
|
@@ -295,10 +384,13 @@ const DEFAULT_LOCK_WAIT_MS = 2e3;
|
|
|
295
384
|
* user (`EPERM`) or whose record is incomplete, is waited for. On Windows,
|
|
296
385
|
* empty or zero-filled records of at most 64 bytes can also be recovered after
|
|
297
386
|
* exclusive inspection proves no creator still has the file open. A failed
|
|
298
|
-
* inspection preserves the lock.
|
|
299
|
-
*
|
|
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
|
|
300
391
|
* off exponentially and times out after the deadline. Linux identity records
|
|
301
|
-
* distinguish PID reuse
|
|
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.
|
|
302
394
|
* Missing or inaccessible identity metadata never authorizes takeover. Takeover
|
|
303
395
|
* proves only that the recorded process exited: an operation that starts other
|
|
304
396
|
* writers must stop them with it or leave its successor a way to find them.
|
|
@@ -326,7 +418,12 @@ async function withFileLock(filename, operation, options) {
|
|
|
326
418
|
retriedUnconfirmedPermissionError = true;
|
|
327
419
|
} else if (await takeOverExitedLock(lockPath)) continue;
|
|
328
420
|
}
|
|
329
|
-
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
|
+
}
|
|
330
427
|
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
331
428
|
delay = Math.min(delay * 2, LOCK_RETRY_MAX_MS);
|
|
332
429
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
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
|
|
@@ -9,8 +9,9 @@
|
|
|
9
9
|
* replaced; readers stay lock-free because the rename commit is atomic. A lock
|
|
10
10
|
* whose recorded holder process no longer exists is taken over. Windows also
|
|
11
11
|
* recovers empty or zero-filled records after an exclusive read proves no
|
|
12
|
-
* creator still has the lock file open. Linux
|
|
13
|
-
*
|
|
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.
|
|
14
15
|
* @module @deepseek-ai/dsh-atomic-write
|
|
15
16
|
*/
|
|
16
17
|
/**
|
|
@@ -73,10 +74,13 @@ export interface FileLockOptions {
|
|
|
73
74
|
* user (`EPERM`) or whose record is incomplete, is waited for. On Windows,
|
|
74
75
|
* empty or zero-filled records of at most 64 bytes can also be recovered after
|
|
75
76
|
* exclusive inspection proves no creator still has the file open. A failed
|
|
76
|
-
* inspection preserves the lock.
|
|
77
|
-
*
|
|
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
|
|
78
81
|
* off exponentially and times out after the deadline. Linux identity records
|
|
79
|
-
* distinguish PID reuse
|
|
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.
|
|
80
84
|
* Missing or inaccessible identity metadata never authorizes takeover. Takeover
|
|
81
85
|
* proves only that the recorded process exited: an operation that starts other
|
|
82
86
|
* writers must stop them with it or leave its successor a way to find them.
|
|
@@ -12,7 +12,8 @@ export declare function createWriterLock(path: string, record: string): Promise<
|
|
|
12
12
|
/**
|
|
13
13
|
* Determine whether a recorded writer exited, including Linux PID reuse and a previous boot.
|
|
14
14
|
* @param record - untrusted contents of an existing lock file.
|
|
15
|
-
* @
|
|
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.
|
|
16
17
|
*/
|
|
17
|
-
export declare function writerExited(record: string): Promise<boolean>;
|
|
18
|
+
export declare function writerExited(record: string, path?: string): Promise<boolean>;
|
|
18
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
|
|
@@ -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.5");
|
|
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.5",
|
|
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.5" })
|
|
661
661
|
});
|
|
662
662
|
}
|
|
663
663
|
//#endregion
|
|
@@ -167,7 +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
|
-
return `3.0.4-rc.
|
|
170
|
+
return `3.0.4-rc.5-b482da5-dirty`;
|
|
171
171
|
}
|
|
172
172
|
/** Each panel row subscribes only to its own selection state. */
|
|
173
173
|
function PanelRow({ id, label, wide, usePanelInfo, selectPanel, renderSlot }) {
|
|
@@ -12,8 +12,8 @@ declare const CONTROL_ID_SCHEMAS: {
|
|
|
12
12
|
childSessionId: z.ZodString;
|
|
13
13
|
mode: z.ZodLiteral<"continuable">;
|
|
14
14
|
delivery: z.ZodEnum<{
|
|
15
|
-
queue: "queue";
|
|
16
15
|
steer: "steer";
|
|
16
|
+
queue: "queue";
|
|
17
17
|
}>;
|
|
18
18
|
}, z.core.$strip>;
|
|
19
19
|
readonly 'subagent.interrupt': z.ZodObject<{
|
|
@@ -1153,6 +1153,15 @@ var WorkspaceRegistry = class extends Service {
|
|
|
1153
1153
|
if (sessions === void 0) return;
|
|
1154
1154
|
await this.indexHeaders(sessions.list().filter((session) => !this.deletedSessionIds.includes(session.id)).map((session) => session.header));
|
|
1155
1155
|
}
|
|
1156
|
+
/** Retry unresolved membership using known headers or individual persistence lookups. */
|
|
1157
|
+
async refreshUnresolvedMembers() {
|
|
1158
|
+
const sessions = this.ctx.get("sessions");
|
|
1159
|
+
for (const [, record] of this.requireTable().entries()) for (const id of record.sessionIds) {
|
|
1160
|
+
if (this.sessionPaths.has(id)) continue;
|
|
1161
|
+
const header = sessions?.get(id)?.header ?? this.headers.get(id) ?? (await this.ctx.sessionPersistence.stat(id))?.header;
|
|
1162
|
+
if (header !== void 0) await this.indexHeader(header);
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1156
1165
|
reportFilteredCandidates() {
|
|
1157
1166
|
for (const entity of this.entities.values()) {
|
|
1158
1167
|
const record = this.requireTable().get(entity.id);
|
|
@@ -1201,12 +1210,11 @@ var WorkspaceRegistry = class extends Service {
|
|
|
1201
1210
|
await domain.refresh();
|
|
1202
1211
|
const changed = this.snapshotIdentity() !== this.refreshedState;
|
|
1203
1212
|
this.state = domain.global.get();
|
|
1204
|
-
|
|
1205
|
-
if (changed || missingHeaders) {
|
|
1213
|
+
if (changed) {
|
|
1206
1214
|
await this.replaceHeaderIndex(await this.listStoredHeaders());
|
|
1207
1215
|
await this.indexLiveSessions();
|
|
1208
1216
|
this.rebuildEntities();
|
|
1209
|
-
}
|
|
1217
|
+
} else await this.refreshUnresolvedMembers();
|
|
1210
1218
|
await this.recoverPendingMutation();
|
|
1211
1219
|
await this.recoverSessionDeletion();
|
|
1212
1220
|
const newlyDeleted = this.deletedSessionIds.filter((id) => !previousDeleted.has(id));
|
|
@@ -319,6 +319,8 @@ export declare class WorkspaceRegistry extends Service {
|
|
|
319
319
|
/** Every stored session's header, projected from the persistence snapshot listing. */
|
|
320
320
|
private listStoredHeaders;
|
|
321
321
|
private indexLiveSessions;
|
|
322
|
+
/** Retry unresolved membership using known headers or individual persistence lookups. */
|
|
323
|
+
private refreshUnresolvedMembers;
|
|
322
324
|
private reportFilteredCandidates;
|
|
323
325
|
private readSessionHeader;
|
|
324
326
|
private requireTable;
|
|
@@ -986,6 +986,20 @@ export class WorkspaceRegistry extends Service {
|
|
|
986
986
|
return;
|
|
987
987
|
await this.indexHeaders(sessions.list().filter(session => !this.deletedSessionIds.includes(session.id)).map(session => session.header));
|
|
988
988
|
}
|
|
989
|
+
/** Retry unresolved membership using known headers or individual persistence lookups. */
|
|
990
|
+
async refreshUnresolvedMembers() {
|
|
991
|
+
const sessions = this.ctx.get('sessions');
|
|
992
|
+
for (const [, record] of this.requireTable().entries()) {
|
|
993
|
+
for (const id of record.sessionIds) {
|
|
994
|
+
if (this.sessionPaths.has(id))
|
|
995
|
+
continue;
|
|
996
|
+
const header = sessions?.get(id)?.header ?? this.headers.get(id)
|
|
997
|
+
?? (await this.ctx.sessionPersistence.stat(id))?.header;
|
|
998
|
+
if (header !== undefined)
|
|
999
|
+
await this.indexHeader(header);
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
989
1003
|
reportFilteredCandidates() {
|
|
990
1004
|
for (const entity of this.entities.values()) {
|
|
991
1005
|
const record = this.requireTable().get(entity.id);
|
|
@@ -1045,12 +1059,14 @@ export class WorkspaceRegistry extends Service {
|
|
|
1045
1059
|
await domain.refresh();
|
|
1046
1060
|
const changed = this.snapshotIdentity() !== this.refreshedState;
|
|
1047
1061
|
this.state = domain.global.get();
|
|
1048
|
-
|
|
1049
|
-
if (changed || missingHeaders) {
|
|
1062
|
+
if (changed) {
|
|
1050
1063
|
await this.replaceHeaderIndex(await this.listStoredHeaders());
|
|
1051
1064
|
await this.indexLiveSessions();
|
|
1052
1065
|
this.rebuildEntities();
|
|
1053
1066
|
}
|
|
1067
|
+
else {
|
|
1068
|
+
await this.refreshUnresolvedMembers();
|
|
1069
|
+
}
|
|
1054
1070
|
await this.recoverPendingMutation();
|
|
1055
1071
|
await this.recoverSessionDeletion();
|
|
1056
1072
|
const newlyDeleted = this.deletedSessionIds.filter(id => !previousDeleted.has(id));
|