dsh-global-todo 0.12.5 → 0.12.6
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 +16 -0
- package/lib/index.js +1 -1
- package/lib/store.js +47 -10
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,19 @@
|
|
|
1
|
+
## [0.12.6] — 2026-10-21
|
|
2
|
+
|
|
3
|
+
Saving on Windows never worked. Fixed, with the root cause written down (#2).
|
|
4
|
+
|
|
5
|
+
- **`open(temp, 'r')` → opened for writing.** The atomic write reopened its temp file read-only
|
|
6
|
+
and called `handle.sync()`. On Windows that is `FlushFileBuffers`, which requires a writable
|
|
7
|
+
handle, so **every** save threw `EPERM` — and because the sync sat before `rename()`, the
|
|
8
|
+
target file was never replaced: the list looked empty while `*.tmp` debris accumulated. POSIX
|
|
9
|
+
allows `fsync` on a read-only descriptor, which is why Linux and macOS never showed it.
|
|
10
|
+
- **The sync is best effort now.** A platform that refuses to flush the temp file gets a warning,
|
|
11
|
+
and the rename still happens: losing a save is worse than a save that only reached the page
|
|
12
|
+
cache. The temp file is unlinked when a write fails, so a failure leaves no debris.
|
|
13
|
+
- **The write path is a function with tests.** `atomicWrite()` takes an injectable fs surface; four
|
|
14
|
+
tests pin the invariant that failed: the handle must be opened for writing, a refused sync must
|
|
15
|
+
not lose the save, a failed write must clean up, and the payload must reach the file first.
|
|
16
|
+
|
|
1
17
|
## [0.12.5] — 2026-10-21
|
|
2
18
|
|
|
3
19
|
A check for the one install mistake that is invisible until the desktop complains.
|
package/lib/index.js
CHANGED
|
@@ -36,7 +36,7 @@ import { createStore, resolveStorePath } from './store.js'
|
|
|
36
36
|
import { createTools } from './tools.js'
|
|
37
37
|
|
|
38
38
|
/** Must equal `package.json`'s version; `npm run check` enforces that. */
|
|
39
|
-
export const VERSION = '0.12.
|
|
39
|
+
export const VERSION = '0.12.6'
|
|
40
40
|
|
|
41
41
|
/** Plugin id. Also the Loader row id in `cordis.patch.yml`. */
|
|
42
42
|
export const name = 'dsh-global-todo'
|
package/lib/store.js
CHANGED
|
@@ -19,7 +19,48 @@
|
|
|
19
19
|
* @module dsh-global-todo/store
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
|
-
import { open, mkdir, readFile, rename, stat,
|
|
22
|
+
import { open, mkdir, readFile, rename, stat, unlink } from 'node:fs/promises'
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Replace a file atomically, and survive a platform that refuses `fsync`.
|
|
26
|
+
*
|
|
27
|
+
* Opened for **writing** on purpose: on Windows `handle.sync()` is `FlushFileBuffers`,
|
|
28
|
+
* which needs a writable handle. Opening the temp file read-only and syncing it threw
|
|
29
|
+
* `EPERM` on every write, and because that sync sat before `rename()`, the target file
|
|
30
|
+
* was never replaced — the list looked empty while the data piled up as `*.tmp` debris.
|
|
31
|
+
* That was issue #2.
|
|
32
|
+
*
|
|
33
|
+
* The sync itself is best effort: durability matters, but losing a save because a
|
|
34
|
+
* platform will not flush a handle is strictly worse than a save that reaches the page
|
|
35
|
+
* cache. A failing sync is reported and the rename still happens.
|
|
36
|
+
*
|
|
37
|
+
* @param {string} file absolute path of the target
|
|
38
|
+
* @param {string} payload bytes to write
|
|
39
|
+
* @param {{ fs?: object, onSyncError?: (error: unknown) => void }} [options] injected for tests
|
|
40
|
+
*/
|
|
41
|
+
export async function atomicWrite(file, payload, options = {}) {
|
|
42
|
+
const io = options.fs ?? { open, rename, unlink }
|
|
43
|
+
const temp = `${file}.${process.pid}.${Date.now()}.tmp`
|
|
44
|
+
let handle = null
|
|
45
|
+
let renamed = false
|
|
46
|
+
try {
|
|
47
|
+
handle = await io.open(temp, 'w', 0o600)
|
|
48
|
+
await handle.writeFile(payload, 'utf8')
|
|
49
|
+
try {
|
|
50
|
+
await handle.sync()
|
|
51
|
+
} catch (error) {
|
|
52
|
+
options.onSyncError?.(error)
|
|
53
|
+
}
|
|
54
|
+
await handle.close()
|
|
55
|
+
handle = null
|
|
56
|
+
await io.rename(temp, file)
|
|
57
|
+
renamed = true
|
|
58
|
+
} finally {
|
|
59
|
+
if (handle !== null) await handle.close().catch(() => {})
|
|
60
|
+
// Only debris is cleaned up: after a successful rename the temp file is gone.
|
|
61
|
+
if (!renamed) await io.unlink(temp).catch(() => {})
|
|
62
|
+
}
|
|
63
|
+
}
|
|
23
64
|
import { homedir } from 'node:os'
|
|
24
65
|
import path from 'node:path'
|
|
25
66
|
|
|
@@ -187,16 +228,12 @@ export function createStore(options) {
|
|
|
187
228
|
const directory = path.dirname(file)
|
|
188
229
|
await mkdir(directory, { recursive: true })
|
|
189
230
|
const payload = `${JSON.stringify({ version: SCHEMA_VERSION, todos: doc.todos }, null, 2)}\n`
|
|
190
|
-
const temp = `${file}.${process.pid}.${now()}.tmp`
|
|
191
231
|
try {
|
|
192
|
-
await
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
}
|
|
197
|
-
await handle.close()
|
|
198
|
-
}
|
|
199
|
-
await rename(temp, file)
|
|
232
|
+
await atomicWrite(file, payload, {
|
|
233
|
+
// A platform that will not flush the temp file is a warning, not a lost save.
|
|
234
|
+
onSyncError: (error) =>
|
|
235
|
+
logger?.warn?.(`dsh-global-todo: 落盘后 fsync 失败(已继续)${file}:${error?.message ?? error}`),
|
|
236
|
+
})
|
|
200
237
|
} catch (error) {
|
|
201
238
|
throw new TodoError('store-unwritable', `写入待办文件失败 ${file}:${error?.message ?? error}`, { file })
|
|
202
239
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-global-todo",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.6",
|
|
4
4
|
"icon": "./docs/icon.svg",
|
|
5
5
|
"description": "Cross-session todo list for DeepSeek Harness: one JSON file a person and an agent both write to, with step plans that ship as a numbered hand-off, session read-back, and a human confirmation before an agent's \"done\" counts.",
|
|
6
6
|
"type": "module",
|