dsh-lcx-codex 0.3.3 → 0.4.0-rc.13
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/ARCHITECTURE.md +93 -0
- package/CHANGELOG.md +143 -41
- package/README.md +208 -100
- package/README_EN.md +133 -101
- package/assets/dsh-lcx-codex-banner.jpg +0 -0
- package/cordis.patch.yml +10 -4
- package/lib/client.js +117 -194
- package/lib/compact-v2.js +124 -273
- package/lib/dsh-responses.js +299 -0
- package/lib/index.js +548 -1593
- package/lib/json-store.js +34 -0
- package/lib/legacy-v3.js +20 -0
- package/lib/native-checkpoint.js +93 -0
- package/lib/responses-replay.js +281 -0
- package/lib/route.js +222 -0
- package/lib/service-mutex.js +72 -0
- package/lib/transport.js +59 -380
- package/lib/web-run-output.js +13 -152
- package/lib/web-search-alpha.js +26 -448
- package/lib/web-search-capability.js +8 -10
- package/lib/web-search-hosted.js +22 -372
- package/lib/web-search-ref-store.js +7 -7
- package/package.json +20 -14
- package/scripts/probe-alpha.mjs +28 -76
- package/scripts/validate-dsh-schema.mjs +31 -0
- package/lib/checkpoint-store-v3.js +0 -459
- package/lib/compact.js +0 -752
- package/lib/dsh-pi-responses.js +0 -339
- package/lib/private-file.js +0 -18
- package/lib/session-lease.js +0 -70
- package/lib/web-search-store.js +0 -181
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
import { pathToFileURL } from 'node:url'
|
|
4
|
+
import { ALPHA_SEARCH_OUTPUT, ALPHA_SEARCH_PARAMETERS } from '../lib/web-search-alpha.js'
|
|
5
|
+
import { HOSTED_SEARCH_OUTPUT, HOSTED_SEARCH_PARAMETERS } from '../lib/web-search-hosted.js'
|
|
6
|
+
|
|
7
|
+
async function dshTools() {
|
|
8
|
+
try {
|
|
9
|
+
return await import('@deepseek-ai/dsh-tools')
|
|
10
|
+
} catch (error) {
|
|
11
|
+
if (error?.code !== 'ERR_MODULE_NOT_FOUND') throw error
|
|
12
|
+
const root = process.platform === 'win32'
|
|
13
|
+
? process.env.npm_config_prefix
|
|
14
|
+
? join(process.env.npm_config_prefix, 'node_modules')
|
|
15
|
+
: join(process.env.APPDATA ?? '', 'npm', 'node_modules')
|
|
16
|
+
: execFileSync('npm', ['root', '-g'], { encoding: 'utf8', windowsHide: true }).trim()
|
|
17
|
+
const entry = join(root, '@deepseek-ai', 'dsh', 'node_modules', '@deepseek-ai', 'dsh-tools', 'lib', 'index.js')
|
|
18
|
+
return import(pathToFileURL(entry).href)
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const { assertSupportedJsonSchema } = await dshTools()
|
|
23
|
+
for (const [name, schema] of Object.entries({
|
|
24
|
+
HOSTED_SEARCH_PARAMETERS,
|
|
25
|
+
HOSTED_SEARCH_OUTPUT,
|
|
26
|
+
ALPHA_SEARCH_PARAMETERS,
|
|
27
|
+
ALPHA_SEARCH_OUTPUT,
|
|
28
|
+
})) {
|
|
29
|
+
assertSupportedJsonSchema(schema)
|
|
30
|
+
process.stdout.write(`${name}: ok\n`)
|
|
31
|
+
}
|
|
@@ -1,459 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
chmodSync,
|
|
3
|
-
closeSync,
|
|
4
|
-
fsyncSync,
|
|
5
|
-
mkdirSync,
|
|
6
|
-
openSync,
|
|
7
|
-
readFileSync,
|
|
8
|
-
readdirSync,
|
|
9
|
-
renameSync,
|
|
10
|
-
statSync,
|
|
11
|
-
unlinkSync,
|
|
12
|
-
writeFileSync,
|
|
13
|
-
} from 'node:fs'
|
|
14
|
-
import { dirname, join, basename } from 'node:path'
|
|
15
|
-
import { randomUUID } from 'node:crypto'
|
|
16
|
-
import { PORTABLE_HISTORY_BYTE_BUDGET } from './compact.js'
|
|
17
|
-
import { ensurePrivateFileAcl } from './private-file.js'
|
|
18
|
-
|
|
19
|
-
export const CHECKPOINT_V3_VERSION = 3
|
|
20
|
-
export const CHECKPOINT_V3_MAX_RECORDS = 256
|
|
21
|
-
export const CHECKPOINT_V3_MAX_BYTES = 16 * 1024 * 1024
|
|
22
|
-
|
|
23
|
-
const LOCK_STALE_MS = 30 * 1000
|
|
24
|
-
const LOCK_WAIT_MS = 5 * 1000
|
|
25
|
-
const TEMP_STALE_MS = 60 * 60 * 1000
|
|
26
|
-
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu
|
|
27
|
-
|
|
28
|
-
export class CheckpointV3Error extends Error {
|
|
29
|
-
constructor(message, code, cause) {
|
|
30
|
-
super(message, cause === undefined ? undefined : { cause })
|
|
31
|
-
this.name = 'CheckpointV3Error'
|
|
32
|
-
this.code = code
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function isObject(value) {
|
|
37
|
-
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function bytes(value, pretty = false) {
|
|
41
|
-
return Buffer.byteLength(JSON.stringify(value, null, pretty ? 2 : undefined), 'utf8')
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function validImageAttachmentRef(ref) {
|
|
45
|
-
return isObject(ref) && /^sha256:[a-f0-9]{64}$/iu.test(String(ref.attachmentId ?? '')) &&
|
|
46
|
-
['image/png', 'image/jpeg', 'image/webp', 'image/gif'].includes(ref.mediaType) &&
|
|
47
|
-
Number.isSafeInteger(ref.bytes) && ref.bytes > 0 && Number.isSafeInteger(ref.width) && ref.width > 0 &&
|
|
48
|
-
Number.isSafeInteger(ref.height) && ref.height > 0 && (ref.name === undefined || typeof ref.name === 'string')
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function validContentPart(part) {
|
|
52
|
-
if (!isObject(part) || typeof part.type !== 'string' || part.type.length === 0 || part.type === 'input_image') return false
|
|
53
|
-
if (part.type === 'dsh_image_attachment') return validImageAttachmentRef(part.attachment)
|
|
54
|
-
if ((part.type === 'input_text' || part.type === 'output_text' || part.type === 'summary_text' || part.type === 'refusal') && typeof part.text !== 'string') return false
|
|
55
|
-
return true
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function validMessageItem(item) {
|
|
59
|
-
return typeof item.role === 'string' && item.role.length > 0 && Array.isArray(item.content) && item.content.every(validContentPart)
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function validPortableMessageItem(item) {
|
|
63
|
-
return typeof item.role === 'string' && item.role.length > 0 && Array.isArray(item.content) &&
|
|
64
|
-
item.content.every(validContentPart)
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function validNativeOutputItem(item) {
|
|
68
|
-
if (!isObject(item) || typeof item.type !== 'string' || item.type.length === 0) return false
|
|
69
|
-
if (item.id !== undefined && (typeof item.id !== 'string' || item.id.length === 0)) return false
|
|
70
|
-
if (item.type === 'message') return validMessageItem(item)
|
|
71
|
-
if (item.type === 'compaction') return validNativeCompaction(item)
|
|
72
|
-
if (item.type === 'function_call') {
|
|
73
|
-
return typeof item.call_id === 'string' && item.call_id.length > 0 &&
|
|
74
|
-
typeof item.name === 'string' && item.name.length > 0 &&
|
|
75
|
-
typeof item.arguments === 'string'
|
|
76
|
-
}
|
|
77
|
-
if (item.type === 'function_call_output') {
|
|
78
|
-
return typeof item.call_id === 'string' && item.call_id.length > 0 &&
|
|
79
|
-
(typeof item.output === 'string' || (Array.isArray(item.output) && item.output.every(validContentPart)))
|
|
80
|
-
}
|
|
81
|
-
if (item.type === 'reasoning') {
|
|
82
|
-
return (item.encrypted_content === undefined || typeof item.encrypted_content === 'string') &&
|
|
83
|
-
(item.content === undefined || (Array.isArray(item.content) && item.content.every(validContentPart))) &&
|
|
84
|
-
(item.summary === undefined || (Array.isArray(item.summary) && item.summary.every(validContentPart)))
|
|
85
|
-
}
|
|
86
|
-
return true
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function validPortableItem(item) {
|
|
90
|
-
if (!isObject(item) || typeof item.type !== 'string' || item.type.length === 0) return false
|
|
91
|
-
if (item.type === 'compaction' || item.type === 'context_compaction' || item.type === 'compaction_trigger') return false
|
|
92
|
-
if (item.type.startsWith('response.')) return false
|
|
93
|
-
if (Object.prototype.hasOwnProperty.call(item, 'encrypted_content')) return false
|
|
94
|
-
if (item.id !== undefined && (typeof item.id !== 'string' || item.id.length === 0)) return false
|
|
95
|
-
if (item.type === 'message') return validPortableMessageItem(item)
|
|
96
|
-
if (item.type === 'function_call') {
|
|
97
|
-
return typeof item.call_id === 'string' && item.call_id.length > 0 &&
|
|
98
|
-
typeof item.name === 'string' && item.name.length > 0 &&
|
|
99
|
-
typeof item.arguments === 'string'
|
|
100
|
-
}
|
|
101
|
-
if (item.type === 'function_call_output') {
|
|
102
|
-
return typeof item.call_id === 'string' && item.call_id.length > 0 &&
|
|
103
|
-
(typeof item.output === 'string' || (Array.isArray(item.output) && item.output.every(validContentPart)))
|
|
104
|
-
}
|
|
105
|
-
return true
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function validNativeCompaction(item) {
|
|
109
|
-
return isObject(item) && item.type === 'compaction' &&
|
|
110
|
-
typeof item.encrypted_content === 'string' && item.encrypted_content.length > 0
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
function validateItemCollection(items, validator, strictPairing) {
|
|
114
|
-
const ids = new Set()
|
|
115
|
-
const callIndexes = new Map()
|
|
116
|
-
const outputIndexes = new Map()
|
|
117
|
-
for (let index = 0; index < items.length; index += 1) {
|
|
118
|
-
const item = items[index]
|
|
119
|
-
if (!validator(item)) return false
|
|
120
|
-
if (item.id !== undefined) {
|
|
121
|
-
if (ids.has(item.id)) return false
|
|
122
|
-
ids.add(item.id)
|
|
123
|
-
}
|
|
124
|
-
if (item.type === 'function_call') {
|
|
125
|
-
if (callIndexes.has(item.call_id)) return false
|
|
126
|
-
callIndexes.set(item.call_id, index)
|
|
127
|
-
}
|
|
128
|
-
if (item.type === 'function_call_output') {
|
|
129
|
-
if (outputIndexes.has(item.call_id)) return false
|
|
130
|
-
outputIndexes.set(item.call_id, index)
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
if (strictPairing) {
|
|
134
|
-
if (callIndexes.size !== outputIndexes.size) return false
|
|
135
|
-
for (const [callId, callIndex] of callIndexes) {
|
|
136
|
-
if (!outputIndexes.has(callId) || callIndex >= outputIndexes.get(callId)) return false
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
return true
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
function validRecord(record, { strictPortablePairing = false } = {}) {
|
|
143
|
-
if (!isObject(record) || record.version !== CHECKPOINT_V3_VERSION ||
|
|
144
|
-
typeof record.checkpointId !== 'string' || record.checkpointId.length < 8 ||
|
|
145
|
-
typeof record.lineageId !== 'string' || record.lineageId.length === 0 ||
|
|
146
|
-
typeof record.sourceSessionId !== 'string' || record.sourceSessionId.length === 0 ||
|
|
147
|
-
record.sourceSessionId !== record.lineageId ||
|
|
148
|
-
(record.sourceBranchId !== undefined && (typeof record.sourceBranchId !== 'string' || record.sourceBranchId.length === 0)) ||
|
|
149
|
-
(record.parentCheckpointId !== undefined && (typeof record.parentCheckpointId !== 'string' || !UUID_PATTERN.test(record.parentCheckpointId))) ||
|
|
150
|
-
(record.transport !== undefined && typeof record.transport !== 'string') ||
|
|
151
|
-
typeof record.provider !== 'string' || record.provider.length === 0 ||
|
|
152
|
-
typeof record.model !== 'string' || record.model.length === 0 ||
|
|
153
|
-
typeof record.modelKey !== 'string' || record.modelKey !== `${record.provider}:${record.model}` ||
|
|
154
|
-
typeof record.baseURLFingerprint !== 'string' || record.baseURLFingerprint.length === 0 ||
|
|
155
|
-
typeof record.routeFingerprint !== 'string' || record.routeFingerprint.length === 0 ||
|
|
156
|
-
!Array.isArray(record.nativeOutput) || record.nativeOutput.length === 0 ||
|
|
157
|
-
!validNativeCompaction(record.nativeCompaction) ||
|
|
158
|
-
!Array.isArray(record.portableHistory) ||
|
|
159
|
-
typeof record.portableSummary !== 'string' ||
|
|
160
|
-
(record.portableImageCount !== undefined && (!Number.isSafeInteger(record.portableImageCount) || record.portableImageCount < 0)) ||
|
|
161
|
-
!Number.isFinite(record.createdAt)) return false
|
|
162
|
-
|
|
163
|
-
const nativeCompactions = record.nativeOutput.filter((item) => isObject(item) && item.type === 'compaction')
|
|
164
|
-
if (nativeCompactions.length !== 1 ||
|
|
165
|
-
nativeCompactions[0].encrypted_content !== record.nativeCompaction.encrypted_content) return false
|
|
166
|
-
if (!validateItemCollection(record.nativeOutput, validNativeOutputItem, true)) return false
|
|
167
|
-
if (!validateItemCollection(record.portableHistory, validPortableItem, strictPortablePairing)) return false
|
|
168
|
-
return bytes(record.nativeOutput) <= PORTABLE_HISTORY_BYTE_BUDGET &&
|
|
169
|
-
bytes(record.portableHistory) <= PORTABLE_HISTORY_BYTE_BUDGET &&
|
|
170
|
-
bytes(record.portableSummary) <= 16 * 1024
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
function emptyData() {
|
|
174
|
-
return { version: CHECKPOINT_V3_VERSION, checkpoints: {} }
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
function normalizedLimits(options = {}) {
|
|
178
|
-
const maxRecords = options.maxRecords ?? options.maxRecordCount ?? CHECKPOINT_V3_MAX_RECORDS
|
|
179
|
-
const maxBytes = options.maxBytes ?? options.maxTotalBytes ?? CHECKPOINT_V3_MAX_BYTES
|
|
180
|
-
if (!Number.isSafeInteger(maxRecords) || maxRecords < 1 || !Number.isSafeInteger(maxBytes) || maxBytes < 1) {
|
|
181
|
-
throw new CheckpointV3Error('Invalid checkpoint v3 store limits', 'LCX_CHECKPOINT_V3_LIMIT_INVALID')
|
|
182
|
-
}
|
|
183
|
-
return { maxRecords, maxBytes }
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
function validateData(parsed, file, limits, { strict = false, strictIds = new Set() } = {}) {
|
|
187
|
-
if (parsed?.version !== CHECKPOINT_V3_VERSION) {
|
|
188
|
-
throw new CheckpointV3Error(`Unsupported checkpoint v3 store in ${file}`, 'LCX_CHECKPOINT_V3_VERSION_UNSUPPORTED')
|
|
189
|
-
}
|
|
190
|
-
if (!isObject(parsed.checkpoints)) {
|
|
191
|
-
throw new CheckpointV3Error(`Invalid checkpoint v3 store in ${file}`, 'LCX_CHECKPOINT_V3_CORRUPT')
|
|
192
|
-
}
|
|
193
|
-
const entries = Object.entries(parsed.checkpoints)
|
|
194
|
-
if (entries.length > limits.maxRecords) {
|
|
195
|
-
throw new CheckpointV3Error(`Checkpoint v3 store exceeds record limit (${entries.length}/${limits.maxRecords})`, 'LCX_CHECKPOINT_V3_LIMIT_EXCEEDED')
|
|
196
|
-
}
|
|
197
|
-
for (const [id, record] of entries) {
|
|
198
|
-
if (id !== record?.checkpointId) {
|
|
199
|
-
throw new CheckpointV3Error(`Checkpoint v3 map key does not match record checkpointId (${id})`, 'LCX_CHECKPOINT_V3_KEY_MISMATCH')
|
|
200
|
-
}
|
|
201
|
-
if (!validRecord(record, { strictPortablePairing: strict || strictIds.has(id) })) {
|
|
202
|
-
throw new CheckpointV3Error(`Invalid checkpoint v3 record ${id} in ${file}`, 'LCX_CHECKPOINT_V3_CORRUPT')
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
for (const [id, record] of entries) {
|
|
206
|
-
if (record.parentCheckpointId !== undefined) {
|
|
207
|
-
const parent = parsed.checkpoints[record.parentCheckpointId]
|
|
208
|
-
if (!parent) throw new CheckpointV3Error(`Checkpoint v3 parent ${record.parentCheckpointId} for ${id} is missing`, 'LCX_CHECKPOINT_V3_PARENT_MISSING')
|
|
209
|
-
if (parent.lineageId !== record.lineageId) throw new CheckpointV3Error(`Checkpoint v3 parent ${record.parentCheckpointId} for ${id} has a different lineage`, 'LCX_CHECKPOINT_V3_PARENT_MISMATCH')
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
const data = { version: CHECKPOINT_V3_VERSION, checkpoints: parsed.checkpoints }
|
|
213
|
-
if (bytes(data, true) > limits.maxBytes) {
|
|
214
|
-
throw new CheckpointV3Error(`Checkpoint v3 store exceeds byte limit (${bytes(data, true)}/${limits.maxBytes})`, 'LCX_CHECKPOINT_V3_LIMIT_EXCEEDED')
|
|
215
|
-
}
|
|
216
|
-
return data
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
function sleepSync(milliseconds) {
|
|
220
|
-
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, milliseconds)
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
function fileSignature(file) {
|
|
224
|
-
try {
|
|
225
|
-
const info = statSync(file)
|
|
226
|
-
return { mtimeMs: info.mtimeMs, ctimeMs: info.ctimeMs, size: info.size, ino: info.ino }
|
|
227
|
-
} catch (error) {
|
|
228
|
-
if (error?.code === 'ENOENT') return undefined
|
|
229
|
-
throw error
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
function sameFileSignature(left, right) {
|
|
234
|
-
return left?.mtimeMs === right?.mtimeMs && left?.ctimeMs === right?.ctimeMs &&
|
|
235
|
-
left?.size === right?.size && left?.ino === right?.ino
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
function lockMetadata(text) {
|
|
239
|
-
const lines = String(text ?? '').split(/\r?\n/u)
|
|
240
|
-
const pid = Number(lines[0])
|
|
241
|
-
const startedAt = Number(lines[1])
|
|
242
|
-
const token = typeof lines[2] === 'string' && lines[2].length > 0 ? lines[2] : undefined
|
|
243
|
-
return {
|
|
244
|
-
pid: Number.isSafeInteger(pid) && pid > 0 ? pid : undefined,
|
|
245
|
-
startedAt: Number.isFinite(startedAt) && startedAt > 0 ? startedAt : undefined,
|
|
246
|
-
token,
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
function processAlive(pid) {
|
|
251
|
-
if (!Number.isSafeInteger(pid) || pid <= 0) return false
|
|
252
|
-
try {
|
|
253
|
-
process.kill(pid, 0)
|
|
254
|
-
return true
|
|
255
|
-
} catch (error) {
|
|
256
|
-
return error?.code === 'EPERM'
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
function staleLockSnapshot(file) {
|
|
261
|
-
try {
|
|
262
|
-
const info = statSync(file)
|
|
263
|
-
const text = readFileSync(file, 'utf8')
|
|
264
|
-
const metadata = lockMetadata(text)
|
|
265
|
-
const age = Date.now() - Math.max(info.mtimeMs, metadata.startedAt ?? 0)
|
|
266
|
-
if (age <= LOCK_STALE_MS || processAlive(metadata.pid)) return undefined
|
|
267
|
-
return { text, mtimeMs: info.mtimeMs, size: info.size }
|
|
268
|
-
} catch {
|
|
269
|
-
return undefined
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
function unlinkLockIfUnchanged(file, snapshot) {
|
|
274
|
-
if (!snapshot) return false
|
|
275
|
-
try {
|
|
276
|
-
const info = statSync(file)
|
|
277
|
-
if (info.mtimeMs !== snapshot.mtimeMs || info.size !== snapshot.size || readFileSync(file, 'utf8') !== snapshot.text) return false
|
|
278
|
-
unlinkSync(file)
|
|
279
|
-
return true
|
|
280
|
-
} catch {
|
|
281
|
-
return false
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
function cleanupStaleTemps(file) {
|
|
286
|
-
const directory = dirname(file)
|
|
287
|
-
const prefix = `.${basename(file)}.`
|
|
288
|
-
let names
|
|
289
|
-
try {
|
|
290
|
-
names = readdirSync(directory)
|
|
291
|
-
} catch {
|
|
292
|
-
return
|
|
293
|
-
}
|
|
294
|
-
const now = Date.now()
|
|
295
|
-
for (const name of names) {
|
|
296
|
-
if (!name.startsWith(prefix) || !name.endsWith('.tmp')) continue
|
|
297
|
-
const temporary = join(directory, name)
|
|
298
|
-
try {
|
|
299
|
-
const info = statSync(temporary)
|
|
300
|
-
if (info.isFile() && now - info.mtimeMs > TEMP_STALE_MS) unlinkSync(temporary)
|
|
301
|
-
} catch {
|
|
302
|
-
// A concurrent writer may have removed or replaced the candidate.
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
export class CheckpointV3Store {
|
|
308
|
-
constructor(file, options = {}) {
|
|
309
|
-
this.file = file
|
|
310
|
-
this.lockFile = `${file}.lock`
|
|
311
|
-
this.limits = normalizedLimits(options)
|
|
312
|
-
this.fileSignature = undefined
|
|
313
|
-
this.data = this.load()
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
load(options = {}) {
|
|
317
|
-
cleanupStaleTemps(this.file)
|
|
318
|
-
try {
|
|
319
|
-
const data = validateData(JSON.parse(readFileSync(this.file, 'utf8')), this.file, this.limits, options)
|
|
320
|
-
this.fileSignature = fileSignature(this.file)
|
|
321
|
-
return data
|
|
322
|
-
} catch (error) {
|
|
323
|
-
if (error?.code === 'ENOENT') {
|
|
324
|
-
this.fileSignature = undefined
|
|
325
|
-
return emptyData()
|
|
326
|
-
}
|
|
327
|
-
if (error instanceof CheckpointV3Error) throw error
|
|
328
|
-
throw new CheckpointV3Error(`Unable to load checkpoint v3 store ${this.file}`, 'LCX_CHECKPOINT_V3_CORRUPT', error)
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
refreshIfChanged() {
|
|
333
|
-
const currentSignature = fileSignature(this.file)
|
|
334
|
-
if (!sameFileSignature(currentSignature, this.fileSignature)) this.data = this.load()
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
get(id) {
|
|
338
|
-
this.refreshIfChanged()
|
|
339
|
-
return Object.prototype.hasOwnProperty.call(this.data.checkpoints, id) ? this.data.checkpoints[id] : undefined
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
has(id) {
|
|
343
|
-
return this.get(id) !== undefined
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
put(id, record) {
|
|
347
|
-
if (typeof id !== 'string' || id !== record?.checkpointId) {
|
|
348
|
-
throw new CheckpointV3Error(`Checkpoint v3 map key does not match record checkpointId (${id})`, 'LCX_CHECKPOINT_V3_KEY_MISMATCH')
|
|
349
|
-
}
|
|
350
|
-
if (!validRecord(record, { strictPortablePairing: true })) {
|
|
351
|
-
throw new CheckpointV3Error(`Invalid checkpoint v3 record ${id}`, 'LCX_CHECKPOINT_V3_CORRUPT')
|
|
352
|
-
}
|
|
353
|
-
mkdirSync(dirname(this.file), { recursive: true })
|
|
354
|
-
if (fileSignature(this.file)) ensurePrivateFileAcl(this.file)
|
|
355
|
-
ensurePrivateFileAcl(dirname(this.file))
|
|
356
|
-
const release = this.acquireLock()
|
|
357
|
-
try {
|
|
358
|
-
const latest = this.load()
|
|
359
|
-
const candidate = {
|
|
360
|
-
version: CHECKPOINT_V3_VERSION,
|
|
361
|
-
checkpoints: { ...latest.checkpoints, [id]: structuredClone(record) },
|
|
362
|
-
}
|
|
363
|
-
this.data = validateData(candidate, this.file, this.limits, { strictIds: new Set([id]) })
|
|
364
|
-
this.saveAtomicUnlocked()
|
|
365
|
-
} finally {
|
|
366
|
-
release()
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
acquireLock() {
|
|
371
|
-
const started = Date.now()
|
|
372
|
-
while (Date.now() - started <= LOCK_WAIT_MS) {
|
|
373
|
-
let fd
|
|
374
|
-
const token = randomUUID()
|
|
375
|
-
const lockText = `${process.pid}\n${Date.now()}\n${token}\n`
|
|
376
|
-
try {
|
|
377
|
-
fd = openSync(this.lockFile, 'wx', 0o600)
|
|
378
|
-
try {
|
|
379
|
-
writeFileSync(fd, lockText, { encoding: 'utf8' })
|
|
380
|
-
fsyncSync(fd)
|
|
381
|
-
} finally {
|
|
382
|
-
closeSync(fd)
|
|
383
|
-
fd = undefined
|
|
384
|
-
}
|
|
385
|
-
ensurePrivateFileAcl(this.lockFile)
|
|
386
|
-
return () => {
|
|
387
|
-
try {
|
|
388
|
-
const current = readFileSync(this.lockFile, 'utf8')
|
|
389
|
-
if (current === lockText) unlinkSync(this.lockFile)
|
|
390
|
-
} catch {
|
|
391
|
-
// The lock may have been removed by stale recovery after a crash.
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
} catch (error) {
|
|
395
|
-
if (fd !== undefined) {
|
|
396
|
-
try { closeSync(fd) } catch {}
|
|
397
|
-
}
|
|
398
|
-
if (error?.code !== 'EEXIST') {
|
|
399
|
-
throw new CheckpointV3Error(`Unable to acquire checkpoint v3 lock ${this.lockFile}`, 'LCX_CHECKPOINT_V3_LOCK_FAILED', error)
|
|
400
|
-
}
|
|
401
|
-
if (unlinkLockIfUnchanged(this.lockFile, staleLockSnapshot(this.lockFile))) continue
|
|
402
|
-
sleepSync(10)
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
throw new CheckpointV3Error(`Timed out acquiring checkpoint v3 lock`, 'LCX_CHECKPOINT_V3_LOCK_TIMEOUT')
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
saveAtomic(options = {}) {
|
|
409
|
-
mkdirSync(dirname(this.file), { recursive: true })
|
|
410
|
-
if (fileSignature(this.file)) ensurePrivateFileAcl(this.file)
|
|
411
|
-
ensurePrivateFileAcl(dirname(this.file))
|
|
412
|
-
const release = this.acquireLock()
|
|
413
|
-
try {
|
|
414
|
-
const latest = this.load()
|
|
415
|
-
const merged = {
|
|
416
|
-
version: CHECKPOINT_V3_VERSION,
|
|
417
|
-
checkpoints: { ...latest.checkpoints, ...this.data.checkpoints },
|
|
418
|
-
}
|
|
419
|
-
this.data = validateData(merged, this.file, this.limits, { strict: options.strict === true })
|
|
420
|
-
this.saveAtomicUnlocked()
|
|
421
|
-
} finally {
|
|
422
|
-
release()
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
saveAtomicUnlocked() {
|
|
427
|
-
cleanupStaleTemps(this.file)
|
|
428
|
-
const directory = dirname(this.file)
|
|
429
|
-
mkdirSync(directory, { recursive: true })
|
|
430
|
-
const temporary = join(directory, `.${basename(this.file)}.${randomUUID()}.tmp`)
|
|
431
|
-
const serialized = JSON.stringify(this.data, null, 2)
|
|
432
|
-
let fd
|
|
433
|
-
try {
|
|
434
|
-
fd = openSync(temporary, 'wx', 0o600)
|
|
435
|
-
// Harden the empty temporary file before it receives checkpoint content.
|
|
436
|
-
if (!ensurePrivateFileAcl(temporary)) {
|
|
437
|
-
throw new Error(`Unable to apply private ACL to checkpoint temporary file ${temporary}`)
|
|
438
|
-
}
|
|
439
|
-
writeFileSync(fd, serialized, { encoding: 'utf8' })
|
|
440
|
-
fsyncSync(fd)
|
|
441
|
-
closeSync(fd)
|
|
442
|
-
fd = undefined
|
|
443
|
-
try { chmodSync(temporary, 0o600) } catch {
|
|
444
|
-
// Windows may not expose POSIX mode bits; the ACL was applied before writing.
|
|
445
|
-
}
|
|
446
|
-
renameSync(temporary, this.file)
|
|
447
|
-
ensurePrivateFileAcl(this.file)
|
|
448
|
-
this.fileSignature = fileSignature(this.file)
|
|
449
|
-
} catch (error) {
|
|
450
|
-
if (fd !== undefined) {
|
|
451
|
-
try { closeSync(fd) } catch {}
|
|
452
|
-
}
|
|
453
|
-
try { unlinkSync(temporary) } catch {
|
|
454
|
-
// Preserve the original write failure.
|
|
455
|
-
}
|
|
456
|
-
throw new CheckpointV3Error(`Unable to atomically write checkpoint v3 store ${this.file}`, 'LCX_CHECKPOINT_V3_WRITE_FAILED', error)
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
}
|