clay-server 4.1.0-beta.4 → 4.1.0-beta.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.
Files changed (91) hide show
  1. package/lib/autonomous-run-record.js +32 -0
  2. package/lib/autonomous-run-scheduler.js +92 -0
  3. package/lib/durable-scheduler-record.js +61 -0
  4. package/lib/durable-scheduler-store.js +185 -0
  5. package/lib/durable-scheduler.js +495 -0
  6. package/lib/loop-cron.js +47 -0
  7. package/lib/loop-durable-schedule.js +159 -0
  8. package/lib/loop-guidance.js +35 -0
  9. package/lib/loop-registry-occurrence.js +76 -0
  10. package/lib/project-autonomous-run.js +498 -0
  11. package/lib/project-connection.js +4 -1
  12. package/lib/project-full-access.js +86 -0
  13. package/lib/project-http.js +3 -1
  14. package/lib/project-loop-interview.js +163 -0
  15. package/lib/project-loop.js +150 -64
  16. package/lib/project-pair-autonomous-stop.js +16 -0
  17. package/lib/project-pair-owned-stop.js +18 -0
  18. package/lib/project-pair-task-control.js +14 -1
  19. package/lib/project-scheduled-messages.js +316 -0
  20. package/lib/project-scheduled-task-execution.js +236 -0
  21. package/lib/project-scheduled-tasks.js +483 -0
  22. package/lib/project-session-pair.js +16 -19
  23. package/lib/project-sessions.js +60 -28
  24. package/lib/project-user-message.js +24 -18
  25. package/lib/project.js +260 -52
  26. package/lib/public/app.js +60 -23
  27. package/lib/public/css/home-hub.css +15 -2
  28. package/lib/public/css/home-sidebar.css +50 -2
  29. package/lib/public/css/input.css +71 -0
  30. package/lib/public/css/mates.css +0 -1
  31. package/lib/public/css/pane.css +58 -7
  32. package/lib/public/css/rewind.css +11 -2
  33. package/lib/public/css/scheduled-tasks.css +123 -0
  34. package/lib/public/css/sticky-notes.css +24 -4
  35. package/lib/public/index.html +19 -134
  36. package/lib/public/modules/app-loop-ui.js +4 -8
  37. package/lib/public/modules/app-messages.js +22 -6
  38. package/lib/public/modules/app-projects.js +2 -0
  39. package/lib/public/modules/app-rate-limit.js +8 -5
  40. package/lib/public/modules/autonomous-run.js +285 -0
  41. package/lib/public/modules/home-sidebar.js +16 -8
  42. package/lib/public/modules/home-surface.js +5 -0
  43. package/lib/public/modules/input.js +5 -4
  44. package/lib/public/modules/loop-interview.js +151 -0
  45. package/lib/public/modules/project-logs.js +2 -2
  46. package/lib/public/modules/scheduled-message-state.js +46 -0
  47. package/lib/public/modules/scheduled-task-edit-state.js +7 -0
  48. package/lib/public/modules/scheduled-task-engine.js +98 -0
  49. package/lib/public/modules/scheduled-task-form.js +122 -0
  50. package/lib/public/modules/scheduled-task-recurrence.js +46 -0
  51. package/lib/public/modules/scheduled-task-schedule-rule.js +115 -0
  52. package/lib/public/modules/scheduled-tasks.js +468 -0
  53. package/lib/public/modules/scheduler-config.js +6 -52
  54. package/lib/public/modules/scheduler.js +9 -21
  55. package/lib/public/modules/sidebar.js +0 -11
  56. package/lib/public/modules/split-view.js +2 -0
  57. package/lib/public/modules/sticky-notes-browser.js +1 -1
  58. package/lib/public/modules/sticky-notes.js +117 -4
  59. package/lib/public/modules/tool-palette-order.js +2 -3
  60. package/lib/public/modules/tools.js +5 -2
  61. package/lib/public/style.css +1 -0
  62. package/lib/record-revision.js +11 -0
  63. package/lib/schedule-rule.js +151 -0
  64. package/lib/schedule-validation.js +124 -0
  65. package/lib/scheduled-run-state.js +87 -0
  66. package/lib/scheduled-task-records.js +127 -0
  67. package/lib/scheduler.js +179 -196
  68. package/lib/sdk-bridge.js +123 -11
  69. package/lib/sdk-message-processor.js +3 -0
  70. package/lib/server-durable-scheduler.js +26 -0
  71. package/lib/server.js +22 -0
  72. package/lib/session-pair-factory.js +42 -4
  73. package/lib/session-pair-query-start.js +22 -0
  74. package/lib/session-query-tool-bridge.js +52 -0
  75. package/lib/session-split-groups.js +35 -1
  76. package/lib/sessions.js +51 -3
  77. package/lib/users-home-surface-preferences.js +2 -1
  78. package/lib/users-scheduled-task-preferences.js +44 -0
  79. package/lib/users.js +6 -0
  80. package/lib/ws-schema.js +25 -0
  81. package/lib/yoke/adapters/antigravity.js +1 -0
  82. package/lib/yoke/adapters/claude-worker.js +7 -4
  83. package/lib/yoke/adapters/claude.js +31 -1
  84. package/lib/yoke/adapters/codex.js +3 -0
  85. package/lib/yoke/claude-user-input.js +2 -0
  86. package/lib/yoke/index.js +2 -0
  87. package/lib/yoke/mcp-bridge-server.js +7 -1
  88. package/lib/yoke/session-tool-transport.js +53 -0
  89. package/lib/yoke/user-input.js +21 -7
  90. package/package.json +1 -1
  91. package/lib/public/modules/app-loop-wizard.js +0 -439
@@ -0,0 +1,32 @@
1
+ var crypto = require("crypto");
2
+ var ACTIVE_STATES = ["armed", "running", "reviewing", "waiting-worker", "waiting-user", "paused"];
3
+ var TERMINAL_STATES = ["completed", "stopped", "limit", "error"];
4
+ function clean(value, max) { return typeof value === "string" ? value.trim().substring(0, max) : ""; }
5
+ function integer(value, fallback, max) { var number = Number(value); return Number.isFinite(number) ? Math.max(1, Math.min(max, Math.floor(number))) : fallback; }
6
+ function criteria(value) { return (Array.isArray(value) ? value : []).slice(0, 10).map(function (item) { return clean(item, 1000); }).filter(Boolean); }
7
+ function project(run) {
8
+ if (!run) return null;
9
+ return { id: run.id, state: run.state, objective: run.objective || "", successCriteria: run.successCriteria || [], continuationCount: run.continuationCount || 0,
10
+ maxContinuations: run.maxContinuations, startedAt: run.startedAt || null, deadlineAt: run.deadlineAt || null, maxMinutes: run.maxMinutes,
11
+ waitingReason: run.waitingReason || "", terminalReason: run.terminalReason || "", outcome: run.outcome || null, pausedAfterRestart: !!run.pausedAfterRestart,
12
+ armToken: run.state === "armed" ? run.armToken : undefined };
13
+ }
14
+ function restore(value) {
15
+ if (!value || typeof value !== "object" || ACTIVE_STATES.concat(TERMINAL_STATES).indexOf(value.state) === -1) return null;
16
+ var evidence = value.outcome && Array.isArray(value.outcome.evidence) ? value.outcome.evidence.slice(0, 20).map(function (item) { return clean(item, 2000); }).filter(Boolean) : [];
17
+ var assessments = value.outcome && Array.isArray(value.outcome.criteria) ? value.outcome.criteria.slice(0, 10).map(function (item) {
18
+ return item && { criterion: clean(item.criterion, 1000), met: item.met === true, evidence: clean(item.evidence, 2000) };
19
+ }).filter(function (item) { return item && item.criterion && item.evidence; }) : [];
20
+ var snapshot = value.permissionSnapshot && typeof value.permissionSnapshot === "object" ? value.permissionSnapshot : {};
21
+ var run = { version: 1, id: clean(value.id, 200) || crypto.randomUUID(), state: value.state, objective: clean(value.objective, 20000), successCriteria: criteria(value.successCriteria),
22
+ continuationCount: Math.max(0, Math.min(100, Math.floor(Number(value.continuationCount) || 0))), maxContinuations: integer(value.maxContinuations, 10, 100), maxMinutes: integer(value.maxMinutes, 60, 1440),
23
+ createdAt: Number.isFinite(value.createdAt) ? value.createdAt : Date.now(), startedAt: Number.isFinite(value.startedAt) ? value.startedAt : null, deadlineAt: Number.isFinite(value.deadlineAt) ? value.deadlineAt : null,
24
+ finishedAt: Number.isFinite(value.finishedAt) ? value.finishedAt : null, waitingReason: clean(value.waitingReason, 2000), terminalReason: clean(value.terminalReason, 2000), pausedAfterRestart: !!value.pausedAfterRestart,
25
+ cancellationEpoch: Math.max(0, Math.floor(Number(value.cancellationEpoch) || 0)), rateLimitUntil: Number.isFinite(value.rateLimitUntil) ? value.rateLimitUntil : null, continuationPending: false,
26
+ wakeSequence: Math.max(0, Math.floor(Number(value.wakeSequence) || 0)), completionRequested: !!value.completionRequested,
27
+ permissionSnapshot: { permissionMode: clean(snapshot.permissionMode, 100) || null, permissionModeBeforeFullAccess: clean(snapshot.permissionModeBeforeFullAccess, 100) || null, enabled: snapshot.enabled === true } };
28
+ if (value.state === "armed") run.armToken = clean(value.armToken, 200) || crypto.randomUUID();
29
+ if (value.outcome && typeof value.outcome === "object") run.outcome = { assessment: clean(value.outcome.assessment, 40), summary: clean(value.outcome.summary, 4000), evidence: evidence, criteria: assessments, reportedAt: Number.isFinite(value.outcome.reportedAt) ? value.outcome.reportedAt : null };
30
+ return run;
31
+ }
32
+ module.exports = { project: project, restore: restore };
@@ -0,0 +1,92 @@
1
+ var sessionProvenance = require("./session-provenance");
2
+
3
+ function attachAutonomousRunScheduler(ctx) {
4
+ var scheduler = ctx.scheduler;
5
+ var type = "autonomous-wake:" + ctx.projectId;
6
+ var unregister = null;
7
+ function isMultiUser() { return typeof ctx.isMultiUser === "function" ? ctx.isMultiUser() : !!ctx.isMultiUser; }
8
+
9
+ function origin(session) {
10
+ var existing = session.sessionOriginId;
11
+ var value = sessionProvenance.ensureOrigin(session);
12
+ if (!existing) ctx.sm.saveSessionFile(session);
13
+ return value;
14
+ }
15
+ function owner(session) {
16
+ if (isMultiUser()) {
17
+ if (!session.ownerId) throw new Error("Until complete requires an explicit session owner.");
18
+ return session.ownerId;
19
+ }
20
+ return "local-user";
21
+ }
22
+ function matching(job, session, run, kind) {
23
+ return job.type === type && job.projectId === ctx.projectId && job.target.id === origin(session) &&
24
+ job.payload.runId === run.id && (!kind || job.payload.kind === kind);
25
+ }
26
+ function queued(session, run, kind) {
27
+ var jobs = scheduler.listJobs();
28
+ for (var i = jobs.length - 1; i >= 0; i--) if (jobs[i].state === "queued" && matching(jobs[i], session, run, kind)) return jobs[i];
29
+ return null;
30
+ }
31
+ function schedule(session, run, kind, runAt) {
32
+ var sessionOrigin = origin(session);
33
+ run.wakeSequence = Math.max(0, Math.floor(Number(run.wakeSequence) || 0)) + 1;
34
+ if (ctx.sm && typeof ctx.sm.saveSessionFile === "function") ctx.sm.saveSessionFile(session);
35
+ var spec = {
36
+ type: type,
37
+ idempotencyKey: "autonomous:" + sessionOrigin + ":" + run.id + ":" + kind + ":" + run.cancellationEpoch + ":" + run.wakeSequence,
38
+ ownerId: owner(session),
39
+ projectId: ctx.projectId,
40
+ target: { kind: "project-session-origin", id: sessionOrigin },
41
+ payload: { kind: kind, runId: run.id, epoch: run.cancellationEpoch, wakeSequence: run.wakeSequence, sessionOrigin: sessionOrigin, cliSessionId: session.cliSessionId || null },
42
+ runAt: Number(runAt),
43
+ };
44
+ var current = queued(session, run, kind);
45
+ return current ? scheduler.replaceQueued(current.id, spec) : scheduler.enqueue(spec);
46
+ }
47
+ function cancel(session, run, kinds) {
48
+ if (!run) return;
49
+ var jobs = scheduler.listJobs();
50
+ for (var i = 0; i < jobs.length; i++) {
51
+ if (jobs[i].state !== "queued" || !matching(jobs[i], session, run)) continue;
52
+ if (kinds && kinds.indexOf(jobs[i].payload.kind) === -1) continue;
53
+ scheduler.cancel(jobs[i].id, "autonomous_run_cancelled_or_replaced");
54
+ }
55
+ }
56
+ function findSession(job) {
57
+ var found = null;
58
+ ctx.sm.sessions.forEach(function (session) {
59
+ if (!found && session.sessionOriginId === job.target.id) found = session;
60
+ });
61
+ return found;
62
+ }
63
+ function exact(job) {
64
+ var session = findSession(job);
65
+ var run = session && session.autonomousRun;
66
+ if (!session || !run || run.id !== job.payload.runId) return null;
67
+ if (job.payload.kind !== "deadline" && owner(session) !== job.ownerId) return null;
68
+ if (Number(run.cancellationEpoch || 0) !== Number(job.payload.epoch)) return null;
69
+ if (run.state === "armed" || run.state === "paused" || ["completed", "stopped", "limit", "error"].indexOf(run.state) !== -1) return null;
70
+ if (job.payload.kind !== "deadline" && ctx.authorize && !ctx.authorize(session)) return null;
71
+ return { session: session, run: run };
72
+ }
73
+ function canRun(job) {
74
+ var current;
75
+ try { current = exact(job); } catch (error) { return true; }
76
+ if (!current) return true;
77
+ return job.payload.kind !== "continuation" || (!current.session.isProcessing && !current.session._queryStarting);
78
+ }
79
+ function dispatch(job, execution) {
80
+ var current = exact(job);
81
+ if (!current) throw new Error("The Until complete wake no longer belongs to an authorized active run.");
82
+ execution.recordMetadata({ projectId: ctx.projectId, runId: current.run.id, kind: job.payload.kind, sessionOrigin: job.target.id, epoch: job.payload.epoch });
83
+ execution.recordReceipt({ state: "dispatching", runId: current.run.id, kind: job.payload.kind, sessionOrigin: job.target.id, recordedAt: Date.now() });
84
+ return ctx.onWake(job.payload.kind, current.session, current.run);
85
+ }
86
+ function start() {
87
+ if (!unregister) unregister = scheduler.registerHandler(type, dispatch, { canRun: canRun });
88
+ }
89
+ return { schedule: schedule, cancel: cancel, start: start, shutdown: function () { if (unregister) { unregister(); unregister = null; } } };
90
+ }
91
+
92
+ module.exports = { attachAutonomousRunScheduler: attachAutonomousRunScheduler };
@@ -0,0 +1,61 @@
1
+ var STATES = ["queued", "claimed", "running", "completed", "failed", "cancelled", "interrupted"];
2
+
3
+ function boundedString(value, name) {
4
+ if (typeof value !== "string" || !value.trim() || value.length > 256) throw new Error("invalid " + name);
5
+ }
6
+
7
+ function timestamp(value, name) {
8
+ if (typeof value !== "number" || !isFinite(value) || value < 0) throw new Error("invalid " + name);
9
+ }
10
+
11
+ function jsonSize(value, name, maximum) {
12
+ var serialized;
13
+ try { serialized = JSON.stringify(value); } catch (error) { throw new Error("invalid " + name); }
14
+ if (serialized === undefined || Buffer.byteLength(serialized, "utf8") > maximum) throw new Error("invalid " + name);
15
+ }
16
+
17
+ function validateExecution(job) {
18
+ if (job.state === "queued" || job.state === "cancelled") {
19
+ if (job.execution !== null) throw new Error("invalid execution for " + job.state + " job");
20
+ return;
21
+ }
22
+ if (!job.execution || typeof job.execution !== "object" || Array.isArray(job.execution)) {
23
+ throw new Error("missing saved execution");
24
+ }
25
+ boundedString(job.execution.claimId, "saved claim id");
26
+ timestamp(job.execution.claimedAt, "saved claimedAt");
27
+ if (job.execution.startedAt !== undefined) timestamp(job.execution.startedAt, "saved startedAt");
28
+ if (job.execution.completedAt !== undefined) timestamp(job.execution.completedAt, "saved completedAt");
29
+ if (job.execution.failedAt !== undefined) timestamp(job.execution.failedAt, "saved failedAt");
30
+ if (job.execution.interruptedAt !== undefined) timestamp(job.execution.interruptedAt, "saved interruptedAt");
31
+ jsonSize(job.execution, "saved execution", 256 * 1024);
32
+ }
33
+
34
+ function validateSavedJobs(jobs) {
35
+ var ids = Object.create(null);
36
+ var keys = Object.create(null);
37
+ for (var i = 0; i < jobs.length; i++) {
38
+ var job = jobs[i];
39
+ if (!job || typeof job !== "object" || Array.isArray(job)) throw new Error("invalid saved job");
40
+ boundedString(job.id, "saved job id");
41
+ boundedString(job.idempotencyKey, "saved idempotencyKey");
42
+ boundedString(job.type, "saved job type");
43
+ boundedString(job.ownerId, "saved ownerId");
44
+ boundedString(job.projectId, "saved projectId");
45
+ if (ids[job.id] || keys[job.idempotencyKey]) throw new Error("duplicate saved job identity");
46
+ if (STATES.indexOf(job.state) === -1) throw new Error("invalid saved state");
47
+ timestamp(job.runAt, "saved runAt");
48
+ timestamp(job.createdAt, "saved createdAt");
49
+ timestamp(job.updatedAt, "saved updatedAt");
50
+ if (!job.target || typeof job.target !== "object" || Array.isArray(job.target)) throw new Error("invalid saved target");
51
+ boundedString(job.target.kind, "saved target kind");
52
+ boundedString(job.target.id, "saved target id");
53
+ jsonSize(job.target, "saved target", 4096);
54
+ jsonSize(job.payload, "saved payload", 256 * 1024);
55
+ validateExecution(job);
56
+ ids[job.id] = true;
57
+ keys[job.idempotencyKey] = true;
58
+ }
59
+ }
60
+
61
+ module.exports = { validateSavedJobs: validateSavedJobs };
@@ -0,0 +1,185 @@
1
+ var fs = require("fs");
2
+ var path = require("path");
3
+ var crypto = require("crypto");
4
+
5
+ function clone(value) {
6
+ return JSON.parse(JSON.stringify(value));
7
+ }
8
+
9
+ function makeError(message, code, cause) {
10
+ var error = new Error(message);
11
+ error.code = code;
12
+ if (cause) error.cause = cause;
13
+ return error;
14
+ }
15
+
16
+ function pidIsAlive(pid) {
17
+ if (typeof pid !== "number" || !isFinite(pid) || Math.floor(pid) !== pid || pid <= 0) return false;
18
+ try {
19
+ process.kill(pid, 0);
20
+ return true;
21
+ } catch (error) {
22
+ return !!(error && error.code !== "ESRCH");
23
+ }
24
+ }
25
+
26
+ function attachDurableSchedulerStore(ctx) {
27
+ var filePath = ctx.filePath;
28
+ var namespace = ctx.namespace;
29
+ var fsImpl = ctx.fs || fs;
30
+ var lockPath = filePath + ".lock";
31
+ var guardPath = lockPath + ".acquire";
32
+ var lockToken = crypto.randomBytes(16).toString("hex");
33
+ var lockFd = null;
34
+ var closed = false;
35
+
36
+ if (!filePath || !namespace) throw new Error("Scheduler store requires filePath and namespace");
37
+
38
+ function acquireLockUnderGuard() {
39
+ try {
40
+ lockFd = fsImpl.openSync(lockPath, "wx", 0o600);
41
+ fsImpl.writeFileSync(lockFd, JSON.stringify({ pid: process.pid, namespace: namespace, token: lockToken }) + "\n");
42
+ return;
43
+ } catch (error) {
44
+ if (!error || error.code !== "EEXIST") {
45
+ if (lockFd !== null) {
46
+ try { fsImpl.closeSync(lockFd); } catch (closeError) {}
47
+ lockFd = null;
48
+ try { fsImpl.unlinkSync(lockPath); } catch (unlinkError) {}
49
+ }
50
+ throw makeError("Unable to lock scheduler store: " + error.message, "SCHEDULER_STORE_LOCK", error);
51
+ }
52
+ }
53
+
54
+ var lock;
55
+ try {
56
+ lock = JSON.parse(fsImpl.readFileSync(lockPath, "utf8"));
57
+ } catch (error) {
58
+ throw makeError("Scheduler store lock is unreadable", "SCHEDULER_STORE_LOCK", error);
59
+ }
60
+ if (pidIsAlive(lock.pid)) {
61
+ throw makeError("Scheduler store is already owned by process " + lock.pid, "SCHEDULER_STORE_LOCKED");
62
+ }
63
+ var stalePath = lockPath + ".stale-" + process.pid + "-" + lockToken;
64
+ try {
65
+ fsImpl.renameSync(lockPath, stalePath);
66
+ lockFd = fsImpl.openSync(lockPath, "wx", 0o600);
67
+ fsImpl.writeFileSync(lockFd, JSON.stringify({ pid: process.pid, namespace: namespace, token: lockToken }) + "\n");
68
+ try { fsImpl.unlinkSync(stalePath); } catch (cleanupError) {}
69
+ } catch (error) {
70
+ if (lockFd !== null) {
71
+ try { fsImpl.closeSync(lockFd); } catch (closeError) {}
72
+ lockFd = null;
73
+ try { fsImpl.unlinkSync(lockPath); } catch (unlinkError) {}
74
+ }
75
+ try { fsImpl.unlinkSync(stalePath); } catch (cleanupError) {}
76
+ throw makeError("Unable to recover stale scheduler store lock", "SCHEDULER_STORE_LOCK", error);
77
+ }
78
+ }
79
+
80
+ function acquireLock() {
81
+ fsImpl.mkdirSync(path.dirname(filePath), { recursive: true, mode: 0o700 });
82
+ try {
83
+ fsImpl.mkdirSync(guardPath, { mode: 0o700 });
84
+ } catch (error) {
85
+ if (error && error.code === "EEXIST") {
86
+ throw makeError("Scheduler store acquisition is already in progress or requires manual stale-guard recovery", "SCHEDULER_STORE_LOCKED", error);
87
+ }
88
+ throw makeError("Unable to guard scheduler store acquisition", "SCHEDULER_STORE_LOCK", error);
89
+ }
90
+
91
+ var acquisitionError = null;
92
+ try {
93
+ acquireLockUnderGuard();
94
+ } catch (error) {
95
+ acquisitionError = error;
96
+ }
97
+ try {
98
+ fsImpl.rmdirSync(guardPath);
99
+ } catch (error) {
100
+ if (lockFd !== null) {
101
+ try { fsImpl.closeSync(lockFd); } catch (closeError) {}
102
+ lockFd = null;
103
+ try { fsImpl.unlinkSync(lockPath); } catch (unlinkError) {}
104
+ }
105
+ throw makeError("Unable to release scheduler store acquisition guard", "SCHEDULER_STORE_LOCK", error);
106
+ }
107
+ if (acquisitionError) throw acquisitionError;
108
+ }
109
+
110
+ function emptyData() {
111
+ return { version: 1, namespace: namespace, jobs: [] };
112
+ }
113
+
114
+ function load() {
115
+ if (closed) throw makeError("Scheduler store is closed", "SCHEDULER_STORE_CLOSED");
116
+ if (!fsImpl.existsSync(filePath)) return emptyData();
117
+ var parsed;
118
+ try {
119
+ parsed = JSON.parse(fsImpl.readFileSync(filePath, "utf8"));
120
+ } catch (error) {
121
+ throw makeError("Scheduler store is corrupt or unreadable", "SCHEDULER_STORE_CORRUPT", error);
122
+ }
123
+ if (!parsed || parsed.version !== 1 || parsed.namespace !== namespace || !Array.isArray(parsed.jobs)) {
124
+ throw makeError("Scheduler store has an invalid schema or namespace", "SCHEDULER_STORE_CORRUPT");
125
+ }
126
+ return clone(parsed);
127
+ }
128
+
129
+ function save(data) {
130
+ if (closed) throw makeError("Scheduler store is closed", "SCHEDULER_STORE_CLOSED");
131
+ var serialized;
132
+ try {
133
+ serialized = JSON.stringify(data, null, 2) + "\n";
134
+ } catch (error) {
135
+ throw makeError("Scheduler store data is not serializable", "SCHEDULER_STORE_WRITE", error);
136
+ }
137
+ var temporary = filePath + ".tmp-" + process.pid;
138
+ var temporaryFd = null;
139
+ try {
140
+ temporaryFd = fsImpl.openSync(temporary, "w", 0o600);
141
+ fsImpl.writeFileSync(temporaryFd, serialized, "utf8");
142
+ if (typeof fsImpl.fsyncSync === "function") fsImpl.fsyncSync(temporaryFd);
143
+ fsImpl.closeSync(temporaryFd);
144
+ temporaryFd = null;
145
+ fsImpl.renameSync(temporary, filePath);
146
+ try { fsImpl.chmodSync(filePath, 0o600); } catch (error) {}
147
+ var directoryFd = null;
148
+ try {
149
+ directoryFd = fsImpl.openSync(path.dirname(filePath), "r");
150
+ if (typeof fsImpl.fsyncSync === "function") fsImpl.fsyncSync(directoryFd);
151
+ } catch (error) {
152
+ var unsupported = error && (error.code === "EINVAL" || error.code === "ENOTSUP" || error.code === "EISDIR" || error.code === "EBADF" || error.code === "EPERM" || error.code === "EACCES");
153
+ if (!unsupported) throw error;
154
+ } finally {
155
+ if (directoryFd !== null) {
156
+ try { fsImpl.closeSync(directoryFd); } catch (closeError) {}
157
+ }
158
+ }
159
+ } catch (error) {
160
+ if (temporaryFd !== null) {
161
+ try { fsImpl.closeSync(temporaryFd); } catch (closeError) {}
162
+ }
163
+ try { fsImpl.unlinkSync(temporary); } catch (cleanupError) {}
164
+ throw makeError("Scheduler store write failed", "SCHEDULER_STORE_WRITE", error);
165
+ }
166
+ }
167
+
168
+ function close() {
169
+ if (closed) return;
170
+ closed = true;
171
+ if (lockFd !== null) {
172
+ try { fsImpl.closeSync(lockFd); } catch (error) {}
173
+ lockFd = null;
174
+ }
175
+ try {
176
+ var lock = JSON.parse(fsImpl.readFileSync(lockPath, "utf8"));
177
+ if (lock.pid === process.pid && lock.namespace === namespace && lock.token === lockToken) fsImpl.unlinkSync(lockPath);
178
+ } catch (error) {}
179
+ }
180
+
181
+ acquireLock();
182
+ return { load: load, save: save, close: close, filePath: filePath, namespace: namespace };
183
+ }
184
+
185
+ module.exports = { attachDurableSchedulerStore: attachDurableSchedulerStore };