clay-server 4.1.0-beta.5 → 4.1.0-beta.7
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/LICENSE +661 -21
- package/LICENSES/MIT-legacy.txt +21 -0
- package/LICENSES/README.md +7 -0
- package/README.md +2 -2
- package/bin/cli.js +0 -2
- package/lib/autonomous-run-record.js +32 -0
- package/lib/autonomous-run-scheduler.js +92 -0
- package/lib/daemon.js +0 -2
- package/lib/default-ai-runtime.js +117 -0
- package/lib/durable-scheduler-record.js +61 -0
- package/lib/durable-scheduler-store.js +185 -0
- package/lib/durable-scheduler.js +495 -0
- package/lib/git-cli.js +1 -0
- package/lib/issue-work-session.js +10 -0
- package/lib/issues-comments.js +40 -0
- package/lib/issues-commit-verification.js +30 -0
- package/lib/issues-mcp-server.js +27 -0
- package/lib/issues-schema.js +119 -0
- package/lib/issues-service.js +339 -0
- package/lib/issues-store.js +210 -0
- package/lib/loop-cron.js +47 -0
- package/lib/loop-durable-schedule.js +159 -0
- package/lib/loop-guidance.js +35 -0
- package/lib/loop-registry-occurrence.js +76 -0
- package/lib/model-entry.js +15 -0
- package/lib/project-autonomous-run.js +498 -0
- package/lib/project-capsule-turn.js +1 -0
- package/lib/project-connection.js +6 -1
- package/lib/project-delegated-session.js +1 -0
- package/lib/project-filesystem.js +10 -10
- package/lib/project-full-access.js +86 -0
- package/lib/project-http.js +3 -1
- package/lib/project-issue-launch.js +154 -0
- package/lib/project-issues.js +58 -0
- package/lib/project-logs-mcp-server.js +8 -12
- package/lib/project-loop-interview.js +163 -0
- package/lib/project-loop.js +150 -64
- package/lib/project-message-delivery.js +4 -2
- package/lib/project-models.js +71 -3
- package/lib/project-pair-autonomous-stop.js +16 -0
- package/lib/project-pair-owned-stop.js +18 -0
- package/lib/project-pair-task-control.js +14 -1
- package/lib/project-pending-message-queue.js +264 -0
- package/lib/project-scheduled-messages.js +316 -0
- package/lib/project-scheduled-task-execution.js +236 -0
- package/lib/project-scheduled-tasks.js +481 -0
- package/lib/project-session-pair.js +16 -19
- package/lib/project-sessions.js +114 -31
- package/lib/project-tool-llm-runtime.js +48 -0
- package/lib/project-user-message.js +202 -60
- package/lib/project-workspace-query.js +25 -0
- package/lib/project.js +361 -69
- package/lib/public/app.js +85 -22
- package/lib/public/css/default-ai.css +33 -0
- package/lib/public/css/home-hub.css +15 -2
- package/lib/public/css/home-sidebar.css +50 -2
- package/lib/public/css/input.css +160 -5
- package/lib/public/css/issues.css +101 -0
- package/lib/public/css/messages.css +19 -0
- package/lib/public/css/pane.css +58 -7
- package/lib/public/css/pending-message-queue.css +187 -0
- package/lib/public/css/rewind.css +11 -2
- package/lib/public/css/scheduled-tasks.css +122 -0
- package/lib/public/index.html +42 -150
- package/lib/public/modules/app-connection.js +3 -0
- package/lib/public/modules/app-cursors.js +29 -12
- package/lib/public/modules/app-loop-ui.js +4 -8
- package/lib/public/modules/app-message-router.js +8 -0
- package/lib/public/modules/app-messages.js +34 -9
- package/lib/public/modules/app-projects.js +2 -0
- package/lib/public/modules/app-rate-limit.js +8 -5
- package/lib/public/modules/autonomous-run.js +285 -0
- package/lib/public/modules/clay-file-links.js +111 -0
- package/lib/public/modules/clay-issue-links.js +131 -0
- package/lib/public/modules/clay-log-links.js +2 -0
- package/lib/public/modules/cursor-sharing-preference.js +38 -0
- package/lib/public/modules/default-ai.js +377 -0
- package/lib/public/modules/filebrowser.js +43 -2
- package/lib/public/modules/home-sidebar.js +16 -8
- package/lib/public/modules/home-surface.js +5 -0
- package/lib/public/modules/input.js +11 -20
- package/lib/public/modules/issues-context.js +117 -0
- package/lib/public/modules/issues-render.js +29 -0
- package/lib/public/modules/issues.js +270 -0
- package/lib/public/modules/loop-interview.js +151 -0
- package/lib/public/modules/markdown.js +4 -0
- package/lib/public/modules/message-delivery.js +5 -3
- package/lib/public/modules/pending-message-image-editor.js +143 -0
- package/lib/public/modules/pending-message-queue-model.js +22 -0
- package/lib/public/modules/pending-message-queue.js +449 -0
- package/lib/public/modules/project-logs.js +2 -2
- package/lib/public/modules/scheduled-message-state.js +46 -0
- package/lib/public/modules/scheduled-task-edit-state.js +7 -0
- package/lib/public/modules/scheduled-task-engine.js +48 -0
- package/lib/public/modules/scheduled-task-form.js +122 -0
- package/lib/public/modules/scheduled-task-recurrence.js +46 -0
- package/lib/public/modules/scheduled-task-schedule-rule.js +115 -0
- package/lib/public/modules/scheduled-tasks.js +459 -0
- package/lib/public/modules/scheduler-config.js +6 -52
- package/lib/public/modules/scheduler.js +9 -21
- package/lib/public/modules/sidebar-mobile.js +3 -0
- package/lib/public/modules/sidebar.js +0 -11
- package/lib/public/modules/split-view.js +2 -0
- package/lib/public/modules/sticky-note-markdown.js +1 -0
- package/lib/public/modules/terminal.js +2 -0
- package/lib/public/modules/tool-palette-order.js +3 -3
- package/lib/public/modules/tools.js +5 -2
- package/lib/public/style.css +3 -0
- package/lib/record-revision.js +11 -0
- package/lib/schedule-rule.js +151 -0
- package/lib/schedule-validation.js +124 -0
- package/lib/scheduled-run-state.js +87 -0
- package/lib/scheduled-task-records.js +127 -0
- package/lib/scheduler.js +179 -196
- package/lib/sdk-bridge.js +157 -22
- package/lib/sdk-message-processor.js +3 -0
- package/lib/server-cursor-sharing.js +65 -0
- package/lib/server-default-ai.js +265 -0
- package/lib/server-durable-scheduler.js +26 -0
- package/lib/server-home-chat.js +29 -9
- package/lib/server-home-clay-entry.js +4 -1
- package/lib/server-home-debate-planning.js +32 -14
- package/lib/server-home-mate-creation.js +30 -11
- package/lib/server-home-models.js +14 -6
- package/lib/server-issue-links.js +18 -0
- package/lib/server.js +63 -0
- package/lib/session-notes-mcp-server.js +1 -1
- package/lib/session-pair-factory.js +42 -4
- package/lib/session-pair-query-start.js +22 -0
- package/lib/session-query-tool-bridge.js +52 -0
- package/lib/session-search.js +4 -1
- package/lib/session-split-groups.js +35 -1
- package/lib/sessions.js +51 -3
- package/lib/tool-llm.js +1 -0
- package/lib/users-default-ai-preferences.js +80 -0
- package/lib/users-home-surface-preferences.js +2 -1
- package/lib/users-preferences.js +35 -0
- package/lib/users-scheduled-task-preferences.js +44 -0
- package/lib/users.js +16 -0
- package/lib/workspace-query-projection.js +29 -0
- package/lib/workspace-query-service.js +19 -29
- package/lib/ws-schema.js +61 -1
- package/lib/yoke/adapters/antigravity.js +1 -0
- package/lib/yoke/adapters/claude-worker.js +11 -6
- package/lib/yoke/adapters/claude.js +35 -1
- package/lib/yoke/adapters/codex.js +3 -0
- package/lib/yoke/claude-user-input.js +2 -0
- package/lib/yoke/index.js +2 -0
- package/lib/yoke/mcp-bridge-server.js +7 -1
- package/lib/yoke/package.json +1 -1
- package/lib/yoke/session-tool-transport.js +53 -0
- package/lib/yoke/user-input.js +21 -7
- package/package.json +5 -6
- package/bin/claude-relay.js +0 -6
- package/lib/public/modules/app-loop-wizard.js +0 -439
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Chad
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Licensing scope
|
|
2
|
+
|
|
3
|
+
Clay Studio as a whole is licensed under the GNU Affero General Public License, version 3 only (AGPL-3.0-only), under the terms in the root [LICENSE](../LICENSE). This project-wide license does not narrow the grants that apply to preexisting portions previously covered by the project’s MIT license.
|
|
4
|
+
|
|
5
|
+
Preexisting MIT-covered portions retain the grants and applicable copyright and permission notices from the MIT license. The byte-for-byte prior license text is preserved in [MIT-legacy.txt](MIT-legacy.txt), and this notice does not revoke or alter those earlier grants or notices.
|
|
6
|
+
|
|
7
|
+
Third-party components and assets retain their own licenses and notices. In particular, the bundled Source Serif 4 font remains under the [SIL Open Font License 1.1](../lib/public/fonts/source-serif-4/LICENSE.md).
|
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
<a href="https://www.npmjs.com/package/clay-server"><img src="https://img.shields.io/npm/v/clay-server" alt="npm version" /></a>
|
|
15
15
|
<a href="https://www.npmjs.com/package/clay-server"><img src="https://img.shields.io/npm/dw/clay-server" alt="npm downloads" /></a>
|
|
16
16
|
<a href="https://github.com/chadbyte/clay"><img src="https://img.shields.io/github/stars/chadbyte/clay" alt="GitHub stars" /></a>
|
|
17
|
-
<a href="https://github.com/chadbyte/clay/blob/main/LICENSE"><img src="https://img.shields.io/badge/License-
|
|
17
|
+
<a href="https://github.com/chadbyte/clay/blob/main/LICENSE"><img src="https://img.shields.io/badge/License-AGPL--3.0--only-blue.svg" alt="License: AGPL-3.0-only" /></a>
|
|
18
18
|
</p>
|
|
19
19
|
|
|
20
20
|
Clay Studio turns local coding agents into a shared, persistent team. Run different agents from one browser workspace, join a teammate's live session, keep project knowledge between conversations, and automate work without handing your workspace to another cloud.
|
|
@@ -105,4 +105,4 @@ Clay Studio is not affiliated with the providers of the coding agents it support
|
|
|
105
105
|
|
|
106
106
|
## License
|
|
107
107
|
|
|
108
|
-
[
|
|
108
|
+
Clay Studio as a whole is licensed under the [GNU Affero General Public License, version 3 only](LICENSE) (AGPL-3.0-only). Preexisting portions previously covered by the project’s MIT license retain the grants and applicable notices from that license; see [LICENSES/MIT-legacy.txt](LICENSES/MIT-legacy.txt) and the [licensing scope notice](LICENSES/README.md). Third-party components remain under their respective licenses.
|
package/bin/cli.js
CHANGED
|
@@ -6,10 +6,8 @@ if (_nodeMajor < 20) {
|
|
|
6
6
|
console.error("");
|
|
7
7
|
console.error("\x1b[31m[clay] Node.js 20+ is required (current: " + process.version + ")\x1b[0m");
|
|
8
8
|
console.error("[clay] The Claude Agent SDK 0.2.40+ requires Node 20 for Symbol.dispose support.");
|
|
9
|
-
console.error("[clay] If you cannot upgrade Node, use claude-relay@2.4.3 which supports Node 18.");
|
|
10
9
|
console.error("");
|
|
11
10
|
console.error(" Upgrade Node: nvm install 22 && nvm use 22");
|
|
12
|
-
console.error(" Or use older: npx claude-relay@2.4.3");
|
|
13
11
|
console.error("");
|
|
14
12
|
process.exit(78);
|
|
15
13
|
}
|
|
@@ -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 };
|
package/lib/daemon.js
CHANGED
|
@@ -5,10 +5,8 @@ var nodeMajor = parseInt(process.versions.node.split(".")[0], 10);
|
|
|
5
5
|
if (nodeMajor < 20) {
|
|
6
6
|
console.error("\x1b[31m[clay] Node.js 20+ is required (current: " + process.version + ")\x1b[0m");
|
|
7
7
|
console.error("[clay] The Claude Agent SDK 0.2.40+ requires Node 20 for Symbol.dispose support.");
|
|
8
|
-
console.error("[clay] If you cannot upgrade Node, use claude-relay@2.4.3 which supports Node 18.");
|
|
9
8
|
console.error("");
|
|
10
9
|
console.error(" Upgrade Node: nvm install 22 && nvm use 22");
|
|
11
|
-
console.error(" Or use older: npx claude-relay@2.4.3");
|
|
12
10
|
process.exit(78); // EX_CONFIG — fatal config error, don't auto-restart
|
|
13
11
|
}
|
|
14
12
|
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// Pure and injected resolution for a Clay-owned Default AI runtime.
|
|
2
|
+
|
|
3
|
+
var modelEntryMatches = require("./model-entry").modelEntryMatches;
|
|
4
|
+
var modelEntryValue = require("./model-entry").modelEntryValue;
|
|
5
|
+
|
|
6
|
+
var KNOWN_VENDORS = ["claude", "codex", "grok", "kimi", "copilot", "qwen", "junie", "antigravity", "opencode", "kiro"];
|
|
7
|
+
var VENDOR_FAMILY_ORDER = {
|
|
8
|
+
claude: ["opus", "sonnet", "haiku"],
|
|
9
|
+
codex: ["astra", "sol", "gpt", "o"],
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
function text(entry) {
|
|
13
|
+
if (typeof entry === "string") return entry;
|
|
14
|
+
return entry && [entry.value, entry.id, entry.resolvedModel, entry.displayName, entry.name].filter(Boolean).join(" ") || "";
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function selectTopModel(vendor, catalog) {
|
|
18
|
+
var models = catalog && Array.isArray(catalog.models) ? catalog.models : [];
|
|
19
|
+
if (!models.length) return "";
|
|
20
|
+
var defaultModel = catalog.defaultModel;
|
|
21
|
+
if (KNOWN_VENDORS.indexOf(vendor) === -1 && defaultModel) {
|
|
22
|
+
for (var di = 0; di < models.length; di++) if (modelEntryMatches(models[di], defaultModel)) return modelEntryValue(models[di]);
|
|
23
|
+
}
|
|
24
|
+
var familyOrder = VENDOR_FAMILY_ORDER[vendor] || [];
|
|
25
|
+
if (familyOrder.length) {
|
|
26
|
+
for (var fi = 0; fi < familyOrder.length; fi++) {
|
|
27
|
+
for (var mi = 0; mi < models.length; mi++) if (new RegExp("(^|[^a-z])" + familyOrder[fi] + "([^a-z]|$)", "i").test(text(models[mi]))) return modelEntryValue(models[mi]);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
if (defaultModel) for (var ci = 0; ci < models.length; ci++) if (modelEntryMatches(models[ci], defaultModel)) return modelEntryValue(models[ci]);
|
|
31
|
+
return modelEntryValue(models[0]);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function modelEntry(models, value) {
|
|
35
|
+
for (var i = 0; i < models.length; i++) if (modelEntryMatches(models[i], value)) return models[i];
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function effortValue(entry) {
|
|
40
|
+
if (typeof entry === "string") return entry;
|
|
41
|
+
return entry && (entry.value || entry.id || entry.name || "");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function supportedEfforts(model, catalog) {
|
|
45
|
+
if (model && Array.isArray(model.supportedEffortLevels)) return { levels: model.supportedEffortLevels.map(effortValue).filter(Boolean), specified: true };
|
|
46
|
+
if (catalog && Array.isArray(catalog.effortLevels)) return { levels: catalog.effortLevels.map(effortValue).filter(Boolean), specified: true };
|
|
47
|
+
return { levels: [], specified: false };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function resolveEffort(preference, selectedModel, catalog) {
|
|
51
|
+
var support = supportedEfforts(selectedModel, catalog);
|
|
52
|
+
var supported = support.levels;
|
|
53
|
+
var explicit = preference && preference.effort || "";
|
|
54
|
+
if (explicit && support.specified && supported.indexOf(explicit) === -1) return { ok: false, error: "The saved Default AI effort is unavailable for the selected model." };
|
|
55
|
+
if (explicit) return { ok: true, effort: explicit };
|
|
56
|
+
if (preference && Object.prototype.hasOwnProperty.call(preference, "effort")) return { ok: true, effort: "" };
|
|
57
|
+
var modelDefault = selectedModel && (selectedModel.defaultReasoningEffort || selectedModel.defaultEffort) || "";
|
|
58
|
+
if (modelDefault && (!support.specified || supported.indexOf(modelDefault) !== -1)) return { ok: true, effort: modelDefault };
|
|
59
|
+
var providerDefault = catalog && catalog.defaultEffort || "";
|
|
60
|
+
if (providerDefault && (!support.specified || supported.indexOf(providerDefault) !== -1)) return { ok: true, effort: providerDefault };
|
|
61
|
+
return { ok: true, effort: "" };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function exactModel(models, requested) {
|
|
65
|
+
if (!requested) return "";
|
|
66
|
+
for (var i = 0; i < models.length; i++) if (modelEntryMatches(models[i], requested)) return modelEntryValue(models[i]);
|
|
67
|
+
return "";
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function identityError(options) {
|
|
71
|
+
if (options.multiUser === true && !(typeof options.userId === "string" && options.userId)) return "Authenticated user identity is required.";
|
|
72
|
+
return "";
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function resolveDefaultAiRuntime(options) {
|
|
76
|
+
options = options || {};
|
|
77
|
+
if (options.identity && typeof options.identity === "object") {
|
|
78
|
+
options = Object.assign({}, options.identity, options);
|
|
79
|
+
}
|
|
80
|
+
var identityProblem = identityError(options);
|
|
81
|
+
if (identityProblem) return { ready: false, error: identityProblem };
|
|
82
|
+
if (typeof options.getPreference !== "function") return { ready: false, error: "Default AI preference reader is unavailable." };
|
|
83
|
+
var stored = await options.getPreference(options.userId);
|
|
84
|
+
if (!stored || stored.error) return { ready: false, error: stored && stored.error || "Default AI preference could not be read." };
|
|
85
|
+
var preference = stored.preference || null;
|
|
86
|
+
var getAvailability = options.getInstalledVendors || options.getAvailability;
|
|
87
|
+
var installed = typeof getAvailability === "function" ? await getAvailability(options.userId) : options.installedVendors || [];
|
|
88
|
+
installed = Array.isArray(installed) ? installed : [];
|
|
89
|
+
var vendor = preference && preference.vendor || options.preferredVendor || installed[0] || "";
|
|
90
|
+
if (!vendor || installed.indexOf(vendor) === -1) return { ready: false, error: "Default AI vendor is unavailable.", preference: preference, vendor: vendor };
|
|
91
|
+
if (typeof options.getCatalog !== "function") return { ready: false, error: "Default AI model catalog is unavailable.", preference: preference, vendor: vendor };
|
|
92
|
+
var catalog = await options.getCatalog(vendor, options.userId);
|
|
93
|
+
var models = catalog && Array.isArray(catalog.models) ? catalog.models : [];
|
|
94
|
+
if (!catalog || catalog.status !== "ready" || !models.length) return { ready: false, error: catalog && catalog.error || "Default AI models are unavailable.", preference: preference, vendor: vendor, catalog: catalog || null };
|
|
95
|
+
var model = preference && preference.model ? exactModel(models, preference.model) : "";
|
|
96
|
+
if (preference && preference.model && !model) return { ready: false, error: "The saved Default AI model is unavailable.", preference: preference, vendor: vendor, catalog: catalog };
|
|
97
|
+
if (!model) model = selectTopModel(vendor, catalog);
|
|
98
|
+
if (!model) return { ready: false, error: "No usable Default AI model is available.", preference: preference, vendor: vendor, catalog: catalog };
|
|
99
|
+
var selectedModel = modelEntry(models, model);
|
|
100
|
+
var effort = resolveEffort(preference, selectedModel, catalog);
|
|
101
|
+
if (!effort.ok) return { ready: false, error: effort.error, preference: preference, vendor: vendor, model: model, catalog: catalog };
|
|
102
|
+
return { ready: true, vendor: vendor, model: model, effort: effort.effort, preference: preference, source: stored.source || (preference ? "explicit" : "default"), catalog: catalog };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function createDefaultAiRuntimeResolver(deps) {
|
|
106
|
+
deps = deps || {};
|
|
107
|
+
return {
|
|
108
|
+
resolve: function (options) { return resolveDefaultAiRuntime(Object.assign({}, deps, options || {})); },
|
|
109
|
+
save: function (userId, preference) {
|
|
110
|
+
if (deps.multiUser === true && !(typeof userId === "string" && userId)) return Promise.resolve({ ok: false, error: "Authenticated user identity is required." });
|
|
111
|
+
if (typeof deps.savePreference !== "function") return Promise.resolve({ ok: false, error: "Default AI preference writer is unavailable." });
|
|
112
|
+
return Promise.resolve(deps.savePreference(userId, preference));
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
module.exports = { resolveDefaultAiRuntime: resolveDefaultAiRuntime, createDefaultAiRuntimeResolver: createDefaultAiRuntimeResolver, selectTopModel: selectTopModel, exactModel: exactModel, resolveEffort: resolveEffort, KNOWN_VENDORS: KNOWN_VENDORS, VENDOR_FAMILY_ORDER: VENDOR_FAMILY_ORDER };
|
|
@@ -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 };
|