@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.
Files changed (2) hide show
  1. package/dist/bin.js +1845 -477
  2. 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(serialize) {
29488
+ constructor(serialize2) {
29489
29489
  super({ objectMode: true });
29490
- this.serialize = 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(serialize, deserialize2) {
29523
+ constructor(serialize2, deserialize2) {
29524
29524
  super({ objectMode: true });
29525
- this.serialize = 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, serialize, deserialize2, argument, metadata, options, callback) {
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: serialize,
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, serialize, deserialize2, metadata, options, callback) {
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: serialize,
30175
+ requestSerialize: serialize2,
30176
30176
  responseDeserialize: deserialize2
30177
30177
  };
30178
30178
  let callProperties = {
30179
30179
  metadata: checkedArguments.metadata,
30180
- call: new call_1.ClientWritableStreamImpl(serialize),
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, serialize, deserialize2, argument, metadata, options) {
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: serialize,
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, serialize, deserialize2, metadata, options) {
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: serialize,
30324
+ requestSerialize: serialize2,
30325
30325
  responseDeserialize: deserialize2
30326
30326
  };
30327
30327
  let callProperties = {
30328
30328
  metadata: checkedArguments.metadata,
30329
- call: new call_1.ClientDuplexStreamImpl(serialize, deserialize2),
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 serialize = attrs.requestSerialize;
30421
+ const serialize2 = attrs.requestSerialize;
30422
30422
  const deserialize2 = attrs.responseDeserialize;
30423
- const methodFunc = partial2(requesterFuncs[methodType], attrs.path, serialize, deserialize2);
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, serialize, deserialize2) {
30434
+ function partial2(fn, path26, serialize2, deserialize2) {
30435
30435
  return function(...args) {
30436
- return fn.call(this, path26, serialize, deserialize2, ...args);
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 serialize(arg) {
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, serialize, deserialize2, type) {
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 serialize ? serialize(obj) : obj;
66522
+ return serialize2 ? serialize2(obj) : obj;
66523
66523
  }
66524
66524
  if (typeof obj !== "object") {
66525
- return serialize ? serialize(obj) : obj;
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 (serialize === false) {
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 serialize === "function") {
66542
- return serialize(cloned);
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, serialize) {
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" ? serialize(censor(...args)) : serialize(censor);
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 serialize2;
71367
+ let serialize3;
71368
71368
  if (typeof schemaOrStatus === "string" || typeof schemaOrStatus === "number") {
71369
71369
  if (typeof contentType2 === "string") {
71370
- serialize2 = this[kRouteContext][kSchemaResponse]?.[schemaOrStatus]?.[contentType2];
71370
+ serialize3 = this[kRouteContext][kSchemaResponse]?.[schemaOrStatus]?.[contentType2];
71371
71371
  } else {
71372
- serialize2 = this[kRouteContext][kSchemaResponse]?.[schemaOrStatus];
71372
+ serialize3 = this[kRouteContext][kSchemaResponse]?.[schemaOrStatus];
71373
71373
  }
71374
71374
  } else if (typeof schemaOrStatus === "object") {
71375
- serialize2 = this[kRouteContext][kReplyCacheSerializeFns]?.get(schemaOrStatus);
71375
+ serialize3 = this[kRouteContext][kReplyCacheSerializeFns]?.get(schemaOrStatus);
71376
71376
  }
71377
- return serialize2;
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 serialize2;
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
- serialize2 = this[kRouteContext][kSchemaResponse]?.[httpStatus]?.[contentType2];
71408
+ serialize3 = this[kRouteContext][kSchemaResponse]?.[httpStatus]?.[contentType2];
71409
71409
  } else {
71410
- serialize2 = this[kRouteContext][kSchemaResponse]?.[httpStatus];
71410
+ serialize3 = this[kRouteContext][kSchemaResponse]?.[httpStatus];
71411
71411
  }
71412
- if (serialize2 == null) {
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
- serialize2 = this[kRouteContext][kReplyCacheSerializeFns].get(schema);
71418
+ serialize3 = this[kRouteContext][kReplyCacheSerializeFns].get(schema);
71419
71419
  } else {
71420
- serialize2 = this.compileSerializationSchema(schema, httpStatus, contentType2);
71420
+ serialize3 = this.compileSerializationSchema(schema, httpStatus, contentType2);
71421
71421
  }
71422
71422
  }
71423
- return serialize2(input);
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 serialize(this[kRouteContext], payload, this.raw.statusCode);
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 = serialize(reply[kRouteContext], payload, reply.raw.statusCode, reply[kReplyHeaders]["content-type"]);
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 serialize(context2, data, statusCode, contentType2) {
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
- serialize(parse4(uri, options), options);
77944
+ serialize2(parse4(uri, options), options);
77945
77945
  } else if (typeof uri === "object") {
77946
77946
  uri = /** @type {T} */
77947
- parse4(serialize(uri, options), options);
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 serialize(resolved, schemelessOptions);
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(serialize(base, options), options);
77961
- relative = parse4(serialize(relative, options), options);
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 = serialize(normalizeComponentEncoding(parse4(uriA, options), true), { ...options, skipEscape: true });
78013
+ uriA = serialize2(normalizeComponentEncoding(parse4(uriA, options), true), { ...options, skipEscape: true });
78014
78014
  } else if (typeof uriA === "object") {
78015
- uriA = serialize(normalizeComponentEncoding(uriA, true), { ...options, skipEscape: true });
78015
+ uriA = serialize2(normalizeComponentEncoding(uriA, true), { ...options, skipEscape: true });
78016
78016
  }
78017
78017
  if (typeof uriB === "string") {
78018
78018
  uriB = unescape(uriB);
78019
- uriB = serialize(normalizeComponentEncoding(parse4(uriB, options), true), { ...options, skipEscape: true });
78019
+ uriB = serialize2(normalizeComponentEncoding(parse4(uriB, options), true), { ...options, skipEscape: true });
78020
78020
  } else if (typeof uriB === "object") {
78021
- uriB = serialize(normalizeComponentEncoding(uriB, true), { ...options, skipEscape: true });
78021
+ uriB = serialize2(normalizeComponentEncoding(uriB, true), { ...options, skipEscape: true });
78022
78022
  }
78023
78023
  return uriA.toLowerCase() === uriB.toLowerCase();
78024
78024
  }
78025
- function serialize(cmpts, opts) {
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 serialize = makeSerialize(this.scope.get());
81738
- this.scope.value(serializeName, { ref: serialize });
81739
- sch.serialize = 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: randomUUID27 } = __require("node:crypto");
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-${randomUUID27()}`;
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: createHash10 } = __require("crypto");
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 = createHash10("sha1").update(key2 + GUID).digest("base64");
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: createHash10 } = __require("crypto");
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 = createHash10("sha1").update(key2 + GUID).digest("base64");
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 = 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 serialize(name25, val, options) {
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.deleteFrom("agent_sessions").where("id", "=", id).execute();
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
- let opening;
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 = entry;
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 resolveTarget(deps, payload, targetRemoteId, requiredTier) {
208413
- if (payload.sourceRemoteServerId && payload.sourceRemoteServerId === targetRemoteId) {
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 deps.storage.remoteServers.getAll(payload.userId);
208426
- return servers.filter((s3) => s3.cross_remote_access !== "off").filter((s3) => s3.id !== payload.sourceRemoteServerId).map((s3) => ({
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
- if (typeof content === "string") return content;
229951
- return content.filter((p2) => p2.type === "text").map((p2) => p2.text).join(" ");
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, opts?.origin, disposition);
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
- if (session.status !== "running") {
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
- const userEntryIndex = await this.pushEntry(sessionId, {
232517
- type: "user",
232518
- content,
232519
- timestamp: Date.now(),
232520
- ...opts?.origin ? { origin: opts.origin } : {},
232521
- notificationDisposition: disposition
232522
- // Lifecycle activation: an entry-write failure must abort before stdin
232523
- // (strict), or the session would be marked active with no durable turn.
232524
- }, true, userId, { strictPersist: opts?.onUserEntryPersisted !== void 0 });
232525
- if (opts?.onUserEntryPersisted) await opts.onUserEntryPersisted(userEntryIndex);
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", origin, notificationDisposition = "result") {
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
- origin,
233286
- notificationDisposition
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, origin, notificationDisposition) {
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
- await this.pushEntry(session.id, {
233312
- type: "user",
233313
- content: userMessage,
233314
- timestamp: Date.now(),
233315
- ...origin ? { origin } : {},
233316
- notificationDisposition
233317
- }, true, userId);
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
- setTimeout(() => {
233321
- const context2 = this.buildFullConversationContext(session.store.entries);
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.process?.stdin?.write(formatted);
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
- }, 500);
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 randomUUID6 } from "crypto";
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 createHash3 } from "crypto";
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 = createHash3("sha256").update(diff).update("\0").update(status).digest("hex");
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.status === "sending_feedback") {
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 ?? randomUUID6();
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
- let reviewerSession = null;
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
- if (reviewerSession.permission_mode !== "plan") {
241587
- let switched = false;
241588
- try {
241589
- switched = await this.agentOps.switchMode(opts.reviewerSessionId, opts.project.path, "plan");
241590
- } catch {
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
- const sent = await this.agentOps.sendUserMessage(opts.reviewerSessionId, prompt, opts.project.path, void 0, REVIEWER_TURN).catch(() => false);
241606
- if (!sent) {
241607
- await this.failRun(run2, "\u5411\u4E0A\u6B21 reviewer \u6295\u9012\u590D\u5BA1\u4EFB\u52A1\u5931\u8D25");
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
- this.emitRunUpdated(run2);
241611
- return run2;
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 p2 = this.participants.get(event.sessionId);
241770
- if (!p2 || p2.role !== "reviewer") return;
241771
- const run2 = await this.storage.workflowRuns.getById(p2.runId);
241772
- if (!run2 || run2.status !== "waiting_reviewer") return;
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 feedback = extractLastAssistantInTurn(entries, boundary) ?? "(reviewer \u6CA1\u6709\u8F93\u51FA\u53EF\u7528\u7684\u53CD\u9988\u6587\u672C)";
241776
- let driftNote = null;
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
- driftNote = "\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";
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 ok = await this.agentOps.sendUserMessage(run2.source_session_id, buildFeedbackMessage(feedback), project?.path ?? void 0, void 0, FEEDBACK_TURN).catch(() => false);
241826
- if (!ok) {
241827
- await this.storage.workflowRuns.transition(runId, "sending_feedback", "waiting_feedback", {
241828
- error: "\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"
241829
- });
241830
- throw new WorkflowError("send-failed", "\u53D1\u9001\u53CD\u9988\u5931\u8D25");
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" || !run2.reviewer_session_id) {
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 sent = await this.agentOps.sendUserMessage(run2.reviewer_session_id, FINAL_VERDICT_PROMPT, project?.path ?? void 0, void 0, REVIEWER_TURN).catch(() => false);
241863
- if (!sent) {
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
- await this.storage.workflowRuns.transition(p2.runId, "waiting_feedback", "discussing", { error: null }) || await this.storage.workflowRuns.transition(p2.runId, "waiting_reviewer", "discussing", { error: null });
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 randomUUID7 } from "crypto";
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 = randomUUID7();
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 = randomUUID7();
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 randomUUID8 } from "crypto";
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 = randomUUID8();
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 createHash4, randomUUID as randomUUID9 } from "crypto";
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 createHash4("sha256").update(JSON.stringify(value)).digest("hex");
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 = randomUUID9();
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 ?? randomUUID9();
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 createHash5, randomUUID as randomUUID10 } from "node:crypto";
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 createHash5("sha256").update(canonical).digest("hex");
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 ?? randomUUID10();
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) return this.replayPrepare(raced, input, now3);
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 = randomUUID10();
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, input.instruction, projectPath, input.userId ?? "local", {
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 randomUUID11 } from "node:crypto";
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: params.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 ?? randomUUID11();
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
- return Boolean(project?.path) && agentSessionManager.sendUserMessage(
246384
- input.sessionId,
246385
- input.instruction,
246386
- project.path,
246387
- input.userId
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 randomUUID12 } from "crypto";
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 = randomUUID12();
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 randomUUID13 } from "crypto";
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 = randomUUID13();
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 randomUUID14 } from "crypto";
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: randomUUID14(),
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 result = await proxyToRemoteAuto(
247753
- executorMode,
247754
- "POST",
247755
- `/api/path/execute`,
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
- { reverseConnectManager: fastify2.reverseConnectManager }
247766
- );
247767
- if (result.ok) {
247768
- const remoteData = result.data;
247769
- const localProcessId = `remote-${executor.id}-${remoteData.processId}`;
247770
- const remoteInfo = {
247771
- remoteServerId: executorMode,
247772
- remoteProcessId: remoteData.processId,
247773
- executorId: executor.id,
247774
- projectId: project.id
247775
- };
247776
- fastify2.remoteExecutorMap.set(localProcessId, remoteInfo);
247777
- fastify2.remoteExecutorMonitor.watch(localProcessId, remoteInfo);
247778
- await fastify2.storage.remoteExecutorProcesses.insert(localProcessId, {
247779
- remoteServerId: executorMode,
247780
- remoteProcessId: remoteData.processId,
247781
- executorId: executor.id,
247782
- projectId: project.id,
247783
- branch: branch ?? void 0,
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.eventBus.emit({
247827
- type: "executor:stopped",
247828
- projectId: remoteInfo.projectId ?? "",
247829
- executorId: remoteInfo.executorId,
247830
- processId: req.params.processId,
247831
- exitCode: 0,
247832
- target: remoteInfo.remoteServerId
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 remoteConfig = await getRemoteConfig3(fastify2, project);
250062
- if (!remoteConfig) {
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 params = [
250066
- `path=${encodeURIComponent(remoteConfig.remotePath)}`,
250067
- `filePath=${encodeURIComponent(filePath)}`
250068
- ];
250069
- if (branch) params.push(`branch=${encodeURIComponent(branch)}`);
250070
- const result = await proxyToRemoteAuto(
250071
- remoteConfig.serverId,
250072
- "GET",
250073
- `/api/path/file-content?${params.join("&")}`,
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
- return reply.code(proxyStatus(result)).send(result.data);
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 remoteConfig = await getRemoteConfig3(fastify2, project);
250169
- if (!remoteConfig) {
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 params = [
250173
- `path=${encodeURIComponent(remoteConfig.remotePath)}`,
250174
- `filePath=${encodeURIComponent(filePath)}`
250175
- ];
250176
- if (branch) params.push(`branch=${encodeURIComponent(branch)}`);
250177
- const result = await proxyToRemoteAuto(
250178
- remoteConfig.serverId,
250179
- "GET",
250180
- `/api/path/file-download?${params.join("&")}`,
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 randomUUID15 } from "node:crypto";
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, `${randomUUID15()}.txt`);
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 randomUUID16 } from "node:crypto";
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, randomUUID16());
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 createHash6 } from "crypto";
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 createHash6("sha256").update(userId).update("\0").update(conversation).digest("hex");
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 { createHash as createHash7, randomUUID as randomUUID17 } from "crypto";
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 = randomUUID17();
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 content === "string" && content.trim().length > 0;
251988
- const isValidArray = Array.isArray(content) && content.length > 0;
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(content);
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 deliveryKey = `${req.params.sessionId}\0${idempotencyKey}`;
252130
- return serializeInstructionDelivery(deliveryKey, async () => {
252131
- const claim = await fastify2.storage.agentInstructionDeliveries.claim({
252132
- sessionId: req.params.sessionId,
252133
- idempotencyKey,
252134
- contentHash: instructionContentHash(content),
252135
- claimToken: instructionReceiverToken
252136
- });
252137
- if (claim === "conflict") {
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
- let ownershipLost = false;
252145
- const renew = async () => {
252146
- try {
252147
- if (!await fastify2.storage.agentInstructionDeliveries.renewClaim({
252148
- sessionId: req.params.sessionId,
252149
- idempotencyKey,
252150
- claimToken: instructionReceiverToken
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 = randomUUID17();
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
- async function authorizeLocal(sessionId, authResult) {
253113
- const activity = await fastify2.storage.agentSessions.getActivityById(sessionId, "runtime");
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
- return send(reply, await remote().activate({ ...common, localSessionId: sessionId, userId: authResult }), {
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 createHash8, randomUUID as randomUUID18 } from "crypto";
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 ?? randomUUID18();
253795
- const createPayloadHash = createHash8("sha256").update(JSON.stringify({ message: body.message ?? null })).digest("hex");
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: randomUUID18(),
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: randomUUID18(),
253807
- workItemId: randomUUID18(),
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 randomUUID19 } from "crypto";
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 = randomUUID19();
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 randomUUID20 } from "crypto";
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 = randomUUID20();
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 randomUUID21 } from "crypto";
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 = randomUUID21();
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
- return reply.code(400).send({ error: "action must be approve, cancel or finalize" });
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 randomUUID22 } from "crypto";
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 = randomUUID22();
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
- const row = await fastify2.storage.remoteExecutorProcesses.getById(processId);
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
- send({ type: "finished", exitCode: 0 });
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 createHash9, verify as cryptoVerify } from "crypto";
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 = createHash9("sha256").update(publicKey).digest("hex");
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 randomUUID23 } from "crypto";
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 = randomUUID23();
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 randomUUID24 } from "node:crypto";
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 = randomUUID24();
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 work 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. If no remote matches, say so instead of silently falling back to local.",
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 this agent may access, 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.",
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
- return textResult(resolved.reason === "offline" ? `Remote ${remoteId} is offline` : NOT_ACCESSIBLE, true);
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 randomUUID25 } from "crypto";
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 = randomUUID25();
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 randomUUID26 } from "node:crypto";
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: randomUUID26()
267264
+ nonce: randomUUID28()
265897
267265
  };
265898
267266
  const candidatePath = `${lockPath}.candidate-${process.pid}-${owner.nonce}`;
265899
267267
  fs8.mkdirSync(candidatePath, { mode: 448 });