@vibedeckx/linux-x64 0.3.40 → 0.3.41
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/dist/bin.js +1845 -477
- package/package.json +1 -1
package/dist/bin.js
CHANGED
|
@@ -29485,9 +29485,9 @@ ${callerStack}`;
|
|
|
29485
29485
|
};
|
|
29486
29486
|
exports.ClientReadableStreamImpl = ClientReadableStreamImpl;
|
|
29487
29487
|
var ClientWritableStreamImpl = class extends stream_1.Writable {
|
|
29488
|
-
constructor(
|
|
29488
|
+
constructor(serialize2) {
|
|
29489
29489
|
super({ objectMode: true });
|
|
29490
|
-
this.serialize =
|
|
29490
|
+
this.serialize = serialize2;
|
|
29491
29491
|
}
|
|
29492
29492
|
cancel() {
|
|
29493
29493
|
var _a26;
|
|
@@ -29520,9 +29520,9 @@ ${callerStack}`;
|
|
|
29520
29520
|
};
|
|
29521
29521
|
exports.ClientWritableStreamImpl = ClientWritableStreamImpl;
|
|
29522
29522
|
var ClientDuplexStreamImpl = class extends stream_1.Duplex {
|
|
29523
|
-
constructor(
|
|
29523
|
+
constructor(serialize2, deserialize2) {
|
|
29524
29524
|
super({ objectMode: true });
|
|
29525
|
-
this.serialize =
|
|
29525
|
+
this.serialize = serialize2;
|
|
29526
29526
|
this.deserialize = deserialize2;
|
|
29527
29527
|
}
|
|
29528
29528
|
cancel() {
|
|
@@ -30092,14 +30092,14 @@ var require_client = __commonJS({
|
|
|
30092
30092
|
return { metadata: arg1, options: arg2, callback: arg3 };
|
|
30093
30093
|
}
|
|
30094
30094
|
}
|
|
30095
|
-
makeUnaryRequest(method,
|
|
30095
|
+
makeUnaryRequest(method, serialize2, deserialize2, argument, metadata, options, callback) {
|
|
30096
30096
|
var _a26, _b18;
|
|
30097
30097
|
const checkedArguments = this.checkOptionalUnaryResponseArguments(metadata, options, callback);
|
|
30098
30098
|
const methodDefinition = {
|
|
30099
30099
|
path: method,
|
|
30100
30100
|
requestStream: false,
|
|
30101
30101
|
responseStream: false,
|
|
30102
|
-
requestSerialize:
|
|
30102
|
+
requestSerialize: serialize2,
|
|
30103
30103
|
responseDeserialize: deserialize2
|
|
30104
30104
|
};
|
|
30105
30105
|
let callProperties = {
|
|
@@ -30165,19 +30165,19 @@ var require_client = __commonJS({
|
|
|
30165
30165
|
call.halfClose();
|
|
30166
30166
|
return emitter;
|
|
30167
30167
|
}
|
|
30168
|
-
makeClientStreamRequest(method,
|
|
30168
|
+
makeClientStreamRequest(method, serialize2, deserialize2, metadata, options, callback) {
|
|
30169
30169
|
var _a26, _b18;
|
|
30170
30170
|
const checkedArguments = this.checkOptionalUnaryResponseArguments(metadata, options, callback);
|
|
30171
30171
|
const methodDefinition = {
|
|
30172
30172
|
path: method,
|
|
30173
30173
|
requestStream: true,
|
|
30174
30174
|
responseStream: false,
|
|
30175
|
-
requestSerialize:
|
|
30175
|
+
requestSerialize: serialize2,
|
|
30176
30176
|
responseDeserialize: deserialize2
|
|
30177
30177
|
};
|
|
30178
30178
|
let callProperties = {
|
|
30179
30179
|
metadata: checkedArguments.metadata,
|
|
30180
|
-
call: new call_1.ClientWritableStreamImpl(
|
|
30180
|
+
call: new call_1.ClientWritableStreamImpl(serialize2),
|
|
30181
30181
|
channel: this[CHANNEL_SYMBOL],
|
|
30182
30182
|
methodDefinition,
|
|
30183
30183
|
callOptions: checkedArguments.options,
|
|
@@ -30256,14 +30256,14 @@ var require_client = __commonJS({
|
|
|
30256
30256
|
}
|
|
30257
30257
|
return { metadata, options };
|
|
30258
30258
|
}
|
|
30259
|
-
makeServerStreamRequest(method,
|
|
30259
|
+
makeServerStreamRequest(method, serialize2, deserialize2, argument, metadata, options) {
|
|
30260
30260
|
var _a26, _b18;
|
|
30261
30261
|
const checkedArguments = this.checkMetadataAndOptions(metadata, options);
|
|
30262
30262
|
const methodDefinition = {
|
|
30263
30263
|
path: method,
|
|
30264
30264
|
requestStream: false,
|
|
30265
30265
|
responseStream: true,
|
|
30266
|
-
requestSerialize:
|
|
30266
|
+
requestSerialize: serialize2,
|
|
30267
30267
|
responseDeserialize: deserialize2
|
|
30268
30268
|
};
|
|
30269
30269
|
let callProperties = {
|
|
@@ -30314,19 +30314,19 @@ var require_client = __commonJS({
|
|
|
30314
30314
|
call.halfClose();
|
|
30315
30315
|
return stream;
|
|
30316
30316
|
}
|
|
30317
|
-
makeBidiStreamRequest(method,
|
|
30317
|
+
makeBidiStreamRequest(method, serialize2, deserialize2, metadata, options) {
|
|
30318
30318
|
var _a26, _b18;
|
|
30319
30319
|
const checkedArguments = this.checkMetadataAndOptions(metadata, options);
|
|
30320
30320
|
const methodDefinition = {
|
|
30321
30321
|
path: method,
|
|
30322
30322
|
requestStream: true,
|
|
30323
30323
|
responseStream: true,
|
|
30324
|
-
requestSerialize:
|
|
30324
|
+
requestSerialize: serialize2,
|
|
30325
30325
|
responseDeserialize: deserialize2
|
|
30326
30326
|
};
|
|
30327
30327
|
let callProperties = {
|
|
30328
30328
|
metadata: checkedArguments.metadata,
|
|
30329
|
-
call: new call_1.ClientDuplexStreamImpl(
|
|
30329
|
+
call: new call_1.ClientDuplexStreamImpl(serialize2, deserialize2),
|
|
30330
30330
|
channel: this[CHANNEL_SYMBOL],
|
|
30331
30331
|
methodDefinition,
|
|
30332
30332
|
callOptions: checkedArguments.options
|
|
@@ -30418,9 +30418,9 @@ var require_make_client = __commonJS({
|
|
|
30418
30418
|
methodType = "unary";
|
|
30419
30419
|
}
|
|
30420
30420
|
}
|
|
30421
|
-
const
|
|
30421
|
+
const serialize2 = attrs.requestSerialize;
|
|
30422
30422
|
const deserialize2 = attrs.responseDeserialize;
|
|
30423
|
-
const methodFunc = partial2(requesterFuncs[methodType], attrs.path,
|
|
30423
|
+
const methodFunc = partial2(requesterFuncs[methodType], attrs.path, serialize2, deserialize2);
|
|
30424
30424
|
ServiceClientImpl.prototype[name25] = methodFunc;
|
|
30425
30425
|
Object.assign(ServiceClientImpl.prototype[name25], attrs);
|
|
30426
30426
|
if (attrs.originalName && !isPrototypePolluted(attrs.originalName)) {
|
|
@@ -30431,9 +30431,9 @@ var require_make_client = __commonJS({
|
|
|
30431
30431
|
ServiceClientImpl.serviceName = serviceName;
|
|
30432
30432
|
return ServiceClientImpl;
|
|
30433
30433
|
}
|
|
30434
|
-
function partial2(fn, path26,
|
|
30434
|
+
function partial2(fn, path26, serialize2, deserialize2) {
|
|
30435
30435
|
return function(...args) {
|
|
30436
|
-
return fn.call(this, path26,
|
|
30436
|
+
return fn.call(this, path26, serialize2, deserialize2, ...args);
|
|
30437
30437
|
};
|
|
30438
30438
|
}
|
|
30439
30439
|
function isProtobufTypeDefinition(obj) {
|
|
@@ -39265,7 +39265,7 @@ var require_src14 = __commonJS({
|
|
|
39265
39265
|
};
|
|
39266
39266
|
}
|
|
39267
39267
|
function createSerializer(cls) {
|
|
39268
|
-
return function
|
|
39268
|
+
return function serialize2(arg) {
|
|
39269
39269
|
if (Array.isArray(arg)) {
|
|
39270
39270
|
throw new Error(`Failed to serialize message: expected object with ${cls.name} structure, got array instead`);
|
|
39271
39271
|
}
|
|
@@ -46809,13 +46809,13 @@ var require_server = __commonJS({
|
|
|
46809
46809
|
(0, channelz_1.unregisterChannelzRef)(this.channelzRef);
|
|
46810
46810
|
this.shutdown = true;
|
|
46811
46811
|
}
|
|
46812
|
-
register(name25, handler,
|
|
46812
|
+
register(name25, handler, serialize2, deserialize2, type) {
|
|
46813
46813
|
if (this.handlers.has(name25)) {
|
|
46814
46814
|
return false;
|
|
46815
46815
|
}
|
|
46816
46816
|
this.handlers.set(name25, {
|
|
46817
46817
|
func: handler,
|
|
46818
|
-
serialize,
|
|
46818
|
+
serialize: serialize2,
|
|
46819
46819
|
deserialize: deserialize2,
|
|
46820
46820
|
type,
|
|
46821
46821
|
path: name25
|
|
@@ -66510,7 +66510,7 @@ var require_redact = __commonJS({
|
|
|
66510
66510
|
const {
|
|
66511
66511
|
paths = [],
|
|
66512
66512
|
censor = "[REDACTED]",
|
|
66513
|
-
serialize = JSON.stringify,
|
|
66513
|
+
serialize: serialize2 = JSON.stringify,
|
|
66514
66514
|
strict = true,
|
|
66515
66515
|
remove = false
|
|
66516
66516
|
} = options;
|
|
@@ -66519,10 +66519,10 @@ var require_redact = __commonJS({
|
|
|
66519
66519
|
return function redact(obj) {
|
|
66520
66520
|
if (strict && (obj === null || typeof obj !== "object")) {
|
|
66521
66521
|
if (obj === null || obj === void 0) {
|
|
66522
|
-
return
|
|
66522
|
+
return serialize2 ? serialize2(obj) : obj;
|
|
66523
66523
|
}
|
|
66524
66524
|
if (typeof obj !== "object") {
|
|
66525
|
-
return
|
|
66525
|
+
return serialize2 ? serialize2(obj) : obj;
|
|
66526
66526
|
}
|
|
66527
66527
|
}
|
|
66528
66528
|
const cloned = selectiveClone(obj, pathStructure);
|
|
@@ -66532,14 +66532,14 @@ var require_redact = __commonJS({
|
|
|
66532
66532
|
actualCensor = censor;
|
|
66533
66533
|
}
|
|
66534
66534
|
redactPaths(cloned, paths, actualCensor, remove);
|
|
66535
|
-
if (
|
|
66535
|
+
if (serialize2 === false) {
|
|
66536
66536
|
cloned.restore = function() {
|
|
66537
66537
|
return deepClone(original);
|
|
66538
66538
|
};
|
|
66539
66539
|
return cloned;
|
|
66540
66540
|
}
|
|
66541
|
-
if (typeof
|
|
66542
|
-
return
|
|
66541
|
+
if (typeof serialize2 === "function") {
|
|
66542
|
+
return serialize2(cloned);
|
|
66543
66543
|
}
|
|
66544
66544
|
return JSON.stringify(cloned);
|
|
66545
66545
|
};
|
|
@@ -66628,7 +66628,7 @@ var require_redaction = __commonJS({
|
|
|
66628
66628
|
var rx = /[^.[\]]+|\[([^[\]]*?)\]/g;
|
|
66629
66629
|
var CENSOR = "[Redacted]";
|
|
66630
66630
|
var strict = false;
|
|
66631
|
-
function redaction(opts,
|
|
66631
|
+
function redaction(opts, serialize2) {
|
|
66632
66632
|
const { paths, censor, remove } = handle(opts);
|
|
66633
66633
|
const shape = paths.reduce((o, str2) => {
|
|
66634
66634
|
rx.lastIndex = 0;
|
|
@@ -66662,10 +66662,10 @@ var require_redaction = __commonJS({
|
|
|
66662
66662
|
return o;
|
|
66663
66663
|
}, {});
|
|
66664
66664
|
const result = {
|
|
66665
|
-
[redactFmtSym]: Redact({ paths, censor, serialize, strict, remove })
|
|
66665
|
+
[redactFmtSym]: Redact({ paths, censor, serialize: serialize2, strict, remove })
|
|
66666
66666
|
};
|
|
66667
66667
|
const topCensor = (...args) => {
|
|
66668
|
-
return typeof censor === "function" ?
|
|
66668
|
+
return typeof censor === "function" ? serialize2(censor(...args)) : serialize2(censor);
|
|
66669
66669
|
};
|
|
66670
66670
|
return [...Object.keys(shape), ...Object.getOwnPropertySymbols(shape)].reduce((o, k2) => {
|
|
66671
66671
|
if (shape[k2] === null) {
|
|
@@ -66677,7 +66677,7 @@ var require_redaction = __commonJS({
|
|
|
66677
66677
|
o[k2] = Redact({
|
|
66678
66678
|
paths: shape[k2],
|
|
66679
66679
|
censor: wrappedCensor,
|
|
66680
|
-
serialize,
|
|
66680
|
+
serialize: serialize2,
|
|
66681
66681
|
strict,
|
|
66682
66682
|
remove
|
|
66683
66683
|
});
|
|
@@ -71364,17 +71364,17 @@ var require_reply = __commonJS({
|
|
|
71364
71364
|
};
|
|
71365
71365
|
Reply.prototype.status = Reply.prototype.code;
|
|
71366
71366
|
Reply.prototype.getSerializationFunction = function(schemaOrStatus, contentType2) {
|
|
71367
|
-
let
|
|
71367
|
+
let serialize3;
|
|
71368
71368
|
if (typeof schemaOrStatus === "string" || typeof schemaOrStatus === "number") {
|
|
71369
71369
|
if (typeof contentType2 === "string") {
|
|
71370
|
-
|
|
71370
|
+
serialize3 = this[kRouteContext][kSchemaResponse]?.[schemaOrStatus]?.[contentType2];
|
|
71371
71371
|
} else {
|
|
71372
|
-
|
|
71372
|
+
serialize3 = this[kRouteContext][kSchemaResponse]?.[schemaOrStatus];
|
|
71373
71373
|
}
|
|
71374
71374
|
} else if (typeof schemaOrStatus === "object") {
|
|
71375
|
-
|
|
71375
|
+
serialize3 = this[kRouteContext][kReplyCacheSerializeFns]?.get(schemaOrStatus);
|
|
71376
71376
|
}
|
|
71377
|
-
return
|
|
71377
|
+
return serialize3;
|
|
71378
71378
|
};
|
|
71379
71379
|
Reply.prototype.compileSerializationSchema = function(schema, httpStatus = null, contentType2 = null) {
|
|
71380
71380
|
const { request } = this;
|
|
@@ -71400,27 +71400,27 @@ var require_reply = __commonJS({
|
|
|
71400
71400
|
};
|
|
71401
71401
|
Reply.prototype.serializeInput = function(input, schema, httpStatus, contentType2) {
|
|
71402
71402
|
const possibleContentType = httpStatus;
|
|
71403
|
-
let
|
|
71403
|
+
let serialize3;
|
|
71404
71404
|
httpStatus = typeof schema === "string" || typeof schema === "number" ? schema : httpStatus;
|
|
71405
71405
|
contentType2 = httpStatus && possibleContentType !== httpStatus ? possibleContentType : contentType2;
|
|
71406
71406
|
if (httpStatus != null) {
|
|
71407
71407
|
if (contentType2 != null) {
|
|
71408
|
-
|
|
71408
|
+
serialize3 = this[kRouteContext][kSchemaResponse]?.[httpStatus]?.[contentType2];
|
|
71409
71409
|
} else {
|
|
71410
|
-
|
|
71410
|
+
serialize3 = this[kRouteContext][kSchemaResponse]?.[httpStatus];
|
|
71411
71411
|
}
|
|
71412
|
-
if (
|
|
71412
|
+
if (serialize3 == null) {
|
|
71413
71413
|
if (contentType2) throw new FST_ERR_MISSING_CONTENTTYPE_SERIALIZATION_FN(httpStatus, contentType2);
|
|
71414
71414
|
throw new FST_ERR_MISSING_SERIALIZATION_FN(httpStatus);
|
|
71415
71415
|
}
|
|
71416
71416
|
} else {
|
|
71417
71417
|
if (this[kRouteContext][kReplyCacheSerializeFns]?.has(schema)) {
|
|
71418
|
-
|
|
71418
|
+
serialize3 = this[kRouteContext][kReplyCacheSerializeFns].get(schema);
|
|
71419
71419
|
} else {
|
|
71420
|
-
|
|
71420
|
+
serialize3 = this.compileSerializationSchema(schema, httpStatus, contentType2);
|
|
71421
71421
|
}
|
|
71422
71422
|
}
|
|
71423
|
-
return
|
|
71423
|
+
return serialize3(input);
|
|
71424
71424
|
};
|
|
71425
71425
|
Reply.prototype.serialize = function(payload) {
|
|
71426
71426
|
if (this[kReplySerializer] !== null) {
|
|
@@ -71429,7 +71429,7 @@ var require_reply = __commonJS({
|
|
|
71429
71429
|
if (this[kRouteContext] && this[kRouteContext][kReplySerializerDefault]) {
|
|
71430
71430
|
return this[kRouteContext][kReplySerializerDefault](payload, this.raw.statusCode);
|
|
71431
71431
|
} else {
|
|
71432
|
-
return
|
|
71432
|
+
return serialize2(this[kRouteContext], payload, this.raw.statusCode);
|
|
71433
71433
|
}
|
|
71434
71434
|
}
|
|
71435
71435
|
};
|
|
@@ -71502,7 +71502,7 @@ var require_reply = __commonJS({
|
|
|
71502
71502
|
} else if (reply[kRouteContext] && reply[kRouteContext][kReplySerializerDefault]) {
|
|
71503
71503
|
payload = reply[kRouteContext][kReplySerializerDefault](payload, reply.raw.statusCode);
|
|
71504
71504
|
} else {
|
|
71505
|
-
payload =
|
|
71505
|
+
payload = serialize2(reply[kRouteContext], payload, reply.raw.statusCode, reply[kReplyHeaders]["content-type"]);
|
|
71506
71506
|
}
|
|
71507
71507
|
} catch (e) {
|
|
71508
71508
|
wrapSerializationError(e, reply);
|
|
@@ -71853,7 +71853,7 @@ var require_reply = __commonJS({
|
|
|
71853
71853
|
internals.preHandlerCallback(null, reply.request, reply);
|
|
71854
71854
|
}
|
|
71855
71855
|
}
|
|
71856
|
-
function
|
|
71856
|
+
function serialize2(context2, data, statusCode, contentType2) {
|
|
71857
71857
|
const fnSerialize = getSchemaSerializer(context2, statusCode, contentType2);
|
|
71858
71858
|
if (fnSerialize) {
|
|
71859
71859
|
return fnSerialize(data);
|
|
@@ -77941,10 +77941,10 @@ var require_fast_uri = __commonJS({
|
|
|
77941
77941
|
function normalize(uri, options) {
|
|
77942
77942
|
if (typeof uri === "string") {
|
|
77943
77943
|
uri = /** @type {T} */
|
|
77944
|
-
|
|
77944
|
+
serialize2(parse4(uri, options), options);
|
|
77945
77945
|
} else if (typeof uri === "object") {
|
|
77946
77946
|
uri = /** @type {T} */
|
|
77947
|
-
parse4(
|
|
77947
|
+
parse4(serialize2(uri, options), options);
|
|
77948
77948
|
}
|
|
77949
77949
|
return uri;
|
|
77950
77950
|
}
|
|
@@ -77952,13 +77952,13 @@ var require_fast_uri = __commonJS({
|
|
|
77952
77952
|
const schemelessOptions = options ? Object.assign({ scheme: "null" }, options) : { scheme: "null" };
|
|
77953
77953
|
const resolved = resolveComponent(parse4(baseURI, schemelessOptions), parse4(relativeURI, schemelessOptions), schemelessOptions, true);
|
|
77954
77954
|
schemelessOptions.skipEscape = true;
|
|
77955
|
-
return
|
|
77955
|
+
return serialize2(resolved, schemelessOptions);
|
|
77956
77956
|
}
|
|
77957
77957
|
function resolveComponent(base, relative, options, skipNormalization) {
|
|
77958
77958
|
const target = {};
|
|
77959
77959
|
if (!skipNormalization) {
|
|
77960
|
-
base = parse4(
|
|
77961
|
-
relative = parse4(
|
|
77960
|
+
base = parse4(serialize2(base, options), options);
|
|
77961
|
+
relative = parse4(serialize2(relative, options), options);
|
|
77962
77962
|
}
|
|
77963
77963
|
options = options || {};
|
|
77964
77964
|
if (!options.tolerant && relative.scheme) {
|
|
@@ -78010,19 +78010,19 @@ var require_fast_uri = __commonJS({
|
|
|
78010
78010
|
function equal(uriA, uriB, options) {
|
|
78011
78011
|
if (typeof uriA === "string") {
|
|
78012
78012
|
uriA = unescape(uriA);
|
|
78013
|
-
uriA =
|
|
78013
|
+
uriA = serialize2(normalizeComponentEncoding(parse4(uriA, options), true), { ...options, skipEscape: true });
|
|
78014
78014
|
} else if (typeof uriA === "object") {
|
|
78015
|
-
uriA =
|
|
78015
|
+
uriA = serialize2(normalizeComponentEncoding(uriA, true), { ...options, skipEscape: true });
|
|
78016
78016
|
}
|
|
78017
78017
|
if (typeof uriB === "string") {
|
|
78018
78018
|
uriB = unescape(uriB);
|
|
78019
|
-
uriB =
|
|
78019
|
+
uriB = serialize2(normalizeComponentEncoding(parse4(uriB, options), true), { ...options, skipEscape: true });
|
|
78020
78020
|
} else if (typeof uriB === "object") {
|
|
78021
|
-
uriB =
|
|
78021
|
+
uriB = serialize2(normalizeComponentEncoding(uriB, true), { ...options, skipEscape: true });
|
|
78022
78022
|
}
|
|
78023
78023
|
return uriA.toLowerCase() === uriB.toLowerCase();
|
|
78024
78024
|
}
|
|
78025
|
-
function
|
|
78025
|
+
function serialize2(cmpts, opts) {
|
|
78026
78026
|
const component = {
|
|
78027
78027
|
host: cmpts.host,
|
|
78028
78028
|
scheme: cmpts.scheme,
|
|
@@ -78178,7 +78178,7 @@ var require_fast_uri = __commonJS({
|
|
|
78178
78178
|
resolve: resolve3,
|
|
78179
78179
|
resolveComponent,
|
|
78180
78180
|
equal,
|
|
78181
|
-
serialize,
|
|
78181
|
+
serialize: serialize2,
|
|
78182
78182
|
parse: parse4
|
|
78183
78183
|
};
|
|
78184
78184
|
module.exports = fastUri;
|
|
@@ -81734,9 +81734,9 @@ var require_serialize = __commonJS({
|
|
|
81734
81734
|
const serializeFuncCode = gen.toString();
|
|
81735
81735
|
sourceCode = `${gen.scopeRefs(names_1.default.scope)}return ${serializeFuncCode}`;
|
|
81736
81736
|
const makeSerialize = new Function(`${names_1.default.scope}`, sourceCode);
|
|
81737
|
-
const
|
|
81738
|
-
this.scope.value(serializeName, { ref:
|
|
81739
|
-
sch.serialize =
|
|
81737
|
+
const serialize2 = makeSerialize(this.scope.get());
|
|
81738
|
+
this.scope.value(serializeName, { ref: serialize2 });
|
|
81739
|
+
sch.serialize = serialize2;
|
|
81740
81740
|
} catch (e) {
|
|
81741
81741
|
if (sourceCode)
|
|
81742
81742
|
this.logger.error("Error compiling serializer, function code:", sourceCode);
|
|
@@ -93805,7 +93805,7 @@ var require_parse_url = __commonJS({
|
|
|
93805
93805
|
var require_form_data = __commonJS({
|
|
93806
93806
|
"../../node_modules/.pnpm/light-my-request@6.6.0/node_modules/light-my-request/lib/form-data.js"(exports, module) {
|
|
93807
93807
|
"use strict";
|
|
93808
|
-
var { randomUUID:
|
|
93808
|
+
var { randomUUID: randomUUID29 } = __require("node:crypto");
|
|
93809
93809
|
var { Readable: Readable4 } = __require("node:stream");
|
|
93810
93810
|
var textEncoder;
|
|
93811
93811
|
function isFormDataLike(payload) {
|
|
@@ -93813,7 +93813,7 @@ var require_form_data = __commonJS({
|
|
|
93813
93813
|
}
|
|
93814
93814
|
function formDataToStream(formdata) {
|
|
93815
93815
|
textEncoder = textEncoder ?? new TextEncoder();
|
|
93816
|
-
const boundary = `----formdata-${
|
|
93816
|
+
const boundary = `----formdata-${randomUUID29()}`;
|
|
93817
93817
|
const prefix = `--${boundary}\r
|
|
93818
93818
|
Content-Disposition: form-data`;
|
|
93819
93819
|
const escape2 = (str2) => str2.replace(/\n/g, "%0A").replace(/\r/g, "%0D").replace(/"/g, "%22");
|
|
@@ -107959,7 +107959,7 @@ var require_websocket = __commonJS({
|
|
|
107959
107959
|
var http = __require("http");
|
|
107960
107960
|
var net2 = __require("net");
|
|
107961
107961
|
var tls = __require("tls");
|
|
107962
|
-
var { randomBytes: randomBytes5, createHash:
|
|
107962
|
+
var { randomBytes: randomBytes5, createHash: createHash11 } = __require("crypto");
|
|
107963
107963
|
var { Duplex, Readable: Readable4 } = __require("stream");
|
|
107964
107964
|
var { URL: URL2 } = __require("url");
|
|
107965
107965
|
var PerMessageDeflate = require_permessage_deflate();
|
|
@@ -108619,7 +108619,7 @@ var require_websocket = __commonJS({
|
|
|
108619
108619
|
abortHandshake(websocket, socket, "Invalid Upgrade header");
|
|
108620
108620
|
return;
|
|
108621
108621
|
}
|
|
108622
|
-
const digest =
|
|
108622
|
+
const digest = createHash11("sha1").update(key2 + GUID).digest("base64");
|
|
108623
108623
|
if (res.headers["sec-websocket-accept"] !== digest) {
|
|
108624
108624
|
abortHandshake(websocket, socket, "Invalid Sec-WebSocket-Accept header");
|
|
108625
108625
|
return;
|
|
@@ -108986,7 +108986,7 @@ var require_websocket_server = __commonJS({
|
|
|
108986
108986
|
var EventEmitter3 = __require("events");
|
|
108987
108987
|
var http = __require("http");
|
|
108988
108988
|
var { Duplex } = __require("stream");
|
|
108989
|
-
var { createHash:
|
|
108989
|
+
var { createHash: createHash11 } = __require("crypto");
|
|
108990
108990
|
var extension = require_extension();
|
|
108991
108991
|
var PerMessageDeflate = require_permessage_deflate();
|
|
108992
108992
|
var subprotocol = require_subprotocol();
|
|
@@ -109287,7 +109287,7 @@ var require_websocket_server = __commonJS({
|
|
|
109287
109287
|
);
|
|
109288
109288
|
}
|
|
109289
109289
|
if (this._state > RUNNING) return abortHandshake(socket, 503);
|
|
109290
|
-
const digest =
|
|
109290
|
+
const digest = createHash11("sha1").update(key2 + GUID).digest("base64");
|
|
109291
109291
|
const headers = [
|
|
109292
109292
|
"HTTP/1.1 101 Switching Protocols",
|
|
109293
109293
|
"Upgrade: websocket",
|
|
@@ -149412,7 +149412,7 @@ var init_chunk_JW73PKED = __esm({
|
|
|
149412
149412
|
"use strict";
|
|
149413
149413
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
149414
149414
|
exports.parse = parse22;
|
|
149415
|
-
exports.serialize =
|
|
149415
|
+
exports.serialize = serialize2;
|
|
149416
149416
|
var cookieNameRegExp = /^[\u0021-\u003A\u003C\u003E-\u007E]+$/;
|
|
149417
149417
|
var cookieValueRegExp = /^[\u0021-\u003A\u003C-\u007E]*$/;
|
|
149418
149418
|
var domainValueRegExp = /^([.]?[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)([.][a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)*$/i;
|
|
@@ -149470,7 +149470,7 @@ var init_chunk_JW73PKED = __esm({
|
|
|
149470
149470
|
}
|
|
149471
149471
|
return min;
|
|
149472
149472
|
}
|
|
149473
|
-
function
|
|
149473
|
+
function serialize2(name25, val, options) {
|
|
149474
149474
|
const enc = options?.encode || encodeURIComponent;
|
|
149475
149475
|
if (!cookieNameRegExp.test(name25)) {
|
|
149476
149476
|
throw new TypeError(`argument name is invalid: ${name25}`);
|
|
@@ -186780,7 +186780,8 @@ var WORKFLOW_ACTIVE_STATUSES = [
|
|
|
186780
186780
|
"waiting_reviewer",
|
|
186781
186781
|
"waiting_feedback",
|
|
186782
186782
|
"discussing",
|
|
186783
|
-
"sending_feedback"
|
|
186783
|
+
"sending_feedback",
|
|
186784
|
+
"waiting_rereview"
|
|
186784
186785
|
];
|
|
186785
186786
|
|
|
186786
186787
|
// src/storage/types.ts
|
|
@@ -187052,12 +187053,14 @@ var mapRemoteCreationIntent = (row) => ({
|
|
|
187052
187053
|
created_at: row.created_at,
|
|
187053
187054
|
updated_at: row.updated_at,
|
|
187054
187055
|
prepare_operation_id: row.prepare_operation_id ?? null,
|
|
187055
|
-
prepared_at: row.prepared_at ?? null
|
|
187056
|
+
prepared_at: row.prepared_at ?? null,
|
|
187057
|
+
first_turn_grant_context: row.first_turn_grant_context ?? null
|
|
187056
187058
|
});
|
|
187057
187059
|
var mapRemoteReviewerCreationIntent = (row) => ({
|
|
187058
187060
|
...row,
|
|
187059
187061
|
review_span: row.review_span,
|
|
187060
187062
|
review_context_mode: row.review_context_mode ?? null,
|
|
187063
|
+
loop_max_rounds: row.loop_max_rounds ?? null,
|
|
187061
187064
|
status: row.status
|
|
187062
187065
|
});
|
|
187063
187066
|
var nowActivityAt = () => sql`cast((julianday('now') - 2440587.5) * 86400000 as integer)`;
|
|
@@ -187312,7 +187315,10 @@ var createAgentSessionRepos = (kdb, h) => ({
|
|
|
187312
187315
|
return row.history_epoch;
|
|
187313
187316
|
},
|
|
187314
187317
|
delete: async (id) => {
|
|
187315
|
-
await kdb.
|
|
187318
|
+
await kdb.transaction().execute(async (trx) => {
|
|
187319
|
+
await trx.deleteFrom("agent_sessions").where("id", "=", id).execute();
|
|
187320
|
+
await trx.deleteFrom("agent_session_remote_grants").where("session_id", "=", id).execute();
|
|
187321
|
+
});
|
|
187316
187322
|
},
|
|
187317
187323
|
// The original inline statement is `INSERT ... ON CONFLICT(session_id,
|
|
187318
187324
|
// entry_index) DO UPDATE SET data = excluded.data` (NOT `INSERT OR
|
|
@@ -187740,6 +187746,7 @@ var createAgentSessionRepos = (kdb, h) => ({
|
|
|
187740
187746
|
if (row) {
|
|
187741
187747
|
await trx.deleteFrom("notification_sync_cursors").where("remote_server_id", "=", row.remote_server_id).where("remote_session_id", "=", row.remote_session_id).execute();
|
|
187742
187748
|
}
|
|
187749
|
+
await trx.deleteFrom("agent_session_remote_grants").where("session_id", "=", localSessionId).execute();
|
|
187743
187750
|
return (result.numDeletedRows ?? 0n) > 0n;
|
|
187744
187751
|
});
|
|
187745
187752
|
},
|
|
@@ -187770,6 +187777,7 @@ var createAgentSessionRepos = (kdb, h) => ({
|
|
|
187770
187777
|
up_to_entry_index: intent.upToEntryIndex ?? null,
|
|
187771
187778
|
prepare_operation_id: intent.prepareOperationId ?? null,
|
|
187772
187779
|
prepared_at: null,
|
|
187780
|
+
first_turn_grant_context: null,
|
|
187773
187781
|
status: "pending",
|
|
187774
187782
|
error: null,
|
|
187775
187783
|
created_at: h.nowMs(),
|
|
@@ -187806,6 +187814,15 @@ var createAgentSessionRepos = (kdb, h) => ({
|
|
|
187806
187814
|
markPrepared: async (localSessionId, preparedAt) => {
|
|
187807
187815
|
await kdb.updateTable("remote_session_creation_intents").set({ prepared_at: preparedAt, error: null, updated_at: h.nowMs() }).where("local_session_id", "=", localSessionId).execute();
|
|
187808
187816
|
},
|
|
187817
|
+
// Write-once, then read back whatever won: a concurrent retry must reuse
|
|
187818
|
+
// the first writer's text rather than believe its own.
|
|
187819
|
+
setFirstTurnGrantContext: async (localSessionId, text2) => {
|
|
187820
|
+
return kdb.transaction().execute(async (trx) => {
|
|
187821
|
+
await trx.updateTable("remote_session_creation_intents").set({ first_turn_grant_context: text2 }).where("local_session_id", "=", localSessionId).where("first_turn_grant_context", "is", null).execute();
|
|
187822
|
+
const row = await trx.selectFrom("remote_session_creation_intents").select("first_turn_grant_context").where("local_session_id", "=", localSessionId).executeTakeFirst();
|
|
187823
|
+
return row?.first_turn_grant_context ?? void 0;
|
|
187824
|
+
});
|
|
187825
|
+
},
|
|
187809
187826
|
discardStaleLifecycleIntents: async ({ cutoff, limit }) => {
|
|
187810
187827
|
const victims = await kdb.selectFrom("remote_session_creation_intents").select("local_session_id").where("status", "=", "pending").where("prepare_operation_id", "is not", null).where(sql`cast((julianday(updated_at) - 2440587.5) * 86400000 as integer) < ${cutoff}`).limit(limit).execute();
|
|
187811
187828
|
if (victims.length === 0) return 0;
|
|
@@ -187828,6 +187845,7 @@ var createAgentSessionRepos = (kdb, h) => ({
|
|
|
187828
187845
|
source_turn_end_index: intent.sourceTurnEndIndex ?? null,
|
|
187829
187846
|
review_span: intent.reviewSpan,
|
|
187830
187847
|
review_context_mode: intent.reviewContextMode ?? null,
|
|
187848
|
+
loop_max_rounds: intent.loopMaxRounds ?? null,
|
|
187831
187849
|
agent_type: intent.agentType,
|
|
187832
187850
|
intent_brief: intent.intentBrief ?? null,
|
|
187833
187851
|
user_id: intent.userId ?? null,
|
|
@@ -187837,7 +187855,7 @@ var createAgentSessionRepos = (kdb, h) => ({
|
|
|
187837
187855
|
updated_at: h.nowMs()
|
|
187838
187856
|
}).onConflict((oc) => oc.column("local_reviewer_session_id").doNothing()).execute();
|
|
187839
187857
|
const row = await trx.selectFrom("remote_reviewer_creation_intents").selectAll().where("local_reviewer_session_id", "=", intent.localReviewerSessionId).executeTakeFirstOrThrow();
|
|
187840
|
-
const sameIdentity = row.remote_reviewer_session_id === intent.remoteReviewerSessionId && row.remote_run_id === intent.remoteRunId && row.project_id === intent.projectId && row.remote_server_id === intent.remoteServerId && (row.branch ?? "") === (intent.branch ?? "") && row.remote_path === intent.remotePath && row.source_remote_session_id === intent.sourceRemoteSessionId && row.review_focus === (intent.reviewFocus ?? null) && row.source_turn_end_index === (intent.sourceTurnEndIndex ?? null) && row.review_span === intent.reviewSpan && (row.review_context_mode ?? null) === (intent.reviewContextMode ?? null) && row.agent_type === intent.agentType && row.intent_brief === (intent.intentBrief ?? null) && row.user_id === (intent.userId ?? null);
|
|
187858
|
+
const sameIdentity = row.remote_reviewer_session_id === intent.remoteReviewerSessionId && row.remote_run_id === intent.remoteRunId && row.project_id === intent.projectId && row.remote_server_id === intent.remoteServerId && (row.branch ?? "") === (intent.branch ?? "") && row.remote_path === intent.remotePath && row.source_remote_session_id === intent.sourceRemoteSessionId && row.review_focus === (intent.reviewFocus ?? null) && row.source_turn_end_index === (intent.sourceTurnEndIndex ?? null) && row.review_span === intent.reviewSpan && (row.review_context_mode ?? null) === (intent.reviewContextMode ?? null) && (row.loop_max_rounds ?? null) === (intent.loopMaxRounds ?? null) && row.agent_type === intent.agentType && row.intent_brief === (intent.intentBrief ?? null) && row.user_id === (intent.userId ?? null);
|
|
187841
187859
|
if (!sameIdentity) {
|
|
187842
187860
|
throw new Error(`Remote reviewer creation intent ${intent.localReviewerSessionId} has conflicting identity`);
|
|
187843
187861
|
}
|
|
@@ -188147,6 +188165,12 @@ var createCrossRemoteAuditRepo = (kdb) => ({
|
|
|
188147
188165
|
insert: async (entry) => {
|
|
188148
188166
|
await kdb.insertInto("cross_remote_audit").values({ id: crypto3.randomUUID(), created_at: (/* @__PURE__ */ new Date()).toISOString(), ...entry }).execute();
|
|
188149
188167
|
},
|
|
188168
|
+
listSessionTargets: async (sessionId, userId, limit = 8) => {
|
|
188169
|
+
let query = kdb.selectFrom("cross_remote_audit").select(["target_remote_id"]).select((eb) => eb.fn.max("seq").as("last_seq")).where("session_id", "=", sessionId).where("status", "not in", ["denied", "offline"]);
|
|
188170
|
+
if (userId) query = query.where("user_id", "=", userId);
|
|
188171
|
+
const rows = await query.groupBy("target_remote_id").orderBy("last_seq", "desc").limit(limit).execute();
|
|
188172
|
+
return rows.map((row) => row.target_remote_id);
|
|
188173
|
+
},
|
|
188150
188174
|
listByTarget: async (targetRemoteId, limit = 100) => {
|
|
188151
188175
|
const rows = await kdb.selectFrom("cross_remote_audit").selectAll().where("target_remote_id", "=", targetRemoteId).orderBy("seq", "desc").limit(limit).execute();
|
|
188152
188176
|
return rows.map(mapRow);
|
|
@@ -188154,6 +188178,33 @@ var createCrossRemoteAuditRepo = (kdb) => ({
|
|
|
188154
188178
|
}
|
|
188155
188179
|
});
|
|
188156
188180
|
|
|
188181
|
+
// src/storage/repositories/session-remote-grants.ts
|
|
188182
|
+
var createSessionRemoteGrantRepo = (kdb) => ({
|
|
188183
|
+
sessionRemoteGrants: {
|
|
188184
|
+
list: async (sessionId) => {
|
|
188185
|
+
const rows = await kdb.selectFrom("agent_session_remote_grants").select("remote_server_id").where("session_id", "=", sessionId).orderBy("granted_at", "asc").orderBy("remote_server_id", "asc").execute();
|
|
188186
|
+
return rows.map((row) => row.remote_server_id);
|
|
188187
|
+
},
|
|
188188
|
+
replace: async (sessionId, userId, remoteServerIds) => {
|
|
188189
|
+
const unique = [...new Set(remoteServerIds)];
|
|
188190
|
+
const grantedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
188191
|
+
await kdb.transaction().execute(async (trx) => {
|
|
188192
|
+
await trx.deleteFrom("agent_session_remote_grants").where("session_id", "=", sessionId).execute();
|
|
188193
|
+
if (unique.length === 0) return;
|
|
188194
|
+
await trx.insertInto("agent_session_remote_grants").values(unique.map((remoteServerId) => ({
|
|
188195
|
+
session_id: sessionId,
|
|
188196
|
+
remote_server_id: remoteServerId,
|
|
188197
|
+
user_id: userId,
|
|
188198
|
+
granted_at: grantedAt
|
|
188199
|
+
}))).execute();
|
|
188200
|
+
});
|
|
188201
|
+
},
|
|
188202
|
+
deleteBySession: async (sessionId) => {
|
|
188203
|
+
await kdb.deleteFrom("agent_session_remote_grants").where("session_id", "=", sessionId).execute();
|
|
188204
|
+
}
|
|
188205
|
+
}
|
|
188206
|
+
});
|
|
188207
|
+
|
|
188157
188208
|
// src/storage/repositories/merge-targets.ts
|
|
188158
188209
|
var createMergeTargetsRepo = (kdb) => ({
|
|
188159
188210
|
mergeTargets: {
|
|
@@ -188789,7 +188840,10 @@ var createWorkflowRunRepos = (kdb) => ({
|
|
|
188789
188840
|
...opts,
|
|
188790
188841
|
reviewer_session_id: opts.reviewer_session_id ?? null,
|
|
188791
188842
|
review_span: opts.review_span ?? "this_turn",
|
|
188792
|
-
status: opts.status ?? "waiting_reviewer"
|
|
188843
|
+
status: opts.status ?? "waiting_reviewer",
|
|
188844
|
+
loop_id: opts.loop_id ?? null,
|
|
188845
|
+
round: opts.round ?? 1,
|
|
188846
|
+
max_rounds: opts.max_rounds ?? null
|
|
188793
188847
|
}).execute();
|
|
188794
188848
|
const row = await kdb.selectFrom("workflow_runs").selectAll().where("id", "=", opts.id).executeTakeFirstOrThrow();
|
|
188795
188849
|
return asRun(row);
|
|
@@ -188817,6 +188871,10 @@ var createWorkflowRunRepos = (kdb) => ({
|
|
|
188817
188871
|
const row = await kdb.selectFrom("workflow_runs").selectAll().where("source_session_id", "=", sourceSessionId).where("status", "=", "completed").where("reviewer_session_id", "is not", null).orderBy("created_at", "desc").orderBy(sql`rowid`, "desc").executeTakeFirst();
|
|
188818
188872
|
return row ? asRun(row) : void 0;
|
|
188819
188873
|
},
|
|
188874
|
+
getLoopRound: async (loopId, round) => {
|
|
188875
|
+
const row = await kdb.selectFrom("workflow_runs").selectAll().where("loop_id", "=", loopId).where("round", "=", round).orderBy("created_at", "desc").orderBy(sql`rowid`, "desc").executeTakeFirst();
|
|
188876
|
+
return row ? asRun(row) : void 0;
|
|
188877
|
+
},
|
|
188820
188878
|
listReviewedSourceSessions: async (projectId, branch) => {
|
|
188821
188879
|
const rows = await kdb.selectFrom("workflow_runs").select("source_session_id").distinct().where("project_id", "=", projectId).where("branch", "is", branch).where("status", "=", "completed").where("reviewer_session_id", "is not", null).execute();
|
|
188822
188880
|
return rows.map((r) => r.source_session_id);
|
|
@@ -188843,6 +188901,51 @@ var createWorkflowRunRepos = (kdb) => ({
|
|
|
188843
188901
|
await trx.insertInto("notification_outbox").values(outbox).onConflict((oc) => oc.column("id").doNothing()).execute();
|
|
188844
188902
|
return true;
|
|
188845
188903
|
});
|
|
188904
|
+
},
|
|
188905
|
+
// Step CAS, run CAS and the outbox row in ONE transaction. A guard that
|
|
188906
|
+
// fails throws to roll the whole thing back — a claimed step with an
|
|
188907
|
+
// un-advanced run would be unrecoverable after a restart.
|
|
188908
|
+
claimStepAndTransition: async ({ stepId, turnEndIndex, outputSnapshot, run: run2, nextRun }) => {
|
|
188909
|
+
const LOST = /* @__PURE__ */ Symbol("cas-lost");
|
|
188910
|
+
try {
|
|
188911
|
+
await kdb.transaction().execute(async (trx) => {
|
|
188912
|
+
const step = await trx.updateTable("workflow_run_steps").set({
|
|
188913
|
+
status: "claimed",
|
|
188914
|
+
turn_end_index: turnEndIndex,
|
|
188915
|
+
output_snapshot: outputSnapshot,
|
|
188916
|
+
error: null,
|
|
188917
|
+
updated_at: sql`datetime('now')`
|
|
188918
|
+
}).where("id", "=", stepId).where("status", "=", "dispatched").executeTakeFirst();
|
|
188919
|
+
if ((step.numUpdatedRows ?? 0n) === 0n) throw LOST;
|
|
188920
|
+
if (run2) {
|
|
188921
|
+
const moved = await trx.updateTable("workflow_runs").set({ ...run2.patch ?? {}, status: run2.to, updated_at: sql`datetime('now')` }).where("id", "=", run2.id).where("status", "=", run2.from).executeTakeFirst();
|
|
188922
|
+
if ((moved.numUpdatedRows ?? 0n) === 0n) throw LOST;
|
|
188923
|
+
if (run2.outbox) {
|
|
188924
|
+
await trx.insertInto("notification_outbox").values(run2.outbox).onConflict((oc) => oc.column("id").doNothing()).execute();
|
|
188925
|
+
}
|
|
188926
|
+
}
|
|
188927
|
+
if (nextRun) {
|
|
188928
|
+
await sql`
|
|
188929
|
+
INSERT INTO workflow_runs
|
|
188930
|
+
(id, project_id, branch, source_session_id, source_turn_end_index, reviewer_session_id,
|
|
188931
|
+
review_focus, review_target, review_span, status, loop_id, round, max_rounds)
|
|
188932
|
+
SELECT ${nextRun.id}, ${nextRun.project_id}, ${nextRun.branch}, ${nextRun.source_session_id},
|
|
188933
|
+
${nextRun.source_turn_end_index}, NULL, ${nextRun.review_focus}, ${nextRun.review_target},
|
|
188934
|
+
'this_turn', 'waiting_rereview', ${nextRun.loop_id}, ${nextRun.round}, ${nextRun.max_rounds}
|
|
188935
|
+
WHERE NOT EXISTS (
|
|
188936
|
+
SELECT 1 FROM workflow_runs
|
|
188937
|
+
WHERE status IN (${sql.join(ACTIVE)})
|
|
188938
|
+
AND (source_session_id = ${nextRun.source_session_id}
|
|
188939
|
+
OR reviewer_session_id = ${nextRun.source_session_id})
|
|
188940
|
+
)
|
|
188941
|
+
`.execute(trx);
|
|
188942
|
+
}
|
|
188943
|
+
});
|
|
188944
|
+
return true;
|
|
188945
|
+
} catch (err) {
|
|
188946
|
+
if (err === LOST) return false;
|
|
188947
|
+
throw err;
|
|
188948
|
+
}
|
|
188846
188949
|
}
|
|
188847
188950
|
}
|
|
188848
188951
|
});
|
|
@@ -188872,6 +188975,79 @@ var createTurnSnapshotRepos = (kdb) => ({
|
|
|
188872
188975
|
}
|
|
188873
188976
|
});
|
|
188874
188977
|
|
|
188978
|
+
// src/storage/repositories/workflow-run-steps.ts
|
|
188979
|
+
var asStep = (row) => row;
|
|
188980
|
+
var createWorkflowRunStepRepos = (kdb) => ({
|
|
188981
|
+
workflowRunSteps: {
|
|
188982
|
+
// Lookup and insert share a transaction so two concurrent opens of the
|
|
188983
|
+
// same (run, kind) resolve to one row; the partial unique index is the
|
|
188984
|
+
// backstop if they ever interleave anyway.
|
|
188985
|
+
open: async (opts) => {
|
|
188986
|
+
return kdb.transaction().execute(async (trx) => {
|
|
188987
|
+
const existing = await trx.selectFrom("workflow_run_steps").selectAll().where("run_id", "=", opts.run_id).where("kind", "=", opts.kind).where("status", "=", "dispatched").executeTakeFirst();
|
|
188988
|
+
if (existing) return { step: asStep(existing), reused: true };
|
|
188989
|
+
const max = await trx.selectFrom("workflow_run_steps").select((eb) => eb.fn.max("round").as("round")).where("run_id", "=", opts.run_id).where("status", "!=", "abandoned").executeTakeFirst();
|
|
188990
|
+
const current = Number(max?.round ?? 0);
|
|
188991
|
+
const round = opts.kind === "feedback" ? Math.max(current, 1) : current + 1;
|
|
188992
|
+
await trx.insertInto("workflow_run_steps").values({
|
|
188993
|
+
id: opts.id,
|
|
188994
|
+
run_id: opts.run_id,
|
|
188995
|
+
round,
|
|
188996
|
+
role: opts.role,
|
|
188997
|
+
kind: opts.kind,
|
|
188998
|
+
session_id: opts.session_id,
|
|
188999
|
+
idempotency_key: opts.idempotency_key,
|
|
189000
|
+
payload_hash: opts.payload_hash,
|
|
189001
|
+
status: "dispatched",
|
|
189002
|
+
user_entry_index: null,
|
|
189003
|
+
turn_end_index: null,
|
|
189004
|
+
output_snapshot: null,
|
|
189005
|
+
error: null
|
|
189006
|
+
}).execute();
|
|
189007
|
+
const row = await trx.selectFrom("workflow_run_steps").selectAll().where("id", "=", opts.id).executeTakeFirstOrThrow();
|
|
189008
|
+
return { step: asStep(row), reused: false };
|
|
189009
|
+
});
|
|
189010
|
+
},
|
|
189011
|
+
getById: async (id) => {
|
|
189012
|
+
const row = await kdb.selectFrom("workflow_run_steps").selectAll().where("id", "=", id).executeTakeFirst();
|
|
189013
|
+
return row ? asStep(row) : void 0;
|
|
189014
|
+
},
|
|
189015
|
+
getOpenBySession: async (sessionId) => {
|
|
189016
|
+
const rows = await kdb.selectFrom("workflow_run_steps").selectAll().where("session_id", "=", sessionId).where("status", "=", "dispatched").orderBy("created_at", "asc").orderBy(sql`rowid`, "asc").execute();
|
|
189017
|
+
return rows.map(asStep);
|
|
189018
|
+
},
|
|
189019
|
+
listAllOpen: async () => {
|
|
189020
|
+
const rows = await kdb.selectFrom("workflow_run_steps").selectAll().where("status", "=", "dispatched").orderBy("created_at", "asc").orderBy(sql`rowid`, "asc").execute();
|
|
189021
|
+
return rows.map(asStep);
|
|
189022
|
+
},
|
|
189023
|
+
listByRun: async (runId) => {
|
|
189024
|
+
const rows = await kdb.selectFrom("workflow_run_steps").selectAll().where("run_id", "=", runId).orderBy("round", "asc").orderBy(sql`rowid`, "asc").execute();
|
|
189025
|
+
return rows.map(asStep);
|
|
189026
|
+
},
|
|
189027
|
+
hasAny: async (runId) => {
|
|
189028
|
+
const row = await kdb.selectFrom("workflow_run_steps").select("id").where("run_id", "=", runId).limit(1).executeTakeFirst();
|
|
189029
|
+
return row !== void 0;
|
|
189030
|
+
},
|
|
189031
|
+
setUserEntryIndex: async (id, entryIndex) => {
|
|
189032
|
+
const result = await kdb.updateTable("workflow_run_steps").set({ user_entry_index: entryIndex, updated_at: sql`datetime('now')` }).where("id", "=", id).where("status", "=", "dispatched").executeTakeFirst();
|
|
189033
|
+
return (result.numUpdatedRows ?? 0n) > 0n;
|
|
189034
|
+
},
|
|
189035
|
+
abandon: async (id, error48) => {
|
|
189036
|
+
const result = await kdb.updateTable("workflow_run_steps").set({ status: "abandoned", error: error48, updated_at: sql`datetime('now')` }).where("id", "=", id).where("status", "=", "dispatched").executeTakeFirst();
|
|
189037
|
+
return (result.numUpdatedRows ?? 0n) > 0n;
|
|
189038
|
+
},
|
|
189039
|
+
abandonOpenByRun: async (runId, error48, role) => {
|
|
189040
|
+
let q = kdb.updateTable("workflow_run_steps").set({ status: "abandoned", error: error48, updated_at: sql`datetime('now')` }).where("run_id", "=", runId).where("status", "=", "dispatched");
|
|
189041
|
+
if (role) q = q.where("role", "=", role);
|
|
189042
|
+
const result = await q.executeTakeFirst();
|
|
189043
|
+
return Number(result.numUpdatedRows ?? 0n);
|
|
189044
|
+
},
|
|
189045
|
+
setError: async (id, error48) => {
|
|
189046
|
+
await kdb.updateTable("workflow_run_steps").set({ error: error48, updated_at: sql`datetime('now')` }).where("id", "=", id).where("status", "=", "dispatched").execute();
|
|
189047
|
+
}
|
|
189048
|
+
}
|
|
189049
|
+
});
|
|
189050
|
+
|
|
188875
189051
|
// src/storage/repositories/notifications.ts
|
|
188876
189052
|
var mapOutbox = (row) => ({
|
|
188877
189053
|
seq: row.seq,
|
|
@@ -204556,6 +204732,22 @@ var initializeSchema = (db) => {
|
|
|
204556
204732
|
CREATE INDEX IF NOT EXISTS idx_remote_session_creation_intents_pending
|
|
204557
204733
|
ON remote_session_creation_intents(status, remote_server_id, updated_at);
|
|
204558
204734
|
|
|
204735
|
+
-- Per-session cross-remote allowlist. The machine tier
|
|
204736
|
+
-- (remote_servers.cross_remote_access) is the ceiling; this table is what
|
|
204737
|
+
-- the agent of one session may actually reach. No FK on session_id:
|
|
204738
|
+
-- remote- prefixed sessions live only in remote_session_mappings /
|
|
204739
|
+
-- creation intents, so half the keys would have nothing to point at.
|
|
204740
|
+
CREATE TABLE IF NOT EXISTS agent_session_remote_grants (
|
|
204741
|
+
session_id TEXT NOT NULL,
|
|
204742
|
+
remote_server_id TEXT NOT NULL REFERENCES remote_servers(id) ON DELETE CASCADE,
|
|
204743
|
+
user_id TEXT NOT NULL,
|
|
204744
|
+
granted_at TEXT NOT NULL,
|
|
204745
|
+
PRIMARY KEY (session_id, remote_server_id)
|
|
204746
|
+
);
|
|
204747
|
+
|
|
204748
|
+
CREATE INDEX IF NOT EXISTS idx_session_remote_grants_session
|
|
204749
|
+
ON agent_session_remote_grants(session_id);
|
|
204750
|
+
|
|
204559
204751
|
CREATE TABLE IF NOT EXISTS remote_reviewer_creation_intents (
|
|
204560
204752
|
local_reviewer_session_id TEXT PRIMARY KEY,
|
|
204561
204753
|
remote_reviewer_session_id TEXT NOT NULL,
|
|
@@ -204569,6 +204761,7 @@ var initializeSchema = (db) => {
|
|
|
204569
204761
|
source_turn_end_index INTEGER,
|
|
204570
204762
|
review_span TEXT NOT NULL CHECK (review_span IN ('this_turn', 'session_start')),
|
|
204571
204763
|
review_context_mode TEXT CHECK (review_context_mode IN ('briefed', 'blind')),
|
|
204764
|
+
loop_max_rounds INTEGER,
|
|
204572
204765
|
agent_type TEXT NOT NULL,
|
|
204573
204766
|
intent_brief TEXT,
|
|
204574
204767
|
user_id TEXT,
|
|
@@ -204605,6 +204798,12 @@ var initializeSchema = (db) => {
|
|
|
204605
204798
|
);
|
|
204606
204799
|
|
|
204607
204800
|
CREATE INDEX IF NOT EXISTS idx_cross_remote_audit_target ON cross_remote_audit(target_remote_id, seq);
|
|
204801
|
+
|
|
204802
|
+
-- listSessionTargets asks "which machines has this session touched?" on the
|
|
204803
|
+
-- path of every artifact hover, so it must not scan a table that grows with
|
|
204804
|
+
-- every gateway call ever made. seq trails session_id to serve the
|
|
204805
|
+
-- most-recent-first ordering from the index.
|
|
204806
|
+
CREATE INDEX IF NOT EXISTS idx_cross_remote_audit_session ON cross_remote_audit(session_id, seq);
|
|
204608
204807
|
`);
|
|
204609
204808
|
const workspaceCheckoutsTable = db.prepare(
|
|
204610
204809
|
"SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'workspace_checkouts'"
|
|
@@ -204720,6 +204919,9 @@ var initializeSchema = (db) => {
|
|
|
204720
204919
|
db.exec("ALTER TABLE remote_session_creation_intents ADD COLUMN prepare_operation_id TEXT DEFAULT NULL");
|
|
204721
204920
|
db.exec("ALTER TABLE remote_session_creation_intents ADD COLUMN prepared_at INTEGER DEFAULT NULL");
|
|
204722
204921
|
}
|
|
204922
|
+
if (!remoteCreationIntentColumns.some((column) => column.name === "first_turn_grant_context")) {
|
|
204923
|
+
db.exec("ALTER TABLE remote_session_creation_intents ADD COLUMN first_turn_grant_context TEXT DEFAULT NULL");
|
|
204924
|
+
}
|
|
204723
204925
|
db.exec(`
|
|
204724
204926
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_remote_session_creation_intents_prepare_operation
|
|
204725
204927
|
ON remote_session_creation_intents(prepare_operation_id)
|
|
@@ -205633,6 +205835,10 @@ var initializeSchema = (db) => {
|
|
|
205633
205835
|
status TEXT NOT NULL DEFAULT 'waiting_reviewer',
|
|
205634
205836
|
error TEXT,
|
|
205635
205837
|
prepared_context TEXT,
|
|
205838
|
+
loop_id TEXT,
|
|
205839
|
+
round INTEGER NOT NULL DEFAULT 1,
|
|
205840
|
+
max_rounds INTEGER,
|
|
205841
|
+
verdict TEXT,
|
|
205636
205842
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
205637
205843
|
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
205638
205844
|
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE
|
|
@@ -205644,6 +205850,35 @@ var initializeSchema = (db) => {
|
|
|
205644
205850
|
CREATE INDEX IF NOT EXISTS idx_workflow_runs_project_branch_status
|
|
205645
205851
|
ON workflow_runs(project_id, branch, status);
|
|
205646
205852
|
|
|
205853
|
+
-- One row per logical dispatch from the workflow engine to a session: the
|
|
205854
|
+
-- identity a turn completion is attributed to. Growth: a handful of rows
|
|
205855
|
+
-- per run (one per reviewer prompt / final-verdict request / feedback
|
|
205856
|
+
-- send), deleted with the run. No FK on session_id: like workflow_runs and
|
|
205857
|
+
-- notification_outbox, a step must outlive deletion of its session.
|
|
205858
|
+
CREATE TABLE IF NOT EXISTS workflow_run_steps (
|
|
205859
|
+
id TEXT PRIMARY KEY,
|
|
205860
|
+
run_id TEXT NOT NULL,
|
|
205861
|
+
round INTEGER NOT NULL,
|
|
205862
|
+
role TEXT NOT NULL,
|
|
205863
|
+
kind TEXT NOT NULL,
|
|
205864
|
+
session_id TEXT NOT NULL,
|
|
205865
|
+
idempotency_key TEXT NOT NULL,
|
|
205866
|
+
payload_hash TEXT NOT NULL,
|
|
205867
|
+
status TEXT NOT NULL DEFAULT 'dispatched',
|
|
205868
|
+
user_entry_index INTEGER,
|
|
205869
|
+
turn_end_index INTEGER,
|
|
205870
|
+
output_snapshot TEXT,
|
|
205871
|
+
error TEXT,
|
|
205872
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
205873
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
205874
|
+
FOREIGN KEY (run_id) REFERENCES workflow_runs(id) ON DELETE CASCADE
|
|
205875
|
+
);
|
|
205876
|
+
CREATE INDEX IF NOT EXISTS idx_workflow_run_steps_open ON workflow_run_steps(session_id, status);
|
|
205877
|
+
CREATE INDEX IF NOT EXISTS idx_workflow_run_steps_run ON workflow_run_steps(run_id, round);
|
|
205878
|
+
-- At most one open dispatch per (run, kind): a retry reuses it.
|
|
205879
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_workflow_run_steps_one_open
|
|
205880
|
+
ON workflow_run_steps(run_id, kind) WHERE status = 'dispatched';
|
|
205881
|
+
|
|
205647
205882
|
CREATE TABLE IF NOT EXISTS turn_snapshots (
|
|
205648
205883
|
session_id TEXT NOT NULL,
|
|
205649
205884
|
turn_end_index INTEGER NOT NULL,
|
|
@@ -205785,10 +206020,23 @@ var initializeSchema = (db) => {
|
|
|
205785
206020
|
if (!workflowRunsInfo.some((col) => col.name === "prepared_context")) {
|
|
205786
206021
|
db.exec("ALTER TABLE workflow_runs ADD COLUMN prepared_context TEXT");
|
|
205787
206022
|
}
|
|
206023
|
+
for (const [name25, ddl] of [
|
|
206024
|
+
["loop_id", "loop_id TEXT"],
|
|
206025
|
+
["round", "round INTEGER NOT NULL DEFAULT 1"],
|
|
206026
|
+
["max_rounds", "max_rounds INTEGER"],
|
|
206027
|
+
["verdict", "verdict TEXT"]
|
|
206028
|
+
]) {
|
|
206029
|
+
if (!workflowRunsInfo.some((col) => col.name === name25)) {
|
|
206030
|
+
db.exec(`ALTER TABLE workflow_runs ADD COLUMN ${ddl}`);
|
|
206031
|
+
}
|
|
206032
|
+
}
|
|
205788
206033
|
const reviewerIntentsInfo = db.prepare("PRAGMA table_info(remote_reviewer_creation_intents)").all();
|
|
205789
206034
|
if (!reviewerIntentsInfo.some((col) => col.name === "review_context_mode")) {
|
|
205790
206035
|
db.exec("ALTER TABLE remote_reviewer_creation_intents ADD COLUMN review_context_mode TEXT CHECK (review_context_mode IN ('briefed', 'blind'))");
|
|
205791
206036
|
}
|
|
206037
|
+
if (!reviewerIntentsInfo.some((col) => col.name === "loop_max_rounds")) {
|
|
206038
|
+
db.exec("ALTER TABLE remote_reviewer_creation_intents ADD COLUMN loop_max_rounds INTEGER");
|
|
206039
|
+
}
|
|
205792
206040
|
db.exec(`
|
|
205793
206041
|
CREATE TABLE IF NOT EXISTS user_settings (
|
|
205794
206042
|
user_id TEXT NOT NULL,
|
|
@@ -205920,10 +206168,12 @@ var createSqliteStorage = async (dbPath) => {
|
|
|
205920
206168
|
...createAgentSessionRepos(kdb, h),
|
|
205921
206169
|
...createWorkspaceRepos(kdb, h),
|
|
205922
206170
|
...createCrossRemoteAuditRepo(kdb),
|
|
206171
|
+
...createSessionRemoteGrantRepo(kdb),
|
|
205923
206172
|
...createMergeTargetsRepo(kdb),
|
|
205924
206173
|
...createSearchCacheRepos(kdb, h),
|
|
205925
206174
|
...createWorkflowRunRepos(kdb),
|
|
205926
206175
|
...createTurnSnapshotRepos(kdb),
|
|
206176
|
+
...createWorkflowRunStepRepos(kdb),
|
|
205927
206177
|
...createNotificationRepos(kdb),
|
|
205928
206178
|
...createProjectChatRepos(kdb),
|
|
205929
206179
|
close: async () => {
|
|
@@ -207604,12 +207854,16 @@ var sessionFailedId = (sessionId, turnEndEntryIndex) => `session:${sessionId}:tu
|
|
|
207604
207854
|
var reviewReadyId = (workflowRunId, turnEndEntryIndex) => `workflow:${workflowRunId}:turn:${turnEndEntryIndex}:review-ready`;
|
|
207605
207855
|
var workflowFailedId = (workflowRunId, stateVersion) => `workflow:${workflowRunId}:failed:${stateVersion}`;
|
|
207606
207856
|
function findTurnOpeningUserEntry(entries, beforeIndex) {
|
|
207607
|
-
|
|
207857
|
+
const index = findTurnOpeningUserEntryIndex(entries, beforeIndex);
|
|
207858
|
+
return index === null ? void 0 : entries[index];
|
|
207859
|
+
}
|
|
207860
|
+
function findTurnOpeningUserEntryIndex(entries, beforeIndex) {
|
|
207861
|
+
let opening = null;
|
|
207608
207862
|
for (let i = Math.min(beforeIndex, entries.length) - 1; i >= 0; i--) {
|
|
207609
207863
|
const entry = entries[i];
|
|
207610
207864
|
if (!entry) continue;
|
|
207611
207865
|
if (entry.type === "turn_end") break;
|
|
207612
|
-
if (entry.type === "user") opening =
|
|
207866
|
+
if (entry.type === "user") opening = i;
|
|
207613
207867
|
}
|
|
207614
207868
|
return opening;
|
|
207615
207869
|
}
|
|
@@ -207809,6 +208063,13 @@ var ClaudeCodeProvider = class {
|
|
|
207809
208063
|
}
|
|
207810
208064
|
};
|
|
207811
208065
|
|
|
208066
|
+
// src/protocol/mcp/tool-names.ts
|
|
208067
|
+
function qualifiedMcpToolName(tool2, server) {
|
|
208068
|
+
const serverName = server?.trim();
|
|
208069
|
+
if (!serverName || tool2.startsWith("mcp__")) return tool2;
|
|
208070
|
+
return `mcp__${serverName}__${tool2}`;
|
|
208071
|
+
}
|
|
208072
|
+
|
|
207812
208073
|
// src/protocol/codex/codec.ts
|
|
207813
208074
|
function parseCodexLine(line) {
|
|
207814
208075
|
let msg;
|
|
@@ -208080,7 +208341,7 @@ var CodexProvider = class _CodexProvider {
|
|
|
208080
208341
|
}
|
|
208081
208342
|
case "mcpToolCall": {
|
|
208082
208343
|
const id = item.id ?? this.generateId();
|
|
208083
|
-
const toolName = item.tool ? canonicalizeSessionToolName(item.tool, item.server) : "MCP";
|
|
208344
|
+
const toolName = item.tool ? qualifiedMcpToolName(canonicalizeSessionToolName(item.tool, item.server), item.server) : "MCP";
|
|
208084
208345
|
const output = item.error?.message ?? (item.result ? JSON.stringify(item.result) : "");
|
|
208085
208346
|
return [
|
|
208086
208347
|
{ type: "tool_use", tool: toolName, input: item.arguments, toolUseId: id },
|
|
@@ -208395,6 +208656,7 @@ var TOOL_TIERS = {
|
|
|
208395
208656
|
remote_mcp_close: "exec"
|
|
208396
208657
|
};
|
|
208397
208658
|
var MAX_IN_FLIGHT_PER_SESSION = 4;
|
|
208659
|
+
var MAX_SESSION_GRANTS = 64;
|
|
208398
208660
|
var REMOTE_MCP_CAPABILITIES = [
|
|
208399
208661
|
"http:POST /api/path/cross-remote/mcp/open",
|
|
208400
208662
|
"http:POST /api/path/cross-remote/mcp/list-tools",
|
|
@@ -208409,11 +208671,8 @@ function isSessionUsable(deps, sessionId) {
|
|
|
208409
208671
|
if (sessionId.startsWith("remote-")) return deps.remoteSessionMap.has(sessionId);
|
|
208410
208672
|
return deps.agentSessionManager.getSessionProcessAlive(sessionId);
|
|
208411
208673
|
}
|
|
208412
|
-
async function
|
|
208413
|
-
|
|
208414
|
-
return { ok: false, reason: "not_accessible" };
|
|
208415
|
-
}
|
|
208416
|
-
const server = await deps.storage.remoteServers.getById(targetRemoteId, payload.userId);
|
|
208674
|
+
async function canReachRemote(deps, userId, targetRemoteId, requiredTier) {
|
|
208675
|
+
const server = await deps.storage.remoteServers.getById(targetRemoteId, userId);
|
|
208417
208676
|
if (!server) return { ok: false, reason: "not_accessible" };
|
|
208418
208677
|
if (!tierSatisfies(server.cross_remote_access, requiredTier)) {
|
|
208419
208678
|
return { ok: false, reason: "not_accessible" };
|
|
@@ -208421,9 +208680,23 @@ async function resolveTarget(deps, payload, targetRemoteId, requiredTier) {
|
|
|
208421
208680
|
if (!isOnline(deps, server)) return { ok: false, reason: "offline" };
|
|
208422
208681
|
return { ok: true, server };
|
|
208423
208682
|
}
|
|
208683
|
+
async function grantedTo(deps, sessionId) {
|
|
208684
|
+
return new Set(await deps.storage.sessionRemoteGrants.list(sessionId));
|
|
208685
|
+
}
|
|
208686
|
+
async function resolveTarget(deps, payload, targetRemoteId, requiredTier) {
|
|
208687
|
+
if (payload.sourceRemoteServerId && payload.sourceRemoteServerId === targetRemoteId) {
|
|
208688
|
+
return { ok: false, reason: "not_accessible" };
|
|
208689
|
+
}
|
|
208690
|
+
const granted = await grantedTo(deps, payload.sessionId);
|
|
208691
|
+
if (!granted.has(targetRemoteId)) return { ok: false, reason: "not_granted" };
|
|
208692
|
+
return canReachRemote(deps, payload.userId, targetRemoteId, requiredTier);
|
|
208693
|
+
}
|
|
208424
208694
|
async function listAccessibleRemotes(deps, payload) {
|
|
208425
|
-
const servers = await
|
|
208426
|
-
|
|
208695
|
+
const [servers, granted] = await Promise.all([
|
|
208696
|
+
deps.storage.remoteServers.getAll(payload.userId),
|
|
208697
|
+
grantedTo(deps, payload.sessionId)
|
|
208698
|
+
]);
|
|
208699
|
+
return servers.filter((s3) => s3.cross_remote_access !== "off").filter((s3) => s3.id !== payload.sourceRemoteServerId).filter((s3) => granted.has(s3.id)).map((s3) => ({
|
|
208427
208700
|
id: s3.id,
|
|
208428
208701
|
name: s3.name,
|
|
208429
208702
|
access: s3.cross_remote_access,
|
|
@@ -208431,6 +208704,22 @@ async function listAccessibleRemotes(deps, payload) {
|
|
|
208431
208704
|
mcp_broker_supported: supportsRemoteMcpBroker(s3)
|
|
208432
208705
|
}));
|
|
208433
208706
|
}
|
|
208707
|
+
async function validateSessionGrantIds(storage2, ids, userId, sourceRemoteServerId) {
|
|
208708
|
+
if (ids.length > MAX_SESSION_GRANTS) {
|
|
208709
|
+
return `At most ${MAX_SESSION_GRANTS} remotes can be granted to one session`;
|
|
208710
|
+
}
|
|
208711
|
+
for (const id of ids) {
|
|
208712
|
+
if (sourceRemoteServerId && id === sourceRemoteServerId) {
|
|
208713
|
+
return `Remote ${id} is the machine this session runs on`;
|
|
208714
|
+
}
|
|
208715
|
+
const server = await storage2.remoteServers.getById(id, userId);
|
|
208716
|
+
if (!server) return `Remote ${id} not found`;
|
|
208717
|
+
if (server.cross_remote_access === "off") {
|
|
208718
|
+
return `Remote ${server.name} has cross-remote access turned off`;
|
|
208719
|
+
}
|
|
208720
|
+
}
|
|
208721
|
+
return null;
|
|
208722
|
+
}
|
|
208434
208723
|
var SessionConcurrencyGuard = class {
|
|
208435
208724
|
constructor(maxInFlight = MAX_IN_FLIGHT_PER_SESSION) {
|
|
208436
208725
|
this.maxInFlight = maxInFlight;
|
|
@@ -229930,6 +230219,45 @@ async function resolveFastChatModel(storage2, userId) {
|
|
|
229930
230219
|
return resolveModel(config2.fast, config2.apiKeys);
|
|
229931
230220
|
}
|
|
229932
230221
|
|
|
230222
|
+
// src/cross-remote-grant-context.ts
|
|
230223
|
+
var VREMOTES_BLOCK_RE = /<vremotes(?:\s[^>]*)?>[\s\S]*?<\/vremotes>/g;
|
|
230224
|
+
function stripRemoteGrantContext(text2) {
|
|
230225
|
+
return text2.replace(VREMOTES_BLOCK_RE, "").trimEnd();
|
|
230226
|
+
}
|
|
230227
|
+
async function listGrantedServers(storage2, sessionId, userId) {
|
|
230228
|
+
const ids = await storage2.sessionRemoteGrants.list(sessionId);
|
|
230229
|
+
if (ids.length === 0) return [];
|
|
230230
|
+
const servers = await Promise.all(
|
|
230231
|
+
ids.map((id) => storage2.remoteServers.getById(id, userId).catch(() => void 0))
|
|
230232
|
+
);
|
|
230233
|
+
return servers.filter((s3) => s3 !== void 0).sort((a, b2) => a.name.localeCompare(b2.name));
|
|
230234
|
+
}
|
|
230235
|
+
async function buildRemoteGrantContext(storage2, sessionId, userId) {
|
|
230236
|
+
const usable = (await listGrantedServers(storage2, sessionId, userId)).filter((s3) => s3.cross_remote_access !== "off");
|
|
230237
|
+
if (usable.length === 0) return null;
|
|
230238
|
+
const listed = usable.map((s3) => `${s3.name} (id: ${s3.id}, ${s3.cross_remote_access})`).join(", ");
|
|
230239
|
+
const names = usable.map((s3) => s3.name.replace(/["<>]/g, "")).join(", ");
|
|
230240
|
+
return [
|
|
230241
|
+
`<vremotes names="${names}">`,
|
|
230242
|
+
`Cross-remote access granted for this session: ${listed}.`,
|
|
230243
|
+
"Use the cross-remote MCP tools when a request concerns one of these machines. Being granted does not mean every command should run there; the local workspace remains the default target.",
|
|
230244
|
+
// Diagnosing on a remote tends to carry over into fixing there, so the
|
|
230245
|
+
// default for changes is spelled out separately from "where to look".
|
|
230246
|
+
"Remote access is for inspection and diagnosis. Make code changes (editing files, git operations, installing dependencies) in the local workspace, even when the problem was found on a remote; only modify a remote machine when the user explicitly asks for the change to be made there. If a fix can only be applied on the remote (for example, machine-specific config), propose it and ask before changing anything.",
|
|
230247
|
+
"</vremotes>"
|
|
230248
|
+
].join("\n");
|
|
230249
|
+
}
|
|
230250
|
+
function appendContextBlock(content, block) {
|
|
230251
|
+
if (!block) return content;
|
|
230252
|
+
if (typeof content === "string") return content.length > 0 ? `${content}
|
|
230253
|
+
|
|
230254
|
+
${block}` : block;
|
|
230255
|
+
return [...content, { type: "text", text: block }];
|
|
230256
|
+
}
|
|
230257
|
+
async function appendRemoteGrantContext(storage2, sessionId, userId, content) {
|
|
230258
|
+
return appendContextBlock(content, await buildRemoteGrantContext(storage2, sessionId, userId));
|
|
230259
|
+
}
|
|
230260
|
+
|
|
229933
230261
|
// src/utils/session-title.ts
|
|
229934
230262
|
var TITLE_MAX_CHARS = 60;
|
|
229935
230263
|
var AI_TIMEOUT_MS = 15e3;
|
|
@@ -229947,8 +230275,8 @@ async function isChatModelConfigured(storage2, userId) {
|
|
|
229947
230275
|
return isModelConfigured(config2, config2.fast);
|
|
229948
230276
|
}
|
|
229949
230277
|
function extractUserText(content) {
|
|
229950
|
-
|
|
229951
|
-
return
|
|
230278
|
+
const text2 = typeof content === "string" ? content : content.filter((p2) => p2.type === "text").map((p2) => p2.text).join(" ");
|
|
230279
|
+
return stripRemoteGrantContext(text2);
|
|
229952
230280
|
}
|
|
229953
230281
|
function snippetTitle(text2) {
|
|
229954
230282
|
const trimmed = text2.trim();
|
|
@@ -230675,6 +231003,12 @@ function describeImageToolOutput(output) {
|
|
|
230675
231003
|
}
|
|
230676
231004
|
var AgentSessionManager = class {
|
|
230677
231005
|
sessions = /* @__PURE__ */ new Map();
|
|
231006
|
+
/**
|
|
231007
|
+
* How long a freshly spawned process gets before the woken session's context
|
|
231008
|
+
* is written to its stdin. The wake awaits this write (see
|
|
231009
|
+
* wakeDormantSessionInner); tests shorten it.
|
|
231010
|
+
*/
|
|
231011
|
+
wakeStdinDelayMs = 500;
|
|
230678
231012
|
storage;
|
|
230679
231013
|
eventBus = null;
|
|
230680
231014
|
/**
|
|
@@ -232495,7 +232829,7 @@ var AgentSessionManager = class {
|
|
|
232495
232829
|
console.error(`[AgentSession] Cannot wake dormant session ${sessionId} without projectPath`);
|
|
232496
232830
|
return false;
|
|
232497
232831
|
}
|
|
232498
|
-
return this.wakeDormantSession(session, projectPath, content, userId,
|
|
232832
|
+
return this.wakeDormantSession(session, projectPath, content, userId, disposition, opts);
|
|
232499
232833
|
}
|
|
232500
232834
|
if (session.workspaceCheckoutId) {
|
|
232501
232835
|
await this.resolveSessionWorktreePath(session, projectPath ?? session.checkoutPath ?? "");
|
|
@@ -232504,7 +232838,8 @@ var AgentSessionManager = class {
|
|
|
232504
232838
|
return false;
|
|
232505
232839
|
}
|
|
232506
232840
|
this.applyCompletionTimerAction(session, session.completion.userTurnStarted());
|
|
232507
|
-
|
|
232841
|
+
const openedFromIdle = session.status !== "running";
|
|
232842
|
+
if (openedFromIdle) {
|
|
232508
232843
|
session.status = "running";
|
|
232509
232844
|
if (!session.skipDb) await this.storage.agentSessions.updateStatus(sessionId, "running");
|
|
232510
232845
|
this.broadcastPatch(sessionId, ConversationPatch.updateStatus("running"));
|
|
@@ -232513,16 +232848,21 @@ var AgentSessionManager = class {
|
|
|
232513
232848
|
this.touchSession(session);
|
|
232514
232849
|
await this.finalizeStreamingEntry(session);
|
|
232515
232850
|
session.store.currentAssistantIndex = null;
|
|
232516
|
-
|
|
232517
|
-
|
|
232518
|
-
|
|
232519
|
-
|
|
232520
|
-
|
|
232521
|
-
|
|
232522
|
-
|
|
232523
|
-
|
|
232524
|
-
|
|
232525
|
-
|
|
232851
|
+
try {
|
|
232852
|
+
const userEntryIndex = await this.pushEntry(sessionId, {
|
|
232853
|
+
type: "user",
|
|
232854
|
+
content,
|
|
232855
|
+
timestamp: Date.now(),
|
|
232856
|
+
...opts?.origin ? { origin: opts.origin } : {},
|
|
232857
|
+
notificationDisposition: disposition
|
|
232858
|
+
// Lifecycle activation: an entry-write failure must abort before stdin
|
|
232859
|
+
// (strict), or the session would be marked active with no durable turn.
|
|
232860
|
+
}, true, userId, { strictPersist: opts?.onUserEntryPersisted !== void 0 });
|
|
232861
|
+
if (opts?.onUserEntryPersisted) await opts.onUserEntryPersisted(userEntryIndex);
|
|
232862
|
+
} catch (error48) {
|
|
232863
|
+
if (openedFromIdle) await this.abortSendBeforeStdin(session);
|
|
232864
|
+
throw error48;
|
|
232865
|
+
}
|
|
232526
232866
|
try {
|
|
232527
232867
|
const provider = getProvider(session.agentType);
|
|
232528
232868
|
const formatted = provider.formatUserInput(content, session.id);
|
|
@@ -232547,6 +232887,7 @@ var AgentSessionManager = class {
|
|
|
232547
232887
|
return true;
|
|
232548
232888
|
} catch (error48) {
|
|
232549
232889
|
console.error(`[AgentSession] Failed to send message:`, error48);
|
|
232890
|
+
if (openedFromIdle && session.turnOpenSince === null) await this.abortSendBeforeStdin(session);
|
|
232550
232891
|
return false;
|
|
232551
232892
|
}
|
|
232552
232893
|
}
|
|
@@ -233269,7 +233610,7 @@ var AgentSessionManager = class {
|
|
|
233269
233610
|
* — see the `retentionDeleting` / `processStartsInFlight` pair in
|
|
233270
233611
|
* `beginProcessStart`.
|
|
233271
233612
|
*/
|
|
233272
|
-
async wakeDormantSession(session, projectPath, userMessage, userId = "local",
|
|
233613
|
+
async wakeDormantSession(session, projectPath, userMessage, userId = "local", notificationDisposition = "result", opts) {
|
|
233273
233614
|
const release = this.beginProcessStart(session);
|
|
233274
233615
|
if (!release) {
|
|
233275
233616
|
console.log(`[AgentSession] Refusing to wake ${session.id}: retention is deleting it`);
|
|
@@ -233277,21 +233618,21 @@ var AgentSessionManager = class {
|
|
|
233277
233618
|
}
|
|
233278
233619
|
console.log(`[AgentSession] Waking dormant session ${session.id}`);
|
|
233279
233620
|
try {
|
|
233280
|
-
await this.wakeDormantSessionInner(
|
|
233621
|
+
return await this.wakeDormantSessionInner(
|
|
233281
233622
|
session,
|
|
233282
233623
|
projectPath,
|
|
233283
233624
|
userMessage,
|
|
233284
233625
|
userId,
|
|
233285
|
-
|
|
233286
|
-
|
|
233626
|
+
notificationDisposition,
|
|
233627
|
+
opts
|
|
233287
233628
|
);
|
|
233288
|
-
return true;
|
|
233289
233629
|
} finally {
|
|
233290
233630
|
release();
|
|
233291
233631
|
this.unloadIfSpawnAbandoned(session);
|
|
233292
233632
|
}
|
|
233293
233633
|
}
|
|
233294
|
-
async wakeDormantSessionInner(session, projectPath, userMessage, userId,
|
|
233634
|
+
async wakeDormantSessionInner(session, projectPath, userMessage, userId, notificationDisposition, opts) {
|
|
233635
|
+
const origin = opts?.origin;
|
|
233295
233636
|
const absoluteWorktreePath = await this.resolveSessionWorktreePath(session, projectPath);
|
|
233296
233637
|
await this.hydrateForSpawn(session);
|
|
233297
233638
|
await this.ensureResidentCapacity(
|
|
@@ -233308,27 +233649,69 @@ var AgentSessionManager = class {
|
|
|
233308
233649
|
provider.onSessionDestroyed?.(session.id);
|
|
233309
233650
|
provider.onSessionCreated?.(session.id, session.permissionMode);
|
|
233310
233651
|
await this.spawnAgent(session, absoluteWorktreePath);
|
|
233311
|
-
|
|
233312
|
-
|
|
233313
|
-
|
|
233314
|
-
|
|
233315
|
-
|
|
233316
|
-
|
|
233317
|
-
|
|
233652
|
+
try {
|
|
233653
|
+
const userEntryIndex = await this.pushEntry(session.id, {
|
|
233654
|
+
type: "user",
|
|
233655
|
+
content: userMessage,
|
|
233656
|
+
timestamp: Date.now(),
|
|
233657
|
+
...origin ? { origin } : {},
|
|
233658
|
+
notificationDisposition
|
|
233659
|
+
}, true, userId, { strictPersist: opts?.onUserEntryPersisted !== void 0 });
|
|
233660
|
+
if (opts?.onUserEntryPersisted) await opts.onUserEntryPersisted(userEntryIndex);
|
|
233661
|
+
} catch (error48) {
|
|
233662
|
+
await this.abortSendBeforeStdin(session);
|
|
233663
|
+
throw error48;
|
|
233664
|
+
}
|
|
233318
233665
|
session.turnOpenSince = Date.now();
|
|
233319
233666
|
session.turnDisposition = notificationDisposition;
|
|
233320
|
-
|
|
233321
|
-
|
|
233322
|
-
if (context2) {
|
|
233323
|
-
const provider2 = getProvider(session.agentType);
|
|
233324
|
-
const formatted = provider2.formatUserInput(context2, session.id);
|
|
233667
|
+
const written = await new Promise((resolve3) => {
|
|
233668
|
+
setTimeout(() => {
|
|
233325
233669
|
try {
|
|
233326
|
-
session.
|
|
233670
|
+
const context2 = this.buildFullConversationContext(session.store.entries);
|
|
233671
|
+
const stdin = session.process?.stdin;
|
|
233672
|
+
if (!context2 || !stdin) return resolve3(false);
|
|
233673
|
+
stdin.write(getProvider(session.agentType).formatUserInput(context2, session.id));
|
|
233674
|
+
resolve3(true);
|
|
233327
233675
|
} catch (error48) {
|
|
233328
233676
|
console.error(`[AgentSession] Failed to send context to woken session:`, error48);
|
|
233677
|
+
resolve3(false);
|
|
233329
233678
|
}
|
|
233330
|
-
}
|
|
233331
|
-
}
|
|
233679
|
+
}, this.wakeStdinDelayMs);
|
|
233680
|
+
});
|
|
233681
|
+
if (!written) {
|
|
233682
|
+
session.turnOpenSince = null;
|
|
233683
|
+
await this.abortSendBeforeStdin(session);
|
|
233684
|
+
}
|
|
233685
|
+
return written;
|
|
233686
|
+
}
|
|
233687
|
+
/**
|
|
233688
|
+
* A send flipped an idle session to "running" and then aborted before stdin
|
|
233689
|
+
* was written (strict entry-write failure, a throwing evidence hook, or the
|
|
233690
|
+
* write itself failing). Two things must be undone, or the NEXT send breaks:
|
|
233691
|
+
*
|
|
233692
|
+
* - the turn boundary: the user entry may already be in the transcript, and
|
|
233693
|
+
* an entry with no `turn_end` after it reads as the OPENER of whatever
|
|
233694
|
+
* turn comes next — wrong notification disposition, and a workflow
|
|
233695
|
+
* dispatch whose recorded index no longer matches its own turn. Close it
|
|
233696
|
+
* with a `failed` turn_end; `internal` so it notifies nobody.
|
|
233697
|
+
* - the status: nothing is in flight, so a session left "running" would
|
|
233698
|
+
* refuse every retry as busy.
|
|
233699
|
+
*
|
|
233700
|
+
* Only while the process is still ours and alive — an exit that landed
|
|
233701
|
+
* meanwhile has already settled both through its own handler.
|
|
233702
|
+
*/
|
|
233703
|
+
async abortSendBeforeStdin(session) {
|
|
233704
|
+
if (session.status !== "running" || session.turnOpenSince !== null) return;
|
|
233705
|
+
if (session.process && session.process.exitCode !== null) return;
|
|
233706
|
+
await this.pushTurnEnd(session, "failed", "internal", Date.now());
|
|
233707
|
+
session.status = "stopped";
|
|
233708
|
+
if (!session.skipDb) {
|
|
233709
|
+
await this.storage.agentSessions.updateStatusPreservingTimestamp(session.id, "stopped").catch((err) => {
|
|
233710
|
+
console.error(`[AgentSession] Failed to restore idle status for ${session.id}:`, err);
|
|
233711
|
+
});
|
|
233712
|
+
}
|
|
233713
|
+
this.broadcastPatch(session.id, ConversationPatch.updateStatus("stopped"));
|
|
233714
|
+
this.eventBus?.emit({ type: "session:status", projectId: session.projectId, branch: session.branch, sessionId: session.id, status: "stopped" });
|
|
233332
233715
|
}
|
|
233333
233716
|
/**
|
|
233334
233717
|
* Rebuild an in-memory MessageStore (entries, replay patches, tool tracker,
|
|
@@ -234788,6 +235171,7 @@ async function createRemoteWorkflowReviewer(deps, params) {
|
|
|
234788
235171
|
reviewContextMode: params.reviewContextMode ?? null,
|
|
234789
235172
|
agentType: params.reviewerAgentType,
|
|
234790
235173
|
intentBrief: params.intentBrief ?? null,
|
|
235174
|
+
loopMaxRounds: params.loopMaxRounds ?? null,
|
|
234791
235175
|
userId: params.userId ?? null
|
|
234792
235176
|
});
|
|
234793
235177
|
let registeredLocalSessionId = null;
|
|
@@ -234799,7 +235183,9 @@ async function createRemoteWorkflowReviewer(deps, params) {
|
|
|
234799
235183
|
reviewSpan: params.reviewSpan,
|
|
234800
235184
|
reviewerAgentType: params.reviewerAgentType,
|
|
234801
235185
|
runId: remoteRunId,
|
|
234802
|
-
newReviewerSessionId: remoteReviewerSessionId
|
|
235186
|
+
newReviewerSessionId: remoteReviewerSessionId,
|
|
235187
|
+
// Additive tunnel field; an older worker ignores it (single-pass review).
|
|
235188
|
+
...params.loopMaxRounds !== void 0 ? { loop: { maxRounds: params.loopMaxRounds } } : {}
|
|
234803
235189
|
};
|
|
234804
235190
|
const proxyOpts = { reverseConnectManager: deps.reverseConnectManager ?? void 0 };
|
|
234805
235191
|
const result = params.phase === "prepare" ? await proxyToRemoteAuto(
|
|
@@ -235018,6 +235404,7 @@ function recoverPendingRemoteReviewerOnce(deps, intent) {
|
|
|
235018
235404
|
reviewContextMode: intent.review_context_mode ?? void 0,
|
|
235019
235405
|
reviewerAgentType: intent.agent_type,
|
|
235020
235406
|
intentBrief: intent.intent_brief ?? void 0,
|
|
235407
|
+
loopMaxRounds: intent.loop_max_rounds ?? void 0,
|
|
235021
235408
|
userId: intent.user_id ?? void 0,
|
|
235022
235409
|
remoteRunId: intent.remote_run_id,
|
|
235023
235410
|
remoteReviewerSessionId: intent.remote_reviewer_session_id,
|
|
@@ -240949,11 +241336,79 @@ var ProjectChatManager = class {
|
|
|
240949
241336
|
};
|
|
240950
241337
|
|
|
240951
241338
|
// src/workflow-engine.ts
|
|
240952
|
-
import { randomUUID as
|
|
241339
|
+
import { randomUUID as randomUUID7 } from "crypto";
|
|
241340
|
+
|
|
241341
|
+
// src/instruction-delivery.ts
|
|
241342
|
+
import { createHash as createHash3, randomUUID as randomUUID6 } from "crypto";
|
|
241343
|
+
var instructionReceiverToken = randomUUID6();
|
|
241344
|
+
var instructionDeliveryLocks = /* @__PURE__ */ new Map();
|
|
241345
|
+
var sessionMutationLocks = /* @__PURE__ */ new Map();
|
|
241346
|
+
async function serialize(table, key2, effect) {
|
|
241347
|
+
const previous = table.get(key2) ?? Promise.resolve();
|
|
241348
|
+
let release;
|
|
241349
|
+
const gate = new Promise((resolve3) => {
|
|
241350
|
+
release = resolve3;
|
|
241351
|
+
});
|
|
241352
|
+
const tail = previous.then(() => gate);
|
|
241353
|
+
table.set(key2, tail);
|
|
241354
|
+
await previous;
|
|
241355
|
+
try {
|
|
241356
|
+
return await effect();
|
|
241357
|
+
} finally {
|
|
241358
|
+
release();
|
|
241359
|
+
if (table.get(key2) === tail) table.delete(key2);
|
|
241360
|
+
}
|
|
241361
|
+
}
|
|
241362
|
+
function serializeSessionMutation(sessionId, effect) {
|
|
241363
|
+
return serialize(sessionMutationLocks, sessionId, effect);
|
|
241364
|
+
}
|
|
241365
|
+
function instructionContentHash(content) {
|
|
241366
|
+
const canonical = typeof content === "string" ? JSON.stringify({ type: "string", content }) : JSON.stringify(content.map((part) => part.type === "text" ? { type: "text", text: part.text } : { type: "image", mediaType: part.mediaType, data: part.data }));
|
|
241367
|
+
return createHash3("sha256").update(canonical).digest("hex");
|
|
241368
|
+
}
|
|
241369
|
+
async function deliverInstruction(input) {
|
|
241370
|
+
const { storage: storage2, sessionId, idempotencyKey } = input;
|
|
241371
|
+
const ledger = storage2.agentInstructionDeliveries;
|
|
241372
|
+
const ref = { sessionId, idempotencyKey, claimToken: instructionReceiverToken };
|
|
241373
|
+
return serialize(instructionDeliveryLocks, `${sessionId}\0${idempotencyKey}`, async () => {
|
|
241374
|
+
const claim = await ledger.claim({ ...ref, contentHash: instructionContentHash(input.rawContent) });
|
|
241375
|
+
if (claim === "conflict") return "conflict";
|
|
241376
|
+
if (claim === "sent") return "replayed";
|
|
241377
|
+
if (claim === "busy") return "busy";
|
|
241378
|
+
let ownershipLost = false;
|
|
241379
|
+
const renew = async () => {
|
|
241380
|
+
try {
|
|
241381
|
+
if (!await ledger.renewClaim(ref)) ownershipLost = true;
|
|
241382
|
+
} catch {
|
|
241383
|
+
ownershipLost = true;
|
|
241384
|
+
}
|
|
241385
|
+
};
|
|
241386
|
+
await renew();
|
|
241387
|
+
const heartbeat = setInterval(() => {
|
|
241388
|
+
void renew();
|
|
241389
|
+
}, 1e4);
|
|
241390
|
+
heartbeat.unref();
|
|
241391
|
+
try {
|
|
241392
|
+
if (ownershipLost) return "ownership_lost_before_send";
|
|
241393
|
+
if (!await input.deliver()) {
|
|
241394
|
+
await ledger.release(ref);
|
|
241395
|
+
return "not_running";
|
|
241396
|
+
}
|
|
241397
|
+
await renew();
|
|
241398
|
+
if (ownershipLost) return "ownership_lost_after_send";
|
|
241399
|
+
return await ledger.markSent(ref) ? "delivered" : "unconfirmed";
|
|
241400
|
+
} catch (error48) {
|
|
241401
|
+
await ledger.release(ref);
|
|
241402
|
+
throw error48;
|
|
241403
|
+
} finally {
|
|
241404
|
+
clearInterval(heartbeat);
|
|
241405
|
+
}
|
|
241406
|
+
});
|
|
241407
|
+
}
|
|
240953
241408
|
|
|
240954
241409
|
// src/utils/review-target.ts
|
|
240955
241410
|
import { execFileSync as execFileSync4 } from "child_process";
|
|
240956
|
-
import { createHash as
|
|
241411
|
+
import { createHash as createHash4 } from "crypto";
|
|
240957
241412
|
var MAX_BUFFER2 = 10 * 1024 * 1024;
|
|
240958
241413
|
function git2(cwd, args) {
|
|
240959
241414
|
return execFileSync4("git", args, {
|
|
@@ -240968,7 +241423,7 @@ function captureReviewTarget(worktreePath) {
|
|
|
240968
241423
|
const baseHead = git2(worktreePath, ["rev-parse", "HEAD"]).trim();
|
|
240969
241424
|
const diff = git2(worktreePath, ["diff"]);
|
|
240970
241425
|
const status = git2(worktreePath, ["status", "--porcelain"]);
|
|
240971
|
-
const diffDigest =
|
|
241426
|
+
const diffDigest = createHash4("sha256").update(diff).update("\0").update(status).digest("hex");
|
|
240972
241427
|
const diffStat = git2(worktreePath, ["diff", "--shortstat"]).trim() || null;
|
|
240973
241428
|
return { baseHead, diffDigest, diffStat, capturedAt: Date.now() };
|
|
240974
241429
|
} catch {
|
|
@@ -240987,6 +241442,31 @@ function hasDrifted(worktreePath, target) {
|
|
|
240987
241442
|
return current.baseHead !== target.baseHead || current.diffDigest !== target.diffDigest;
|
|
240988
241443
|
}
|
|
240989
241444
|
|
|
241445
|
+
// src/utils/review-verdict.ts
|
|
241446
|
+
var VERDICTS = ["ship", "needs-changes", "cannot-verify"];
|
|
241447
|
+
function bareValue(line) {
|
|
241448
|
+
return line.replace(/^\s*(?:#+|>|[-*+]|\d+[.)、])\s*/, "").replace(/[*_`]/g, "").replace(/verdict/gi, "").replace(/^[\s::—–\-.。]+|[\s::—–\-.。]+$/g, "").toLowerCase();
|
|
241449
|
+
}
|
|
241450
|
+
function parseVerdict(text2) {
|
|
241451
|
+
if (!text2) return null;
|
|
241452
|
+
const lines = text2.split(/\r?\n/);
|
|
241453
|
+
let labelLine = -1;
|
|
241454
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
241455
|
+
if (/verdict/i.test(lines[i])) {
|
|
241456
|
+
labelLine = i;
|
|
241457
|
+
break;
|
|
241458
|
+
}
|
|
241459
|
+
}
|
|
241460
|
+
if (labelLine < 0) return null;
|
|
241461
|
+
let value = bareValue(lines[labelLine]);
|
|
241462
|
+
if (value === "") {
|
|
241463
|
+
const next = lines.slice(labelLine + 1).find((l) => l.trim() !== "");
|
|
241464
|
+
if (next === void 0) return null;
|
|
241465
|
+
value = bareValue(next);
|
|
241466
|
+
}
|
|
241467
|
+
return VERDICTS.includes(value) ? value : null;
|
|
241468
|
+
}
|
|
241469
|
+
|
|
240990
241470
|
// src/workflow-engine.ts
|
|
240991
241471
|
var WorkflowError = class extends Error {
|
|
240992
241472
|
constructor(code, message) {
|
|
@@ -240996,6 +241476,10 @@ var WorkflowError = class extends Error {
|
|
|
240996
241476
|
code;
|
|
240997
241477
|
};
|
|
240998
241478
|
var TERMINAL_STATUSES = /* @__PURE__ */ new Set(["completed", "cancelled", "failed"]);
|
|
241479
|
+
var DELIVERY_UNKNOWN_PREFIX = "\u6295\u9012\u7ED3\u679C\u672A\u77E5";
|
|
241480
|
+
var REREVIEW_DELIVERY_UNKNOWN = `${DELIVERY_UNKNOWN_PREFIX}\uFF1A\u590D\u5BA1\u4EFB\u52A1\u53EF\u80FD\u5DF2\u9001\u8FBE reviewer\u3002\u82E5\u5176\u5B8C\u6210\uFF0C\u7ED3\u679C\u4F1A\u81EA\u52A8\u5F52\u5C5E\uFF1B\u5426\u5219\u8BF7\u7ED3\u675F\u672C\u6B21 review \u540E\u91CD\u65B0\u53D1\u8D77\u3002`;
|
|
241481
|
+
var DispatchSupersededError = class extends Error {
|
|
241482
|
+
};
|
|
240999
241483
|
var REVIEWER_TURN = {
|
|
241000
241484
|
origin: "workflow",
|
|
241001
241485
|
notificationDisposition: "milestone-managed"
|
|
@@ -241227,6 +241711,8 @@ function describeActivationFailure(result) {
|
|
|
241227
241711
|
return `reviewer \u65E0\u6CD5\u542F\u52A8\uFF08${result.errorCode}\uFF09`;
|
|
241228
241712
|
}
|
|
241229
241713
|
}
|
|
241714
|
+
var LOOP_MAX_ROUNDS_DEFAULT = 3;
|
|
241715
|
+
var LOOP_MAX_ROUNDS_LIMIT = 10;
|
|
241230
241716
|
var WorkflowEngine = class {
|
|
241231
241717
|
constructor(storage2, agentOps) {
|
|
241232
241718
|
this.storage = storage2;
|
|
@@ -241255,9 +241741,11 @@ var WorkflowEngine = class {
|
|
|
241255
241741
|
}
|
|
241256
241742
|
/** Boot recovery (spec §3.4). Call once after storage is ready. */
|
|
241257
241743
|
async init() {
|
|
241744
|
+
const settled = await this.reconcileOpenSteps();
|
|
241258
241745
|
const active = await this.storage.workflowRuns.getAllActive();
|
|
241259
241746
|
for (const run2 of active) {
|
|
241260
|
-
if (run2.
|
|
241747
|
+
if (settled.has(run2.id)) {
|
|
241748
|
+
} else if (run2.status === "sending_feedback") {
|
|
241261
241749
|
await this.storage.workflowRuns.update(run2.id, {
|
|
241262
241750
|
status: "waiting_feedback",
|
|
241263
241751
|
error: "\u53D1\u9001\u72B6\u6001\u672A\u77E5\uFF1A\u670D\u52A1\u5728\u53D1\u9001\u53CD\u9988\u671F\u95F4\u91CD\u542F\u3002\u8BF7\u68C0\u67E5 source session \u662F\u5426\u5DF2\u6536\u5230\u53CD\u9988\uFF0C\u518D\u51B3\u5B9A\u91CD\u53D1\u6216\u7ED3\u675F\u3002"
|
|
@@ -241277,6 +241765,101 @@ var WorkflowEngine = class {
|
|
|
241277
241765
|
this.trackParticipants(run2);
|
|
241278
241766
|
}
|
|
241279
241767
|
}
|
|
241768
|
+
/**
|
|
241769
|
+
* Restart reconciliation (design §2.7). A crash emits no completion event —
|
|
241770
|
+
* crash repair writes `turn_end{server_restart}` silently — so every step
|
|
241771
|
+
* still `dispatched` is decided here, from evidence:
|
|
241772
|
+
*
|
|
241773
|
+
* - no entry index (for a fresh reviewer: neither on the step nor on the
|
|
241774
|
+
* session row) ⇒ stdin was never written ⇒ proven no side effect: abandon
|
|
241775
|
+
* and roll the run back exactly as a live send failure would. The user
|
|
241776
|
+
* gets the ordinary button back and may even edit before re-sending.
|
|
241777
|
+
* - an index, and the turn it opened completed ⇒ late claim, same
|
|
241778
|
+
* transaction as a live one.
|
|
241779
|
+
* - an index, and that turn ended any other way ⇒ abandon; whether the
|
|
241780
|
+
* instruction was acted on is unprovable, so the run keeps an honest note.
|
|
241781
|
+
* - an index and no turn_end after it ⇒ leave it; nothing to decide yet.
|
|
241782
|
+
*
|
|
241783
|
+
* Returns the ids of runs whose state this pass decided.
|
|
241784
|
+
*/
|
|
241785
|
+
async reconcileOpenSteps() {
|
|
241786
|
+
const settled = /* @__PURE__ */ new Set();
|
|
241787
|
+
const steps = this.storage.workflowRunSteps;
|
|
241788
|
+
for (const step of await steps.listAllOpen()) {
|
|
241789
|
+
try {
|
|
241790
|
+
const run2 = await this.storage.workflowRuns.getById(step.run_id);
|
|
241791
|
+
if (!run2) {
|
|
241792
|
+
await steps.abandon(step.id, "run no longer exists");
|
|
241793
|
+
continue;
|
|
241794
|
+
}
|
|
241795
|
+
const entryIndex = await this.effectiveEntryIndex(step);
|
|
241796
|
+
if (entryIndex === null) {
|
|
241797
|
+
await steps.abandon(step.id, "never delivered: no entry index was recorded before the restart");
|
|
241798
|
+
if (await this.rollBackUndeliveredStep(run2, step)) settled.add(run2.id);
|
|
241799
|
+
continue;
|
|
241800
|
+
}
|
|
241801
|
+
const entries = await this.agentOps.getRawMessages(step.session_id);
|
|
241802
|
+
let turnEndIndex = null;
|
|
241803
|
+
for (let i = entryIndex + 1; i < entries.length; i++) {
|
|
241804
|
+
if (entries[i]?.type === "turn_end") {
|
|
241805
|
+
turnEndIndex = i;
|
|
241806
|
+
break;
|
|
241807
|
+
}
|
|
241808
|
+
}
|
|
241809
|
+
if (turnEndIndex === null) continue;
|
|
241810
|
+
const turnEnd = entries[turnEndIndex];
|
|
241811
|
+
const outcome = turnEnd.outcome ?? "completed";
|
|
241812
|
+
const completed = outcome === "completed" || outcome === "completed_with_pending_tasks";
|
|
241813
|
+
if (completed && findTurnOpeningUserEntryIndex(entries, turnEndIndex) === entryIndex) {
|
|
241814
|
+
await this.claimStep(step, entries, turnEndIndex);
|
|
241815
|
+
settled.add(run2.id);
|
|
241816
|
+
continue;
|
|
241817
|
+
}
|
|
241818
|
+
await steps.abandon(step.id, `turn ended: ${outcome}`);
|
|
241819
|
+
if (TERMINAL_STATUSES.has(run2.status)) continue;
|
|
241820
|
+
if (step.kind === "feedback") {
|
|
241821
|
+
await this.storage.workflowRuns.transition(run2.id, "sending_feedback", "waiting_feedback", {
|
|
241822
|
+
error: "\u53D1\u9001\u72B6\u6001\u672A\u77E5\uFF1A\u670D\u52A1\u5728\u53D1\u9001\u53CD\u9988\u671F\u95F4\u91CD\u542F\u3002\u8BF7\u68C0\u67E5 source session \u662F\u5426\u5DF2\u6536\u5230\u53CD\u9988\uFF0C\u518D\u51B3\u5B9A\u91CD\u53D1\u6216\u7ED3\u675F\u3002"
|
|
241823
|
+
});
|
|
241824
|
+
} else if (run2.status === "waiting_reviewer") {
|
|
241825
|
+
await this.storage.workflowRuns.update(run2.id, {
|
|
241826
|
+
error: "reviewer \u7684\u8FD9\u4E00\u8F6E\u56E0\u670D\u52A1\u91CD\u542F\u800C\u4E2D\u65AD\uFF0C\u6CA1\u6709\u4EA7\u51FA\u53EF\u7528\u7684\u7ED3\u8BBA\u3002\u53EF\u5728\u5176\u7A97\u53E3\u4E2D\u7EE7\u7EED\u5BF9\u8BDD\u540E\u91CD\u65B0\u751F\u6210\u7EC8\u7A3F\uFF0C\u6216\u7ED3\u675F\u672C\u6B21 review\u3002"
|
|
241827
|
+
});
|
|
241828
|
+
}
|
|
241829
|
+
settled.add(run2.id);
|
|
241830
|
+
} catch (err) {
|
|
241831
|
+
console.error(`[WorkflowEngine] reconciling step ${step.id} failed; left dispatched:`, err);
|
|
241832
|
+
}
|
|
241833
|
+
}
|
|
241834
|
+
return settled;
|
|
241835
|
+
}
|
|
241836
|
+
/** The run-side half of "this dispatch provably never reached stdin". */
|
|
241837
|
+
async rollBackUndeliveredStep(run2, step) {
|
|
241838
|
+
if (TERMINAL_STATUSES.has(run2.status)) return false;
|
|
241839
|
+
const note = "\u4E0A\u6B21\u6295\u9012\u56E0\u670D\u52A1\u91CD\u542F\u672A\u9001\u8FBE\uFF08\u76EE\u6807 session \u6CA1\u6709\u6536\u5230\u4EFB\u4F55\u5185\u5BB9\uFF09\uFF0C\u53EF\u76F4\u63A5\u91CD\u65B0\u64CD\u4F5C\u3002";
|
|
241840
|
+
switch (step.kind) {
|
|
241841
|
+
case "feedback":
|
|
241842
|
+
if (!await this.storage.workflowRuns.transition(run2.id, "sending_feedback", "waiting_feedback", { error: note }) && run2.status === "waiting_feedback") {
|
|
241843
|
+
await this.storage.workflowRuns.update(run2.id, { error: note });
|
|
241844
|
+
}
|
|
241845
|
+
return true;
|
|
241846
|
+
case "final_verdict":
|
|
241847
|
+
return this.storage.workflowRuns.transition(run2.id, "waiting_reviewer", "discussing", { error: note });
|
|
241848
|
+
case "rereview_prompt":
|
|
241849
|
+
if (run2.loop_id && run2.round > 1) {
|
|
241850
|
+
return this.storage.workflowRuns.transition(run2.id, "waiting_reviewer", "waiting_rereview", {
|
|
241851
|
+
reviewer_session_id: null,
|
|
241852
|
+
error: "\u590D\u5BA1\u4EFB\u52A1\u56E0\u670D\u52A1\u91CD\u542F\u672A\u9001\u8FBE reviewer\uFF0C\u53EF\u76F4\u63A5\u91CD\u65B0\u53D1\u8D77\u8FD9\u4E00\u8F6E\u590D\u5BA1\u3002"
|
|
241853
|
+
});
|
|
241854
|
+
}
|
|
241855
|
+
await this.failRun(run2, "\u590D\u5BA1\u4EFB\u52A1\u56E0\u670D\u52A1\u91CD\u542F\u672A\u9001\u8FBE reviewer\u3002\u8BF7\u91CD\u65B0\u53D1\u8D77 review\u3002");
|
|
241856
|
+
return true;
|
|
241857
|
+
case "reviewer_prompt":
|
|
241858
|
+
if (run2.status === "preparing") return false;
|
|
241859
|
+
await this.failRun(run2, "reviewer \u7684\u9996\u6761\u6307\u4EE4\u56E0\u670D\u52A1\u91CD\u542F\u672A\u9001\u8FBE\u3002\u8BF7\u91CD\u65B0\u53D1\u8D77 review\u3002");
|
|
241860
|
+
return true;
|
|
241861
|
+
}
|
|
241862
|
+
}
|
|
241280
241863
|
trackParticipants(run2) {
|
|
241281
241864
|
this.participants.set(run2.source_session_id, { runId: run2.id, role: "source" });
|
|
241282
241865
|
if (run2.reviewer_session_id) {
|
|
@@ -241351,6 +241934,325 @@ var WorkflowEngine = class {
|
|
|
241351
241934
|
* when one exists, otherwise the source session (a run can fail before its
|
|
241352
241935
|
* reviewer is ever created).
|
|
241353
241936
|
*/
|
|
241937
|
+
// ---------- review loop (Phase 2 cut 1) ----------
|
|
241938
|
+
/**
|
|
241939
|
+
* The gate run for the next round, if this finished round continues a loop:
|
|
241940
|
+
* a loop run whose verdict was not `ship`, and whose source has not been
|
|
241941
|
+
* taken by a review the user started in the meantime. The source was
|
|
241942
|
+
* released when the feedback was sent, and this claim is asynchronous — a
|
|
241943
|
+
* newer review wins and the old loop simply stops. This in-memory check
|
|
241944
|
+
* mirrors the start entry's synchronous reservation; the insert itself is
|
|
241945
|
+
* guarded again inside the transaction against the persisted active runs.
|
|
241946
|
+
*/
|
|
241947
|
+
nextRoundGate(run2, sourceTurnEndIndex) {
|
|
241948
|
+
if (!run2.loop_id || run2.verdict === "ship") return void 0;
|
|
241949
|
+
const holder = this.participants.get(run2.source_session_id);
|
|
241950
|
+
if (holder && holder.runId !== run2.id) return void 0;
|
|
241951
|
+
return {
|
|
241952
|
+
id: randomUUID7(),
|
|
241953
|
+
project_id: run2.project_id,
|
|
241954
|
+
branch: run2.branch,
|
|
241955
|
+
source_session_id: run2.source_session_id,
|
|
241956
|
+
source_turn_end_index: sourceTurnEndIndex,
|
|
241957
|
+
review_focus: run2.review_focus,
|
|
241958
|
+
// Captured when the re-review is actually confirmed — the workspace may
|
|
241959
|
+
// keep changing while the gate waits.
|
|
241960
|
+
review_target: null,
|
|
241961
|
+
loop_id: run2.loop_id,
|
|
241962
|
+
round: run2.round + 1,
|
|
241963
|
+
max_rounds: run2.max_rounds
|
|
241964
|
+
};
|
|
241965
|
+
}
|
|
241966
|
+
/** The insert is conditional (see claimStepAndTransition); read it back to know. */
|
|
241967
|
+
async announceNextRoundGate(nextRunId) {
|
|
241968
|
+
if (!nextRunId) return;
|
|
241969
|
+
const gate = await this.storage.workflowRuns.getById(nextRunId);
|
|
241970
|
+
if (!gate) return;
|
|
241971
|
+
this.trackParticipants(gate);
|
|
241972
|
+
this.emitRunUpdated(gate);
|
|
241973
|
+
}
|
|
241974
|
+
/** The reviewer the loop continues with: the previous round's. */
|
|
241975
|
+
async loopReviewerSessionId(gate) {
|
|
241976
|
+
if (!gate.loop_id) return null;
|
|
241977
|
+
const previous = await this.storage.workflowRuns.getLoopRound(gate.loop_id, gate.round - 1);
|
|
241978
|
+
return previous?.reviewer_session_id ?? null;
|
|
241979
|
+
}
|
|
241980
|
+
/**
|
|
241981
|
+
* Gate action `rereview`: confirm the next round. From here on the round is
|
|
241982
|
+
* an ordinary re-review on the reuse path.
|
|
241983
|
+
*
|
|
241984
|
+
* Unlike the one-shot start, a prompt that provably did not leave does NOT
|
|
241985
|
+
* fail the run — one concurrent chat with the reviewer must not end the
|
|
241986
|
+
* loop. The run returns to the gate with the reviewer unbound, and the same
|
|
241987
|
+
* gate can be retried. An unknown outcome stays in `waiting_reviewer` under
|
|
241988
|
+
* the usual step reconciliation.
|
|
241989
|
+
*/
|
|
241990
|
+
async approveRereview(runId, opts = {}) {
|
|
241991
|
+
const gate = await this.storage.workflowRuns.getById(runId);
|
|
241992
|
+
if (!gate || gate.status !== "waiting_rereview") {
|
|
241993
|
+
throw new WorkflowError("bad-state", "run \u4E0D\u5728\u7B49\u5F85\u590D\u5BA1\u786E\u8BA4\u7684\u72B6\u6001");
|
|
241994
|
+
}
|
|
241995
|
+
const overCap = gate.max_rounds !== null && gate.round > gate.max_rounds;
|
|
241996
|
+
if (overCap && !opts.extend) {
|
|
241997
|
+
throw new WorkflowError("bad-state", `\u5DF2\u8FBE\u8F6E\u6B21\u4E0A\u9650\uFF08${gate.max_rounds} \u8F6E\uFF09\uFF0C\u9700\u8981\u786E\u8BA4\u8FFD\u52A0\u4E00\u8F6E`);
|
|
241998
|
+
}
|
|
241999
|
+
const project = await this.storage.projects.getById(gate.project_id);
|
|
242000
|
+
if (!project?.path) throw new WorkflowError("reviewer-unavailable", "\u9879\u76EE\u5728\u672C\u673A\u6CA1\u6709\u53EF\u7528\u8DEF\u5F84");
|
|
242001
|
+
const reviewerSessionId = await this.loopReviewerSessionId(gate);
|
|
242002
|
+
if (!reviewerSessionId) throw new WorkflowError("reviewer-unavailable", "\u627E\u4E0D\u5230\u4E0A\u4E00\u8F6E\u7684 reviewer session");
|
|
242003
|
+
const held = this.participants.get(reviewerSessionId);
|
|
242004
|
+
if (held) {
|
|
242005
|
+
throw held.runId === gate.id ? new WorkflowError("bad-state", "\u8FD9\u4E00\u8F6E\u590D\u5BA1\u6B63\u5728\u53D1\u8D77\u4E2D") : new WorkflowError("session-busy", "\u4E0A\u4E00\u8F6E\u7684 reviewer \u6B63\u5728\u53E6\u4E00\u4E2A review \u91CC");
|
|
242006
|
+
}
|
|
242007
|
+
this.participants.set(reviewerSessionId, { runId: gate.id, role: "reviewer" });
|
|
242008
|
+
const releaseReservation = () => {
|
|
242009
|
+
const current = this.participants.get(reviewerSessionId);
|
|
242010
|
+
if (current?.runId === gate.id && current.role === "reviewer") this.participants.delete(reviewerSessionId);
|
|
242011
|
+
};
|
|
242012
|
+
let reviewerSession;
|
|
242013
|
+
let entries;
|
|
242014
|
+
let turnEndIndex;
|
|
242015
|
+
let target;
|
|
242016
|
+
try {
|
|
242017
|
+
const activeElsewhere = await this.storage.workflowRuns.getActiveBySession(reviewerSessionId);
|
|
242018
|
+
if (activeElsewhere && activeElsewhere.id !== gate.id) {
|
|
242019
|
+
throw new WorkflowError("session-busy", "\u4E0A\u4E00\u8F6E\u7684 reviewer \u6B63\u5728\u53E6\u4E00\u4E2A review \u91CC");
|
|
242020
|
+
}
|
|
242021
|
+
const reviewerNow = await this.storage.agentSessions.getById(reviewerSessionId);
|
|
242022
|
+
if (reviewerNow?.status === "running") {
|
|
242023
|
+
throw new WorkflowError("session-busy", "reviewer \u6B63\u5728\u56DE\u590D\u4E2D\uFF0C\u8BF7\u7B49\u5F85\u5176\u5B8C\u6210\u540E\u518D\u53D1\u8D77\u590D\u5BA1");
|
|
242024
|
+
}
|
|
242025
|
+
reviewerSession = await this.assertReviewerReusable(reviewerSessionId, gate.project_id, gate.branch);
|
|
242026
|
+
const sourceSession = await this.storage.agentSessions.getById(gate.source_session_id);
|
|
242027
|
+
if (sourceSession?.status === "running") {
|
|
242028
|
+
throw new WorkflowError("source-running", "source session \u6B63\u5728\u8FD0\u884C\uFF0C\u8BF7\u7B49\u5F85\u5F53\u524D turn \u5B8C\u6210\u540E\u518D\u53D1\u8D77\u590D\u5BA1");
|
|
242029
|
+
}
|
|
242030
|
+
entries = await this.agentOps.getRawMessages(gate.source_session_id);
|
|
242031
|
+
turnEndIndex = extractLatestTurnEndIndex(entries) ?? gate.source_turn_end_index;
|
|
242032
|
+
const sourceProjection = await this.storage.agentSessions.getActivityById(gate.source_session_id, "workflow-reviewer");
|
|
242033
|
+
const worktreePath = sourceProjection?.worktreePath ?? resolveWorktreePath(project.path, gate.branch);
|
|
242034
|
+
target = captureReviewTarget(worktreePath);
|
|
242035
|
+
const claimed = await this.storage.workflowRuns.transition(gate.id, "waiting_rereview", "waiting_reviewer", {
|
|
242036
|
+
reviewer_session_id: reviewerSessionId,
|
|
242037
|
+
error: null
|
|
242038
|
+
});
|
|
242039
|
+
if (!claimed) throw new WorkflowError("bad-state", "run \u72B6\u6001\u5DF2\u53D8\u5316\uFF08\u53EF\u80FD\u5DF2\u88AB\u5904\u7406\uFF09");
|
|
242040
|
+
} catch (err) {
|
|
242041
|
+
releaseReservation();
|
|
242042
|
+
throw err;
|
|
242043
|
+
}
|
|
242044
|
+
await this.storage.workflowRuns.update(gate.id, {
|
|
242045
|
+
source_turn_end_index: turnEndIndex,
|
|
242046
|
+
review_target: JSON.stringify(target),
|
|
242047
|
+
...overCap ? { max_rounds: gate.round } : {}
|
|
242048
|
+
});
|
|
242049
|
+
const run2 = await this.storage.workflowRuns.getById(gate.id);
|
|
242050
|
+
const outcome = await this.dispatchRereview({
|
|
242051
|
+
run: run2,
|
|
242052
|
+
reviewerSession,
|
|
242053
|
+
project: { id: project.id, path: project.path },
|
|
242054
|
+
entries,
|
|
242055
|
+
turnEndIndex,
|
|
242056
|
+
target
|
|
242057
|
+
});
|
|
242058
|
+
if (outcome.kind === "mode-switch-failed" || outcome.kind === "no_side_effect") {
|
|
242059
|
+
const busy = outcome.kind === "no_side_effect" && outcome.busy;
|
|
242060
|
+
const reason = outcome.kind === "mode-switch-failed" ? "\u65E0\u6CD5\u5C06 reviewer \u6062\u590D\u4E3A\u53EA\u8BFB plan \u6A21\u5F0F\uFF0C\u590D\u5BA1\u672A\u53D1\u51FA\u3002\u53EF\u91CD\u8BD5\uFF0C\u6216\u7ED3\u675F\u5FAA\u73AF\u3002" : busy ? "reviewer \u6B63\u5728\u56DE\u590D\u4E2D\uFF0C\u590D\u5BA1\u672A\u53D1\u51FA\u3002\u8BF7\u7B49\u5F85\u5176\u5B8C\u6210\u540E\u91CD\u8BD5\u3002" : "\u590D\u5BA1\u4EFB\u52A1\u672A\u9001\u8FBE reviewer\uFF08\u5176 session \u53EF\u80FD\u672A\u8FD0\u884C\uFF09\u3002\u53EF\u91CD\u8BD5\uFF0C\u6216\u7ED3\u675F\u5FAA\u73AF\u3002";
|
|
242061
|
+
const back = await this.storage.workflowRuns.transition(gate.id, "waiting_reviewer", "waiting_rereview", {
|
|
242062
|
+
reviewer_session_id: null,
|
|
242063
|
+
error: reason
|
|
242064
|
+
});
|
|
242065
|
+
if (back) releaseReservation();
|
|
242066
|
+
const rolled = await this.storage.workflowRuns.getById(gate.id);
|
|
242067
|
+
if (rolled) this.emitRunUpdated(rolled);
|
|
242068
|
+
throw new WorkflowError(busy ? "session-busy" : outcome.kind === "mode-switch-failed" ? "reviewer-unavailable" : "send-failed", reason);
|
|
242069
|
+
}
|
|
242070
|
+
const started = outcome.kind === "unknown" ? await this.storage.workflowRuns.update(gate.id, { error: REREVIEW_DELIVERY_UNKNOWN }) ?? run2 : run2;
|
|
242071
|
+
this.emitRunUpdated(started);
|
|
242072
|
+
return started;
|
|
242073
|
+
}
|
|
242074
|
+
/**
|
|
242075
|
+
* Gate action `accept`: take the reviewer's result as final and finish the
|
|
242076
|
+
* run WITHOUT sending anything to the source. The natural exit on a `ship`
|
|
242077
|
+
* verdict (the panel makes it the primary action there), but allowed on any
|
|
242078
|
+
* verdict — it is the user's call. A loop ends here: no feedback step, so no
|
|
242079
|
+
* next-round gate.
|
|
242080
|
+
*/
|
|
242081
|
+
async acceptResult(runId) {
|
|
242082
|
+
const accepted = await this.storage.workflowRuns.transition(runId, "waiting_feedback", "completed", { error: null });
|
|
242083
|
+
if (!accepted) throw new WorkflowError("bad-state", "run \u4E0D\u5728\u7B49\u5F85\u53CD\u9988\u786E\u8BA4\u7684\u72B6\u6001");
|
|
242084
|
+
const done = await this.storage.workflowRuns.getById(runId);
|
|
242085
|
+
this.untrackRun(done);
|
|
242086
|
+
this.emitRunUpdated(done);
|
|
242087
|
+
return done;
|
|
242088
|
+
}
|
|
242089
|
+
// ---------- re-review (reuse path), shared by the start entry and the loop gate ----------
|
|
242090
|
+
/**
|
|
242091
|
+
* The checks that make an existing session usable as this workspace's
|
|
242092
|
+
* reviewer right now. Throws `reviewer-unavailable`; returns the row.
|
|
242093
|
+
*/
|
|
242094
|
+
async assertReviewerReusable(reviewerSessionId, projectId, branch) {
|
|
242095
|
+
const reviewerSession = await this.storage.agentSessions.getById(reviewerSessionId);
|
|
242096
|
+
if (!reviewerSession) {
|
|
242097
|
+
throw new WorkflowError("reviewer-unavailable", "\u4E0A\u6B21 reviewer session \u5DF2\u4E0D\u5B58\u5728");
|
|
242098
|
+
}
|
|
242099
|
+
const reviewerProjection = await this.storage.agentSessions.getActivityById(reviewerSessionId, "workflow-reviewer");
|
|
242100
|
+
if (!reviewerProjection || reviewerProjection.projectId !== projectId) {
|
|
242101
|
+
throw new WorkflowError("reviewer-unavailable", "reviewer session \u4E0D\u5C5E\u4E8E\u5F53\u524D\u9879\u76EE");
|
|
242102
|
+
}
|
|
242103
|
+
if (reviewerProjection.branch !== branch) {
|
|
242104
|
+
throw new WorkflowError("reviewer-unavailable", "reviewer session \u4E0D\u5C5E\u4E8E\u5F53\u524D branch");
|
|
242105
|
+
}
|
|
242106
|
+
if (reviewerProjection.binding === "checkout" && (reviewerProjection.checkoutDeletedAt !== null || reviewerProjection.checkoutStatus !== "ready")) {
|
|
242107
|
+
throw new WorkflowError("reviewer-unavailable", "reviewer session \u7684 workspace checkout \u4E0D\u53EF\u7528");
|
|
242108
|
+
}
|
|
242109
|
+
if (!REVIEWER_AGENT_TYPES.has(reviewerSession.agent_type)) {
|
|
242110
|
+
throw new WorkflowError("reviewer-unavailable", "reviewer agent \u7C7B\u578B\u4E0D\u53EF\u7528");
|
|
242111
|
+
}
|
|
242112
|
+
if (reviewerSession.status !== "stopped") {
|
|
242113
|
+
throw new WorkflowError("reviewer-unavailable", "reviewer session \u6B63\u5728\u8FD0\u884C\u6216\u4E0D\u53EF\u7528");
|
|
242114
|
+
}
|
|
242115
|
+
return reviewerSession;
|
|
242116
|
+
}
|
|
242117
|
+
/**
|
|
242118
|
+
* Put the reviewer back in read-only plan mode if needed and dispatch the
|
|
242119
|
+
* re-review prompt. Reports what happened and decides nothing: the one-shot
|
|
242120
|
+
* start fails its run on a prompt that did not leave, the loop gate returns
|
|
242121
|
+
* to the gate.
|
|
242122
|
+
*/
|
|
242123
|
+
async dispatchRereview(opts) {
|
|
242124
|
+
if (opts.reviewerSession.permission_mode !== "plan") {
|
|
242125
|
+
let switched = false;
|
|
242126
|
+
try {
|
|
242127
|
+
switched = await this.agentOps.switchMode(opts.reviewerSession.id, opts.project.path, "plan");
|
|
242128
|
+
} catch {
|
|
242129
|
+
}
|
|
242130
|
+
if (!switched) return { kind: "mode-switch-failed" };
|
|
242131
|
+
}
|
|
242132
|
+
const prompt = buildRereviewerPrompt({
|
|
242133
|
+
taskContext: extractTaskContextBefore(opts.entries, opts.turnEndIndex),
|
|
242134
|
+
// Scoped to the fix turn: an older turn's summary would describe the
|
|
242135
|
+
// pre-review state and mislead the acceptance pass.
|
|
242136
|
+
authorSelfReport: extractAuthorSelfReport(opts.entries, opts.turnEndIndex, { withinTurn: true }),
|
|
242137
|
+
reviewFocus: opts.run.review_focus,
|
|
242138
|
+
target: opts.target
|
|
242139
|
+
});
|
|
242140
|
+
return this.dispatchStep({
|
|
242141
|
+
run: opts.run,
|
|
242142
|
+
kind: "rereview_prompt",
|
|
242143
|
+
role: "reviewer",
|
|
242144
|
+
sessionId: opts.reviewerSession.id,
|
|
242145
|
+
payload: prompt,
|
|
242146
|
+
projectPath: opts.project.path,
|
|
242147
|
+
turn: REVIEWER_TURN
|
|
242148
|
+
});
|
|
242149
|
+
}
|
|
242150
|
+
// ---------- dispatch identity (Phase 2 prerequisite design §2) ----------
|
|
242151
|
+
/**
|
|
242152
|
+
* The user-entry index a step's dispatch was written at. The send path's
|
|
242153
|
+
* evidence hook stores it strictly before stdin, so `null` means stdin was
|
|
242154
|
+
* never written. A fresh reviewer's first instruction goes through the
|
|
242155
|
+
* lifecycle service, which records the same evidence on the session row; the
|
|
242156
|
+
* engine copies it over after activation returns, and this read covers the
|
|
242157
|
+
* gap (a completion or a crash can land first).
|
|
242158
|
+
*/
|
|
242159
|
+
async effectiveEntryIndex(step) {
|
|
242160
|
+
if (step.user_entry_index !== null) return step.user_entry_index;
|
|
242161
|
+
if (step.kind !== "reviewer_prompt") return null;
|
|
242162
|
+
const row = await this.storage.agentSessions.getLifecycleById(step.session_id);
|
|
242163
|
+
return row?.activation_user_entry_index ?? null;
|
|
242164
|
+
}
|
|
242165
|
+
/**
|
|
242166
|
+
* A retry must be the SAME instruction. If this run already has an open
|
|
242167
|
+
* step of this kind (its outcome is unknown, or it is still in flight) and
|
|
242168
|
+
* the payload differs, refuse: re-using the key with new content is a ledger
|
|
242169
|
+
* conflict, and silently sending the old content would ignore the edit.
|
|
242170
|
+
* Call BEFORE any run CAS so a refusal leaves the run untouched.
|
|
242171
|
+
*/
|
|
242172
|
+
async assertRetryIsSameInstruction(runId, kind, payload) {
|
|
242173
|
+
const open5 = (await this.storage.workflowRunSteps.listByRun(runId)).find((st) => st.kind === kind && st.status === "dispatched");
|
|
242174
|
+
if (open5 && open5.payload_hash !== instructionContentHash(payload)) {
|
|
242175
|
+
throw new WorkflowError("bad-state", "\u4E0A\u4E00\u6B21\u6295\u9012\u7ED3\u679C\u672A\u77E5\uFF1A\u8BF7\u5148\u6309\u539F\u6587\u91CD\u8BD5\uFF0C\u6216\u7ED3\u675F\u672C\u6B21 review \u540E\u91CD\u65B0\u53D1\u8D77\u3002");
|
|
242176
|
+
}
|
|
242177
|
+
}
|
|
242178
|
+
/**
|
|
242179
|
+
* One keyed dispatch to an ALREADY-ACTIVE session (everything except a
|
|
242180
|
+
* fresh reviewer's activation). Only idle sessions are dispatched to, and
|
|
242181
|
+
* the idle check shares the per-session mutex with the user's `/message`
|
|
242182
|
+
* route: a dispatch that lands mid-turn would be queued or injected by the
|
|
242183
|
+
* CLI and could no longer be told apart from the turn it rode in on. An idle
|
|
242184
|
+
* dispatch always opens its own turn, which is what lets attribution be the
|
|
242185
|
+
* single rule "the turn's opening entry is the step's entry".
|
|
242186
|
+
*/
|
|
242187
|
+
async dispatchStep(opts) {
|
|
242188
|
+
const steps = this.storage.workflowRunSteps;
|
|
242189
|
+
const stepId = randomUUID7();
|
|
242190
|
+
const { step, reused } = await steps.open({
|
|
242191
|
+
id: stepId,
|
|
242192
|
+
run_id: opts.run.id,
|
|
242193
|
+
role: opts.role,
|
|
242194
|
+
kind: opts.kind,
|
|
242195
|
+
session_id: opts.sessionId,
|
|
242196
|
+
idempotency_key: `run:${opts.run.id}:step:${stepId}`,
|
|
242197
|
+
payload_hash: instructionContentHash(opts.payload)
|
|
242198
|
+
});
|
|
242199
|
+
return serializeSessionMutation(opts.sessionId, async () => {
|
|
242200
|
+
const target = await this.storage.agentSessions.getById(opts.sessionId);
|
|
242201
|
+
if (target?.status === "running") {
|
|
242202
|
+
if (!reused) await steps.abandon(step.id, "target session busy");
|
|
242203
|
+
return { kind: "no_side_effect", busy: true, reason: "target session is mid-turn" };
|
|
242204
|
+
}
|
|
242205
|
+
let runtimeAccepted = false;
|
|
242206
|
+
try {
|
|
242207
|
+
const result = await deliverInstruction({
|
|
242208
|
+
storage: this.storage,
|
|
242209
|
+
sessionId: opts.sessionId,
|
|
242210
|
+
idempotencyKey: step.idempotency_key,
|
|
242211
|
+
rawContent: opts.payload,
|
|
242212
|
+
deliver: async () => {
|
|
242213
|
+
runtimeAccepted = await this.agentOps.sendUserMessage(
|
|
242214
|
+
opts.sessionId,
|
|
242215
|
+
opts.payload,
|
|
242216
|
+
opts.projectPath,
|
|
242217
|
+
void 0,
|
|
242218
|
+
{
|
|
242219
|
+
...opts.turn,
|
|
242220
|
+
onUserEntryPersisted: async (entryIndex) => {
|
|
242221
|
+
if (!await steps.setUserEntryIndex(step.id, entryIndex)) throw new DispatchSupersededError();
|
|
242222
|
+
}
|
|
242223
|
+
}
|
|
242224
|
+
);
|
|
242225
|
+
return runtimeAccepted;
|
|
242226
|
+
}
|
|
242227
|
+
});
|
|
242228
|
+
switch (result) {
|
|
242229
|
+
case "delivered":
|
|
242230
|
+
case "replayed":
|
|
242231
|
+
await steps.setError(step.id, null);
|
|
242232
|
+
return { kind: "accepted", step };
|
|
242233
|
+
case "unconfirmed":
|
|
242234
|
+
case "ownership_lost_after_send":
|
|
242235
|
+
console.warn(`[WorkflowEngine] step ${step.id}: delivered but ledger result was ${result}`);
|
|
242236
|
+
return { kind: "accepted", step };
|
|
242237
|
+
case "not_running":
|
|
242238
|
+
case "conflict":
|
|
242239
|
+
case "ownership_lost_before_send":
|
|
242240
|
+
await steps.abandon(step.id, `not delivered: ${result}`);
|
|
242241
|
+
return { kind: "no_side_effect", busy: false, reason: result };
|
|
242242
|
+
case "busy":
|
|
242243
|
+
await steps.setError(step.id, DELIVERY_UNKNOWN_PREFIX);
|
|
242244
|
+
return { kind: "unknown", step, reason: result };
|
|
242245
|
+
}
|
|
242246
|
+
} catch (err) {
|
|
242247
|
+
if (runtimeAccepted) {
|
|
242248
|
+
console.warn(`[WorkflowEngine] step ${step.id}: delivered, then ledger threw:`, err);
|
|
242249
|
+
return { kind: "accepted", step };
|
|
242250
|
+
}
|
|
242251
|
+
await steps.abandon(step.id, `not delivered: ${err instanceof Error ? err.message : String(err)}`);
|
|
242252
|
+
return { kind: "no_side_effect", busy: false, reason: "send threw before stdin" };
|
|
242253
|
+
}
|
|
242254
|
+
});
|
|
242255
|
+
}
|
|
241354
242256
|
async failRun(run2, error48) {
|
|
241355
242257
|
if (TERMINAL_STATUSES.has(run2.status)) return;
|
|
241356
242258
|
const from = run2.status;
|
|
@@ -241373,6 +242275,7 @@ var WorkflowEngine = class {
|
|
|
241373
242275
|
}
|
|
241374
242276
|
);
|
|
241375
242277
|
if (!ok) return;
|
|
242278
|
+
await this.storage.workflowRunSteps.abandonOpenByRun(run2.id, "run failed");
|
|
241376
242279
|
const failed = await this.storage.workflowRuns.getById(run2.id);
|
|
241377
242280
|
if (failed) {
|
|
241378
242281
|
this.untrackRun(failed);
|
|
@@ -241480,7 +242383,7 @@ var WorkflowEngine = class {
|
|
|
241480
242383
|
if (opts.blind && opts.reviewerSessionId) {
|
|
241481
242384
|
throw new WorkflowError("reviewer-unavailable", "blind review \u4E0D\u80FD\u590D\u7528\u5DF2\u6709 reviewer session");
|
|
241482
242385
|
}
|
|
241483
|
-
const runId = opts.runId ??
|
|
242386
|
+
const runId = opts.runId ?? randomUUID7();
|
|
241484
242387
|
const existingRun = opts.runId ? await this.storage.workflowRuns.getById(runId) : void 0;
|
|
241485
242388
|
if (existingRun) {
|
|
241486
242389
|
const sameRequest = existingRun.project_id === opts.project.id && existingRun.branch === opts.branch && existingRun.source_session_id === opts.sourceSessionId && existingRun.review_focus === (opts.reviewFocus ?? null) && existingRun.review_span === (opts.reviewSpan ?? "this_turn");
|
|
@@ -241536,29 +242439,7 @@ var WorkflowEngine = class {
|
|
|
241536
242439
|
}
|
|
241537
242440
|
const worktreePath = sourceProjection.worktreePath ?? resolveWorktreePath(opts.project.path, opts.branch);
|
|
241538
242441
|
const target = captureReviewTarget(worktreePath);
|
|
241539
|
-
|
|
241540
|
-
if (opts.reviewerSessionId) {
|
|
241541
|
-
reviewerSession = await this.storage.agentSessions.getById(opts.reviewerSessionId);
|
|
241542
|
-
if (!reviewerSession) {
|
|
241543
|
-
throw new WorkflowError("reviewer-unavailable", "\u4E0A\u6B21 reviewer session \u5DF2\u4E0D\u5B58\u5728");
|
|
241544
|
-
}
|
|
241545
|
-
const reviewerProjection = await this.storage.agentSessions.getActivityById(opts.reviewerSessionId, "workflow-reviewer");
|
|
241546
|
-
if (!reviewerProjection || reviewerProjection.projectId !== opts.project.id) {
|
|
241547
|
-
throw new WorkflowError("reviewer-unavailable", "reviewer session \u4E0D\u5C5E\u4E8E\u5F53\u524D\u9879\u76EE");
|
|
241548
|
-
}
|
|
241549
|
-
if (reviewerProjection.branch !== opts.branch) {
|
|
241550
|
-
throw new WorkflowError("reviewer-unavailable", "reviewer session \u4E0D\u5C5E\u4E8E\u5F53\u524D branch");
|
|
241551
|
-
}
|
|
241552
|
-
if (reviewerProjection.binding === "checkout" && (reviewerProjection.checkoutDeletedAt !== null || reviewerProjection.checkoutStatus !== "ready")) {
|
|
241553
|
-
throw new WorkflowError("reviewer-unavailable", "reviewer session \u7684 workspace checkout \u4E0D\u53EF\u7528");
|
|
241554
|
-
}
|
|
241555
|
-
if (!REVIEWER_AGENT_TYPES.has(reviewerSession.agent_type)) {
|
|
241556
|
-
throw new WorkflowError("reviewer-unavailable", "reviewer agent \u7C7B\u578B\u4E0D\u53EF\u7528");
|
|
241557
|
-
}
|
|
241558
|
-
if (reviewerSession.status !== "stopped") {
|
|
241559
|
-
throw new WorkflowError("reviewer-unavailable", "reviewer session \u6B63\u5728\u8FD0\u884C\u6216\u4E0D\u53EF\u7528");
|
|
241560
|
-
}
|
|
241561
|
-
}
|
|
242442
|
+
const reviewerSession = opts.reviewerSessionId ? await this.assertReviewerReusable(opts.reviewerSessionId, opts.project.id, opts.branch) : null;
|
|
241562
242443
|
if (existingRun && existingRun.source_turn_end_index !== turnEndIndex) {
|
|
241563
242444
|
throw new WorkflowError("reviewer-unavailable", "workflow run cutoff does not match the replay request");
|
|
241564
242445
|
}
|
|
@@ -241574,7 +242455,8 @@ var WorkflowEngine = class {
|
|
|
241574
242455
|
review_span: opts.reviewSpan ?? "this_turn",
|
|
241575
242456
|
// Reuse skips the preparing state entirely: its prompt is delivered
|
|
241576
242457
|
// below, before this method returns.
|
|
241577
|
-
status: opts.reviewerSessionId ? "waiting_reviewer" : "preparing"
|
|
242458
|
+
status: opts.reviewerSessionId ? "waiting_reviewer" : "preparing",
|
|
242459
|
+
...opts.loop ? { loop_id: runId, round: 1, max_rounds: opts.loop.maxRounds } : {}
|
|
241578
242460
|
});
|
|
241579
242461
|
this.trackParticipants(run2);
|
|
241580
242462
|
if (existingRun && !existingRun.reviewer_session_id && !opts.reviewerSessionId && run2.status === "waiting_reviewer") {
|
|
@@ -241583,32 +242465,26 @@ var WorkflowEngine = class {
|
|
|
241583
242465
|
run2.status = "preparing";
|
|
241584
242466
|
}
|
|
241585
242467
|
if (opts.reviewerSessionId && reviewerSession) {
|
|
241586
|
-
|
|
241587
|
-
|
|
241588
|
-
|
|
241589
|
-
|
|
241590
|
-
|
|
241591
|
-
|
|
241592
|
-
if (!switched) {
|
|
241593
|
-
await this.failRun(run2, "\u65E0\u6CD5\u5C06 reviewer \u6062\u590D\u4E3A\u53EA\u8BFB plan \u6A21\u5F0F");
|
|
241594
|
-
throw new WorkflowError("reviewer-unavailable", "\u65E0\u6CD5\u5C06 reviewer \u6062\u590D\u4E3A\u53EA\u8BFB plan \u6A21\u5F0F");
|
|
241595
|
-
}
|
|
241596
|
-
}
|
|
241597
|
-
const prompt = buildRereviewerPrompt({
|
|
241598
|
-
taskContext: extractTaskContextBefore(entries, turnEndIndex),
|
|
241599
|
-
// Scoped to the fix turn: an older turn's summary would describe the
|
|
241600
|
-
// pre-review state and mislead the acceptance pass.
|
|
241601
|
-
authorSelfReport: extractAuthorSelfReport(entries, turnEndIndex, { withinTurn: true }),
|
|
241602
|
-
reviewFocus: opts.reviewFocus ?? null,
|
|
242468
|
+
const outcome = await this.dispatchRereview({
|
|
242469
|
+
run: run2,
|
|
242470
|
+
reviewerSession,
|
|
242471
|
+
project: opts.project,
|
|
242472
|
+
entries,
|
|
242473
|
+
turnEndIndex,
|
|
241603
242474
|
target
|
|
241604
242475
|
});
|
|
241605
|
-
|
|
241606
|
-
|
|
241607
|
-
|
|
241608
|
-
throw new WorkflowError("send-failed", "\u5411\u4E0A\u6B21 reviewer \u6295\u9012\u590D\u5BA1\u4EFB\u52A1\u5931\u8D25");
|
|
242476
|
+
if (outcome.kind === "mode-switch-failed") {
|
|
242477
|
+
await this.failRun(run2, "\u65E0\u6CD5\u5C06 reviewer \u6062\u590D\u4E3A\u53EA\u8BFB plan \u6A21\u5F0F");
|
|
242478
|
+
throw new WorkflowError("reviewer-unavailable", "\u65E0\u6CD5\u5C06 reviewer \u6062\u590D\u4E3A\u53EA\u8BFB plan \u6A21\u5F0F");
|
|
241609
242479
|
}
|
|
241610
|
-
|
|
241611
|
-
|
|
242480
|
+
if (outcome.kind === "no_side_effect") {
|
|
242481
|
+
const message = outcome.busy ? "\u4E0A\u6B21 reviewer \u6B63\u5728\u8FD0\u884C\uFF0C\u65E0\u6CD5\u6295\u9012\u590D\u5BA1\u4EFB\u52A1" : "\u5411\u4E0A\u6B21 reviewer \u6295\u9012\u590D\u5BA1\u4EFB\u52A1\u5931\u8D25";
|
|
242482
|
+
await this.failRun(run2, message);
|
|
242483
|
+
throw new WorkflowError(outcome.busy ? "session-busy" : "send-failed", message);
|
|
242484
|
+
}
|
|
242485
|
+
const started = outcome.kind === "unknown" ? await this.storage.workflowRuns.update(run2.id, { error: REREVIEW_DELIVERY_UNKNOWN }) ?? run2 : run2;
|
|
242486
|
+
this.emitRunUpdated(started);
|
|
242487
|
+
return started;
|
|
241612
242488
|
}
|
|
241613
242489
|
let scope = null;
|
|
241614
242490
|
let endSnap = null;
|
|
@@ -241712,6 +242588,7 @@ var WorkflowEngine = class {
|
|
|
241712
242588
|
}
|
|
241713
242589
|
const pending = this.pendingActivations.get(runId) ?? parsePreparedContext(run2.prepared_context);
|
|
241714
242590
|
let outcome;
|
|
242591
|
+
let step;
|
|
241715
242592
|
try {
|
|
241716
242593
|
const entries = pending ? null : await this.agentOps.getRawMessages(run2.source_session_id);
|
|
241717
242594
|
const prompt = buildReviewerPrompt({
|
|
@@ -241724,6 +242601,15 @@ var WorkflowEngine = class {
|
|
|
241724
242601
|
target: JSON.parse(run2.review_target),
|
|
241725
242602
|
scope: pending?.scope ?? null
|
|
241726
242603
|
});
|
|
242604
|
+
step = (await this.storage.workflowRunSteps.open({
|
|
242605
|
+
id: randomUUID7(),
|
|
242606
|
+
run_id: run2.id,
|
|
242607
|
+
role: "reviewer",
|
|
242608
|
+
kind: "reviewer_prompt",
|
|
242609
|
+
session_id: run2.reviewer_session_id,
|
|
242610
|
+
idempotency_key: reviewerActivationKey(run2.id),
|
|
242611
|
+
payload_hash: instructionContentHash(prompt)
|
|
242612
|
+
})).step;
|
|
241727
242613
|
outcome = await this.agentOps.activateReviewer({
|
|
241728
242614
|
sessionId: run2.reviewer_session_id,
|
|
241729
242615
|
activationKey: reviewerActivationKey(run2.id),
|
|
@@ -241734,6 +242620,9 @@ var WorkflowEngine = class {
|
|
|
241734
242620
|
await this.failRun(run2, `\u6FC0\u6D3B reviewer \u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}`);
|
|
241735
242621
|
throw new WorkflowError("spawn-failed", "\u6FC0\u6D3B reviewer session \u5931\u8D25");
|
|
241736
242622
|
}
|
|
242623
|
+
if ((outcome.kind === "activated" || outcome.kind === "replayed" || outcome.kind === "uncertain") && outcome.view.userEntryIndex !== null) {
|
|
242624
|
+
await this.storage.workflowRunSteps.setUserEntryIndex(step.id, outcome.view.userEntryIndex);
|
|
242625
|
+
}
|
|
241737
242626
|
switch (outcome.kind) {
|
|
241738
242627
|
case "activated":
|
|
241739
242628
|
case "replayed":
|
|
@@ -241765,25 +242654,150 @@ var WorkflowEngine = class {
|
|
|
241765
242654
|
this.emitRunUpdated(updated);
|
|
241766
242655
|
return updated;
|
|
241767
242656
|
}
|
|
242657
|
+
/**
|
|
242658
|
+
* Attribute a turn completion. One rule (design §2.5): the index of the user
|
|
242659
|
+
* entry that OPENED the completed turn equals the entry index recorded on a
|
|
242660
|
+
* `dispatched` step of this session. Nothing is inferred from "this session
|
|
242661
|
+
* is a reviewer and its run is waiting" — a session is re-used across rounds
|
|
242662
|
+
* and the user talks to it in between.
|
|
242663
|
+
*
|
|
242664
|
+
* The engine is the only claimant. Runs created before steps existed have no
|
|
242665
|
+
* step rows at all and keep the old whole-session rule for one release.
|
|
242666
|
+
*/
|
|
241768
242667
|
async handleTaskCompleted(event) {
|
|
241769
|
-
const
|
|
241770
|
-
if (
|
|
241771
|
-
|
|
241772
|
-
|
|
242668
|
+
const open5 = await this.storage.workflowRunSteps.getOpenBySession(event.sessionId);
|
|
242669
|
+
if (open5.length === 0) {
|
|
242670
|
+
await this.handleLegacyReviewerCompletion(event);
|
|
242671
|
+
return;
|
|
242672
|
+
}
|
|
241773
242673
|
const entries = await this.agentOps.getRawMessages(event.sessionId);
|
|
241774
242674
|
const boundary = event.turnEndEntryIndex ?? extractLatestTurnEndIndex(entries) ?? entries.length;
|
|
241775
|
-
const
|
|
241776
|
-
let
|
|
242675
|
+
const openingIndex = findTurnOpeningUserEntryIndex(entries, boundary);
|
|
242676
|
+
let step;
|
|
242677
|
+
if (openingIndex !== null) {
|
|
242678
|
+
for (const candidate of open5) {
|
|
242679
|
+
if (await this.effectiveEntryIndex(candidate) === openingIndex) {
|
|
242680
|
+
step = candidate;
|
|
242681
|
+
break;
|
|
242682
|
+
}
|
|
242683
|
+
}
|
|
242684
|
+
}
|
|
242685
|
+
if (!step) {
|
|
242686
|
+
for (const candidate of open5) {
|
|
242687
|
+
if (candidate.role !== "reviewer") continue;
|
|
242688
|
+
const waiting = await this.storage.workflowRuns.getById(candidate.run_id);
|
|
242689
|
+
if (waiting?.status !== "waiting_reviewer") continue;
|
|
242690
|
+
const noted = await this.storage.workflowRuns.update(waiting.id, {
|
|
242691
|
+
error: "\u6536\u5230 reviewer \u7684\u5B8C\u6210\u4E8B\u4EF6\uFF0C\u4F46\u65E0\u6CD5\u786E\u8BA4\u5B83\u5BF9\u5E94\u672C\u6B21\u6D3E\u53D1\u3002\u8BF7\u6253\u5F00\u5176\u7A97\u53E3\u67E5\u770B\uFF0C\u6216\u7ED3\u675F\u672C\u6B21 review\u3002"
|
|
242692
|
+
});
|
|
242693
|
+
if (noted) this.emitRunUpdated(noted);
|
|
242694
|
+
}
|
|
242695
|
+
return;
|
|
242696
|
+
}
|
|
242697
|
+
await this.claimStep(step, entries, boundary);
|
|
242698
|
+
}
|
|
242699
|
+
/**
|
|
242700
|
+
* Attribute the turn ending at `boundary` to `step`. Shared by the live
|
|
242701
|
+
* completion event and restart reconciliation, so a late claim goes through
|
|
242702
|
+
* exactly the transaction a live one does.
|
|
242703
|
+
*/
|
|
242704
|
+
async claimStep(step, entries, boundary) {
|
|
242705
|
+
const output = extractLastAssistantInTurn(entries, boundary);
|
|
242706
|
+
if (step.kind === "feedback") {
|
|
242707
|
+
const run3 = await this.storage.workflowRuns.getById(step.run_id);
|
|
242708
|
+
const nextRun = run3 ? this.nextRoundGate(run3, boundary) : void 0;
|
|
242709
|
+
if (run3 && (run3.status === "sending_feedback" || run3.status === "waiting_feedback")) {
|
|
242710
|
+
const completed = await this.storage.workflowRuns.claimStepAndTransition({
|
|
242711
|
+
stepId: step.id,
|
|
242712
|
+
turnEndIndex: boundary,
|
|
242713
|
+
outputSnapshot: output,
|
|
242714
|
+
run: { id: run3.id, from: run3.status, to: "completed", patch: { error: null } },
|
|
242715
|
+
nextRun
|
|
242716
|
+
});
|
|
242717
|
+
if (completed) {
|
|
242718
|
+
const done = await this.storage.workflowRuns.getById(run3.id);
|
|
242719
|
+
if (done) {
|
|
242720
|
+
this.untrackRun(done);
|
|
242721
|
+
this.emitRunUpdated(done);
|
|
242722
|
+
}
|
|
242723
|
+
await this.announceNextRoundGate(nextRun?.id);
|
|
242724
|
+
return;
|
|
242725
|
+
}
|
|
242726
|
+
}
|
|
242727
|
+
const claimed = await this.storage.workflowRuns.claimStepAndTransition({
|
|
242728
|
+
stepId: step.id,
|
|
242729
|
+
turnEndIndex: boundary,
|
|
242730
|
+
outputSnapshot: output,
|
|
242731
|
+
nextRun
|
|
242732
|
+
});
|
|
242733
|
+
if (claimed) await this.announceNextRoundGate(nextRun?.id);
|
|
242734
|
+
return;
|
|
242735
|
+
}
|
|
242736
|
+
const run2 = await this.storage.workflowRuns.getById(step.run_id);
|
|
242737
|
+
if (!run2 || run2.status !== "waiting_reviewer") return;
|
|
242738
|
+
const feedback = output ?? "(reviewer \u6CA1\u6709\u8F93\u51FA\u53EF\u7528\u7684\u53CD\u9988\u6587\u672C)";
|
|
242739
|
+
const driftNote = await this.computeDriftNote(run2);
|
|
242740
|
+
const ok = await this.storage.workflowRuns.claimStepAndTransition({
|
|
242741
|
+
stepId: step.id,
|
|
242742
|
+
turnEndIndex: boundary,
|
|
242743
|
+
outputSnapshot: feedback,
|
|
242744
|
+
run: {
|
|
242745
|
+
id: run2.id,
|
|
242746
|
+
from: "waiting_reviewer",
|
|
242747
|
+
to: "waiting_feedback",
|
|
242748
|
+
// `error: null` also clears a "delivery outcome unknown" note: the
|
|
242749
|
+
// completion just answered it.
|
|
242750
|
+
// Parsed by exact match; null = unrecognised = the human decides.
|
|
242751
|
+
patch: { feedback_snapshot: feedback, error: driftNote, verdict: parseVerdict(output) },
|
|
242752
|
+
outbox: this.reviewReadyOutbox(run2, step.session_id, boundary)
|
|
242753
|
+
}
|
|
242754
|
+
});
|
|
242755
|
+
if (!ok) return;
|
|
242756
|
+
this.onMilestoneCreated?.();
|
|
242757
|
+
const updated = await this.storage.workflowRuns.getById(run2.id);
|
|
242758
|
+
if (updated) this.emitRunUpdated(updated);
|
|
242759
|
+
}
|
|
242760
|
+
async computeDriftNote(run2) {
|
|
241777
242761
|
try {
|
|
241778
242762
|
const target = run2.review_target ? JSON.parse(run2.review_target) : null;
|
|
241779
242763
|
const project = await this.storage.projects.getById(run2.project_id);
|
|
241780
242764
|
const sourceProjection = await this.storage.agentSessions.getActivityById(run2.source_session_id, "workflow-reviewer");
|
|
241781
242765
|
const worktreePath = sourceProjection?.worktreePath ?? (project ? resolveWorktreePath(project.path ?? "", run2.branch) : null);
|
|
241782
242766
|
if (target && worktreePath && hasDrifted(worktreePath, target)) {
|
|
241783
|
-
|
|
242767
|
+
return "\u6CE8\u610F\uFF1Aworkspace \u5728 review \u671F\u95F4\u53D1\u751F\u4E86\u53D8\u5316\uFF0C\u90E8\u5206\u53CD\u9988\u53EF\u80FD\u9488\u5BF9\u7684\u4E0D\u662F\u88AB\u5BA1\u5DE5\u4F5C\u3002";
|
|
241784
242768
|
}
|
|
241785
242769
|
} catch {
|
|
241786
242770
|
}
|
|
242771
|
+
return null;
|
|
242772
|
+
}
|
|
242773
|
+
reviewReadyOutbox(run2, reviewerSessionId, boundary) {
|
|
242774
|
+
return {
|
|
242775
|
+
id: reviewReadyId(run2.id, boundary),
|
|
242776
|
+
kind: "review_ready",
|
|
242777
|
+
project_id: run2.project_id,
|
|
242778
|
+
branch: run2.branch,
|
|
242779
|
+
// Target the reviewer: that's where the feedback and the
|
|
242780
|
+
// approve/discard controls are.
|
|
242781
|
+
session_id: reviewerSessionId,
|
|
242782
|
+
workflow_run_id: run2.id,
|
|
242783
|
+
created_at: Date.now()
|
|
242784
|
+
};
|
|
242785
|
+
}
|
|
242786
|
+
/**
|
|
242787
|
+
* Pre-step runs only (created before `workflow_run_steps` existed): the old
|
|
242788
|
+
* whole-session rule. A run that HAS step rows never comes through here — a
|
|
242789
|
+
* completion it cannot attribute is not accepted. Remove after one release.
|
|
242790
|
+
*/
|
|
242791
|
+
async handleLegacyReviewerCompletion(event) {
|
|
242792
|
+
const p2 = this.participants.get(event.sessionId);
|
|
242793
|
+
if (!p2 || p2.role !== "reviewer") return;
|
|
242794
|
+
const run2 = await this.storage.workflowRuns.getById(p2.runId);
|
|
242795
|
+
if (!run2 || run2.status !== "waiting_reviewer") return;
|
|
242796
|
+
if (await this.storage.workflowRunSteps.hasAny(run2.id)) return;
|
|
242797
|
+
const entries = await this.agentOps.getRawMessages(event.sessionId);
|
|
242798
|
+
const boundary = event.turnEndEntryIndex ?? extractLatestTurnEndIndex(entries) ?? entries.length;
|
|
242799
|
+
const feedback = extractLastAssistantInTurn(entries, boundary) ?? "(reviewer \u6CA1\u6709\u8F93\u51FA\u53EF\u7528\u7684\u53CD\u9988\u6587\u672C)";
|
|
242800
|
+
const driftNote = await this.computeDriftNote(run2);
|
|
241787
242801
|
const ok = await this.storage.workflowRuns.transitionWithOutbox(
|
|
241788
242802
|
run2.id,
|
|
241789
242803
|
"waiting_reviewer",
|
|
@@ -241792,17 +242806,7 @@ var WorkflowEngine = class {
|
|
|
241792
242806
|
feedback_snapshot: feedback,
|
|
241793
242807
|
...driftNote ? { error: driftNote } : {}
|
|
241794
242808
|
},
|
|
241795
|
-
|
|
241796
|
-
id: reviewReadyId(run2.id, boundary),
|
|
241797
|
-
kind: "review_ready",
|
|
241798
|
-
project_id: run2.project_id,
|
|
241799
|
-
branch: run2.branch,
|
|
241800
|
-
// Target the reviewer: that's where the feedback and the
|
|
241801
|
-
// approve/discard controls are.
|
|
241802
|
-
session_id: event.sessionId,
|
|
241803
|
-
workflow_run_id: run2.id,
|
|
241804
|
-
created_at: Date.now()
|
|
241805
|
-
}
|
|
242809
|
+
this.reviewReadyOutbox(run2, event.sessionId, boundary)
|
|
241806
242810
|
);
|
|
241807
242811
|
if (!ok) return;
|
|
241808
242812
|
this.onMilestoneCreated?.();
|
|
@@ -241814,20 +242818,38 @@ var WorkflowEngine = class {
|
|
|
241814
242818
|
if (!run2 || run2.status !== "waiting_feedback") {
|
|
241815
242819
|
throw new WorkflowError("bad-state", "run \u4E0D\u5728\u7B49\u5F85\u53CD\u9988\u786E\u8BA4\u7684\u72B6\u6001");
|
|
241816
242820
|
}
|
|
242821
|
+
const feedback = editedPayload ?? run2.feedback_snapshot ?? "";
|
|
242822
|
+
const payload = buildFeedbackMessage(feedback);
|
|
242823
|
+
await this.assertRetryIsSameInstruction(runId, "feedback", payload);
|
|
242824
|
+
const sourceSession = await this.storage.agentSessions.getById(run2.source_session_id);
|
|
242825
|
+
if (sourceSession?.status === "running") {
|
|
242826
|
+
throw new WorkflowError("session-busy", "source session \u6B63\u5728\u8FD0\u884C\uFF0C\u8BF7\u7B49\u5F85\u5176\u5B8C\u6210\u540E\u518D\u53D1\u9001\u53CD\u9988");
|
|
242827
|
+
}
|
|
241817
242828
|
const claimed = await this.storage.workflowRuns.transition(runId, "waiting_feedback", "sending_feedback", {
|
|
241818
242829
|
...editedPayload !== void 0 ? { feedback_snapshot: editedPayload } : {},
|
|
241819
242830
|
error: null
|
|
241820
242831
|
// clear stale warnings (error column is nullable)
|
|
241821
242832
|
});
|
|
241822
242833
|
if (!claimed) throw new WorkflowError("bad-state", "run \u72B6\u6001\u5DF2\u53D8\u5316\uFF08\u53EF\u80FD\u5DF2\u88AB\u5904\u7406\uFF09");
|
|
241823
|
-
const feedback = editedPayload ?? run2.feedback_snapshot ?? "";
|
|
241824
242834
|
const project = await this.storage.projects.getById(run2.project_id);
|
|
241825
|
-
const
|
|
241826
|
-
|
|
241827
|
-
|
|
241828
|
-
|
|
241829
|
-
|
|
241830
|
-
|
|
242835
|
+
const outcome = await this.dispatchStep({
|
|
242836
|
+
run: run2,
|
|
242837
|
+
kind: "feedback",
|
|
242838
|
+
role: "source",
|
|
242839
|
+
sessionId: run2.source_session_id,
|
|
242840
|
+
payload,
|
|
242841
|
+
projectPath: project?.path ?? void 0,
|
|
242842
|
+
turn: FEEDBACK_TURN
|
|
242843
|
+
});
|
|
242844
|
+
if (outcome.kind !== "accepted") {
|
|
242845
|
+
const error48 = outcome.kind === "unknown" ? `${DELIVERY_UNKNOWN_PREFIX}\uFF1A\u53CD\u9988\u53EF\u80FD\u5DF2\u9001\u8FBE source session\u3002\u8BF7\u68C0\u67E5\u5176\u7A97\u53E3\uFF1B\u518D\u6B21\u786E\u8BA4\u4F1A\u6309\u539F\u6587\u91CD\u8BD5\uFF08\u5DF2\u9001\u8FBE\u5219\u4E0D\u4F1A\u91CD\u590D\u53D1\u9001\uFF09\u3002` : outcome.busy ? "source session \u6B63\u5728\u8FD0\u884C\uFF0C\u8BF7\u7B49\u5F85\u5176\u5B8C\u6210\u540E\u518D\u53D1\u9001\u53CD\u9988\u3002" : "\u53D1\u9001\u5931\u8D25\uFF1A\u76EE\u6807 session \u53EF\u80FD\u672A\u8FD0\u884C\u3002\u8BF7\u5728\u5176\u7A97\u53E3\u4E2D\u5524\u9192\u540E\u91CD\u8BD5\uFF0C\u6216\u7ED3\u675F\u672C\u6B21 review\u3002";
|
|
242846
|
+
await this.storage.workflowRuns.transition(runId, "sending_feedback", "waiting_feedback", { error: error48 });
|
|
242847
|
+
const rolled = await this.storage.workflowRuns.getById(runId);
|
|
242848
|
+
if (rolled) this.emitRunUpdated(rolled);
|
|
242849
|
+
if (outcome.kind === "no_side_effect" && outcome.busy) {
|
|
242850
|
+
throw new WorkflowError("session-busy", "source session \u6B63\u5728\u8FD0\u884C\uFF0C\u8BF7\u7B49\u5F85\u5176\u5B8C\u6210\u540E\u518D\u53D1\u9001\u53CD\u9988");
|
|
242851
|
+
}
|
|
242852
|
+
throw new WorkflowError("send-failed", outcome.kind === "unknown" ? "\u53CD\u9988\u6295\u9012\u7ED3\u679C\u672A\u77E5" : "\u53D1\u9001\u53CD\u9988\u5931\u8D25");
|
|
241831
242853
|
}
|
|
241832
242854
|
const completedOk = await this.storage.workflowRuns.transition(runId, "sending_feedback", "completed");
|
|
241833
242855
|
if (!completedOk) {
|
|
@@ -241849,7 +242871,7 @@ var WorkflowEngine = class {
|
|
|
241849
242871
|
*/
|
|
241850
242872
|
async requestFinalVerdict(runId) {
|
|
241851
242873
|
const run2 = await this.storage.workflowRuns.getById(runId);
|
|
241852
|
-
if (!run2 || run2.status !== "discussing"
|
|
242874
|
+
if (!run2 || !run2.reviewer_session_id || run2.status !== "discussing") {
|
|
241853
242875
|
throw new WorkflowError("bad-state", "run \u4E0D\u5728\u8BA8\u8BBA\u72B6\u6001");
|
|
241854
242876
|
}
|
|
241855
242877
|
const reviewerSession = await this.storage.agentSessions.getById(run2.reviewer_session_id);
|
|
@@ -241859,10 +242881,25 @@ var WorkflowEngine = class {
|
|
|
241859
242881
|
const claimed = await this.storage.workflowRuns.transition(runId, "discussing", "waiting_reviewer", { error: null });
|
|
241860
242882
|
if (!claimed) throw new WorkflowError("bad-state", "run \u72B6\u6001\u5DF2\u53D8\u5316\uFF08\u53EF\u80FD\u5DF2\u88AB\u5904\u7406\uFF09");
|
|
241861
242883
|
const project = await this.storage.projects.getById(run2.project_id);
|
|
241862
|
-
const
|
|
241863
|
-
|
|
242884
|
+
const outcome = await this.dispatchStep({
|
|
242885
|
+
run: run2,
|
|
242886
|
+
kind: "final_verdict",
|
|
242887
|
+
role: "reviewer",
|
|
242888
|
+
sessionId: run2.reviewer_session_id,
|
|
242889
|
+
payload: FINAL_VERDICT_PROMPT,
|
|
242890
|
+
projectPath: project?.path ?? void 0,
|
|
242891
|
+
turn: REVIEWER_TURN
|
|
242892
|
+
});
|
|
242893
|
+
if (outcome.kind === "unknown") {
|
|
242894
|
+
const noted = await this.storage.workflowRuns.update(runId, {
|
|
242895
|
+
error: `${DELIVERY_UNKNOWN_PREFIX}\uFF1A\u7EC8\u7A3F\u8BF7\u6C42\u53EF\u80FD\u5DF2\u9001\u8FBE reviewer\u3002\u82E5\u5176\u5B8C\u6210\uFF0C\u7ED3\u679C\u4F1A\u81EA\u52A8\u5F52\u5C5E\uFF1B\u5426\u5219\u53EF\u5728 reviewer \u7A97\u53E3\u7EE7\u7EED\u5BF9\u8BDD\u540E\u91CD\u65B0\u751F\u6210\u7EC8\u7A3F\uFF0C\u6216\u7ED3\u675F\u672C\u6B21 review\u3002`
|
|
242896
|
+
});
|
|
242897
|
+
if (noted) this.emitRunUpdated(noted);
|
|
242898
|
+
throw new WorkflowError("send-failed", "\u7EC8\u7A3F\u8BF7\u6C42\u6295\u9012\u7ED3\u679C\u672A\u77E5");
|
|
242899
|
+
}
|
|
242900
|
+
if (outcome.kind === "no_side_effect") {
|
|
241864
242901
|
const rolledBack = await this.storage.workflowRuns.transition(runId, "waiting_reviewer", "discussing", {
|
|
241865
|
-
error: "\u53D1\u9001\u5931\u8D25\uFF1Areviewer session \u53EF\u80FD\u672A\u8FD0\u884C\u3002\u8BF7\u5728\u5176\u7A97\u53E3\u4E2D\u5524\u9192\u540E\u91CD\u8BD5\uFF0C\u6216\u7ED3\u675F\u672C\u6B21 review\u3002"
|
|
242902
|
+
error: outcome.busy ? "reviewer \u6B63\u5728\u56DE\u590D\u4E2D\uFF0C\u8BF7\u7B49\u5F85\u5176\u5B8C\u6210\u540E\u518D\u751F\u6210\u7EC8\u7A3F\u3002" : "\u53D1\u9001\u5931\u8D25\uFF1Areviewer session \u53EF\u80FD\u672A\u8FD0\u884C\u3002\u8BF7\u5728\u5176\u7A97\u53E3\u4E2D\u5524\u9192\u540E\u91CD\u8BD5\uFF0C\u6216\u7ED3\u675F\u672C\u6B21 review\u3002"
|
|
241866
242903
|
});
|
|
241867
242904
|
if (!rolledBack) {
|
|
241868
242905
|
console.warn(
|
|
@@ -241871,6 +242908,7 @@ var WorkflowEngine = class {
|
|
|
241871
242908
|
}
|
|
241872
242909
|
const rolled = await this.storage.workflowRuns.getById(runId);
|
|
241873
242910
|
if (rolled) this.emitRunUpdated(rolled);
|
|
242911
|
+
if (outcome.busy) throw new WorkflowError("session-busy", "reviewer \u6B63\u5728\u56DE\u590D\u4E2D\uFF0C\u8BF7\u7B49\u5F85\u5176\u5B8C\u6210\u540E\u518D\u751F\u6210\u7EC8\u7A3F");
|
|
241874
242912
|
throw new WorkflowError("send-failed", "\u5411 reviewer \u53D1\u9001\u7EC8\u7A3F\u8BF7\u6C42\u5931\u8D25");
|
|
241875
242913
|
}
|
|
241876
242914
|
const updated = await this.storage.workflowRuns.getById(runId);
|
|
@@ -241882,7 +242920,8 @@ var WorkflowEngine = class {
|
|
|
241882
242920
|
if (!run2) return void 0;
|
|
241883
242921
|
if (TERMINAL_STATUSES.has(run2.status)) return run2;
|
|
241884
242922
|
const patch = reason ? { error: reason } : void 0;
|
|
241885
|
-
const cancelled = await this.storage.workflowRuns.transition(runId, "preparing", "cancelled", patch) || await this.storage.workflowRuns.transition(runId, "waiting_reviewer", "cancelled", patch) || await this.storage.workflowRuns.transition(runId, "waiting_feedback", "cancelled", patch) || await this.storage.workflowRuns.transition(runId, "discussing", "cancelled", patch)
|
|
242923
|
+
const cancelled = await this.storage.workflowRuns.transition(runId, "preparing", "cancelled", patch) || await this.storage.workflowRuns.transition(runId, "waiting_reviewer", "cancelled", patch) || await this.storage.workflowRuns.transition(runId, "waiting_feedback", "cancelled", patch) || await this.storage.workflowRuns.transition(runId, "discussing", "cancelled", patch) || // Declining the next round: the loop ends here.
|
|
242924
|
+
await this.storage.workflowRuns.transition(runId, "waiting_rereview", "cancelled", patch);
|
|
241886
242925
|
if (!cancelled) {
|
|
241887
242926
|
const current = await this.storage.workflowRuns.getById(runId);
|
|
241888
242927
|
if (current?.status === "sending_feedback") {
|
|
@@ -241890,6 +242929,7 @@ var WorkflowEngine = class {
|
|
|
241890
242929
|
}
|
|
241891
242930
|
return current;
|
|
241892
242931
|
}
|
|
242932
|
+
await this.storage.workflowRunSteps.abandonOpenByRun(runId, "run cancelled");
|
|
241893
242933
|
const updated = await this.storage.workflowRuns.getById(runId);
|
|
241894
242934
|
if (updated) {
|
|
241895
242935
|
this.untrackRun(updated);
|
|
@@ -241917,7 +242957,10 @@ var WorkflowEngine = class {
|
|
|
241917
242957
|
if (!p2) return;
|
|
241918
242958
|
if (p2.role === "reviewer") {
|
|
241919
242959
|
try {
|
|
241920
|
-
|
|
242960
|
+
const fromGate = await this.storage.workflowRuns.transition(p2.runId, "waiting_feedback", "discussing", { error: null });
|
|
242961
|
+
if (!fromGate && await this.storage.workflowRuns.transition(p2.runId, "waiting_reviewer", "discussing", { error: null })) {
|
|
242962
|
+
await this.storage.workflowRunSteps.abandonOpenByRun(p2.runId, "user started a discussion", "reviewer");
|
|
242963
|
+
}
|
|
241921
242964
|
const updated = await this.storage.workflowRuns.getById(p2.runId);
|
|
241922
242965
|
if (updated) this.emitRunUpdated(updated);
|
|
241923
242966
|
} catch (err) {
|
|
@@ -242322,7 +243365,7 @@ var RemotePatchCache = class {
|
|
|
242322
243365
|
};
|
|
242323
243366
|
|
|
242324
243367
|
// src/reverse-connect-manager.ts
|
|
242325
|
-
import { randomUUID as
|
|
243368
|
+
import { randomUUID as randomUUID8 } from "crypto";
|
|
242326
243369
|
var PING_INTERVAL_MS = 3e4;
|
|
242327
243370
|
var PONG_TIMEOUT_MS = 1e4;
|
|
242328
243371
|
var DEFAULT_HTTP_TIMEOUT_MS = 3e4;
|
|
@@ -242416,7 +243459,7 @@ var ReverseConnectManager = class {
|
|
|
242416
243459
|
errorCode: "network_error"
|
|
242417
243460
|
};
|
|
242418
243461
|
}
|
|
242419
|
-
const requestId =
|
|
243462
|
+
const requestId = randomUUID8();
|
|
242420
243463
|
const frame = {
|
|
242421
243464
|
type: "http_request",
|
|
242422
243465
|
requestId,
|
|
@@ -242451,7 +243494,7 @@ var ReverseConnectManager = class {
|
|
|
242451
243494
|
if (!conn || conn.ws.readyState !== 1) {
|
|
242452
243495
|
return { ok: false, status: 0, headers: {}, body: "" };
|
|
242453
243496
|
}
|
|
242454
|
-
const requestId =
|
|
243497
|
+
const requestId = randomUUID8();
|
|
242455
243498
|
const frame = {
|
|
242456
243499
|
type: "http_request",
|
|
242457
243500
|
requestId,
|
|
@@ -242787,7 +243830,7 @@ var BrowserManager = class {
|
|
|
242787
243830
|
};
|
|
242788
243831
|
|
|
242789
243832
|
// src/remote-executor-monitor.ts
|
|
242790
|
-
import { randomUUID as
|
|
243833
|
+
import { randomUUID as randomUUID9 } from "crypto";
|
|
242791
243834
|
var RemoteExecutorMonitor = class {
|
|
242792
243835
|
constructor(reverseConnectManager, eventBus, storage2, remoteExecutorMap) {
|
|
242793
243836
|
this.reverseConnectManager = reverseConnectManager;
|
|
@@ -242826,7 +243869,7 @@ var RemoteExecutorMonitor = class {
|
|
|
242826
243869
|
console.log(`[RemoteExecutorMonitor] remote ${remoteInfo.remoteServerId} not connected for ${localProcessId}, deferring`);
|
|
242827
243870
|
return;
|
|
242828
243871
|
}
|
|
242829
|
-
const channelId =
|
|
243872
|
+
const channelId = randomUUID9();
|
|
242830
243873
|
const wsPath = `/api/executor-processes/${remoteInfo.remoteProcessId}/logs`;
|
|
242831
243874
|
const adapter = new VirtualWsAdapter(
|
|
242832
243875
|
(data) => rcm.sendChannelData(remoteInfo.remoteServerId, channelId, data),
|
|
@@ -242910,7 +243953,7 @@ var RemoteExecutorMonitor = class {
|
|
|
242910
243953
|
};
|
|
242911
243954
|
|
|
242912
243955
|
// src/scheduler.ts
|
|
242913
|
-
import { createHash as
|
|
243956
|
+
import { createHash as createHash5, randomUUID as randomUUID10 } from "crypto";
|
|
242914
243957
|
import { existsSync as existsSync5 } from "fs";
|
|
242915
243958
|
import path8 from "path";
|
|
242916
243959
|
var OUTPUT_CAP = 2e5;
|
|
@@ -242918,7 +243961,7 @@ var RUNS_KEEP = 50;
|
|
|
242918
243961
|
var CLAIM_LEASE_MS = 3e4;
|
|
242919
243962
|
var CLAIM_HEARTBEAT_MS = 1e4;
|
|
242920
243963
|
function effectFingerprint(value) {
|
|
242921
|
-
return
|
|
243964
|
+
return createHash5("sha256").update(JSON.stringify(value)).digest("hex");
|
|
242922
243965
|
}
|
|
242923
243966
|
var REPORT_INSTRUCTION = "\n\n---\nWhen the task is complete, end your final message with a concise Markdown report: what you did, files changed, key results, and any problems encountered.";
|
|
242924
243967
|
function buildRunContent(task) {
|
|
@@ -242949,7 +243992,7 @@ var SchedulerService = class {
|
|
|
242949
243992
|
storage;
|
|
242950
243993
|
processManager;
|
|
242951
243994
|
remote;
|
|
242952
|
-
ownerToken =
|
|
243995
|
+
ownerToken = randomUUID10();
|
|
242953
243996
|
jobs = /* @__PURE__ */ new Map();
|
|
242954
243997
|
/** scheduleId -> runId of the currently active run (overlap guard). */
|
|
242955
243998
|
activeRuns = /* @__PURE__ */ new Map();
|
|
@@ -243046,7 +244089,7 @@ var SchedulerService = class {
|
|
|
243046
244089
|
async executeRun(scheduleId, preallocatedRunId) {
|
|
243047
244090
|
const task = await this.storage.scheduledTasks.getById(scheduleId);
|
|
243048
244091
|
if (!task) return { error: "Schedule not found" };
|
|
243049
|
-
const runId = preallocatedRunId ??
|
|
244092
|
+
const runId = preallocatedRunId ?? randomUUID10();
|
|
243050
244093
|
const activeRunId = this.activeRuns.get(scheduleId);
|
|
243051
244094
|
if (activeRunId) {
|
|
243052
244095
|
if (activeRunId === runId) return { runId, skipped: false };
|
|
@@ -243976,7 +245019,7 @@ var SessionRetentionSweeper = class {
|
|
|
243976
245019
|
};
|
|
243977
245020
|
|
|
243978
245021
|
// src/agent-session-lifecycle.ts
|
|
243979
|
-
import { createHash as
|
|
245022
|
+
import { createHash as createHash6, randomUUID as randomUUID11 } from "node:crypto";
|
|
243980
245023
|
function lifecycleHttpStatus(kind) {
|
|
243981
245024
|
switch (kind) {
|
|
243982
245025
|
case "activated":
|
|
@@ -244073,7 +245116,7 @@ var ActivationLeaseLostError = class extends Error {
|
|
|
244073
245116
|
};
|
|
244074
245117
|
function hashInstruction(instruction) {
|
|
244075
245118
|
const canonical = typeof instruction === "string" ? JSON.stringify({ text: instruction }) : JSON.stringify(instruction);
|
|
244076
|
-
return
|
|
245119
|
+
return createHash6("sha256").update(canonical).digest("hex");
|
|
244077
245120
|
}
|
|
244078
245121
|
var branchOf = (row) => row.branch === "" ? null : row.branch;
|
|
244079
245122
|
function viewOf(row, now3) {
|
|
@@ -244130,7 +245173,8 @@ var AgentSessionLifecycleService = class {
|
|
|
244130
245173
|
model: input.model,
|
|
244131
245174
|
purpose: input.purpose,
|
|
244132
245175
|
owner: input.owner,
|
|
244133
|
-
startSnapshot: input.startSnapshot
|
|
245176
|
+
startSnapshot: input.startSnapshot,
|
|
245177
|
+
grants: input.grants
|
|
244134
245178
|
});
|
|
244135
245179
|
if (prepared.kind === "idempotency_conflict" || prepared.kind === "workspace_unavailable") return prepared;
|
|
244136
245180
|
if (prepared.kind === "expired") return { kind: "expired", view: prepared.view };
|
|
@@ -244167,8 +245211,11 @@ var AgentSessionLifecycleService = class {
|
|
|
244167
245211
|
}
|
|
244168
245212
|
const projectPath = await this.resolveProjectPath(input.projectId);
|
|
244169
245213
|
if (!projectPath) return { kind: "workspace_unavailable", detail: "project not found" };
|
|
244170
|
-
const sessionId = input.sessionId ??
|
|
245214
|
+
const sessionId = input.sessionId ?? randomUUID11();
|
|
244171
245215
|
const model = input.model?.trim() ? input.model.trim() : null;
|
|
245216
|
+
if (input.grants) {
|
|
245217
|
+
await this.storage.sessionRemoteGrants.replace(sessionId, input.grants.userId, input.grants.remoteServerIds);
|
|
245218
|
+
}
|
|
244172
245219
|
try {
|
|
244173
245220
|
await this.runtime.prepareSessionRow({
|
|
244174
245221
|
sessionId,
|
|
@@ -244186,7 +245233,15 @@ var AgentSessionLifecycleService = class {
|
|
|
244186
245233
|
});
|
|
244187
245234
|
} catch (error48) {
|
|
244188
245235
|
const raced = await this.storage.agentSessions.getLifecycleByPrepareOperationId(input.operationId);
|
|
244189
|
-
if (raced)
|
|
245236
|
+
if (raced) {
|
|
245237
|
+
if (input.grants && raced.id !== sessionId) {
|
|
245238
|
+
await this.storage.sessionRemoteGrants.deleteBySession(sessionId).catch(() => {
|
|
245239
|
+
});
|
|
245240
|
+
}
|
|
245241
|
+
return this.replayPrepare(raced, input, now3);
|
|
245242
|
+
}
|
|
245243
|
+
if (input.grants) await this.storage.sessionRemoteGrants.deleteBySession(sessionId).catch(() => {
|
|
245244
|
+
});
|
|
244190
245245
|
if (error48 instanceof WorkspaceCheckoutUnavailableError || /workspace checkout/i.test(String(error48?.message))) {
|
|
244191
245246
|
return { kind: "workspace_unavailable", detail: error48.message };
|
|
244192
245247
|
}
|
|
@@ -244218,7 +245273,7 @@ var AgentSessionLifecycleService = class {
|
|
|
244218
245273
|
// -------------------------------------------------------------------------
|
|
244219
245274
|
async activate(input) {
|
|
244220
245275
|
const contentHash = hashInstruction(input.instruction);
|
|
244221
|
-
const leaseOwner =
|
|
245276
|
+
const leaseOwner = randomUUID11();
|
|
244222
245277
|
for (let pass = 0; pass < 3; pass++) {
|
|
244223
245278
|
const now3 = this.now();
|
|
244224
245279
|
const row2 = await this.storage.agentSessions.getLifecycleById(input.sessionId);
|
|
@@ -244330,11 +245385,18 @@ var AgentSessionLifecycleService = class {
|
|
|
244330
245385
|
const view = viewOf(beforeSend, this.now());
|
|
244331
245386
|
return beforeSend.lifecycle_state === "expired" ? { kind: "expired", view } : { kind: "activation_conflict", view };
|
|
244332
245387
|
}
|
|
245388
|
+
if (input.grants) {
|
|
245389
|
+
await this.storage.sessionRemoteGrants.replace(sessionId, input.grants.userId, input.grants.remoteServerIds);
|
|
245390
|
+
}
|
|
245391
|
+
const instruction = await appendRemoteGrantContext(this.storage, sessionId, input.userId, input.instruction).catch((err) => {
|
|
245392
|
+
console.error(`[SessionLifecycle] grant context build failed for ${sessionId}:`, err);
|
|
245393
|
+
return input.instruction;
|
|
245394
|
+
});
|
|
244333
245395
|
let userEntryIndex = null;
|
|
244334
245396
|
let accepted = false;
|
|
244335
245397
|
let sendError;
|
|
244336
245398
|
try {
|
|
244337
|
-
accepted = await this.runtime.sendUserMessage(sessionId,
|
|
245399
|
+
accepted = await this.runtime.sendUserMessage(sessionId, instruction, projectPath, input.userId ?? "local", {
|
|
244338
245400
|
origin: input.origin,
|
|
244339
245401
|
notificationDisposition: input.notificationDisposition,
|
|
244340
245402
|
onUserEntryPersisted: async (entryIndex) => {
|
|
@@ -244521,7 +245583,7 @@ var AgentSessionLifecycleService = class {
|
|
|
244521
245583
|
};
|
|
244522
245584
|
|
|
244523
245585
|
// src/remote-session-lifecycle.ts
|
|
244524
|
-
import { randomUUID as
|
|
245586
|
+
import { randomUUID as randomUUID12 } from "node:crypto";
|
|
244525
245587
|
var LIFECYCLE_PREPARE_CAPABILITY = "http:POST /api/path/agent-sessions/prepare";
|
|
244526
245588
|
function parseWorkerBody(data) {
|
|
244527
245589
|
if (!data || typeof data !== "object") return null;
|
|
@@ -244579,6 +245641,12 @@ var RemoteSessionLifecycleAdapter = class {
|
|
|
244579
245641
|
this.deps.remoteSessionMap.set(localSessionId, { remoteServerId: params.remoteServerId, remoteSessionId, branch: params.branch });
|
|
244580
245642
|
}
|
|
244581
245643
|
const crossRemoteMcp = await this.mintFor(localSessionId, params.userId, params.remoteServerId);
|
|
245644
|
+
const frozen = await this.withFrozenGrantContext(localSessionId, params.userId, params.instruction);
|
|
245645
|
+
if (!frozen.ok) {
|
|
245646
|
+
if (preRegistered) this.deps.remoteSessionMap.delete(localSessionId);
|
|
245647
|
+
return { kind: "remote_unreachable", status: 0, detail: frozen.error };
|
|
245648
|
+
}
|
|
245649
|
+
const instruction = frozen.instruction;
|
|
244582
245650
|
const activityAt = this.now();
|
|
244583
245651
|
const result = await proxyToRemoteAuto(
|
|
244584
245652
|
params.remoteServerId,
|
|
@@ -244594,7 +245662,7 @@ var RemoteSessionLifecycleAdapter = class {
|
|
|
244594
245662
|
operationId: params.operationId,
|
|
244595
245663
|
purpose: params.purpose,
|
|
244596
245664
|
owner: params.owner ?? null,
|
|
244597
|
-
instruction
|
|
245665
|
+
instruction,
|
|
244598
245666
|
force: params.force === true,
|
|
244599
245667
|
origin: params.origin,
|
|
244600
245668
|
notificationDisposition: params.notificationDisposition,
|
|
@@ -244640,8 +245708,11 @@ var RemoteSessionLifecycleAdapter = class {
|
|
|
244640
245708
|
if (!same) return { kind: "idempotency_conflict", view: null, detail: "same prepare operation with different configuration" };
|
|
244641
245709
|
return { kind: "ids", localSessionId: existing.local_session_id, remoteSessionId: existing.remote_session_id, existing };
|
|
244642
245710
|
}
|
|
244643
|
-
const remoteSessionId = params.remoteSessionId ??
|
|
245711
|
+
const remoteSessionId = params.remoteSessionId ?? randomUUID12();
|
|
244644
245712
|
const localSessionId = params.localSessionId ?? `remote-${params.remoteServerId}-${params.projectId}-${remoteSessionId}`;
|
|
245713
|
+
if (params.grantedRemoteIds) {
|
|
245714
|
+
await this.deps.storage.sessionRemoteGrants.replace(localSessionId, params.userId ?? "", params.grantedRemoteIds);
|
|
245715
|
+
}
|
|
244645
245716
|
await intents.begin({
|
|
244646
245717
|
localSessionId,
|
|
244647
245718
|
remoteSessionId,
|
|
@@ -244658,6 +245729,30 @@ var RemoteSessionLifecycleAdapter = class {
|
|
|
244658
245729
|
});
|
|
244659
245730
|
return { kind: "ids", localSessionId, remoteSessionId, existing: void 0 };
|
|
244660
245731
|
}
|
|
245732
|
+
/**
|
|
245733
|
+
* The grant block for a remote session's FIRST instruction, frozen on the
|
|
245734
|
+
* intent row (§6).
|
|
245735
|
+
*
|
|
245736
|
+
* The worker hashes the whole instruction to decide whether a retry of the
|
|
245737
|
+
* same activation key is a replay or a conflict, and a failed activation
|
|
245738
|
+
* (`resident_limit`, `spawn_failed`) keeps that hash. Regenerating the block
|
|
245739
|
+
* from current grants would therefore turn "activation failed, user grants
|
|
245740
|
+
* the machine, retries" — the very flow the composer invites — into
|
|
245741
|
+
* `idempotency_conflict`. So the first text wins for every later attempt;
|
|
245742
|
+
* the gateway still judges by the live grant table, and the next ordinary
|
|
245743
|
+
* message regenerates the block from scratch.
|
|
245744
|
+
*/
|
|
245745
|
+
async withFrozenGrantContext(localSessionId, userId, instruction) {
|
|
245746
|
+
let frozen;
|
|
245747
|
+
try {
|
|
245748
|
+
const fresh = await buildRemoteGrantContext(this.deps.storage, localSessionId, userId) ?? "";
|
|
245749
|
+
frozen = await this.deps.storage.remoteSessionCreationIntents.setFirstTurnGrantContext(localSessionId, fresh) ?? fresh;
|
|
245750
|
+
} catch (err) {
|
|
245751
|
+
console.error(`[RemoteLifecycle] grant context freeze failed for ${localSessionId}:`, err);
|
|
245752
|
+
return { ok: false, error: `grant context could not be recorded: ${err?.message ?? err}` };
|
|
245753
|
+
}
|
|
245754
|
+
return { ok: true, instruction: appendContextBlock(instruction, frozen.length > 0 ? frozen : null) };
|
|
245755
|
+
}
|
|
244661
245756
|
mintFor(localSessionId, userId, remoteServerId) {
|
|
244662
245757
|
return mintCrossRemoteMcpConfig(
|
|
244663
245758
|
{ storage: this.deps.storage },
|
|
@@ -244711,6 +245806,14 @@ var RemoteSessionLifecycleAdapter = class {
|
|
|
244711
245806
|
const target = await this.resolve(params.localSessionId);
|
|
244712
245807
|
if (!target) return { kind: "not_found" };
|
|
244713
245808
|
const { remoteServerId, remoteSessionId, projectId, branch, remotePath, mapping, intent } = target;
|
|
245809
|
+
if (params.grantedRemoteIds) {
|
|
245810
|
+
await this.deps.storage.sessionRemoteGrants.replace(params.localSessionId, params.userId ?? "", params.grantedRemoteIds);
|
|
245811
|
+
}
|
|
245812
|
+
const frozen = await this.withFrozenGrantContext(params.localSessionId, params.userId, params.instruction);
|
|
245813
|
+
if (!frozen.ok) {
|
|
245814
|
+
return { kind: "remote_unreachable", status: 0, detail: frozen.error };
|
|
245815
|
+
}
|
|
245816
|
+
params = { ...params, instruction: frozen.instruction };
|
|
244714
245817
|
if (!await this.supports(remoteServerId)) {
|
|
244715
245818
|
return this.legacyActivate(params, target);
|
|
244716
245819
|
}
|
|
@@ -246380,12 +247483,23 @@ var sharedServices = async (fastify2, opts) => {
|
|
|
246380
247483
|
sendAgentInstruction: async (input) => {
|
|
246381
247484
|
if (input.target === "local") {
|
|
246382
247485
|
const project = await opts.storage.projects.getById(input.projectId, input.userId);
|
|
246383
|
-
|
|
246384
|
-
|
|
246385
|
-
|
|
246386
|
-
|
|
246387
|
-
|
|
246388
|
-
|
|
247486
|
+
if (!project?.path) return false;
|
|
247487
|
+
const projectPath = project.path;
|
|
247488
|
+
return serializeSessionMutation(input.sessionId, async () => {
|
|
247489
|
+
const result2 = await deliverInstruction({
|
|
247490
|
+
storage: opts.storage,
|
|
247491
|
+
sessionId: input.sessionId,
|
|
247492
|
+
idempotencyKey: input.idempotencyKey,
|
|
247493
|
+
rawContent: input.instruction,
|
|
247494
|
+
deliver: () => agentSessionManager.sendUserMessage(
|
|
247495
|
+
input.sessionId,
|
|
247496
|
+
input.instruction,
|
|
247497
|
+
projectPath,
|
|
247498
|
+
input.userId
|
|
247499
|
+
)
|
|
247500
|
+
});
|
|
247501
|
+
return result2 === "delivered" || result2 === "replayed";
|
|
247502
|
+
});
|
|
246389
247503
|
}
|
|
246390
247504
|
const activityAt = Date.now();
|
|
246391
247505
|
const result = await proxyToRemoteAuto(
|
|
@@ -246654,7 +247768,7 @@ var shared_services_default = (0, import_fastify_plugin2.default)(sharedServices
|
|
|
246654
247768
|
var import_fastify_plugin3 = __toESM(require_plugin2(), 1);
|
|
246655
247769
|
import path10 from "path";
|
|
246656
247770
|
import { exec as exec3 } from "child_process";
|
|
246657
|
-
import { randomUUID as
|
|
247771
|
+
import { randomUUID as randomUUID13 } from "crypto";
|
|
246658
247772
|
import { readdir, mkdir as mkdir3 } from "fs/promises";
|
|
246659
247773
|
|
|
246660
247774
|
// src/dialog.ts
|
|
@@ -246783,7 +247897,7 @@ var routes2 = async (fastify2) => {
|
|
|
246783
247897
|
return reply.code(409).send({ error: "Project with this path already exists" });
|
|
246784
247898
|
}
|
|
246785
247899
|
}
|
|
246786
|
-
const id =
|
|
247900
|
+
const id = randomUUID13();
|
|
246787
247901
|
const project = await fastify2.storage.projects.create({
|
|
246788
247902
|
id,
|
|
246789
247903
|
name: name25,
|
|
@@ -247491,7 +248605,7 @@ var project_remote_routes_default = (0, import_fastify_plugin6.default)(routes5,
|
|
|
247491
248605
|
|
|
247492
248606
|
// src/routes/executor-routes.ts
|
|
247493
248607
|
var import_fastify_plugin7 = __toESM(require_plugin2(), 1);
|
|
247494
|
-
import { randomUUID as
|
|
248608
|
+
import { randomUUID as randomUUID14 } from "crypto";
|
|
247495
248609
|
function normalizeSqlTimestamp(value) {
|
|
247496
248610
|
if (!value) return null;
|
|
247497
248611
|
if (value.includes("T") && /(Z|[+-]\d{2}:?\d{2})$/.test(value)) return value;
|
|
@@ -247571,7 +248685,7 @@ var routes6 = async (fastify2) => {
|
|
|
247571
248685
|
}
|
|
247572
248686
|
const parsedType2 = executor_type === "prompt" ? "prompt" : "command";
|
|
247573
248687
|
const parsedProvider = prompt_provider === "codex" ? "codex" : "claude";
|
|
247574
|
-
const id =
|
|
248688
|
+
const id = randomUUID14();
|
|
247575
248689
|
const executor = await fastify2.storage.executors.create({
|
|
247576
248690
|
id,
|
|
247577
248691
|
project_id: req.params.projectId,
|
|
@@ -247664,8 +248778,145 @@ var executor_routes_default = (0, import_fastify_plugin7.default)(routes6, { nam
|
|
|
247664
248778
|
// src/routes/process-routes.ts
|
|
247665
248779
|
var import_fastify_plugin8 = __toESM(require_plugin2(), 1);
|
|
247666
248780
|
import path11 from "path";
|
|
247667
|
-
import { randomUUID as
|
|
248781
|
+
import { randomUUID as randomUUID16 } from "crypto";
|
|
248782
|
+
|
|
248783
|
+
// src/remote-executor-starts.ts
|
|
248784
|
+
import { createHash as createHash7, randomUUID as randomUUID15 } from "crypto";
|
|
248785
|
+
var IDEMPOTENT_EXECUTE_MIN_WORKER_VERSION = "0.3.1";
|
|
248786
|
+
var isTransportFailure = (result) => result.status === 0 || result.errorCode === "timeout" || result.errorCode === "network_error";
|
|
248787
|
+
async function listWorkerRunningProcessIds(fastify2, remoteServerId) {
|
|
248788
|
+
const result = await proxyToRemoteAuto(
|
|
248789
|
+
remoteServerId,
|
|
248790
|
+
"GET",
|
|
248791
|
+
"/api/executor-processes/running",
|
|
248792
|
+
void 0,
|
|
248793
|
+
{ reverseConnectManager: fastify2.reverseConnectManager }
|
|
248794
|
+
);
|
|
248795
|
+
if (!result.ok) return null;
|
|
248796
|
+
const processes = result.data?.processes;
|
|
248797
|
+
if (!Array.isArray(processes)) return null;
|
|
248798
|
+
const ids = /* @__PURE__ */ new Set();
|
|
248799
|
+
for (const proc of processes) {
|
|
248800
|
+
const id = proc?.id;
|
|
248801
|
+
if (typeof id === "string") ids.add(id);
|
|
248802
|
+
}
|
|
248803
|
+
return ids;
|
|
248804
|
+
}
|
|
248805
|
+
function createRemoteExecutorStarts(fastify2) {
|
|
248806
|
+
const pending = /* @__PURE__ */ new Map();
|
|
248807
|
+
const supportsIdempotentExecute = async (remoteServerId) => {
|
|
248808
|
+
const server = await fastify2.storage.remoteServers.getById(remoteServerId);
|
|
248809
|
+
const version2 = server?.worker_version;
|
|
248810
|
+
if (!version2) return false;
|
|
248811
|
+
return (compareVersionStrings(version2, IDEMPOTENT_EXECUTE_MIN_WORKER_VERSION) ?? -1) >= 0;
|
|
248812
|
+
};
|
|
248813
|
+
const register = async (start2, remoteProcessId) => {
|
|
248814
|
+
const localProcessId = `remote-${start2.executorId}-${remoteProcessId}`;
|
|
248815
|
+
if (fastify2.remoteExecutorMap.has(localProcessId)) return localProcessId;
|
|
248816
|
+
const remoteInfo = {
|
|
248817
|
+
remoteServerId: start2.remoteServerId,
|
|
248818
|
+
remoteProcessId,
|
|
248819
|
+
executorId: start2.executorId,
|
|
248820
|
+
projectId: start2.projectId
|
|
248821
|
+
};
|
|
248822
|
+
fastify2.remoteExecutorMap.set(localProcessId, remoteInfo);
|
|
248823
|
+
fastify2.remoteExecutorMonitor.watch(localProcessId, remoteInfo);
|
|
248824
|
+
await fastify2.storage.remoteExecutorProcesses.insert(localProcessId, {
|
|
248825
|
+
remoteServerId: start2.remoteServerId,
|
|
248826
|
+
remoteProcessId,
|
|
248827
|
+
executorId: start2.executorId,
|
|
248828
|
+
projectId: start2.projectId,
|
|
248829
|
+
branch: start2.branch ?? void 0,
|
|
248830
|
+
machineId: fastify2.reverseConnectManager.getMachineId(start2.remoteServerId)
|
|
248831
|
+
});
|
|
248832
|
+
fastify2.eventBus.emit({
|
|
248833
|
+
type: "executor:started",
|
|
248834
|
+
projectId: start2.projectId,
|
|
248835
|
+
executorId: start2.executorId,
|
|
248836
|
+
processId: localProcessId,
|
|
248837
|
+
target: start2.remoteServerId
|
|
248838
|
+
});
|
|
248839
|
+
return localProcessId;
|
|
248840
|
+
};
|
|
248841
|
+
const start = async (request) => {
|
|
248842
|
+
for (const [localProcessId, info] of fastify2.remoteExecutorMap) {
|
|
248843
|
+
if (info.executorId === request.executorId && info.remoteServerId === request.remoteServerId) {
|
|
248844
|
+
return { kind: "already_running", processId: localProcessId };
|
|
248845
|
+
}
|
|
248846
|
+
}
|
|
248847
|
+
const key2 = `${request.executorId}:${request.remoteServerId}`;
|
|
248848
|
+
let entry = pending.get(key2);
|
|
248849
|
+
if (entry?.inFlight) return { kind: "starting" };
|
|
248850
|
+
if (!entry) {
|
|
248851
|
+
entry = {
|
|
248852
|
+
...request,
|
|
248853
|
+
processId: randomUUID15(),
|
|
248854
|
+
effectFingerprint: createHash7("sha256").update(JSON.stringify({ executorId: request.executorId, remoteServerId: request.remoteServerId, body: request.body })).digest("hex"),
|
|
248855
|
+
inFlight: false
|
|
248856
|
+
};
|
|
248857
|
+
pending.set(key2, entry);
|
|
248858
|
+
}
|
|
248859
|
+
const current = entry;
|
|
248860
|
+
current.inFlight = true;
|
|
248861
|
+
try {
|
|
248862
|
+
const result = await proxyToRemoteAuto(
|
|
248863
|
+
current.remoteServerId,
|
|
248864
|
+
"POST",
|
|
248865
|
+
"/api/path/execute",
|
|
248866
|
+
{ ...current.body, processId: current.processId, effectFingerprint: current.effectFingerprint },
|
|
248867
|
+
{ reverseConnectManager: fastify2.reverseConnectManager }
|
|
248868
|
+
);
|
|
248869
|
+
if (result.ok) {
|
|
248870
|
+
pending.delete(key2);
|
|
248871
|
+
const remoteProcessId = result.data.processId;
|
|
248872
|
+
return { kind: "started", processId: await register(current, remoteProcessId) };
|
|
248873
|
+
}
|
|
248874
|
+
if (isTransportFailure(result)) {
|
|
248875
|
+
if (!await supportsIdempotentExecute(current.remoteServerId)) pending.delete(key2);
|
|
248876
|
+
const reason = result.data?.error ?? "Remote server unreachable";
|
|
248877
|
+
return { kind: "unknown", error: reason };
|
|
248878
|
+
}
|
|
248879
|
+
pending.delete(key2);
|
|
248880
|
+
return { kind: "rejected", result };
|
|
248881
|
+
} finally {
|
|
248882
|
+
current.inFlight = false;
|
|
248883
|
+
}
|
|
248884
|
+
};
|
|
248885
|
+
const reconcile = async (remoteServerId) => {
|
|
248886
|
+
const candidates = [...pending.entries()].filter(
|
|
248887
|
+
([, entry]) => entry.remoteServerId === remoteServerId && !entry.inFlight
|
|
248888
|
+
);
|
|
248889
|
+
if (candidates.length === 0) return;
|
|
248890
|
+
for (const [, entry] of candidates) entry.inFlight = true;
|
|
248891
|
+
try {
|
|
248892
|
+
const running = await listWorkerRunningProcessIds(fastify2, remoteServerId);
|
|
248893
|
+
if (!running) return;
|
|
248894
|
+
for (const [key2, entry] of candidates) {
|
|
248895
|
+
if (!running.has(entry.processId) || pending.get(key2) !== entry) continue;
|
|
248896
|
+
pending.delete(key2);
|
|
248897
|
+
await register(entry, entry.processId);
|
|
248898
|
+
console.log(`[RemoteExecutorStarts] confirmed start after reconnect: executor=${entry.executorId} target=${remoteServerId} process=${entry.processId}`);
|
|
248899
|
+
}
|
|
248900
|
+
} finally {
|
|
248901
|
+
for (const [, entry] of candidates) entry.inFlight = false;
|
|
248902
|
+
}
|
|
248903
|
+
};
|
|
248904
|
+
fastify2.reverseConnectManager.setStatusChangeHandler((_remoteServerId, status) => {
|
|
248905
|
+
if (status !== "online") return;
|
|
248906
|
+
const servers = new Set([...pending.values()].map((entry) => entry.remoteServerId));
|
|
248907
|
+
for (const serverId of servers) {
|
|
248908
|
+
if (!fastify2.reverseConnectManager.isConnected(serverId)) continue;
|
|
248909
|
+
reconcile(serverId).catch((error48) => {
|
|
248910
|
+
console.error(`[RemoteExecutorStarts] reconcile failed for ${serverId}:`, error48);
|
|
248911
|
+
});
|
|
248912
|
+
}
|
|
248913
|
+
});
|
|
248914
|
+
return { start, reconcile };
|
|
248915
|
+
}
|
|
248916
|
+
|
|
248917
|
+
// src/routes/process-routes.ts
|
|
247668
248918
|
var routes7 = async (fastify2) => {
|
|
248919
|
+
const remoteStarts = createRemoteExecutorStarts(fastify2);
|
|
247669
248920
|
fastify2.post("/api/path/execute", async (req, reply) => {
|
|
247670
248921
|
const userId = requireAuth(req, reply);
|
|
247671
248922
|
if (userId === null) return;
|
|
@@ -247682,7 +248933,7 @@ var routes7 = async (fastify2) => {
|
|
|
247682
248933
|
const resolvedBase = resolveWorktreePath(projectPath, branch ?? null);
|
|
247683
248934
|
const resolvedCwd = cwd ? path11.join(resolvedBase, cwd) : null;
|
|
247684
248935
|
const tempExecutor = {
|
|
247685
|
-
id:
|
|
248936
|
+
id: randomUUID16(),
|
|
247686
248937
|
project_id: project.id,
|
|
247687
248938
|
workspace_id: "",
|
|
247688
248939
|
name: "remote-command",
|
|
@@ -247749,11 +249000,12 @@ var routes7 = async (fastify2) => {
|
|
|
247749
249000
|
if (!remoteConfig) {
|
|
247750
249001
|
return reply.code(400).send({ error: `Remote server configuration not found for executor_mode="${executorMode}"` });
|
|
247751
249002
|
}
|
|
247752
|
-
const
|
|
247753
|
-
|
|
247754
|
-
|
|
247755
|
-
|
|
247756
|
-
|
|
249003
|
+
const outcome = await remoteStarts.start({
|
|
249004
|
+
executorId: executor.id,
|
|
249005
|
+
projectId: project.id,
|
|
249006
|
+
remoteServerId: executorMode,
|
|
249007
|
+
branch,
|
|
249008
|
+
body: {
|
|
247757
249009
|
path: remoteConfig.remote_path,
|
|
247758
249010
|
command: executor.command,
|
|
247759
249011
|
executor_type: executor.executor_type,
|
|
@@ -247761,38 +249013,27 @@ var routes7 = async (fastify2) => {
|
|
|
247761
249013
|
branch: branch ?? void 0,
|
|
247762
249014
|
cwd: executor.cwd || void 0,
|
|
247763
249015
|
pty: executor.pty
|
|
247764
|
-
}
|
|
247765
|
-
|
|
247766
|
-
)
|
|
247767
|
-
|
|
247768
|
-
|
|
247769
|
-
|
|
247770
|
-
|
|
247771
|
-
|
|
247772
|
-
|
|
247773
|
-
|
|
247774
|
-
|
|
247775
|
-
|
|
247776
|
-
|
|
247777
|
-
|
|
247778
|
-
|
|
247779
|
-
|
|
247780
|
-
|
|
247781
|
-
|
|
247782
|
-
|
|
247783
|
-
|
|
247784
|
-
machineId: fastify2.reverseConnectManager.getMachineId(executorMode)
|
|
247785
|
-
});
|
|
247786
|
-
fastify2.eventBus.emit({
|
|
247787
|
-
type: "executor:started",
|
|
247788
|
-
projectId: project.id,
|
|
247789
|
-
executorId: executor.id,
|
|
247790
|
-
processId: localProcessId,
|
|
247791
|
-
target: executorMode
|
|
247792
|
-
});
|
|
247793
|
-
return reply.code(200).send({ processId: localProcessId });
|
|
249016
|
+
}
|
|
249017
|
+
});
|
|
249018
|
+
switch (outcome.kind) {
|
|
249019
|
+
case "started":
|
|
249020
|
+
return reply.code(200).send({ processId: outcome.processId });
|
|
249021
|
+
case "already_running":
|
|
249022
|
+
return reply.code(409).send({
|
|
249023
|
+
error: "Executor is already running on this target",
|
|
249024
|
+
code: "already_running",
|
|
249025
|
+
processId: outcome.processId
|
|
249026
|
+
});
|
|
249027
|
+
case "starting":
|
|
249028
|
+
return reply.code(409).send({ error: "Executor is already starting on this target", code: "starting" });
|
|
249029
|
+
case "unknown":
|
|
249030
|
+
return reply.code(503).send({
|
|
249031
|
+
error: `Start result unknown (${outcome.error}). Starting again once the remote is back is safe.`,
|
|
249032
|
+
code: "start_unknown"
|
|
249033
|
+
});
|
|
249034
|
+
case "rejected":
|
|
249035
|
+
return reply.code(proxyStatus(outcome.result)).send(outcome.result.data);
|
|
247794
249036
|
}
|
|
247795
|
-
return reply.code(proxyStatus(result)).send(result.data);
|
|
247796
249037
|
}
|
|
247797
249038
|
if (!project.path) {
|
|
247798
249039
|
return reply.code(400).send({ error: "Project has no local path" });
|
|
@@ -247822,15 +249063,27 @@ var routes7 = async (fastify2) => {
|
|
|
247822
249063
|
void 0,
|
|
247823
249064
|
{ reverseConnectManager: fastify2.reverseConnectManager }
|
|
247824
249065
|
);
|
|
247825
|
-
if (result.ok) {
|
|
247826
|
-
fastify2.
|
|
247827
|
-
|
|
247828
|
-
|
|
247829
|
-
|
|
247830
|
-
|
|
247831
|
-
|
|
247832
|
-
|
|
247833
|
-
|
|
249066
|
+
if (!result.ok && result.status === 404) {
|
|
249067
|
+
const running = await listWorkerRunningProcessIds(fastify2, remoteInfo.remoteServerId);
|
|
249068
|
+
if (!running || running.has(remoteInfo.remoteProcessId)) {
|
|
249069
|
+
return reply.code(502).send({
|
|
249070
|
+
error: running ? "Remote process could not be stopped and is still running" : "Remote process could not be stopped and its state could not be verified"
|
|
249071
|
+
});
|
|
249072
|
+
}
|
|
249073
|
+
}
|
|
249074
|
+
if (result.ok || result.status === 404) {
|
|
249075
|
+
fastify2.remoteExecutorMonitor.unwatch(req.params.processId);
|
|
249076
|
+
if (!remoteInfo.stoppedEmitted) {
|
|
249077
|
+
remoteInfo.stoppedEmitted = true;
|
|
249078
|
+
fastify2.eventBus.emit({
|
|
249079
|
+
type: "executor:stopped",
|
|
249080
|
+
projectId: remoteInfo.projectId ?? "",
|
|
249081
|
+
executorId: remoteInfo.executorId,
|
|
249082
|
+
processId: req.params.processId,
|
|
249083
|
+
exitCode: 0,
|
|
249084
|
+
target: remoteInfo.remoteServerId
|
|
249085
|
+
});
|
|
249086
|
+
}
|
|
247834
249087
|
fastify2.remoteExecutorMap.delete(req.params.processId);
|
|
247835
249088
|
await fastify2.storage.remoteExecutorProcesses.markFinished(req.params.processId, 0, "killed");
|
|
247836
249089
|
}
|
|
@@ -249529,6 +250782,53 @@ import fs3 from "fs/promises";
|
|
|
249529
250782
|
import { createReadStream as createReadStream2, constants as fsConstants } from "fs";
|
|
249530
250783
|
import { execFile } from "child_process";
|
|
249531
250784
|
import { promisify as promisify2 } from "util";
|
|
250785
|
+
|
|
250786
|
+
// src/artifact-read-targets.ts
|
|
250787
|
+
function isOutsideArtifactPath(filePath) {
|
|
250788
|
+
return filePath.startsWith("/") || filePath === "~" || filePath.startsWith("~/");
|
|
250789
|
+
}
|
|
250790
|
+
var UUID = "[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}";
|
|
250791
|
+
var REMOTE_SESSION_PROJECT_RE = new RegExp(`^remote-${UUID}-(${UUID})-`);
|
|
250792
|
+
async function sessionBelongsToProject(deps, sessionId, projectId) {
|
|
250793
|
+
const embedded = REMOTE_SESSION_PROJECT_RE.exec(sessionId)?.[1];
|
|
250794
|
+
if (embedded) return embedded === projectId;
|
|
250795
|
+
if (sessionId.startsWith("remote-")) return false;
|
|
250796
|
+
const session = await deps.storage.agentSessions.getById(sessionId);
|
|
250797
|
+
return session?.project_id === projectId;
|
|
250798
|
+
}
|
|
250799
|
+
async function resolveArtifactReadTargets(deps, opts) {
|
|
250800
|
+
const candidates = [];
|
|
250801
|
+
const sessionId = opts.sessionId && await sessionBelongsToProject(deps, opts.sessionId, opts.projectId) ? opts.sessionId : null;
|
|
250802
|
+
if (sessionId) {
|
|
250803
|
+
const sessionServerId = deps.remoteSessionMap.get(sessionId)?.remoteServerId;
|
|
250804
|
+
if (sessionServerId) candidates.push(sessionServerId);
|
|
250805
|
+
candidates.push(
|
|
250806
|
+
...await deps.storage.crossRemoteAudit.listSessionTargets(sessionId, opts.userId)
|
|
250807
|
+
);
|
|
250808
|
+
}
|
|
250809
|
+
const targets = [];
|
|
250810
|
+
const seen = /* @__PURE__ */ new Set();
|
|
250811
|
+
for (const serverId of candidates) {
|
|
250812
|
+
if (seen.has(serverId)) continue;
|
|
250813
|
+
seen.add(serverId);
|
|
250814
|
+
if (opts.primary?.serverId === serverId) {
|
|
250815
|
+
targets.push(opts.primary);
|
|
250816
|
+
continue;
|
|
250817
|
+
}
|
|
250818
|
+
const link = await deps.storage.projectRemotes.getByProjectAndServer(opts.projectId, serverId);
|
|
250819
|
+
if (link) {
|
|
250820
|
+
targets.push({ serverId, remotePath: link.remote_path, branch: opts.branch ?? null });
|
|
250821
|
+
continue;
|
|
250822
|
+
}
|
|
250823
|
+
const reach = await canReachRemote(deps, opts.userId, serverId, "read");
|
|
250824
|
+
if (reach.ok) targets.push({ serverId, remotePath: "/", branch: null });
|
|
250825
|
+
}
|
|
250826
|
+
if (opts.primary && !seen.has(opts.primary.serverId)) targets.push(opts.primary);
|
|
250827
|
+
return targets;
|
|
250828
|
+
}
|
|
250829
|
+
|
|
250830
|
+
// src/routes/file-routes.ts
|
|
250831
|
+
var SOURCE_SERVER_HEADER = "X-Vibedeckx-Source-Server";
|
|
249532
250832
|
async function getRemoteConfig3(fastify2, project) {
|
|
249533
250833
|
const remotes = await fastify2.storage.projectRemotes.getByProject(project.id);
|
|
249534
250834
|
if (remotes.length === 0) return null;
|
|
@@ -249538,6 +250838,42 @@ async function getRemoteConfig3(fastify2, project) {
|
|
|
249538
250838
|
remotePath: primary.remote_path
|
|
249539
250839
|
};
|
|
249540
250840
|
}
|
|
250841
|
+
async function readTargetsFor(fastify2, opts) {
|
|
250842
|
+
const remoteConfig = await getRemoteConfig3(fastify2, opts.project);
|
|
250843
|
+
const primary = remoteConfig ? { ...remoteConfig, branch: opts.branch ?? null } : null;
|
|
250844
|
+
if (!isOutsideArtifactPath(opts.filePath)) return primary ? [primary] : [];
|
|
250845
|
+
return resolveArtifactReadTargets(fastify2, {
|
|
250846
|
+
projectId: opts.project.id,
|
|
250847
|
+
userId: opts.userId,
|
|
250848
|
+
sessionId: opts.sessionId,
|
|
250849
|
+
branch: opts.branch,
|
|
250850
|
+
primary
|
|
250851
|
+
});
|
|
250852
|
+
}
|
|
250853
|
+
function readParams(target, filePath) {
|
|
250854
|
+
const params = [
|
|
250855
|
+
`path=${encodeURIComponent(target.remotePath)}`,
|
|
250856
|
+
`filePath=${encodeURIComponent(filePath)}`
|
|
250857
|
+
];
|
|
250858
|
+
if (target.branch) params.push(`branch=${encodeURIComponent(target.branch)}`);
|
|
250859
|
+
return params.join("&");
|
|
250860
|
+
}
|
|
250861
|
+
async function firstRemoteHit(targets, send) {
|
|
250862
|
+
let firstMiss = null;
|
|
250863
|
+
let firstError = null;
|
|
250864
|
+
for (const target of targets) {
|
|
250865
|
+
const result = await send(target);
|
|
250866
|
+
const attempt = { result, serverId: target.serverId };
|
|
250867
|
+
const status = proxyStatus(result);
|
|
250868
|
+
if (status === 200) return attempt;
|
|
250869
|
+
if (status === 404 || status === 403 || result.status === 0) firstMiss ??= attempt;
|
|
250870
|
+
else firstError ??= attempt;
|
|
250871
|
+
}
|
|
250872
|
+
return firstError ?? firstMiss ?? {
|
|
250873
|
+
result: { ok: false, status: 404, data: { error: "File not found" } },
|
|
250874
|
+
serverId: ""
|
|
250875
|
+
};
|
|
250876
|
+
}
|
|
249541
250877
|
function emitFilesChanged(fastify2, projectId, branch, change) {
|
|
249542
250878
|
try {
|
|
249543
250879
|
fastify2.eventBus.emit({ type: "files:changed", projectId, branch, change });
|
|
@@ -250058,23 +251394,30 @@ var routes10 = async (fastify2) => {
|
|
|
250058
251394
|
const target = req.query.target;
|
|
250059
251395
|
const useRemote = target === "remote" || !target && !project.path;
|
|
250060
251396
|
if (useRemote) {
|
|
250061
|
-
const
|
|
250062
|
-
|
|
251397
|
+
const targets = await readTargetsFor(fastify2, {
|
|
251398
|
+
project,
|
|
251399
|
+
filePath,
|
|
251400
|
+
branch,
|
|
251401
|
+
sessionId: req.query.sessionId,
|
|
251402
|
+
userId
|
|
251403
|
+
});
|
|
251404
|
+
if (targets.length === 0) {
|
|
250063
251405
|
return reply.code(400).send({ error: "Project has no remote configuration" });
|
|
250064
251406
|
}
|
|
250065
|
-
const
|
|
250066
|
-
|
|
250067
|
-
|
|
250068
|
-
|
|
250069
|
-
|
|
250070
|
-
|
|
250071
|
-
|
|
250072
|
-
|
|
250073
|
-
|
|
250074
|
-
void 0,
|
|
250075
|
-
{ reverseConnectManager: fastify2.reverseConnectManager }
|
|
251407
|
+
const { result, serverId } = await firstRemoteHit(
|
|
251408
|
+
targets,
|
|
251409
|
+
(target2) => proxyToRemoteAuto(
|
|
251410
|
+
target2.serverId,
|
|
251411
|
+
"GET",
|
|
251412
|
+
`/api/path/file-content?${readParams(target2, filePath)}`,
|
|
251413
|
+
void 0,
|
|
251414
|
+
{ reverseConnectManager: fastify2.reverseConnectManager }
|
|
251415
|
+
)
|
|
250076
251416
|
);
|
|
250077
|
-
|
|
251417
|
+
const status = proxyStatus(result);
|
|
251418
|
+
if (status === 200 && serverId) reply.header(SOURCE_SERVER_HEADER, serverId);
|
|
251419
|
+
const body = status === 200 && result.data && typeof result.data === "object" ? { ...result.data, serverId } : result.data;
|
|
251420
|
+
return reply.code(status).send(body);
|
|
250078
251421
|
}
|
|
250079
251422
|
if (!project.path) {
|
|
250080
251423
|
return reply.code(400).send({ error: "Project has no local path" });
|
|
@@ -250165,22 +251508,27 @@ var routes10 = async (fastify2) => {
|
|
|
250165
251508
|
const target = req.query.target;
|
|
250166
251509
|
const useRemote = target === "remote" || !target && !project.path;
|
|
250167
251510
|
if (useRemote) {
|
|
250168
|
-
const
|
|
250169
|
-
|
|
251511
|
+
const targets = await readTargetsFor(fastify2, {
|
|
251512
|
+
project,
|
|
251513
|
+
filePath,
|
|
251514
|
+
branch,
|
|
251515
|
+
sessionId: req.query.sessionId,
|
|
251516
|
+
userId
|
|
251517
|
+
});
|
|
251518
|
+
if (targets.length === 0) {
|
|
250170
251519
|
return reply.code(400).send({ error: "Project has no remote configuration" });
|
|
250171
251520
|
}
|
|
250172
|
-
const
|
|
250173
|
-
|
|
250174
|
-
|
|
250175
|
-
|
|
250176
|
-
|
|
250177
|
-
|
|
250178
|
-
|
|
250179
|
-
|
|
250180
|
-
|
|
250181
|
-
void 0,
|
|
250182
|
-
{ reverseConnectManager: fastify2.reverseConnectManager }
|
|
251521
|
+
const { result, serverId } = await firstRemoteHit(
|
|
251522
|
+
targets,
|
|
251523
|
+
(target2) => proxyToRemoteAuto(
|
|
251524
|
+
target2.serverId,
|
|
251525
|
+
"GET",
|
|
251526
|
+
`/api/path/file-download?${readParams(target2, filePath)}`,
|
|
251527
|
+
void 0,
|
|
251528
|
+
{ reverseConnectManager: fastify2.reverseConnectManager }
|
|
251529
|
+
)
|
|
250183
251530
|
);
|
|
251531
|
+
if (proxyStatus(result) === 200 && serverId) reply.header(SOURCE_SERVER_HEADER, serverId);
|
|
250184
251532
|
if (Buffer.isBuffer(result.data)) {
|
|
250185
251533
|
const fileName2 = path13.basename(filePath);
|
|
250186
251534
|
return reply.code(proxyStatus(result)).header("Content-Disposition", `attachment; filename="${fileName2}"`).type("application/octet-stream").send(result.data);
|
|
@@ -250347,7 +251695,7 @@ var import_fastify_plugin12 = __toESM(require_plugin2(), 1);
|
|
|
250347
251695
|
import { chmod, mkdir as mkdir4, open as open2 } from "node:fs/promises";
|
|
250348
251696
|
import { constants as fsConstants2 } from "node:fs";
|
|
250349
251697
|
import path15 from "node:path";
|
|
250350
|
-
import { randomUUID as
|
|
251698
|
+
import { randomUUID as randomUUID17 } from "node:crypto";
|
|
250351
251699
|
|
|
250352
251700
|
// src/utils/temp-file-sweep.ts
|
|
250353
251701
|
import { readdir as readdir2, rm, stat as stat2 } from "node:fs/promises";
|
|
@@ -250392,7 +251740,7 @@ var FILE_FLAGS = fsConstants2.O_WRONLY | fsConstants2.O_CREAT | fsConstants2.O_E
|
|
|
250392
251740
|
async function writePasteToTempFile(content) {
|
|
250393
251741
|
await mkdir4(PASTE_DIR, { recursive: true, mode: DIR_MODE });
|
|
250394
251742
|
await chmod(PASTE_DIR, DIR_MODE);
|
|
250395
|
-
const filePath = path15.join(PASTE_DIR, `${
|
|
251743
|
+
const filePath = path15.join(PASTE_DIR, `${randomUUID17()}.txt`);
|
|
250396
251744
|
const handle = await open2(filePath, FILE_FLAGS, FILE_MODE);
|
|
250397
251745
|
try {
|
|
250398
251746
|
await handle.writeFile(content, "utf8");
|
|
@@ -250407,7 +251755,7 @@ async function writePasteToTempFile(content) {
|
|
|
250407
251755
|
import { chmod as chmod2, mkdir as mkdir5, open as open3 } from "node:fs/promises";
|
|
250408
251756
|
import { constants as fsConstants3 } from "node:fs";
|
|
250409
251757
|
import path16 from "node:path";
|
|
250410
|
-
import { randomUUID as
|
|
251758
|
+
import { randomUUID as randomUUID18 } from "node:crypto";
|
|
250411
251759
|
var MAX_ATTACHMENT_BYTES = 20 * 1024 * 1024;
|
|
250412
251760
|
var ATTACHMENT_BODY_LIMIT = Math.ceil(MAX_ATTACHMENT_BYTES * 4 / 3) + 1024 * 1024;
|
|
250413
251761
|
var DIR_MODE2 = 448;
|
|
@@ -250429,7 +251777,7 @@ async function writeAttachmentToTempFile(rawName, data) {
|
|
|
250429
251777
|
const name25 = sanitizeAttachmentName(rawName);
|
|
250430
251778
|
await mkdir5(ATTACHMENT_DIR, { recursive: true, mode: DIR_MODE2 });
|
|
250431
251779
|
await chmod2(ATTACHMENT_DIR, DIR_MODE2);
|
|
250432
|
-
const dir = path16.join(ATTACHMENT_DIR,
|
|
251780
|
+
const dir = path16.join(ATTACHMENT_DIR, randomUUID18());
|
|
250433
251781
|
await mkdir5(dir, { mode: DIR_MODE2 });
|
|
250434
251782
|
const filePath = path16.join(dir, name25);
|
|
250435
251783
|
const handle = await open3(filePath, FILE_FLAGS2, FILE_MODE2);
|
|
@@ -250442,8 +251790,30 @@ async function writeAttachmentToTempFile(rawName, data) {
|
|
|
250442
251790
|
return { path: filePath, name: name25, size: data.length };
|
|
250443
251791
|
}
|
|
250444
251792
|
|
|
251793
|
+
// src/routes/session-ownership.ts
|
|
251794
|
+
async function authorizeLocalSession(storage2, sessionId, authResult) {
|
|
251795
|
+
const activity = await storage2.agentSessions.getActivityById(sessionId, "runtime");
|
|
251796
|
+
if (!activity) return false;
|
|
251797
|
+
return Boolean(await storage2.projects.getById(activity.projectId, authResult));
|
|
251798
|
+
}
|
|
251799
|
+
async function authorizeRemoteSession(storage2, sessionId, authResult) {
|
|
251800
|
+
const { projectId } = await resolveRemoteSessionOwner(storage2, sessionId);
|
|
251801
|
+
if (!projectId) return false;
|
|
251802
|
+
return Boolean(await storage2.projects.getById(projectId, authResult));
|
|
251803
|
+
}
|
|
251804
|
+
async function resolveRemoteSessionOwner(storage2, sessionId) {
|
|
251805
|
+
const [intent, mapping] = await Promise.all([
|
|
251806
|
+
storage2.remoteSessionCreationIntents.getByLocal(sessionId),
|
|
251807
|
+
storage2.remoteSessionMappings.getByLocal(sessionId)
|
|
251808
|
+
]);
|
|
251809
|
+
return {
|
|
251810
|
+
projectId: mapping?.project_id ?? intent?.project_id,
|
|
251811
|
+
remoteServerId: mapping?.remote_server_id ?? intent?.remote_server_id
|
|
251812
|
+
};
|
|
251813
|
+
}
|
|
251814
|
+
|
|
250445
251815
|
// src/utils/review-brief.ts
|
|
250446
|
-
import { createHash as
|
|
251816
|
+
import { createHash as createHash8 } from "crypto";
|
|
250447
251817
|
var BASE_TIMEOUT_MS = 15e3;
|
|
250448
251818
|
var JUDGMENT_TIMEOUT_MS = 9e4;
|
|
250449
251819
|
var TIMEOUT_MS_PER_1K_CHARS = 120;
|
|
@@ -250711,7 +252081,7 @@ var BRIEF_CACHE_MAX_ENTRIES = 50;
|
|
|
250711
252081
|
var briefCache = /* @__PURE__ */ new Map();
|
|
250712
252082
|
var briefInFlight = /* @__PURE__ */ new Map();
|
|
250713
252083
|
function cacheKey(userId, conversation) {
|
|
250714
|
-
return
|
|
252084
|
+
return createHash8("sha256").update(userId).update("\0").update(conversation).digest("hex");
|
|
250715
252085
|
}
|
|
250716
252086
|
function readBriefCache(key2) {
|
|
250717
252087
|
const hit = briefCache.get(key2);
|
|
@@ -250811,7 +252181,7 @@ function workspaceMissingOnRemoteBody(missing) {
|
|
|
250811
252181
|
}
|
|
250812
252182
|
|
|
250813
252183
|
// src/routes/agent-session-routes.ts
|
|
250814
|
-
import {
|
|
252184
|
+
import { randomUUID as randomUUID19 } from "crypto";
|
|
250815
252185
|
|
|
250816
252186
|
// src/protocol/model-suggestions.ts
|
|
250817
252187
|
var MODEL_SUGGESTIONS = {
|
|
@@ -250838,45 +252208,6 @@ function messageTextLength(content) {
|
|
|
250838
252208
|
return n;
|
|
250839
252209
|
}
|
|
250840
252210
|
var routes11 = async (fastify2) => {
|
|
250841
|
-
const instructionReceiverToken = randomUUID17();
|
|
250842
|
-
const instructionDeliveryLocks = /* @__PURE__ */ new Map();
|
|
250843
|
-
const sessionMutationLocks = /* @__PURE__ */ new Map();
|
|
250844
|
-
async function serializeInstructionDelivery(key2, effect) {
|
|
250845
|
-
const previous = instructionDeliveryLocks.get(key2) ?? Promise.resolve();
|
|
250846
|
-
let release;
|
|
250847
|
-
const gate = new Promise((resolve3) => {
|
|
250848
|
-
release = resolve3;
|
|
250849
|
-
});
|
|
250850
|
-
const tail = previous.then(() => gate);
|
|
250851
|
-
instructionDeliveryLocks.set(key2, tail);
|
|
250852
|
-
await previous;
|
|
250853
|
-
try {
|
|
250854
|
-
return await effect();
|
|
250855
|
-
} finally {
|
|
250856
|
-
release();
|
|
250857
|
-
if (instructionDeliveryLocks.get(key2) === tail) instructionDeliveryLocks.delete(key2);
|
|
250858
|
-
}
|
|
250859
|
-
}
|
|
250860
|
-
async function serializeSessionMutation(sessionId, effect) {
|
|
250861
|
-
const previous = sessionMutationLocks.get(sessionId) ?? Promise.resolve();
|
|
250862
|
-
let release;
|
|
250863
|
-
const gate = new Promise((resolve3) => {
|
|
250864
|
-
release = resolve3;
|
|
250865
|
-
});
|
|
250866
|
-
const tail = previous.then(() => gate);
|
|
250867
|
-
sessionMutationLocks.set(sessionId, tail);
|
|
250868
|
-
await previous;
|
|
250869
|
-
try {
|
|
250870
|
-
return await effect();
|
|
250871
|
-
} finally {
|
|
250872
|
-
release();
|
|
250873
|
-
if (sessionMutationLocks.get(sessionId) === tail) sessionMutationLocks.delete(sessionId);
|
|
250874
|
-
}
|
|
250875
|
-
}
|
|
250876
|
-
function instructionContentHash(content) {
|
|
250877
|
-
const canonical = typeof content === "string" ? JSON.stringify({ type: "string", content }) : JSON.stringify(content.map((part) => part.type === "text" ? { type: "text", text: part.text } : { type: "image", mediaType: part.mediaType, data: part.data }));
|
|
250878
|
-
return createHash7("sha256").update(canonical).digest("hex");
|
|
250879
|
-
}
|
|
250880
252211
|
function proxyAuto(remoteServerId, method, apiPath, body) {
|
|
250881
252212
|
return proxyToRemoteAuto(remoteServerId, method, apiPath, body, {
|
|
250882
252213
|
reverseConnectManager: fastify2.reverseConnectManager
|
|
@@ -251712,7 +253043,7 @@ var routes11 = async (fastify2) => {
|
|
|
251712
253043
|
return reply.code(400).send({ error: "Project has no local path" });
|
|
251713
253044
|
}
|
|
251714
253045
|
try {
|
|
251715
|
-
const preSessionId =
|
|
253046
|
+
const preSessionId = randomUUID19();
|
|
251716
253047
|
const crossRemoteMcp = await mintCrossRemoteMcpConfig(
|
|
251717
253048
|
{ storage: fastify2.storage },
|
|
251718
253049
|
{ userId, sessionId: preSessionId, sourceRemoteServerId: null }
|
|
@@ -251982,22 +253313,39 @@ var routes11 = async (fastify2) => {
|
|
|
251982
253313
|
const authResult = requireUserFacingUserId(req, reply);
|
|
251983
253314
|
if (authResult === null) return;
|
|
251984
253315
|
const userId = resolveUserId(authResult);
|
|
251985
|
-
const { content, idempotencyKey } = req.body;
|
|
253316
|
+
const { content: rawContent, idempotencyKey } = req.body;
|
|
251986
253317
|
console.log(`[API] POST /message: sessionId=${req.params.sessionId}, isRemote=${req.params.sessionId.startsWith("remote-")}, remoteMapSize=${fastify2.remoteSessionMap.size}`);
|
|
251987
|
-
const isValidString = typeof
|
|
251988
|
-
const isValidArray = Array.isArray(
|
|
253318
|
+
const isValidString = typeof rawContent === "string" && rawContent.trim().length > 0;
|
|
253319
|
+
const isValidArray = Array.isArray(rawContent) && rawContent.length > 0;
|
|
251989
253320
|
if (!isValidString && !isValidArray) {
|
|
251990
253321
|
return reply.code(400).send({ error: "Content is required" });
|
|
251991
253322
|
}
|
|
251992
253323
|
if (idempotencyKey !== void 0 && (typeof idempotencyKey !== "string" || idempotencyKey.length < 1 || idempotencyKey.length > 512)) {
|
|
251993
253324
|
return reply.code(400).send({ error: "idempotencyKey must contain 1-512 characters" });
|
|
251994
253325
|
}
|
|
251995
|
-
const textLen = messageTextLength(
|
|
253326
|
+
const textLen = messageTextLength(rawContent);
|
|
251996
253327
|
if (textLen > MESSAGE_TEXT_CHAR_LIMIT) {
|
|
251997
253328
|
return reply.code(413).send({
|
|
251998
253329
|
error: `Message text exceeds ${MESSAGE_TEXT_CHAR_LIMIT} characters (got ${textLen}). Use /api/agent-sessions/:id/paste for long content.`
|
|
251999
253330
|
});
|
|
252000
253331
|
}
|
|
253332
|
+
if (req.body.grantedRemoteIds !== void 0) {
|
|
253333
|
+
const raw = req.body.grantedRemoteIds;
|
|
253334
|
+
if (!Array.isArray(raw) || raw.some((id) => typeof id !== "string")) {
|
|
253335
|
+
return reply.code(400).send({ error: "grantedRemoteIds must be an array of strings" });
|
|
253336
|
+
}
|
|
253337
|
+
const owned = req.params.sessionId.startsWith("remote-") ? await authorizeRemoteSession(fastify2.storage, req.params.sessionId, authResult) : await authorizeLocalSession(fastify2.storage, req.params.sessionId, authResult);
|
|
253338
|
+
if (!owned) return reply.code(404).send({ error: "Session not found" });
|
|
253339
|
+
const ids = [...new Set(raw)];
|
|
253340
|
+
const sourceRemoteServerId = req.params.sessionId.startsWith("remote-") ? (await resolveRemoteSessionOwner(fastify2.storage, req.params.sessionId)).remoteServerId : void 0;
|
|
253341
|
+
const rejection = await validateSessionGrantIds(fastify2.storage, ids, authResult, sourceRemoteServerId);
|
|
253342
|
+
if (rejection) return reply.code(400).send({ error: rejection });
|
|
253343
|
+
await fastify2.storage.sessionRemoteGrants.replace(req.params.sessionId, userId, ids);
|
|
253344
|
+
}
|
|
253345
|
+
const content = await appendRemoteGrantContext(fastify2.storage, req.params.sessionId, userId, rawContent).catch((err) => {
|
|
253346
|
+
console.error(`[API] grant context build failed for ${req.params.sessionId}:`, err);
|
|
253347
|
+
return rawContent;
|
|
253348
|
+
});
|
|
252001
253349
|
if (req.params.sessionId.startsWith("remote-")) {
|
|
252002
253350
|
const remoteInfo = await getAuthorizedRemoteSessionInfo(req.params.sessionId, authResult);
|
|
252003
253351
|
if (!remoteInfo) {
|
|
@@ -252126,70 +253474,30 @@ var routes11 = async (fastify2) => {
|
|
|
252126
253474
|
}
|
|
252127
253475
|
return reply.code(200).send({ success: true });
|
|
252128
253476
|
}
|
|
252129
|
-
const
|
|
252130
|
-
|
|
252131
|
-
|
|
252132
|
-
|
|
252133
|
-
|
|
252134
|
-
|
|
252135
|
-
|
|
252136
|
-
|
|
252137
|
-
|
|
253477
|
+
const result = await deliverInstruction({
|
|
253478
|
+
storage: fastify2.storage,
|
|
253479
|
+
sessionId: req.params.sessionId,
|
|
253480
|
+
idempotencyKey,
|
|
253481
|
+
rawContent,
|
|
253482
|
+
deliver
|
|
253483
|
+
});
|
|
253484
|
+
switch (result) {
|
|
253485
|
+
case "delivered":
|
|
253486
|
+
return reply.code(200).send({ success: true });
|
|
253487
|
+
case "replayed":
|
|
253488
|
+
return reply.code(200).send({ success: true, replayed: true });
|
|
253489
|
+
case "conflict":
|
|
252138
253490
|
return reply.code(409).send({ error: "Idempotency key was already used with different content" });
|
|
252139
|
-
|
|
252140
|
-
if (claim === "sent") return reply.code(200).send({ success: true, replayed: true });
|
|
252141
|
-
if (claim === "busy") {
|
|
253491
|
+
case "busy":
|
|
252142
253492
|
return reply.code(409).send({ error: "Instruction delivery is already in progress" });
|
|
252143
|
-
|
|
252144
|
-
|
|
252145
|
-
|
|
252146
|
-
|
|
252147
|
-
|
|
252148
|
-
|
|
252149
|
-
|
|
252150
|
-
|
|
252151
|
-
})) ownershipLost = true;
|
|
252152
|
-
} catch {
|
|
252153
|
-
ownershipLost = true;
|
|
252154
|
-
}
|
|
252155
|
-
};
|
|
252156
|
-
await renew();
|
|
252157
|
-
const heartbeat = setInterval(() => {
|
|
252158
|
-
void renew();
|
|
252159
|
-
}, 1e4);
|
|
252160
|
-
heartbeat.unref();
|
|
252161
|
-
try {
|
|
252162
|
-
if (ownershipLost) return reply.code(409).send({ error: "Instruction delivery ownership was lost" });
|
|
252163
|
-
if (!await deliver()) {
|
|
252164
|
-
await fastify2.storage.agentInstructionDeliveries.release({
|
|
252165
|
-
sessionId: req.params.sessionId,
|
|
252166
|
-
idempotencyKey,
|
|
252167
|
-
claimToken: instructionReceiverToken
|
|
252168
|
-
});
|
|
252169
|
-
return reply.code(404).send({ error: "Session not found or not running" });
|
|
252170
|
-
}
|
|
252171
|
-
await renew();
|
|
252172
|
-
if (ownershipLost) return reply.code(409).send({ error: "Instruction delivery ownership was lost" });
|
|
252173
|
-
const confirmed = await fastify2.storage.agentInstructionDeliveries.markSent({
|
|
252174
|
-
sessionId: req.params.sessionId,
|
|
252175
|
-
idempotencyKey,
|
|
252176
|
-
claimToken: instructionReceiverToken
|
|
252177
|
-
});
|
|
252178
|
-
if (!confirmed) {
|
|
252179
|
-
return reply.code(503).send({ error: "Instruction delivery could not be confirmed" });
|
|
252180
|
-
}
|
|
252181
|
-
return reply.code(200).send({ success: true });
|
|
252182
|
-
} catch (error48) {
|
|
252183
|
-
await fastify2.storage.agentInstructionDeliveries.release({
|
|
252184
|
-
sessionId: req.params.sessionId,
|
|
252185
|
-
idempotencyKey,
|
|
252186
|
-
claimToken: instructionReceiverToken
|
|
252187
|
-
});
|
|
252188
|
-
throw error48;
|
|
252189
|
-
} finally {
|
|
252190
|
-
clearInterval(heartbeat);
|
|
252191
|
-
}
|
|
252192
|
-
});
|
|
253493
|
+
case "not_running":
|
|
253494
|
+
return reply.code(404).send({ error: "Session not found or not running" });
|
|
253495
|
+
case "ownership_lost_before_send":
|
|
253496
|
+
case "ownership_lost_after_send":
|
|
253497
|
+
return reply.code(409).send({ error: "Instruction delivery ownership was lost" });
|
|
253498
|
+
case "unconfirmed":
|
|
253499
|
+
return reply.code(503).send({ error: "Instruction delivery could not be confirmed" });
|
|
253500
|
+
}
|
|
252193
253501
|
});
|
|
252194
253502
|
});
|
|
252195
253503
|
async function resolveUploadTargetRemoteInfo(sessionId, userId) {
|
|
@@ -252576,7 +253884,7 @@ var routes11 = async (fastify2) => {
|
|
|
252576
253884
|
messages
|
|
252577
253885
|
});
|
|
252578
253886
|
}
|
|
252579
|
-
const preSessionId =
|
|
253887
|
+
const preSessionId = randomUUID19();
|
|
252580
253888
|
const crossRemoteMcp = await mintCrossRemoteMcpConfig(
|
|
252581
253889
|
{ storage: fastify2.storage },
|
|
252582
253890
|
{ userId, sessionId: preSessionId, sourceRemoteServerId: null }
|
|
@@ -252953,6 +254261,15 @@ var routes12 = async (fastify2, opts) => {
|
|
|
252953
254261
|
};
|
|
252954
254262
|
const ALL_PURPOSES = ["interactive", "interactive_upload", "commander", "project_chat", "workflow_review"];
|
|
252955
254263
|
const CLIENT_PURPOSES = ["interactive", "interactive_upload"];
|
|
254264
|
+
async function checkGrants(value, userId, sourceRemoteServerId) {
|
|
254265
|
+
if (value === void 0) return { ok: true, ids: void 0 };
|
|
254266
|
+
if (!Array.isArray(value) || value.some((id) => typeof id !== "string")) {
|
|
254267
|
+
return { ok: false, error: "grantedRemoteIds must be an array of strings" };
|
|
254268
|
+
}
|
|
254269
|
+
const ids = [...new Set(value)];
|
|
254270
|
+
const rejection = await validateSessionGrantIds(fastify2.storage, ids, userId, sourceRemoteServerId);
|
|
254271
|
+
return rejection ? { ok: false, error: rejection } : { ok: true, ids };
|
|
254272
|
+
}
|
|
252956
254273
|
fastify2.post("/api/path/agent-sessions/prepare", async (req, reply) => {
|
|
252957
254274
|
const authResult = requireAuth(req, reply);
|
|
252958
254275
|
if (authResult === null) return;
|
|
@@ -253034,6 +254351,8 @@ var routes12 = async (fastify2, opts) => {
|
|
|
253034
254351
|
if (!validKey(req.body.operationId)) return reply.code(400).send({ error: "operationId must contain 1-512 characters" });
|
|
253035
254352
|
const purpose = purposeOf(req.body.purpose, CLIENT_PURPOSES);
|
|
253036
254353
|
if (!purpose) return reply.code(400).send({ error: "Invalid purpose" });
|
|
254354
|
+
const grants = await checkGrants(req.body.grantedRemoteIds, ctx.userId, ctx.remote?.serverId ?? null);
|
|
254355
|
+
if (!grants.ok) return reply.code(400).send({ error: grants.error });
|
|
253037
254356
|
const common = {
|
|
253038
254357
|
operationId: req.body.operationId,
|
|
253039
254358
|
branch: req.body.branch ?? null,
|
|
@@ -253054,14 +254373,16 @@ var routes12 = async (fastify2, opts) => {
|
|
|
253054
254373
|
projectId: ctx.project.id,
|
|
253055
254374
|
remoteServerId: ctx.remote.serverId,
|
|
253056
254375
|
remotePath: ctx.remote.path,
|
|
253057
|
-
userId: ctx.userId
|
|
254376
|
+
userId: ctx.userId,
|
|
254377
|
+
grantedRemoteIds: grants.ids
|
|
253058
254378
|
});
|
|
253059
254379
|
return send(reply, result2);
|
|
253060
254380
|
}
|
|
253061
254381
|
const result = await fastify2.agentSessionLifecycle.prepare({
|
|
253062
254382
|
...common,
|
|
253063
254383
|
sessionId: typeof req.body.sessionId === "string" ? req.body.sessionId : void 0,
|
|
253064
|
-
projectId: ctx.project.id
|
|
254384
|
+
projectId: ctx.project.id,
|
|
254385
|
+
grants: grants.ids && { userId: ctx.userId, remoteServerIds: grants.ids }
|
|
253065
254386
|
});
|
|
253066
254387
|
return send(reply, result);
|
|
253067
254388
|
});
|
|
@@ -253071,6 +254392,8 @@ var routes12 = async (fastify2, opts) => {
|
|
|
253071
254392
|
if (!validKey(req.body.operationId)) return reply.code(400).send({ error: "operationId must contain 1-512 characters" });
|
|
253072
254393
|
if (!validInstruction(req.body.instruction)) return reply.code(400).send({ error: "Instruction is required" });
|
|
253073
254394
|
if (req.body.purpose !== void 0 && req.body.purpose !== "interactive") return reply.code(400).send({ error: "Invalid purpose" });
|
|
254395
|
+
const grants = await checkGrants(req.body.grantedRemoteIds, ctx.userId, ctx.remote?.serverId ?? null);
|
|
254396
|
+
if (!grants.ok) return reply.code(400).send({ error: grants.error });
|
|
253074
254397
|
const common = {
|
|
253075
254398
|
operationId: req.body.operationId,
|
|
253076
254399
|
branch: req.body.branch ?? null,
|
|
@@ -253094,7 +254417,8 @@ var routes12 = async (fastify2, opts) => {
|
|
|
253094
254417
|
purpose: "interactive",
|
|
253095
254418
|
projectId: ctx.project.id,
|
|
253096
254419
|
remoteServerId: ctx.remote.serverId,
|
|
253097
|
-
remotePath: ctx.remote.path
|
|
254420
|
+
remotePath: ctx.remote.path,
|
|
254421
|
+
grantedRemoteIds: grants.ids
|
|
253098
254422
|
});
|
|
253099
254423
|
return send(reply, result2, fallback);
|
|
253100
254424
|
}
|
|
@@ -253105,24 +254429,13 @@ var routes12 = async (fastify2, opts) => {
|
|
|
253105
254429
|
purpose: "interactive",
|
|
253106
254430
|
sessionId,
|
|
253107
254431
|
projectId: ctx.project.id,
|
|
253108
|
-
crossRemoteMcp
|
|
254432
|
+
crossRemoteMcp,
|
|
254433
|
+
grants: grants.ids && { userId: ctx.userId, remoteServerIds: grants.ids }
|
|
253109
254434
|
});
|
|
253110
254435
|
return send(reply, result, fallback);
|
|
253111
254436
|
});
|
|
253112
|
-
|
|
253113
|
-
|
|
253114
|
-
if (!activity) return false;
|
|
253115
|
-
return Boolean(await fastify2.storage.projects.getById(activity.projectId, authResult));
|
|
253116
|
-
}
|
|
253117
|
-
async function authorizeRemote(sessionId, authResult) {
|
|
253118
|
-
const [intent, mapping] = await Promise.all([
|
|
253119
|
-
fastify2.storage.remoteSessionCreationIntents.getByLocal(sessionId),
|
|
253120
|
-
fastify2.storage.remoteSessionMappings.getByLocal(sessionId)
|
|
253121
|
-
]);
|
|
253122
|
-
const projectId = mapping?.project_id ?? intent?.project_id;
|
|
253123
|
-
if (!projectId) return false;
|
|
253124
|
-
return Boolean(await fastify2.storage.projects.getById(projectId, authResult));
|
|
253125
|
-
}
|
|
254437
|
+
const authorizeLocal = (sessionId, authResult) => authorizeLocalSession(fastify2.storage, sessionId, authResult);
|
|
254438
|
+
const authorizeRemote = (sessionId, authResult) => authorizeRemoteSession(fastify2.storage, sessionId, authResult);
|
|
253126
254439
|
fastify2.post("/api/agent-sessions/:sessionId/activate", { bodyLimit: 10 * 1024 * 1024 }, async (req, reply) => {
|
|
253127
254440
|
const authResult = requireUserFacingUserId(req, reply);
|
|
253128
254441
|
if (authResult === null) return;
|
|
@@ -253139,19 +254452,29 @@ var routes12 = async (fastify2, opts) => {
|
|
|
253139
254452
|
if (sessionId.startsWith("remote-")) {
|
|
253140
254453
|
if (!await authorizeRemote(sessionId, authResult)) return reply.code(404).send({ kind: "not_found", error: "Session not found" });
|
|
253141
254454
|
const intent = await fastify2.storage.remoteSessionCreationIntents.getByLocal(sessionId);
|
|
253142
|
-
|
|
254455
|
+
const remoteGrants = await checkGrants(req.body.grantedRemoteIds, authResult, intent?.remote_server_id ?? null);
|
|
254456
|
+
if (!remoteGrants.ok) return reply.code(400).send({ error: remoteGrants.error });
|
|
254457
|
+
return send(reply, await remote().activate({
|
|
254458
|
+
...common,
|
|
254459
|
+
localSessionId: sessionId,
|
|
254460
|
+
userId: authResult,
|
|
254461
|
+
grantedRemoteIds: remoteGrants.ids
|
|
254462
|
+
}), {
|
|
253143
254463
|
permissionMode: intent?.permission_mode ?? "edit",
|
|
253144
254464
|
agentType: intent?.agent_type ?? "claude-code",
|
|
253145
254465
|
model: intent?.model ?? null
|
|
253146
254466
|
});
|
|
253147
254467
|
}
|
|
253148
254468
|
if (!await authorizeLocal(sessionId, authResult)) return reply.code(404).send({ kind: "not_found", error: "Session not found" });
|
|
254469
|
+
const grants = await checkGrants(req.body.grantedRemoteIds, authResult, null);
|
|
254470
|
+
if (!grants.ok) return reply.code(400).send({ error: grants.error });
|
|
253149
254471
|
const crossRemoteMcp = crossRemoteOf(req.body.crossRemoteMcp) ?? await mintCrossRemoteMcpConfig({ storage: fastify2.storage }, { userId: authResult, sessionId, sourceRemoteServerId: null }).catch(() => void 0);
|
|
253150
254472
|
const result = await fastify2.agentSessionLifecycle.activate({
|
|
253151
254473
|
...common,
|
|
253152
254474
|
sessionId,
|
|
253153
254475
|
crossRemoteMcp,
|
|
253154
|
-
userId: resolveUserId(authResult)
|
|
254476
|
+
userId: resolveUserId(authResult),
|
|
254477
|
+
grants: grants.ids && { userId: resolveUserId(authResult), remoteServerIds: grants.ids }
|
|
253155
254478
|
});
|
|
253156
254479
|
return send(reply, result, { permissionMode: "edit", agentType: "claude-code", model: null });
|
|
253157
254480
|
});
|
|
@@ -253623,7 +254946,7 @@ var chat_session_routes_default = (0, import_fastify_plugin16.default)(routes15,
|
|
|
253623
254946
|
|
|
253624
254947
|
// src/routes/project-chat-routes.ts
|
|
253625
254948
|
var import_fastify_plugin17 = __toESM(require_plugin2(), 1);
|
|
253626
|
-
import { createHash as
|
|
254949
|
+
import { createHash as createHash9, randomUUID as randomUUID20 } from "crypto";
|
|
253627
254950
|
var MAX_TITLE_LENGTH = 200;
|
|
253628
254951
|
var MAX_MESSAGE_LENGTH = 1e5;
|
|
253629
254952
|
var THREAD_PAGE_LIMIT = 50;
|
|
@@ -253791,20 +255114,20 @@ var routes16 = async (fastify2) => {
|
|
|
253791
255114
|
if (!project) return reply.code(404).send({ error: "Project not found" });
|
|
253792
255115
|
const body = parseCreateBody(req.body);
|
|
253793
255116
|
if (!body) return reply.code(400).send({ error: "Body must contain only an optional non-empty message" });
|
|
253794
|
-
const createRequestId = body.createRequestId ??
|
|
253795
|
-
const createPayloadHash =
|
|
255117
|
+
const createRequestId = body.createRequestId ?? randomUUID20();
|
|
255118
|
+
const createPayloadHash = createHash9("sha256").update(JSON.stringify({ message: body.message ?? null })).digest("hex");
|
|
253796
255119
|
let accepted;
|
|
253797
255120
|
try {
|
|
253798
255121
|
accepted = await fastify2.storage.projectChatThreads.createIdempotent({
|
|
253799
|
-
id:
|
|
255122
|
+
id: randomUUID20(),
|
|
253800
255123
|
project_id: projectId,
|
|
253801
255124
|
user_id: userId,
|
|
253802
255125
|
title: null,
|
|
253803
255126
|
create_request_id: createRequestId,
|
|
253804
255127
|
create_payload_hash: createPayloadHash,
|
|
253805
255128
|
...body.message !== void 0 ? { initialTurn: {
|
|
253806
|
-
messageId:
|
|
253807
|
-
workItemId:
|
|
255129
|
+
messageId: randomUUID20(),
|
|
255130
|
+
workItemId: randomUUID20(),
|
|
253808
255131
|
content: body.message
|
|
253809
255132
|
} } : {}
|
|
253810
255133
|
});
|
|
@@ -254085,7 +255408,7 @@ var project_activity_routes_default = (0, import_fastify_plugin18.default)(route
|
|
|
254085
255408
|
|
|
254086
255409
|
// src/routes/task-routes.ts
|
|
254087
255410
|
var import_fastify_plugin19 = __toESM(require_plugin2(), 1);
|
|
254088
|
-
import { randomUUID as
|
|
255411
|
+
import { randomUUID as randomUUID21 } from "crypto";
|
|
254089
255412
|
var routes18 = async (fastify2) => {
|
|
254090
255413
|
fastify2.get(
|
|
254091
255414
|
"/api/projects/:projectId/tasks",
|
|
@@ -254149,7 +255472,7 @@ Description: ${description}`,
|
|
|
254149
255472
|
title = description.length > 50 ? description.slice(0, 50) + "..." : description;
|
|
254150
255473
|
}
|
|
254151
255474
|
}
|
|
254152
|
-
const id =
|
|
255475
|
+
const id = randomUUID21();
|
|
254153
255476
|
const task = await fastify2.storage.tasks.create({
|
|
254154
255477
|
id,
|
|
254155
255478
|
project_id: req.params.projectId,
|
|
@@ -254250,7 +255573,7 @@ var task_routes_default = (0, import_fastify_plugin19.default)(routes18, { name:
|
|
|
254250
255573
|
|
|
254251
255574
|
// src/routes/rule-routes.ts
|
|
254252
255575
|
var import_fastify_plugin20 = __toESM(require_plugin2(), 1);
|
|
254253
|
-
import { randomUUID as
|
|
255576
|
+
import { randomUUID as randomUUID22 } from "crypto";
|
|
254254
255577
|
var routes19 = async (fastify2) => {
|
|
254255
255578
|
fastify2.get(
|
|
254256
255579
|
"/api/projects/:projectId/rules",
|
|
@@ -254277,7 +255600,7 @@ var routes19 = async (fastify2) => {
|
|
|
254277
255600
|
if (!name25 || !content) {
|
|
254278
255601
|
return reply.code(400).send({ error: "name and content are required" });
|
|
254279
255602
|
}
|
|
254280
|
-
const id =
|
|
255603
|
+
const id = randomUUID22();
|
|
254281
255604
|
const rule = await fastify2.storage.rules.create({
|
|
254282
255605
|
id,
|
|
254283
255606
|
project_id: req.params.projectId,
|
|
@@ -254341,7 +255664,7 @@ var rule_routes_default = (0, import_fastify_plugin20.default)(routes19, { name:
|
|
|
254341
255664
|
|
|
254342
255665
|
// src/routes/command-routes.ts
|
|
254343
255666
|
var import_fastify_plugin21 = __toESM(require_plugin2(), 1);
|
|
254344
|
-
import { randomUUID as
|
|
255667
|
+
import { randomUUID as randomUUID23 } from "crypto";
|
|
254345
255668
|
var routes20 = async (fastify2) => {
|
|
254346
255669
|
fastify2.get(
|
|
254347
255670
|
"/api/projects/:projectId/commands",
|
|
@@ -254368,7 +255691,7 @@ var routes20 = async (fastify2) => {
|
|
|
254368
255691
|
if (!name25 || !content) {
|
|
254369
255692
|
return reply.code(400).send({ error: "name and content are required" });
|
|
254370
255693
|
}
|
|
254371
|
-
const id =
|
|
255694
|
+
const id = randomUUID23();
|
|
254372
255695
|
const command = await fastify2.storage.commands.create({
|
|
254373
255696
|
id,
|
|
254374
255697
|
project_id: req.params.projectId,
|
|
@@ -254419,6 +255742,15 @@ function parseReviewerAgentType(raw) {
|
|
|
254419
255742
|
if (raw === void 0) return void 0;
|
|
254420
255743
|
return typeof raw === "string" && REVIEWER_AGENT_TYPES.has(raw) ? raw : null;
|
|
254421
255744
|
}
|
|
255745
|
+
function parseLoop(raw) {
|
|
255746
|
+
if (raw === void 0 || raw === null) return void 0;
|
|
255747
|
+
if (typeof raw !== "object") return null;
|
|
255748
|
+
const maxRounds = raw.maxRounds;
|
|
255749
|
+
if (maxRounds === void 0) return { maxRounds: LOOP_MAX_ROUNDS_DEFAULT };
|
|
255750
|
+
if (typeof maxRounds !== "number" || !Number.isInteger(maxRounds) || maxRounds < 1 || maxRounds > LOOP_MAX_ROUNDS_LIMIT) return null;
|
|
255751
|
+
return { maxRounds };
|
|
255752
|
+
}
|
|
255753
|
+
var LOOP_ERROR = `loop.maxRounds must be an integer between 1 and ${LOOP_MAX_ROUNDS_LIMIT}`;
|
|
254422
255754
|
function parseReviewSpan(raw) {
|
|
254423
255755
|
if (raw === void 0) return "this_turn";
|
|
254424
255756
|
return raw === "this_turn" || raw === "session_start" ? raw : null;
|
|
@@ -254644,6 +255976,8 @@ async function routes21(fastify2) {
|
|
|
254644
255976
|
if (reviewerAgentType === null) return reply.code(400).send({ error: "reviewerAgentType must be one of: claude-code, codex" });
|
|
254645
255977
|
const reviewSpan = parseReviewSpan(req.body?.reviewSpan);
|
|
254646
255978
|
if (reviewSpan === null) return reply.code(400).send({ error: "reviewSpan must be one of: this_turn, session_start" });
|
|
255979
|
+
const loop = parseLoop(req.body?.loop);
|
|
255980
|
+
if (loop === null) return reply.code(400).send({ error: LOOP_ERROR });
|
|
254647
255981
|
const reviewerSessionIdRaw = req.body?.reviewerSessionId;
|
|
254648
255982
|
if (reviewerSessionIdRaw !== void 0 && (typeof reviewerSessionIdRaw !== "string" || reviewerSessionIdRaw.trim() === "")) {
|
|
254649
255983
|
return reply.code(400).send({ error: "reviewerSessionId must be a non-empty string" });
|
|
@@ -254701,7 +256035,10 @@ async function routes21(fastify2) {
|
|
|
254701
256035
|
sourceTurnEndIndex,
|
|
254702
256036
|
reviewSpan,
|
|
254703
256037
|
reviewerSessionId: bareReviewerSessionId,
|
|
254704
|
-
intentBrief: clientBrief
|
|
256038
|
+
intentBrief: clientBrief,
|
|
256039
|
+
// Additive tunnel field: a worker that predates review loops ignores
|
|
256040
|
+
// it and runs a single-pass review (the run comes back without loop_id).
|
|
256041
|
+
loop
|
|
254705
256042
|
});
|
|
254706
256043
|
if (!result.ok) return sendProxyFailure(reply, result);
|
|
254707
256044
|
bareRun = result.data.run;
|
|
@@ -254741,6 +256078,7 @@ async function routes21(fastify2) {
|
|
|
254741
256078
|
reviewContextMode,
|
|
254742
256079
|
reviewerAgentType: reviewerAgentType ?? "claude-code",
|
|
254743
256080
|
intentBrief,
|
|
256081
|
+
loopMaxRounds: loop?.maxRounds,
|
|
254744
256082
|
userId,
|
|
254745
256083
|
...twoPhase ? { phase: "prepare" } : {}
|
|
254746
256084
|
});
|
|
@@ -254807,7 +256145,8 @@ async function routes21(fastify2) {
|
|
|
254807
256145
|
reviewFocus,
|
|
254808
256146
|
sourceTurnEndIndex,
|
|
254809
256147
|
reviewSpan,
|
|
254810
|
-
reviewerAgentType
|
|
256148
|
+
reviewerAgentType,
|
|
256149
|
+
loop
|
|
254811
256150
|
});
|
|
254812
256151
|
} catch (err) {
|
|
254813
256152
|
const status = errStatus(err);
|
|
@@ -254835,7 +256174,8 @@ async function routes21(fastify2) {
|
|
|
254835
256174
|
reviewSpan,
|
|
254836
256175
|
reviewerSessionId,
|
|
254837
256176
|
intentBrief: clientBrief,
|
|
254838
|
-
blind
|
|
256177
|
+
blind,
|
|
256178
|
+
loop
|
|
254839
256179
|
});
|
|
254840
256180
|
return reply.code(201).send({ run: run2 });
|
|
254841
256181
|
} catch (err) {
|
|
@@ -255019,7 +256359,15 @@ async function routes21(fastify2) {
|
|
|
255019
256359
|
const run2 = await fastify2.workflowEngine.requestFinalVerdict(req.params.id);
|
|
255020
256360
|
return reply.send({ run: run2 });
|
|
255021
256361
|
}
|
|
255022
|
-
|
|
256362
|
+
if (action === "accept") {
|
|
256363
|
+
const run2 = await fastify2.workflowEngine.acceptResult(req.params.id);
|
|
256364
|
+
return reply.send({ run: run2 });
|
|
256365
|
+
}
|
|
256366
|
+
if (action === "rereview") {
|
|
256367
|
+
const run2 = await fastify2.workflowEngine.approveRereview(req.params.id, { extend: req.body?.extend === true });
|
|
256368
|
+
return reply.send({ run: run2 });
|
|
256369
|
+
}
|
|
256370
|
+
return reply.code(400).send({ error: "action must be approve, cancel, finalize, accept or rereview" });
|
|
255023
256371
|
} catch (err) {
|
|
255024
256372
|
const status = errStatus(err);
|
|
255025
256373
|
if (status) return reply.code(status).send({ error: err.message });
|
|
@@ -255060,6 +256408,8 @@ async function routes21(fastify2) {
|
|
|
255060
256408
|
if (!sourceSessionId) return reply.code(400).send({ error: "sourceSessionId is required" });
|
|
255061
256409
|
const reviewSpan = parseReviewSpan(req.body?.reviewSpan);
|
|
255062
256410
|
if (reviewSpan === null) return reply.code(400).send({ error: "reviewSpan must be one of: this_turn, session_start" });
|
|
256411
|
+
const loop = parseLoop(req.body?.loop);
|
|
256412
|
+
if (loop === null) return reply.code(400).send({ error: LOOP_ERROR });
|
|
255063
256413
|
const reviewContextMode = parseReviewContextMode(req.body?.reviewContextMode);
|
|
255064
256414
|
if (reviewContextMode === null) return reply.code(400).send({ error: "reviewContextMode must be one of: briefed, blind" });
|
|
255065
256415
|
const blind = reviewContextMode === "blind";
|
|
@@ -255106,6 +256456,7 @@ async function routes21(fastify2) {
|
|
|
255106
256456
|
reviewerSessionId,
|
|
255107
256457
|
intentBrief,
|
|
255108
256458
|
blind,
|
|
256459
|
+
loop,
|
|
255109
256460
|
runId: runId || void 0,
|
|
255110
256461
|
newReviewerSessionId: newReviewerSessionId || void 0
|
|
255111
256462
|
});
|
|
@@ -255135,6 +256486,8 @@ async function routes21(fastify2) {
|
|
|
255135
256486
|
if (!sourceSessionId) return reply.code(400).send({ error: "sourceSessionId is required" });
|
|
255136
256487
|
const reviewSpan = parseReviewSpan(req.body?.reviewSpan);
|
|
255137
256488
|
if (reviewSpan === null) return reply.code(400).send({ error: "reviewSpan must be one of: this_turn, session_start" });
|
|
256489
|
+
const loop = parseLoop(req.body?.loop);
|
|
256490
|
+
if (loop === null) return reply.code(400).send({ error: LOOP_ERROR });
|
|
255138
256491
|
const reviewerAgentType = parseReviewerAgentType(req.body?.reviewerAgentType);
|
|
255139
256492
|
if (reviewerAgentType === null) return reply.code(400).send({ error: "reviewerAgentType must be one of: claude-code, codex" });
|
|
255140
256493
|
const runId = typeof req.body?.runId === "string" ? req.body.runId.trim() : "";
|
|
@@ -255161,6 +256514,7 @@ async function routes21(fastify2) {
|
|
|
255161
256514
|
sourceTurnEndIndex,
|
|
255162
256515
|
reviewSpan,
|
|
255163
256516
|
reviewerAgentType,
|
|
256517
|
+
loop,
|
|
255164
256518
|
runId,
|
|
255165
256519
|
newReviewerSessionId
|
|
255166
256520
|
});
|
|
@@ -255660,7 +257014,7 @@ var translate_routes_default = (0, import_fastify_plugin24.default)(routes23, {
|
|
|
255660
257014
|
var import_fastify_plugin25 = __toESM(require_plugin2(), 1);
|
|
255661
257015
|
|
|
255662
257016
|
// src/routes/executor-stream-handlers.ts
|
|
255663
|
-
import { randomUUID as
|
|
257017
|
+
import { randomUUID as randomUUID24 } from "crypto";
|
|
255664
257018
|
function attachLocalProcessStream(fastify2, processId, send, onTerminal) {
|
|
255665
257019
|
const noop4 = { cleanup: () => {
|
|
255666
257020
|
}, handleInput: () => {
|
|
@@ -255736,7 +257090,7 @@ function attachRemoteProcessStream(fastify2, processId, send, onTerminal) {
|
|
|
255736
257090
|
return;
|
|
255737
257091
|
}
|
|
255738
257092
|
console.log(`[diag:remote-stop] ${(/* @__PURE__ */ new Date()).toISOString()} attach processId=${processId} executorId=${info.executorId} server=${info.remoteServerId} transport=reverse-connect remoteProcessId=${info.remoteProcessId}`);
|
|
255739
|
-
const channelId =
|
|
257093
|
+
const channelId = randomUUID24();
|
|
255740
257094
|
const wsPath = `/api/executor-processes/${info.remoteProcessId}/logs`;
|
|
255741
257095
|
const adapter = new VirtualWsAdapter(
|
|
255742
257096
|
(data) => fastify2.reverseConnectManager.sendChannelData(info.remoteServerId, channelId, data),
|
|
@@ -255817,13 +257171,18 @@ function attachRemoteProcessStream(fastify2, processId, send, onTerminal) {
|
|
|
255817
257171
|
remoteWs.on("close", async () => {
|
|
255818
257172
|
clearInterval(pingInterval);
|
|
255819
257173
|
if (!terminalSignalSent) {
|
|
257174
|
+
let row;
|
|
255820
257175
|
try {
|
|
255821
|
-
|
|
255822
|
-
console.log(`[diag:remote-stop] ${(/* @__PURE__ */ new Date()).toISOString()} upstream CLOSE without real finished \u2192 FABRICATING finished processId=${processId} executorId=${info.executorId} transport=reverse-connect dbStatus=${row?.status} dbExitCode=${row?.exit_code ?? "null"} sentExitCode=${row?.exit_code ?? 0} \u2014 THIS flips UI to Stopped while remote process may still be running`);
|
|
255823
|
-
send({ type: "finished", exitCode: row?.exit_code ?? 0 });
|
|
257176
|
+
row = await fastify2.storage.remoteExecutorProcesses.getById(processId);
|
|
255824
257177
|
} catch (error48) {
|
|
255825
257178
|
console.error(`[ExecutorStream] Failed to fetch process row on close:`, error48);
|
|
255826
|
-
|
|
257179
|
+
}
|
|
257180
|
+
if (row && row.status !== "running") {
|
|
257181
|
+
console.log(`[diag:remote-stop] ${(/* @__PURE__ */ new Date()).toISOString()} upstream CLOSE without real finished, row already terminal processId=${processId} dbStatus=${row.status} dbExitCode=${row.exit_code ?? "null"}`);
|
|
257182
|
+
send({ type: "finished", exitCode: row.exit_code ?? 0 });
|
|
257183
|
+
} else {
|
|
257184
|
+
console.log(`[diag:remote-stop] ${(/* @__PURE__ */ new Date()).toISOString()} upstream CLOSE without real finished processId=${processId} executorId=${info.executorId} dbStatus=${row?.status ?? "missing"} \u2014 sending retryable error, process state unchanged`);
|
|
257185
|
+
send({ type: "error", message: "Remote connection lost", retryable: true });
|
|
255827
257186
|
}
|
|
255828
257187
|
terminalSignalSent = true;
|
|
255829
257188
|
} else {
|
|
@@ -256499,7 +257858,7 @@ var websocket_routes_default = (0, import_fastify_plugin25.default)(routes24, {
|
|
|
256499
257858
|
|
|
256500
257859
|
// src/routes/reverse-connect-routes.ts
|
|
256501
257860
|
var import_fastify_plugin26 = __toESM(require_plugin2(), 1);
|
|
256502
|
-
import { randomBytes as randomBytes4, createHash as
|
|
257861
|
+
import { randomBytes as randomBytes4, createHash as createHash10, verify as cryptoVerify } from "crypto";
|
|
256503
257862
|
|
|
256504
257863
|
// src/utils/rate-limited-warn.ts
|
|
256505
257864
|
function createRateLimitedWarn(windowMs, maxKeys) {
|
|
@@ -256649,7 +258008,7 @@ var routes25 = async (fastify2) => {
|
|
|
256649
258008
|
socket.close(4003, "Bad machine signature");
|
|
256650
258009
|
return;
|
|
256651
258010
|
}
|
|
256652
|
-
const fingerprint =
|
|
258011
|
+
const fingerprint = createHash10("sha256").update(publicKey).digest("hex");
|
|
256653
258012
|
const { owned, created } = await fastify2.storage.machineIdentity.claimOrVerify(fingerprint, publicKey, ownerId);
|
|
256654
258013
|
if (!owned) {
|
|
256655
258014
|
console.warn(`[ReverseConnect] Machine ${fingerprint.slice(0, 12)} owner mismatch \u2014 rejecting ${serverId}`);
|
|
@@ -257016,7 +258375,7 @@ var browser_routes_default = (0, import_fastify_plugin29.default)(routes28, { na
|
|
|
257016
258375
|
|
|
257017
258376
|
// src/routes/browser-proxy-routes.ts
|
|
257018
258377
|
var import_fastify_plugin30 = __toESM(require_plugin2(), 1);
|
|
257019
|
-
import { randomUUID as
|
|
258378
|
+
import { randomUUID as randomUUID25 } from "crypto";
|
|
257020
258379
|
|
|
257021
258380
|
// src/utils/ssrf-guard.ts
|
|
257022
258381
|
var import_undici2 = __toESM(require_undici(), 1);
|
|
@@ -257606,7 +258965,7 @@ var routes29 = async (fastify2) => {
|
|
|
257606
258965
|
const rcm = fastify2.reverseConnectManager;
|
|
257607
258966
|
if (resolved.remoteServerId && rcm.isConnected(resolved.remoteServerId)) {
|
|
257608
258967
|
const remoteServerId = resolved.remoteServerId;
|
|
257609
|
-
const channelId =
|
|
258968
|
+
const channelId = randomUUID25();
|
|
257610
258969
|
const parsed = new URL(resolved.fetchUrl);
|
|
257611
258970
|
const wsPath = parsed.pathname;
|
|
257612
258971
|
const wsQuery = parsed.search ? parsed.search.slice(1) : void 0;
|
|
@@ -257758,7 +259117,7 @@ function runOneShot(command, opts) {
|
|
|
257758
259117
|
}
|
|
257759
259118
|
|
|
257760
259119
|
// src/remote-mcp-session-manager.ts
|
|
257761
|
-
import { randomUUID as
|
|
259120
|
+
import { randomUUID as randomUUID26 } from "node:crypto";
|
|
257762
259121
|
|
|
257763
259122
|
// src/protocol/mcp/client.ts
|
|
257764
259123
|
var McpClientError = class extends Error {
|
|
@@ -262832,7 +264191,7 @@ var RemoteMcpSessionManager = class {
|
|
|
262832
264191
|
try {
|
|
262833
264192
|
const tools = await client.listTools(clampTimeout(timeoutMs));
|
|
262834
264193
|
if (generation !== this.generation) throw new McpClientError("MCP broker was reset while opening");
|
|
262835
|
-
const workerHandle =
|
|
264194
|
+
const workerHandle = randomUUID26();
|
|
262836
264195
|
this.sessions.set(workerHandle, { client, transport: spec.type, serverInfo, tools, lastUsedAt: Date.now() });
|
|
262837
264196
|
return { workerHandle, transport: spec.type, serverInfo, tools };
|
|
262838
264197
|
} catch (error48) {
|
|
@@ -263087,13 +264446,16 @@ var import_fastify_plugin32 = __toESM(require_plugin2(), 1);
|
|
|
263087
264446
|
var PROTOCOL_VERSION = "2024-11-05";
|
|
263088
264447
|
var AUDIT_ARGS_MAX = 1024;
|
|
263089
264448
|
var NOT_ACCESSIBLE = "remote not found or not accessible";
|
|
264449
|
+
var NOT_GRANTED = "remote not granted to this session; ask the user to allow it from the composer's + menu (Remote access)";
|
|
263090
264450
|
var REMOTE_ID_PROP = {
|
|
263091
264451
|
remoteId: { type: "string", description: "Target remote server id from list_accessible_remotes" }
|
|
263092
264452
|
};
|
|
263093
264453
|
var CROSS_REMOTE_MCP_INSTRUCTIONS = [
|
|
263094
264454
|
"Use these tools when the task requires inspecting or operating another remote machine, or using an MCP server reachable from that remote.",
|
|
263095
264455
|
"Treat a machine or host name in the user's request (for example, 'look at the ubuntu machine') as an explicit target signal, not as a request to inspect the current local workspace. Call `list_accessible_remotes` and match the user's wording against the returned remote names and ids before reading files or running local commands.",
|
|
263096
|
-
"If exactly one accessible remote matches the named machine, perform the requested
|
|
264456
|
+
"If exactly one accessible remote matches the named machine, perform the requested inspection on that remote. If multiple remotes match, or the wording could reasonably refer to either the local machine or a remote, ask the user which target they mean before operating.",
|
|
264457
|
+
"Naming a machine targets the investigation, not the fix: make code changes (editing files, git operations, installing dependencies) in the local workspace, even when the problem was found on a remote. Only modify a remote machine when the user explicitly asks for the change to be made there; if a fix can only be applied on the remote, propose it and ask first.",
|
|
264458
|
+
"The user grants remotes to this session from the composer's + menu (Remote access). If the list is empty, or the machine the user named is missing from it, tell them to grant it there; never silently fall back to the local workspace.",
|
|
263097
264459
|
"Cross-remote can discover accessible machines, inspect files, directories, paths, and processes, run commands on exec-tier remotes, and persistently use MCP servers reachable from those remotes. Available operations depend on the remote's access tier, online state, and worker capabilities.",
|
|
263098
264460
|
"Call `list_accessible_remotes` first to discover the remote id, access tier, online state, and whether its MCP broker is supported.",
|
|
263099
264461
|
"For a remote MCP server, call `remote_mcp_open` once, use the returned tool schemas and handle for repeated `remote_mcp_call` calls, then call `remote_mcp_close` when the work is complete. Do not reopen the MCP server for every tool call.",
|
|
@@ -263104,7 +264466,7 @@ var CROSS_REMOTE_MCP_INSTRUCTIONS = [
|
|
|
263104
264466
|
var TOOLS = [
|
|
263105
264467
|
{
|
|
263106
264468
|
name: "list_accessible_remotes",
|
|
263107
|
-
description: "List remote machines
|
|
264469
|
+
description: "List the remote machines the user has granted to this session, including their names, ids, access tiers, and online status. Call this before acting whenever the user names or otherwise identifies a machine/host, so the request is routed to the intended target instead of the current local machine.",
|
|
263108
264470
|
inputSchema: { type: "object", properties: {}, additionalProperties: false }
|
|
263109
264471
|
},
|
|
263110
264472
|
{
|
|
@@ -263146,7 +264508,7 @@ var TOOLS = [
|
|
|
263146
264508
|
},
|
|
263147
264509
|
{
|
|
263148
264510
|
name: "remote_bash",
|
|
263149
|
-
description: "Run a shell command on a target remote machine. Requires 'exec' access.",
|
|
264511
|
+
description: "Run a shell command on a target remote machine. Requires 'exec' access. Use it for inspection and diagnosis; do not edit code or change state on the remote unless the user explicitly asked for that change there.",
|
|
263150
264512
|
inputSchema: {
|
|
263151
264513
|
type: "object",
|
|
263152
264514
|
properties: {
|
|
@@ -263425,7 +264787,8 @@ var routes31 = async (fastify2) => {
|
|
|
263425
264787
|
if (!resolved.ok) {
|
|
263426
264788
|
const status = resolved.reason === "offline" ? "offline" : "denied";
|
|
263427
264789
|
await audit(payload, remoteId, toolName, target.summary, status, null, startedAt);
|
|
263428
|
-
|
|
264790
|
+
if (resolved.reason === "offline") return textResult(`Remote ${remoteId} is offline`, true);
|
|
264791
|
+
return textResult(resolved.reason === "not_granted" ? NOT_GRANTED : NOT_ACCESSIBLE, true);
|
|
263429
264792
|
}
|
|
263430
264793
|
if (isMcpTool && !supportsRemoteMcpBroker(resolved.server)) {
|
|
263431
264794
|
await audit(payload, remoteId, toolName, target.summary, "denied", null, startedAt);
|
|
@@ -263606,7 +264969,7 @@ var session_mcp_routes_default = (0, import_fastify_plugin33.default)(routes32,
|
|
|
263606
264969
|
|
|
263607
264970
|
// src/routes/schedule-routes.ts
|
|
263608
264971
|
var import_fastify_plugin34 = __toESM(require_plugin2(), 1);
|
|
263609
|
-
import { randomUUID as
|
|
264972
|
+
import { randomUUID as randomUUID27 } from "crypto";
|
|
263610
264973
|
import path20 from "path";
|
|
263611
264974
|
var RUN_TYPES = ["command", "prompt"];
|
|
263612
264975
|
var CWD_MODES = ["branch", "directory"];
|
|
@@ -263709,7 +265072,7 @@ var routes33 = async (fastify2) => {
|
|
|
263709
265072
|
if (!owned) return reply.code(400).send({ error: "Invalid source" });
|
|
263710
265073
|
source = { session_id: b2.source.session_id, tool_use_id: b2.source.tool_use_id };
|
|
263711
265074
|
}
|
|
263712
|
-
const newId =
|
|
265075
|
+
const newId = randomUUID27();
|
|
263713
265076
|
const schedule = await fastify2.storage.scheduledTasks.create({
|
|
263714
265077
|
id: newId,
|
|
263715
265078
|
project_id: req.params.projectId,
|
|
@@ -264607,7 +265970,7 @@ var createServer = async (opts) => {
|
|
|
264607
265970
|
reply.header("access-control-allow-origin", "*");
|
|
264608
265971
|
reply.header("access-control-allow-methods", "GET, POST, PUT, DELETE, OPTIONS");
|
|
264609
265972
|
reply.header("access-control-allow-headers", "Content-Type, Upgrade, Connection, X-Vibedeckx-Api-Key, X-Request-Id, Authorization, traceparent");
|
|
264610
|
-
reply.header("access-control-expose-headers", "traceparent");
|
|
265973
|
+
reply.header("access-control-expose-headers", "traceparent, X-Vibedeckx-Source-Server");
|
|
264611
265974
|
done();
|
|
264612
265975
|
});
|
|
264613
265976
|
server.addHook("onRequest", (req, reply, done) => {
|
|
@@ -264647,7 +266010,12 @@ var createServer = async (opts) => {
|
|
|
264647
266010
|
// check this before calling /api/reverse-connect/identity so an auth
|
|
264648
266011
|
// middleware 401 from an older hub (which lacks the endpoint AND its
|
|
264649
266012
|
// exemptions) is never confused with a genuinely rejected token.
|
|
264650
|
-
reverseConnectIdentity: true
|
|
266013
|
+
reverseConnectIdentity: true,
|
|
266014
|
+
// Per-session cross-remote grants (the composer's "Remote access").
|
|
266015
|
+
// Off without a public URL (no gateway token is minted at all) and off in
|
|
266016
|
+
// solo/no-auth mode (the mint refuses an unscoped token), so the UI hides
|
|
266017
|
+
// the entry rather than offering a control that cannot do anything.
|
|
266018
|
+
crossRemoteSessionGrants: crossRemoteMcpEnabled() && authEnabled
|
|
264651
266019
|
}));
|
|
264652
266020
|
server.register(shared_services_default, { storage: opts.storage, authEnabled });
|
|
264653
266021
|
server.register(import_websocket2.default);
|
|
@@ -265474,7 +266842,7 @@ var ReverseConnectClient = class {
|
|
|
265474
266842
|
|
|
265475
266843
|
// src/connect-daemon.ts
|
|
265476
266844
|
import { spawn as spawn6 } from "node:child_process";
|
|
265477
|
-
import { randomUUID as
|
|
266845
|
+
import { randomUUID as randomUUID28 } from "node:crypto";
|
|
265478
266846
|
import fs8 from "node:fs";
|
|
265479
266847
|
import path23 from "node:path";
|
|
265480
266848
|
var CONNECT_DAEMON_CHILD_ENV = "VIBEDECKX_INTERNAL_CONNECT_DAEMON";
|
|
@@ -265893,7 +267261,7 @@ function createDaemonStateLockCandidate(lockPath) {
|
|
|
265893
267261
|
schemaVersion: 1,
|
|
265894
267262
|
pid: process.pid,
|
|
265895
267263
|
processStartTicks,
|
|
265896
|
-
nonce:
|
|
267264
|
+
nonce: randomUUID28()
|
|
265897
267265
|
};
|
|
265898
267266
|
const candidatePath = `${lockPath}.candidate-${process.pid}-${owner.nonce}`;
|
|
265899
267267
|
fs8.mkdirSync(candidatePath, { mode: 448 });
|