@the-open-engine/zeroshot 6.11.0 → 6.13.0
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/cli/commands/providers.js +13 -13
- package/cli/index.js +77 -35
- package/cli/lib/first-run.js +12 -11
- package/cli/lib/update-checker.js +517 -132
- package/lib/agent-cli-provider/adapters/opencode.d.ts.map +1 -1
- package/lib/agent-cli-provider/adapters/opencode.js +3 -0
- package/lib/agent-cli-provider/adapters/opencode.js.map +1 -1
- package/lib/agent-cli-provider/types.d.ts +1 -0
- package/lib/agent-cli-provider/types.d.ts.map +1 -1
- package/lib/agent-cli-provider/types.js.map +1 -1
- package/lib/cluster/client.cjs +146 -0
- package/lib/cluster/client.d.ts +44 -0
- package/lib/cluster/client.mjs +141 -0
- package/lib/cluster/connection.cjs +382 -0
- package/lib/cluster/connection.d.ts +41 -0
- package/lib/cluster/connection.mjs +378 -0
- package/lib/cluster/errors.cjs +44 -0
- package/lib/cluster/errors.d.ts +23 -0
- package/lib/cluster/errors.mjs +32 -0
- package/lib/cluster/frames.cjs +49 -0
- package/lib/cluster/frames.d.ts +12 -0
- package/lib/cluster/frames.mjs +45 -0
- package/lib/cluster/generated/protocol-schema.cjs +5 -0
- package/lib/cluster/generated/protocol-schema.d.ts +1 -0
- package/lib/cluster/generated/protocol-schema.mjs +2 -0
- package/lib/cluster/generated/protocol.cjs +22 -0
- package/lib/cluster/generated/protocol.d.ts +781 -0
- package/lib/cluster/generated/protocol.mjs +19 -0
- package/lib/cluster/index.cjs +40 -0
- package/lib/cluster/index.d.ts +10 -0
- package/lib/cluster/index.mjs +6 -0
- package/lib/cluster/queue.cjs +71 -0
- package/lib/cluster/queue.d.ts +15 -0
- package/lib/cluster/queue.mjs +67 -0
- package/lib/cluster/socket.cjs +15 -0
- package/lib/cluster/socket.d.ts +11 -0
- package/lib/cluster/socket.mjs +12 -0
- package/lib/cluster/subscriptions.cjs +270 -0
- package/lib/cluster/subscriptions.d.ts +69 -0
- package/lib/cluster/subscriptions.mjs +264 -0
- package/lib/cluster/validators.cjs +46 -0
- package/lib/cluster/validators.d.ts +3 -0
- package/lib/cluster/validators.mjs +42 -0
- package/lib/settings.js +195 -23
- package/lib/setup-apply.js +45 -32
- package/lib/setup-undo.js +25 -16
- package/package.json +25 -3
- package/scripts/build-cluster.js +43 -0
- package/scripts/generate-cluster-types.js +203 -0
- package/scripts/release-dry-run.js +6 -6
- package/scripts/rust-distribution.js +933 -0
- package/src/agent/agent-hook-executor.js +14 -6
- package/src/agent/agent-lifecycle.js +25 -8
- package/src/agent/agent-task-executor.js +711 -139
- package/src/agent/output-reformatter.js +54 -41
- package/src/agent/pr-verification.js +11 -3
- package/src/agent/task-execution-handle.js +373 -0
- package/src/agent-cli-provider/adapters/opencode.ts +3 -0
- package/src/agent-cli-provider/types.ts +1 -0
- package/src/agent-wrapper.js +5 -2
- package/src/cluster/client.ts +199 -0
- package/src/cluster/connection.ts +300 -0
- package/src/cluster/errors.ts +32 -0
- package/src/cluster/frames.ts +44 -0
- package/src/cluster/generated/protocol-schema.ts +2 -0
- package/src/cluster/generated/protocol.ts +183 -0
- package/src/cluster/index.ts +38 -0
- package/src/cluster/queue.ts +84 -0
- package/src/cluster/socket.ts +31 -0
- package/src/cluster/subscriptions.ts +256 -0
- package/src/cluster/validators.ts +56 -0
- package/src/cluster/ws.d.ts +7 -0
- package/src/isolation-manager.js +16 -0
- package/src/issue-providers/jira-provider.js +1 -1
- package/src/issue-providers/linear-provider.js +4 -4
- package/task-lib/runner.js +3 -0
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
var _a;
|
|
2
|
+
import { ClusterProtocolError, ClusterStateError } from './errors.mjs';
|
|
3
|
+
import { isRecord } from './frames.mjs';
|
|
4
|
+
import { assertDefinition } from './validators.mjs';
|
|
5
|
+
class SubscriptionStream {
|
|
6
|
+
connection;
|
|
7
|
+
registration;
|
|
8
|
+
parse;
|
|
9
|
+
#done = false;
|
|
10
|
+
constructor(connection, registration, parse) {
|
|
11
|
+
this.connection = connection;
|
|
12
|
+
this.registration = registration;
|
|
13
|
+
this.parse = parse;
|
|
14
|
+
}
|
|
15
|
+
get subscriptionId() { return this.registration.id; }
|
|
16
|
+
get retainedCount() { return this.registration.queue.retainedCount; }
|
|
17
|
+
[Symbol.asyncIterator]() { return this; }
|
|
18
|
+
async next() {
|
|
19
|
+
if (this.#done)
|
|
20
|
+
return { done: true, value: undefined };
|
|
21
|
+
const queued = await this.registration.queue.recv();
|
|
22
|
+
if (queued.done) {
|
|
23
|
+
if (this.registration.overflowed) {
|
|
24
|
+
this.registration.overflowed = false;
|
|
25
|
+
this.#done = true;
|
|
26
|
+
return { done: false, value: { type: 'closed', reason: 'SLOW_CONSUMER' } };
|
|
27
|
+
}
|
|
28
|
+
this.#done = true;
|
|
29
|
+
return { done: true, value: undefined };
|
|
30
|
+
}
|
|
31
|
+
try {
|
|
32
|
+
const value = this.parse(queued.value);
|
|
33
|
+
if (value.type === 'closed')
|
|
34
|
+
this.#finishWithoutCancel();
|
|
35
|
+
return { done: false, value };
|
|
36
|
+
}
|
|
37
|
+
catch (cause) {
|
|
38
|
+
await this.#terminate(true);
|
|
39
|
+
throw cause;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
async return() {
|
|
43
|
+
await this.#terminate(true);
|
|
44
|
+
return { done: true, value: undefined };
|
|
45
|
+
}
|
|
46
|
+
async throw(error) {
|
|
47
|
+
await this.#terminate(true);
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
cancel() { return this.#terminate(true); }
|
|
51
|
+
async #terminate(sendCancel) {
|
|
52
|
+
if (this.#done)
|
|
53
|
+
return;
|
|
54
|
+
this.#done = true;
|
|
55
|
+
this.connection.unregisterSubscription(this.registration.id, this.registration);
|
|
56
|
+
this.registration.queue.closeAndDiscard();
|
|
57
|
+
if (sendCancel) {
|
|
58
|
+
try {
|
|
59
|
+
await this.connection.cancelSubscription(this.registration);
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
this.connection.recordDiagnostic(error);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
#finishWithoutCancel() {
|
|
67
|
+
if (this.#done)
|
|
68
|
+
return;
|
|
69
|
+
this.#done = true;
|
|
70
|
+
this.connection.unregisterSubscription(this.registration.id, this.registration);
|
|
71
|
+
this.registration.queue.closeAndDiscard();
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function parseCursorlessClosed(params, definition) {
|
|
75
|
+
assertDefinition(definition, params);
|
|
76
|
+
return { type: 'closed', reason: params.reason };
|
|
77
|
+
}
|
|
78
|
+
function parseWatchClosed(params) {
|
|
79
|
+
assertDefinition('SubscriptionClosedNotification', params);
|
|
80
|
+
const cursor = params.lastDeliveredCursor;
|
|
81
|
+
return {
|
|
82
|
+
type: 'closed',
|
|
83
|
+
reason: params.reason,
|
|
84
|
+
...(cursor === undefined ? {} : { lastDeliveredCursor: cursor }),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
function subscriptionParser(field, eventDefinition, closedDefinition) {
|
|
88
|
+
return (frame) => {
|
|
89
|
+
if (!isRecord(frame.params)) {
|
|
90
|
+
throw new ClusterProtocolError('notification params are missing', 'INVALID_SUBSCRIPTION_EVENT');
|
|
91
|
+
}
|
|
92
|
+
if (frame.method === 'subscription/closed') {
|
|
93
|
+
return parseCursorlessClosed(frame.params, closedDefinition);
|
|
94
|
+
}
|
|
95
|
+
if (frame.method !== 'event') {
|
|
96
|
+
throw new ClusterProtocolError('malformed subscription event', 'INVALID_SUBSCRIPTION_EVENT');
|
|
97
|
+
}
|
|
98
|
+
assertDefinition(eventDefinition, frame.params);
|
|
99
|
+
return { type: 'event', event: frame.params[field] };
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
export class LogsSubscriptionStream extends SubscriptionStream {
|
|
103
|
+
constructor(connection, registration) {
|
|
104
|
+
super(connection, registration, subscriptionParser('record', 'LogEventNotification', 'LogsClosedNotification'));
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
export class AgentAttachSubscriptionStream extends SubscriptionStream {
|
|
108
|
+
constructor(connection, registration) {
|
|
109
|
+
super(connection, registration, subscriptionParser('event', 'AgentAttachEventNotification', 'AgentAttachClosedNotification'));
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
export class WatchSubscriptionStream {
|
|
113
|
+
#connection;
|
|
114
|
+
#registration;
|
|
115
|
+
#lastSeenRunId;
|
|
116
|
+
#lastSeenCursor;
|
|
117
|
+
#lastDelivered;
|
|
118
|
+
#runId;
|
|
119
|
+
#done = false;
|
|
120
|
+
#reconnectConsumed = false;
|
|
121
|
+
#readBarrier = Promise.resolve();
|
|
122
|
+
constructor(init) {
|
|
123
|
+
this.#connection = init.connection;
|
|
124
|
+
this.#registration = init.registration;
|
|
125
|
+
this.#lastSeenRunId = init.lastSeenRunId;
|
|
126
|
+
this.#lastSeenCursor = init.lastSeenCursor;
|
|
127
|
+
this.#lastDelivered = init.params.fromCursor;
|
|
128
|
+
this.#runId = init.result.runId ?? init.params.runId;
|
|
129
|
+
}
|
|
130
|
+
get subscriptionId() { return this.#registration.id; }
|
|
131
|
+
get retainedCount() { return this.#registration.queue.retainedCount; }
|
|
132
|
+
get lastDeliveredCursor() { return this.#lastDelivered; }
|
|
133
|
+
[Symbol.asyncIterator]() { return this; }
|
|
134
|
+
next() {
|
|
135
|
+
const read = this.#readBarrier.then(() => this.#nextLogical());
|
|
136
|
+
this.#readBarrier = read.then(() => undefined, () => undefined);
|
|
137
|
+
return read;
|
|
138
|
+
}
|
|
139
|
+
async #nextLogical() {
|
|
140
|
+
while (!this.#done) {
|
|
141
|
+
const queued = await this.#registration.queue.recv();
|
|
142
|
+
if (queued.done)
|
|
143
|
+
return this.#finishQueue();
|
|
144
|
+
const item = await this.#decode(queued.value);
|
|
145
|
+
if (item)
|
|
146
|
+
return { done: false, value: item };
|
|
147
|
+
}
|
|
148
|
+
return { done: true, value: undefined };
|
|
149
|
+
}
|
|
150
|
+
#finishQueue() {
|
|
151
|
+
this.#done = true;
|
|
152
|
+
if (!this.#registration.overflowed)
|
|
153
|
+
return { done: true, value: undefined };
|
|
154
|
+
this.#registration.overflowed = false;
|
|
155
|
+
const cursor = this.#lastDelivered;
|
|
156
|
+
return {
|
|
157
|
+
done: false,
|
|
158
|
+
value: { type: 'closed', reason: 'SLOW_CONSUMER', ...(cursor === undefined ? {} : { lastDeliveredCursor: cursor }) },
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
async #decode(frame) {
|
|
162
|
+
if (!isRecord(frame.params))
|
|
163
|
+
return this.#invalid('notification params are missing');
|
|
164
|
+
if (frame.method === 'subscription/closed') {
|
|
165
|
+
const closed = parseWatchClosed(frame.params);
|
|
166
|
+
if (closed.lastDeliveredCursor !== undefined)
|
|
167
|
+
this.#lastDelivered = closed.lastDeliveredCursor;
|
|
168
|
+
this.#finishWithoutCancel();
|
|
169
|
+
return closed;
|
|
170
|
+
}
|
|
171
|
+
if (frame.method !== 'event')
|
|
172
|
+
return this.#invalid('malformed watch event');
|
|
173
|
+
try {
|
|
174
|
+
assertDefinition('EventNotification', frame.params);
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
return this.#invalid('malformed watch event');
|
|
178
|
+
}
|
|
179
|
+
const { runId, cursor, event } = frame.params;
|
|
180
|
+
if (this.#lastSeenRunId === runId && this.#lastSeenCursor === cursor)
|
|
181
|
+
return undefined;
|
|
182
|
+
this.#lastSeenRunId = runId;
|
|
183
|
+
this.#lastSeenCursor = cursor;
|
|
184
|
+
this.#runId = runId;
|
|
185
|
+
this.#lastDelivered = cursor;
|
|
186
|
+
return { type: 'event', runId, cursor, event };
|
|
187
|
+
}
|
|
188
|
+
async return() {
|
|
189
|
+
await this.#terminate(true);
|
|
190
|
+
return { done: true, value: undefined };
|
|
191
|
+
}
|
|
192
|
+
async throw(error) {
|
|
193
|
+
await this.#terminate(true);
|
|
194
|
+
throw error;
|
|
195
|
+
}
|
|
196
|
+
cancel() { return this.#terminate(true); }
|
|
197
|
+
reconnect(freshConnection) {
|
|
198
|
+
if (this.#reconnectConsumed) {
|
|
199
|
+
throw new ClusterStateError('watch stream reconnect is one-shot', 'RECONNECT_CONSUMED');
|
|
200
|
+
}
|
|
201
|
+
this.#reconnectConsumed = true;
|
|
202
|
+
return this.#reconnectOn(freshConnection);
|
|
203
|
+
}
|
|
204
|
+
async #reconnectOn(freshConnection) {
|
|
205
|
+
await this.#terminate(true);
|
|
206
|
+
try {
|
|
207
|
+
const runId = this.#runId;
|
|
208
|
+
const fromCursor = this.#lastDelivered;
|
|
209
|
+
const params = {
|
|
210
|
+
...(runId === undefined ? {} : { runId }),
|
|
211
|
+
...(fromCursor === undefined ? {} : { fromCursor }),
|
|
212
|
+
};
|
|
213
|
+
const established = await freshConnection.openSubscription('watch', params);
|
|
214
|
+
return {
|
|
215
|
+
result: established.result,
|
|
216
|
+
stream: new _a({
|
|
217
|
+
connection: freshConnection,
|
|
218
|
+
registration: established.registration,
|
|
219
|
+
result: established.result,
|
|
220
|
+
params,
|
|
221
|
+
...(this.#lastSeenRunId === undefined ? {} : { lastSeenRunId: this.#lastSeenRunId }),
|
|
222
|
+
...(this.#lastSeenCursor === undefined ? {} : { lastSeenCursor: this.#lastSeenCursor }),
|
|
223
|
+
}),
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
catch (error) {
|
|
227
|
+
await freshConnection.close();
|
|
228
|
+
throw error;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
async #terminate(sendCancel) {
|
|
232
|
+
if (this.#done)
|
|
233
|
+
return;
|
|
234
|
+
this.#done = true;
|
|
235
|
+
this.#connection.unregisterSubscription(this.#registration.id, this.#registration);
|
|
236
|
+
this.#registration.queue.closeAndDiscard();
|
|
237
|
+
if (sendCancel) {
|
|
238
|
+
try {
|
|
239
|
+
await this.#connection.cancelSubscription(this.#registration);
|
|
240
|
+
}
|
|
241
|
+
catch (error) {
|
|
242
|
+
this.#connection.recordDiagnostic(error);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
#finishWithoutCancel() {
|
|
247
|
+
if (this.#done)
|
|
248
|
+
return;
|
|
249
|
+
this.#done = true;
|
|
250
|
+
this.#connection.unregisterSubscription(this.#registration.id, this.#registration);
|
|
251
|
+
this.#registration.queue.closeAndDiscard();
|
|
252
|
+
}
|
|
253
|
+
async #invalid(message) {
|
|
254
|
+
await this.#terminate(true);
|
|
255
|
+
throw new ClusterProtocolError(message, 'INVALID_SUBSCRIPTION_EVENT');
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
_a = WatchSubscriptionStream;
|
|
259
|
+
Object.defineProperty(SubscriptionStream.prototype, Symbol.asyncDispose, {
|
|
260
|
+
configurable: true, value: SubscriptionStream.prototype.cancel,
|
|
261
|
+
});
|
|
262
|
+
Object.defineProperty(WatchSubscriptionStream.prototype, Symbol.asyncDispose, {
|
|
263
|
+
configurable: true, value: WatchSubscriptionStream.prototype.cancel,
|
|
264
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.assertDefinition = assertDefinition;
|
|
4
|
+
exports.assertMethodResult = assertMethodResult;
|
|
5
|
+
const _2020_js_1 = require("ajv/dist/2020.js");
|
|
6
|
+
const protocol_js_1 = require("./generated/protocol.cjs");
|
|
7
|
+
const protocol_schema_js_1 = require("./generated/protocol-schema.cjs");
|
|
8
|
+
const errors_js_1 = require("./errors.cjs");
|
|
9
|
+
const ajv = new _2020_js_1.default({
|
|
10
|
+
allErrors: true,
|
|
11
|
+
strict: false,
|
|
12
|
+
validateFormats: false,
|
|
13
|
+
});
|
|
14
|
+
ajv.addSchema(protocol_schema_js_1.CLUSTER_PROTOCOL_SCHEMA, 'openengine-cluster-v1');
|
|
15
|
+
const validators = new Map();
|
|
16
|
+
function validatorFor(definition) {
|
|
17
|
+
const cached = validators.get(definition);
|
|
18
|
+
if (cached)
|
|
19
|
+
return cached;
|
|
20
|
+
const validator = ajv.compile({
|
|
21
|
+
$ref: `openengine-cluster-v1#/$defs/${definition}`,
|
|
22
|
+
});
|
|
23
|
+
validators.set(definition, validator);
|
|
24
|
+
return validator;
|
|
25
|
+
}
|
|
26
|
+
function assertDefinition(definition, value) {
|
|
27
|
+
const validate = validatorFor(definition);
|
|
28
|
+
if (validate(value))
|
|
29
|
+
return;
|
|
30
|
+
const details = (validate.errors ?? []).map((error) => `${error.instancePath || '/'} ${error.message ?? 'is invalid'}`).join('; ');
|
|
31
|
+
throw new errors_js_1.ClusterProtocolError(`${definition} validation failed: ${details}`, 'INVALID_RESPONSE');
|
|
32
|
+
}
|
|
33
|
+
function assertMethodResult(method, value) {
|
|
34
|
+
try {
|
|
35
|
+
assertDefinition(protocol_js_1.METHOD_RESULT_DEFINITIONS[method], value);
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
const receivedVersion = method === 'initialize' && value !== null && typeof value === 'object'
|
|
39
|
+
? value.protocolVersion
|
|
40
|
+
: undefined;
|
|
41
|
+
if (typeof receivedVersion === 'string' && receivedVersion !== protocol_js_1.PROTOCOL_VERSION) {
|
|
42
|
+
throw new errors_js_1.ClusterProtocolError(`unsupported protocol version ${receivedVersion}`, 'UNSUPPORTED_PROTOCOL_VERSION');
|
|
43
|
+
}
|
|
44
|
+
throw error;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import Ajv2020 from 'ajv/dist/2020.js';
|
|
2
|
+
import { METHOD_RESULT_DEFINITIONS, PROTOCOL_VERSION, } from './generated/protocol.mjs';
|
|
3
|
+
import { CLUSTER_PROTOCOL_SCHEMA } from './generated/protocol-schema.mjs';
|
|
4
|
+
import { ClusterProtocolError } from './errors.mjs';
|
|
5
|
+
const ajv = new Ajv2020({
|
|
6
|
+
allErrors: true,
|
|
7
|
+
strict: false,
|
|
8
|
+
validateFormats: false,
|
|
9
|
+
});
|
|
10
|
+
ajv.addSchema(CLUSTER_PROTOCOL_SCHEMA, 'openengine-cluster-v1');
|
|
11
|
+
const validators = new Map();
|
|
12
|
+
function validatorFor(definition) {
|
|
13
|
+
const cached = validators.get(definition);
|
|
14
|
+
if (cached)
|
|
15
|
+
return cached;
|
|
16
|
+
const validator = ajv.compile({
|
|
17
|
+
$ref: `openengine-cluster-v1#/$defs/${definition}`,
|
|
18
|
+
});
|
|
19
|
+
validators.set(definition, validator);
|
|
20
|
+
return validator;
|
|
21
|
+
}
|
|
22
|
+
export function assertDefinition(definition, value) {
|
|
23
|
+
const validate = validatorFor(definition);
|
|
24
|
+
if (validate(value))
|
|
25
|
+
return;
|
|
26
|
+
const details = (validate.errors ?? []).map((error) => `${error.instancePath || '/'} ${error.message ?? 'is invalid'}`).join('; ');
|
|
27
|
+
throw new ClusterProtocolError(`${definition} validation failed: ${details}`, 'INVALID_RESPONSE');
|
|
28
|
+
}
|
|
29
|
+
export function assertMethodResult(method, value) {
|
|
30
|
+
try {
|
|
31
|
+
assertDefinition(METHOD_RESULT_DEFINITIONS[method], value);
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
const receivedVersion = method === 'initialize' && value !== null && typeof value === 'object'
|
|
35
|
+
? value.protocolVersion
|
|
36
|
+
: undefined;
|
|
37
|
+
if (typeof receivedVersion === 'string' && receivedVersion !== PROTOCOL_VERSION) {
|
|
38
|
+
throw new ClusterProtocolError(`unsupported protocol version ${receivedVersion}`, 'UNSUPPORTED_PROTOCOL_VERSION');
|
|
39
|
+
}
|
|
40
|
+
throw error;
|
|
41
|
+
}
|
|
42
|
+
}
|
package/lib/settings.js
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
const fs = require('fs');
|
|
7
7
|
const path = require('path');
|
|
8
8
|
const os = require('os');
|
|
9
|
+
const crypto = require('crypto');
|
|
10
|
+
const lockfile = require('proper-lockfile');
|
|
9
11
|
const { validateMountConfig, validateEnvPassthrough } = require('./docker-config');
|
|
10
12
|
const {
|
|
11
13
|
VALID_PROVIDERS,
|
|
@@ -13,6 +15,18 @@ const {
|
|
|
13
15
|
normalizeProviderSettings,
|
|
14
16
|
} = require('./provider-names');
|
|
15
17
|
|
|
18
|
+
const SETTINGS_LOCK_STALE_MS = 5000;
|
|
19
|
+
const SETTINGS_LOCK_TIMEOUT_MS = 500;
|
|
20
|
+
const SETTINGS_LOCK_RETRY_MS = 20;
|
|
21
|
+
const SETTINGS_LOCK_SLEEP = new Int32Array(new SharedArrayBuffer(4));
|
|
22
|
+
|
|
23
|
+
class SettingsValidationError extends Error {
|
|
24
|
+
constructor(message) {
|
|
25
|
+
super(message);
|
|
26
|
+
this.name = 'SettingsValidationError';
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
16
30
|
// Lazy-loaded to avoid circular dependency (issue-providers → settings → issue-providers)
|
|
17
31
|
let _issueProviderFns = null;
|
|
18
32
|
function getIssueProviderFns() {
|
|
@@ -115,10 +129,11 @@ const DEFAULT_SETTINGS_BASE = {
|
|
|
115
129
|
defaultDelivery: 'none', // 'none' | 'pr' | 'ship' - folded into resolveEffectiveRunPlan same as defaultDocker
|
|
116
130
|
strictSchema: true, // true = reliable json output (default), false = live streaming (may crash - see bold-meadow-11)
|
|
117
131
|
logLevel: 'normal',
|
|
118
|
-
//
|
|
132
|
+
// Automatic update notification cache
|
|
119
133
|
autoCheckUpdates: true, // Check npm registry for newer versions
|
|
120
|
-
lastUpdateCheckAt: null, // Unix timestamp of last
|
|
121
|
-
lastSeenVersion: null, //
|
|
134
|
+
lastUpdateCheckAt: null, // Unix timestamp of last automatic attempt (null = never checked)
|
|
135
|
+
lastSeenVersion: null, // Compatibility key containing the cached valid npm-latest version
|
|
136
|
+
lastUpdateCheckClaim: null, // Opaque ownership token for an in-flight automatic refresh
|
|
122
137
|
// Claude command - customize how to invoke Claude CLI (default: 'claude')
|
|
123
138
|
// Example: 'ccr code' for claude-code-router integration
|
|
124
139
|
claudeCommand: 'claude',
|
|
@@ -260,48 +275,204 @@ function normalizeLoadedSettings(parsed) {
|
|
|
260
275
|
if (parsed.providerSettings) {
|
|
261
276
|
normalized.providerSettings = normalizeProviderSettings(parsed.providerSettings);
|
|
262
277
|
}
|
|
278
|
+
if (
|
|
279
|
+
normalized.autoCheckUpdates === false ||
|
|
280
|
+
typeof normalized.lastUpdateCheckClaim !== 'string' ||
|
|
281
|
+
normalized.lastUpdateCheckClaim.trim().length === 0
|
|
282
|
+
) {
|
|
283
|
+
normalized.lastUpdateCheckClaim = null;
|
|
284
|
+
}
|
|
263
285
|
return normalized;
|
|
264
286
|
}
|
|
265
287
|
|
|
288
|
+
function resolvedDefaultSettings() {
|
|
289
|
+
return {
|
|
290
|
+
...DEFAULT_SETTINGS,
|
|
291
|
+
providerSettings: mergeProviderSettings(getProviderDefaults()),
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function mergeLoadedSettings(parsed) {
|
|
296
|
+
const normalized = normalizeLoadedSettings(parsed);
|
|
297
|
+
const merged = { ...resolvedDefaultSettings(), ...normalized };
|
|
298
|
+
merged.defaultProvider =
|
|
299
|
+
normalizeProviderName(merged.defaultProvider) || DEFAULT_SETTINGS.defaultProvider;
|
|
300
|
+
merged.providerSettings = mergeProviderSettings(getProviderDefaults(), normalized.providerSettings);
|
|
301
|
+
if (
|
|
302
|
+
merged.autoCheckUpdates === false ||
|
|
303
|
+
typeof merged.lastUpdateCheckClaim !== 'string' ||
|
|
304
|
+
merged.lastUpdateCheckClaim.trim().length === 0
|
|
305
|
+
) {
|
|
306
|
+
merged.lastUpdateCheckClaim = null;
|
|
307
|
+
}
|
|
308
|
+
return applyLegacyModelBounds(merged);
|
|
309
|
+
}
|
|
310
|
+
|
|
266
311
|
/**
|
|
267
312
|
* Load settings from disk, merging with defaults
|
|
268
313
|
*/
|
|
269
|
-
function loadSettings() {
|
|
314
|
+
function loadSettings(options = {}) {
|
|
270
315
|
const settingsFile = getSettingsFile();
|
|
271
316
|
if (!fs.existsSync(settingsFile)) {
|
|
272
|
-
|
|
317
|
+
return resolvedDefaultSettings();
|
|
318
|
+
}
|
|
319
|
+
try {
|
|
320
|
+
return mergeLoadedSettings(JSON.parse(fs.readFileSync(settingsFile, 'utf8')));
|
|
321
|
+
} catch {
|
|
322
|
+
if (!options.silent) console.error('Warning: Could not load settings, using defaults');
|
|
323
|
+
return resolvedDefaultSettings();
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function readSettingsForMutation(settingsFile) {
|
|
328
|
+
if (!fs.existsSync(settingsFile)) {
|
|
273
329
|
return {
|
|
274
|
-
|
|
275
|
-
|
|
330
|
+
settings: resolvedDefaultSettings(),
|
|
331
|
+
requiresClaimInvalidation: false,
|
|
332
|
+
requiresRecovery: false,
|
|
276
333
|
};
|
|
277
334
|
}
|
|
335
|
+
let parsed;
|
|
278
336
|
try {
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
merged.defaultProvider =
|
|
283
|
-
normalizeProviderName(merged.defaultProvider) || DEFAULT_SETTINGS.defaultProvider;
|
|
284
|
-
merged.providerSettings = mergeProviderSettings(getProviderDefaults(), parsed.providerSettings);
|
|
285
|
-
return applyLegacyModelBounds(merged);
|
|
286
|
-
} catch {
|
|
287
|
-
console.error('Warning: Could not load settings, using defaults');
|
|
337
|
+
parsed = JSON.parse(fs.readFileSync(settingsFile, 'utf8'));
|
|
338
|
+
} catch (error) {
|
|
339
|
+
if (!(error instanceof SyntaxError)) throw error;
|
|
288
340
|
return {
|
|
289
|
-
|
|
290
|
-
|
|
341
|
+
settings: resolvedDefaultSettings(),
|
|
342
|
+
requiresClaimInvalidation: false,
|
|
343
|
+
requiresRecovery: true,
|
|
291
344
|
};
|
|
292
345
|
}
|
|
346
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
347
|
+
return {
|
|
348
|
+
settings: resolvedDefaultSettings(),
|
|
349
|
+
requiresClaimInvalidation: false,
|
|
350
|
+
requiresRecovery: true,
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
const updatesDisabled = parsed.autoCheckUpdates === false || parsed.updatePolicy === 'off';
|
|
356
|
+
return {
|
|
357
|
+
settings: mergeLoadedSettings(parsed),
|
|
358
|
+
requiresClaimInvalidation: updatesDisabled && parsed.lastUpdateCheckClaim !== null,
|
|
359
|
+
requiresRecovery: false,
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function getAtomicSettingsMode(settingsFile) {
|
|
364
|
+
try {
|
|
365
|
+
return fs.statSync(settingsFile).mode & 0o600;
|
|
366
|
+
} catch (error) {
|
|
367
|
+
if (error.code === 'ENOENT') return 0o600;
|
|
368
|
+
throw error;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function atomicWriteSettings(settingsFile, settings) {
|
|
373
|
+
const dir = path.dirname(settingsFile);
|
|
374
|
+
const temporaryFile = path.join(
|
|
375
|
+
dir,
|
|
376
|
+
`.${path.basename(settingsFile)}.${process.pid}.${crypto.randomUUID()}.tmp`
|
|
377
|
+
);
|
|
378
|
+
|
|
379
|
+
let operationError;
|
|
380
|
+
try {
|
|
381
|
+
fs.writeFileSync(temporaryFile, JSON.stringify(settings, null, 2), {
|
|
382
|
+
encoding: 'utf8',
|
|
383
|
+
flag: 'wx',
|
|
384
|
+
mode: getAtomicSettingsMode(settingsFile),
|
|
385
|
+
});
|
|
386
|
+
fs.renameSync(temporaryFile, settingsFile);
|
|
387
|
+
} catch (error) {
|
|
388
|
+
operationError = error;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
try {
|
|
392
|
+
fs.unlinkSync(temporaryFile);
|
|
393
|
+
} catch (error) {
|
|
394
|
+
if (error.code !== 'ENOENT' && !operationError) operationError = error;
|
|
395
|
+
}
|
|
396
|
+
if (operationError) throw operationError;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function acquireSettingsLock(settingsFile, lockfilePath, timeoutMs) {
|
|
400
|
+
const deadline = Date.now() + timeoutMs;
|
|
401
|
+
while (true) {
|
|
402
|
+
try {
|
|
403
|
+
return lockfile.lockSync(settingsFile, {
|
|
404
|
+
lockfilePath,
|
|
405
|
+
realpath: false,
|
|
406
|
+
stale: SETTINGS_LOCK_STALE_MS,
|
|
407
|
+
});
|
|
408
|
+
} catch (error) {
|
|
409
|
+
if (error.code !== 'ELOCKED' || Date.now() >= deadline) throw error;
|
|
410
|
+
Atomics.wait(SETTINGS_LOCK_SLEEP, 0, 0, SETTINGS_LOCK_RETRY_MS);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
293
413
|
}
|
|
294
414
|
|
|
295
415
|
/**
|
|
296
|
-
*
|
|
416
|
+
* Atomically mutate global settings under the process-shared settings lock.
|
|
417
|
+
* The mutator receives only the freshly re-read, normalized state.
|
|
418
|
+
*
|
|
419
|
+
* @param {(settings: object) => any} mutator
|
|
420
|
+
* @param {object} [options]
|
|
421
|
+
* @returns {any} the mutator's return value
|
|
297
422
|
*/
|
|
298
|
-
function
|
|
423
|
+
function mutateSettings(mutator, options = {}) {
|
|
424
|
+
if (typeof mutator !== 'function') {
|
|
425
|
+
throw new TypeError('Global settings mutation requires a callback');
|
|
426
|
+
}
|
|
427
|
+
|
|
299
428
|
const settingsFile = getSettingsFile();
|
|
300
429
|
const dir = path.dirname(settingsFile);
|
|
301
|
-
|
|
430
|
+
const lockfilePath = `${settingsFile}.lock`;
|
|
431
|
+
let release;
|
|
432
|
+
let result;
|
|
433
|
+
let mutationError;
|
|
434
|
+
|
|
435
|
+
try {
|
|
302
436
|
fs.mkdirSync(dir, { recursive: true });
|
|
437
|
+
release = acquireSettingsLock(
|
|
438
|
+
settingsFile,
|
|
439
|
+
lockfilePath,
|
|
440
|
+
options.lockTimeoutMs ?? SETTINGS_LOCK_TIMEOUT_MS
|
|
441
|
+
);
|
|
442
|
+
|
|
443
|
+
const { settings, requiresClaimInvalidation, requiresRecovery } =
|
|
444
|
+
readSettingsForMutation(settingsFile);
|
|
445
|
+
const before = JSON.stringify(settings);
|
|
446
|
+
result = mutator(settings);
|
|
447
|
+
if (result && typeof result.then === 'function') {
|
|
448
|
+
throw new TypeError('Global settings mutations must be synchronous');
|
|
449
|
+
}
|
|
450
|
+
if (settings.autoCheckUpdates === false || settings.updatePolicy === 'off') {
|
|
451
|
+
settings.lastUpdateCheckClaim = null;
|
|
452
|
+
}
|
|
453
|
+
if (requiresRecovery || requiresClaimInvalidation || JSON.stringify(settings) !== before) {
|
|
454
|
+
atomicWriteSettings(settingsFile, settings);
|
|
455
|
+
}
|
|
456
|
+
} catch (error) {
|
|
457
|
+
mutationError =
|
|
458
|
+
error instanceof SettingsValidationError
|
|
459
|
+
? error
|
|
460
|
+
: new Error(`Unable to persist global settings: ${error.message}`, { cause: error });
|
|
461
|
+
} finally {
|
|
462
|
+
if (release) {
|
|
463
|
+
try {
|
|
464
|
+
release();
|
|
465
|
+
} catch (error) {
|
|
466
|
+
if (!mutationError) {
|
|
467
|
+
mutationError = new Error(`Unable to release global settings lock: ${error.message}`, {
|
|
468
|
+
cause: error,
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
303
473
|
}
|
|
304
|
-
|
|
474
|
+
if (mutationError) throw mutationError;
|
|
475
|
+
return result;
|
|
305
476
|
}
|
|
306
477
|
|
|
307
478
|
/**
|
|
@@ -537,9 +708,10 @@ function getClaudeCommand() {
|
|
|
537
708
|
|
|
538
709
|
module.exports = {
|
|
539
710
|
loadSettings,
|
|
540
|
-
|
|
711
|
+
mutateSettings,
|
|
541
712
|
validateSetting,
|
|
542
713
|
coerceValue,
|
|
714
|
+
SettingsValidationError,
|
|
543
715
|
DEFAULT_SETTINGS,
|
|
544
716
|
getSettingsFile,
|
|
545
717
|
settingsFileExists,
|