From 25454a5c8ca28a6528d7d299139fc9405ccf91df Mon Sep 17 00:00:00 2001 From: oliver Date: Thu, 10 Sep 2026 10:34:41 +0800 Subject: [PATCH] Recover from corrupt ops-cron store and tighten cron tool schemas. Co-authored-by: Cursor --- lib/store.js | 39 +++++++++++++++++++++++++++++++++++++-- lib/tools.js | 6 +++--- package.json | 2 +- test/preset.test.js | 17 +++++++++++++++++ 4 files changed, 58 insertions(+), 6 deletions(-) diff --git a/lib/store.js b/lib/store.js index c1d5e33..0467927 100644 --- a/lib/store.js +++ b/lib/store.js @@ -216,11 +216,46 @@ export function createStore(options = {}) { async function load() { try { const raw = await readFile(filePath, 'utf8') + // Reject embedded NUL — JSON.parse would throw an opaque "Unexpected token". + if (raw.includes('\u0000')) { + const error = new Error(`ops-cron store is corrupt (NUL byte) at ${filePath}; moved aside and starting empty`) + error.code = 'STORE_CORRUPT' + try { + const bak = `${filePath}.corrupt.${Date.now()}.bak` + await writeFile(bak, raw, 'utf8') + error.message = `${error.message} (backup: ${bak})` + } catch { + // best-effort backup + } + console.warn(`[dsh-ops-cron] ${error.message}`) + state = emptyState() + loaded = true + await persist() + return snapshot() + } const parsed = JSON.parse(raw) state = hydrate(parsed) } catch (error) { - if (error && error.code === 'ENOENT') state = emptyState() - else throw error + if (error && error.code === 'ENOENT') { + state = emptyState() + } else if (error instanceof SyntaxError) { + const wrapped = new Error(`ops-cron store JSON parse failed at ${filePath}: ${error.message}`) + wrapped.code = 'STORE_CORRUPT' + wrapped.cause = error + try { + const raw = await readFile(filePath, 'utf8').catch(() => '') + if (raw) await writeFile(`${filePath}.corrupt.${Date.now()}.bak`, raw, 'utf8') + } catch { + // ignore + } + console.warn(`[dsh-ops-cron] ${wrapped.message}; starting empty store`) + state = emptyState() + loaded = true + await persist() + return snapshot() + } else { + throw error + } } loaded = true return snapshot() diff --git a/lib/tools.js b/lib/tools.js index 4d003ab..9ca9d19 100644 --- a/lib/tools.js +++ b/lib/tools.js @@ -41,9 +41,9 @@ const JOB_SCHEMA = { schedule: { type: 'object', additionalProperties: true }, createdAt: { type: 'number' }, updatedAt: { type: 'number' }, - lastRunAt: {}, - lastStatus: {}, - nextRunAt: {}, + lastRunAt: { type: ['number', 'null'] }, + lastStatus: { type: ['string', 'null'] }, + nextRunAt: { type: ['number', 'null'] }, }, } diff --git a/package.json b/package.json index b8e4055..f696f7a 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "dsh-ops-cron", "description": "Scheduled tasks for DeepSeek Harness: Agent cron_* tools + sidebar 定时任务, with DSH or WhatsApp/IM delivery per job.", - "version": "0.1.11", + "version": "0.1.12", "private": false, "type": "module", "repository": { diff --git a/test/preset.test.js b/test/preset.test.js index 70a52c3..5a42a68 100644 --- a/test/preset.test.js +++ b/test/preset.test.js @@ -60,6 +60,23 @@ test('createJobRecord fires ASAP when one-shot at is slightly in the past', () = ) }) +test('createStore recovers from NUL-corrupt store.json', async () => { + const { createStore } = await import('../lib/store.js') + const { mkdtemp, writeFile, readFile, rm } = await import('node:fs/promises') + const { tmpdir } = await import('node:os') + const { join } = await import('node:path') + const dir = await mkdtemp(join(tmpdir(), 'dsh-cron-store-')) + const filePath = join(dir, 'store.json') + await writeFile(filePath, `{\u0000"jobs":[]}`, 'utf8') + const store = createStore({ filePath }) + const state = await store.read() + assert.equal(Array.isArray(state.jobs), true) + assert.equal(state.jobs.length, 0) + const rewritten = await readFile(filePath, 'utf8') + assert.equal(rewritten.includes('\u0000'), false) + await rm(dir, { recursive: true, force: true }) +}) + test('resolveCreateAgentPreset prefers explicit then peer then session then host default', async () => { assert.equal( await resolveCreateAgentPreset({ agent_preset: 'explicit' }, {}, {}),